s-app-scoder-agent 0.8.20 → 0.9.2
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 +50 -6
- package/dist/agent.js +754 -79
- package/dist/agents-guide.js +201 -0
- package/dist/agents.js +76 -3
- package/dist/compaction.js +12 -4
- package/dist/config.js +16 -2
- package/dist/context.js +37 -11
- package/dist/diagnostics.js +188 -0
- package/dist/index.js +277 -6
- package/dist/mcp-import.js +204 -0
- package/dist/mcp.js +553 -54
- package/dist/memory.js +140 -0
- package/dist/permissions.js +18 -3
- package/dist/prompt.js +204 -84
- package/dist/repo-map.js +177 -0
- package/dist/sandbox.js +6 -5
- package/dist/session.js +29 -8
- package/dist/skills.js +211 -62
- package/dist/snapshot.js +5 -0
- package/dist/steering.js +4 -0
- package/dist/tools.js +374 -57
- package/dist/tui/app.js +79 -28
- package/dist/tui/ask-user-modal.js +25 -0
- package/dist/tui/chat-views.js +15 -8
- package/dist/tui/command-registry.js +297 -9
- package/dist/tui/commands.js +224 -36
- package/dist/tui/markdown.js +170 -12
- package/dist/tui/sidebar.js +36 -13
- package/dist/tui/state.js +101 -2
- package/dist/tui/theme.js +4 -0
- package/dist/tui/transcript.js +145 -0
- package/dist/watch.js +8 -0
- package/dist/webfetch.js +5 -2
- package/package.json +1 -1
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { listSessions, resolveSessionId } from "../session.js";
|
|
2
|
-
import { discoverSkills } from "../skills.js";
|
|
3
|
-
import {
|
|
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";
|
|
4
9
|
/** Format usage/cost block (dùng chung — REPL console.log, TUI notice). */
|
|
5
10
|
export function formatUsage(u) {
|
|
6
11
|
return u.calls > 0
|
|
@@ -30,8 +35,30 @@ export function formatMcpStatus(status) {
|
|
|
30
35
|
if (status.length === 0)
|
|
31
36
|
return "no MCP servers configured — add mcpServers to .scoderrc.json";
|
|
32
37
|
return status
|
|
33
|
-
.map((srv) =>
|
|
34
|
-
|
|
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
|
+
})
|
|
35
62
|
.join("\n");
|
|
36
63
|
}
|
|
37
64
|
/**
|
|
@@ -55,11 +82,73 @@ export function coreCommands() {
|
|
|
55
82
|
notice(formatUsage(host.agent()?.getUsage() ?? { calls: 0, inputTokens: 0, outputTokens: 0, cachedTokens: 0 }));
|
|
56
83
|
},
|
|
57
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
|
+
},
|
|
58
98
|
{
|
|
59
99
|
name: "/skills",
|
|
60
|
-
description: "list
|
|
61
|
-
run(
|
|
62
|
-
|
|
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");
|
|
63
152
|
},
|
|
64
153
|
},
|
|
65
154
|
{
|
|
@@ -122,10 +211,86 @@ export function coreCommands() {
|
|
|
122
211
|
return;
|
|
123
212
|
}
|
|
124
213
|
host.agent()?.setPermissionMode(mode);
|
|
214
|
+
host.updateConfig?.({ permissionMode: mode });
|
|
125
215
|
host.config = withPermissionMode(host.config, mode);
|
|
126
216
|
notice(`permission mode → ${mode}`);
|
|
127
217
|
},
|
|
128
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
|
+
},
|
|
129
294
|
{
|
|
130
295
|
name: "/sessions",
|
|
131
296
|
description: "list saved sessions — /sessions resume <id|last>",
|
|
@@ -147,8 +312,115 @@ export function coreCommands() {
|
|
|
147
312
|
notice(formatSessions(listSessions(host.config.workdir)));
|
|
148
313
|
},
|
|
149
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
|
+
},
|
|
150
418
|
];
|
|
151
419
|
}
|
|
420
|
+
/** Merged catalog of builtin command metadata — /help & completion read this. */
|
|
421
|
+
export function commandCatalog() {
|
|
422
|
+
return BUILTIN_COMMANDS.map((def) => ({ ...def }));
|
|
423
|
+
}
|
|
152
424
|
/** Clone config với permissionMode mới (host.config thay thế được). */
|
|
153
425
|
function withPermissionMode(cfg, mode) {
|
|
154
426
|
return { ...cfg, permissionMode: mode };
|
|
@@ -159,11 +431,14 @@ function withPermissionMode(cfg, mode) {
|
|
|
159
431
|
* /diff; REPL: không có) TRƯỚC khi gọi hàm này.
|
|
160
432
|
*/
|
|
161
433
|
export async function dispatchCommand(input, host, notice, extra = []) {
|
|
162
|
-
const m = /^\/([a-zA-Z][a-zA-Z0-9]*)(?:\s+(
|
|
434
|
+
const m = /^\/([a-zA-Z][a-zA-Z0-9-]*)(?:\s+([\s\S]*\S))?\s*$/.exec(input.trim());
|
|
163
435
|
if (!m)
|
|
164
436
|
return { handled: false, outcome: null };
|
|
165
|
-
const
|
|
437
|
+
const raw = "/" + m[1].toLowerCase();
|
|
166
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;
|
|
167
442
|
const slash = { cmd, arg };
|
|
168
443
|
if (cmd === "/exit" || cmd === "/quit")
|
|
169
444
|
return { handled: true, outcome: { quit: true } };
|
|
@@ -172,6 +447,19 @@ export async function dispatchCommand(input, host, notice, extra = []) {
|
|
|
172
447
|
host.replaceAgent(null);
|
|
173
448
|
return { handled: true, outcome: { notice: "conversation cleared — context reset" } };
|
|
174
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
|
+
}
|
|
175
463
|
const entry = [...extra, ...coreCommands()].find((c) => c.name === cmd);
|
|
176
464
|
if (!entry) {
|
|
177
465
|
notice(`unknown command ${cmd} — try /help`, "error");
|