Publish Claude Code source snapshot from a single baseline commit
Squash the imported source snapshot and follow-up documentation history into one root commit so the archive starts from a single coherent state. Constraint: Repository intentionally tracks an analyzed Claude Code source snapshot Constraint: Author and committer must be instructkr <no-contact@instruct.kr> Rejected: Preserve the four-step import/docs history | user explicitly requested one squashed commit Confidence: high Scope-risk: broad Reversibility: clean Directive: Keep future analysis and refactor commits separate from this archive baseline Tested: git status clean; local history rewritten to one commit; force-pushed main to origin and instructkr Not-tested: Fresh clone verification after push
This commit is contained in:
754
src/commands.ts
Normal file
754
src/commands.ts
Normal file
@@ -0,0 +1,754 @@
|
||||
// biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered
|
||||
import addDir from './commands/add-dir/index.js'
|
||||
import autofixPr from './commands/autofix-pr/index.js'
|
||||
import backfillSessions from './commands/backfill-sessions/index.js'
|
||||
import btw from './commands/btw/index.js'
|
||||
import goodClaude from './commands/good-claude/index.js'
|
||||
import issue from './commands/issue/index.js'
|
||||
import feedback from './commands/feedback/index.js'
|
||||
import clear from './commands/clear/index.js'
|
||||
import color from './commands/color/index.js'
|
||||
import commit from './commands/commit.js'
|
||||
import copy from './commands/copy/index.js'
|
||||
import desktop from './commands/desktop/index.js'
|
||||
import commitPushPr from './commands/commit-push-pr.js'
|
||||
import compact from './commands/compact/index.js'
|
||||
import config from './commands/config/index.js'
|
||||
import { context, contextNonInteractive } from './commands/context/index.js'
|
||||
import cost from './commands/cost/index.js'
|
||||
import diff from './commands/diff/index.js'
|
||||
import ctx_viz from './commands/ctx_viz/index.js'
|
||||
import doctor from './commands/doctor/index.js'
|
||||
import memory from './commands/memory/index.js'
|
||||
import help from './commands/help/index.js'
|
||||
import ide from './commands/ide/index.js'
|
||||
import init from './commands/init.js'
|
||||
import initVerifiers from './commands/init-verifiers.js'
|
||||
import keybindings from './commands/keybindings/index.js'
|
||||
import login from './commands/login/index.js'
|
||||
import logout from './commands/logout/index.js'
|
||||
import installGitHubApp from './commands/install-github-app/index.js'
|
||||
import installSlackApp from './commands/install-slack-app/index.js'
|
||||
import breakCache from './commands/break-cache/index.js'
|
||||
import mcp from './commands/mcp/index.js'
|
||||
import mobile from './commands/mobile/index.js'
|
||||
import onboarding from './commands/onboarding/index.js'
|
||||
import pr_comments from './commands/pr_comments/index.js'
|
||||
import releaseNotes from './commands/release-notes/index.js'
|
||||
import rename from './commands/rename/index.js'
|
||||
import resume from './commands/resume/index.js'
|
||||
import review, { ultrareview } from './commands/review.js'
|
||||
import session from './commands/session/index.js'
|
||||
import share from './commands/share/index.js'
|
||||
import skills from './commands/skills/index.js'
|
||||
import status from './commands/status/index.js'
|
||||
import tasks from './commands/tasks/index.js'
|
||||
import teleport from './commands/teleport/index.js'
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
const agentsPlatform =
|
||||
process.env.USER_TYPE === 'ant'
|
||||
? require('./commands/agents-platform/index.js').default
|
||||
: null
|
||||
/* eslint-enable @typescript-eslint/no-require-imports */
|
||||
import securityReview from './commands/security-review.js'
|
||||
import bughunter from './commands/bughunter/index.js'
|
||||
import terminalSetup from './commands/terminalSetup/index.js'
|
||||
import usage from './commands/usage/index.js'
|
||||
import theme from './commands/theme/index.js'
|
||||
import vim from './commands/vim/index.js'
|
||||
import { feature } from 'bun:bundle'
|
||||
// Dead code elimination: conditional imports
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
const proactive =
|
||||
feature('PROACTIVE') || feature('KAIROS')
|
||||
? require('./commands/proactive.js').default
|
||||
: null
|
||||
const briefCommand =
|
||||
feature('KAIROS') || feature('KAIROS_BRIEF')
|
||||
? require('./commands/brief.js').default
|
||||
: null
|
||||
const assistantCommand = feature('KAIROS')
|
||||
? require('./commands/assistant/index.js').default
|
||||
: null
|
||||
const bridge = feature('BRIDGE_MODE')
|
||||
? require('./commands/bridge/index.js').default
|
||||
: null
|
||||
const remoteControlServerCommand =
|
||||
feature('DAEMON') && feature('BRIDGE_MODE')
|
||||
? require('./commands/remoteControlServer/index.js').default
|
||||
: null
|
||||
const voiceCommand = feature('VOICE_MODE')
|
||||
? require('./commands/voice/index.js').default
|
||||
: null
|
||||
const forceSnip = feature('HISTORY_SNIP')
|
||||
? require('./commands/force-snip.js').default
|
||||
: null
|
||||
const workflowsCmd = feature('WORKFLOW_SCRIPTS')
|
||||
? (
|
||||
require('./commands/workflows/index.js') as typeof import('./commands/workflows/index.js')
|
||||
).default
|
||||
: null
|
||||
const webCmd = feature('CCR_REMOTE_SETUP')
|
||||
? (
|
||||
require('./commands/remote-setup/index.js') as typeof import('./commands/remote-setup/index.js')
|
||||
).default
|
||||
: null
|
||||
const clearSkillIndexCache = feature('EXPERIMENTAL_SKILL_SEARCH')
|
||||
? (
|
||||
require('./services/skillSearch/localSearch.js') as typeof import('./services/skillSearch/localSearch.js')
|
||||
).clearSkillIndexCache
|
||||
: null
|
||||
const subscribePr = feature('KAIROS_GITHUB_WEBHOOKS')
|
||||
? require('./commands/subscribe-pr.js').default
|
||||
: null
|
||||
const ultraplan = feature('ULTRAPLAN')
|
||||
? require('./commands/ultraplan.js').default
|
||||
: null
|
||||
const torch = feature('TORCH') ? require('./commands/torch.js').default : null
|
||||
const peersCmd = feature('UDS_INBOX')
|
||||
? (
|
||||
require('./commands/peers/index.js') as typeof import('./commands/peers/index.js')
|
||||
).default
|
||||
: null
|
||||
const forkCmd = feature('FORK_SUBAGENT')
|
||||
? (
|
||||
require('./commands/fork/index.js') as typeof import('./commands/fork/index.js')
|
||||
).default
|
||||
: null
|
||||
const buddy = feature('BUDDY')
|
||||
? (
|
||||
require('./commands/buddy/index.js') as typeof import('./commands/buddy/index.js')
|
||||
).default
|
||||
: null
|
||||
/* eslint-enable @typescript-eslint/no-require-imports */
|
||||
import thinkback from './commands/thinkback/index.js'
|
||||
import thinkbackPlay from './commands/thinkback-play/index.js'
|
||||
import permissions from './commands/permissions/index.js'
|
||||
import plan from './commands/plan/index.js'
|
||||
import fast from './commands/fast/index.js'
|
||||
import passes from './commands/passes/index.js'
|
||||
import privacySettings from './commands/privacy-settings/index.js'
|
||||
import hooks from './commands/hooks/index.js'
|
||||
import files from './commands/files/index.js'
|
||||
import branch from './commands/branch/index.js'
|
||||
import agents from './commands/agents/index.js'
|
||||
import plugin from './commands/plugin/index.js'
|
||||
import reloadPlugins from './commands/reload-plugins/index.js'
|
||||
import rewind from './commands/rewind/index.js'
|
||||
import heapDump from './commands/heapdump/index.js'
|
||||
import mockLimits from './commands/mock-limits/index.js'
|
||||
import bridgeKick from './commands/bridge-kick.js'
|
||||
import version from './commands/version.js'
|
||||
import summary from './commands/summary/index.js'
|
||||
import {
|
||||
resetLimits,
|
||||
resetLimitsNonInteractive,
|
||||
} from './commands/reset-limits/index.js'
|
||||
import antTrace from './commands/ant-trace/index.js'
|
||||
import perfIssue from './commands/perf-issue/index.js'
|
||||
import sandboxToggle from './commands/sandbox-toggle/index.js'
|
||||
import chrome from './commands/chrome/index.js'
|
||||
import stickers from './commands/stickers/index.js'
|
||||
import advisor from './commands/advisor.js'
|
||||
import { logError } from './utils/log.js'
|
||||
import { toError } from './utils/errors.js'
|
||||
import { logForDebugging } from './utils/debug.js'
|
||||
import {
|
||||
getSkillDirCommands,
|
||||
clearSkillCaches,
|
||||
getDynamicSkills,
|
||||
} from './skills/loadSkillsDir.js'
|
||||
import { getBundledSkills } from './skills/bundledSkills.js'
|
||||
import { getBuiltinPluginSkillCommands } from './plugins/builtinPlugins.js'
|
||||
import {
|
||||
getPluginCommands,
|
||||
clearPluginCommandCache,
|
||||
getPluginSkills,
|
||||
clearPluginSkillsCache,
|
||||
} from './utils/plugins/loadPluginCommands.js'
|
||||
import memoize from 'lodash-es/memoize.js'
|
||||
import { isUsing3PServices, isClaudeAISubscriber } from './utils/auth.js'
|
||||
import { isFirstPartyAnthropicBaseUrl } from './utils/model/providers.js'
|
||||
import env from './commands/env/index.js'
|
||||
import exit from './commands/exit/index.js'
|
||||
import exportCommand from './commands/export/index.js'
|
||||
import model from './commands/model/index.js'
|
||||
import tag from './commands/tag/index.js'
|
||||
import outputStyle from './commands/output-style/index.js'
|
||||
import remoteEnv from './commands/remote-env/index.js'
|
||||
import upgrade from './commands/upgrade/index.js'
|
||||
import {
|
||||
extraUsage,
|
||||
extraUsageNonInteractive,
|
||||
} from './commands/extra-usage/index.js'
|
||||
import rateLimitOptions from './commands/rate-limit-options/index.js'
|
||||
import statusline from './commands/statusline.js'
|
||||
import effort from './commands/effort/index.js'
|
||||
import stats from './commands/stats/index.js'
|
||||
// insights.ts is 113KB (3200 lines, includes diffLines/html rendering). Lazy
|
||||
// shim defers the heavy module until /insights is actually invoked.
|
||||
const usageReport: Command = {
|
||||
type: 'prompt',
|
||||
name: 'insights',
|
||||
description: 'Generate a report analyzing your Claude Code sessions',
|
||||
contentLength: 0,
|
||||
progressMessage: 'analyzing your sessions',
|
||||
source: 'builtin',
|
||||
async getPromptForCommand(args, context) {
|
||||
const real = (await import('./commands/insights.js')).default
|
||||
if (real.type !== 'prompt') throw new Error('unreachable')
|
||||
return real.getPromptForCommand(args, context)
|
||||
},
|
||||
}
|
||||
import oauthRefresh from './commands/oauth-refresh/index.js'
|
||||
import debugToolCall from './commands/debug-tool-call/index.js'
|
||||
import { getSettingSourceName } from './utils/settings/constants.js'
|
||||
import {
|
||||
type Command,
|
||||
getCommandName,
|
||||
isCommandEnabled,
|
||||
} from './types/command.js'
|
||||
|
||||
// Re-export types from the centralized location
|
||||
export type {
|
||||
Command,
|
||||
CommandBase,
|
||||
CommandResultDisplay,
|
||||
LocalCommandResult,
|
||||
LocalJSXCommandContext,
|
||||
PromptCommand,
|
||||
ResumeEntrypoint,
|
||||
} from './types/command.js'
|
||||
export { getCommandName, isCommandEnabled } from './types/command.js'
|
||||
|
||||
// Commands that get eliminated from the external build
|
||||
export const INTERNAL_ONLY_COMMANDS = [
|
||||
backfillSessions,
|
||||
breakCache,
|
||||
bughunter,
|
||||
commit,
|
||||
commitPushPr,
|
||||
ctx_viz,
|
||||
goodClaude,
|
||||
issue,
|
||||
initVerifiers,
|
||||
...(forceSnip ? [forceSnip] : []),
|
||||
mockLimits,
|
||||
bridgeKick,
|
||||
version,
|
||||
...(ultraplan ? [ultraplan] : []),
|
||||
...(subscribePr ? [subscribePr] : []),
|
||||
resetLimits,
|
||||
resetLimitsNonInteractive,
|
||||
onboarding,
|
||||
share,
|
||||
summary,
|
||||
teleport,
|
||||
antTrace,
|
||||
perfIssue,
|
||||
env,
|
||||
oauthRefresh,
|
||||
debugToolCall,
|
||||
agentsPlatform,
|
||||
autofixPr,
|
||||
].filter(Boolean)
|
||||
|
||||
// Declared as a function so that we don't run this until getCommands is called,
|
||||
// since underlying functions read from config, which can't be read at module initialization time
|
||||
const COMMANDS = memoize((): Command[] => [
|
||||
addDir,
|
||||
advisor,
|
||||
agents,
|
||||
branch,
|
||||
btw,
|
||||
chrome,
|
||||
clear,
|
||||
color,
|
||||
compact,
|
||||
config,
|
||||
copy,
|
||||
desktop,
|
||||
context,
|
||||
contextNonInteractive,
|
||||
cost,
|
||||
diff,
|
||||
doctor,
|
||||
effort,
|
||||
exit,
|
||||
fast,
|
||||
files,
|
||||
heapDump,
|
||||
help,
|
||||
ide,
|
||||
init,
|
||||
keybindings,
|
||||
installGitHubApp,
|
||||
installSlackApp,
|
||||
mcp,
|
||||
memory,
|
||||
mobile,
|
||||
model,
|
||||
outputStyle,
|
||||
remoteEnv,
|
||||
plugin,
|
||||
pr_comments,
|
||||
releaseNotes,
|
||||
reloadPlugins,
|
||||
rename,
|
||||
resume,
|
||||
session,
|
||||
skills,
|
||||
stats,
|
||||
status,
|
||||
statusline,
|
||||
stickers,
|
||||
tag,
|
||||
theme,
|
||||
feedback,
|
||||
review,
|
||||
ultrareview,
|
||||
rewind,
|
||||
securityReview,
|
||||
terminalSetup,
|
||||
upgrade,
|
||||
extraUsage,
|
||||
extraUsageNonInteractive,
|
||||
rateLimitOptions,
|
||||
usage,
|
||||
usageReport,
|
||||
vim,
|
||||
...(webCmd ? [webCmd] : []),
|
||||
...(forkCmd ? [forkCmd] : []),
|
||||
...(buddy ? [buddy] : []),
|
||||
...(proactive ? [proactive] : []),
|
||||
...(briefCommand ? [briefCommand] : []),
|
||||
...(assistantCommand ? [assistantCommand] : []),
|
||||
...(bridge ? [bridge] : []),
|
||||
...(remoteControlServerCommand ? [remoteControlServerCommand] : []),
|
||||
...(voiceCommand ? [voiceCommand] : []),
|
||||
thinkback,
|
||||
thinkbackPlay,
|
||||
permissions,
|
||||
plan,
|
||||
privacySettings,
|
||||
hooks,
|
||||
exportCommand,
|
||||
sandboxToggle,
|
||||
...(!isUsing3PServices() ? [logout, login()] : []),
|
||||
passes,
|
||||
...(peersCmd ? [peersCmd] : []),
|
||||
tasks,
|
||||
...(workflowsCmd ? [workflowsCmd] : []),
|
||||
...(torch ? [torch] : []),
|
||||
...(process.env.USER_TYPE === 'ant' && !process.env.IS_DEMO
|
||||
? INTERNAL_ONLY_COMMANDS
|
||||
: []),
|
||||
])
|
||||
|
||||
export const builtInCommandNames = memoize(
|
||||
(): Set<string> =>
|
||||
new Set(COMMANDS().flatMap(_ => [_.name, ...(_.aliases ?? [])])),
|
||||
)
|
||||
|
||||
async function getSkills(cwd: string): Promise<{
|
||||
skillDirCommands: Command[]
|
||||
pluginSkills: Command[]
|
||||
bundledSkills: Command[]
|
||||
builtinPluginSkills: Command[]
|
||||
}> {
|
||||
try {
|
||||
const [skillDirCommands, pluginSkills] = await Promise.all([
|
||||
getSkillDirCommands(cwd).catch(err => {
|
||||
logError(toError(err))
|
||||
logForDebugging(
|
||||
'Skill directory commands failed to load, continuing without them',
|
||||
)
|
||||
return []
|
||||
}),
|
||||
getPluginSkills().catch(err => {
|
||||
logError(toError(err))
|
||||
logForDebugging('Plugin skills failed to load, continuing without them')
|
||||
return []
|
||||
}),
|
||||
])
|
||||
// Bundled skills are registered synchronously at startup
|
||||
const bundledSkills = getBundledSkills()
|
||||
// Built-in plugin skills come from enabled built-in plugins
|
||||
const builtinPluginSkills = getBuiltinPluginSkillCommands()
|
||||
logForDebugging(
|
||||
`getSkills returning: ${skillDirCommands.length} skill dir commands, ${pluginSkills.length} plugin skills, ${bundledSkills.length} bundled skills, ${builtinPluginSkills.length} builtin plugin skills`,
|
||||
)
|
||||
return {
|
||||
skillDirCommands,
|
||||
pluginSkills,
|
||||
bundledSkills,
|
||||
builtinPluginSkills,
|
||||
}
|
||||
} catch (err) {
|
||||
// This should never happen since we catch at the Promise level, but defensive
|
||||
logError(toError(err))
|
||||
logForDebugging('Unexpected error in getSkills, returning empty')
|
||||
return {
|
||||
skillDirCommands: [],
|
||||
pluginSkills: [],
|
||||
bundledSkills: [],
|
||||
builtinPluginSkills: [],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
const getWorkflowCommands = feature('WORKFLOW_SCRIPTS')
|
||||
? (
|
||||
require('./tools/WorkflowTool/createWorkflowCommand.js') as typeof import('./tools/WorkflowTool/createWorkflowCommand.js')
|
||||
).getWorkflowCommands
|
||||
: null
|
||||
/* eslint-enable @typescript-eslint/no-require-imports */
|
||||
|
||||
/**
|
||||
* Filters commands by their declared `availability` (auth/provider requirement).
|
||||
* Commands without `availability` are treated as universal.
|
||||
* This runs before `isEnabled()` so that provider-gated commands are hidden
|
||||
* regardless of feature-flag state.
|
||||
*
|
||||
* Not memoized — auth state can change mid-session (e.g. after /login),
|
||||
* so this must be re-evaluated on every getCommands() call.
|
||||
*/
|
||||
export function meetsAvailabilityRequirement(cmd: Command): boolean {
|
||||
if (!cmd.availability) return true
|
||||
for (const a of cmd.availability) {
|
||||
switch (a) {
|
||||
case 'claude-ai':
|
||||
if (isClaudeAISubscriber()) return true
|
||||
break
|
||||
case 'console':
|
||||
// Console API key user = direct 1P API customer (not 3P, not claude.ai).
|
||||
// Excludes 3P (Bedrock/Vertex/Foundry) who don't set ANTHROPIC_BASE_URL
|
||||
// and gateway users who proxy through a custom base URL.
|
||||
if (
|
||||
!isClaudeAISubscriber() &&
|
||||
!isUsing3PServices() &&
|
||||
isFirstPartyAnthropicBaseUrl()
|
||||
)
|
||||
return true
|
||||
break
|
||||
default: {
|
||||
const _exhaustive: never = a
|
||||
void _exhaustive
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads all command sources (skills, plugins, workflows). Memoized by cwd
|
||||
* because loading is expensive (disk I/O, dynamic imports).
|
||||
*/
|
||||
const loadAllCommands = memoize(async (cwd: string): Promise<Command[]> => {
|
||||
const [
|
||||
{ skillDirCommands, pluginSkills, bundledSkills, builtinPluginSkills },
|
||||
pluginCommands,
|
||||
workflowCommands,
|
||||
] = await Promise.all([
|
||||
getSkills(cwd),
|
||||
getPluginCommands(),
|
||||
getWorkflowCommands ? getWorkflowCommands(cwd) : Promise.resolve([]),
|
||||
])
|
||||
|
||||
return [
|
||||
...bundledSkills,
|
||||
...builtinPluginSkills,
|
||||
...skillDirCommands,
|
||||
...workflowCommands,
|
||||
...pluginCommands,
|
||||
...pluginSkills,
|
||||
...COMMANDS(),
|
||||
]
|
||||
})
|
||||
|
||||
/**
|
||||
* Returns commands available to the current user. The expensive loading is
|
||||
* memoized, but availability and isEnabled checks run fresh every call so
|
||||
* auth changes (e.g. /login) take effect immediately.
|
||||
*/
|
||||
export async function getCommands(cwd: string): Promise<Command[]> {
|
||||
const allCommands = await loadAllCommands(cwd)
|
||||
|
||||
// Get dynamic skills discovered during file operations
|
||||
const dynamicSkills = getDynamicSkills()
|
||||
|
||||
// Build base commands without dynamic skills
|
||||
const baseCommands = allCommands.filter(
|
||||
_ => meetsAvailabilityRequirement(_) && isCommandEnabled(_),
|
||||
)
|
||||
|
||||
if (dynamicSkills.length === 0) {
|
||||
return baseCommands
|
||||
}
|
||||
|
||||
// Dedupe dynamic skills - only add if not already present
|
||||
const baseCommandNames = new Set(baseCommands.map(c => c.name))
|
||||
const uniqueDynamicSkills = dynamicSkills.filter(
|
||||
s =>
|
||||
!baseCommandNames.has(s.name) &&
|
||||
meetsAvailabilityRequirement(s) &&
|
||||
isCommandEnabled(s),
|
||||
)
|
||||
|
||||
if (uniqueDynamicSkills.length === 0) {
|
||||
return baseCommands
|
||||
}
|
||||
|
||||
// Insert dynamic skills after plugin skills but before built-in commands
|
||||
const builtInNames = new Set(COMMANDS().map(c => c.name))
|
||||
const insertIndex = baseCommands.findIndex(c => builtInNames.has(c.name))
|
||||
|
||||
if (insertIndex === -1) {
|
||||
return [...baseCommands, ...uniqueDynamicSkills]
|
||||
}
|
||||
|
||||
return [
|
||||
...baseCommands.slice(0, insertIndex),
|
||||
...uniqueDynamicSkills,
|
||||
...baseCommands.slice(insertIndex),
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears only the memoization caches for commands, WITHOUT clearing skill caches.
|
||||
* Use this when dynamic skills are added to invalidate cached command lists.
|
||||
*/
|
||||
export function clearCommandMemoizationCaches(): void {
|
||||
loadAllCommands.cache?.clear?.()
|
||||
getSkillToolCommands.cache?.clear?.()
|
||||
getSlashCommandToolSkills.cache?.clear?.()
|
||||
// getSkillIndex in skillSearch/localSearch.ts is a separate memoization layer
|
||||
// built ON TOP of getSkillToolCommands/getCommands. Clearing only the inner
|
||||
// caches is a no-op for the outer — lodash memoize returns the cached result
|
||||
// without ever reaching the cleared inners. Must clear it explicitly.
|
||||
clearSkillIndexCache?.()
|
||||
}
|
||||
|
||||
export function clearCommandsCache(): void {
|
||||
clearCommandMemoizationCaches()
|
||||
clearPluginCommandCache()
|
||||
clearPluginSkillsCache()
|
||||
clearSkillCaches()
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter AppState.mcp.commands to MCP-provided skills (prompt-type,
|
||||
* model-invocable, loaded from MCP). These live outside getCommands() so
|
||||
* callers that need MCP skills in their skill index thread them through
|
||||
* separately.
|
||||
*/
|
||||
export function getMcpSkillCommands(
|
||||
mcpCommands: readonly Command[],
|
||||
): readonly Command[] {
|
||||
if (feature('MCP_SKILLS')) {
|
||||
return mcpCommands.filter(
|
||||
cmd =>
|
||||
cmd.type === 'prompt' &&
|
||||
cmd.loadedFrom === 'mcp' &&
|
||||
!cmd.disableModelInvocation,
|
||||
)
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
// SkillTool shows ALL prompt-based commands that the model can invoke
|
||||
// This includes both skills (from /skills/) and commands (from /commands/)
|
||||
export const getSkillToolCommands = memoize(
|
||||
async (cwd: string): Promise<Command[]> => {
|
||||
const allCommands = await getCommands(cwd)
|
||||
return allCommands.filter(
|
||||
cmd =>
|
||||
cmd.type === 'prompt' &&
|
||||
!cmd.disableModelInvocation &&
|
||||
cmd.source !== 'builtin' &&
|
||||
// Always include skills from /skills/ dirs, bundled skills, and legacy /commands/ entries
|
||||
// (they all get an auto-derived description from the first line if frontmatter is missing).
|
||||
// Plugin/MCP commands still require an explicit description to appear in the listing.
|
||||
(cmd.loadedFrom === 'bundled' ||
|
||||
cmd.loadedFrom === 'skills' ||
|
||||
cmd.loadedFrom === 'commands_DEPRECATED' ||
|
||||
cmd.hasUserSpecifiedDescription ||
|
||||
cmd.whenToUse),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
// Filters commands to include only skills. Skills are commands that provide
|
||||
// specialized capabilities for the model to use. They are identified by
|
||||
// loadedFrom being 'skills', 'plugin', or 'bundled', or having disableModelInvocation set.
|
||||
export const getSlashCommandToolSkills = memoize(
|
||||
async (cwd: string): Promise<Command[]> => {
|
||||
try {
|
||||
const allCommands = await getCommands(cwd)
|
||||
return allCommands.filter(
|
||||
cmd =>
|
||||
cmd.type === 'prompt' &&
|
||||
cmd.source !== 'builtin' &&
|
||||
(cmd.hasUserSpecifiedDescription || cmd.whenToUse) &&
|
||||
(cmd.loadedFrom === 'skills' ||
|
||||
cmd.loadedFrom === 'plugin' ||
|
||||
cmd.loadedFrom === 'bundled' ||
|
||||
cmd.disableModelInvocation),
|
||||
)
|
||||
} catch (error) {
|
||||
logError(toError(error))
|
||||
// Return empty array rather than throwing - skills are non-critical
|
||||
// This prevents skill loading failures from breaking the entire system
|
||||
logForDebugging('Returning empty skills array due to load failure')
|
||||
return []
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
/**
|
||||
* Commands that are safe to use in remote mode (--remote).
|
||||
* These only affect local TUI state and don't depend on local filesystem,
|
||||
* git, shell, IDE, MCP, or other local execution context.
|
||||
*
|
||||
* Used in two places:
|
||||
* 1. Pre-filtering commands in main.tsx before REPL renders (prevents race with CCR init)
|
||||
* 2. Preserving local-only commands in REPL's handleRemoteInit after CCR filters
|
||||
*/
|
||||
export const REMOTE_SAFE_COMMANDS: Set<Command> = new Set([
|
||||
session, // Shows QR code / URL for remote session
|
||||
exit, // Exit the TUI
|
||||
clear, // Clear screen
|
||||
help, // Show help
|
||||
theme, // Change terminal theme
|
||||
color, // Change agent color
|
||||
vim, // Toggle vim mode
|
||||
cost, // Show session cost (local cost tracking)
|
||||
usage, // Show usage info
|
||||
copy, // Copy last message
|
||||
btw, // Quick note
|
||||
feedback, // Send feedback
|
||||
plan, // Plan mode toggle
|
||||
keybindings, // Keybinding management
|
||||
statusline, // Status line toggle
|
||||
stickers, // Stickers
|
||||
mobile, // Mobile QR code
|
||||
])
|
||||
|
||||
/**
|
||||
* Builtin commands of type 'local' that ARE safe to execute when received
|
||||
* over the Remote Control bridge. These produce text output that streams
|
||||
* back to the mobile/web client and have no terminal-only side effects.
|
||||
*
|
||||
* 'local-jsx' commands are blocked by type (they render Ink UI) and
|
||||
* 'prompt' commands are allowed by type (they expand to text sent to the
|
||||
* model) — this set only gates 'local' commands.
|
||||
*
|
||||
* When adding a new 'local' command that should work from mobile, add it
|
||||
* here. Default is blocked.
|
||||
*/
|
||||
export const BRIDGE_SAFE_COMMANDS: Set<Command> = new Set(
|
||||
[
|
||||
compact, // Shrink context — useful mid-session from a phone
|
||||
clear, // Wipe transcript
|
||||
cost, // Show session cost
|
||||
summary, // Summarize conversation
|
||||
releaseNotes, // Show changelog
|
||||
files, // List tracked files
|
||||
].filter((c): c is Command => c !== null),
|
||||
)
|
||||
|
||||
/**
|
||||
* Whether a slash command is safe to execute when its input arrived over the
|
||||
* Remote Control bridge (mobile/web client).
|
||||
*
|
||||
* PR #19134 blanket-blocked all slash commands from bridge inbound because
|
||||
* `/model` from iOS was popping the local Ink picker. This predicate relaxes
|
||||
* that with an explicit allowlist: 'prompt' commands (skills) expand to text
|
||||
* and are safe by construction; 'local' commands need an explicit opt-in via
|
||||
* BRIDGE_SAFE_COMMANDS; 'local-jsx' commands render Ink UI and stay blocked.
|
||||
*/
|
||||
export function isBridgeSafeCommand(cmd: Command): boolean {
|
||||
if (cmd.type === 'local-jsx') return false
|
||||
if (cmd.type === 'prompt') return true
|
||||
return BRIDGE_SAFE_COMMANDS.has(cmd)
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter commands to only include those safe for remote mode.
|
||||
* Used to pre-filter commands when rendering the REPL in --remote mode,
|
||||
* preventing local-only commands from being briefly available before
|
||||
* the CCR init message arrives.
|
||||
*/
|
||||
export function filterCommandsForRemoteMode(commands: Command[]): Command[] {
|
||||
return commands.filter(cmd => REMOTE_SAFE_COMMANDS.has(cmd))
|
||||
}
|
||||
|
||||
export function findCommand(
|
||||
commandName: string,
|
||||
commands: Command[],
|
||||
): Command | undefined {
|
||||
return commands.find(
|
||||
_ =>
|
||||
_.name === commandName ||
|
||||
getCommandName(_) === commandName ||
|
||||
_.aliases?.includes(commandName),
|
||||
)
|
||||
}
|
||||
|
||||
export function hasCommand(commandName: string, commands: Command[]): boolean {
|
||||
return findCommand(commandName, commands) !== undefined
|
||||
}
|
||||
|
||||
export function getCommand(commandName: string, commands: Command[]): Command {
|
||||
const command = findCommand(commandName, commands)
|
||||
if (!command) {
|
||||
throw ReferenceError(
|
||||
`Command ${commandName} not found. Available commands: ${commands
|
||||
.map(_ => {
|
||||
const name = getCommandName(_)
|
||||
return _.aliases ? `${name} (aliases: ${_.aliases.join(', ')})` : name
|
||||
})
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
.join(', ')}`,
|
||||
)
|
||||
}
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a command's description with its source annotation for user-facing UI.
|
||||
* Use this in typeahead, help screens, and other places where users need to see
|
||||
* where a command comes from.
|
||||
*
|
||||
* For model-facing prompts (like SkillTool), use cmd.description directly.
|
||||
*/
|
||||
export function formatDescriptionWithSource(cmd: Command): string {
|
||||
if (cmd.type !== 'prompt') {
|
||||
return cmd.description
|
||||
}
|
||||
|
||||
if (cmd.kind === 'workflow') {
|
||||
return `${cmd.description} (workflow)`
|
||||
}
|
||||
|
||||
if (cmd.source === 'plugin') {
|
||||
const pluginName = cmd.pluginInfo?.pluginManifest.name
|
||||
if (pluginName) {
|
||||
return `(${pluginName}) ${cmd.description}`
|
||||
}
|
||||
return `${cmd.description} (plugin)`
|
||||
}
|
||||
|
||||
if (cmd.source === 'builtin' || cmd.source === 'mcp') {
|
||||
return cmd.description
|
||||
}
|
||||
|
||||
if (cmd.source === 'bundled') {
|
||||
return `${cmd.description} (bundled)`
|
||||
}
|
||||
|
||||
return `${cmd.description} (${getSettingSourceName(cmd.source)})`
|
||||
}
|
||||
Reference in New Issue
Block a user