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.
- package/CHANGELOG.md +250 -0
- package/LICENSE +21 -0
- package/README.md +214 -0
- package/bin/icopilot.js +6 -0
- package/dist/acp/router.js +123 -0
- package/dist/acp/schema.js +53 -0
- package/dist/agents/aggregator.js +187 -0
- package/dist/agents/custom-agents.js +97 -0
- package/dist/agents/goal-driven.js +411 -0
- package/dist/agents/multi-repo.js +350 -0
- package/dist/agents/parallel-runner.js +181 -0
- package/dist/agents/router.js +144 -0
- package/dist/agents/self-heal.js +481 -0
- package/dist/agents/tdd-agent.js +278 -0
- package/dist/api/github-models.js +158 -0
- package/dist/bridge/ide-bridge.js +479 -0
- package/dist/cloud/routine-executor.js +34 -0
- package/dist/cloud/routine-scheduler.js +67 -0
- package/dist/cloud/routine-storage.js +297 -0
- package/dist/commands/acp-cmd.js +143 -0
- package/dist/commands/actions-cmd.js +624 -0
- package/dist/commands/agent-cmd.js +144 -0
- package/dist/commands/alias-cmd.js +132 -0
- package/dist/commands/bookmark-cmd.js +77 -0
- package/dist/commands/changelog-cmd.js +99 -0
- package/dist/commands/changes-cmd.js +120 -0
- package/dist/commands/clipboard-cmd.js +217 -0
- package/dist/commands/cloud-routine-cmd.js +265 -0
- package/dist/commands/codegen-cmd.js +544 -0
- package/dist/commands/compare-cmd.js +116 -0
- package/dist/commands/context-cmd.js +247 -0
- package/dist/commands/context-viz-cmd.js +43 -0
- package/dist/commands/conventions-cmd.js +116 -0
- package/dist/commands/cost-cmd.js +51 -0
- package/dist/commands/deps-cmd.js +294 -0
- package/dist/commands/diagram-cmd.js +658 -0
- package/dist/commands/diff-review-cmd.js +92 -0
- package/dist/commands/doc-cmd.js +412 -0
- package/dist/commands/doctor-cmd.js +152 -0
- package/dist/commands/editor-cmd.js +49 -0
- package/dist/commands/env-cmd.js +86 -0
- package/dist/commands/explain-cmd.js +78 -0
- package/dist/commands/explain-shell-cmd.js +22 -0
- package/dist/commands/explore-cmd.js +231 -0
- package/dist/commands/feedback-cmd.js +98 -0
- package/dist/commands/fix-cmd.js +17 -0
- package/dist/commands/generate-cmd.js +38 -0
- package/dist/commands/git-extra.js +197 -0
- package/dist/commands/git-log-cmd.js +98 -0
- package/dist/commands/git-undo-cmd.js +137 -0
- package/dist/commands/git.js +155 -0
- package/dist/commands/history-cmd.js +122 -0
- package/dist/commands/index-cmd.js +65 -0
- package/dist/commands/init-cmd.js +73 -0
- package/dist/commands/lint-cmd.js +133 -0
- package/dist/commands/memory-cmd.js +98 -0
- package/dist/commands/metrics-cmd.js +97 -0
- package/dist/commands/mode-prefix.js +30 -0
- package/dist/commands/multi-cmd.js +44 -0
- package/dist/commands/notify-cmd.js +204 -0
- package/dist/commands/profile-cmd.js +101 -0
- package/dist/commands/prompts.js +17 -0
- package/dist/commands/rag-cmd.js +60 -0
- package/dist/commands/readme-cmd.js +564 -0
- package/dist/commands/reasoning-cmd.js +34 -0
- package/dist/commands/refactor-cmd.js +96 -0
- package/dist/commands/release-cmd.js +450 -0
- package/dist/commands/repo-cmd.js +195 -0
- package/dist/commands/route-cmd.js +21 -0
- package/dist/commands/schedule-cmd.js +109 -0
- package/dist/commands/search-cmd.js +47 -0
- package/dist/commands/security-cmd.js +156 -0
- package/dist/commands/settings-cmd.js +238 -0
- package/dist/commands/skill-cmd.js +338 -0
- package/dist/commands/slash.js +2721 -0
- package/dist/commands/snippets-cmd.js +83 -0
- package/dist/commands/space-cmd.js +92 -0
- package/dist/commands/stash-cmd.js +156 -0
- package/dist/commands/stats-cmd.js +36 -0
- package/dist/commands/style-cmd.js +85 -0
- package/dist/commands/suggest-cmd.js +40 -0
- package/dist/commands/summary-cmd.js +138 -0
- package/dist/commands/task-cmd.js +58 -0
- package/dist/commands/team-memory-cmd.js +97 -0
- package/dist/commands/template-cmd.js +475 -0
- package/dist/commands/test-cmd.js +146 -0
- package/dist/commands/todo-cmd.js +172 -0
- package/dist/commands/tokens-cmd.js +277 -0
- package/dist/commands/trigger-cmd.js +147 -0
- package/dist/commands/undo-cmd.js +18 -0
- package/dist/commands/voice-cmd.js +89 -0
- package/dist/commands/watch-cmd.js +110 -0
- package/dist/commands/web-cmd.js +183 -0
- package/dist/commands/worktree-cmd.js +119 -0
- package/dist/config-profile.js +66 -0
- package/dist/config.js +288 -0
- package/dist/context/compactor.js +53 -0
- package/dist/context/dep-context.js +329 -0
- package/dist/context/file-refs.js +54 -0
- package/dist/context/git-context.js +229 -0
- package/dist/context/image-input.js +66 -0
- package/dist/context/memory.js +55 -0
- package/dist/context/persistent-memory.js +104 -0
- package/dist/context/pinned.js +96 -0
- package/dist/context/priority.js +150 -0
- package/dist/context/read-only.js +48 -0
- package/dist/context/smart-files.js +286 -0
- package/dist/context/team-memory.js +156 -0
- package/dist/extensions/loader.js +149 -0
- package/dist/extensions/marketplace.js +49 -0
- package/dist/extensions/slack-provider.js +181 -0
- package/dist/extensions/team.js +56 -0
- package/dist/extensions/teams-provider.js +222 -0
- package/dist/extensions/voice.js +18 -0
- package/dist/hooks/lifecycle.js +215 -0
- package/dist/hooks/precommit.js +463 -0
- package/dist/index/embeddings.js +23 -0
- package/dist/index/indexer.js +86 -0
- package/dist/index/retrieve.js +20 -0
- package/dist/index/store.js +95 -0
- package/dist/index.js +286 -0
- package/dist/intelligence/dead-code.js +457 -0
- package/dist/intelligence/error-watch.js +263 -0
- package/dist/intelligence/navigation.js +141 -0
- package/dist/intelligence/stack-trace.js +210 -0
- package/dist/intelligence/symbol-index.js +410 -0
- package/dist/knowledge/auto-memory.js +412 -0
- package/dist/knowledge/conventions.js +475 -0
- package/dist/knowledge/corrections.js +213 -0
- package/dist/knowledge/rag.js +450 -0
- package/dist/knowledge/style-learner.js +324 -0
- package/dist/logger.js +35 -0
- package/dist/mcp/client.js +144 -0
- package/dist/mcp/config.js +24 -0
- package/dist/mcp/index.js +89 -0
- package/dist/modes/auto-compact.js +20 -0
- package/dist/modes/autopilot.js +157 -0
- package/dist/modes/background.js +82 -0
- package/dist/modes/interactive.js +187 -0
- package/dist/modes/oneshot.js +36 -0
- package/dist/modes/tui.js +265 -0
- package/dist/modes/turn.js +342 -0
- package/dist/notifications/manager.js +107 -0
- package/dist/plugins/marketplace.js +244 -0
- package/dist/providers/custom-provider.js +298 -0
- package/dist/providers/local-model.js +121 -0
- package/dist/routing/profiles.js +44 -0
- package/dist/routing/router.js +18 -0
- package/dist/sandbox/container.js +151 -0
- package/dist/security/audit.js +237 -0
- package/dist/security/content-filter.js +449 -0
- package/dist/security/proxy.js +301 -0
- package/dist/security/retention.js +281 -0
- package/dist/security/roles.js +252 -0
- package/dist/server/api-server.js +679 -0
- package/dist/session/bookmarks.js +72 -0
- package/dist/session/cloud-session.js +291 -0
- package/dist/session/handoff.js +405 -0
- package/dist/session/manager.js +35 -0
- package/dist/session/session.js +296 -0
- package/dist/session/share.js +313 -0
- package/dist/session/undo-journal.js +91 -0
- package/dist/snippets/store.js +60 -0
- package/dist/spaces/space-config.js +156 -0
- package/dist/spaces/space.js +220 -0
- package/dist/stats/store.js +101 -0
- package/dist/tools/apply-patch.js +134 -0
- package/dist/tools/auto-check.js +218 -0
- package/dist/tools/diff-edit.js +150 -0
- package/dist/tools/diff-prompt.js +36 -0
- package/dist/tools/edit-file.js +66 -0
- package/dist/tools/file-ops.js +205 -0
- package/dist/tools/glob.js +17 -0
- package/dist/tools/grep.js +56 -0
- package/dist/tools/image.js +194 -0
- package/dist/tools/list-directory.js +228 -0
- package/dist/tools/memory.js +17 -0
- package/dist/tools/multi-edit.js +299 -0
- package/dist/tools/policy.js +95 -0
- package/dist/tools/registry.js +484 -0
- package/dist/tools/retry.js +74 -0
- package/dist/tools/run-in-terminal.js +162 -0
- package/dist/tools/safety.js +64 -0
- package/dist/tools/sandbox.js +15 -0
- package/dist/tools/search-symbols.js +212 -0
- package/dist/tools/shell.js +118 -0
- package/dist/tools/web.js +167 -0
- package/dist/ui/prompt.js +37 -0
- package/dist/ui/render.js +96 -0
- package/dist/ui/screen.js +13 -0
- package/dist/ui/theme.js +56 -0
- package/dist/util/browser.js +34 -0
- package/dist/util/completion.js +350 -0
- package/dist/util/cost.js +28 -0
- package/dist/util/keybindings.js +113 -0
- package/dist/util/lazy.js +26 -0
- package/dist/util/perf.js +25 -0
- package/dist/util/token-worker.js +11 -0
- package/dist/util/tokens.js +50 -0
- package/dist/workflows/builtins.js +128 -0
- package/dist/workflows/engine.js +496 -0
- package/dist/workflows/file-trigger.js +197 -0
- 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
|
+
}
|