cvc-tui 0.4.4 → 0.4.5

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 (141) hide show
  1. package/NOTICES.md +13 -0
  2. package/dist/app/completion.js +102 -0
  3. package/dist/app/createGatewayEventHandler.js +508 -0
  4. package/dist/app/createSlashHandler.js +101 -0
  5. package/dist/app/delegationStore.js +51 -0
  6. package/dist/app/gatewayContext.js +17 -0
  7. package/dist/app/historyStore.js +123 -0
  8. package/dist/app/inputBuffer.js +120 -0
  9. package/dist/app/inputSelectionStore.js +8 -0
  10. package/dist/app/inputStore.js +28 -0
  11. package/dist/app/interfaces.js +6 -0
  12. package/dist/app/overlayStore.js +40 -0
  13. package/dist/app/promptStore.js +44 -0
  14. package/dist/app/queueStore.js +25 -0
  15. package/dist/app/scroll.js +44 -0
  16. package/dist/app/setupHandoff.js +28 -0
  17. package/dist/app/slash/commands/core.js +479 -0
  18. package/dist/app/slash/commands/debug.js +44 -0
  19. package/dist/app/slash/commands/ops.js +498 -0
  20. package/dist/app/slash/commands/session.js +431 -0
  21. package/dist/app/slash/commands/setup.js +20 -0
  22. package/dist/app/slash/commands/toggles.js +40 -0
  23. package/dist/app/slash/registry.js +18 -0
  24. package/dist/app/slash/types.js +1 -0
  25. package/dist/app/spawnHistoryStore.js +105 -0
  26. package/dist/app/turnController.js +650 -0
  27. package/dist/app/turnStore.js +48 -0
  28. package/dist/app/uiStore.js +36 -0
  29. package/dist/app/useComposerState.js +265 -0
  30. package/dist/app/useConfigSync.js +144 -0
  31. package/dist/app/useInputHandlers.js +403 -0
  32. package/dist/app/useLongRunToolCharms.js +50 -0
  33. package/dist/app/useMainApp.js +629 -0
  34. package/dist/app/useSessionLifecycle.js +175 -0
  35. package/dist/app/useSubmission.js +287 -0
  36. package/dist/app.js +15 -0
  37. package/dist/banner.js +63 -0
  38. package/dist/components/agentsOverlay.js +474 -0
  39. package/dist/components/appChrome.js +252 -0
  40. package/dist/components/appLayout.js +121 -0
  41. package/dist/components/appOverlays.js +65 -0
  42. package/dist/components/branding.js +97 -0
  43. package/dist/components/fpsOverlay.js +22 -0
  44. package/dist/components/helpHint.js +21 -0
  45. package/dist/components/markdown.js +501 -0
  46. package/dist/components/maskedPrompt.js +12 -0
  47. package/dist/components/messageLine.js +82 -0
  48. package/dist/components/modelPicker.js +254 -0
  49. package/dist/components/overlayControls.js +30 -0
  50. package/dist/components/overlays/confirmPrompt.js +25 -0
  51. package/dist/components/overlays/helpOverlay.js +76 -0
  52. package/dist/components/overlays/historySearch.js +49 -0
  53. package/dist/components/overlays/modelPicker.js +60 -0
  54. package/dist/components/overlays/overlayUtils.js +19 -0
  55. package/dist/components/overlays/secretPrompt.js +36 -0
  56. package/dist/components/overlays/sessionPicker.js +93 -0
  57. package/dist/components/overlays/skillsHub.js +71 -0
  58. package/dist/components/prompts.js +95 -0
  59. package/dist/components/queuedMessages.js +24 -0
  60. package/dist/components/sessionPicker.js +130 -0
  61. package/dist/components/skillsHub.js +165 -0
  62. package/dist/components/streamingAssistant.js +35 -0
  63. package/dist/components/streamingMarkdown.js +144 -0
  64. package/dist/components/textInput.js +794 -0
  65. package/dist/components/themed.js +12 -0
  66. package/dist/components/thinking.js +496 -0
  67. package/dist/components/todoPanel.js +40 -0
  68. package/dist/components/transcript.js +22 -0
  69. package/dist/config/env.js +18 -0
  70. package/dist/config/limits.js +22 -0
  71. package/dist/config/timing.js +25 -0
  72. package/dist/content/charms.js +5 -0
  73. package/dist/content/faces.js +21 -0
  74. package/dist/content/fortunes.js +29 -0
  75. package/dist/content/hotkeys.js +38 -0
  76. package/dist/content/placeholders.js +15 -0
  77. package/dist/content/setup.js +14 -0
  78. package/dist/content/verbs.js +41 -0
  79. package/dist/domain/details.js +53 -0
  80. package/dist/domain/messages.js +63 -0
  81. package/dist/domain/paths.js +16 -0
  82. package/dist/domain/providers.js +11 -0
  83. package/dist/domain/roles.js +6 -0
  84. package/dist/domain/slash.js +11 -0
  85. package/dist/domain/usage.js +1 -0
  86. package/dist/domain/viewport.js +33 -0
  87. package/dist/entry.js +64 -70236
  88. package/dist/gateway/client.js +312 -0
  89. package/dist/gatewayClient.js +574 -0
  90. package/dist/gatewayTypes.js +1 -0
  91. package/dist/hooks/useCompletion.js +86 -0
  92. package/dist/hooks/useGitBranch.js +58 -0
  93. package/dist/hooks/useInputHistory.js +12 -0
  94. package/dist/hooks/useQueue.js +57 -0
  95. package/dist/hooks/useVirtualHistory.js +401 -0
  96. package/dist/lib/circularBuffer.js +43 -0
  97. package/dist/lib/clipboard.js +126 -0
  98. package/dist/lib/editor.js +41 -0
  99. package/dist/lib/editor.test.js +58 -0
  100. package/dist/lib/emoji.js +49 -0
  101. package/dist/lib/externalCli.js +11 -0
  102. package/dist/lib/forceTruecolor.js +26 -0
  103. package/dist/lib/fpsStore.js +36 -0
  104. package/dist/lib/gracefulExit.js +29 -0
  105. package/dist/lib/history.js +69 -0
  106. package/dist/lib/inputMetrics.js +143 -0
  107. package/dist/lib/liveProgress.js +51 -0
  108. package/dist/lib/liveProgress.test.js +89 -0
  109. package/dist/lib/mathUnicode.js +685 -0
  110. package/dist/lib/memory.js +123 -0
  111. package/dist/lib/memoryMonitor.js +76 -0
  112. package/dist/lib/messages.js +3 -0
  113. package/dist/lib/messages.test.js +25 -0
  114. package/dist/lib/osc52.js +53 -0
  115. package/dist/lib/perfPane.js +94 -0
  116. package/dist/lib/platform.js +312 -0
  117. package/dist/lib/precisionWheel.js +25 -0
  118. package/dist/lib/react-devtools-stub.js +12 -0
  119. package/dist/lib/reasoning.js +39 -0
  120. package/dist/lib/rpc.js +26 -0
  121. package/dist/lib/subagentTree.js +287 -0
  122. package/dist/lib/syntax.js +89 -0
  123. package/dist/lib/terminalModes.js +46 -0
  124. package/dist/lib/terminalParity.js +48 -0
  125. package/dist/lib/terminalSetup.js +321 -0
  126. package/dist/lib/text.js +203 -0
  127. package/dist/lib/text.test.js +18 -0
  128. package/dist/lib/todo.js +2 -0
  129. package/dist/lib/todo.test.js +22 -0
  130. package/dist/lib/viewportStore.js +82 -0
  131. package/dist/lib/virtualHeights.js +61 -0
  132. package/dist/lib/wheelAccel.js +143 -0
  133. package/dist/protocol/interpolation.js +4 -0
  134. package/dist/protocol/paste.js +3 -0
  135. package/dist/theme.js +398 -0
  136. package/dist/types.js +1 -0
  137. package/dist/vendor/cvc-ink/dist/entry-exports.js +52737 -0
  138. package/dist/vendor/cvc-ink/index.js +1 -0
  139. package/dist/vendor/cvc-ink/package.json +9 -0
  140. package/dist/vendor/cvc-ink/text-input.js +1 -0
  141. package/package.json +9 -9
@@ -0,0 +1,498 @@
1
+ import { applyDelegationStatus, getDelegationState } from '../../delegationStore.js';
2
+ import { patchOverlayState } from '../../overlayStore.js';
3
+ import { getSpawnHistory, pushDiskSnapshot, setDiffPair } from '../../spawnHistoryStore.js';
4
+ export const opsCommands = [
5
+ {
6
+ help: 'stop background processes',
7
+ name: 'stop',
8
+ run: (_arg, ctx) => {
9
+ ctx.gateway
10
+ .rpc('process.stop', {})
11
+ .then(ctx.guarded(r => {
12
+ const killed = Number(r.killed ?? 0);
13
+ const noun = killed === 1 ? 'process' : 'processes';
14
+ ctx.transcript.sys(`stopped ${killed} background ${noun}`);
15
+ }))
16
+ .catch(ctx.guardedErr);
17
+ }
18
+ },
19
+ {
20
+ aliases: ['reload_mcp'],
21
+ help: 'reload MCP servers in the live session (warns about prompt cache invalidation)',
22
+ name: 'reload-mcp',
23
+ run: (arg, ctx) => {
24
+ // Parse arg: `now` / `always` skip the confirmation gate.
25
+ // `always` additionally persists approvals.mcp_reload_confirm=false.
26
+ const a = (arg || '').trim().toLowerCase();
27
+ const params = {
28
+ session_id: ctx.sid
29
+ };
30
+ if (a === 'now' || a === 'approve' || a === 'once' || a === 'yes') {
31
+ params.confirm = true;
32
+ }
33
+ else if (a === 'always') {
34
+ params.confirm = true;
35
+ params.always = true;
36
+ }
37
+ ctx.gateway
38
+ .rpc('reload.mcp', params)
39
+ .then(ctx.guarded(r => {
40
+ if (r.status === 'confirm_required') {
41
+ ctx.transcript.sys(r.message || '/reload-mcp requires confirmation');
42
+ return;
43
+ }
44
+ if (r.status === 'reloaded') {
45
+ ctx.transcript.sys(params.always
46
+ ? 'MCP servers reloaded · future /reload-mcp will run without confirmation'
47
+ : 'MCP servers reloaded');
48
+ return;
49
+ }
50
+ ctx.transcript.sys('reload complete');
51
+ }))
52
+ .catch(ctx.guardedErr);
53
+ }
54
+ },
55
+ {
56
+ help: 're-read ~/.hermes/.env into the running gateway (CLI parity)',
57
+ name: 'reload',
58
+ run: (_arg, ctx) => {
59
+ ctx.gateway
60
+ .rpc('reload.env', {})
61
+ .then(ctx.guarded(r => {
62
+ const n = Number(r.updated ?? 0);
63
+ const noun = n === 1 ? 'var' : 'vars';
64
+ ctx.transcript.sys(`reloaded .env (${n} ${noun} updated)`);
65
+ }))
66
+ .catch(ctx.guardedErr);
67
+ }
68
+ },
69
+ {
70
+ help: 'manage browser CDP connection [connect|disconnect|status]',
71
+ name: 'browser',
72
+ run: (arg, ctx) => {
73
+ const [rawAction = 'status', ...rest] = arg.trim().split(/\s+/).filter(Boolean);
74
+ const action = rawAction.toLowerCase();
75
+ if (!['connect', 'disconnect', 'status'].includes(action)) {
76
+ return ctx.transcript.sys('usage: /browser [connect|disconnect|status] [url] · persistent: set browser.cdp_url in config.yaml');
77
+ }
78
+ const sid = ctx.sid ?? null;
79
+ const url = action === 'connect' ? rest.join(' ').trim() || 'http://127.0.0.1:9222' : undefined;
80
+ if (url) {
81
+ ctx.transcript.sys(`checking Chrome remote debugging at ${url}...`);
82
+ }
83
+ ctx.gateway
84
+ .rpc('browser.manage', { action, session_id: sid, ...(url && { url }) })
85
+ .then(ctx.guarded(r => {
86
+ // Without a session we can't subscribe to streamed
87
+ // browser.progress events, so flush the bundled list.
88
+ if (!sid) {
89
+ r.messages?.forEach(message => ctx.transcript.sys(message));
90
+ }
91
+ if (action === 'status') {
92
+ return ctx.transcript.sys(r.connected
93
+ ? `browser connected: ${r.url || '(url unavailable)'}`
94
+ : 'browser not connected (try /browser connect <url> or set browser.cdp_url in config.yaml)');
95
+ }
96
+ if (action === 'disconnect') {
97
+ return ctx.transcript.sys('browser disconnected');
98
+ }
99
+ if (r.connected) {
100
+ ctx.transcript.sys('Browser connected to live Chrome via CDP');
101
+ ctx.transcript.sys(`Endpoint: ${r.url || '(url unavailable)'}`);
102
+ ctx.transcript.sys('next browser tool call will use this CDP endpoint');
103
+ }
104
+ }))
105
+ .catch(ctx.guardedErr);
106
+ }
107
+ },
108
+ {
109
+ help: 'list, diff, or restore checkpoints',
110
+ name: 'rollback',
111
+ run: (arg, ctx) => {
112
+ if (!ctx.sid) {
113
+ return ctx.transcript.sys('no active session — nothing to rollback');
114
+ }
115
+ const trimmed = arg.trim();
116
+ const [first = '', ...rest] = trimmed.split(/\s+/).filter(Boolean);
117
+ const lower = first.toLowerCase();
118
+ if (!trimmed || lower === 'list' || lower === 'ls') {
119
+ return ctx.gateway
120
+ .rpc('rollback.list', { session_id: ctx.sid })
121
+ .then(ctx.guarded(r => {
122
+ if (!r.enabled) {
123
+ return ctx.transcript.sys('checkpoints are not enabled');
124
+ }
125
+ const checkpoints = r.checkpoints ?? [];
126
+ if (!checkpoints.length) {
127
+ return ctx.transcript.sys('no checkpoints found');
128
+ }
129
+ ctx.transcript.panel('Rollback checkpoints', [
130
+ {
131
+ rows: checkpoints.map((c, idx) => [
132
+ `${idx + 1}. ${c.hash.slice(0, 10)}`,
133
+ [c.timestamp, c.message].filter(Boolean).join(' · ') || '(no metadata)'
134
+ ])
135
+ }
136
+ ]);
137
+ }))
138
+ .catch(ctx.guardedErr);
139
+ }
140
+ if (lower === 'diff') {
141
+ const hash = rest[0];
142
+ if (!hash) {
143
+ return ctx.transcript.sys('usage: /rollback diff <checkpoint>');
144
+ }
145
+ return ctx.gateway
146
+ .rpc('rollback.diff', { hash, session_id: ctx.sid })
147
+ .then(ctx.guarded(r => {
148
+ const body = (r.rendered || r.diff || '').trim();
149
+ if (!body && !r.stat) {
150
+ return ctx.transcript.sys('no changes since this checkpoint');
151
+ }
152
+ const text = [r.stat || '', body].filter(Boolean).join('\n\n');
153
+ ctx.transcript.page(text, 'Rollback diff');
154
+ }))
155
+ .catch(ctx.guardedErr);
156
+ }
157
+ const hash = first;
158
+ const filePath = rest.join(' ').trim();
159
+ return ctx.gateway
160
+ .rpc('rollback.restore', {
161
+ ...(filePath ? { file_path: filePath } : {}),
162
+ hash,
163
+ session_id: ctx.sid
164
+ })
165
+ .then(ctx.guarded(r => {
166
+ if (!r.success) {
167
+ return ctx.transcript.sys(`rollback failed: ${r.error || r.message || 'unknown error'}`);
168
+ }
169
+ const target = filePath || 'workspace';
170
+ const detail = r.reason || r.message || r.restored_to || 'restored';
171
+ ctx.transcript.sys(`rollback restored ${target}: ${detail}`);
172
+ if ((r.history_removed ?? 0) > 0) {
173
+ ctx.transcript.setHistoryItems(prev => ctx.transcript.trimLastExchange(prev));
174
+ }
175
+ }))
176
+ .catch(ctx.guardedErr);
177
+ }
178
+ },
179
+ {
180
+ aliases: ['tasks'],
181
+ help: 'open the spawn-tree dashboard (live audit + kill/pause controls)',
182
+ name: 'agents',
183
+ run: (arg, ctx) => {
184
+ const sub = arg.trim().toLowerCase();
185
+ // Stay compatible with the gateway `/agents [pause|resume|status]` CLI —
186
+ // explicit subcommands skip the overlay and act directly so scripts and
187
+ // multi-step flows can drive it without entering interactive mode.
188
+ if (sub === 'pause' || sub === 'resume' || sub === 'unpause') {
189
+ const paused = sub === 'pause';
190
+ ctx.gateway.gw
191
+ .request('delegation.pause', { paused })
192
+ .then(r => {
193
+ applyDelegationStatus({ paused: r?.paused });
194
+ ctx.transcript.sys(`delegation · ${r?.paused ? 'paused' : 'resumed'}`);
195
+ })
196
+ .catch(ctx.guardedErr);
197
+ return;
198
+ }
199
+ if (sub === 'status') {
200
+ const d = getDelegationState();
201
+ ctx.transcript.sys(`delegation · ${d.paused ? 'paused' : 'active'} · caps d${d.maxSpawnDepth ?? '?'}/${d.maxConcurrentChildren ?? '?'}`);
202
+ return;
203
+ }
204
+ patchOverlayState({ agents: true, agentsInitialHistoryIndex: 0 });
205
+ }
206
+ },
207
+ {
208
+ help: 'replay a completed spawn tree · `/replay [N|last|list|load <path>]`',
209
+ name: 'replay',
210
+ run: (arg, ctx) => {
211
+ const history = getSpawnHistory();
212
+ const raw = arg.trim();
213
+ const lower = raw.toLowerCase();
214
+ // ── Disk-backed listing ─────────────────────────────────────
215
+ if (lower === 'list' || lower === 'ls') {
216
+ ctx.gateway
217
+ .rpc('spawn_tree.list', {
218
+ limit: 30,
219
+ session_id: ctx.sid ?? 'default'
220
+ })
221
+ .then(ctx.guarded(r => {
222
+ const entries = r.entries ?? [];
223
+ if (!entries.length) {
224
+ return ctx.transcript.sys('no archived spawn trees on disk for this session');
225
+ }
226
+ const rows = entries.map(e => {
227
+ const ts = e.finished_at ? new Date(e.finished_at * 1000).toLocaleString() : '?';
228
+ const label = e.label || `${e.count} subagents`;
229
+ return [`${ts} · ${e.count}×`, `${label}\n ${e.path}`];
230
+ });
231
+ ctx.transcript.panel('Archived spawn trees', [{ rows }]);
232
+ }))
233
+ .catch(ctx.guardedErr);
234
+ return;
235
+ }
236
+ // ── Disk-backed load by path ─────────────────────────────────
237
+ if (lower.startsWith('load ')) {
238
+ const path = raw.slice(5).trim();
239
+ if (!path) {
240
+ return ctx.transcript.sys('usage: /replay load <path>');
241
+ }
242
+ ctx.gateway
243
+ .rpc('spawn_tree.load', { path })
244
+ .then(ctx.guarded(r => {
245
+ if (!r.subagents?.length) {
246
+ return ctx.transcript.sys('snapshot empty or unreadable');
247
+ }
248
+ // Push onto the in-memory history so the overlay picks it up
249
+ // by index 1 just like any other snapshot.
250
+ pushDiskSnapshot(r, path);
251
+ patchOverlayState({ agents: true, agentsInitialHistoryIndex: 1 });
252
+ }))
253
+ .catch(ctx.guardedErr);
254
+ return;
255
+ }
256
+ // ── In-memory nav (same-session) ─────────────────────────────
257
+ if (!history.length) {
258
+ return ctx.transcript.sys('no completed spawn trees this session · try /replay list');
259
+ }
260
+ let index = 1;
261
+ if (raw && lower !== 'last') {
262
+ const parsed = parseInt(raw, 10);
263
+ if (Number.isNaN(parsed) || parsed < 1 || parsed > history.length) {
264
+ return ctx.transcript.sys(`replay: index out of range 1..${history.length} · use /replay list for disk`);
265
+ }
266
+ index = parsed;
267
+ }
268
+ patchOverlayState({ agents: true, agentsInitialHistoryIndex: index });
269
+ }
270
+ },
271
+ {
272
+ help: 'diff two completed spawn trees · `/replay-diff <baseline> <candidate>` (indexes from /replay list or history N)',
273
+ name: 'replay-diff',
274
+ run: (arg, ctx) => {
275
+ const parts = arg.trim().split(/\s+/).filter(Boolean);
276
+ if (parts.length !== 2) {
277
+ return ctx.transcript.sys('usage: /replay-diff <a> <b> (e.g. /replay-diff 1 2 for last two)');
278
+ }
279
+ const [a, b] = parts;
280
+ const history = getSpawnHistory();
281
+ const resolve = (token) => {
282
+ const n = parseInt(token, 10);
283
+ if (Number.isFinite(n) && n >= 1 && n <= history.length) {
284
+ return history[n - 1] ?? null;
285
+ }
286
+ return null;
287
+ };
288
+ const baseline = resolve(a);
289
+ const candidate = resolve(b);
290
+ if (!baseline || !candidate) {
291
+ return ctx.transcript.sys(`replay-diff: could not resolve indices · history has ${history.length} entries`);
292
+ }
293
+ setDiffPair({ baseline, candidate });
294
+ patchOverlayState({ agents: true, agentsInitialHistoryIndex: 0 });
295
+ }
296
+ },
297
+ {
298
+ aliases: ['reload_skills'],
299
+ help: 're-scan installed skills in the live TUI gateway',
300
+ name: 'reload-skills',
301
+ run: (_arg, ctx) => {
302
+ ctx.gateway
303
+ .rpc('skills.reload', {})
304
+ .then(ctx.guarded(r => {
305
+ ctx.transcript.page(r.output || 'skills reloaded', 'Reload Skills');
306
+ ctx.gateway
307
+ .rpc('commands.catalog', {})
308
+ .then(ctx.guarded(catalog => {
309
+ if (!catalog?.pairs) {
310
+ return;
311
+ }
312
+ ctx.local.setCatalog({
313
+ canon: (catalog.canon ?? {}),
314
+ categories: catalog.categories ?? [],
315
+ pairs: catalog.pairs,
316
+ skillCount: (catalog.skill_count ?? 0),
317
+ sub: (catalog.sub ?? {})
318
+ });
319
+ }))
320
+ .catch(() => { });
321
+ }))
322
+ .catch(ctx.guardedErr);
323
+ }
324
+ },
325
+ {
326
+ help: 'browse, inspect, install skills',
327
+ name: 'skills',
328
+ run: (arg, ctx, cmd) => {
329
+ const text = arg.trim();
330
+ if (!text) {
331
+ return patchOverlayState({ skillsHub: true });
332
+ }
333
+ const [sub, ...rest] = text.split(/\s+/);
334
+ const query = rest.join(' ').trim();
335
+ const { rpc } = ctx.gateway;
336
+ const { panel, sys } = ctx.transcript;
337
+ const runViaSlashWorker = () => {
338
+ ctx.gateway.gw
339
+ .request('slash.exec', { command: cmd.slice(1), session_id: ctx.sid })
340
+ .then(r => {
341
+ if (ctx.stale()) {
342
+ return;
343
+ }
344
+ const body = r?.output || '/skills: no output';
345
+ const formatted = r?.warning ? `warning: ${r.warning}\n${body}` : body;
346
+ const long = formatted.length > 180 || formatted.split('\n').filter(Boolean).length > 2;
347
+ long ? ctx.transcript.page(formatted, 'Skills') : ctx.transcript.sys(formatted);
348
+ })
349
+ .catch(ctx.guardedErr);
350
+ };
351
+ if (sub === 'list') {
352
+ rpc('skills.manage', { action: 'list' })
353
+ .then(ctx.guarded(r => {
354
+ const cats = Object.entries(r.skills ?? {}).sort();
355
+ if (!cats.length) {
356
+ return sys('no skills available');
357
+ }
358
+ panel('Skills', cats.map(([title, items]) => ({ items, title })));
359
+ }))
360
+ .catch(ctx.guardedErr);
361
+ return;
362
+ }
363
+ if (sub === 'inspect') {
364
+ if (!query) {
365
+ return sys('usage: /skills inspect <name>');
366
+ }
367
+ rpc('skills.manage', { action: 'inspect', query })
368
+ .then(ctx.guarded(r => {
369
+ const info = r.info ?? {};
370
+ if (!info.name) {
371
+ return sys(`unknown skill: ${query}`);
372
+ }
373
+ const rows = [
374
+ ['Name', String(info.name)],
375
+ ['Category', String(info.category ?? '')],
376
+ ['Path', String(info.path ?? '')]
377
+ ];
378
+ const sections = [{ rows }];
379
+ if (info.description) {
380
+ sections.push({ text: String(info.description) });
381
+ }
382
+ panel('Skill', sections);
383
+ }))
384
+ .catch(ctx.guardedErr);
385
+ return;
386
+ }
387
+ if (sub === 'search') {
388
+ if (!query) {
389
+ return sys('usage: /skills search <query>');
390
+ }
391
+ rpc('skills.manage', { action: 'search', query })
392
+ .then(ctx.guarded(r => {
393
+ const results = r.results ?? [];
394
+ if (!results.length) {
395
+ return sys(`no results for: ${query}`);
396
+ }
397
+ panel(`Search: ${query}`, [{ rows: results.map(s => [s.name, s.description ?? '']) }]);
398
+ }))
399
+ .catch(ctx.guardedErr);
400
+ return;
401
+ }
402
+ if (sub === 'install') {
403
+ if (!query) {
404
+ return sys('usage: /skills install <name or url>');
405
+ }
406
+ sys(`installing ${query}…`);
407
+ rpc('skills.manage', { action: 'install', query })
408
+ .then(ctx.guarded(r => sys(r.installed ? `installed ${r.name ?? query}` : 'install failed')))
409
+ .catch(ctx.guardedErr);
410
+ return;
411
+ }
412
+ if (sub === 'browse') {
413
+ const pageNum = query ? parseInt(query, 10) : 1;
414
+ if (Number.isNaN(pageNum) || pageNum < 1) {
415
+ return sys('usage: /skills browse [page] (page must be a positive number)');
416
+ }
417
+ sys('fetching community skills (scans 6 sources, may take ~15s)…');
418
+ rpc('skills.manage', { action: 'browse', page: pageNum })
419
+ .then(ctx.guarded(r => {
420
+ const items = r.items ?? [];
421
+ if (!items.length) {
422
+ return sys(`no skills on page ${pageNum}${r.total ? ` (total ${r.total})` : ''}`);
423
+ }
424
+ const rows = items.map(s => [
425
+ s.trust ? `${s.name} · ${s.trust}` : s.name,
426
+ String(s.description ?? '').slice(0, 160)
427
+ ]);
428
+ const footer = [];
429
+ if (r.page && r.total_pages) {
430
+ footer.push(`page ${r.page} of ${r.total_pages}`);
431
+ }
432
+ if (r.total) {
433
+ footer.push(`${r.total} skills total`);
434
+ }
435
+ if (r.page && r.total_pages && r.page < r.total_pages) {
436
+ footer.push(`/skills browse ${r.page + 1} for more`);
437
+ }
438
+ panel(`Browse Skills${pageNum > 1 ? ` — p${pageNum}` : ''}`, [
439
+ { rows },
440
+ ...(footer.length ? [{ text: footer.join(' · ') }] : [])
441
+ ]);
442
+ }))
443
+ .catch(ctx.guardedErr);
444
+ return;
445
+ }
446
+ runViaSlashWorker();
447
+ }
448
+ },
449
+ {
450
+ help: 'enable or disable tools (client-side history reset on change)',
451
+ name: 'tools',
452
+ run: (arg, ctx, cmd) => {
453
+ const [subcommand, ...names] = arg.trim().split(/\s+/).filter(Boolean);
454
+ if (subcommand !== 'disable' && subcommand !== 'enable') {
455
+ ctx.gateway.gw
456
+ .request('slash.exec', { command: cmd.slice(1), session_id: ctx.sid })
457
+ .then(r => {
458
+ if (ctx.stale()) {
459
+ return;
460
+ }
461
+ const body = r?.output || '/tools: no output';
462
+ const text = r?.warning ? `warning: ${r.warning}\n${body}` : body;
463
+ const long = text.length > 180 || text.split('\n').filter(Boolean).length > 2;
464
+ long ? ctx.transcript.page(text, 'Tools') : ctx.transcript.sys(text);
465
+ })
466
+ .catch(ctx.guardedErr);
467
+ return;
468
+ }
469
+ if (!names.length) {
470
+ ctx.transcript.sys(`usage: /tools ${subcommand} <name> [name ...]`);
471
+ ctx.transcript.sys(`built-in toolset: /tools ${subcommand} web`);
472
+ ctx.transcript.sys(`MCP tool: /tools ${subcommand} github:create_issue`);
473
+ return;
474
+ }
475
+ ctx.gateway
476
+ .rpc('tools.configure', { action: subcommand, names, session_id: ctx.sid })
477
+ .then(ctx.guarded(r => {
478
+ if (r.info) {
479
+ ctx.session.setSessionStartedAt(Date.now());
480
+ ctx.session.resetVisibleHistory(r.info);
481
+ }
482
+ if (r.changed?.length) {
483
+ ctx.transcript.sys(`${subcommand === 'disable' ? 'disabled' : 'enabled'}: ${r.changed.join(', ')}`);
484
+ }
485
+ if (r.unknown?.length) {
486
+ ctx.transcript.sys(`unknown toolsets: ${r.unknown.join(', ')}`);
487
+ }
488
+ if (r.missing_servers?.length) {
489
+ ctx.transcript.sys(`missing MCP servers: ${r.missing_servers.join(', ')}`);
490
+ }
491
+ if (r.reset) {
492
+ ctx.transcript.sys('session reset. new tool configuration is active.');
493
+ }
494
+ }))
495
+ .catch(ctx.guardedErr);
496
+ }
497
+ }
498
+ ];