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.
- 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
package/dist/cli/args.js
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
export const HELP = `scoder — autonomous coding agent CLI
|
|
2
|
+
|
|
3
|
+
USAGE
|
|
4
|
+
scoder [options] "<task>" Run a task autonomously until the build passes
|
|
5
|
+
scoder Start interactive session (chat + tools)
|
|
6
|
+
scoder tui [task] Full-screen terminal UI (chat pane, live tool feed)
|
|
7
|
+
scoder init First-run wizard: writes .scoderrc.json + tests the connection
|
|
8
|
+
scoder list List saved sessions for this directory
|
|
9
|
+
scoder models [provider] [--refresh]
|
|
10
|
+
Model catalog (context window, pricing) —
|
|
11
|
+
synced from models.dev, cached 24h
|
|
12
|
+
scoder resume <id|last> [task] Resume a saved session (optionally with a follow-up task)
|
|
13
|
+
scoder logs [id|last] [filter] Print a session's JSONL log (requires --log-file runs)
|
|
14
|
+
scoder cost [--session <id>] [--last <n>]
|
|
15
|
+
Token usage & estimated cost from JSONL logs
|
|
16
|
+
scoder serve [--port <n>] [--host <addr>]
|
|
17
|
+
Run as an HTTP server: POST tasks, stream
|
|
18
|
+
progress via Server-Sent Events (see below)
|
|
19
|
+
scoder --version | --help
|
|
20
|
+
|
|
21
|
+
OPTIONS
|
|
22
|
+
-m, --model <name> LLM model (env SCODER_MODEL / OPENAI_MODEL, default gpt-4o-mini)
|
|
23
|
+
--base-url <url> OpenAI-compatible API base URL (env SCODER_BASE_URL / OPENAI_BASE_URL)
|
|
24
|
+
-k, --api-key <key> API key (env SCODER_API_KEY / OPENAI_API_KEY)
|
|
25
|
+
--provider <name> openai | anthropic (env SCODER_PROVIDER; auto-detected from model/baseUrl)
|
|
26
|
+
-B, --build <cmd> Build command = stop condition (auto-detected if omitted)
|
|
27
|
+
-d, --dir <path> Working directory (default: current directory)
|
|
28
|
+
-s, --max-steps <n> Max autonomous loop iterations (default 40)
|
|
29
|
+
-y, --yes Non-interactive mode (auto-approves commands)
|
|
30
|
+
--permission <mode> auto | ask | readonly (env SCODER_PERMISSION, default auto)
|
|
31
|
+
--agent <name> primary agent to run (default build; see /agent): build | plan | review | <custom>
|
|
32
|
+
--no-save Disable session auto-save to .scoder/sessions/
|
|
33
|
+
--log-file Write a JSONL event log to .scoder/logs/<session>.jsonl
|
|
34
|
+
(or set SCODER_LOG_FILE=1 / =/path/to/log.jsonl)
|
|
35
|
+
--session <id> (cost) Aggregate usage of one specific session
|
|
36
|
+
--last <n> (cost) Aggregate usage of the n most recent sessions
|
|
37
|
+
-h, --help Show this help
|
|
38
|
+
-v, --version Show version
|
|
39
|
+
|
|
40
|
+
CONFIG FILE
|
|
41
|
+
.scoderrc.json in the working directory (see .scoderrc.example.json):
|
|
42
|
+
{ "model": "...", "baseUrl": "...", "buildCmd": "...", "maxSteps": 40,
|
|
43
|
+
"permissions": { "mode": "auto", "allowCommands": ["npm "], "denyCommands": ["curl "] } }
|
|
44
|
+
|
|
45
|
+
EXAMPLES
|
|
46
|
+
scoder "add a divide function to src/math.ts with tests"
|
|
47
|
+
scoder --build "make test" "fix the failing test in utils"
|
|
48
|
+
scoder --permission ask "npm install express and wire up a route"
|
|
49
|
+
scoder -m claude-sonnet-4-5 "refactor the parser module" # auto-detects Anthropic
|
|
50
|
+
scoder resume last "now add unit tests"
|
|
51
|
+
`;
|
|
52
|
+
export function parseArgs(argv) {
|
|
53
|
+
const args = { task: null, yes: false, help: false, version: false };
|
|
54
|
+
const rest = [];
|
|
55
|
+
for (let i = 0; i < argv.length; i++) {
|
|
56
|
+
const a = argv[i];
|
|
57
|
+
const next = () => argv[++i];
|
|
58
|
+
switch (a) {
|
|
59
|
+
case "-m":
|
|
60
|
+
case "--model":
|
|
61
|
+
args.model = next();
|
|
62
|
+
break;
|
|
63
|
+
case "-k":
|
|
64
|
+
case "--api-key":
|
|
65
|
+
args.apiKey = next();
|
|
66
|
+
break;
|
|
67
|
+
case "--base-url":
|
|
68
|
+
args.baseUrl = next();
|
|
69
|
+
break;
|
|
70
|
+
case "-B":
|
|
71
|
+
case "--build":
|
|
72
|
+
args.buildCmd = next();
|
|
73
|
+
break;
|
|
74
|
+
case "-d":
|
|
75
|
+
case "--dir":
|
|
76
|
+
args.workdir = next();
|
|
77
|
+
break;
|
|
78
|
+
case "-s":
|
|
79
|
+
case "--max-steps":
|
|
80
|
+
args.maxSteps = Number(next());
|
|
81
|
+
break;
|
|
82
|
+
case "-y":
|
|
83
|
+
case "--yes":
|
|
84
|
+
args.yes = true;
|
|
85
|
+
break;
|
|
86
|
+
case "--agent":
|
|
87
|
+
args.agent = next();
|
|
88
|
+
break;
|
|
89
|
+
case "--permission": {
|
|
90
|
+
const mode = next();
|
|
91
|
+
if (mode !== "auto" && mode !== "ask" && mode !== "readonly") {
|
|
92
|
+
throw new Error(`Invalid --permission mode: ${mode} (expected auto | ask | readonly)`);
|
|
93
|
+
}
|
|
94
|
+
args.permission = mode;
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
case "--no-save":
|
|
98
|
+
args.noSave = true;
|
|
99
|
+
break;
|
|
100
|
+
case "--no-color":
|
|
101
|
+
args.noColor = true;
|
|
102
|
+
break;
|
|
103
|
+
case "--no-motion":
|
|
104
|
+
args.noMotion = true;
|
|
105
|
+
break;
|
|
106
|
+
case "--plain":
|
|
107
|
+
args.plain = true;
|
|
108
|
+
break;
|
|
109
|
+
case "--high-contrast":
|
|
110
|
+
args.highContrast = true;
|
|
111
|
+
break;
|
|
112
|
+
case "--log-file":
|
|
113
|
+
args.logFile = true;
|
|
114
|
+
break;
|
|
115
|
+
case "--session":
|
|
116
|
+
args.costSession = next();
|
|
117
|
+
break;
|
|
118
|
+
case "--last": {
|
|
119
|
+
const n = Number(next());
|
|
120
|
+
if (!Number.isFinite(n) || n < 1) {
|
|
121
|
+
throw new Error(`Invalid --last value (expected a positive integer): ${n}`);
|
|
122
|
+
}
|
|
123
|
+
args.costLast = Math.floor(n);
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
case "--provider": {
|
|
127
|
+
const p = next();
|
|
128
|
+
if (p !== "openai" && p !== "anthropic") {
|
|
129
|
+
throw new Error(`Invalid --provider: ${p} (expected openai | anthropic)`);
|
|
130
|
+
}
|
|
131
|
+
args.provider = p;
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
case "--port": {
|
|
135
|
+
const n = Number(next());
|
|
136
|
+
if (!Number.isInteger(n) || n < 0 || n > 65535) {
|
|
137
|
+
throw new Error(`Invalid --port value (expected 0-65535): ${n}`);
|
|
138
|
+
}
|
|
139
|
+
args.servePort = n;
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
case "--host":
|
|
143
|
+
args.serveHost = next();
|
|
144
|
+
break;
|
|
145
|
+
case "-h":
|
|
146
|
+
case "--help":
|
|
147
|
+
args.help = true;
|
|
148
|
+
break;
|
|
149
|
+
case "-v":
|
|
150
|
+
case "--version":
|
|
151
|
+
args.version = true;
|
|
152
|
+
break;
|
|
153
|
+
default: {
|
|
154
|
+
if (a.startsWith("-")) {
|
|
155
|
+
// MCP subcommand flags (--command, --url, --trust, --args) are not
|
|
156
|
+
// known globally — pass them through as positional args for runMcpCommand
|
|
157
|
+
// to interpret. Only throw if we are NOT in an mcp subcommand context.
|
|
158
|
+
const isMcpContext = rest[0] === "mcp" || argv.slice(0, i).includes("mcp");
|
|
159
|
+
if (!isMcpContext)
|
|
160
|
+
throw new Error(`Unknown option: ${a} (see --help)`);
|
|
161
|
+
rest.push(a);
|
|
162
|
+
// Consume the next token as this option's value if present.
|
|
163
|
+
if (i + 1 < argv.length && !argv[i + 1].startsWith("-")) {
|
|
164
|
+
rest.push(argv[++i]);
|
|
165
|
+
}
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
rest.push(a);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
args.task = rest.length > 0 ? rest.join(" ") : null;
|
|
173
|
+
// Subcommands: scoder list | scoder resume <id|last> [follow-up task] | scoder logs [id]
|
|
174
|
+
if (rest.length > 0 && rest[0] === "list") {
|
|
175
|
+
args.subcommand = "list";
|
|
176
|
+
args.task = null;
|
|
177
|
+
}
|
|
178
|
+
else if (rest[0] === "resume") {
|
|
179
|
+
args.subcommand = "resume";
|
|
180
|
+
args.resumeId = rest[1] ?? "last";
|
|
181
|
+
args.task = rest.length > 2 ? rest.slice(2).join(" ") : null;
|
|
182
|
+
}
|
|
183
|
+
else if (rest[0] === "logs") {
|
|
184
|
+
args.subcommand = "logs";
|
|
185
|
+
args.logsId = rest[1] ?? "last";
|
|
186
|
+
args.logsFilter = rest.length > 2 ? rest.slice(2).join(" ") : undefined;
|
|
187
|
+
}
|
|
188
|
+
else if (rest[0] === "cost") {
|
|
189
|
+
args.subcommand = "cost";
|
|
190
|
+
args.task = null;
|
|
191
|
+
}
|
|
192
|
+
else if (rest[0] === "serve") {
|
|
193
|
+
args.subcommand = "serve";
|
|
194
|
+
args.task = null;
|
|
195
|
+
}
|
|
196
|
+
else if (rest[0] === "tui") {
|
|
197
|
+
// scoder tui [initial-task] — full-screen Ink interface
|
|
198
|
+
args.subcommand = "tui";
|
|
199
|
+
args.task = rest.length > 1 ? rest.slice(1).join(" ") : null;
|
|
200
|
+
}
|
|
201
|
+
else if (rest[0] === "models") {
|
|
202
|
+
// scoder models [provider] [--refresh] — models.dev catalog (I-02)
|
|
203
|
+
args.subcommand = "models";
|
|
204
|
+
args.modelsProvider = rest[1] && !rest[1].startsWith("--") ? rest[1] : undefined;
|
|
205
|
+
args.modelsRefresh = rest.includes("--refresh");
|
|
206
|
+
args.task = null;
|
|
207
|
+
}
|
|
208
|
+
else if (rest[0] === "init") {
|
|
209
|
+
args.subcommand = "init";
|
|
210
|
+
args.task = null;
|
|
211
|
+
}
|
|
212
|
+
else if (rest[0] === "mcp") {
|
|
213
|
+
// scoder mcp list|add <name>|remove <name>|test <name>
|
|
214
|
+
// e.g. scoder mcp add github --command "npx mcp-github" --url "..." --trust readonly
|
|
215
|
+
args.subcommand = "mcp";
|
|
216
|
+
const m = rest[1];
|
|
217
|
+
if (m === "list" || m === "add" || m === "remove" || m === "test" || m === "import") {
|
|
218
|
+
args.mcpCmd = m;
|
|
219
|
+
}
|
|
220
|
+
else if (m) {
|
|
221
|
+
args.mcpCmd = "list";
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
args.mcpCmd = "list";
|
|
225
|
+
}
|
|
226
|
+
args.mcpArgs = rest.slice(2);
|
|
227
|
+
args.task = null;
|
|
228
|
+
}
|
|
229
|
+
return args;
|
|
230
|
+
}
|
package/dist/cli/init.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import { createInterface } from "node:readline";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import * as ui from "../io/terminal.js";
|
|
5
|
+
/**
|
|
6
|
+
* `scoder init` — first-run wizard: asks for the essentials, writes
|
|
7
|
+
* .scoderrc.json, then offers a live connection test against the provider.
|
|
8
|
+
*/
|
|
9
|
+
export async function runInit() {
|
|
10
|
+
const { existsSync, writeFileSync } = await import("node:fs");
|
|
11
|
+
const rcPath = resolve(process.cwd(), ".scoderrc.json");
|
|
12
|
+
ui.banner("scoder init — configure this project", [
|
|
13
|
+
["file", rcPath],
|
|
14
|
+
["note", "answers are saved to .scoderrc.json (env vars still override)"],
|
|
15
|
+
]);
|
|
16
|
+
if (existsSync(rcPath)) {
|
|
17
|
+
const rl0 = createInterface({ input: process.stdin, output: process.stdout });
|
|
18
|
+
const overwrite = await new Promise((res) => rl0.question(`.scoderrc.json already exists — overwrite? [y/N] `, res));
|
|
19
|
+
rl0.close();
|
|
20
|
+
if (!/^y(es)?$/i.test(overwrite.trim())) {
|
|
21
|
+
ui.info("kept the existing .scoderrc.json");
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
26
|
+
const ask = (q, def) => new Promise((res) => rl.question(`${q}${def ? ` [${def}]` : ""}: `, (a) => res(a.trim() || def)));
|
|
27
|
+
// I-03: probe local engines first — suggest when found (no auto-switch when
|
|
28
|
+
// a cloud key exists; user always decides).
|
|
29
|
+
ui.info("probing local engines (ollama :11434, lmstudio :1234, vllm :8000)…");
|
|
30
|
+
const { discoverLocalEngines } = await import("../llm/provider-registry.js");
|
|
31
|
+
const locals = await discoverLocalEngines();
|
|
32
|
+
for (const e of locals) {
|
|
33
|
+
ui.info(` found ${e.provider.id} at ${e.provider.baseUrl} — ${e.models.length} model(s), e.g. ${e.models[0]}`);
|
|
34
|
+
}
|
|
35
|
+
const suggestBase = locals[0]?.provider.baseUrl ?? "https://api.openai.com/v1";
|
|
36
|
+
const suggestModel = locals[0]?.models[0] ?? "gpt-4o-mini";
|
|
37
|
+
const baseUrl = await ask("API base URL (OpenAI-compatible)", suggestBase);
|
|
38
|
+
const apiKey = await ask("API key (SCODER_API_KEY/OPENAI_API_KEY also work at runtime)", "");
|
|
39
|
+
const model = await ask("Model", suggestModel);
|
|
40
|
+
const buildCmd = await ask("Build command (empty = auto-detect)", "");
|
|
41
|
+
const permission = (await ask("Permission mode (auto | ask | readonly)", "auto")).toLowerCase();
|
|
42
|
+
rl.close();
|
|
43
|
+
const provider = /api\.anthropic\.com/.test(baseUrl) || /^claude-/i.test(model) ? "anthropic" : "openai";
|
|
44
|
+
const rc = {
|
|
45
|
+
model,
|
|
46
|
+
baseUrl,
|
|
47
|
+
provider,
|
|
48
|
+
...(buildCmd ? { buildCmd } : {}),
|
|
49
|
+
permissions: { mode: ["auto", "ask", "readonly"].includes(permission) ? permission : "auto" },
|
|
50
|
+
};
|
|
51
|
+
writeFileSync(rcPath, JSON.stringify(rc, null, 2) + "\n");
|
|
52
|
+
ui.success(`wrote ${rcPath} (provider: ${provider})`);
|
|
53
|
+
const rl2 = createInterface({ input: process.stdin, output: process.stdout });
|
|
54
|
+
const doTest = await new Promise((res) => rl2.question("Test the connection now? [Y/n] ", res));
|
|
55
|
+
rl2.close();
|
|
56
|
+
if (!/^y(es)?$/i.test(doTest.trim()) && doTest.trim() !== "") {
|
|
57
|
+
ui.info("skipped connection test");
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (!apiKey) {
|
|
61
|
+
ui.warn("no API key provided — set SCODER_API_KEY/OPENAI_API_KEY before running tasks");
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
ui.info(`connecting to ${baseUrl} (${provider}) …`);
|
|
65
|
+
try {
|
|
66
|
+
const { LlmClient } = await import("../llm/llm.js");
|
|
67
|
+
const { AnthropicClient } = await import("../llm/provider-anthropic.js");
|
|
68
|
+
const client = provider === "anthropic"
|
|
69
|
+
? new AnthropicClient({ baseUrl, apiKey, model, maxTokens: 16, temperature: 0 })
|
|
70
|
+
: new LlmClient({ baseUrl, apiKey, model, maxTokens: 16, temperature: 0 });
|
|
71
|
+
const res = await client.chat([{ role: "user", content: "Reply with exactly: OK" }], []);
|
|
72
|
+
const reply = (res.message.content ?? "").trim();
|
|
73
|
+
ui.success(`connection OK — model replied: ${JSON.stringify(reply)}`);
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
ui.error(`connection test failed: ${err.message}`);
|
|
77
|
+
process.exitCode = 2;
|
|
78
|
+
}
|
|
79
|
+
}
|
package/dist/cli/main.js
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { loadConfig } from "../config/config.js";
|
|
5
|
+
import { Agent } from "../agent.js";
|
|
6
|
+
import { listSessions, resolveSessionId } from "../session/session.js";
|
|
7
|
+
import { JsonlLogger } from "../session/logger.js";
|
|
8
|
+
import { collectUsage, formatCostReport } from "../session/cost.js";
|
|
9
|
+
import { getVersion as getPackageVersion } from "../shared/version.js";
|
|
10
|
+
import { selectableAgents } from "../context/agents.js";
|
|
11
|
+
import * as ui from "../io/terminal.js";
|
|
12
|
+
import { parseArgs, HELP } from "./args.js";
|
|
13
|
+
import { runOnce } from "./run-once.js";
|
|
14
|
+
import { runInit } from "./init.js";
|
|
15
|
+
import { repl } from "./repl.js";
|
|
16
|
+
import { runMcpCommand } from "./mcp-command.js";
|
|
17
|
+
// Single source of truth: read version from package.json (see src/version.ts).
|
|
18
|
+
const VERSION = getPackageVersion();
|
|
19
|
+
// Safety net: a stray promise rejection (e.g. a provider socket aborting after
|
|
20
|
+
// its consumer already moved on) must log — never hard-crash the agent.
|
|
21
|
+
process.on("unhandledRejection", (reason) => {
|
|
22
|
+
const msg = reason instanceof Error ? `${reason.name}: ${reason.message}` : String(reason);
|
|
23
|
+
ui.warn(`background error (ignored): ${msg.slice(0, 200)}`);
|
|
24
|
+
});
|
|
25
|
+
async function main() {
|
|
26
|
+
let args;
|
|
27
|
+
try {
|
|
28
|
+
args = parseArgs(process.argv.slice(2));
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
ui.error(err.message);
|
|
32
|
+
process.exit(2);
|
|
33
|
+
}
|
|
34
|
+
if (args.version) {
|
|
35
|
+
console.log(VERSION);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (args.help) {
|
|
39
|
+
console.log(HELP);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (args.noColor)
|
|
43
|
+
process.env.NO_COLOR = "1";
|
|
44
|
+
if (args.noMotion)
|
|
45
|
+
process.env.SCODER_NO_MOTION = "1";
|
|
46
|
+
if (args.plain)
|
|
47
|
+
process.env.SCODER_PLAIN = "1";
|
|
48
|
+
if (args.highContrast)
|
|
49
|
+
process.env.SCODER_THEME = "high-contrast";
|
|
50
|
+
const cfg = loadConfig({
|
|
51
|
+
model: args.model,
|
|
52
|
+
baseUrl: args.baseUrl,
|
|
53
|
+
apiKey: args.apiKey,
|
|
54
|
+
buildCmd: args.buildCmd ?? undefined,
|
|
55
|
+
workdir: args.workdir ? resolve(args.workdir) : undefined,
|
|
56
|
+
maxSteps: args.maxSteps,
|
|
57
|
+
yes: args.yes,
|
|
58
|
+
permissionMode: args.permission ?? undefined,
|
|
59
|
+
provider: args.provider ?? undefined,
|
|
60
|
+
noSave: args.noSave,
|
|
61
|
+
logFile: args.logFile,
|
|
62
|
+
agent: args.agent,
|
|
63
|
+
});
|
|
64
|
+
// v2: --agent must reference a selectable (mode primary|all) agent.
|
|
65
|
+
if (args.agent) {
|
|
66
|
+
const ok = selectableAgents(cfg.workdir).some((a) => a.name === args.agent);
|
|
67
|
+
if (!ok) {
|
|
68
|
+
ui.error(`unknown or non-primary agent "${args.agent}" — selectable: ${selectableAgents(cfg.workdir).map((a) => a.name).join(", ")}`);
|
|
69
|
+
process.exit(2);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// Subcommands
|
|
73
|
+
if (args.subcommand === "init") {
|
|
74
|
+
await runInit();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (args.subcommand === "list") {
|
|
78
|
+
const sessions = listSessions(cfg.workdir);
|
|
79
|
+
if (sessions.length === 0) {
|
|
80
|
+
console.log("No saved sessions in this directory.");
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
for (const s of sessions) {
|
|
84
|
+
console.log(`${s.id} steps=${String(s.steps).padStart(3)} ${s.updatedAt} ${s.task}`);
|
|
85
|
+
}
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (args.subcommand === "resume") {
|
|
89
|
+
const s = resolveSessionId(cfg.workdir, args.resumeId ?? "last");
|
|
90
|
+
if (!s) {
|
|
91
|
+
ui.error(`Session not found: ${args.resumeId ?? "last"}. Run \`scoder list\` to see saved sessions.`);
|
|
92
|
+
process.exit(2);
|
|
93
|
+
}
|
|
94
|
+
const agent = new Agent(cfg);
|
|
95
|
+
process.on("SIGINT", () => {
|
|
96
|
+
ui.warn("\nSIGINT received — finishing current tool, then stopping...");
|
|
97
|
+
agent.abort();
|
|
98
|
+
});
|
|
99
|
+
try {
|
|
100
|
+
const result = await agent.resume(s, args.task ?? undefined);
|
|
101
|
+
agent.dispose();
|
|
102
|
+
process.exit(result.success ? 0 : 1);
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
agent.dispose();
|
|
106
|
+
ui.error(err.message);
|
|
107
|
+
process.exit(2);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (args.subcommand === "logs") {
|
|
111
|
+
let id = args.logsId ?? "last";
|
|
112
|
+
if (id === "last") {
|
|
113
|
+
const sessions = listSessions(cfg.workdir);
|
|
114
|
+
if (sessions.length === 0) {
|
|
115
|
+
ui.error("No saved sessions in this directory. Run with --log-file to record logs.");
|
|
116
|
+
process.exit(2);
|
|
117
|
+
}
|
|
118
|
+
id = sessions[0].id;
|
|
119
|
+
}
|
|
120
|
+
const events = JsonlLogger.read(cfg.workdir, id);
|
|
121
|
+
if (events === null || events.length === 0) {
|
|
122
|
+
ui.error(`No log found for session ${id}. Logs require --log-file (or SCODER_LOG_FILE=1).`);
|
|
123
|
+
process.exit(2);
|
|
124
|
+
}
|
|
125
|
+
const filter = args.logsFilter?.toLowerCase();
|
|
126
|
+
for (const ev of events) {
|
|
127
|
+
const line = JSON.stringify(ev);
|
|
128
|
+
if (filter && !line.toLowerCase().includes(filter))
|
|
129
|
+
continue;
|
|
130
|
+
console.log(line);
|
|
131
|
+
}
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (args.subcommand === "cost") {
|
|
135
|
+
const sessionCosts = collectUsage(cfg.workdir, {
|
|
136
|
+
sessionId: args.costSession,
|
|
137
|
+
last: args.costLast,
|
|
138
|
+
pricingOverrides: cfg.modelPricing,
|
|
139
|
+
});
|
|
140
|
+
console.log(formatCostReport(sessionCosts));
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (args.subcommand === "models") {
|
|
144
|
+
const { loadCatalog, cacheAgeHours } = await import("../llm/model-catalog.js");
|
|
145
|
+
const filter = args.modelsProvider ?? null;
|
|
146
|
+
const { models, source } = await loadCatalog(args.modelsRefresh === true);
|
|
147
|
+
const age = cacheAgeHours();
|
|
148
|
+
const ageStr = source === "fallback" ? "offline fallback snapshot" : age < 0 ? "?" : `${age.toFixed(1)}h ago`;
|
|
149
|
+
console.log(`models catalog — source: ${source} (updated ${ageStr}), ${Object.keys(models).length} entries\n`);
|
|
150
|
+
const rows = Object.values(models)
|
|
151
|
+
.filter((m) => !filter || m.provider === filter || m.id.includes(filter))
|
|
152
|
+
.sort((a, b) => a.provider.localeCompare(b.provider) || a.id.localeCompare(b.id))
|
|
153
|
+
.slice(0, 60);
|
|
154
|
+
for (const m of rows) {
|
|
155
|
+
const flags = [m.supportCache ? "cache" : null, m.supportReasoning ? "reasoning" : null].filter(Boolean).join(",");
|
|
156
|
+
console.log(` ${m.provider}/${m.id} ctx=${(m.contextWindow / 1000).toFixed(0)}k${m.pricing ? ` $${m.pricing.input}/$${m.pricing.output} per 1M` : ""}${flags ? ` [${flags}]` : ""}`);
|
|
157
|
+
}
|
|
158
|
+
if (rows.length === 0)
|
|
159
|
+
console.log(` (no models match "${filter}")`);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (args.subcommand === "mcp") {
|
|
163
|
+
await runMcpCommand(cfg, args.mcpCmd ?? "list", args.mcpArgs ?? []);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (args.subcommand === "tui") {
|
|
167
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
168
|
+
ui.warn("TUI requires an interactive terminal (TTY) — falling back to the classic REPL.");
|
|
169
|
+
await repl(cfg);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (!cfg.apiKey) {
|
|
173
|
+
ui.warn("No API key configured — tasks will fail until SCODER_API_KEY (or OPENAI_API_KEY) is set.");
|
|
174
|
+
}
|
|
175
|
+
const { runTui } = await import("../tui/app.js");
|
|
176
|
+
await runTui(cfg, args.task);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (args.subcommand === "serve") {
|
|
180
|
+
const { startServer } = await import("../server/server.js");
|
|
181
|
+
if (!cfg.apiKey) {
|
|
182
|
+
ui.warn("No API key configured — runs will fail until SCODER_API_KEY (or OPENAI_API_KEY) is set.");
|
|
183
|
+
}
|
|
184
|
+
const srv = await startServer(cfg, { host: args.serveHost, port: args.servePort });
|
|
185
|
+
const token = process.env.SCODER_SERVER_TOKEN ? " · token auth" : "";
|
|
186
|
+
ui.banner(`scoder serve @ http://${args.serveHost ?? "127.0.0.1"}:${srv.port}${token}`, [
|
|
187
|
+
["task", `POST /api/runs { "task": "..." } → 202 { id, eventsUrl }`],
|
|
188
|
+
["events", `GET /api/runs/<id>/events → SSE stream`],
|
|
189
|
+
["list", `GET /api/runs`],
|
|
190
|
+
["abort", `POST /api/runs/<id>/abort`],
|
|
191
|
+
["workdir", cfg.workdir],
|
|
192
|
+
["model", `${cfg.model} · permission ${cfg.permissionMode}`],
|
|
193
|
+
]);
|
|
194
|
+
const shutdown = async () => {
|
|
195
|
+
ui.info("shutting down server...");
|
|
196
|
+
await srv.close();
|
|
197
|
+
process.exit(0);
|
|
198
|
+
};
|
|
199
|
+
process.on("SIGINT", () => {
|
|
200
|
+
ui.warn("\nSIGINT — shutting down server, aborting in-flight runs...");
|
|
201
|
+
void shutdown();
|
|
202
|
+
});
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (args.task) {
|
|
206
|
+
const code = await runOnce(cfg, args.task);
|
|
207
|
+
process.exit(code);
|
|
208
|
+
}
|
|
209
|
+
await repl(cfg);
|
|
210
|
+
}
|
|
211
|
+
export { main };
|