s-app-scoder-agent 0.9.3 → 0.9.6
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/README.md +615 -638
- package/dist/{guardrails.js → agent/guardrails.js} +1 -1
- package/dist/agent/interaction.js +70 -0
- package/dist/agent/mcp-bridge.js +136 -0
- package/dist/agent/prompt-context.js +83 -0
- package/dist/agent/run-loop.js +318 -0
- package/dist/agent/session-persistence.js +61 -0
- package/dist/agent/tool-executor.js +158 -0
- package/dist/agent/types.js +88 -0
- package/dist/agent/verification.js +99 -0
- package/dist/agent.js +239 -1002
- package/dist/cli/args.js +230 -0
- package/dist/cli/init.js +79 -0
- package/dist/cli/main.js +211 -0
- package/dist/cli/mcp-command.js +185 -0
- package/dist/cli/repl.js +77 -0
- package/dist/cli/run-once.js +26 -0
- package/dist/{tui/commands.js → commands/definitions.js} +6 -16
- package/dist/commands/registry.js +260 -0
- package/dist/{config.js → config/config.js} +1 -1
- package/dist/{prompt-builder.js → context/prompt-builder.js} +12 -1
- package/dist/{prompt.js → context/prompt.js} +111 -9
- package/dist/{skills.js → context/skills.js} +41 -2
- package/dist/index.js +5 -798
- package/dist/io/console-reporter.js +24 -0
- package/dist/io/reporter.js +1 -0
- package/dist/mcp/connection.js +227 -0
- package/dist/mcp/env.js +8 -0
- package/dist/{mcp.js → mcp/manager.js} +3 -515
- package/dist/mcp/sse.js +28 -0
- package/dist/mcp/transport-http.js +140 -0
- package/dist/mcp/transport-stdio.js +98 -0
- package/dist/mcp/types.js +11 -0
- package/dist/media/image.js +149 -0
- package/dist/{server.js → server/server.js} +1 -1
- package/dist/{cost.js → session/cost.js} +1 -1
- package/dist/{session.js → session/session.js} +1 -1
- package/dist/{version.js → shared/version.js} +1 -1
- package/dist/tools/apply-patch.js +62 -0
- package/dist/tools/edit-file.js +29 -0
- package/dist/tools/file-edit-common.js +124 -0
- package/dist/tools/list-files.js +64 -0
- package/dist/tools/path-safety.js +75 -0
- package/dist/tools/read-file.js +64 -0
- package/dist/tools/registry.js +297 -0
- package/dist/tools/run-command.js +192 -0
- package/dist/tools/schemas.js +8 -0
- package/dist/tools/search.js +237 -0
- package/dist/tools/task-tool.js +48 -0
- package/dist/tools/types.js +1 -0
- package/dist/tools/write-file.js +34 -0
- package/dist/tui/app.js +59 -532
- package/dist/tui/ask-user-modal.js +2 -1
- package/dist/tui/at-complete.js +2 -93
- package/dist/tui/charset.js +41 -0
- package/dist/tui/chat-views.js +72 -16
- package/dist/tui/chrome.js +11 -5
- package/dist/tui/hooks/use-command-host.js +26 -0
- package/dist/tui/hooks/use-event-queue.js +69 -0
- package/dist/tui/hooks/use-keybinds.js +87 -0
- package/dist/tui/hooks/use-scroll.js +38 -0
- package/dist/tui/hooks/use-session-resume.js +47 -0
- package/dist/tui/hooks/use-submit.js +106 -0
- package/dist/tui/image.js +2 -149
- package/dist/tui/input.js +1 -1
- package/dist/tui/markdown/ast.js +5 -0
- package/dist/tui/markdown/diagram.js +239 -0
- package/dist/tui/markdown/inline-render.js +24 -0
- package/dist/tui/markdown/inline.js +93 -0
- package/dist/tui/markdown/parse.js +139 -0
- package/dist/tui/markdown/render.js +146 -0
- package/dist/tui/markdown/table.js +106 -0
- package/dist/tui/markdown.js +32 -379
- package/dist/tui/permission-modal.js +2 -1
- package/dist/tui/selectors.js +157 -0
- package/dist/tui/session-picker.js +4 -3
- package/dist/tui/sidebar.js +130 -15
- package/dist/tui/state.js +47 -118
- package/dist/tui/text-width.js +139 -0
- package/dist/tui/theme.js +7 -2
- package/dist/{build.js → workspace/build.js} +1 -1
- package/dist/workspace/file-index.js +93 -0
- package/package.json +5 -1
- package/dist/tools.js +0 -1223
- package/dist/tui/command-registry.js +0 -477
- package/dist/tui/review.js +0 -64
- package/dist/tui/transcript.js +0 -145
- /package/dist/{agents-guide.js → context/agents-guide.js} +0 -0
- /package/dist/{agents.js → context/agents.js} +0 -0
- /package/dist/{compaction.js → context/compaction.js} +0 -0
- /package/dist/{context.js → context/context.js} +0 -0
- /package/dist/{memory.js → context/memory.js} +0 -0
- /package/dist/{repo-map.js → context/repo-map.js} +0 -0
- /package/dist/{ui.js → io/terminal.js} +0 -0
- /package/dist/{llm.js → llm/llm.js} +0 -0
- /package/dist/{model-catalog.js → llm/model-catalog.js} +0 -0
- /package/dist/{provider-anthropic.js → llm/provider-anthropic.js} +0 -0
- /package/dist/{provider-registry.js → llm/provider-registry.js} +0 -0
- /package/dist/{retry.js → llm/retry.js} +0 -0
- /package/dist/{mcp-import.js → mcp/mcp-import.js} +0 -0
- /package/dist/{permissions.js → security/permissions.js} +0 -0
- /package/dist/{sandbox.js → security/sandbox.js} +0 -0
- /package/dist/{shell-scanner.js → security/shell-scanner.js} +0 -0
- /package/dist/{logger.js → session/logger.js} +0 -0
- /package/dist/{steering.js → session/steering.js} +0 -0
- /package/dist/{constants.js → shared/constants.js} +0 -0
- /package/dist/{json-schema.js → shared/json-schema.js} +0 -0
- /package/dist/{webfetch.js → tools/webfetch.js} +0 -0
- /package/dist/{diagnostics.js → workspace/diagnostics.js} +0 -0
- /package/dist/{diff.js → workspace/diff.js} +0 -0
- /package/dist/{snapshot.js → workspace/snapshot.js} +0 -0
- /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
|
-
}
|
package/dist/tui/review.js
DELETED
|
@@ -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
|
-
}
|