s-app-scoder-agent 0.9.3 → 0.9.7

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 (112) hide show
  1. package/README.md +615 -638
  2. package/dist/{guardrails.js → agent/guardrails.js} +1 -1
  3. package/dist/agent/interaction.js +70 -0
  4. package/dist/agent/mcp-bridge.js +136 -0
  5. package/dist/agent/prompt-context.js +83 -0
  6. package/dist/agent/run-loop.js +318 -0
  7. package/dist/agent/session-persistence.js +61 -0
  8. package/dist/agent/tool-executor.js +158 -0
  9. package/dist/agent/types.js +88 -0
  10. package/dist/agent/verification.js +99 -0
  11. package/dist/agent.js +239 -1002
  12. package/dist/cli/args.js +230 -0
  13. package/dist/cli/init.js +79 -0
  14. package/dist/cli/main.js +211 -0
  15. package/dist/cli/mcp-command.js +185 -0
  16. package/dist/cli/repl.js +77 -0
  17. package/dist/cli/run-once.js +26 -0
  18. package/dist/{tui/commands.js → commands/definitions.js} +6 -16
  19. package/dist/commands/registry.js +260 -0
  20. package/dist/{config.js → config/config.js} +1 -1
  21. package/dist/{prompt-builder.js → context/prompt-builder.js} +12 -1
  22. package/dist/{prompt.js → context/prompt.js} +111 -9
  23. package/dist/{skills.js → context/skills.js} +41 -2
  24. package/dist/index.js +5 -798
  25. package/dist/io/console-reporter.js +24 -0
  26. package/dist/io/reporter.js +1 -0
  27. package/dist/mcp/connection.js +227 -0
  28. package/dist/mcp/env.js +8 -0
  29. package/dist/{mcp.js → mcp/manager.js} +3 -515
  30. package/dist/mcp/sse.js +28 -0
  31. package/dist/mcp/transport-http.js +140 -0
  32. package/dist/mcp/transport-stdio.js +98 -0
  33. package/dist/mcp/types.js +11 -0
  34. package/dist/media/image.js +149 -0
  35. package/dist/{server.js → server/server.js} +1 -1
  36. package/dist/{cost.js → session/cost.js} +1 -1
  37. package/dist/{session.js → session/session.js} +1 -1
  38. package/dist/{version.js → shared/version.js} +1 -1
  39. package/dist/tools/apply-patch.js +62 -0
  40. package/dist/tools/edit-file.js +29 -0
  41. package/dist/tools/file-edit-common.js +124 -0
  42. package/dist/tools/list-files.js +64 -0
  43. package/dist/tools/path-safety.js +75 -0
  44. package/dist/tools/read-file.js +64 -0
  45. package/dist/tools/registry.js +297 -0
  46. package/dist/tools/run-command.js +192 -0
  47. package/dist/tools/schemas.js +8 -0
  48. package/dist/tools/search.js +237 -0
  49. package/dist/tools/task-tool.js +48 -0
  50. package/dist/tools/types.js +1 -0
  51. package/dist/tools/write-file.js +34 -0
  52. package/dist/tui/app.js +59 -532
  53. package/dist/tui/ask-user-modal.js +2 -1
  54. package/dist/tui/at-complete.js +2 -93
  55. package/dist/tui/charset.js +41 -0
  56. package/dist/tui/chat-views.js +72 -16
  57. package/dist/tui/chrome.js +11 -5
  58. package/dist/tui/hooks/use-command-host.js +26 -0
  59. package/dist/tui/hooks/use-event-queue.js +69 -0
  60. package/dist/tui/hooks/use-keybinds.js +87 -0
  61. package/dist/tui/hooks/use-scroll.js +38 -0
  62. package/dist/tui/hooks/use-session-resume.js +47 -0
  63. package/dist/tui/hooks/use-submit.js +106 -0
  64. package/dist/tui/image.js +2 -149
  65. package/dist/tui/input.js +1 -1
  66. package/dist/tui/markdown/ast.js +5 -0
  67. package/dist/tui/markdown/diagram.js +239 -0
  68. package/dist/tui/markdown/inline-render.js +24 -0
  69. package/dist/tui/markdown/inline.js +93 -0
  70. package/dist/tui/markdown/parse.js +139 -0
  71. package/dist/tui/markdown/render.js +146 -0
  72. package/dist/tui/markdown/table.js +106 -0
  73. package/dist/tui/markdown.js +32 -379
  74. package/dist/tui/permission-modal.js +2 -1
  75. package/dist/tui/selectors.js +157 -0
  76. package/dist/tui/session-picker.js +4 -3
  77. package/dist/tui/sidebar.js +130 -15
  78. package/dist/tui/state.js +47 -118
  79. package/dist/tui/text-width.js +139 -0
  80. package/dist/tui/theme.js +7 -2
  81. package/dist/{build.js → workspace/build.js} +1 -1
  82. package/dist/workspace/file-index.js +93 -0
  83. package/package.json +5 -1
  84. package/dist/tools.js +0 -1223
  85. package/dist/tui/command-registry.js +0 -477
  86. package/dist/tui/review.js +0 -64
  87. package/dist/tui/transcript.js +0 -145
  88. /package/dist/{agents-guide.js → context/agents-guide.js} +0 -0
  89. /package/dist/{agents.js → context/agents.js} +0 -0
  90. /package/dist/{compaction.js → context/compaction.js} +0 -0
  91. /package/dist/{context.js → context/context.js} +0 -0
  92. /package/dist/{memory.js → context/memory.js} +0 -0
  93. /package/dist/{repo-map.js → context/repo-map.js} +0 -0
  94. /package/dist/{ui.js → io/terminal.js} +0 -0
  95. /package/dist/{llm.js → llm/llm.js} +0 -0
  96. /package/dist/{model-catalog.js → llm/model-catalog.js} +0 -0
  97. /package/dist/{provider-anthropic.js → llm/provider-anthropic.js} +0 -0
  98. /package/dist/{provider-registry.js → llm/provider-registry.js} +0 -0
  99. /package/dist/{retry.js → llm/retry.js} +0 -0
  100. /package/dist/{mcp-import.js → mcp/mcp-import.js} +0 -0
  101. /package/dist/{permissions.js → security/permissions.js} +0 -0
  102. /package/dist/{sandbox.js → security/sandbox.js} +0 -0
  103. /package/dist/{shell-scanner.js → security/shell-scanner.js} +0 -0
  104. /package/dist/{logger.js → session/logger.js} +0 -0
  105. /package/dist/{steering.js → session/steering.js} +0 -0
  106. /package/dist/{constants.js → shared/constants.js} +0 -0
  107. /package/dist/{json-schema.js → shared/json-schema.js} +0 -0
  108. /package/dist/{webfetch.js → tools/webfetch.js} +0 -0
  109. /package/dist/{diagnostics.js → workspace/diagnostics.js} +0 -0
  110. /package/dist/{diff.js → workspace/diff.js} +0 -0
  111. /package/dist/{snapshot.js → workspace/snapshot.js} +0 -0
  112. /package/dist/{watch.js → workspace/watch.js} +0 -0
@@ -1,477 +0,0 @@
1
- import { listSessions, resolveSessionId } from "../session.js";
2
- import { discoverSkills, skillWarnings } from "../skills.js";
3
- import { selectableAgents } from "../agents.js";
4
- import { agentsMdTemplate } from "../agents-guide.js";
5
- import { existsSync, writeFileSync } from "node:fs";
6
- import { join } from "node:path";
7
- import { formatSlashHelp, parseMcpExec, BUILTIN_COMMANDS, resolveCommandDef, discoverCustomCommands, runCustomCommand } from "./commands.js";
8
- import { exportTranscript, findInTranscript } from "./transcript.js";
9
- /** Format usage/cost block (dùng chung — REPL console.log, TUI notice). */
10
- export function formatUsage(u) {
11
- return u.calls > 0
12
- ? `session usage: ${u.calls} LLM calls · tokens in ${u.inputTokens} / out ${u.outputTokens}` +
13
- (u.cachedTokens > 0 ? ` (+${u.cachedTokens} cached)` : "")
14
- : "no LLM calls yet in this session";
15
- }
16
- /** Format skills listing (dùng chung). */
17
- export function formatSkills(skills) {
18
- if (skills.length === 0) {
19
- return "no skills discovered — add .scoder/skills/<name>/SKILL.md (name + description frontmatter)";
20
- }
21
- return ("Skills:\n" +
22
- skills
23
- .map((s) => ` ${s.name.padEnd(20)} ${s.description || "(no description)"} [${s.scope}]`)
24
- .join("\n"));
25
- }
26
- /** Format sessions listing (dùng chung). */
27
- export function formatSessions(all) {
28
- return all.length === 0
29
- ? "no saved sessions — completed runs auto-save to .scoder/sessions/"
30
- : all.slice(0, 10).map((x) => `${x.id.slice(0, 8)} ${x.updatedAt.slice(0, 16).replace("T", " ")} ${x.steps} steps ${x.task}`).join("\n") +
31
- "\nresume: /sessions resume <id|last>";
32
- }
33
- /** Format MCP status listing (dùng chung). */
34
- export function formatMcpStatus(status) {
35
- if (status.length === 0)
36
- return "no MCP servers configured — add mcpServers to .scoderrc.json";
37
- return status
38
- .map((srv) => {
39
- const capParts = [];
40
- if (srv.serverInfo?.name)
41
- capParts.push(srv.serverInfo.name + (srv.serverInfo.version ? " v" + srv.serverInfo.version : ""));
42
- if (srv.capabilities?.ping === true)
43
- capParts.push("ping");
44
- if (srv.capabilities?.tools)
45
- capParts.push("tools");
46
- if (srv.capabilities?.resources)
47
- capParts.push("resources");
48
- if (srv.capabilities?.prompts)
49
- capParts.push("prompts");
50
- const capTag = capParts.length > 0 ? ` [${capParts.join(", ")}]` : "";
51
- const counts = [
52
- `${srv.tools.length} tools`,
53
- ...(srv.resourceCount ? [`${srv.resourceCount} resources`] : []),
54
- ...(srv.promptCount ? [`${srv.promptCount} prompts`] : []),
55
- ].join(", ");
56
- return (`${srv.connected ? "✓" : "·"} ${srv.name} — ${srv.command}${capTag}` +
57
- (srv.connected
58
- ? ` (${counts})\n` +
59
- srv.tools.map((t) => ` mcp__${srv.name}__${t.name} ${t.description ?? ""}`).join("\n")
60
- : " (not connected)"));
61
- })
62
- .join("\n");
63
- }
64
- /**
65
- * Handlers dùng chung. Command đặc thù frontend (/review, /undo, /theme — chỉ
66
- * TUI; /diff TUI có turn-scope riêng) KHÔNG nằm đây — frontend xử lý trước
67
- * khi dispatch, rồi mới gọi dispatchCommand() cho phần còn lại.
68
- */
69
- export function coreCommands() {
70
- return [
71
- {
72
- name: "/help",
73
- description: "show this command list",
74
- run(_slash, _host, notice) {
75
- notice(formatSlashHelp("scoder tui"));
76
- },
77
- },
78
- {
79
- name: "/cost",
80
- description: "token usage of the current session",
81
- run(_slash, host, notice) {
82
- notice(formatUsage(host.agent()?.getUsage() ?? { calls: 0, inputTokens: 0, outputTokens: 0, cachedTokens: 0 }));
83
- },
84
- },
85
- {
86
- name: "/compact",
87
- description: "summarize older history now (manual compaction)",
88
- async run(_slash, host, notice) {
89
- const agent = host.agent();
90
- if (!agent)
91
- return notice("agent not ready — start a task first", "warn");
92
- if (host.isRunning?.())
93
- return notice("agent is running — wait or Esc first", "warn");
94
- const r = await agent.compactHistory();
95
- notice(r.message, r.ok ? "info" : "warn");
96
- },
97
- },
98
- {
99
- name: "/skills",
100
- description: "list/info/lint/reload skills — /skills [info <name>|lint|reload]",
101
- run(slash, host, notice) {
102
- const arg = slash.arg.trim();
103
- const skills = host.agent()?.getSkills() ?? discoverSkills(host.config.workdir);
104
- if (!arg) {
105
- notice(formatSkills(skills));
106
- return;
107
- }
108
- const parts = arg.split(/\s+/);
109
- const sub = parts[0].toLowerCase();
110
- if (sub === "info") {
111
- const name = parts.slice(1).join(" ").trim();
112
- if (!name)
113
- return notice("usage: /skills info <name>", "warn");
114
- const skill = skills.find((s) => s.name.toLowerCase() === name.toLowerCase());
115
- if (!skill)
116
- return notice(`skill "${name}" not found`, "error");
117
- const lines = [
118
- `# ${skill.name}${skill.version ? ` v${skill.version}` : ""}`,
119
- `description: ${skill.description}`,
120
- `scope: ${skill.scope}`,
121
- skill.tags?.length ? `tags: ${skill.tags.join(", ")}` : null,
122
- skill.allowedTools?.length ? `allowed-tools: ${skill.allowedTools.join(", ")}` : null,
123
- skill.references?.length ? `references: ${skill.references.join(", ")}` : null,
124
- skill.whenToUse ? `when-to-use: ${skill.whenToUse}` : null,
125
- "",
126
- skill.body,
127
- ].filter(Boolean);
128
- notice(lines.join("\n"));
129
- return;
130
- }
131
- if (sub === "lint") {
132
- const warnings = skillWarnings.filter((w) => !host.config.workdir || w.path.startsWith(host.config.workdir));
133
- if (warnings.length === 0)
134
- return notice("skills lint: ok (no warnings)", "info");
135
- notice(warnings.map((w) => `${w.path}: ${w.message}`).join("\n"), "warn");
136
- return;
137
- }
138
- if (sub === "reload") {
139
- const ag = host.agent();
140
- if (ag)
141
- ag.reloadSkills?.();
142
- notice(formatSkills(host.agent()?.getSkills() ?? discoverSkills(host.config.workdir)));
143
- return;
144
- }
145
- // default: treat arg as skill name → show info
146
- const skill = skills.find((s) => s.name.toLowerCase() === arg.toLowerCase());
147
- if (skill) {
148
- notice(`# ${skill.name}\n${skill.description}\n${skill.body.slice(0, 500)}`, "info");
149
- return;
150
- }
151
- notice(`unknown /skills subcommand: ${arg}`, "error");
152
- },
153
- },
154
- {
155
- name: "/skill",
156
- description: "load a skill into context: /skill <name>",
157
- run(slash, host, notice) {
158
- if (!slash.arg)
159
- return notice("usage: /skill <name>", "warn");
160
- const r = host.agent()?.stageSkill(slash.arg);
161
- if (!r)
162
- return notice("agent not ready", "warn");
163
- notice(r.message, r.ok ? "info" : "error");
164
- },
165
- },
166
- {
167
- name: "/mcp",
168
- description: "MCP servers & tools: /mcp [server] [tool] [json|text]",
169
- async run(slash, host, notice) {
170
- const agent = host.agent();
171
- if (!agent)
172
- return notice("agent not ready", "warn");
173
- const exec = parseMcpExec(slash.arg);
174
- if (exec === null) {
175
- notice(formatMcpStatus(await agent.mcpStatus()));
176
- return;
177
- }
178
- if ("error" in exec)
179
- return notice(exec.error, "warn");
180
- notice(`→ ${exec.fullName}`);
181
- const r = await agent.callMcpTool(exec.fullName, exec.args);
182
- if (r.ok)
183
- notice(r.output.length > 4000 ? r.output.slice(0, 4000) + "\n… (truncated)" : r.output);
184
- else
185
- notice(r.output, "error");
186
- },
187
- },
188
- {
189
- name: "/model",
190
- description: "show the active model & provider — /model <provider/model> to hot-swap",
191
- run(slash, host, notice) {
192
- if (slash.arg) {
193
- const agent = host.agent();
194
- if (!agent)
195
- return notice("agent not ready", "warn");
196
- const msg = agent.setModel(slash.arg.trim());
197
- notice(msg, msg.startsWith("ERROR") ? "error" : "info");
198
- return;
199
- }
200
- const cfg = host.config;
201
- notice(`model: ${cfg.model} · provider: ${cfg.provider} · build: ${cfg.buildCmd ?? "auto-detect"}`);
202
- },
203
- },
204
- {
205
- name: "/permission",
206
- description: "view or switch permission mode: /permission <auto|ask|readonly>",
207
- run(slash, host, notice) {
208
- const mode = slash.arg.toLowerCase();
209
- if (!["auto", "ask", "readonly"].includes(mode)) {
210
- notice(`permission mode: ${host.config.permissionMode} — usage: /permission <auto|ask|readonly>`, "warn");
211
- return;
212
- }
213
- host.agent()?.setPermissionMode(mode);
214
- host.updateConfig?.({ permissionMode: mode });
215
- host.config = withPermissionMode(host.config, mode);
216
- notice(`permission mode → ${mode}`);
217
- },
218
- },
219
- {
220
- name: "/plan",
221
- description: "show the current plan — /plan done|start <id|n|text> to mark steps",
222
- run(slash, host, notice) {
223
- const agent = host.agent();
224
- const plan = agent?.getPlan() ?? [];
225
- if (!slash.arg) {
226
- if (plan.length === 0)
227
- return notice("no plan yet — multi-step tasks get one via write_todos");
228
- const icon = (s) => (s === "completed" ? "✓" : s === "in_progress" ? "▸" : "○");
229
- notice("Plan:\n" +
230
- plan
231
- .map((t, i) => {
232
- let line = ` ${icon(t.status)} ${String(i + 1).padStart(2)}. ${t.content}`;
233
- if (t.verify)
234
- line += `\n verify: ${t.verify}`;
235
- if (t.acceptance)
236
- line += `\n acceptance: ${t.acceptance}`;
237
- if (t.files?.length)
238
- line += `\n files: ${t.files.join(", ")}`;
239
- return line;
240
- })
241
- .join("\n") +
242
- "\nmark: /plan done <id|n> · /plan start <id|n>");
243
- return;
244
- }
245
- const m = /^(done|start)\s+(.+)$/.exec(slash.arg.trim());
246
- if (!agent)
247
- return notice("agent not ready", "warn");
248
- if (!m)
249
- return notice("usage: /plan [done|start <id|n|text>]", "warn");
250
- const r = agent.markPlanStep(m[1] === "done" ? "completed" : "in_progress", m[2]);
251
- notice(r.message, r.ok ? "info" : "warn");
252
- },
253
- },
254
- {
255
- name: "/agent",
256
- description: "list selectable agents — /agent <name> to switch (idle only)",
257
- run(slash, host, notice) {
258
- const agents = selectableAgents(host.config.workdir);
259
- const current = host.config.agent ?? "build";
260
- if (!slash.arg) {
261
- notice("Agents:\n" +
262
- agents.map((a) => ` ${a.name === current ? "*" : " "} ${a.name.padEnd(10)} ${a.description}`).join("\n") +
263
- "\nswitch with /agent <name> (idle only; resets the conversation)");
264
- return;
265
- }
266
- const name = slash.arg.trim();
267
- const def = agents.find((a) => a.name === name);
268
- if (!def) {
269
- return notice(`agent "${name}" not selectable — available: ${agents.map((a) => a.name).join(", ")}`, "error");
270
- }
271
- if (current === name)
272
- return notice(`already on agent "${name}"`);
273
- if (host.isRunning?.())
274
- return notice("agent is running — wait or Esc first", "warn");
275
- host.updateConfig?.({ agent: name });
276
- host.config = { ...host.config, agent: name };
277
- host.replaceAgent(null); // fresh agent with the new def; old conversation ends here
278
- notice(`agent → ${name} (${def.mode}) — conversation reset; /sessions can resume the previous one`);
279
- },
280
- },
281
- {
282
- name: "/init",
283
- description: "create an AGENTS.md template in this project",
284
- run(_slash, host, notice) {
285
- const target = join(host.config.workdir, "AGENTS.md");
286
- if (existsSync(target)) {
287
- notice(`AGENTS.md already exists: ${target}`, "warn");
288
- return;
289
- }
290
- writeFileSync(target, agentsMdTemplate(host.config.workdir), "utf8");
291
- notice(`created ${target} — review and commit it`);
292
- },
293
- },
294
- {
295
- name: "/sessions",
296
- description: "list saved sessions — /sessions resume <id|last>",
297
- async run(slash, host, notice) {
298
- if (slash.arg.startsWith("resume")) {
299
- const ref = slash.arg.replace(/^resume\s*/, "").trim() || "last";
300
- const s = resolveSessionId(host.config.workdir, ref);
301
- if (!s)
302
- return notice(`session not found: ${ref} — bare /sessions lists saved ones`, "error");
303
- if (host.isRunning?.())
304
- return notice("agent is running — wait or Esc first", "warn");
305
- if (host.resumeAndRun) {
306
- await host.resumeAndRun(s);
307
- return;
308
- }
309
- notice(`resumed session ${s.id.slice(0, 8)}`);
310
- return;
311
- }
312
- notice(formatSessions(listSessions(host.config.workdir)));
313
- },
314
- },
315
- {
316
- name: "/commands",
317
- description: "list all commands (builtin + custom) — /commands <name> to inspect",
318
- run(slash, host, notice) {
319
- const all = BUILTIN_COMMANDS.map((c) => ({
320
- name: c.name,
321
- desc: c.description,
322
- source: c.source,
323
- }));
324
- const custom = discoverCustomCommands(host.config.workdir).map((c) => ({
325
- name: "/commands " + c.name,
326
- desc: c.description,
327
- source: c.source,
328
- }));
329
- const combined = [...all, ...custom];
330
- if (!slash.arg) {
331
- // List mode — grouped by source.
332
- const bySource = {};
333
- for (const c of combined)
334
- bySource[c.source] = (bySource[c.source] ?? []).concat(c);
335
- const sections = Object.entries(bySource)
336
- .sort(([a], [b]) => a.localeCompare(b))
337
- .map(([src, cmds]) => ` ${src}:\n${cmds.map((c) => ` ${c.name} ${c.desc}`).join("\n")}`);
338
- notice(`commands:\n${sections.join("\n\n")}`);
339
- return;
340
- }
341
- // Inspect mode — /commands <name>
342
- const name = slash.arg.trim().toLowerCase();
343
- const customCmd = custom.find((c) => c.name === "/commands " + name || c.name === "/commands " + name.slice(1));
344
- if (customCmd) {
345
- // Full info for custom command
346
- const cmd = discoverCustomCommands(host.config.workdir).find((c) => "/commands " + c.name === customCmd.name);
347
- if (!cmd)
348
- return;
349
- notice(`command: ${cmd.name}\n` +
350
- `description: ${cmd.description}\n` +
351
- `exec: ${cmd.exec ?? "prompt"}\n` +
352
- `source: ${cmd.source} — ${cmd.filePath}\n` +
353
- (cmd.args ? `args: ${cmd.args.join(", ")}\n` : "") +
354
- `\n${cmd.body.slice(0, 400)}\n` +
355
- (cmd.body.length > 400 ? "\n… (truncated)" : ""));
356
- return;
357
- }
358
- const builtin = BUILTIN_COMMANDS.find((c) => c.name === "/" + name || c.name === "/commands " + name);
359
- if (builtin) {
360
- notice(`${builtin.name}: ${builtin.description}\nusage: ${builtin.usage}`);
361
- return;
362
- }
363
- notice(`command "/${name}" not found`, "warn");
364
- },
365
- },
366
- {
367
- name: "/export",
368
- description: "export session transcript: /export [md|json] — writes to workdir/scoder-export.{md|json}",
369
- run(slash, host, notice) {
370
- const items = host.transcriptItems ? host.transcriptItems() : [];
371
- if (items.length === 0) {
372
- notice("no transcript to export", "warn");
373
- return;
374
- }
375
- const fmt = slash.arg.trim().toLowerCase() || "md";
376
- if (fmt !== "md" && fmt !== "json") {
377
- notice("usage: /export [md|json]", "warn");
378
- return;
379
- }
380
- const content = exportTranscript(items, fmt);
381
- const fileName = `scoder-export.${fmt}`;
382
- const filePath = join(host.config.workdir, fileName);
383
- try {
384
- writeFileSync(filePath, "\ufeff" + content, "utf8");
385
- notice(`exported ${items.length} items → ${filePath}`);
386
- }
387
- catch (e) {
388
- notice(`export failed: ${e.message}`, "error");
389
- }
390
- },
391
- },
392
- {
393
- name: "/search",
394
- description: "search transcript: /search <query> — shows snippets; TUI jumps to result",
395
- run(slash, host, notice) {
396
- if (!slash.arg.trim()) {
397
- notice("usage: /search <query>", "warn");
398
- return;
399
- }
400
- const items = host.transcriptItems ? host.transcriptItems() : [];
401
- if (items.length === 0) {
402
- notice("no transcript to search", "warn");
403
- return;
404
- }
405
- const matches = findInTranscript(items, slash.arg);
406
- if (matches.length === 0) {
407
- notice(`no results for "${slash.arg.trim()}"`, "warn");
408
- return;
409
- }
410
- notice(`${matches.length} match${matches.length > 1 ? "es" : ""} in transcript:\n` +
411
- matches.slice(0, 20).map((m) => ` [${m.index}] ${m.kind}: ${m.snippet}`).join("\n"));
412
- // TUI: jump to first result
413
- if (host.selectTranscriptItem && matches.length > 0) {
414
- host.selectTranscriptItem(matches[0].index);
415
- }
416
- },
417
- },
418
- ];
419
- }
420
- /** Merged catalog of builtin command metadata — /help & completion read this. */
421
- export function commandCatalog() {
422
- return BUILTIN_COMMANDS.map((def) => ({ ...def }));
423
- }
424
- /** Clone config với permissionMode mới (host.config thay thế được). */
425
- function withPermissionMode(cfg, mode) {
426
- return { ...cfg, permissionMode: mode };
427
- }
428
- /**
429
- * Dispatch một input qua registry. Trả về {handled:false} khi không phải
430
- * slash command. Frontend xử lý command riêng (TUI: /review /undo /theme
431
- * /diff; REPL: không có) TRƯỚC khi gọi hàm này.
432
- */
433
- export async function dispatchCommand(input, host, notice, extra = []) {
434
- const m = /^\/([a-zA-Z][a-zA-Z0-9-]*)(?:\s+([\s\S]*\S))?\s*$/.exec(input.trim());
435
- if (!m)
436
- return { handled: false, outcome: null };
437
- const raw = "/" + m[1].toLowerCase();
438
- const arg = m[2] ?? "";
439
- // Resolve aliases → canonical command (e.g. /quit → /exit).
440
- const resolved = resolveCommandDef(raw);
441
- const cmd = resolved?.name ?? raw;
442
- const slash = { cmd, arg };
443
- if (cmd === "/exit" || cmd === "/quit")
444
- return { handled: true, outcome: { quit: true } };
445
- if (cmd === "/clear") {
446
- host.agent()?.abort();
447
- host.replaceAgent(null);
448
- return { handled: true, outcome: { notice: "conversation cleared — context reset" } };
449
- }
450
- const custom = discoverCustomCommands(host.config.workdir);
451
- const customMatch = custom.find((c) => `/${c.name.toLowerCase()}` === cmd || c.aliases.some((a) => `/${a.toLowerCase().replace(/^\//, "")}` === cmd));
452
- if (customMatch) {
453
- if (customMatch.exec === "shell") {
454
- notice(`custom command "${customMatch.name}" declares shell execution; run it explicitly through a reviewed agent task`, "warn");
455
- }
456
- else {
457
- const prompt = runCustomCommand(customMatch, arg);
458
- notice(`custom command "${customMatch.name}"`, "info");
459
- return { handled: true, outcome: { task: prompt } };
460
- }
461
- return { handled: true, outcome: {} };
462
- }
463
- const entry = [...extra, ...coreCommands()].find((c) => c.name === cmd);
464
- if (!entry) {
465
- notice(`unknown command ${cmd} — try /help`, "error");
466
- return { handled: true, outcome: {} };
467
- }
468
- let outcome;
469
- try {
470
- outcome = await entry.run(slash, host, notice);
471
- }
472
- catch (err) {
473
- notice(err.message, "error");
474
- return { handled: true, outcome: {} };
475
- }
476
- return { handled: true, outcome: outcome ?? {} };
477
- }
@@ -1,64 +0,0 @@
1
- /** Process one MCP gate response — returns notice + optional next call. */
2
- export function reviewFromResponse(prev, tool, args, r) {
3
- const err = (text, level = "error") => ({ ok: false, notice: { text, level }, nextCall: null, state: null });
4
- if (!r.ok)
5
- return err(`prompt gate error: ${r.output.slice(0, 500)}`);
6
- const sc = (r.structured ?? {});
7
- const status = typeof sc.status === "string" ? sc.status : null;
8
- if (status === "pending_level_selection") {
9
- return {
10
- ok: true,
11
- notice: {
12
- text: "Which rewrite level?\n [1] light — minimal, close to original\n [2] balanced — structure + context + criteria\n [3] detailed — full spec with acceptance criteria\nReply 1, 2 or 3.",
13
- level: "info",
14
- },
15
- nextCall: null,
16
- state: { stage: "level", tool, argsBase: args },
17
- };
18
- }
19
- if (status === "pending_user_selection") {
20
- const rewritten = typeof sc.rewritten_prompt === "string" ? sc.rewritten_prompt : "";
21
- return {
22
- ok: true,
23
- notice: {
24
- text: `--- ORIGINAL ---\n${String(sc.original_prompt ?? "(missing)")}\n\n--- REWRITTEN ---\n${rewritten}\n\nWhich should be used?\n [1] original\n [2] rewritten\n [3] update — describe changes, rewrite again\n [4] cancel\nReply 1-4 (or 3 + your changes).`,
25
- level: "info",
26
- },
27
- nextCall: null,
28
- state: { stage: "selection", tool, argsBase: args, rewrittenPrompt: rewritten },
29
- };
30
- }
31
- if (status === "approved")
32
- return { ok: true, notice: { text: `✔ prompt approved:\n${String(sc.selected_prompt ?? "")}`, level: "info" }, nextCall: null, state: null };
33
- if (status === "cancelled")
34
- return { ok: true, notice: { text: "⏹ prompt gate cancelled — nothing approved.", level: "info" }, nextCall: null, state: null };
35
- return err(`prompt gate: unexpected status ${status ?? "(none)"}\n${r.output.slice(0, 500)}`, "warn");
36
- }
37
- /** Handle a user reply while /review is waiting (numbers 1-4 / feedback text). */
38
- export function reviewAnswerToCall(rv, text) {
39
- const m = /^([1-4])(?:\s+([\s\S]+))?$/.exec(text.trim());
40
- if (rv.stage === "level") {
41
- if (!m)
42
- return { notice: { text: "reply 1, 2 or 3 to pick the rewrite level", level: "warn" }, nextCall: null };
43
- const levels = ["light", "balanced", "detailed"];
44
- return { notice: null, nextCall: { tool: rv.tool, args: { ...rv.argsBase, rewrite_level: levels[Number(m[1]) - 1] } } };
45
- }
46
- // stage === "selection"
47
- if (!m)
48
- return { notice: { text: "reply 1-4 (or 3 + your requested changes)", level: "warn" }, nextCall: null };
49
- const n = m[1];
50
- if (n === "3") {
51
- const feedback = m[2]?.trim();
52
- if (!feedback)
53
- return { notice: { text: "update needs your requested changes: 3 <what to change>", level: "warn" }, nextCall: null };
54
- return { notice: null, nextCall: { tool: rv.tool, args: { ...rv.argsBase, selection: "update", feedback, rewritten_prompt: rv.rewrittenPrompt } } };
55
- }
56
- const selection = n === "1" ? "original" : n === "2" ? "rewritten" : "cancel";
57
- return {
58
- notice: null,
59
- nextCall: {
60
- tool: rv.tool,
61
- args: { ...rv.argsBase, selection, ...(selection === "rewritten" ? { rewritten_prompt: rv.rewrittenPrompt } : {}) },
62
- },
63
- };
64
- }