@semalt-ai/code 1.8.5 → 1.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. package/.claude/settings.local.json +7 -1
  2. package/.github/workflows/ci.yml +69 -0
  3. package/ARCHITECTURE.md +6 -95
  4. package/CLAUDE.md +196 -316
  5. package/README.md +148 -4
  6. package/docs/ARCHITECTURE.md +1321 -0
  7. package/docs/CONFIG.md +340 -0
  8. package/docs/HISTORY.md +245 -0
  9. package/examples/embed.js +74 -0
  10. package/index.js +251 -10
  11. package/lib/agent.js +856 -120
  12. package/lib/api.js +239 -50
  13. package/lib/args.js +74 -2
  14. package/lib/audit.js +23 -1
  15. package/lib/background.js +584 -0
  16. package/lib/checkpoints.js +757 -0
  17. package/lib/commands/auth.js +94 -0
  18. package/lib/commands/chat-session.js +489 -0
  19. package/lib/commands/chat-slash.js +415 -0
  20. package/lib/commands/chat-turn.js +669 -0
  21. package/lib/commands/chat.js +407 -0
  22. package/lib/commands/custom.js +157 -0
  23. package/lib/commands/history-utils.js +66 -0
  24. package/lib/commands/index.js +268 -0
  25. package/lib/commands/mcp.js +113 -0
  26. package/lib/commands/oneshot.js +193 -0
  27. package/lib/commands/registry.js +269 -0
  28. package/lib/commands/tasks.js +89 -0
  29. package/lib/compact.js +87 -0
  30. package/lib/config.js +360 -11
  31. package/lib/constants.js +401 -3
  32. package/lib/deny.js +199 -0
  33. package/lib/doctor.js +160 -0
  34. package/lib/headless.js +202 -0
  35. package/lib/hooks.js +286 -0
  36. package/lib/images.js +270 -0
  37. package/lib/internals.js +49 -0
  38. package/lib/mcp/boundary.js +131 -0
  39. package/lib/mcp/client.js +270 -0
  40. package/lib/mcp/oauth.js +134 -0
  41. package/lib/memory.js +209 -0
  42. package/lib/metrics.js +37 -2
  43. package/lib/payload.js +54 -0
  44. package/lib/permission-rules.js +401 -0
  45. package/lib/permissions.js +123 -26
  46. package/lib/pricing.js +67 -0
  47. package/lib/proc.js +62 -0
  48. package/lib/prompts.js +99 -8
  49. package/lib/sandbox.js +568 -0
  50. package/lib/sdk.js +328 -0
  51. package/lib/secrets.js +211 -0
  52. package/lib/skills.js +223 -0
  53. package/lib/subagents.js +516 -0
  54. package/lib/tool_registry.js +2862 -0
  55. package/lib/tool_specs.js +263 -9
  56. package/lib/tools.js +352 -1039
  57. package/lib/ui/anim.js +86 -0
  58. package/lib/ui/ansi.js +17 -27
  59. package/lib/ui/chat-history.js +253 -71
  60. package/lib/ui/create-ui.js +67 -24
  61. package/lib/ui/diff.js +90 -25
  62. package/lib/ui/file-activity.js +236 -0
  63. package/lib/ui/format.js +195 -29
  64. package/lib/ui/input-field.js +21 -11
  65. package/lib/ui/md-stream.js +234 -0
  66. package/lib/ui/render-operation.js +113 -0
  67. package/lib/ui/select.js +1 -4
  68. package/lib/ui/status-bar.js +146 -36
  69. package/lib/ui/stream.js +20 -13
  70. package/lib/ui/theme.js +190 -44
  71. package/lib/ui/tool-operation.js +190 -0
  72. package/lib/ui/utils.js +9 -5
  73. package/lib/ui/web-activity.js +270 -0
  74. package/lib/ui/writer.js +159 -45
  75. package/lib/ui.js +1 -1
  76. package/lib/verify.js +229 -0
  77. package/lib/web-extract.js +213 -0
  78. package/lib/web-summarize.js +68 -0
  79. package/package.json +19 -4
  80. package/scripts/lint.js +57 -0
  81. package/test/agent-loop.test.js +389 -0
  82. package/test/anim-driver.test.js +153 -0
  83. package/test/ask-user-display.test.js +226 -0
  84. package/test/ask-user-gate.test.js +231 -0
  85. package/test/background.test.js +414 -0
  86. package/test/chat-history-nocolor.test.js +155 -0
  87. package/test/chat-relogin.test.js +207 -0
  88. package/test/chat.test.js +114 -0
  89. package/test/checkpoints-agent.test.js +181 -0
  90. package/test/checkpoints.test.js +650 -0
  91. package/test/command-registry.test.js +160 -0
  92. package/test/compact.test.js +116 -0
  93. package/test/completion-lazy.test.js +52 -0
  94. package/test/config-merge.test.js +324 -0
  95. package/test/config-quarantine.test.js +128 -0
  96. package/test/config-write-guard-allow-anywhere.test.js +56 -0
  97. package/test/config-write-guard-skip.test.js +46 -0
  98. package/test/config-write-guard.test.js +153 -0
  99. package/test/context-split.test.js +215 -0
  100. package/test/cost-doctor.test.js +142 -0
  101. package/test/custom-commands-chat.test.js +106 -0
  102. package/test/custom-commands.test.js +230 -0
  103. package/test/defer-detail-band.test.js +403 -0
  104. package/test/deny-windows.test.js +120 -0
  105. package/test/deny.test.js +83 -0
  106. package/test/detail-band-tab-flatten.test.js +242 -0
  107. package/test/download-allow-anywhere.test.js +66 -0
  108. package/test/download-confine.test.js +153 -0
  109. package/test/exec-diff.test.js +268 -0
  110. package/test/executors.test.js +599 -0
  111. package/test/extract-tool-calls.test.js +349 -0
  112. package/test/fetch-url-validation.test.js +219 -0
  113. package/test/file-activity.test.js +522 -0
  114. package/test/fixtures/tool-calls.js +57 -0
  115. package/test/fixtures/web-page.js +91 -0
  116. package/test/git-tools.test.js +384 -0
  117. package/test/grep-glob-serialize.test.js +242 -0
  118. package/test/grep-glob.test.js +268 -0
  119. package/test/grep-path-target.test.js +227 -0
  120. package/test/harness/README.md +57 -0
  121. package/test/harness/chat-harness.js +143 -0
  122. package/test/harness/memwarn-headless-child.js +65 -0
  123. package/test/harness/mock-llm.js +120 -0
  124. package/test/harness/mock-mcp-server.js +142 -0
  125. package/test/harness/sse-server.js +69 -0
  126. package/test/headless.test.js +348 -0
  127. package/test/history-utils.test.js +88 -0
  128. package/test/hooks-agent.test.js +238 -0
  129. package/test/hooks-verify-sandbox.test.js +232 -0
  130. package/test/hooks.test.js +216 -0
  131. package/test/http-get-user-agent.test.js +142 -0
  132. package/test/images-api.test.js +208 -0
  133. package/test/images.test.js +238 -0
  134. package/test/input-field-ctrl-o.test.js +37 -0
  135. package/test/live-height-physical.test.js +281 -0
  136. package/test/max-iterations.test.js +218 -0
  137. package/test/mcp-boundary.test.js +57 -0
  138. package/test/mcp-client.test.js +267 -0
  139. package/test/mcp-oauth.test.js +86 -0
  140. package/test/md-stream.test.js +183 -0
  141. package/test/memory-truncation-warning.test.js +222 -0
  142. package/test/memory.test.js +198 -0
  143. package/test/native-dispatch.test.js +409 -0
  144. package/test/native-live-narration.test.js +254 -0
  145. package/test/output-chokepoint.test.js +188 -0
  146. package/test/output-heredoc-leak.test.js +195 -0
  147. package/test/output-preview.test.js +245 -0
  148. package/test/path-guards.test.js +134 -0
  149. package/test/payload.test.js +99 -0
  150. package/test/permission-rules-agent.test.js +210 -0
  151. package/test/permission-rules.test.js +297 -0
  152. package/test/permissions.test.js +362 -0
  153. package/test/plan-mode.test.js +167 -0
  154. package/test/read-paginate.test.js +275 -0
  155. package/test/readonly-tools.test.js +177 -0
  156. package/test/render-operation.test.js +317 -0
  157. package/test/replay-descriptor-xml.test.js +216 -0
  158. package/test/replay-descriptor.test.js +189 -0
  159. package/test/replay-web-aggregate.test.js +291 -0
  160. package/test/replay-web-persist.test.js +241 -0
  161. package/test/result-cap.test.js +233 -0
  162. package/test/running-glyph-anim.test.js +111 -0
  163. package/test/sandbox-agent.test.js +147 -0
  164. package/test/sandbox-integration.test.js +216 -0
  165. package/test/sandbox.test.js +408 -0
  166. package/test/sdk.test.js +234 -0
  167. package/test/shell-output-cap.test.js +181 -0
  168. package/test/skills-chat.test.js +110 -0
  169. package/test/skills.test.js +295 -0
  170. package/test/smoke.test.js +68 -0
  171. package/test/status-bar-driver.test.js +93 -0
  172. package/test/status-bar-pause.test.js +164 -0
  173. package/test/status-bar-resync.test.js +188 -0
  174. package/test/stream-parser.test.js +171 -0
  175. package/test/subagents-agent.test.js +178 -0
  176. package/test/subagents.test.js +222 -0
  177. package/test/theme-palette.test.js +166 -0
  178. package/test/tool-registry.test.js +85 -0
  179. package/test/trim-budget.test.js +101 -0
  180. package/test/truncate-visible.test.js +78 -0
  181. package/test/verify-agent.test.js +317 -0
  182. package/test/verify.test.js +141 -0
  183. package/test/view-image.test.js +199 -0
  184. package/test/web-activity-ordering.test.js +203 -0
  185. package/test/web-activity.test.js +207 -0
  186. package/test/web-data-extraction-guidance.test.js +71 -0
  187. package/test/web-extract.test.js +185 -0
  188. package/test/web-fetch-agent.test.js +291 -0
  189. package/test/web-fetch-mode.test.js +193 -0
  190. package/test/web-search.test.js +380 -0
  191. package/lib/commands.js +0 -1438
  192. package/path +0 -1
@@ -0,0 +1,415 @@
1
+ 'use strict';
2
+
3
+ // In-chat slash-command handlers, extracted from cmdChat in Task 1.5. Keyed by
4
+ // the canonical registry name (lib/commands/registry.js). Bodies are unchanged
5
+ // except that mutable session fields go through ctx and the session/picker
6
+ // helpers (saveSession, showPendingStep, …) are read from ctx (where cmdChat
7
+ // assigned them). The registry ↔ handler parity check stays in cmdChat.
8
+
9
+ function createSlashHandlers(ctx) {
10
+ const {
11
+ chatHistory, statusBar, inputField, storage, permissionManager,
12
+ getConfig, setConfig, helpText, readFileContext,
13
+ refreshInputSearchItems, showPendingStep, activateNavCapture, saveSession,
14
+ destroy, buildExitArtifacts,
15
+ dashboardListChats, dashboardListModels, dashboardWhoAmI, dashboardLogout,
16
+ loginFlow, ensureDefaultModel, resolveTokenLimit,
17
+ estimateTokens, configShow, isNativeToolsActive, getSystemPrompt,
18
+ agentExecShell, confirmCatastrophicShell, opts, fs, AUDIT_LOG, dbg,
19
+ checkpointStore,
20
+ } = ctx;
21
+
22
+ return {
23
+ exit: async () => {
24
+ saveSession();
25
+ destroy(buildExitArtifacts());
26
+ ctx.resolveExit();
27
+ },
28
+
29
+ '/help': async () => {
30
+ chatHistory.addMessage({ role: 'system', content: helpText() });
31
+ },
32
+
33
+ '/history': async () => {
34
+ if (!getConfig().auth_token) { chatHistory.addMessage({ role: 'system', content: 'Not logged in. Run /login first.' }); return; }
35
+ const sessions = storage.list();
36
+ if (!sessions.length) { chatHistory.addMessage({ role: 'system', content: 'No saved sessions.' }); return; }
37
+ refreshInputSearchItems();
38
+ chatHistory.addMessage({ role: 'system', content: '/history' });
39
+ ctx.pendingAction = { type: 'history', items: sessions, stepIdx: 0 };
40
+ showPendingStep();
41
+ statusBar.update('waiting', 'Select session...');
42
+ activateNavCapture();
43
+ },
44
+
45
+ '/chats': async () => {
46
+ const config = getConfig();
47
+ if (!config.auth_token) { chatHistory.addMessage({ role: 'system', content: 'Not logged in. Run /login first.' }); return; }
48
+ inputField.setDisabled(true);
49
+ statusBar.update('thinking', 'Loading chats...');
50
+ try {
51
+ const response = await dashboardListChats();
52
+ const chats = Array.isArray(response && response.chats) ? response.chats : [];
53
+ if (!chats.length) { chatHistory.addMessage({ role: 'system', content: 'No saved chats found.' }); statusBar.update('idle'); }
54
+ else {
55
+ refreshInputSearchItems(chats.map(c => ({ type: 'chat', text: c.title || `chat #${c.id}` })));
56
+ chatHistory.addMessage({ role: 'system', content: '/chats' });
57
+ ctx.pendingAction = { type: 'chats', items: chats, stepIdx: 0 };
58
+ showPendingStep();
59
+ statusBar.update('waiting', 'Select chat...');
60
+ activateNavCapture();
61
+ }
62
+ } catch (err) {
63
+ chatHistory.addMessage({ role: 'system', content: `✗ ${err.message}`, isError: true });
64
+ statusBar.update('idle');
65
+ }
66
+ inputField.setDisabled(false);
67
+ },
68
+
69
+ '/new': async () => {
70
+ ctx.messages = [];
71
+ ctx.currentChatId = null; ctx.savedUpTo = 0;
72
+ permissionManager.clear();
73
+ chatHistory.addMessage({ role: 'system', content: '✓ Started new conversation.' });
74
+ },
75
+
76
+ '/login': async () => {
77
+ inputField.setDisabled(true);
78
+ statusBar.update('thinking', 'Starting login...');
79
+ await loginFlow(chatHistory, statusBar);
80
+ const picked = await ensureDefaultModel();
81
+ if (picked) {
82
+ ctx.currentModel = picked.modelId;
83
+ ctx.resolvedTokenLimit = await resolveTokenLimit(ctx.currentModel);
84
+ statusBar.setModel(ctx.currentModel);
85
+ statusBar.setContextLimit(ctx.resolvedTokenLimit);
86
+ chatHistory.addMessage({ role: 'system', content: `✓ Model → ${picked.name} (${picked.modelId})` });
87
+ }
88
+ // Relogin is a principal change: the new token may belong to a different
89
+ // user, so the surviving currentChatId would otherwise suppress fresh-chat
90
+ // creation and make saveTurnToDashboard POST to a chat the new user can't
91
+ // see (404 "Chat not found"). Reset chat context AFTER the new token is
92
+ // saved (loginFlow) and the model resolved (ensureDefaultModel) so the next
93
+ // turn's createChatIfNeeded lazily creates a fresh chat under the new token.
94
+ // Mirror /new's FULL reset (messages + approvals too): retaining the prior
95
+ // user's messages/approvals across a token change is unsafe. The way back to
96
+ // an old chat is explicit --resume / /chats.
97
+ ctx.messages = [];
98
+ ctx.currentChatId = null; ctx.savedUpTo = 0;
99
+ permissionManager.clear();
100
+ statusBar.update('idle');
101
+ inputField.setDisabled(false);
102
+ },
103
+
104
+ '/whoami': async () => {
105
+ inputField.setDisabled(true);
106
+ statusBar.update('thinking', 'Loading...');
107
+ try {
108
+ const response = await dashboardWhoAmI();
109
+ const user = response && response.user ? response.user : null;
110
+ if (!user) { chatHistory.addMessage({ role: 'system', content: '✗ Unable to load current user.', isError: true }); }
111
+ else {
112
+ chatHistory.addMessage({ role: 'system', content: `Current User:\n ID: ${user.id}\n Email: ${user.email || '-'}\n Name: ${user.name || '-'}\n Provider: ${user.provider || '-'}` });
113
+ }
114
+ } catch (err) {
115
+ chatHistory.addMessage({ role: 'system', content: `✗ ${err.message}`, isError: true });
116
+ }
117
+ statusBar.update('idle');
118
+ inputField.setDisabled(false);
119
+ },
120
+
121
+ '/logout': async () => {
122
+ const config = getConfig();
123
+ if (!config.auth_token) { chatHistory.addMessage({ role: 'system', content: '✗ Not logged in.' }); return; }
124
+ inputField.setDisabled(true);
125
+ statusBar.update('thinking', 'Logging out...');
126
+ // Defense-in-depth: drop the chat context up front, REGARDLESS of whether
127
+ // the dashboardLogout HTTP call below succeeds (it early-returns on a
128
+ // non-401 error), so no later save can target the logged-out user's chat.
129
+ ctx.currentChatId = null; ctx.savedUpTo = 0;
130
+ try { await dashboardLogout(); } catch (err) { if (err.statusCode !== 401) { chatHistory.addMessage({ role: 'system', content: `✗ ${err.message}`, isError: true }); statusBar.update('idle'); inputField.setDisabled(false); return; } }
131
+ setConfig({ ...config, auth_token: '' });
132
+ chatHistory.addMessage({ role: 'system', content: '✓ Logged out and cleared local CLI token.' });
133
+ statusBar.update('idle');
134
+ inputField.setDisabled(false);
135
+ },
136
+
137
+ '/file': async (arg) => {
138
+ const fp = arg;
139
+ const fileCtx = readFileContext([fp]);
140
+ if (fileCtx) {
141
+ ctx.messages.push({ role: 'user', content: `Here is the file context:\n${fileCtx}` });
142
+ chatHistory.addMessage({ role: 'system', content: `✓ Loaded: ${fp}` });
143
+ } else {
144
+ chatHistory.addMessage({ role: 'system', content: `✗ Could not load: ${fp}`, isError: true });
145
+ }
146
+ },
147
+
148
+ '/image': async (arg) => {
149
+ // Multimodal image input (Task 5.4). Stage an image (read through
150
+ // isPathSafe, size-checked, base64-encoded) for the NEXT user turn, which
151
+ // attaches and clears it (chat-turn.js). Repeatable to attach several.
152
+ const fp = (arg || '').trim();
153
+ if (!fp) { chatHistory.addMessage({ role: 'system', content: '✗ Usage: /image <path>', isError: true }); return; }
154
+ const { readImage } = require('../images');
155
+ const { isPathSafe } = require('../tools');
156
+ try {
157
+ const img = readImage(fp, { maxBytes: getConfig().image_max_bytes, isPathSafe });
158
+ ctx.pendingImages = (ctx.pendingImages || []).concat([img]);
159
+ const kb = Math.max(1, Math.round(img.bytes / 1024));
160
+ chatHistory.addMessage({ role: 'system', content: `✓ Attached image: ${fp} (${img.media_type}, ${kb} KB). Sent with your next message.` });
161
+ } catch (err) {
162
+ chatHistory.addMessage({ role: 'system', content: `✗ ${err.message}`, isError: true });
163
+ }
164
+ },
165
+
166
+ '/models': async () => {
167
+ if (!getConfig().auth_token) { chatHistory.addMessage({ role: 'system', content: 'Not logged in. Run /login first.' }); return; }
168
+ inputField.setDisabled(true);
169
+ statusBar.update('thinking', 'Loading models...');
170
+ try {
171
+ const response = await dashboardListModels();
172
+ const models = Array.isArray(response && response.models) ? response.models : [];
173
+ if (!models.length) { chatHistory.addMessage({ role: 'system', content: '✗ No models available.' }); statusBar.update('idle'); }
174
+ else {
175
+ chatHistory.addMessage({ role: 'system', content: '/models' });
176
+ ctx.pendingAction = { type: 'models', items: models, stepIdx: 0 };
177
+ showPendingStep();
178
+ statusBar.update('waiting', 'Select model...');
179
+ activateNavCapture();
180
+ }
181
+ } catch (err) {
182
+ chatHistory.addMessage({ role: 'system', content: `✗ ${err.message}`, isError: true });
183
+ statusBar.update('idle');
184
+ }
185
+ inputField.setDisabled(false);
186
+ },
187
+
188
+ '/model': async (arg) => {
189
+ if (!getConfig().auth_token) { chatHistory.addMessage({ role: 'system', content: 'Not logged in. Run /login first.' }); return; }
190
+ if (!arg) {
191
+ chatHistory.addMessage({ role: 'system', content: `Current model: ${ctx.currentModel}` });
192
+ return;
193
+ }
194
+ ctx.currentModel = arg;
195
+ ctx.resolvedTokenLimit = await resolveTokenLimit(ctx.currentModel);
196
+ statusBar.setModel(ctx.currentModel);
197
+ statusBar.setContextLimit(ctx.resolvedTokenLimit);
198
+ chatHistory.addMessage({ role: 'system', content: `✓ Model → ${ctx.currentModel}` });
199
+ },
200
+
201
+ '/clear': async () => {
202
+ ctx.messages = [];
203
+ ctx.currentChatId = null; ctx.savedUpTo = 0;
204
+ permissionManager.clear();
205
+ chatHistory.addMessage({ role: 'system', content: '✓ Conversation and approvals cleared.' });
206
+ },
207
+
208
+ '/compact': async () => {
209
+ const { selectForCompaction, summarizationRequest, buildCompactedMessages, approxTokens } = require('../compact');
210
+ const before = approxTokens(ctx.messages, estimateTokens);
211
+ const sel = selectForCompaction(ctx.messages, { keepRecent: 6 });
212
+ if (!sel.head.length) {
213
+ let msg = `Nothing to compact yet — ${ctx.messages.length} messages, ~${before} tokens.`;
214
+ if (ctx.sessionMetrics) msg += '\n' + ctx.sessionMetrics.summary();
215
+ chatHistory.addMessage({ role: 'system', content: msg });
216
+ return;
217
+ }
218
+ // PreCompact hook (Task 3.4): fire before summarizing. Best-effort.
219
+ try {
220
+ await require('../hooks').createHookRunner({ getConfig })
221
+ .run('PreCompact', { reason: 'manual', messageCount: ctx.messages.length });
222
+ } catch { /* hook failures never block compaction */ }
223
+ inputField.setDisabled(true);
224
+ statusBar.update('thinking', 'Compacting…');
225
+ let summary = '';
226
+ try {
227
+ summary = await ctx.chatSync(summarizationRequest(sel.head), { model: ctx.currentModel });
228
+ } catch (err) {
229
+ chatHistory.addMessage({ role: 'system', content: `✗ Compact failed: ${err.message}`, isError: true });
230
+ statusBar.update('idle'); inputField.setDisabled(false); return;
231
+ }
232
+ if (!summary || !summary.trim()) {
233
+ chatHistory.addMessage({ role: 'system', content: '✗ Compact produced no summary; history unchanged.', isError: true });
234
+ statusBar.update('idle'); inputField.setDisabled(false); return;
235
+ }
236
+ ctx.messages = buildCompactedMessages(sel, summary);
237
+ const after = approxTokens(ctx.messages, estimateTokens);
238
+ chatHistory.addMessage({ role: 'system', content: `✓ Compacted ${sel.head.length} older message(s): ~${before} → ~${after} tokens.` });
239
+ statusBar.update('idle');
240
+ inputField.setDisabled(false);
241
+ },
242
+
243
+ '/config': async () => {
244
+ chatHistory.addMessage({ role: 'system', content: configShow(opts.systemPromptFile || null) });
245
+ },
246
+
247
+ '/memory': async () => {
248
+ const { loadProjectMemory, memoryStatusLines } = require('../memory');
249
+ const result = loadProjectMemory();
250
+ chatHistory.addMessage({ role: 'system', content: memoryStatusLines(result).join('\n') });
251
+ },
252
+
253
+ '/skills': async () => {
254
+ const { loadSkills, skillsStatusLines } = require('../skills');
255
+ const result = loadSkills();
256
+ chatHistory.addMessage({ role: 'system', content: skillsStatusLines(result).join('\n') });
257
+ },
258
+
259
+ '/mcp': async () => {
260
+ const { formatStatus, formatServerList } = require('./mcp');
261
+ const cfgServers = (getConfig().mcp && getConfig().mcp.servers) || {};
262
+ const mgr = ctx.mcpManager;
263
+ const status = mgr ? mgr.status() : [];
264
+ // Show live connection status when we have it; otherwise list what's
265
+ // configured (e.g. no servers connected yet).
266
+ const body = status.length ? formatStatus(status) : formatServerList(cfgServers);
267
+ chatHistory.addMessage({ role: 'system', content: body });
268
+ },
269
+
270
+ '/doctor': async () => {
271
+ const { diagnose, formatDoctorReport } = require('../doctor');
272
+ const ping = async () => {
273
+ const cfg = getConfig();
274
+ if (!cfg.auth_token) return null;
275
+ try { const r = await dashboardWhoAmI(); return !!(r && r.user); } catch { return false; }
276
+ };
277
+ const result = await diagnose({ getConfig, pingDashboard: ping });
278
+ chatHistory.addMessage({ role: 'system', content: formatDoctorReport(result) });
279
+ },
280
+
281
+ '/sandbox': async () => {
282
+ const { sandboxStatusReport } = require('../sandbox');
283
+ chatHistory.addMessage({ role: 'system', content: sandboxStatusReport({ getConfig }) });
284
+ },
285
+
286
+ '/plan': async () => {
287
+ ctx.planMode = !ctx.planMode;
288
+ if (ctx.planMode) {
289
+ chatHistory.addMessage({ role: 'system', content: '◷ Plan mode ON — I\'ll investigate with read-only tools and propose a plan. Mutating actions are withheld until you approve. Run /plan again to approve and execute.' });
290
+ } else {
291
+ chatHistory.addMessage({ role: 'system', content: '✓ Plan mode OFF — approved. Mutating actions will now execute; tell me to proceed.' });
292
+ }
293
+ },
294
+
295
+ '/rewind': async (arg) => {
296
+ const { formatCheckpointList, formatRewindResult, normalizeRewindMode, REWIND_MODES } = require('../checkpoints');
297
+ if (!checkpointStore) {
298
+ chatHistory.addMessage({ role: 'system', content: 'Checkpoints are not available in this session.' });
299
+ return;
300
+ }
301
+ const tokens = (arg || '').trim().split(/\s+/).filter(Boolean);
302
+ const force = tokens.includes('force') || tokens.includes('--force');
303
+ const modeToken = tokens.find((t) => REWIND_MODES.includes(t.toLowerCase()));
304
+ const mode = normalizeRewindMode(modeToken);
305
+ const target = tokens.find((t) => t !== 'force' && t !== '--force' && t !== modeToken);
306
+ if (!target || target === 'list') {
307
+ chatHistory.addMessage({ role: 'system', content: formatCheckpointList(checkpointStore.list()) });
308
+ return;
309
+ }
310
+ if (mode === null) {
311
+ chatHistory.addMessage({ role: 'system', content: `Unknown rewind mode "${modeToken}". Use one of: ${REWIND_MODES.join(', ')}.` });
312
+ return;
313
+ }
314
+ // Pass the live conversation so a conversation/both rewind can truncate it
315
+ // back to the checkpoint's turn (cutting on a turn boundary, never mid-pair).
316
+ const res = checkpointStore.rewind(target === 'last' ? 'last' : target, { force, mode, messages: ctx.messages });
317
+ if (res.conversation && res.conversation.ok) {
318
+ // Post-rewind policy: DISCARD. The truncated history replaces the active
319
+ // conversation; the removed tail is dropped (not persisted).
320
+ ctx.messages = res.conversation.messages;
321
+ if (typeof ctx.savedUpTo === 'number') ctx.savedUpTo = Math.min(ctx.savedUpTo, ctx.messages.length);
322
+ saveSession();
323
+ }
324
+ chatHistory.addMessage({ role: 'system', content: formatRewindResult(res) });
325
+ },
326
+
327
+ '/prompt': async () => {
328
+ const nativeTools = isNativeToolsActive(ctx.currentModel);
329
+ const activePrompt = ctx.resolvedSystemPrompt !== null ? ctx.resolvedSystemPrompt : getSystemPrompt(nativeTools);
330
+ const src = ctx.resolvedSystemPrompt !== null ? `file: ${opts.systemPromptFile}` : 'built-in';
331
+ const mode = getConfig().system_prompt_mode || 'system_role';
332
+ chatHistory.addMessage({
333
+ role: 'system',
334
+ content: `System prompt (${src}, mode: ${mode}):\n\n${activePrompt}`,
335
+ });
336
+ },
337
+
338
+ '/approve': async () => {
339
+ const enabled = permissionManager.toggleAll();
340
+ chatHistory.addMessage({ role: 'system', content: `Auto-approve: ${enabled ? 'ON' : 'OFF'}` });
341
+ },
342
+
343
+ '/debug': async (arg) => {
344
+ const a = arg.toLowerCase();
345
+ if (a === 'off' || a === 'false' || a === '0') ctx.debugMode = false;
346
+ else ctx.debugMode = true;
347
+
348
+ let tail = '';
349
+ try {
350
+ const content = fs.readFileSync(AUDIT_LOG, 'utf8');
351
+ const lines = content.trim().split('\n').filter((l) => l.trim()).slice(-5);
352
+ if (lines.length) {
353
+ const formatted = lines.map((line) => {
354
+ try {
355
+ const entry = JSON.parse(line);
356
+ const mark = entry.approved ? '✓' : '✗';
357
+ return ` ${mark} ${entry.ts} ${entry.tag} ${entry.input} → ${entry.result}`;
358
+ } catch {
359
+ return ` ${line}`;
360
+ }
361
+ });
362
+ tail = '\nLast 5 audit entries:\n' + formatted.join('\n');
363
+ } else {
364
+ tail = '\nAudit log is empty.';
365
+ }
366
+ } catch {
367
+ tail = '\nNo audit log found.';
368
+ }
369
+
370
+ const sink = dbg.isFile()
371
+ ? `file (${dbg.getMode()} mode)`
372
+ : 'inline chat history';
373
+ chatHistory.addMessage({
374
+ role: 'system',
375
+ content: `Debug output: ${ctx.debugMode ? 'ON' : 'OFF'} → ${sink}${tail}`,
376
+ });
377
+ },
378
+
379
+ '/shell': async (arg) => {
380
+ const cmd = arg;
381
+ inputField.setDisabled(true);
382
+ statusBar.update('tool', cmd);
383
+ try {
384
+ const shellResult = await agentExecShell(cmd, {
385
+ initiator: 'user',
386
+ confirm: (label, command) => {
387
+ chatHistory.addMessage({
388
+ role: 'system',
389
+ content: `⚠ Catastrophic command (${label}). Confirm to run:\n${command}`,
390
+ isError: true,
391
+ });
392
+ // _navCapture is checked before _disabled in input-field, so the
393
+ // picker drives keystrokes even though the field is disabled here.
394
+ return confirmCatastrophicShell(label, command, {
395
+ captureNavigation: (h) => { inputField.captureNavigation(h); return () => inputField.releaseNavigation(); },
396
+ });
397
+ },
398
+ });
399
+ let output = shellResult.stdout || '';
400
+ if (shellResult.stderr && shellResult.stderr !== 'Permission denied by user') {
401
+ output += (output ? '\n' : '') + `STDERR: ${shellResult.stderr}`;
402
+ }
403
+ const exitSuffix = shellResult.exit_code !== 0 ? ` [exit ${shellResult.exit_code}]` : '';
404
+ const display = output.trim() ? output.trim() + exitSuffix : `(no output)${exitSuffix}`;
405
+ chatHistory.addMessage({ role: 'shell', cmd, content: display, ts: new Date() });
406
+ } catch (err) {
407
+ chatHistory.addMessage({ role: 'system', content: `✗ Shell error: ${err.message}`, isError: true });
408
+ }
409
+ statusBar.update('idle');
410
+ inputField.setDisabled(false);
411
+ },
412
+ };
413
+ }
414
+
415
+ module.exports = { createSlashHandlers };