Files
claude-code-now/src/commands.ts
instructkr a99de1bb3c 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
2026-03-31 03:06:26 -07:00

755 lines
25 KiB
TypeScript

// 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)})`
}