icopilot 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (203) hide show
  1. package/CHANGELOG.md +250 -0
  2. package/LICENSE +21 -0
  3. package/README.md +214 -0
  4. package/bin/icopilot.js +6 -0
  5. package/dist/acp/router.js +123 -0
  6. package/dist/acp/schema.js +53 -0
  7. package/dist/agents/aggregator.js +187 -0
  8. package/dist/agents/custom-agents.js +97 -0
  9. package/dist/agents/goal-driven.js +411 -0
  10. package/dist/agents/multi-repo.js +350 -0
  11. package/dist/agents/parallel-runner.js +181 -0
  12. package/dist/agents/router.js +144 -0
  13. package/dist/agents/self-heal.js +481 -0
  14. package/dist/agents/tdd-agent.js +278 -0
  15. package/dist/api/github-models.js +158 -0
  16. package/dist/bridge/ide-bridge.js +479 -0
  17. package/dist/cloud/routine-executor.js +34 -0
  18. package/dist/cloud/routine-scheduler.js +67 -0
  19. package/dist/cloud/routine-storage.js +297 -0
  20. package/dist/commands/acp-cmd.js +143 -0
  21. package/dist/commands/actions-cmd.js +624 -0
  22. package/dist/commands/agent-cmd.js +144 -0
  23. package/dist/commands/alias-cmd.js +132 -0
  24. package/dist/commands/bookmark-cmd.js +77 -0
  25. package/dist/commands/changelog-cmd.js +99 -0
  26. package/dist/commands/changes-cmd.js +120 -0
  27. package/dist/commands/clipboard-cmd.js +217 -0
  28. package/dist/commands/cloud-routine-cmd.js +265 -0
  29. package/dist/commands/codegen-cmd.js +544 -0
  30. package/dist/commands/compare-cmd.js +116 -0
  31. package/dist/commands/context-cmd.js +247 -0
  32. package/dist/commands/context-viz-cmd.js +43 -0
  33. package/dist/commands/conventions-cmd.js +116 -0
  34. package/dist/commands/cost-cmd.js +51 -0
  35. package/dist/commands/deps-cmd.js +294 -0
  36. package/dist/commands/diagram-cmd.js +658 -0
  37. package/dist/commands/diff-review-cmd.js +92 -0
  38. package/dist/commands/doc-cmd.js +412 -0
  39. package/dist/commands/doctor-cmd.js +152 -0
  40. package/dist/commands/editor-cmd.js +49 -0
  41. package/dist/commands/env-cmd.js +86 -0
  42. package/dist/commands/explain-cmd.js +78 -0
  43. package/dist/commands/explain-shell-cmd.js +22 -0
  44. package/dist/commands/explore-cmd.js +231 -0
  45. package/dist/commands/feedback-cmd.js +98 -0
  46. package/dist/commands/fix-cmd.js +17 -0
  47. package/dist/commands/generate-cmd.js +38 -0
  48. package/dist/commands/git-extra.js +197 -0
  49. package/dist/commands/git-log-cmd.js +98 -0
  50. package/dist/commands/git-undo-cmd.js +137 -0
  51. package/dist/commands/git.js +155 -0
  52. package/dist/commands/history-cmd.js +122 -0
  53. package/dist/commands/index-cmd.js +65 -0
  54. package/dist/commands/init-cmd.js +73 -0
  55. package/dist/commands/lint-cmd.js +133 -0
  56. package/dist/commands/memory-cmd.js +98 -0
  57. package/dist/commands/metrics-cmd.js +97 -0
  58. package/dist/commands/mode-prefix.js +30 -0
  59. package/dist/commands/multi-cmd.js +44 -0
  60. package/dist/commands/notify-cmd.js +204 -0
  61. package/dist/commands/profile-cmd.js +101 -0
  62. package/dist/commands/prompts.js +17 -0
  63. package/dist/commands/rag-cmd.js +60 -0
  64. package/dist/commands/readme-cmd.js +564 -0
  65. package/dist/commands/reasoning-cmd.js +34 -0
  66. package/dist/commands/refactor-cmd.js +96 -0
  67. package/dist/commands/release-cmd.js +450 -0
  68. package/dist/commands/repo-cmd.js +195 -0
  69. package/dist/commands/route-cmd.js +21 -0
  70. package/dist/commands/schedule-cmd.js +109 -0
  71. package/dist/commands/search-cmd.js +47 -0
  72. package/dist/commands/security-cmd.js +156 -0
  73. package/dist/commands/settings-cmd.js +238 -0
  74. package/dist/commands/skill-cmd.js +338 -0
  75. package/dist/commands/slash.js +2721 -0
  76. package/dist/commands/snippets-cmd.js +83 -0
  77. package/dist/commands/space-cmd.js +92 -0
  78. package/dist/commands/stash-cmd.js +156 -0
  79. package/dist/commands/stats-cmd.js +36 -0
  80. package/dist/commands/style-cmd.js +85 -0
  81. package/dist/commands/suggest-cmd.js +40 -0
  82. package/dist/commands/summary-cmd.js +138 -0
  83. package/dist/commands/task-cmd.js +58 -0
  84. package/dist/commands/team-memory-cmd.js +97 -0
  85. package/dist/commands/template-cmd.js +475 -0
  86. package/dist/commands/test-cmd.js +146 -0
  87. package/dist/commands/todo-cmd.js +172 -0
  88. package/dist/commands/tokens-cmd.js +277 -0
  89. package/dist/commands/trigger-cmd.js +147 -0
  90. package/dist/commands/undo-cmd.js +18 -0
  91. package/dist/commands/voice-cmd.js +89 -0
  92. package/dist/commands/watch-cmd.js +110 -0
  93. package/dist/commands/web-cmd.js +183 -0
  94. package/dist/commands/worktree-cmd.js +119 -0
  95. package/dist/config-profile.js +66 -0
  96. package/dist/config.js +288 -0
  97. package/dist/context/compactor.js +53 -0
  98. package/dist/context/dep-context.js +329 -0
  99. package/dist/context/file-refs.js +54 -0
  100. package/dist/context/git-context.js +229 -0
  101. package/dist/context/image-input.js +66 -0
  102. package/dist/context/memory.js +55 -0
  103. package/dist/context/persistent-memory.js +104 -0
  104. package/dist/context/pinned.js +96 -0
  105. package/dist/context/priority.js +150 -0
  106. package/dist/context/read-only.js +48 -0
  107. package/dist/context/smart-files.js +286 -0
  108. package/dist/context/team-memory.js +156 -0
  109. package/dist/extensions/loader.js +149 -0
  110. package/dist/extensions/marketplace.js +49 -0
  111. package/dist/extensions/slack-provider.js +181 -0
  112. package/dist/extensions/team.js +56 -0
  113. package/dist/extensions/teams-provider.js +222 -0
  114. package/dist/extensions/voice.js +18 -0
  115. package/dist/hooks/lifecycle.js +215 -0
  116. package/dist/hooks/precommit.js +463 -0
  117. package/dist/index/embeddings.js +23 -0
  118. package/dist/index/indexer.js +86 -0
  119. package/dist/index/retrieve.js +20 -0
  120. package/dist/index/store.js +95 -0
  121. package/dist/index.js +286 -0
  122. package/dist/intelligence/dead-code.js +457 -0
  123. package/dist/intelligence/error-watch.js +263 -0
  124. package/dist/intelligence/navigation.js +141 -0
  125. package/dist/intelligence/stack-trace.js +210 -0
  126. package/dist/intelligence/symbol-index.js +410 -0
  127. package/dist/knowledge/auto-memory.js +412 -0
  128. package/dist/knowledge/conventions.js +475 -0
  129. package/dist/knowledge/corrections.js +213 -0
  130. package/dist/knowledge/rag.js +450 -0
  131. package/dist/knowledge/style-learner.js +324 -0
  132. package/dist/logger.js +35 -0
  133. package/dist/mcp/client.js +144 -0
  134. package/dist/mcp/config.js +24 -0
  135. package/dist/mcp/index.js +89 -0
  136. package/dist/modes/auto-compact.js +20 -0
  137. package/dist/modes/autopilot.js +157 -0
  138. package/dist/modes/background.js +82 -0
  139. package/dist/modes/interactive.js +187 -0
  140. package/dist/modes/oneshot.js +36 -0
  141. package/dist/modes/tui.js +265 -0
  142. package/dist/modes/turn.js +342 -0
  143. package/dist/notifications/manager.js +107 -0
  144. package/dist/plugins/marketplace.js +244 -0
  145. package/dist/providers/custom-provider.js +298 -0
  146. package/dist/providers/local-model.js +121 -0
  147. package/dist/routing/profiles.js +44 -0
  148. package/dist/routing/router.js +18 -0
  149. package/dist/sandbox/container.js +151 -0
  150. package/dist/security/audit.js +237 -0
  151. package/dist/security/content-filter.js +449 -0
  152. package/dist/security/proxy.js +301 -0
  153. package/dist/security/retention.js +281 -0
  154. package/dist/security/roles.js +252 -0
  155. package/dist/server/api-server.js +679 -0
  156. package/dist/session/bookmarks.js +72 -0
  157. package/dist/session/cloud-session.js +291 -0
  158. package/dist/session/handoff.js +405 -0
  159. package/dist/session/manager.js +35 -0
  160. package/dist/session/session.js +296 -0
  161. package/dist/session/share.js +313 -0
  162. package/dist/session/undo-journal.js +91 -0
  163. package/dist/snippets/store.js +60 -0
  164. package/dist/spaces/space-config.js +156 -0
  165. package/dist/spaces/space.js +220 -0
  166. package/dist/stats/store.js +101 -0
  167. package/dist/tools/apply-patch.js +134 -0
  168. package/dist/tools/auto-check.js +218 -0
  169. package/dist/tools/diff-edit.js +150 -0
  170. package/dist/tools/diff-prompt.js +36 -0
  171. package/dist/tools/edit-file.js +66 -0
  172. package/dist/tools/file-ops.js +205 -0
  173. package/dist/tools/glob.js +17 -0
  174. package/dist/tools/grep.js +56 -0
  175. package/dist/tools/image.js +194 -0
  176. package/dist/tools/list-directory.js +228 -0
  177. package/dist/tools/memory.js +17 -0
  178. package/dist/tools/multi-edit.js +299 -0
  179. package/dist/tools/policy.js +95 -0
  180. package/dist/tools/registry.js +484 -0
  181. package/dist/tools/retry.js +74 -0
  182. package/dist/tools/run-in-terminal.js +162 -0
  183. package/dist/tools/safety.js +64 -0
  184. package/dist/tools/sandbox.js +15 -0
  185. package/dist/tools/search-symbols.js +212 -0
  186. package/dist/tools/shell.js +118 -0
  187. package/dist/tools/web.js +167 -0
  188. package/dist/ui/prompt.js +37 -0
  189. package/dist/ui/render.js +96 -0
  190. package/dist/ui/screen.js +13 -0
  191. package/dist/ui/theme.js +56 -0
  192. package/dist/util/browser.js +34 -0
  193. package/dist/util/completion.js +350 -0
  194. package/dist/util/cost.js +28 -0
  195. package/dist/util/keybindings.js +113 -0
  196. package/dist/util/lazy.js +26 -0
  197. package/dist/util/perf.js +25 -0
  198. package/dist/util/token-worker.js +11 -0
  199. package/dist/util/tokens.js +50 -0
  200. package/dist/workflows/builtins.js +128 -0
  201. package/dist/workflows/engine.js +496 -0
  202. package/dist/workflows/file-trigger.js +197 -0
  203. package/package.json +79 -0
@@ -0,0 +1,2721 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { spawn } from 'node:child_process';
4
+ import { confirm, input, select } from '@inquirer/prompts';
5
+ import { config, setProvider } from '../config.js';
6
+ import { providerRegistry } from '../providers/custom-provider.js';
7
+ import { isLocalProviderName, localModelProvider } from '../providers/local-model.js';
8
+ import { Session } from '../session/session.js';
9
+ import { theme } from '../ui/theme.js';
10
+ import { showDiff, commitFromStaged, prDescription } from './git.js';
11
+ import { showChangesSinceLastTurn, showChangesSinceSessionStart } from './changes-cmd.js';
12
+ import { compactSession } from '../context/compactor.js';
13
+ import { PinnedContext } from '../context/pinned.js';
14
+ import { addReadOnly, getReadOnlyFiles, removeReadOnly } from '../context/read-only.js';
15
+ import { pickSession, exportSession } from '../session/manager.js';
16
+ import { reviewStaged, draftIssue, scaffoldBranch } from './git-extra.js';
17
+ import { indexCommand } from './index-cmd.js';
18
+ import { routeCommand } from './route-cmd.js';
19
+ import { undoCommand } from './undo-cmd.js';
20
+ import { gitUndo } from './git-undo-cmd.js';
21
+ import { costCommand } from './cost-cmd.js';
22
+ import { snippetsCommand } from './snippets-cmd.js';
23
+ import { profileCommand } from './profile-cmd.js';
24
+ import { styleCommand } from './style-cmd.js';
25
+ import { conventionsCommand } from './conventions-cmd.js';
26
+ import { statsCommand } from './stats-cmd.js';
27
+ import { buildExplain } from './explain-cmd.js';
28
+ import { lintCommand } from './lint-cmd.js';
29
+ import { testCommand } from './test-cmd.js';
30
+ import { depsCommand } from './deps-cmd.js';
31
+ import { bookmarkCommand } from './bookmark-cmd.js';
32
+ import { historyCommand } from './history-cmd.js';
33
+ import { searchCommand } from './search-cmd.js';
34
+ import { refactorCommand } from './refactor-cmd.js';
35
+ import { suggestCommand } from './suggest-cmd.js';
36
+ import { buildSummary } from './summary-cmd.js';
37
+ import { compareCommand } from './compare-cmd.js';
38
+ import { ragCommand } from './rag-cmd.js';
39
+ import { gitLogCommand } from './git-log-cmd.js';
40
+ import { envCommand } from './env-cmd.js';
41
+ import { TodoList, todoCommand } from './todo-cmd.js';
42
+ import { tokensCommand } from './tokens-cmd.js';
43
+ import { getReasoningConfig, parseTokenBudget, setReasoningEffort, setThinkTokens, } from './reasoning-cmd.js';
44
+ import { stashCommand } from './stash-cmd.js';
45
+ import { notifyCommand } from './notify-cmd.js';
46
+ import { buildChangelogPrompt } from './changelog-cmd.js';
47
+ import { releaseCommand } from './release-cmd.js';
48
+ import { buildFixPrompt } from './fix-cmd.js';
49
+ import { securityCommand } from './security-cmd.js';
50
+ import { formatInitResult, initProject } from './init-cmd.js';
51
+ import { templateCommand } from './template-cmd.js';
52
+ import { aliasCommand } from './alias-cmd.js';
53
+ import { MetricsCollector, metricsCommand } from './metrics-cmd.js';
54
+ import { reviewDiff } from './diff-review-cmd.js';
55
+ import { formatDiagnostics, runDiagnostics } from './doctor-cmd.js';
56
+ import { explainShellCommand } from './explain-shell-cmd.js';
57
+ import { codegenCommand } from './codegen-cmd.js';
58
+ import { buildGeneratePrompt } from './generate-cmd.js';
59
+ import { voiceCommand } from './voice-cmd.js';
60
+ import { actionsCommand } from './actions-cmd.js';
61
+ import { buildMultiConfig } from './multi-cmd.js';
62
+ import { agentCommand } from './agent-cmd.js';
63
+ import { TDDAgent } from '../agents/tdd-agent.js';
64
+ import { acpCommand } from './acp-cmd.js';
65
+ import { skillCommand } from './skill-cmd.js';
66
+ import { docCommand } from './doc-cmd.js';
67
+ import { triggerCommand as runTriggerCommand } from './trigger-cmd.js';
68
+ import { backgroundTaskManager } from '../modes/background.js';
69
+ import { runAutopilot } from '../modes/autopilot.js';
70
+ import { WorkflowEngine } from '../workflows/engine.js';
71
+ import { BUILTIN_WORKFLOWS, createWorkflowTemplate, getBuiltinWorkflow, renderWorkflowYaml, } from '../workflows/builtins.js';
72
+ import { taskCommand } from './task-cmd.js';
73
+ import { watchCommand } from './watch-cmd.js';
74
+ import { CorrectionMemory } from '../knowledge/corrections.js';
75
+ import { memoryCommand } from './memory-cmd.js';
76
+ import { teamMemoryCommand } from './team-memory-cmd.js';
77
+ import { contextCommand } from './context-cmd.js';
78
+ import { repoCommand } from './repo-cmd.js';
79
+ import { shareCommand } from '../session/share.js';
80
+ import { CloudSession } from '../session/cloud-session.js';
81
+ import { createHandoff, exportHandoffFile, importHandoffFile, previewHandoff, receiveHandoff, } from '../session/handoff.js';
82
+ import { extensionCommand } from '../extensions/loader.js';
83
+ import { pluginCommand } from '../plugins/marketplace.js';
84
+ import { spaceCommand } from './space-cmd.js';
85
+ import { diagramCommand } from './diagram-cmd.js';
86
+ import { readmeCommand } from './readme-cmd.js';
87
+ import { getGitHubRepoSlug, openGitHubIssues, submitFeedback, } from './feedback-cmd.js';
88
+ import { isModelSettingKey, resetSetting, setSetting, showSettings } from './settings-cmd.js';
89
+ import { copyContextToClipboard, copyTextToClipboard, readClipboard } from './clipboard-cmd.js';
90
+ import { resolveModePrefix } from './mode-prefix.js';
91
+ import { openEditor } from './editor-cmd.js';
92
+ import { fetchAndConvert, validateWebUrl } from './web-cmd.js';
93
+ import { detectAutoLintCommand, detectAutoTestCommand } from '../tools/auto-check.js';
94
+ import { ErrorWatcher, suggestFix } from '../intelligence/error-watch.js';
95
+ import { DeadCodeDetector } from '../intelligence/dead-code.js';
96
+ import { findReferences, goToDefinition } from '../intelligence/navigation.js';
97
+ import { ContainerSandbox } from '../sandbox/container.js';
98
+ import { analyzeStackTrace, formatForLLM, parseStackTrace } from '../intelligence/stack-trace.js';
99
+ import { ParallelAgentRunner, } from '../agents/parallel-runner.js';
100
+ import { GoalDrivenAgent, } from '../agents/goal-driven.js';
101
+ import { ProxyManager } from '../security/proxy.js';
102
+ import { formatFilterRules, formatFilterTestResult, loadProjectContentFilter, parseFilterAction, parseFilterPattern, removeProjectFilterRule, saveProjectFilterRule, } from '../security/content-filter.js';
103
+ import { RoleManager, defaultRolesConfigPath } from '../security/roles.js';
104
+ import { RetentionManager, formatPolicies as formatRetentionPolicies, formatPreview as formatRetentionPreview, formatResult as formatRetentionResult, } from '../security/retention.js';
105
+ import { AuditLogger, auditLogPath } from '../security/audit.js';
106
+ import { BridgeServer, DEFAULT_BRIDGE_PORT } from '../bridge/ide-bridge.js';
107
+ import { DEFAULT_API_PORT, getGlobalAPIServer } from '../server/api-server.js';
108
+ import { openBrowser } from '../util/browser.js';
109
+ import { assertSandbox } from '../tools/sandbox.js';
110
+ import { loadPolicy, shellCommandAllowed } from '../tools/policy.js';
111
+ import { checkCommandSafety, formatSafetyWarning } from '../tools/safety.js';
112
+ import { cancelSchedule, listScheduled, scheduleOnce, scheduleRecurring, setScheduleRunner, } from './schedule-cmd.js';
113
+ import { worktreeCommand } from './worktree-cmd.js';
114
+ import { exploreCommand } from './explore-cmd.js';
115
+ import { cloudRoutineCommand } from './cloud-routine-cmd.js';
116
+ const HELP = `
117
+ ${theme.brand('Slash commands')}
118
+ /help show this help
119
+ /clear, /new wipe conversation history
120
+ /model <name> switch the active model (e.g. gpt-4o-mini, llama3.2)
121
+ /provider show current model provider
122
+ /provider list list configured providers
123
+ /provider set <name> switch model provider (github, ollama, vllm, lmstudio, ...)
124
+ /provider test test the active provider connection
125
+ /cwd <path> change repository context
126
+ /diff show git diff (unstaged, then staged)
127
+ /changes [last] show changes since session start or last AI turn
128
+ /git-log show recent git commits
129
+ /context [view] show visual context usage and trim tools
130
+ /usage alias for /context
131
+ /pin <file> pin a file to persistent context (or list pinned files)
132
+ /unpin <file|--all> remove pinned files from persistent context
133
+ /read-only, /ro <path> add a read-only context file
134
+ /read-only drop <path> remove a read-only context file
135
+ /read-only list list read-only context files
136
+ /every <interval> <prompt> schedule a recurring prompt
137
+ /after <delay> <prompt> schedule a one-shot prompt
138
+ /schedule list active scheduled prompts
139
+ /schedule cancel <id> cancel a scheduled prompt
140
+ /tokens show detailed token usage breakdown
141
+ /editor open $VISUAL/$EDITOR for a multi-line prompt
142
+ /reasoning [level] show or set reasoning effort (low|medium|high|max)
143
+ /think-tokens [budget] show or set reasoning token budget (8k, 0.5M, 0=off)
144
+ /history browse recent conversation history
145
+ /compact summarize conversation, free token space
146
+ /settings [key] [value] show, set, or reset runtime settings
147
+ /feedback [type] [text] save feedback locally and optionally open issues
148
+ /sessions list and resume saved sessions
149
+ /cloud create [name] create and connect a cloud session
150
+ /cloud connect <id> connect to a cloud session
151
+ /cloud list list cloud sessions
152
+ /cloud destroy <id> destroy a cloud session
153
+ /cloud sync sync local session state to the cloud
154
+ /export [md|json] [path] export current session transcript/state
155
+ /share share session bundles and clipboard exports
156
+ /paste [image] send clipboard text or image as the next prompt
157
+ /copy <text> copy arbitrary text to the system clipboard
158
+ /copy-context [last] copy conversation context to the clipboard
159
+ /handoff export [path] export resumable handoff bundle
160
+ /handoff import <path> import a handoff bundle into a new session
161
+ /handoff preview <path> inspect a handoff bundle without importing
162
+ /plan toggle Plan Mode
163
+ /edit-format [whole|diff] show or change the active edit format
164
+ /autopilot [goal] toggle autopilot or run a goal immediately
165
+ /goal <description> plan, implement, test, and verify a goal in the background
166
+ /goal status show the current or most recent goal run
167
+ /goal abort abort the active goal run
168
+ /commit generate semantic commit from staged diff
169
+ /pr draft PR description (branch vs default)
170
+ /review review staged changes
171
+ /diff-review [target] review any diff (unstaged, staged, branch, range, file)
172
+ /issue [title] draft a GitHub issue from current context
173
+ /branch <topic> create a conventional feature/fix branch
174
+ /index build|status|search workspace embeddings index
175
+ /rag index|search|stats manage local TF-IDF RAG index
176
+ /search <query> semantic search over indexed workspace code
177
+ /goto <symbol> find a symbol definition with regex navigation
178
+ /refs <symbol> find symbol references with regex navigation
179
+ /route get|set|list multi-model routing profile
180
+ /undo [--hard|file|status] undo last AI git commit or access file undo journal
181
+ /redo redo approved file writes
182
+ /cost estimate current session token cost
183
+ /snippets, /snippet manage reusable prompt snippets
184
+ /profile, /profiles manage saved CLI profiles
185
+ /role [set <name>|list] show or change the active role
186
+ /style [learn|reset] learn or inspect project coding style
187
+ /conventions [subcommand] manage project coding conventions
188
+ /stats [show|reset|path] show or reset local usage stats
189
+ /audit [search|stats|export] inspect tool execution audit trail
190
+ /explain <path> build an explanation prompt for a file/folder
191
+ /suggest <request> suggest a shell command for a task
192
+ /summary build a project architecture summary prompt
193
+ /compare <file-a> <file-b> compare two files with diff + AI prompt
194
+ /env [--full|--check VAR] show current environment context
195
+ /template [name] [--apply] scaffold a built-in project template
196
+ /readme [preview|update] scaffold or refresh README.md from project analysis
197
+ /changelog [range|--last] build a changelog prompt from git commits
198
+ /release <type>|preview automate version bump, changelog, tag, publish
199
+ /fix <error> build an AI troubleshooting prompt for an error
200
+ /heal [--max <n>] run build, apply safe auto-fixes, and retry
201
+ /lint detect available repository linters
202
+ /test detect available repository test frameworks
203
+ /auto-lint [on|off] toggle auto-lint after AI file edits
204
+ /auto-test [on|off] toggle auto-test after AI file edits
205
+ /auto-fix [on|off] toggle auto-repair for failed auto-checks
206
+ /tdd <description>|status start or inspect the latest TDD cycle
207
+ /doctor diagnose local iCopilot setup
208
+ /todo track session todos
209
+ /task, /tasks inspect background tasks
210
+ /deps inspect project dependencies
211
+ /init [--force] create .icopilot project configuration
212
+ /security scan for common secrets and credential leaks
213
+ /proxy show, set, clear, or test proxy configuration
214
+ /filter [list|add|remove|test] manage prompt content filter rules
215
+ /retention inspect or enforce retention policies
216
+ /dead-code [path] scan for unused exports and unreachable files
217
+ /refactor <subcommand> build an AI refactor prompt
218
+ /stacktrace <trace> analyze a stack trace and diagnose root cause
219
+ /metrics show session performance metrics
220
+ /bookmark, /bookmarks manage session rewind bookmarks
221
+ /alias [list|set|remove] manage custom command aliases
222
+ /skill manage reusable skill sources
223
+ /stash stash conversation state for later
224
+ /notify <command> configure Slack/Teams notifications
225
+ /explain-shell <cmd> explain a shell command step by step
226
+ /generate <goal> generate a shell command for a goal
227
+ /actions <desc>|list|validate generate or inspect GitHub Actions workflows
228
+ /codegen <description> generate a module scaffold plus test file
229
+ /multi <models> <prompt> query multiple models in parallel
230
+ /agent <name> [query] build a built-in or custom agent delegation prompt
231
+ /parallel <spec> run multiple agent tasks concurrently
232
+ /explore <question> explore codebase with AI agent
233
+ /trigger <subcommand> manage file-change triggers
234
+ /watch <pattern> <cmd> file watcher configuration
235
+ /web <url> [focus] fetch a web page into conversation context
236
+ /bridge <subcommand> manage IDE bridge websocket server
237
+ /acp [subcommand] manage ACP (Agent Client Protocol) server
238
+ /error-watch <action> watch build errors and suggest fixes
239
+ /memory manage persistent + auto-learned memory
240
+ /corrections manage remembered user corrections
241
+ /team-memory manage shared team memory
242
+ /repo manage multi-repo orchestration
243
+ /space manage project spaces
244
+ /doc <file> [symbol] generate docs for a file or symbol
245
+ /diagram [type] generate Mermaid architecture diagrams
246
+ /extension [list|info|reload] inspect local extensions
247
+ /serve <subcommand> manage HTTP API server
248
+ /worktree <subcommand> manage git worktrees
249
+ /cloud-routine <subcommand> manage cloud-scheduled routines
250
+ /sandbox <run|shell|status|cleanup> use Docker sandbox helpers
251
+ /run <command> run a shell command and optionally add output to chat
252
+ /voice [start|stop|status] voice input (speech-to-text, requires provider plugin)
253
+ /plugin [subcommand] search and manage marketplace plugins
254
+ /workflow [subcommand] manage workflow definitions
255
+ /exit, /quit quit iCopilot
256
+
257
+ ${theme.brand('Inline')}
258
+ /ask <message> discuss only for one turn
259
+ /code <message> implement directly for one turn
260
+ /architect <message> plan briefly, then implement for one turn
261
+ @path/to/file inject file contents into next message
262
+ Ctrl+X Ctrl+E open editor for a multi-line prompt
263
+ Ctrl-C interrupt streaming (does not exit)
264
+ `;
265
+ const errorWatcher = new ErrorWatcher();
266
+ let activeGoalRun = null;
267
+ let lastGoalRun = null;
268
+ const ideBridgeServer = new BridgeServer();
269
+ const apiServer = getGlobalAPIServer();
270
+ const sandboxByCwd = new Map();
271
+ let lastTddResult = null;
272
+ errorWatcher.onError((error) => {
273
+ process.stdout.write(`${theme.warn(`[error-watch] ${formatParsedError(error)}`)}\n${theme.dim(`${suggestFix(error)}\n`)}\n`);
274
+ });
275
+ export async function handleSlash(line, ctx) {
276
+ const trimmed = line.trim();
277
+ if (!trimmed.startsWith('/'))
278
+ return { handled: false, consumed: false };
279
+ const modePrefix = resolveModePrefix(trimmed);
280
+ if (modePrefix.matched) {
281
+ if (modePrefix.consumed) {
282
+ process.stdout.write(theme.warn(`${modePrefix.usage}\n`));
283
+ return done();
284
+ }
285
+ return done(false, modePrefix.forwardInput, modePrefix.turnMode ?? null);
286
+ }
287
+ const spaceIndex = trimmed.indexOf(' ');
288
+ const cmd = (spaceIndex === -1 ? trimmed.slice(1) : trimmed.slice(1, spaceIndex)).trim();
289
+ const arg = spaceIndex === -1 ? '' : trimmed.slice(spaceIndex + 1).trim();
290
+ const rest = arg ? arg.split(/\s+/) : [];
291
+ const s = ctx.session;
292
+ const roleManager = getRoleManager(s.state.cwd);
293
+ const normalizedCommand = cmd.toLowerCase();
294
+ setScheduleRunner(ctx.schedulePrompt ?? null);
295
+ if (normalizedCommand !== 'help' && normalizedCommand !== 'role') {
296
+ const access = roleManager.checkAccess(`command:${normalizedCommand}`);
297
+ if (!access.allowed) {
298
+ process.stdout.write(theme.err(`${access.reason}\n`));
299
+ return done();
300
+ }
301
+ }
302
+ switch (normalizedCommand) {
303
+ case 'help':
304
+ process.stdout.write(HELP);
305
+ return done();
306
+ case 'clear':
307
+ case 'new':
308
+ s.reset();
309
+ process.stdout.write(theme.ok('✔ history cleared.\n'));
310
+ return done();
311
+ case 'model':
312
+ if (!arg) {
313
+ process.stdout.write(theme.dim(`current model: ${s.state.model}\n`));
314
+ }
315
+ else {
316
+ s.setModel(arg);
317
+ process.stdout.write(theme.ok(`✔ model → ${arg}\n`));
318
+ }
319
+ return done();
320
+ case 'provider': {
321
+ if (!arg) {
322
+ process.stdout.write(renderCurrentProvider(s.state.model));
323
+ return done();
324
+ }
325
+ const [subcommand = '', ...providerArgs] = rest;
326
+ if (subcommand === 'list') {
327
+ process.stdout.write(renderProviderList());
328
+ return done();
329
+ }
330
+ if (subcommand === 'set') {
331
+ const target = providerArgs[0]?.trim().toLowerCase();
332
+ if (!target) {
333
+ process.stdout.write(theme.warn('usage: /provider set <name>\n'));
334
+ return done();
335
+ }
336
+ try {
337
+ setProvider(target);
338
+ if (config.provider === target) {
339
+ s.setModel(config.defaultModel);
340
+ }
341
+ }
342
+ catch (error) {
343
+ process.stdout.write(theme.err(`${error?.message || error}\n`));
344
+ return done();
345
+ }
346
+ process.stdout.write(theme.ok(`✔ provider → ${config.provider} (${config.endpoint})\n`));
347
+ return done();
348
+ }
349
+ if (subcommand === 'test') {
350
+ process.stdout.write(await testActiveProvider(s.state.model));
351
+ return done();
352
+ }
353
+ process.stdout.write(theme.warn('usage: /provider [list|set <name>|test]\n'));
354
+ return done();
355
+ }
356
+ case 'cwd':
357
+ if (!arg) {
358
+ process.stdout.write(theme.dim(`cwd: ${s.state.cwd}\n`));
359
+ }
360
+ else {
361
+ const next = path.resolve(s.state.cwd, arg);
362
+ if (!fs.existsSync(next) || !fs.statSync(next).isDirectory()) {
363
+ process.stdout.write(theme.err(`not a directory: ${next}\n`));
364
+ }
365
+ else {
366
+ config.cwd = next;
367
+ s.setCwd(next);
368
+ process.stdout.write(theme.ok(`✔ cwd → ${next}\n`));
369
+ }
370
+ }
371
+ return done();
372
+ case 'diff':
373
+ await showDiff();
374
+ return done();
375
+ case 'changes':
376
+ process.stdout.write(rest[0] === 'last'
377
+ ? await showChangesSinceLastTurn(s)
378
+ : await showChangesSinceSessionStart(s));
379
+ return done();
380
+ case 'git-log':
381
+ process.stdout.write(await gitLogCommand(rest, s.state.cwd));
382
+ return done();
383
+ case 'context':
384
+ case 'usage':
385
+ process.stdout.write(contextCommand(rest, s));
386
+ return done();
387
+ case 'settings': {
388
+ if (!arg) {
389
+ process.stdout.write(showSettings());
390
+ return done();
391
+ }
392
+ if ((rest[0] ?? '').toLowerCase() === 'reset') {
393
+ const key = rest[1];
394
+ if (!key) {
395
+ process.stdout.write(theme.warn('usage: /settings reset <key>\n'));
396
+ return done();
397
+ }
398
+ try {
399
+ process.stdout.write(resetSetting(key));
400
+ if (isModelSettingKey(key))
401
+ s.setModel(config.defaultModel);
402
+ }
403
+ catch (error) {
404
+ process.stdout.write(theme.err(`${error.message}\n`));
405
+ }
406
+ return done();
407
+ }
408
+ if (rest.length < 2) {
409
+ process.stdout.write(theme.warn('usage: /settings [<key> <value> | reset <key>]\n'));
410
+ return done();
411
+ }
412
+ const key = rest[0];
413
+ const value = arg.slice(key.length).trim();
414
+ try {
415
+ process.stdout.write(setSetting(key, value));
416
+ if (isModelSettingKey(key))
417
+ s.setModel(config.defaultModel);
418
+ }
419
+ catch (error) {
420
+ process.stdout.write(theme.err(`${error.message}\n`));
421
+ }
422
+ return done();
423
+ }
424
+ case 'feedback': {
425
+ try {
426
+ const feedback = await resolveFeedbackInput(rest, arg);
427
+ if (!feedback) {
428
+ process.stdout.write(theme.warn('usage: /feedback [bug|feature|praise] <text>\n'));
429
+ return done();
430
+ }
431
+ process.stdout.write(submitFeedback(feedback.type, feedback.text, { cwd: s.state.cwd }));
432
+ const repo = getGitHubRepoSlug(s.state.cwd);
433
+ if (repo) {
434
+ const openIssue = await confirm({
435
+ message: `Open GitHub issue form for ${repo}?`,
436
+ default: false,
437
+ }).catch(() => false);
438
+ if (openIssue) {
439
+ process.stdout.write(openGitHubIssues(repo)
440
+ ? theme.ok(`Opened ${repo} issues in your browser.\n`)
441
+ : theme.warn(`Could not open browser. Visit ${repo} issues manually.\n`));
442
+ }
443
+ }
444
+ }
445
+ catch (error) {
446
+ process.stdout.write(theme.err(`${error.message}\n`));
447
+ }
448
+ return done();
449
+ }
450
+ case 'pin': {
451
+ const pinned = PinnedContext.fromJSON(s.state.pinned);
452
+ if (!arg) {
453
+ process.stdout.write(formatPinnedFiles(pinned.list()));
454
+ return done();
455
+ }
456
+ const added = pinned.add(arg, s.state.cwd);
457
+ if (!added) {
458
+ process.stdout.write(theme.err(`unable to pin file: ${path.resolve(s.state.cwd, arg)}\n`));
459
+ return done();
460
+ }
461
+ s.setPinned(pinned.toJSON());
462
+ process.stdout.write(theme.ok(`✔ pinned ${added.path} (${added.tokens} tokens)\n`));
463
+ return done();
464
+ }
465
+ case 'unpin': {
466
+ const pinned = PinnedContext.fromJSON(s.state.pinned);
467
+ if (!arg) {
468
+ process.stdout.write(theme.warn('usage: /unpin <path|--all>\n'));
469
+ return done();
470
+ }
471
+ if (arg === '--all') {
472
+ const totalTokens = pinned.totalTokens();
473
+ const removed = pinned.clear();
474
+ s.setPinned(pinned.toJSON());
475
+ process.stdout.write(theme.ok(`✔ cleared ${removed} pinned file${removed === 1 ? '' : 's'} (${totalTokens} tokens)\n`));
476
+ return done();
477
+ }
478
+ const target = path.resolve(s.state.cwd, arg);
479
+ const removedFile = pinned
480
+ .list()
481
+ .find((file) => path.normalize(file.path) === path.normalize(target));
482
+ if (!pinned.remove(target)) {
483
+ process.stdout.write(theme.warn(`not pinned: ${target}\n`));
484
+ return done();
485
+ }
486
+ s.setPinned(pinned.toJSON());
487
+ process.stdout.write(theme.ok(`✔ unpinned ${target} (${removedFile?.tokens ?? 0} tokens)\n`));
488
+ return done();
489
+ }
490
+ case 'read-only':
491
+ case 'ro': {
492
+ if (!arg || arg === 'list') {
493
+ process.stdout.write(formatReadOnlyFiles(getReadOnlyFiles()));
494
+ return done();
495
+ }
496
+ const [subcommand = '', ...subArgs] = rest;
497
+ if (subcommand === 'drop') {
498
+ const target = subArgs.join(' ').trim();
499
+ if (!target) {
500
+ process.stdout.write(theme.warn('usage: /read-only drop <path>\n'));
501
+ return done();
502
+ }
503
+ const resolved = path.resolve(s.state.cwd, target);
504
+ if (!removeReadOnly(resolved)) {
505
+ process.stdout.write(theme.warn(`not read-only: ${resolved}\n`));
506
+ return done();
507
+ }
508
+ process.stdout.write(theme.ok(`✔ removed read-only file ${resolved}\n`));
509
+ return done();
510
+ }
511
+ try {
512
+ const added = addReadOnly(arg);
513
+ process.stdout.write(theme.ok(`✔ read-only ${added}\n`));
514
+ }
515
+ catch (error) {
516
+ process.stdout.write(theme.err(`${error instanceof Error ? error.message : String(error)}\n`));
517
+ }
518
+ return done();
519
+ }
520
+ case 'every': {
521
+ const [interval = '', ...promptParts] = rest;
522
+ const prompt = promptParts.join(' ').trim();
523
+ if (!interval || !prompt) {
524
+ process.stdout.write(theme.warn('usage: /every <interval> <prompt>\n'));
525
+ return done();
526
+ }
527
+ try {
528
+ const task = scheduleRecurring(interval, prompt);
529
+ process.stdout.write(theme.ok(`✔ scheduled recurring task ${task.id} (${interval})\n`));
530
+ }
531
+ catch (error) {
532
+ process.stdout.write(theme.err(`${error instanceof Error ? error.message : String(error)}\n`));
533
+ }
534
+ return done();
535
+ }
536
+ case 'after': {
537
+ const [delay = '', ...promptParts] = rest;
538
+ const prompt = promptParts.join(' ').trim();
539
+ if (!delay || !prompt) {
540
+ process.stdout.write(theme.warn('usage: /after <delay> <prompt>\n'));
541
+ return done();
542
+ }
543
+ try {
544
+ const task = scheduleOnce(delay, prompt);
545
+ process.stdout.write(theme.ok(`✔ scheduled one-shot task ${task.id} (${delay})\n`));
546
+ }
547
+ catch (error) {
548
+ process.stdout.write(theme.err(`${error instanceof Error ? error.message : String(error)}\n`));
549
+ }
550
+ return done();
551
+ }
552
+ case 'schedule': {
553
+ if (rest[0] === 'cancel') {
554
+ const id = rest.slice(1).join(' ').trim();
555
+ if (!id) {
556
+ process.stdout.write(theme.warn('usage: /schedule cancel <id>\n'));
557
+ return done();
558
+ }
559
+ process.stdout.write(cancelSchedule(id)
560
+ ? theme.ok(`✔ cancelled schedule ${id}\n`)
561
+ : theme.warn(`schedule not found: ${id}\n`));
562
+ return done();
563
+ }
564
+ process.stdout.write(formatScheduledTasks(listScheduled()));
565
+ return done();
566
+ }
567
+ case 'tokens':
568
+ process.stdout.write(tokensCommand(s));
569
+ return done();
570
+ case 'editor': {
571
+ const content = await openEditor();
572
+ if (!content) {
573
+ process.stdout.write(theme.warn('editor canceled.\n'));
574
+ return done();
575
+ }
576
+ return done(false, content);
577
+ }
578
+ case 'reasoning': {
579
+ if (!arg) {
580
+ process.stdout.write(formatReasoningConfig());
581
+ return done();
582
+ }
583
+ const level = arg.toLowerCase();
584
+ if (level !== 'low' && level !== 'medium' && level !== 'high' && level !== 'max') {
585
+ process.stdout.write(theme.warn('usage: /reasoning [low|medium|high|max]\n'));
586
+ return done();
587
+ }
588
+ setReasoningEffort(level);
589
+ process.stdout.write(theme.ok(`✔ reasoning effort → ${level}\n`));
590
+ return done();
591
+ }
592
+ case 'think-tokens': {
593
+ if (!arg) {
594
+ process.stdout.write(formatReasoningConfig());
595
+ return done();
596
+ }
597
+ try {
598
+ const budget = parseTokenBudget(arg);
599
+ if (budget === 0) {
600
+ setThinkTokens(null);
601
+ process.stdout.write(theme.ok('✔ think token budget disabled\n'));
602
+ return done();
603
+ }
604
+ setThinkTokens(budget);
605
+ process.stdout.write(theme.ok(`✔ think token budget → ${budget} tokens\n`));
606
+ }
607
+ catch (error) {
608
+ process.stdout.write(theme.err(`${error?.message || error}\n`));
609
+ }
610
+ return done();
611
+ }
612
+ case 'compact': {
613
+ const summary = await compactSession(s, ctx.abort.signal);
614
+ s.compactInto(summary);
615
+ process.stdout.write(theme.ok('\n✔ history compacted.\n'));
616
+ return done();
617
+ }
618
+ case 'sessions': {
619
+ const id = await pickSession();
620
+ if (!id) {
621
+ process.stdout.write(theme.warn('No saved session selected.\n'));
622
+ }
623
+ else {
624
+ Object.assign(ctx.session, Session.load(id));
625
+ process.stdout.write(theme.ok(`✔ resumed session ${id}\n`));
626
+ }
627
+ return done();
628
+ }
629
+ case 'cloud': {
630
+ const cloud = new CloudSession({
631
+ endpoint: config.endpoint,
632
+ apiKey: config.token,
633
+ });
634
+ const [subcommand = '', ...subArgs] = rest;
635
+ const action = subcommand.toLowerCase();
636
+ if (!action) {
637
+ process.stdout.write(formatCloudUsage(cloud.getConnectedSessionId()));
638
+ return done();
639
+ }
640
+ if (action === 'create') {
641
+ const name = subArgs.join(' ').trim() || undefined;
642
+ const created = await cloud.create({ name });
643
+ await cloud.sync(created.id, s);
644
+ process.stdout.write(theme.ok(`✔ cloud session ${created.id} created and synced\n`));
645
+ return done();
646
+ }
647
+ if (action === 'connect') {
648
+ const targetId = subArgs[0]?.trim();
649
+ if (!targetId) {
650
+ process.stdout.write(theme.warn('usage: /cloud connect <id>\n'));
651
+ return done();
652
+ }
653
+ const connected = await cloud.connect(targetId);
654
+ process.stdout.write(theme.ok(`✔ connected cloud session ${connected.id}\n`));
655
+ return done();
656
+ }
657
+ if (action === 'list') {
658
+ process.stdout.write(formatCloudSessions(await cloud.list()));
659
+ return done();
660
+ }
661
+ if (action === 'destroy') {
662
+ const targetId = subArgs[0]?.trim();
663
+ if (!targetId) {
664
+ process.stdout.write(theme.warn('usage: /cloud destroy <id>\n'));
665
+ return done();
666
+ }
667
+ const destroyed = await cloud.destroy(targetId);
668
+ process.stdout.write(destroyed
669
+ ? theme.ok(`✔ destroyed cloud session ${targetId}\n`)
670
+ : theme.warn(`cloud session not found: ${targetId}\n`));
671
+ return done();
672
+ }
673
+ if (action === 'sync') {
674
+ const connectedId = cloud.getConnectedSessionId();
675
+ if (!connectedId) {
676
+ process.stdout.write(theme.warn('No cloud session connected. Use /cloud create or /cloud connect <id>.\n'));
677
+ return done();
678
+ }
679
+ const synced = await cloud.sync(connectedId, s);
680
+ process.stdout.write(theme.ok(`✔ synced cloud session ${synced.id}\n`));
681
+ return done();
682
+ }
683
+ process.stdout.write(formatCloudUsage(cloud.getConnectedSessionId()));
684
+ return done();
685
+ }
686
+ case 'export': {
687
+ const [formatArg, ...pathParts] = rest;
688
+ const format = formatArg === 'json' ? 'json' : 'md';
689
+ const outPath = formatArg === 'md' || formatArg === 'json'
690
+ ? pathParts.join(' ').trim() || undefined
691
+ : rest.join(' ').trim() || undefined;
692
+ const written = await exportSession(s, format, outPath);
693
+ process.stdout.write(theme.ok(`✔ exported ${written}\n`));
694
+ return done();
695
+ }
696
+ case 'share':
697
+ process.stdout.write(shareCommand(rest, s));
698
+ return done();
699
+ case 'paste': {
700
+ try {
701
+ if ((rest[0] ?? '').toLowerCase() === 'image') {
702
+ const clipboard = await readClipboard();
703
+ if (clipboard.type !== 'image') {
704
+ process.stdout.write(theme.warn('clipboard does not contain an image\n'));
705
+ return done();
706
+ }
707
+ process.stdout.write(theme.ok(`✔ pasted clipboard image ${clipboard.content}\n`));
708
+ return done(false, `"${clipboard.content}"`);
709
+ }
710
+ const clipboard = await readClipboard();
711
+ if (clipboard.type !== 'text' || !clipboard.content.trim()) {
712
+ process.stdout.write(theme.warn('clipboard is empty, unavailable, or not text\n'));
713
+ return done();
714
+ }
715
+ process.stdout.write(theme.dim('pasted clipboard text into the next prompt\n'));
716
+ return done(false, clipboard.content);
717
+ }
718
+ catch (error) {
719
+ process.stdout.write(theme.err(`clipboard: ${error?.message || error}\n`));
720
+ return done();
721
+ }
722
+ }
723
+ case 'copy': {
724
+ if (!arg) {
725
+ process.stdout.write(theme.warn('usage: /copy <text>\n'));
726
+ return done();
727
+ }
728
+ try {
729
+ await copyTextToClipboard(arg);
730
+ process.stdout.write(theme.ok('✔ copied text to clipboard\n'));
731
+ }
732
+ catch (error) {
733
+ process.stdout.write(theme.err(`clipboard: ${error?.message || error}\n`));
734
+ }
735
+ return done();
736
+ }
737
+ case 'copy-context': {
738
+ try {
739
+ const scope = (rest[0] ?? '').toLowerCase() === 'last' ? 'last' : 'all';
740
+ const selectedMessages = scope === 'last' ? selectLastExchange(s.state.messages) : s.state.messages;
741
+ const summary = buildClipboardSystemSummary(s);
742
+ const fileContext = buildClipboardFileContext(s);
743
+ const synthetic = [];
744
+ if (summary)
745
+ synthetic.push({ role: 'system', content: summary });
746
+ if (fileContext)
747
+ synthetic.push({ role: 'system', content: fileContext });
748
+ const messages = [...synthetic, ...selectedMessages];
749
+ await copyContextToClipboard(messages);
750
+ process.stdout.write(theme.ok(`✔ copied ${messages.length} context message${messages.length === 1 ? '' : 's'} to clipboard\n`));
751
+ }
752
+ catch (error) {
753
+ process.stdout.write(theme.err(`clipboard: ${error?.message || error}\n`));
754
+ }
755
+ return done();
756
+ }
757
+ case 'handoff': {
758
+ const [subcommand = '', ...subArgs] = rest;
759
+ const action = subcommand.toLowerCase();
760
+ if (!action) {
761
+ process.stdout.write('usage: /handoff export [path]\n' +
762
+ ' /handoff import <path>\n' +
763
+ ' /handoff preview <path>\n');
764
+ return done();
765
+ }
766
+ if (action === 'export') {
767
+ const outputPath = subArgs.join(' ').trim() || undefined;
768
+ const bundle = createHandoff(s);
769
+ const written = exportHandoffFile(bundle, outputPath);
770
+ process.stdout.write(theme.ok(`✔ exported handoff ${written}\n`));
771
+ return done();
772
+ }
773
+ if (action === 'preview') {
774
+ const target = subArgs.join(' ').trim();
775
+ if (!target) {
776
+ process.stdout.write(theme.warn('usage: /handoff preview <path>\n'));
777
+ return done();
778
+ }
779
+ const bundle = importHandoffFile(path.resolve(s.state.cwd, target));
780
+ process.stdout.write(previewHandoff(bundle));
781
+ return done();
782
+ }
783
+ if (action === 'import') {
784
+ const target = subArgs.join(' ').trim();
785
+ if (!target) {
786
+ process.stdout.write(theme.warn('usage: /handoff import <path>\n'));
787
+ return done();
788
+ }
789
+ const bundle = importHandoffFile(path.resolve(s.state.cwd, target));
790
+ const imported = receiveHandoff(bundle);
791
+ Object.assign(ctx.session, imported);
792
+ config.cwd = imported.state.cwd;
793
+ process.stdout.write(theme.ok(`✔ imported handoff as ${imported.state.id}\n`));
794
+ return done();
795
+ }
796
+ process.stdout.write(theme.warn(`unknown handoff subcommand: ${action}\n`));
797
+ return done();
798
+ }
799
+ case 'plan': {
800
+ const next = s.state.mode === 'plan' ? 'ask' : 'plan';
801
+ s.setMode(next);
802
+ process.stdout.write(theme.ok(`✔ mode → ${next}\n`));
803
+ return done();
804
+ }
805
+ case 'edit-format': {
806
+ if (!arg) {
807
+ process.stdout.write(theme.dim(`edit format: ${config.editFormat}\n`));
808
+ return done();
809
+ }
810
+ if (arg !== 'whole' && arg !== 'diff') {
811
+ process.stdout.write(theme.warn('usage: /edit-format [whole|diff]\n'));
812
+ return done();
813
+ }
814
+ config.editFormat = arg;
815
+ process.stdout.write(theme.ok(`✔ edit format → ${config.editFormat}\n`));
816
+ return done();
817
+ }
818
+ case 'autopilot': {
819
+ if (!arg) {
820
+ const next = !s.state.autopilotEnabled;
821
+ s.setAutopilotEnabled(next);
822
+ process.stdout.write(theme.ok(`✔ autopilot → ${next ? 'on' : 'off'}\n`));
823
+ return done();
824
+ }
825
+ await runAutopilot(arg, { session: s, signal: ctx.abort.signal });
826
+ return done();
827
+ }
828
+ case 'goal': {
829
+ const action = (rest[0] ?? '').toLowerCase();
830
+ if (!arg) {
831
+ process.stdout.write(theme.warn('usage: /goal <description> | /goal status | /goal abort\n'));
832
+ return done();
833
+ }
834
+ if (action === 'status') {
835
+ process.stdout.write(formatGoalRunStatus(activeGoalRun ?? lastGoalRun));
836
+ return done();
837
+ }
838
+ if (action === 'abort') {
839
+ if (!activeGoalRun) {
840
+ process.stdout.write(theme.warn('No active goal run.\n'));
841
+ return done();
842
+ }
843
+ activeGoalRun.abortController.abort();
844
+ process.stdout.write(theme.ok(`✔ aborting goal: ${activeGoalRun.goal.description}\n`));
845
+ return done();
846
+ }
847
+ if (activeGoalRun) {
848
+ process.stdout.write(theme.warn(`goal already running: ${activeGoalRun.goal.description} (use /goal status)\n`));
849
+ return done();
850
+ }
851
+ const goal = { description: arg };
852
+ const controller = new AbortController();
853
+ const agent = new GoalDrivenAgent({
854
+ session: s,
855
+ signal: controller.signal,
856
+ });
857
+ const plan = agent.plan(goal);
858
+ const goalRun = {
859
+ goal,
860
+ plan,
861
+ agent,
862
+ startedAt: new Date().toISOString(),
863
+ abortController: controller,
864
+ promise: Promise.resolve({
865
+ goal,
866
+ plan,
867
+ success: false,
868
+ attempts: 0,
869
+ summary: '',
870
+ aborted: false,
871
+ stepResults: [],
872
+ verification: {
873
+ ok: false,
874
+ score: 0,
875
+ issues: [],
876
+ attempts: 0,
877
+ },
878
+ }),
879
+ };
880
+ goalRun.promise = agent
881
+ .execute(plan)
882
+ .then((result) => {
883
+ goalRun.result = result;
884
+ lastGoalRun = goalRun;
885
+ if (activeGoalRun === goalRun) {
886
+ activeGoalRun = null;
887
+ }
888
+ process.stdout.write(`\n${formatGoalCompletion(result)}`);
889
+ return result;
890
+ })
891
+ .catch((error) => {
892
+ const message = error instanceof Error ? error.message : String(error);
893
+ goalRun.error = message;
894
+ const progress = agent.getProgress();
895
+ const fallback = progress.result ??
896
+ {
897
+ goal,
898
+ plan,
899
+ success: false,
900
+ attempts: progress.currentAttempt,
901
+ summary: message,
902
+ aborted: progress.phase === 'aborted',
903
+ stepResults: [],
904
+ verification: progress.verification ?? {
905
+ ok: false,
906
+ score: 0,
907
+ issues: [message],
908
+ attempts: progress.currentAttempt,
909
+ },
910
+ };
911
+ goalRun.result = fallback;
912
+ lastGoalRun = goalRun;
913
+ if (activeGoalRun === goalRun) {
914
+ activeGoalRun = null;
915
+ }
916
+ process.stdout.write(theme.err(`\ngoal failed: ${message}\n`));
917
+ return fallback;
918
+ });
919
+ activeGoalRun = goalRun;
920
+ lastGoalRun = goalRun;
921
+ process.stdout.write(theme.ok(`✔ goal started (${plan.steps.length} steps, ~${plan.estimatedTokens} tokens). Use /goal status or /goal abort.\n`));
922
+ return done();
923
+ }
924
+ case 'commit':
925
+ await commitFromStaged(s, ctx.abort.signal);
926
+ return done();
927
+ case 'pr':
928
+ await prDescription(s, ctx.abort.signal);
929
+ return done();
930
+ case 'review':
931
+ await reviewStaged(s, ctx.abort.signal);
932
+ return done();
933
+ case 'diff-review':
934
+ await reviewDiff(s, rest, ctx.abort.signal);
935
+ return done();
936
+ case 'issue':
937
+ await draftIssue(s, ctx.abort.signal, arg || undefined);
938
+ return done();
939
+ case 'branch':
940
+ await scaffoldBranch(s, ctx.abort.signal, arg);
941
+ return done();
942
+ case 'index':
943
+ await indexCommand(rest);
944
+ return done();
945
+ case 'rag':
946
+ process.stdout.write(await ragCommand(rest, s.state.cwd));
947
+ return done();
948
+ case 'search':
949
+ process.stdout.write(await searchCommand(rest, s.state.cwd));
950
+ return done();
951
+ case 'goto': {
952
+ if (!arg) {
953
+ process.stdout.write(theme.warn('usage: /goto <symbol>\n'));
954
+ return done();
955
+ }
956
+ const definition = goToDefinition(arg, s.state.cwd);
957
+ if (!definition) {
958
+ process.stdout.write(theme.warn(`definition not found: ${arg}\n`));
959
+ return done();
960
+ }
961
+ process.stdout.write(formatNavigationResult('Definition', arg, [definition]));
962
+ return done();
963
+ }
964
+ case 'refs': {
965
+ if (!arg) {
966
+ process.stdout.write(theme.warn('usage: /refs <symbol>\n'));
967
+ return done();
968
+ }
969
+ const references = findReferences(arg, s.state.cwd);
970
+ if (references.length === 0) {
971
+ process.stdout.write(theme.warn(`no references found: ${arg}\n`));
972
+ return done();
973
+ }
974
+ process.stdout.write(formatNavigationResult('References', arg, references));
975
+ return done();
976
+ }
977
+ case 'route':
978
+ process.stdout.write(routeCommand(arg));
979
+ return done();
980
+ case 'undo':
981
+ if (rest[0]?.toLowerCase() === 'status') {
982
+ process.stdout.write(await undoCommand('status'));
983
+ return done();
984
+ }
985
+ if (rest[0]?.toLowerCase() === 'file' || rest[0]?.toLowerCase() === 'journal') {
986
+ process.stdout.write(await undoCommand('undo'));
987
+ return done();
988
+ }
989
+ process.stdout.write(await gitUndo({ cwd: s.state.cwd, hard: rest.includes('--hard') }));
990
+ return done();
991
+ case 'redo':
992
+ process.stdout.write(await undoCommand('redo'));
993
+ return done();
994
+ case 'cost':
995
+ process.stdout.write(costCommand(s));
996
+ return done();
997
+ case 'snippets':
998
+ case 'snippet':
999
+ process.stdout.write(await snippetsCommand(rest));
1000
+ return done();
1001
+ case 'profile':
1002
+ case 'profiles':
1003
+ process.stdout.write(await profileCommand(rest));
1004
+ return done();
1005
+ case 'role':
1006
+ process.stdout.write(handleRoleCommand(rest, roleManager));
1007
+ return done();
1008
+ case 'style':
1009
+ process.stdout.write(await styleCommand(rest, s.state.cwd));
1010
+ return done();
1011
+ case 'conventions':
1012
+ process.stdout.write(conventionsCommand(rest, s.state.cwd));
1013
+ return done();
1014
+ case 'stats':
1015
+ process.stdout.write(statsCommand(arg || undefined));
1016
+ return done();
1017
+ case 'audit': {
1018
+ const audit = new AuditLogger();
1019
+ const [subcommand = '', ...subArgs] = rest;
1020
+ const action = subcommand.toLowerCase();
1021
+ if (!action) {
1022
+ process.stdout.write(formatAuditEntries(audit.getRecent()));
1023
+ return done();
1024
+ }
1025
+ if (action === 'search') {
1026
+ const query = subArgs.join(' ').trim();
1027
+ if (!query) {
1028
+ process.stdout.write(theme.warn('usage: /audit search <query>\n'));
1029
+ return done();
1030
+ }
1031
+ const matches = searchAuditEntries(audit.query(), query);
1032
+ process.stdout.write(formatAuditEntries(matches.slice(-20).reverse(), `Audit search: ${query}`));
1033
+ return done();
1034
+ }
1035
+ if (action === 'stats') {
1036
+ process.stdout.write(formatAuditStats(audit.getStats()));
1037
+ return done();
1038
+ }
1039
+ if (action === 'export') {
1040
+ const requested = subArgs.join(' ').trim();
1041
+ const target = requested
1042
+ ? path.resolve(s.state.cwd, requested)
1043
+ : path.join(s.state.cwd, 'audit-export.log');
1044
+ const format = target.toLowerCase().endsWith('.json') ? 'json' : 'jsonl';
1045
+ const written = audit.export(target, format);
1046
+ process.stdout.write(theme.ok(`✔ exported audit log ${written}\n`));
1047
+ return done();
1048
+ }
1049
+ process.stdout.write([
1050
+ theme.warn('usage: /audit'),
1051
+ ' /audit search <query>',
1052
+ ' /audit stats',
1053
+ ' /audit export [path]',
1054
+ ` log: ${auditLogPath()}`,
1055
+ '',
1056
+ ].join('\n'));
1057
+ return done();
1058
+ }
1059
+ case 'metrics':
1060
+ process.stdout.write(metricsCommand(ctx.metrics ?? new MetricsCollector()));
1061
+ return done();
1062
+ case 'explain': {
1063
+ if (!arg) {
1064
+ process.stdout.write(theme.warn('usage: /explain <path>\n'));
1065
+ return done();
1066
+ }
1067
+ const payload = buildExplain(arg, s.state.cwd);
1068
+ process.stdout.write(`${theme.brand('Explain prompt')} ${theme.dim(payload.path)}\n\n${payload.prompt}\n`);
1069
+ return done();
1070
+ }
1071
+ case 'suggest':
1072
+ process.stdout.write(await suggestCommand(arg, s, ctx.abort.signal));
1073
+ return done();
1074
+ case 'summary': {
1075
+ const payload = buildSummary(s.state.cwd);
1076
+ process.stdout.write(`${theme.brand('Summary prompt')} ${theme.dim(payload.projectName)}\n\n${payload.prompt}\n`);
1077
+ return done();
1078
+ }
1079
+ case 'compare':
1080
+ process.stdout.write(compareCommand(rest, s.state.cwd));
1081
+ return done();
1082
+ case 'env':
1083
+ process.stdout.write(envCommand(rest));
1084
+ return done();
1085
+ case 'template':
1086
+ process.stdout.write(templateCommand(rest));
1087
+ return done();
1088
+ case 'readme':
1089
+ process.stdout.write(readmeCommand(rest, s.state.cwd));
1090
+ return done();
1091
+ case 'changelog': {
1092
+ const payload = await buildChangelogPrompt(rest, s.state.cwd);
1093
+ const label = payload.fromRef && payload.toRef ? `${payload.fromRef}..${payload.toRef}` : s.state.cwd;
1094
+ process.stdout.write(`${theme.brand('Changelog prompt')} ${theme.dim(label)}\n\n${payload.prompt}\n`);
1095
+ return done();
1096
+ }
1097
+ case 'release':
1098
+ process.stdout.write(await releaseCommand(rest, s.state.cwd));
1099
+ return done();
1100
+ case 'fix': {
1101
+ const payload = buildFixPrompt(arg);
1102
+ process.stdout.write(`${theme.brand('Fix prompt')}\n\n${payload.prompt}\n`);
1103
+ return done();
1104
+ }
1105
+ case 'heal': {
1106
+ const parsed = parseHealArgs(rest);
1107
+ if ('error' in parsed) {
1108
+ process.stdout.write(theme.warn(`${parsed.error}\n`));
1109
+ return done();
1110
+ }
1111
+ const { SelfHealingBuilder } = await import('../agents/self-heal.js');
1112
+ const builder = new SelfHealingBuilder(s.state.cwd);
1113
+ process.stdout.write(theme.dim(`healing build in ${s.state.cwd}\n`));
1114
+ const result = await builder.healAndRetry(parsed.maxAttempts);
1115
+ process.stdout.write(formatHealResult(result));
1116
+ return done();
1117
+ }
1118
+ case 'lint':
1119
+ process.stdout.write(lintCommand(s.state.cwd));
1120
+ return done();
1121
+ case 'test':
1122
+ process.stdout.write(testCommand(s.state.cwd));
1123
+ return done();
1124
+ case 'auto-lint': {
1125
+ const next = resolveToggle(arg, config.autoLint);
1126
+ if (next === undefined) {
1127
+ process.stdout.write(theme.warn('usage: /auto-lint [on|off]\n'));
1128
+ return done();
1129
+ }
1130
+ config.autoLint = next;
1131
+ const detected = detectAutoLintCommand(s.state.cwd);
1132
+ process.stdout.write(theme.ok(`✔ auto-lint → ${next ? 'on' : 'off'}${next && detected ? ` (${detected})` : ''}\n`));
1133
+ return done();
1134
+ }
1135
+ case 'auto-test': {
1136
+ const next = resolveToggle(arg, config.autoTest);
1137
+ if (next === undefined) {
1138
+ process.stdout.write(theme.warn('usage: /auto-test [on|off]\n'));
1139
+ return done();
1140
+ }
1141
+ config.autoTest = next;
1142
+ const detected = detectAutoTestCommand(s.state.cwd);
1143
+ process.stdout.write(theme.ok(`✔ auto-test → ${next ? 'on' : 'off'}${next && detected ? ` (${detected})` : ''}\n`));
1144
+ return done();
1145
+ }
1146
+ case 'auto-fix': {
1147
+ const next = resolveToggle(arg, config.autoFix);
1148
+ if (next === undefined) {
1149
+ process.stdout.write(theme.warn('usage: /auto-fix [on|off]\n'));
1150
+ return done();
1151
+ }
1152
+ config.autoFix = next;
1153
+ process.stdout.write(theme.ok(`✔ auto-fix → ${next ? 'on' : 'off'}\n`));
1154
+ return done();
1155
+ }
1156
+ case 'tdd':
1157
+ if (!arg) {
1158
+ process.stdout.write(theme.warn('usage: /tdd <description>\n /tdd status\n'));
1159
+ return done();
1160
+ }
1161
+ if (arg.toLowerCase() === 'status') {
1162
+ process.stdout.write(formatTddStatus(lastTddResult));
1163
+ return done();
1164
+ }
1165
+ lastTddResult = new TDDAgent(s.state.cwd).fullCycle(buildTddSpec(arg));
1166
+ process.stdout.write(formatTddCycle(lastTddResult));
1167
+ return done();
1168
+ case 'doctor':
1169
+ process.stdout.write(formatDiagnostics(runDiagnostics()));
1170
+ return done();
1171
+ case 'todo':
1172
+ case 'todos': {
1173
+ const todoState = s.state;
1174
+ const todos = TodoList.fromJSON(todoState.todos);
1175
+ process.stdout.write(todoCommand(rest, todos));
1176
+ s.setTodos(todos.toJSON());
1177
+ return done();
1178
+ }
1179
+ case 'task':
1180
+ process.stdout.write(taskCommand(rest, backgroundTaskManager));
1181
+ return done();
1182
+ case 'tasks':
1183
+ process.stdout.write(taskCommand(['list'], backgroundTaskManager));
1184
+ return done();
1185
+ case 'deps':
1186
+ process.stdout.write(depsCommand(s.state.cwd));
1187
+ return done();
1188
+ case 'init': {
1189
+ const force = rest.includes('--force');
1190
+ process.stdout.write(formatInitResult(initProject(s.state.cwd, { force })));
1191
+ return done();
1192
+ }
1193
+ case 'security':
1194
+ process.stdout.write(securityCommand(s.state.cwd));
1195
+ return done();
1196
+ case 'proxy':
1197
+ process.stdout.write(await proxyCommand(rest));
1198
+ return done();
1199
+ case 'filter':
1200
+ process.stdout.write(handleFilterSlashCommand(s.state.cwd, arg, rest));
1201
+ return done();
1202
+ case 'retention':
1203
+ process.stdout.write(retentionCommand(rest));
1204
+ return done();
1205
+ case 'dead-code': {
1206
+ const scanRoot = arg ? path.resolve(s.state.cwd, arg) : s.state.cwd;
1207
+ const report = new DeadCodeDetector().scan(scanRoot);
1208
+ process.stdout.write(formatDeadCodeReport(scanRoot, report));
1209
+ return done();
1210
+ }
1211
+ case 'refactor':
1212
+ process.stdout.write(refactorCommand(rest, s.state.cwd));
1213
+ return done();
1214
+ case 'stacktrace': {
1215
+ if (!arg) {
1216
+ process.stdout.write(theme.warn('usage: /stacktrace <stack-trace text>\n'));
1217
+ return done();
1218
+ }
1219
+ const trace = parseStackTrace(arg);
1220
+ const analysis = analyzeStackTrace(trace);
1221
+ process.stdout.write(formatStackTraceSummary(trace, analysis));
1222
+ const prompt = [
1223
+ 'You are diagnosing a stack trace for a developer.',
1224
+ 'Use the structured analysis first, then the raw trace.',
1225
+ 'Explain the most likely root cause, identify the best user-code frame to inspect next, and suggest 2-3 fixes ranked by likelihood.',
1226
+ 'Keep the answer practical and specific to the failing code path.',
1227
+ '',
1228
+ `Error type: ${trace.type}`,
1229
+ `Error message: ${trace.error}`,
1230
+ '',
1231
+ formatForLLM(analysis),
1232
+ '',
1233
+ 'Raw stack trace:',
1234
+ trace.raw,
1235
+ ].join('\n');
1236
+ return done(false, prompt);
1237
+ }
1238
+ case 'bookmark':
1239
+ case 'bookmarks': {
1240
+ const result = bookmarkCommand(s, rest);
1241
+ process.stdout.write(result.message.endsWith('\n') ? result.message : `${result.message}\n`);
1242
+ if (result.rewindTo !== undefined) {
1243
+ s.state.messages.length = Math.min(s.state.messages.length, result.rewindTo + 1);
1244
+ const save = s.save;
1245
+ if (typeof save === 'function')
1246
+ save.call(s);
1247
+ process.stdout.write(theme.ok(`✔ rewound to message ${result.rewindTo}\n`));
1248
+ }
1249
+ return done();
1250
+ }
1251
+ case 'alias':
1252
+ process.stdout.write(aliasCommand(rest));
1253
+ return done();
1254
+ case 'skill':
1255
+ process.stdout.write(skillCommand(rest));
1256
+ return done();
1257
+ case 'history':
1258
+ process.stdout.write(historyCommand(rest, s));
1259
+ return done();
1260
+ case 'stash':
1261
+ process.stdout.write(stashCommand(rest, s));
1262
+ return done();
1263
+ case 'notify': {
1264
+ const output = await notifyCommand(rest);
1265
+ if (output) {
1266
+ process.stdout.write(`${output}\n`);
1267
+ }
1268
+ return done();
1269
+ }
1270
+ case 'explain-shell': {
1271
+ const payload = explainShellCommand(arg);
1272
+ process.stdout.write(`${theme.brand('Explain-shell prompt')} ${theme.dim(payload.command)}\n\n${payload.prompt}\n`);
1273
+ return done();
1274
+ }
1275
+ case 'generate': {
1276
+ if (!arg) {
1277
+ process.stdout.write(theme.warn('usage: /generate <goal>\n'));
1278
+ return done();
1279
+ }
1280
+ const payload = buildGeneratePrompt(arg);
1281
+ process.stdout.write(`${theme.brand('Generate prompt')} ${theme.dim(payload.shell)}\n\n${payload.prompt}\n`);
1282
+ return done();
1283
+ }
1284
+ case 'actions':
1285
+ process.stdout.write(actionsCommand(rest, s.state.cwd));
1286
+ return done();
1287
+ case 'codegen':
1288
+ process.stdout.write(codegenCommand(rest, s.state.cwd));
1289
+ return done();
1290
+ case 'multi': {
1291
+ const cfg = buildMultiConfig(rest);
1292
+ if ('error' in cfg) {
1293
+ process.stdout.write(theme.warn(cfg.error + '\n'));
1294
+ }
1295
+ else {
1296
+ process.stdout.write(theme.dim(`multi-model: ${cfg.models.join(', ')} (maxTokens=${cfg.maxTokens})\n`));
1297
+ }
1298
+ return done();
1299
+ }
1300
+ case 'agent':
1301
+ process.stdout.write(agentCommand(rest, s.state.cwd));
1302
+ return done();
1303
+ case 'parallel': {
1304
+ const spec = parseParallelSpec(arg);
1305
+ if ('error' in spec) {
1306
+ process.stdout.write(theme.warn(`${spec.error}\n`));
1307
+ return done();
1308
+ }
1309
+ const runner = new ParallelAgentRunner({
1310
+ model: s.state.model,
1311
+ concurrencyLimit: spec.concurrencyLimit,
1312
+ timeoutMs: spec.timeoutMs,
1313
+ onProgress: (event) => {
1314
+ if (event.status === 'started') {
1315
+ process.stdout.write(theme.dim(`→ ${event.name} [${String(event.type)}] ${event.completed}/${event.total}\n`));
1316
+ return;
1317
+ }
1318
+ if (event.status === 'success' || event.status === 'error') {
1319
+ const marker = event.status === 'success' ? theme.ok('✔') : theme.err('✖');
1320
+ const duration = event.result ? formatDuration(event.result.duration) : '0ms';
1321
+ process.stdout.write(`${marker} ${event.name} ${theme.dim(`(${duration})`)}\n`);
1322
+ }
1323
+ },
1324
+ });
1325
+ process.stdout.write(theme.dim(`running ${spec.agents.length} parallel agent${spec.agents.length === 1 ? '' : 's'} ` +
1326
+ `(concurrency=${runner.concurrencyLimit}, timeout=${runner.timeoutMs}ms)\n`));
1327
+ const result = await runner.runParallel(spec.agents);
1328
+ process.stdout.write(formatParallelResults(result));
1329
+ return done();
1330
+ }
1331
+ case 'explore': {
1332
+ if (!arg) {
1333
+ process.stdout.write(exploreCommand(rest, s.state.cwd));
1334
+ return done();
1335
+ }
1336
+ process.stdout.write(theme.dim(`exploring ${s.state.cwd}\n`));
1337
+ return done(false, exploreCommand(rest, s.state.cwd));
1338
+ }
1339
+ case 'trigger':
1340
+ case 'triggers':
1341
+ process.stdout.write(await runTriggerCommand(rest, s.state.cwd));
1342
+ return done();
1343
+ case 'watch':
1344
+ process.stdout.write(watchCommand(rest));
1345
+ return done();
1346
+ case 'web': {
1347
+ const [rawUrl] = rest;
1348
+ const focus = rawUrl ? arg.slice(rawUrl.length).trim() : '';
1349
+ if (!rawUrl) {
1350
+ process.stdout.write(theme.warn('usage: /web <url> [focus instructions]\n'));
1351
+ return done();
1352
+ }
1353
+ try {
1354
+ const parsedUrl = validateWebUrl(rawUrl);
1355
+ const result = await fetchAndConvert(parsedUrl.toString());
1356
+ const bytes = Buffer.byteLength(result.markdown, 'utf8');
1357
+ const content = buildWebContextMessage(parsedUrl.toString(), result.markdown, focus);
1358
+ s.push({ role: 'user', content });
1359
+ process.stdout.write([
1360
+ `${theme.brand('Web context added')} ${theme.dim(parsedUrl.toString())}`,
1361
+ ` title: ${result.title}`,
1362
+ ` bytes: ${bytes}`,
1363
+ ` tokens: ${result.tokens}`,
1364
+ focus ? ` focus: ${focus}` : '',
1365
+ '',
1366
+ ]
1367
+ .filter(Boolean)
1368
+ .join('\n'));
1369
+ }
1370
+ catch (error) {
1371
+ const message = error instanceof Error ? error.message : String(error);
1372
+ process.stdout.write(theme.err(`${message}\n`));
1373
+ }
1374
+ return done();
1375
+ }
1376
+ case 'bridge':
1377
+ process.stdout.write(await bridgeCommand(rest));
1378
+ return done();
1379
+ case 'error-watch':
1380
+ process.stdout.write(errorWatchCommand(rest));
1381
+ return done();
1382
+ case 'memory':
1383
+ process.stdout.write(memoryCommand(rest, s.state.cwd));
1384
+ return done();
1385
+ case 'corrections':
1386
+ process.stdout.write(correctionsCommand(rest));
1387
+ return done();
1388
+ case 'team-memory':
1389
+ process.stdout.write(teamMemoryCommand(rest, s.state.cwd));
1390
+ return done();
1391
+ case 'repo':
1392
+ process.stdout.write(await repoCommand(rest, {
1393
+ cwd: s.state.cwd,
1394
+ onSwitch: (repo) => {
1395
+ config.cwd = repo.path;
1396
+ s.setCwd(repo.path);
1397
+ },
1398
+ }));
1399
+ return done();
1400
+ case 'space':
1401
+ process.stdout.write(spaceCommand(rest, {
1402
+ cwd: s.state.cwd,
1403
+ onSwitch: (space) => {
1404
+ config.cwd = space.rootPath;
1405
+ s.setCwd(space.rootPath);
1406
+ if (space.config.model) {
1407
+ s.setModel(space.config.model);
1408
+ }
1409
+ s.setSystemPrompt(space.config.systemPrompt);
1410
+ },
1411
+ }));
1412
+ return done();
1413
+ case 'doc':
1414
+ process.stdout.write(docCommand(rest, s.state.cwd));
1415
+ return done();
1416
+ case 'diagram':
1417
+ process.stdout.write(diagramCommand(rest, s.state.cwd));
1418
+ return done();
1419
+ case 'extension':
1420
+ case 'extensions':
1421
+ process.stdout.write(extensionCommand(rest, s.state.cwd));
1422
+ return done();
1423
+ case 'sandbox':
1424
+ process.stdout.write(await sandboxCommand(rest, s.state.cwd));
1425
+ return done();
1426
+ case 'serve':
1427
+ process.stdout.write(await serveCommand(rest));
1428
+ return done();
1429
+ case 'worktree':
1430
+ process.stdout.write(worktreeCommand(rest, s.state.cwd));
1431
+ return done();
1432
+ case 'cloud-routine':
1433
+ process.stdout.write(await cloudRoutineCommand(arg));
1434
+ return done();
1435
+ case 'voice':
1436
+ process.stdout.write(await voiceCommand(rest));
1437
+ return done();
1438
+ case 'plugin':
1439
+ case 'plugins':
1440
+ process.stdout.write(await pluginCommand(rest));
1441
+ return done();
1442
+ case 'workflow':
1443
+ case 'workflows':
1444
+ process.stdout.write(await workflowCommand(rest, s.state.cwd));
1445
+ return done();
1446
+ case 'acp':
1447
+ process.stdout.write(await acpCommand({ subcommand: rest[0], args: rest.slice(1) }));
1448
+ return done();
1449
+ case 'exit':
1450
+ case 'quit':
1451
+ ctx.exit();
1452
+ return done();
1453
+ default:
1454
+ process.stdout.write(theme.warn(`unknown command: /${cmd} (try /help)\n`));
1455
+ return done();
1456
+ }
1457
+ }
1458
+ function done(consumed = true, forwardInput, turnMode = null) {
1459
+ return {
1460
+ handled: true,
1461
+ consumed,
1462
+ ...(forwardInput !== undefined ? { forwardInput } : {}),
1463
+ ...(turnMode ? { turnMode } : {}),
1464
+ };
1465
+ }
1466
+ async function resolveFeedbackInput(rest, rawArg) {
1467
+ const quickType = (rest[0] ?? '').toLowerCase();
1468
+ if (quickType === 'bug' || quickType === 'feature' || quickType === 'praise') {
1469
+ const text = rawArg.slice(quickType.length).trim();
1470
+ return text ? { type: quickType, text } : null;
1471
+ }
1472
+ if (rawArg)
1473
+ return null;
1474
+ const type = await select({
1475
+ message: 'Feedback type',
1476
+ choices: [
1477
+ { name: 'Bug report', value: 'bug' },
1478
+ { name: 'Feature request', value: 'feature' },
1479
+ { name: 'Praise', value: 'praise' },
1480
+ ],
1481
+ }).catch(() => null);
1482
+ if (!type)
1483
+ return null;
1484
+ const text = await input({
1485
+ message: 'Describe your feedback',
1486
+ validate: (value) => (value.trim() ? true : 'Feedback is required'),
1487
+ }).catch(() => '');
1488
+ return text.trim() ? { type, text: text.trim() } : null;
1489
+ }
1490
+ function resolveToggle(value, current) {
1491
+ const normalized = value.trim().toLowerCase();
1492
+ if (!normalized)
1493
+ return !current;
1494
+ if (normalized === 'on')
1495
+ return true;
1496
+ if (normalized === 'off')
1497
+ return false;
1498
+ return undefined;
1499
+ }
1500
+ function buildClipboardSystemSummary(session) {
1501
+ const lines = [
1502
+ 'System prompt summary',
1503
+ `Mode: ${session.state.mode}`,
1504
+ `Model: ${session.state.model}`,
1505
+ `Working directory: ${session.state.cwd}`,
1506
+ ];
1507
+ if (session.state.systemPrompt?.trim()) {
1508
+ lines.push(`Custom system prompt: ${truncateMiddle(session.state.systemPrompt.trim(), 400)}`);
1509
+ }
1510
+ else {
1511
+ lines.push('System prompt source: built-in default');
1512
+ }
1513
+ return lines.join('\n');
1514
+ }
1515
+ function buildClipboardFileContext(session) {
1516
+ const lines = ['File context', `Working directory: ${session.state.cwd}`];
1517
+ const pinned = PinnedContext.fromJSON(session.state.pinned).list();
1518
+ if (pinned.length > 0) {
1519
+ lines.push('Pinned files:');
1520
+ lines.push(...pinned.map((file) => `- ${file.path} (${file.tokens} tokens)`));
1521
+ }
1522
+ const gitContext = session.state.gitContext ?? [];
1523
+ if (gitContext.length > 0) {
1524
+ lines.push('Git context:');
1525
+ lines.push(...gitContext
1526
+ .slice(0, 10)
1527
+ .map((file) => `- ${file.path}${file.status ? ` [${file.status}]` : ''}`));
1528
+ if (gitContext.length > 10)
1529
+ lines.push(`- ... ${gitContext.length - 10} more`);
1530
+ }
1531
+ return lines.length > 2 ? lines.join('\n') : '';
1532
+ }
1533
+ function selectLastExchange(messages) {
1534
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
1535
+ if (messages[index]?.role === 'user')
1536
+ return messages.slice(index);
1537
+ }
1538
+ return messages.slice(-1);
1539
+ }
1540
+ function truncateMiddle(value, maxLength) {
1541
+ if (value.length <= maxLength)
1542
+ return value;
1543
+ const head = Math.ceil((maxLength - 1) / 2);
1544
+ const tail = Math.floor((maxLength - 1) / 2);
1545
+ return `${value.slice(0, head)}…${value.slice(value.length - tail)}`;
1546
+ }
1547
+ async function runSlashShellCommand(command, cwd) {
1548
+ if (!shellCommandAllowed(command, loadPolicy(config.cwd))) {
1549
+ throw new Error('policy denied command execution');
1550
+ }
1551
+ assertSandbox(cwd, config.cwd);
1552
+ const safety = checkCommandSafety(command);
1553
+ if (safety.level === 'critical') {
1554
+ // eslint-disable-next-line no-control-regex
1555
+ throw new Error(formatSafetyWarning(safety).replace(/\x1B\[[0-9;]*m/g, ''));
1556
+ }
1557
+ if (safety.level === 'warn') {
1558
+ process.stdout.write(`${formatSafetyWarning(safety)}\n`);
1559
+ }
1560
+ return new Promise((resolve, reject) => {
1561
+ const isWin = process.platform === 'win32';
1562
+ const shell = isWin ? 'powershell.exe' : 'bash';
1563
+ const args = isWin ? ['-NoProfile', '-Command', command] : ['-lc', command];
1564
+ const child = spawn(shell, args, {
1565
+ cwd,
1566
+ stdio: ['ignore', 'pipe', 'pipe'],
1567
+ windowsHide: true,
1568
+ });
1569
+ let stdout = '';
1570
+ let stderr = '';
1571
+ child.stdout.on('data', (chunk) => {
1572
+ stdout += chunk.toString();
1573
+ });
1574
+ child.stderr.on('data', (chunk) => {
1575
+ stderr += chunk.toString();
1576
+ });
1577
+ child.on('error', reject);
1578
+ child.on('close', (exitCode) => {
1579
+ resolve({ exitCode, stdout, stderr });
1580
+ });
1581
+ });
1582
+ }
1583
+ function formatRunResult(command, result) {
1584
+ const lines = [
1585
+ `${theme.brand('Run')} ${theme.dim(command)}`,
1586
+ ` exit code: ${result.exitCode ?? 'unknown'}`,
1587
+ ];
1588
+ if (result.stdout.trim())
1589
+ lines.push('', result.stdout.trimEnd());
1590
+ if (result.stderr.trim())
1591
+ lines.push('', theme.warn('stderr:'), result.stderr.trimEnd());
1592
+ return `${lines.join('\n')}\n`;
1593
+ }
1594
+ function buildRunContextMessage(command, result) {
1595
+ const content = result.stdout.trim() || result.stderr.trim() || '(no output)';
1596
+ return `Command output from \`${command}\` (exit ${result.exitCode ?? 'unknown'}):\n\n\`\`\`text\n${content}\n\`\`\``;
1597
+ }
1598
+ function buildWebContextMessage(url, markdown, focus) {
1599
+ const lines = [`Content from ${url}:`];
1600
+ if (focus) {
1601
+ lines.push(`Focus on: ${focus}`);
1602
+ }
1603
+ lines.push('', markdown);
1604
+ return lines.join('\n');
1605
+ }
1606
+ function formatReadOnlyFiles(files) {
1607
+ if (!files.length)
1608
+ return `${theme.dim('No read-only files.\n')}`;
1609
+ const lines = [
1610
+ `${theme.brand('Read-only files')}`,
1611
+ ...files.map((file, index) => ` ${index + 1}. ${file}`),
1612
+ '',
1613
+ ];
1614
+ return lines.join('\n');
1615
+ }
1616
+ function formatScheduledTasks(tasks) {
1617
+ if (!tasks.length)
1618
+ return `${theme.dim('No scheduled prompts.\n')}`;
1619
+ const lines = [`${theme.brand('Scheduled prompts')}`];
1620
+ for (const task of tasks) {
1621
+ lines.push(` ${task.id} ${theme.dim(`[${task.type}]`)} ${task.prompt}`, ` every: ${formatInterval(task.interval)} next: ${task.nextRun.toISOString()}`);
1622
+ }
1623
+ lines.push('');
1624
+ return lines.join('\n');
1625
+ }
1626
+ function formatInterval(interval) {
1627
+ const parts = [];
1628
+ let remaining = interval;
1629
+ const hours = Math.floor(remaining / (60 * 60 * 1000));
1630
+ if (hours > 0) {
1631
+ parts.push(`${hours}h`);
1632
+ remaining -= hours * 60 * 60 * 1000;
1633
+ }
1634
+ const minutes = Math.floor(remaining / (60 * 1000));
1635
+ if (minutes > 0) {
1636
+ parts.push(`${minutes}m`);
1637
+ remaining -= minutes * 60 * 1000;
1638
+ }
1639
+ const seconds = Math.floor(remaining / 1000);
1640
+ if (seconds > 0 || parts.length === 0)
1641
+ parts.push(`${seconds}s`);
1642
+ return parts.join('');
1643
+ }
1644
+ function formatReasoningConfig() {
1645
+ const current = getReasoningConfig();
1646
+ return [
1647
+ theme.brand('Reasoning'),
1648
+ ` effort: ${current.effort ?? 'default'}`,
1649
+ ` think tokens: ${typeof current.thinkTokens === 'number' ? current.thinkTokens : 'disabled'}`,
1650
+ '',
1651
+ ].join('\n');
1652
+ }
1653
+ function handleRoleCommand(rest, roleManager) {
1654
+ const [subcommand = '', ...subArgs] = rest;
1655
+ if (!subcommand) {
1656
+ const current = roleManager.getCurrentRole();
1657
+ return `${theme.brand('Current role')} ${theme.hl(current.name)}\n permissions: ${current.permissions.join(', ')}\n`;
1658
+ }
1659
+ if (subcommand === 'list') {
1660
+ const currentRole = roleManager.getCurrentRole().name;
1661
+ const lines = [
1662
+ theme.brand('Roles'),
1663
+ ...roleManager.listRoles().map((role) => {
1664
+ const marker = role.name === currentRole ? theme.ok('●') : theme.dim('○');
1665
+ return ` ${marker} ${role.name} ${theme.dim(role.permissions.join(', '))}`;
1666
+ }),
1667
+ '',
1668
+ ];
1669
+ return lines.join('\n');
1670
+ }
1671
+ if (subcommand === 'set') {
1672
+ const target = subArgs.join(' ').trim();
1673
+ if (!target)
1674
+ return `${theme.warn('usage: /role set <name>\n')}`;
1675
+ try {
1676
+ roleManager.setRole(target);
1677
+ return theme.ok(`✔ role → ${roleManager.getCurrentRole().name}\n`);
1678
+ }
1679
+ catch (error) {
1680
+ const message = error instanceof Error ? error.message : String(error);
1681
+ return theme.err(`${message}\n`);
1682
+ }
1683
+ }
1684
+ return theme.warn('usage: /role\n /role list\n /role set <name>\n');
1685
+ }
1686
+ function getRoleManager(cwd) {
1687
+ const roleManager = new RoleManager(defaultRolesConfigPath(cwd));
1688
+ roleManager.loadRoles();
1689
+ return roleManager;
1690
+ }
1691
+ function correctionsCommand(args) {
1692
+ const memory = new CorrectionMemory();
1693
+ memory.load();
1694
+ const [subcommand = 'list', ...rest] = args;
1695
+ const action = subcommand.toLowerCase();
1696
+ if (action === 'list') {
1697
+ return formatCorrections(memory.list());
1698
+ }
1699
+ if (action === 'add') {
1700
+ const raw = rest.join(' ').trim();
1701
+ const separator = raw.indexOf('->');
1702
+ if (!raw || separator === -1)
1703
+ return correctionsUsage();
1704
+ const wrongBehavior = raw.slice(0, separator).trim();
1705
+ const correctBehavior = raw.slice(separator + 2).trim();
1706
+ if (!wrongBehavior || !correctBehavior)
1707
+ return correctionsUsage();
1708
+ memory.add({
1709
+ pattern: wrongBehavior,
1710
+ wrongBehavior,
1711
+ correctBehavior,
1712
+ category: 'general',
1713
+ });
1714
+ memory.save();
1715
+ return `${theme.ok('Remembered correction')} ${theme.dim('Do NOT')} ${wrongBehavior} ${theme.dim('→')} ${correctBehavior}\n`;
1716
+ }
1717
+ if (action === 'remove') {
1718
+ const id = rest.join(' ').trim();
1719
+ if (!id)
1720
+ return correctionsUsage();
1721
+ const before = memory.list().length;
1722
+ memory.remove(id);
1723
+ const after = memory.list().length;
1724
+ if (before === after)
1725
+ return `${theme.warn(`No correction found for id ${id}.`)}\n`;
1726
+ memory.save();
1727
+ return `${theme.ok('Removed correction')} ${theme.hl(id)}\n`;
1728
+ }
1729
+ if (action === 'clear') {
1730
+ const entries = memory.list();
1731
+ for (const entry of entries)
1732
+ memory.remove(entry.id);
1733
+ memory.save();
1734
+ return `${theme.ok(`Cleared ${entries.length} correction${entries.length === 1 ? '' : 's'}.`)}\n`;
1735
+ }
1736
+ return correctionsUsage();
1737
+ }
1738
+ function formatCorrections(entries) {
1739
+ if (entries.length === 0) {
1740
+ return `${theme.brand('Corrections')}\n ${theme.dim('No remembered corrections.')}\n`;
1741
+ }
1742
+ const lines = entries.map((entry) => {
1743
+ const details = `${entry.category}, used ${entry.frequency}x`;
1744
+ return ` ${theme.hl(entry.id)} ${theme.dim(`(${details})`)}\n Do NOT ${entry.wrongBehavior}\n Instead: ${entry.correctBehavior}`;
1745
+ });
1746
+ return `${theme.brand('Corrections')}\n${lines.join('\n')}\n`;
1747
+ }
1748
+ function correctionsUsage() {
1749
+ return 'Usage: /corrections\n /corrections add <wrong> -> <correct>\n /corrections remove <id>\n /corrections clear\n';
1750
+ }
1751
+ function handleFilterSlashCommand(cwd, arg, rest) {
1752
+ const [subcommand = 'list'] = rest;
1753
+ const action = subcommand.toLowerCase();
1754
+ try {
1755
+ if (action === 'list') {
1756
+ return formatFilterRules(loadProjectContentFilter(cwd), cwd);
1757
+ }
1758
+ if (action === 'add') {
1759
+ const [, name, patternSource, actionSource] = rest;
1760
+ if (!name || !patternSource || !actionSource) {
1761
+ return `${theme.warn('usage: /filter add <name> <pattern> <action>\n')}`;
1762
+ }
1763
+ const filterAction = parseFilterAction(actionSource);
1764
+ if (!filterAction) {
1765
+ return `${theme.warn('filter action must be redact, warn, or block\n')}`;
1766
+ }
1767
+ const savedRule = saveProjectFilterRule(cwd, {
1768
+ name,
1769
+ pattern: parseFilterPattern(patternSource),
1770
+ type: 'custom',
1771
+ action: filterAction,
1772
+ });
1773
+ return theme.ok(`✔ filter rule saved: ${savedRule.name} (${savedRule.type}/${savedRule.action}) /${savedRule.pattern.source}/${savedRule.pattern.flags}\n`);
1774
+ }
1775
+ if (action === 'remove') {
1776
+ const [, name] = rest;
1777
+ if (!name) {
1778
+ return `${theme.warn('usage: /filter remove <name>\n')}`;
1779
+ }
1780
+ const removed = removeProjectFilterRule(cwd, name);
1781
+ if (!removed.removed) {
1782
+ return `${theme.warn(`filter rule not found: ${name}\n`)}`;
1783
+ }
1784
+ return theme.ok(`✔ removed ${removed.source} filter rule: ${name}\n`);
1785
+ }
1786
+ if (action === 'test') {
1787
+ const text = arg.slice(subcommand.length).trim();
1788
+ if (!text) {
1789
+ return `${theme.warn('usage: /filter test <text>\n')}`;
1790
+ }
1791
+ return formatFilterTestResult(loadProjectContentFilter(cwd).filter(text));
1792
+ }
1793
+ return `${theme.warn('usage: /filter | /filter add <name> <pattern> <action> | /filter remove <name> | /filter test <text>\n')}`;
1794
+ }
1795
+ catch (error) {
1796
+ const message = error instanceof Error ? error.message : String(error);
1797
+ return `${theme.err(`content filter error: ${message}\n`)}`;
1798
+ }
1799
+ }
1800
+ async function proxyCommand(args) {
1801
+ const manager = ProxyManager.shared();
1802
+ const [subcommand = 'show', ...rest] = args;
1803
+ const action = subcommand.toLowerCase();
1804
+ if (action === 'show' || action === 'list' || action === 'status') {
1805
+ return formatProxyStatus(manager);
1806
+ }
1807
+ if (action === 'set') {
1808
+ const rawUrl = rest.join(' ').trim();
1809
+ if (!rawUrl)
1810
+ return `${theme.warn('usage: /proxy set <url>\n')}`;
1811
+ try {
1812
+ const saved = manager.setProxy(ProxyManager.parseProxyUrl(rawUrl));
1813
+ return `${theme.ok('✔ proxy configured\n')}${formatProxyDetails(saved, manager.getSource() || 'file', manager.getConfigPath())}`;
1814
+ }
1815
+ catch (error) {
1816
+ return theme.err(`proxy: ${error.message}\n`);
1817
+ }
1818
+ }
1819
+ if (action === 'clear' || action === 'unset') {
1820
+ manager.clearProxy();
1821
+ if (manager.loadConfig() && manager.getSource() === 'env') {
1822
+ return (`${theme.warn('proxy file cleared; environment proxy variables still apply\n')}` +
1823
+ formatProxyStatus(manager));
1824
+ }
1825
+ return theme.ok(`✔ proxy cleared (${manager.getConfigPath()})\n`);
1826
+ }
1827
+ if (action === 'test') {
1828
+ const targetUrl = rest.join(' ').trim() || config.endpoint;
1829
+ if (!manager.loadConfig())
1830
+ return theme.warn('no proxy configured\n');
1831
+ const result = await manager.testConnection(targetUrl);
1832
+ return result.ok
1833
+ ? `${theme.ok('✔ proxy test succeeded')} ${theme.dim(`${targetUrl} status=${result.status ?? 'n/a'} proxied=${result.proxied}`)}\n`
1834
+ : `${theme.err(`proxy test failed: ${result.error ?? 'unknown error'}`)}\n`;
1835
+ }
1836
+ return `${theme.warn('usage: /proxy [show|set <url>|clear|test [url]]\n')}`;
1837
+ }
1838
+ function retentionCommand(args) {
1839
+ const manager = new RetentionManager();
1840
+ const [subcommand = 'show', targetRaw, daysRaw, countRaw, enabledRaw] = args;
1841
+ const action = subcommand.toLowerCase();
1842
+ if (action === 'show' || action === 'list' || action === 'policies') {
1843
+ return formatRetentionPolicies(manager);
1844
+ }
1845
+ if (action === 'preview') {
1846
+ return formatRetentionPreview(manager.preview(), manager);
1847
+ }
1848
+ if (action === 'enforce' || action === 'apply') {
1849
+ return formatRetentionResult(manager.enforce(), manager);
1850
+ }
1851
+ if (action === 'set') {
1852
+ const target = parseRetentionTarget(targetRaw);
1853
+ const maxAgeDays = parseRetentionCount(daysRaw);
1854
+ const parsedMaxCount = countRaw ? parseRetentionCount(countRaw) : undefined;
1855
+ const enabled = enabledRaw ? enabledRaw.toLowerCase() !== 'off' : true;
1856
+ if (!target || maxAgeDays === null || (countRaw && parsedMaxCount === null)) {
1857
+ return `${theme.warn('usage: /retention set <sessions|audit|memory|all> <days> [count] [on|off]\n')}`;
1858
+ }
1859
+ const nextPolicies = manager.setPolicy({
1860
+ target,
1861
+ maxAgeDays,
1862
+ maxCount: parsedMaxCount ?? undefined,
1863
+ enabled,
1864
+ });
1865
+ return `${theme.ok('✔ retention policy saved')}\n${nextPolicies.map((policy) => ` ${policy.target}: age=${policy.maxAgeDays}d${typeof policy.maxCount === 'number' ? ` count=${policy.maxCount}` : ''} ${policy.enabled ? 'enabled' : 'disabled'}`).join('\n')}\n`;
1866
+ }
1867
+ return `${theme.warn('usage: /retention [show|preview|enforce|set <target> <days> [count] [on|off]]\n')}`;
1868
+ }
1869
+ function parseRetentionTarget(value) {
1870
+ if (value === 'sessions' || value === 'audit' || value === 'memory' || value === 'all') {
1871
+ return value;
1872
+ }
1873
+ return null;
1874
+ }
1875
+ function parseRetentionCount(value) {
1876
+ if (!value)
1877
+ return null;
1878
+ const parsed = Number(value);
1879
+ if (!Number.isFinite(parsed))
1880
+ return null;
1881
+ const normalized = Math.trunc(parsed);
1882
+ return normalized >= 0 ? normalized : null;
1883
+ }
1884
+ function renderCurrentProvider(model) {
1885
+ return [
1886
+ `${theme.brand('Current provider')}`,
1887
+ ` name: ${config.provider}`,
1888
+ ` base URL: ${config.endpoint}`,
1889
+ ` model: ${model}`,
1890
+ '',
1891
+ ].join('\n');
1892
+ }
1893
+ function renderProviderList() {
1894
+ const providers = providerRegistry.list();
1895
+ const lines = [`${theme.brand('Providers')}`, ''];
1896
+ for (const provider of providers) {
1897
+ const marker = provider.name === config.provider ? theme.ok('●') : theme.dim('○');
1898
+ const defaultModel = provider.defaultModel || provider.models[0] || 'unknown';
1899
+ lines.push(` ${marker} ${provider.name} ${theme.dim(provider.baseUrl)} ${theme.dim(`model=${defaultModel}`)}`);
1900
+ }
1901
+ lines.push('');
1902
+ return lines.join('\n');
1903
+ }
1904
+ async function testActiveProvider(model) {
1905
+ if (isLocalProviderName(config.provider)) {
1906
+ localModelProvider.configure({
1907
+ provider: config.provider,
1908
+ baseUrl: config.endpoint,
1909
+ model,
1910
+ apiKey: config.token,
1911
+ });
1912
+ const available = await localModelProvider.isAvailable();
1913
+ const models = available ? await localModelProvider.listModels() : [];
1914
+ const header = available
1915
+ ? theme.ok('✔ local provider reachable')
1916
+ : theme.err('✖ local provider unavailable');
1917
+ const discovered = models.length ? models.join(', ') : theme.dim('(no models reported)');
1918
+ return [
1919
+ header,
1920
+ `provider: ${config.provider}`,
1921
+ `base URL: ${config.endpoint}`,
1922
+ `model: ${model}`,
1923
+ `available models: ${discovered}`,
1924
+ '',
1925
+ ].join('\n');
1926
+ }
1927
+ const result = await providerRegistry.testProvider(config.provider);
1928
+ const header = result.ok ? theme.ok('✔ provider reachable') : theme.err('✖ provider unavailable');
1929
+ return [
1930
+ header,
1931
+ `provider: ${result.provider}`,
1932
+ `base URL: ${config.endpoint}`,
1933
+ `model: ${model}`,
1934
+ `available models: ${result.models.length ? result.models.join(', ') : theme.dim('(none reported)')}`,
1935
+ ...(result.error ? [`error: ${result.error}`] : []),
1936
+ '',
1937
+ ].join('\n');
1938
+ }
1939
+ function formatGoalRunStatus(run) {
1940
+ if (!run) {
1941
+ return `${theme.brand('Goal run')}\n ${theme.dim('No goal has been started yet.')}\n`;
1942
+ }
1943
+ const progress = run.agent.getProgress();
1944
+ const lines = [
1945
+ theme.brand('Goal run'),
1946
+ ` goal: ${run.goal.description}`,
1947
+ ` phase: ${progress.phase}`,
1948
+ ` started: ${run.startedAt}`,
1949
+ ` attempt: ${progress.currentAttempt}/${progress.maxAttempts}`,
1950
+ ` steps: ${progress.completedSteps}/${progress.totalSteps}`,
1951
+ ];
1952
+ if (progress.currentStepId) {
1953
+ lines.push(` current step: ${progress.currentStepId}`);
1954
+ }
1955
+ if (progress.verification) {
1956
+ lines.push(` verification: ${progress.verification.ok ? 'passed' : 'failed'} (${progress.verification.score})`);
1957
+ if (progress.verification.issues.length > 0) {
1958
+ lines.push(` issues: ${progress.verification.issues.join(' | ')}`);
1959
+ }
1960
+ }
1961
+ if (run.result?.summary) {
1962
+ lines.push(` summary: ${run.result.summary}`);
1963
+ }
1964
+ if (run.error) {
1965
+ lines.push(` error: ${run.error}`);
1966
+ }
1967
+ return `${lines.join('\n')}\n`;
1968
+ }
1969
+ function formatGoalCompletion(result) {
1970
+ const issues = result.verification.issues.length > 0
1971
+ ? `\n issues: ${result.verification.issues.join(' | ')}`
1972
+ : '';
1973
+ return (`${theme.brand('Goal complete')}\n` +
1974
+ ` goal: ${result.goal.description}\n` +
1975
+ ` status: ${result.success ? 'success' : result.aborted ? 'aborted' : 'failed'}\n` +
1976
+ ` attempts: ${result.attempts}\n` +
1977
+ ` summary: ${result.summary}${issues}\n`);
1978
+ }
1979
+ async function sandboxCommand(args, cwd) {
1980
+ const [subcommand = 'status', ...rest] = args;
1981
+ const sandbox = getContainerSandbox(cwd);
1982
+ switch (subcommand.toLowerCase()) {
1983
+ case 'run': {
1984
+ const command = rest.join(' ').trim();
1985
+ if (!command)
1986
+ return theme.warn('usage: /sandbox run <command>\n');
1987
+ if (!(await sandbox.isDockerAvailable())) {
1988
+ return theme.warn('Docker is not available. Start Docker Desktop or install the Docker CLI.\n');
1989
+ }
1990
+ const containerId = await sandbox.create({ image: sandbox.getDefaultImage() });
1991
+ try {
1992
+ const result = await sandbox.exec(containerId, command);
1993
+ const body = [
1994
+ `${theme.brand('Sandbox run')} ${theme.dim(containerId.slice(0, 12))}`,
1995
+ result.stdout.trimEnd(),
1996
+ result.stderr.trimEnd(),
1997
+ ]
1998
+ .filter(Boolean)
1999
+ .join('\n\n');
2000
+ return `${body}\n`;
2001
+ }
2002
+ finally {
2003
+ await sandbox.destroy(containerId).catch(() => undefined);
2004
+ }
2005
+ }
2006
+ case 'shell': {
2007
+ if (!(await sandbox.isDockerAvailable())) {
2008
+ return theme.warn('Docker is not available. Start Docker Desktop or install the Docker CLI.\n');
2009
+ }
2010
+ const containerId = await sandbox.create({ image: sandbox.getDefaultImage() });
2011
+ return [
2012
+ `${theme.brand('Sandbox shell')} ${theme.dim(containerId.slice(0, 12))}`,
2013
+ `Project mounted read-only from ${cwd}`,
2014
+ `Attach with: docker exec -it ${containerId} sh`,
2015
+ `Cleanup with: /sandbox cleanup`,
2016
+ '',
2017
+ ].join('\n');
2018
+ }
2019
+ case 'status': {
2020
+ if (!(await sandbox.isDockerAvailable())) {
2021
+ return theme.warn('Docker is not available. Start Docker Desktop or install the Docker CLI.\n');
2022
+ }
2023
+ const containers = await sandbox.listRunning();
2024
+ if (!containers.length)
2025
+ return `${theme.dim('No sandbox containers are running.')}\n`;
2026
+ const lines = [`${theme.brand('Sandbox containers')}`, ''];
2027
+ for (const container of containers) {
2028
+ lines.push(` ${container.id.slice(0, 12)} ${container.image} ${theme.dim(container.status)}`);
2029
+ }
2030
+ lines.push('');
2031
+ return lines.join('\n');
2032
+ }
2033
+ case 'cleanup': {
2034
+ if (!(await sandbox.isDockerAvailable())) {
2035
+ return theme.warn('Docker is not available. Start Docker Desktop or install the Docker CLI.\n');
2036
+ }
2037
+ const containers = await sandbox.listRunning();
2038
+ if (!containers.length)
2039
+ return `${theme.dim('No sandbox containers to clean up.')}\n`;
2040
+ await Promise.all(containers.map((container) => sandbox.destroy(container.id).catch(() => undefined)));
2041
+ return theme.ok(`✔ cleaned up ${containers.length} sandbox container${containers.length === 1 ? '' : 's'}\n`);
2042
+ }
2043
+ default:
2044
+ return theme.warn('usage: /sandbox run <command>\n /sandbox shell\n /sandbox status\n /sandbox cleanup\n');
2045
+ }
2046
+ }
2047
+ function getContainerSandbox(cwd) {
2048
+ const resolved = path.resolve(cwd);
2049
+ let sandbox = sandboxByCwd.get(resolved);
2050
+ if (!sandbox) {
2051
+ sandbox = new ContainerSandbox(resolved);
2052
+ sandboxByCwd.set(resolved, sandbox);
2053
+ }
2054
+ return sandbox;
2055
+ }
2056
+ function renderBar(used, cap, width) {
2057
+ const ratio = Math.min(1, used / cap);
2058
+ const fill = Math.round(width * ratio);
2059
+ const bar = '█'.repeat(fill) + '░'.repeat(width - fill);
2060
+ const colored = ratio > 0.9 ? theme.err(bar) : ratio > 0.75 ? theme.warn(bar) : theme.ok(bar);
2061
+ return `[${colored}]`;
2062
+ }
2063
+ function formatPinnedFiles(files) {
2064
+ if (!files.length)
2065
+ return `${theme.dim('No pinned files.\n')}`;
2066
+ const total = files.reduce((sum, file) => sum + file.tokens, 0);
2067
+ const lines = [
2068
+ `${theme.brand('Pinned files')}`,
2069
+ ...files.map((file, index) => ` ${index + 1}. ${file.path} ${theme.dim(`(${file.tokens} tokens)`)}`),
2070
+ ` total: ${theme.hl(String(total))} tokens`,
2071
+ '',
2072
+ ];
2073
+ return lines.join('\n');
2074
+ }
2075
+ function formatProxyStatus(manager) {
2076
+ const proxy = manager.loadConfig();
2077
+ if (!proxy) {
2078
+ return `${theme.brand('Proxy')}\n ${theme.dim('status')} disabled\n ${theme.dim('config')} ${manager.getConfigPath()}\n`;
2079
+ }
2080
+ return formatProxyDetails(proxy, manager.getSource() || 'file', manager.getConfigPath());
2081
+ }
2082
+ function formatProxyDetails(proxy, source, file) {
2083
+ const auth = proxy.auth?.username
2084
+ ? `${proxy.auth.username}${proxy.auth.password ? ':***' : ''}@`
2085
+ : '';
2086
+ const noProxy = proxy.noProxy?.length ? proxy.noProxy.join(', ') : '(none)';
2087
+ return [
2088
+ `${theme.brand('Proxy')} ${theme.dim(`[${source}]`)}`,
2089
+ ` ${theme.dim('url')} ${proxy.type}://${auth}${proxy.host}:${proxy.port}`,
2090
+ ` ${theme.dim('no_proxy')} ${noProxy}`,
2091
+ ` ${theme.dim('config')} ${file}`,
2092
+ '',
2093
+ ].join('\n');
2094
+ }
2095
+ function formatAuditEntries(entries, heading = 'Audit log') {
2096
+ if (!entries.length) {
2097
+ return `${theme.brand(heading)} ${theme.dim(auditLogPath())}\n ${theme.dim('No audit entries found.')}\n`;
2098
+ }
2099
+ const lines = [`${theme.brand(heading)} ${theme.dim(auditLogPath())}`, ''];
2100
+ for (const entry of entries) {
2101
+ const parts = [
2102
+ entry.tool ? theme.hl(entry.tool) : entry.action,
2103
+ theme.dim(entry.result.toUpperCase()),
2104
+ theme.dim(entry.timestamp),
2105
+ ];
2106
+ if (typeof entry.duration === 'number')
2107
+ parts.push(theme.dim(`${entry.duration}ms`));
2108
+ lines.push(` ${parts.join(' ')}`);
2109
+ if (entry.command)
2110
+ lines.push(` command: ${entry.command}`);
2111
+ if (entry.details)
2112
+ lines.push(` details: ${entry.details.replace(/\r?\n/gu, ' ')}`);
2113
+ }
2114
+ lines.push('');
2115
+ return lines.join('\n');
2116
+ }
2117
+ function formatAuditStats(stats) {
2118
+ return [
2119
+ `${theme.brand('Audit stats')} ${theme.dim(auditLogPath())}`,
2120
+ ` total: ${theme.hl(String(stats.total))}`,
2121
+ ` success: ${theme.ok(String(stats.success))}`,
2122
+ ` failure: ${theme.err(String(stats.failure))}`,
2123
+ ` denied: ${theme.warn(String(stats.denied))}`,
2124
+ ` first: ${theme.dim(stats.firstEntry || 'n/a')}`,
2125
+ ` last: ${theme.dim(stats.lastEntry || 'n/a')}`,
2126
+ ` avg time: ${theme.dim(stats.avgDuration !== undefined ? `${stats.avgDuration}ms` : 'n/a')}`,
2127
+ '',
2128
+ theme.brand('Top tools'),
2129
+ formatAuditCounter(stats.byTool),
2130
+ '',
2131
+ ].join('\n');
2132
+ }
2133
+ function formatAuditCounter(counter) {
2134
+ const entries = Object.entries(counter)
2135
+ .sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]))
2136
+ .slice(0, 5);
2137
+ if (entries.length === 0)
2138
+ return ` ${theme.dim('none')}`;
2139
+ return entries
2140
+ .map(([name, count]) => ` ${theme.hl(String(count)).padStart(5)} ${name}`)
2141
+ .join('\n');
2142
+ }
2143
+ function searchAuditEntries(entries, query) {
2144
+ const needle = query.trim().toLowerCase();
2145
+ if (!needle)
2146
+ return [];
2147
+ return entries.filter((entry) => [
2148
+ entry.id,
2149
+ entry.timestamp,
2150
+ entry.action,
2151
+ entry.tool,
2152
+ entry.command,
2153
+ entry.result,
2154
+ entry.user,
2155
+ entry.details,
2156
+ safeAuditArgs(entry.args),
2157
+ ]
2158
+ .filter((part) => typeof part === 'string' && part.length > 0)
2159
+ .some((part) => part.toLowerCase().includes(needle)));
2160
+ }
2161
+ function safeAuditArgs(value) {
2162
+ try {
2163
+ return value === undefined ? '' : JSON.stringify(value);
2164
+ }
2165
+ catch {
2166
+ return '';
2167
+ }
2168
+ }
2169
+ function formatCloudUsage(currentSessionId) {
2170
+ const lines = [
2171
+ 'usage: /cloud create [name]',
2172
+ ' /cloud connect <id>',
2173
+ ' /cloud list',
2174
+ ' /cloud destroy <id>',
2175
+ ' /cloud sync',
2176
+ ];
2177
+ if (currentSessionId)
2178
+ lines.push(`current cloud session: ${currentSessionId}`);
2179
+ return `${lines.join('\n')}\n`;
2180
+ }
2181
+ function formatCloudSessions(sessions) {
2182
+ if (!sessions.length)
2183
+ return `${theme.dim('No cloud sessions.\n')}`;
2184
+ const lines = [`${theme.brand('Cloud sessions')}`, ''];
2185
+ for (const session of sessions) {
2186
+ const status = session.status === 'connected' ? theme.ok('connected') : theme.dim('idle');
2187
+ const label = session.name && session.name !== session.id ? ` ${theme.dim(`(${session.name})`)}` : '';
2188
+ const synced = session.lastSyncedAt ? ` synced ${session.lastSyncedAt}` : '';
2189
+ lines.push(` ${session.id} ${status}${label} ${theme.dim(`msgs=${session.messageCount}`)}`);
2190
+ lines.push(` ${session.endpoint}${synced}`);
2191
+ }
2192
+ lines.push('');
2193
+ return lines.join('\n');
2194
+ }
2195
+ function formatNavigationResult(label, symbol, locations) {
2196
+ const lines = [`${theme.brand(label)} ${theme.hl(symbol)}`, ''];
2197
+ for (const location of locations) {
2198
+ const position = `${location.file}:${location.line}${location.column ? `:${location.column}` : ''}`;
2199
+ lines.push(` ${position}`);
2200
+ lines.push(` ${location.context}`);
2201
+ }
2202
+ lines.push('');
2203
+ return lines.join('\n');
2204
+ }
2205
+ function parseParallelSpec(raw) {
2206
+ const trimmed = raw.trim();
2207
+ if (!trimmed) {
2208
+ return {
2209
+ error: 'usage: /parallel <json-spec|prompt-a, prompt-b>\n' +
2210
+ 'example: /parallel [{"name":"plan","type":"plan","prompt":"outline the release"}]',
2211
+ };
2212
+ }
2213
+ if (trimmed.startsWith('[') || trimmed.startsWith('{')) {
2214
+ try {
2215
+ const parsed = JSON.parse(trimmed);
2216
+ if (Array.isArray(parsed)) {
2217
+ return { agents: normalizeAgentTasks(parsed) };
2218
+ }
2219
+ if (parsed && typeof parsed === 'object') {
2220
+ const spec = parsed;
2221
+ if (Array.isArray(spec.agents)) {
2222
+ return {
2223
+ agents: normalizeAgentTasks(spec.agents),
2224
+ concurrencyLimit: typeof spec.concurrencyLimit === 'number'
2225
+ ? Math.floor(spec.concurrencyLimit)
2226
+ : undefined,
2227
+ timeoutMs: typeof spec.timeoutMs === 'number' ? Math.floor(spec.timeoutMs) : undefined,
2228
+ };
2229
+ }
2230
+ }
2231
+ return {
2232
+ error: 'invalid /parallel JSON: expected an array or an object with an "agents" array',
2233
+ };
2234
+ }
2235
+ catch (error) {
2236
+ const message = error instanceof Error ? error.message : 'failed to parse JSON';
2237
+ return { error: `invalid /parallel JSON: ${message}` };
2238
+ }
2239
+ }
2240
+ const prompts = trimmed
2241
+ .split(',')
2242
+ .map((part) => part.trim())
2243
+ .filter((part) => part.length > 0);
2244
+ if (!prompts.length) {
2245
+ return { error: 'usage: /parallel <json-spec|prompt-a, prompt-b>' };
2246
+ }
2247
+ return {
2248
+ agents: prompts.map((prompt, index) => ({
2249
+ name: `agent-${index + 1}`,
2250
+ type: 'task',
2251
+ prompt,
2252
+ })),
2253
+ };
2254
+ }
2255
+ function normalizeAgentTasks(input) {
2256
+ return input.map((entry, index) => {
2257
+ const task = (entry ?? {});
2258
+ const prompt = typeof task.prompt === 'string' ? task.prompt.trim() : '';
2259
+ return {
2260
+ name: typeof task.name === 'string' && task.name.trim() ? task.name.trim() : `agent-${index + 1}`,
2261
+ type: typeof task.type === 'string' && task.type.trim() ? task.type.trim() : 'task',
2262
+ prompt,
2263
+ ...(typeof task.systemPrompt === 'string' && task.systemPrompt.trim()
2264
+ ? { systemPrompt: task.systemPrompt.trim() }
2265
+ : {}),
2266
+ };
2267
+ });
2268
+ }
2269
+ function formatParallelResults(run) {
2270
+ if (!run.results.length)
2271
+ return `${theme.warn('No agent tasks were provided.')}\n`;
2272
+ const lines = [`${theme.brand('Parallel agent results')}`, '', run.aggregated.summary];
2273
+ if (run.aggregated.conflicts.length) {
2274
+ lines.push('', '## Conflicts', ...run.aggregated.conflicts);
2275
+ }
2276
+ lines.push('');
2277
+ for (const result of run.results) {
2278
+ const status = result.status === 'success' ? theme.ok('SUCCESS') : theme.err('ERROR');
2279
+ lines.push(`${status} ${result.name} ${theme.dim(`(${formatDuration(result.duration)})`)}`);
2280
+ lines.push(result.output.trim() || theme.dim('(empty output)'));
2281
+ lines.push('');
2282
+ }
2283
+ return `${lines.join('\n').trimEnd()}\n`;
2284
+ }
2285
+ function formatDeadCodeReport(rootDir, report) {
2286
+ const lines = [
2287
+ `${theme.brand('Dead code report')} ${theme.dim(rootDir)}`,
2288
+ ` scanned: ${theme.hl(String(report.stats.total))} items`,
2289
+ ` unused: ${theme.hl(String(report.stats.unused))} ${theme.dim(`(${report.stats.percentage.toFixed(2)}%)`)}`,
2290
+ '',
2291
+ ];
2292
+ if (report.unusedExports.length) {
2293
+ lines.push('Unused exports');
2294
+ for (const entry of report.unusedExports) {
2295
+ lines.push(` - ${entry.file}:${entry.line} ${entry.name} ${theme.dim(`(${entry.kind})`)}`);
2296
+ }
2297
+ lines.push('');
2298
+ }
2299
+ if (report.unusedFiles.length) {
2300
+ lines.push('Unused files');
2301
+ for (const file of report.unusedFiles) {
2302
+ lines.push(` - ${file}`);
2303
+ }
2304
+ lines.push('');
2305
+ }
2306
+ if (!report.unusedExports.length && !report.unusedFiles.length) {
2307
+ lines.push(theme.ok('✔ no unused exports or files detected'), '');
2308
+ }
2309
+ return `${lines.join('\n').trimEnd()}\n`;
2310
+ }
2311
+ function formatDuration(durationMs) {
2312
+ if (durationMs < 1000)
2313
+ return `${durationMs}ms`;
2314
+ return `${(durationMs / 1000).toFixed(2)}s`;
2315
+ }
2316
+ function parseHealArgs(args) {
2317
+ let maxAttempts = 3;
2318
+ for (let index = 0; index < args.length; index += 1) {
2319
+ const token = args[index];
2320
+ if (token === '--max') {
2321
+ const value = args[index + 1];
2322
+ const parsed = Number.parseInt(value ?? '', 10);
2323
+ if (!Number.isFinite(parsed) || parsed < 1) {
2324
+ return { error: 'usage: /heal [--max <positive-number>]' };
2325
+ }
2326
+ maxAttempts = parsed;
2327
+ index += 1;
2328
+ continue;
2329
+ }
2330
+ return { error: `unknown /heal option: ${token}` };
2331
+ }
2332
+ return { maxAttempts };
2333
+ }
2334
+ function formatHealResult(result) {
2335
+ const lines = [
2336
+ `${theme.brand('Self-heal build')} ${theme.dim(result.command)}`,
2337
+ ` status: ${result.success ? theme.ok('success') : theme.err('failed')}`,
2338
+ ];
2339
+ if (result.attempts.length === 0) {
2340
+ lines.push(` attempts: ${theme.dim('no safe fixes applied')}`);
2341
+ }
2342
+ else {
2343
+ lines.push(` attempts: ${theme.hl(String(result.attempts.length))}`);
2344
+ for (const [index, attempt] of result.attempts.entries()) {
2345
+ const location = attempt.error.file
2346
+ ? `${attempt.error.file}${attempt.error.line ? `:${attempt.error.line}` : ''}`
2347
+ : 'unknown location';
2348
+ lines.push(` ${index + 1}. ${theme.hl(location)}`);
2349
+ lines.push(` diagnosis: ${attempt.diagnosis}`);
2350
+ lines.push(` fix: ${attempt.fix}`);
2351
+ lines.push(` applied: ${attempt.applied ? theme.ok('yes') : theme.err('no')}`);
2352
+ }
2353
+ }
2354
+ if (!result.success && result.build.errors.length > 0) {
2355
+ lines.push(' remaining errors:');
2356
+ for (const error of result.build.errors.slice(0, 5)) {
2357
+ lines.push(` - ${error.code ? `${error.code} ` : ''}${error.message}${error.file ? ` ${theme.dim(`(${error.file}${error.line ? `:${error.line}` : ''})`)}` : ''}`);
2358
+ }
2359
+ }
2360
+ lines.push('');
2361
+ return lines.join('\n');
2362
+ }
2363
+ function buildTddSpec(description) {
2364
+ const clauses = description
2365
+ .split(/\s+(?:and|then)\s+|,/i)
2366
+ .map((part) => part.trim())
2367
+ .filter(Boolean);
2368
+ const expectedBehaviors = [
2369
+ 'captures the original description',
2370
+ ...clauses.map((clause) => `handles ${clause.toLowerCase()}`),
2371
+ ];
2372
+ return {
2373
+ description,
2374
+ expectedBehaviors: [...new Set(expectedBehaviors)],
2375
+ };
2376
+ }
2377
+ function formatTddCycle(result) {
2378
+ const status = result.finalStatus === 'green' ? theme.ok('green') : theme.err('red');
2379
+ return [
2380
+ `${theme.brand('TDD cycle')} ${status}`,
2381
+ ` spec: ${theme.hl(result.spec.description)}`,
2382
+ ` test: ${result.testFile}`,
2383
+ ` source: ${result.sourceFile}`,
2384
+ ` cycles: ${result.cycles}`,
2385
+ '',
2386
+ ].join('\n');
2387
+ }
2388
+ function formatTddStatus(result) {
2389
+ if (!result) {
2390
+ return `${theme.brand('TDD status')}\n ${theme.dim('No TDD cycle has been run yet.')}\n`;
2391
+ }
2392
+ return [
2393
+ `${theme.brand('TDD status')}`,
2394
+ ` status: ${result.finalStatus === 'green' ? theme.ok('green') : theme.err('red')}`,
2395
+ ` spec: ${theme.hl(result.spec.description)}`,
2396
+ ` test: ${result.testFile}`,
2397
+ ` source: ${result.sourceFile}`,
2398
+ ` cycles: ${result.cycles}`,
2399
+ '',
2400
+ ].join('\n');
2401
+ }
2402
+ async function bridgeCommand(args) {
2403
+ const [subcommand = 'status', rawPort] = args;
2404
+ switch (subcommand.toLowerCase()) {
2405
+ case 'start': {
2406
+ const parsedPort = parseBridgePort(rawPort);
2407
+ if (typeof parsedPort === 'string')
2408
+ return `${theme.warn(parsedPort)}\n`;
2409
+ const port = await ideBridgeServer.start(parsedPort ?? DEFAULT_BRIDGE_PORT);
2410
+ return [
2411
+ theme.ok('✔ IDE bridge started'),
2412
+ ` port: ${theme.hl(String(port))}`,
2413
+ ` connections: ${theme.hl(String(ideBridgeServer.getConnectionCount()))}`,
2414
+ '',
2415
+ ].join('\n');
2416
+ }
2417
+ case 'stop':
2418
+ if (!ideBridgeServer.isRunning()) {
2419
+ return `${theme.warn('IDE bridge is not running.')}\n`;
2420
+ }
2421
+ await ideBridgeServer.stop();
2422
+ return `${theme.ok('✔ IDE bridge stopped')}\n`;
2423
+ case 'status':
2424
+ return formatBridgeStatus();
2425
+ default:
2426
+ return [
2427
+ theme.warn(`unknown bridge subcommand: ${subcommand}`),
2428
+ 'usage: /bridge start [port]',
2429
+ ' /bridge stop',
2430
+ ' /bridge status',
2431
+ '',
2432
+ ].join('\n');
2433
+ }
2434
+ }
2435
+ async function serveCommand(args) {
2436
+ const [subcommand = 'status', rawPort] = args;
2437
+ switch (subcommand.toLowerCase()) {
2438
+ case 'start': {
2439
+ const parsedPort = parseServePort(rawPort);
2440
+ if (typeof parsedPort === 'string')
2441
+ return `${theme.warn(parsedPort)}\n`;
2442
+ const port = await apiServer.start(parsedPort ?? DEFAULT_API_PORT);
2443
+ return [
2444
+ theme.ok('✔ API server started'),
2445
+ ` port: ${theme.hl(String(port))}`,
2446
+ ` sessions: ${theme.hl(String(apiServer.getSessionCount()))}`,
2447
+ '',
2448
+ ].join('\n');
2449
+ }
2450
+ case 'stop':
2451
+ if (!apiServer.isRunning()) {
2452
+ return `${theme.warn('API server is not running.')}\n`;
2453
+ }
2454
+ await apiServer.stop();
2455
+ return `${theme.ok('✔ API server stopped')}\n`;
2456
+ case 'status':
2457
+ if (!apiServer.isRunning()) {
2458
+ return `${theme.warn('API server is stopped.')}\n`;
2459
+ }
2460
+ return [
2461
+ theme.brand('API server status'),
2462
+ ` running: ${theme.hl('yes')}`,
2463
+ ` port: ${theme.hl(String(apiServer.getPort() ?? DEFAULT_API_PORT))}`,
2464
+ ` sessions: ${theme.hl(String(apiServer.getSessionCount()))}`,
2465
+ '',
2466
+ ].join('\n');
2467
+ case 'open': {
2468
+ const parsedPort = parseServePort(rawPort);
2469
+ if (typeof parsedPort === 'string')
2470
+ return `${theme.warn(parsedPort)}\n`;
2471
+ const port = await apiServer.start(parsedPort ?? DEFAULT_API_PORT);
2472
+ const url = `http://127.0.0.1:${port}/`;
2473
+ try {
2474
+ await openBrowser(url);
2475
+ }
2476
+ catch (error) {
2477
+ const message = error instanceof Error ? error.message : String(error);
2478
+ return `${theme.warn(`failed to open browser automatically: ${message}`)}\n${theme.dim(`Open ${url} manually.\n`)}`;
2479
+ }
2480
+ return `${theme.ok(`✔ opened browser UI at ${url}`)}\n`;
2481
+ }
2482
+ default:
2483
+ return [
2484
+ theme.warn(`unknown serve subcommand: ${subcommand}`),
2485
+ 'usage: /serve start [port]',
2486
+ ' /serve stop',
2487
+ ' /serve status',
2488
+ ' /serve open [port]',
2489
+ '',
2490
+ ].join('\n');
2491
+ }
2492
+ }
2493
+ function formatBridgeStatus() {
2494
+ if (!ideBridgeServer.isRunning()) {
2495
+ return `${theme.warn('IDE bridge is stopped.')}\n`;
2496
+ }
2497
+ return [
2498
+ theme.brand('IDE bridge status'),
2499
+ ` running: ${theme.hl('yes')}`,
2500
+ ` port: ${theme.hl(String(ideBridgeServer.getPort() ?? DEFAULT_BRIDGE_PORT))}`,
2501
+ ` connections: ${theme.hl(String(ideBridgeServer.getConnectionCount()))}`,
2502
+ '',
2503
+ ].join('\n');
2504
+ }
2505
+ function parseBridgePort(value) {
2506
+ if (!value)
2507
+ return undefined;
2508
+ const port = Number.parseInt(value, 10);
2509
+ if (!Number.isInteger(port) || port < 1 || port > 65_535) {
2510
+ return `invalid port: ${value}`;
2511
+ }
2512
+ return port;
2513
+ }
2514
+ function parseServePort(value) {
2515
+ if (!value)
2516
+ return undefined;
2517
+ const port = Number.parseInt(value, 10);
2518
+ if (!Number.isInteger(port) || port < 1 || port > 65_535) {
2519
+ return `invalid port: ${value}`;
2520
+ }
2521
+ return port;
2522
+ }
2523
+ async function workflowCommand(args, cwd) {
2524
+ const [subcommand = 'list', rawName] = args;
2525
+ const engine = new WorkflowEngine({ cwd });
2526
+ const workflowDir = path.join(cwd, '.icopilot', 'workflows');
2527
+ switch (subcommand.toLowerCase()) {
2528
+ case 'list': {
2529
+ const builtins = [...BUILTIN_WORKFLOWS].sort((a, b) => a.name.localeCompare(b.name));
2530
+ let local = [];
2531
+ let loadError;
2532
+ try {
2533
+ local = engine.loadWorkflows(cwd);
2534
+ }
2535
+ catch (error) {
2536
+ loadError = error?.message || String(error);
2537
+ }
2538
+ const lines = [`${theme.brand('Workflows')} ${theme.dim(workflowDir)}`, ''];
2539
+ lines.push(formatWorkflowSection('Built-in', builtins), '', formatWorkflowSection('Project', local));
2540
+ if (loadError) {
2541
+ lines.push('', theme.warn(`warning: ${loadError}`));
2542
+ }
2543
+ return `${lines.join('\n')}\n`;
2544
+ }
2545
+ case 'run': {
2546
+ if (!rawName)
2547
+ return theme.warn('usage: /workflow run <name>\n');
2548
+ const workflow = findWorkflowByName(rawName, engine, cwd);
2549
+ if (!workflow)
2550
+ return theme.warn(`workflow not found: ${rawName}\n`);
2551
+ const validation = engine.validateWorkflow(workflow);
2552
+ if (validation.length > 0) {
2553
+ return `${formatValidationErrors(workflow.name, validation)}\n`;
2554
+ }
2555
+ const result = await engine.run(workflow, { cwd });
2556
+ return `${formatWorkflowRun(workflow.name, result)}\n`;
2557
+ }
2558
+ case 'new': {
2559
+ if (!rawName)
2560
+ return theme.warn('usage: /workflow new <name>\n');
2561
+ const name = normalizeWorkflowName(rawName);
2562
+ const targetPath = path.join(workflowDir, `${name}.yaml`);
2563
+ if (fs.existsSync(targetPath)) {
2564
+ return theme.warn(`workflow already exists: ${targetPath}\n`);
2565
+ }
2566
+ fs.mkdirSync(workflowDir, { recursive: true });
2567
+ const workflow = getBuiltinWorkflow(name) ?? createWorkflowTemplate(name);
2568
+ fs.writeFileSync(targetPath, renderWorkflowYaml(workflow), 'utf8');
2569
+ return theme.ok(`✔ created ${targetPath}\n`);
2570
+ }
2571
+ case 'validate': {
2572
+ if (!rawName)
2573
+ return theme.warn('usage: /workflow validate <name>\n');
2574
+ const workflow = findWorkflowByName(rawName, engine, cwd);
2575
+ if (!workflow)
2576
+ return theme.warn(`workflow not found: ${rawName}\n`);
2577
+ const errors = engine.validateWorkflow(workflow);
2578
+ if (errors.length === 0) {
2579
+ return theme.ok(`✔ workflow "${workflow.name}" is valid\n`);
2580
+ }
2581
+ return `${formatValidationErrors(workflow.name, errors)}\n`;
2582
+ }
2583
+ default:
2584
+ return theme.warn('usage: /workflow <list|run|new|validate> [name]\n');
2585
+ }
2586
+ }
2587
+ function findWorkflowByName(name, engine, cwd) {
2588
+ const normalized = normalizeWorkflowName(name);
2589
+ try {
2590
+ const local = engine.loadWorkflows(cwd);
2591
+ const localMatch = local.find((workflow) => normalizeWorkflowName(workflow.name) === normalized);
2592
+ if (localMatch)
2593
+ return localMatch;
2594
+ }
2595
+ catch {
2596
+ /* ignore local workflow parse failures here */
2597
+ }
2598
+ return getBuiltinWorkflow(normalized);
2599
+ }
2600
+ function normalizeWorkflowName(name) {
2601
+ return name
2602
+ .trim()
2603
+ .toLowerCase()
2604
+ .replace(/[^a-z0-9]+/g, '-')
2605
+ .replace(/^-+|-+$/g, '');
2606
+ }
2607
+ function formatWorkflowSection(title, workflows) {
2608
+ if (workflows.length === 0) {
2609
+ return ` ${theme.brand(title)}\n ${theme.dim('none')}`;
2610
+ }
2611
+ return [
2612
+ ` ${theme.brand(title)}`,
2613
+ ...workflows.map((workflow) => ` ${theme.hl(workflow.name)} ${theme.dim(`- ${workflow.description}`)}`),
2614
+ ].join('\n');
2615
+ }
2616
+ function formatValidationErrors(name, errors) {
2617
+ return [
2618
+ `${theme.brand('Workflow validation')} ${theme.dim(name)}`,
2619
+ ...errors.map((error) => ` - ${error.path}: ${error.message}`),
2620
+ ].join('\n');
2621
+ }
2622
+ function formatWorkflowRun(name, result) {
2623
+ const lines = [
2624
+ `${theme.brand('Workflow run')} ${theme.dim(name)}`,
2625
+ ` status: ${result.success ? theme.ok('success') : theme.err('failed')}`,
2626
+ ` duration: ${theme.dim(`${result.duration}ms`)}`,
2627
+ ];
2628
+ for (const step of result.steps) {
2629
+ lines.push(` - ${step.stepId}: ${step.success ? theme.ok('ok') : theme.err('failed')}${step.error ? ` ${theme.dim(step.error)}` : ''}`);
2630
+ }
2631
+ return lines.join('\n');
2632
+ }
2633
+ function errorWatchCommand(args) {
2634
+ const [subcommand, ...rest] = args;
2635
+ const action = subcommand?.toLowerCase();
2636
+ if (!action) {
2637
+ return `${theme.brand('Error watch')}\n /error-watch start <cmd>\n /error-watch stop\n /error-watch status\n`;
2638
+ }
2639
+ switch (action) {
2640
+ case 'start': {
2641
+ const command = rest.join(' ').trim();
2642
+ if (!command) {
2643
+ return theme.warn('usage: /error-watch start <cmd>\n');
2644
+ }
2645
+ try {
2646
+ errorWatcher.start(command);
2647
+ return `${theme.ok('✔ error watcher started')}\n${formatErrorWatchStatus()}`;
2648
+ }
2649
+ catch (error) {
2650
+ const message = error instanceof Error ? error.message : String(error);
2651
+ return theme.err(`failed to start error watcher: ${message}\n`);
2652
+ }
2653
+ }
2654
+ case 'stop':
2655
+ errorWatcher.stop();
2656
+ return `${theme.ok('✔ error watcher stopped')}\n${formatErrorWatchStatus()}`;
2657
+ case 'status':
2658
+ return formatErrorWatchStatus();
2659
+ default:
2660
+ return `${theme.warn(`unknown error-watch subcommand: ${subcommand}`)}\n${theme.dim('usage: /error-watch start <cmd> | /error-watch stop | /error-watch status\n')}`;
2661
+ }
2662
+ }
2663
+ function formatErrorWatchStatus() {
2664
+ const errors = errorWatcher.getErrors();
2665
+ const lines = [
2666
+ theme.brand('Error watch status'),
2667
+ ` active: ${theme.hl(errorWatcher.isRunning() ? 'yes' : 'no')}`,
2668
+ ` command: ${theme.hl(errorWatcher.getCommand() ?? 'n/a')}`,
2669
+ ` errors: ${theme.hl(String(errors.length))}`,
2670
+ ];
2671
+ if (!errors.length) {
2672
+ lines.push('', theme.dim('No parsed errors yet.'));
2673
+ return `${lines.join('\n')}\n`;
2674
+ }
2675
+ lines.push('', theme.brand('Latest errors'));
2676
+ for (const error of errors.slice(-5)) {
2677
+ lines.push(` - ${formatParsedError(error)}`);
2678
+ }
2679
+ return `${lines.join('\n')}\n`;
2680
+ }
2681
+ function formatParsedError(error) {
2682
+ const location = [
2683
+ error.file,
2684
+ error.line !== undefined ? String(error.line) : undefined,
2685
+ error.column !== undefined ? String(error.column) : undefined,
2686
+ ]
2687
+ .filter((part) => Boolean(part))
2688
+ .join(':');
2689
+ const prefix = [error.severity, error.code, location]
2690
+ .filter((part) => Boolean(part))
2691
+ .join(' ');
2692
+ return `${prefix}: ${error.message}`;
2693
+ }
2694
+ function formatStackTraceSummary(trace, analysis) {
2695
+ const relevant = analysis.relevantFrames.length
2696
+ ? analysis.relevantFrames
2697
+ .map((frame, index) => {
2698
+ const location = `${frame.file}:${frame.line}${frame.column ? `:${frame.column}` : ''}`;
2699
+ const fn = frame.function ? ` ${theme.dim(`(${frame.function})`)}` : '';
2700
+ return ` ${index + 1}. ${location}${fn}`;
2701
+ })
2702
+ .join('\n')
2703
+ : ' none\n';
2704
+ const files = analysis.userFiles.length
2705
+ ? analysis.userFiles.map((file) => ` - ${file}`).join('\n')
2706
+ : ' - none detected';
2707
+ const rootCauseLocation = analysis.rootCause.line > 0
2708
+ ? `${analysis.rootCause.file}:${analysis.rootCause.line}${analysis.rootCause.column ? `:${analysis.rootCause.column}` : ''}`
2709
+ : analysis.rootCause.file;
2710
+ return [
2711
+ `${theme.brand('Stack trace analysis')} ${theme.dim(trace.type)}`,
2712
+ ` error: ${trace.error}`,
2713
+ ` root cause: ${rootCauseLocation}${analysis.rootCause.function ? ` ${theme.dim(`(${analysis.rootCause.function})`)}` : ''}`,
2714
+ ' relevant frames:',
2715
+ relevant,
2716
+ ' user files:',
2717
+ files,
2718
+ ` suggestion: ${analysis.suggestion}`,
2719
+ '',
2720
+ ].join('\n');
2721
+ }