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
755 lines
25 KiB
TypeScript
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)})`
|
|
}
|