deepseek-cli-mode 0.0.0-stage → 1.0.0
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/LICENSE +21 -0
- package/README.md +257 -2
- package/bin/deepseek.js +263 -0
- package/docs/PARITY.md +108 -0
- package/index.js +13 -0
- package/package.json +49 -4
- package/runner/runner.js +1298 -0
package/runner/runner.js
ADDED
|
@@ -0,0 +1,1298 @@
|
|
|
1
|
+
// dsh-cli runner — an interactive, Claude-Code-style driver for DeepSeek Harness.
|
|
2
|
+
//
|
|
3
|
+
// Loaded by the `dsh-cli` profile as a Cordis plugin (name: "./runner.js"), this
|
|
4
|
+
// plugin keeps ONE live Agent across many turns in a single process, so the
|
|
5
|
+
// session log, tool state, approvals, and the model's request prefix stay warm
|
|
6
|
+
// between messages — real multi-turn continuity, unlike the one-shot
|
|
7
|
+
// `headless` profile.
|
|
8
|
+
//
|
|
9
|
+
// Modes (read from the launcher's cmdlineArgs):
|
|
10
|
+
// deepseek interactive REPL (offers to resume the last session)
|
|
11
|
+
// deepseek "task..." one-shot: run the task, print the answer, exit
|
|
12
|
+
// deepseek -r | --resume resume the most recent session in this directory
|
|
13
|
+
// deepseek -r <id> resume a specific session
|
|
14
|
+
// deepseek -c | --continue continue the most recent session, no prompting
|
|
15
|
+
// deepseek --list list recent sessions in this directory
|
|
16
|
+
// deepseek -y | --yes auto-approve actions that would ask
|
|
17
|
+
import { randomUUID } from "node:crypto";
|
|
18
|
+
import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
19
|
+
import { basename, join, resolve as resolvePath } from "node:path";
|
|
20
|
+
import { createInterface } from "node:readline";
|
|
21
|
+
import { zstdDecompressSync } from "node:zlib";
|
|
22
|
+
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
23
|
+
import { SessionId } from "@deepseek-ai/dsh-session";
|
|
24
|
+
import { installModelSelection } from "@deepseek-ai/dsh-agent";
|
|
25
|
+
import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
|
|
26
|
+
|
|
27
|
+
/** Stable Cordis plugin name. */
|
|
28
|
+
export const name = "cli-runner";
|
|
29
|
+
/** Core services required before a turn can start. */
|
|
30
|
+
export const inject = ["agents", "agentDefaultModel", "sessionPersistence"];
|
|
31
|
+
|
|
32
|
+
const VERSION = process.env.DSH_CLI_VERSION ?? "0.1.0";
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Terminal helpers
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
const tty = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
38
|
+
const showReasoning = process.env.DSH_CLI_REASONING === "1";
|
|
39
|
+
const paint = (fn, s) => (tty ? fn(s) : s);
|
|
40
|
+
const ansi = {
|
|
41
|
+
dim: (s) => `\x1b[2m${s}\x1b[22m`,
|
|
42
|
+
bold: (s) => `\x1b[1m${s}\x1b[22m`,
|
|
43
|
+
italic: (s) => `\x1b[3m${s}\x1b[23m`,
|
|
44
|
+
cyan: (s) => `\x1b[36m${s}\x1b[39m`,
|
|
45
|
+
green: (s) => `\x1b[32m${s}\x1b[39m`,
|
|
46
|
+
yellow: (s) => `\x1b[33m${s}\x1b[39m`,
|
|
47
|
+
red: (s) => `\x1b[31m${s}\x1b[39m`,
|
|
48
|
+
magenta: (s) => `\x1b[35m${s}\x1b[39m`,
|
|
49
|
+
blue: (s) => `\x1b[94m${s}\x1b[39m`,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
let out = (s) => process.stdout.write(s);
|
|
53
|
+
let err = (s) => process.stderr.write(s);
|
|
54
|
+
|
|
55
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
56
|
+
|
|
57
|
+
function truncate(s, max) {
|
|
58
|
+
const text = String(s);
|
|
59
|
+
return text.length <= max ? text : text.slice(0, max - 1) + "…";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Error text with an optional stack for DSH_CLI_DEBUG=1. */
|
|
63
|
+
function describeError(error) {
|
|
64
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
65
|
+
if (process.env.DSH_CLI_DEBUG === "1" && error instanceof Error && error.stack) return `${message}\n${error.stack}`;
|
|
66
|
+
return message;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Compact token counts for the per-turn usage line. */
|
|
70
|
+
function formatTokens(n) {
|
|
71
|
+
if (!Number.isFinite(n) || n <= 0) return "0";
|
|
72
|
+
if (n >= 1000) return `${(n / 1000).toFixed(1)}k`;
|
|
73
|
+
return String(n);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// Markdown-lite line formatter (tty only; piped output stays raw)
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
function createFormatter() {
|
|
80
|
+
let inFence = false;
|
|
81
|
+
return function format(line) {
|
|
82
|
+
if (!tty) return line;
|
|
83
|
+
const trimmed = line.trimStart();
|
|
84
|
+
if (trimmed.startsWith("```")) {
|
|
85
|
+
inFence = !inFence;
|
|
86
|
+
return paint(ansi.dim, paint(ansi.cyan, line));
|
|
87
|
+
}
|
|
88
|
+
if (inFence) return paint(ansi.cyan, line);
|
|
89
|
+
let text = line;
|
|
90
|
+
// Headings
|
|
91
|
+
text = text.replace(/^(\s*)#{1,6}\s+(.*)$/, (_, lead, body) => lead + paint(ansi.bold, body));
|
|
92
|
+
// Bold, inline code
|
|
93
|
+
text = text.replace(/\*\*([^*\n]+)\*\*/g, (_, body) => paint(ansi.bold, body));
|
|
94
|
+
text = text.replace(/`([^`\n]+)`/g, (_, body) => paint(ansi.cyan, body));
|
|
95
|
+
return text;
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// Tool-call previews
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
const TOOL_ICONS = {
|
|
103
|
+
read: "◇",
|
|
104
|
+
write: "✎",
|
|
105
|
+
edit: "✎",
|
|
106
|
+
str_replace_editor: "✎",
|
|
107
|
+
bash: "$",
|
|
108
|
+
pwsh: "$",
|
|
109
|
+
glob: "⌕",
|
|
110
|
+
grep: "⌕",
|
|
111
|
+
web_search: "◎",
|
|
112
|
+
web_fetch: "◎",
|
|
113
|
+
todo_write: "☑",
|
|
114
|
+
subagent: "⑂",
|
|
115
|
+
goal: "◈",
|
|
116
|
+
skill: "◆",
|
|
117
|
+
present: "▣",
|
|
118
|
+
ask_user_question: "?",
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
function previewToolCall(name, rawArgs) {
|
|
122
|
+
let args;
|
|
123
|
+
try {
|
|
124
|
+
args = JSON.parse(rawArgs);
|
|
125
|
+
} catch {
|
|
126
|
+
return truncate(rawArgs ?? "", 80);
|
|
127
|
+
}
|
|
128
|
+
if (args === null || typeof args !== "object") return "";
|
|
129
|
+
const pick = args.path ?? args.file_path ?? args.filePath ?? args.command ?? args.cmd
|
|
130
|
+
?? args.pattern ?? args.query ?? args.url ?? args.description ?? args.task ?? args.id;
|
|
131
|
+
if (typeof pick !== "string") {
|
|
132
|
+
const json = JSON.stringify(args);
|
|
133
|
+
return json && json !== "{}" ? truncate(json, 80) : "";
|
|
134
|
+
}
|
|
135
|
+
return truncate(pick.replace(/\s+/g, " "), 90);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
// Rendering
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
/**
|
|
142
|
+
* @param {object} options
|
|
143
|
+
* @param {(text: string) => void} options.write where transcript text goes
|
|
144
|
+
* @param {boolean} options.capture collect assistant text instead of printing it
|
|
145
|
+
*/
|
|
146
|
+
function createRenderer({ write, capture }) {
|
|
147
|
+
const format = createFormatter();
|
|
148
|
+
let buffer = ""; // partial text line
|
|
149
|
+
let reasoning = ""; // this attempt's reasoning text
|
|
150
|
+
let lastReasoning = ""; // across the whole session, for /thinking
|
|
151
|
+
let streamedThisTurn = false;
|
|
152
|
+
let lineDirty = false; // something was written without a trailing newline
|
|
153
|
+
let lastAssistantText = "";
|
|
154
|
+
let lastTurnKind = undefined;
|
|
155
|
+
let lastTodos = [];
|
|
156
|
+
let turnUsage = { input: 0, output: 0 };
|
|
157
|
+
|
|
158
|
+
const endLine = () => {
|
|
159
|
+
if (lineDirty) {
|
|
160
|
+
write("\n");
|
|
161
|
+
lineDirty = false;
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const feed = (text) => {
|
|
166
|
+
if (capture) return;
|
|
167
|
+
buffer += text;
|
|
168
|
+
let index;
|
|
169
|
+
while ((index = buffer.indexOf("\n")) !== -1) {
|
|
170
|
+
const line = buffer.slice(0, index);
|
|
171
|
+
buffer = buffer.slice(index + 1);
|
|
172
|
+
write(format(line) + "\n");
|
|
173
|
+
lineDirty = false;
|
|
174
|
+
}
|
|
175
|
+
if (buffer !== "") {
|
|
176
|
+
lineDirty = true;
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const flushText = () => {
|
|
181
|
+
if (buffer !== "") {
|
|
182
|
+
if (!capture) {
|
|
183
|
+
write(format(buffer) + "\n");
|
|
184
|
+
lineDirty = false;
|
|
185
|
+
}
|
|
186
|
+
buffer = "";
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
get lastReasoning() {
|
|
192
|
+
return lastReasoning;
|
|
193
|
+
},
|
|
194
|
+
get lastAssistantText() {
|
|
195
|
+
return lastAssistantText;
|
|
196
|
+
},
|
|
197
|
+
get lastTurnKind() {
|
|
198
|
+
return lastTurnKind;
|
|
199
|
+
},
|
|
200
|
+
get lastTodos() {
|
|
201
|
+
return lastTodos;
|
|
202
|
+
},
|
|
203
|
+
resetTurn() {
|
|
204
|
+
streamedThisTurn = false;
|
|
205
|
+
reasoning = "";
|
|
206
|
+
turnUsage = { input: 0, output: 0 };
|
|
207
|
+
},
|
|
208
|
+
/** Process one live assistant-stream frame. */
|
|
209
|
+
frame(frame) {
|
|
210
|
+
if (frame.type === "start") {
|
|
211
|
+
// A new attempt/step begins: never glue its text onto the previous line.
|
|
212
|
+
flushText();
|
|
213
|
+
endLine();
|
|
214
|
+
streamedThisTurn = false;
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (frame.type === "chunk") {
|
|
218
|
+
const chunk = frame.chunk;
|
|
219
|
+
if (chunk.type === "text-delta") {
|
|
220
|
+
streamedThisTurn = true;
|
|
221
|
+
feed(chunk.text);
|
|
222
|
+
} else if (chunk.type === "reasoning-delta") {
|
|
223
|
+
reasoning += chunk.text;
|
|
224
|
+
if (showReasoning && !capture) {
|
|
225
|
+
write(paint(ansi.dim, paint(ansi.italic, chunk.text)));
|
|
226
|
+
lineDirty = true;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
/** Process one durable session event. */
|
|
232
|
+
event(event) {
|
|
233
|
+
const data = event.data;
|
|
234
|
+
switch (event.type) {
|
|
235
|
+
case "tool/call": {
|
|
236
|
+
if (data.name === "todo_write") break; // the checklist below is the presentation
|
|
237
|
+
endLine();
|
|
238
|
+
const icon = TOOL_ICONS[data.name] ?? "⏺";
|
|
239
|
+
const preview = previewToolCall(data.name, data.arguments);
|
|
240
|
+
write(paint(ansi.dim, paint(ansi.italic, ` ${icon} ${data.name}${preview ? " " + preview : ""}`)) + "\n");
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
case "tool/result": {
|
|
244
|
+
if (data.error) {
|
|
245
|
+
write(paint(ansi.dim, paint(ansi.italic, ` ✗ ${data.error.name}: ${data.error.code}`)) + "\n");
|
|
246
|
+
}
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
case "assistant/message": {
|
|
250
|
+
const blocks = data.message?.content ?? [];
|
|
251
|
+
const text = blocks.filter((b) => b.type === "text").map((b) => b.text).join("");
|
|
252
|
+
if (text !== "") lastAssistantText = text;
|
|
253
|
+
if (data.usage) {
|
|
254
|
+
turnUsage.input += data.usage.inputTokens ?? 0;
|
|
255
|
+
turnUsage.output += data.usage.outputTokens ?? 0;
|
|
256
|
+
}
|
|
257
|
+
if (!streamedThisTurn) {
|
|
258
|
+
// Non-streaming adapter: print the assembled message.
|
|
259
|
+
if (!capture) feed(text);
|
|
260
|
+
}
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
case "todo/write": {
|
|
264
|
+
const todos = data.todos ?? [];
|
|
265
|
+
const previous = new Map(lastTodos.map((todo) => [todo.content, todo.status]));
|
|
266
|
+
const firstRender = lastTodos.length === 0;
|
|
267
|
+
lastTodos = todos;
|
|
268
|
+
if (capture) break;
|
|
269
|
+
endLine();
|
|
270
|
+
flushText();
|
|
271
|
+
const done = todos.filter((todo) => todo.status === "completed").length;
|
|
272
|
+
write(paint(ansi.dim, ` ☑ todos ${done}/${todos.length}`) + "\n");
|
|
273
|
+
// Print the whole list once; later writes show only the items that moved.
|
|
274
|
+
const shown = firstRender ? todos : todos.filter((todo) => previous.get(todo.content) !== todo.status);
|
|
275
|
+
for (const todo of shown) {
|
|
276
|
+
const mark =
|
|
277
|
+
todo.status === "completed"
|
|
278
|
+
? paint(ansi.green, "✓")
|
|
279
|
+
: todo.status === "in_progress"
|
|
280
|
+
? paint(ansi.yellow, "▸")
|
|
281
|
+
: paint(ansi.dim, "☐");
|
|
282
|
+
const content = todo.status === "completed" ? paint(ansi.dim, todo.content) : todo.content;
|
|
283
|
+
write(` ${mark} ${content}\n`);
|
|
284
|
+
}
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
case "turn/end": {
|
|
288
|
+
flushText();
|
|
289
|
+
endLine();
|
|
290
|
+
const kind = data.reason?.kind;
|
|
291
|
+
lastTurnKind = kind;
|
|
292
|
+
if (kind === "error") {
|
|
293
|
+
write(paint(ansi.red, ` ⚠ turn failed: ${data.reason.error?.message ?? "unknown error"}\n`));
|
|
294
|
+
} else if (kind === "max-tokens") {
|
|
295
|
+
write(paint(ansi.yellow, " ⚠ hit the output-token ceiling\n"));
|
|
296
|
+
} else if (kind === "aborted") {
|
|
297
|
+
write(paint(ansi.yellow, " ⏹ interrupted\n"));
|
|
298
|
+
}
|
|
299
|
+
if (reasoning !== "") lastReasoning = reasoning;
|
|
300
|
+
if (!showReasoning && reasoning.length > 0 && tty && !capture) {
|
|
301
|
+
write(paint(ansi.dim, paint(ansi.italic, ` · thought ${reasoning.length} chars (use /thinking)`)) + "\n");
|
|
302
|
+
}
|
|
303
|
+
if (!capture && tty && (turnUsage.input > 0 || turnUsage.output > 0)) {
|
|
304
|
+
write(paint(ansi.dim, ` · ${formatTokens(turnUsage.input)} in / ${formatTokens(turnUsage.output)} out`) + "\n");
|
|
305
|
+
}
|
|
306
|
+
write("\n");
|
|
307
|
+
streamedThisTurn = false;
|
|
308
|
+
reasoning = "";
|
|
309
|
+
break;
|
|
310
|
+
}
|
|
311
|
+
default:
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
},
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ---------------------------------------------------------------------------
|
|
319
|
+
// Session discovery — mirrors the JSONL persistence layout:
|
|
320
|
+
// $DSH_HOME/sessions/<project-key>/<session-id>/session.v4.jsonl.zstd
|
|
321
|
+
// ---------------------------------------------------------------------------
|
|
322
|
+
function projectKey(cwd) {
|
|
323
|
+
if (cwd.length === 0) throw new Error("cannot encode an empty project path");
|
|
324
|
+
let readable = "";
|
|
325
|
+
let separatorRun = false;
|
|
326
|
+
for (let i = 0; i < cwd.length; i++) {
|
|
327
|
+
const code = cwd.charCodeAt(i);
|
|
328
|
+
const ch = String.fromCharCode(code);
|
|
329
|
+
if (ch === "/" || ch === "\\" || ch === ":") {
|
|
330
|
+
if (!separatorRun) readable += "-";
|
|
331
|
+
separatorRun = true;
|
|
332
|
+
} else if (ch !== "~" && /^[A-Za-z0-9._-]$/.test(ch)) {
|
|
333
|
+
readable += ch;
|
|
334
|
+
separatorRun = false;
|
|
335
|
+
} else {
|
|
336
|
+
readable += "~" + code.toString(16).toUpperCase().padStart(4, "0");
|
|
337
|
+
separatorRun = false;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return `--${(readable.replace(/^-+/, "") || "root").slice(0, 251)}--`;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const ZSTD_MAGIC = Buffer.from([0x28, 0xb5, 0x2f, 0xfd]);
|
|
344
|
+
|
|
345
|
+
/** Last few zstd frames of a session log, decoded to turn-lifecycle records. */
|
|
346
|
+
function readSessionTail(sessionDir) {
|
|
347
|
+
try {
|
|
348
|
+
const file = join(sessionDir, "session.v4.jsonl.zstd");
|
|
349
|
+
const buf = readFileSync(file);
|
|
350
|
+
const positions = [];
|
|
351
|
+
let at = 0;
|
|
352
|
+
while ((at = buf.indexOf(ZSTD_MAGIC, at)) !== -1) {
|
|
353
|
+
positions.push(at);
|
|
354
|
+
at += 4;
|
|
355
|
+
}
|
|
356
|
+
const events = [];
|
|
357
|
+
for (let i = Math.max(0, positions.length - 5); i < positions.length; i++) {
|
|
358
|
+
const start = positions[i];
|
|
359
|
+
const end = i + 1 < positions.length ? positions[i + 1] : buf.length;
|
|
360
|
+
let text;
|
|
361
|
+
try {
|
|
362
|
+
text = zstdDecompressSync(buf.subarray(start, end)).toString("utf8");
|
|
363
|
+
} catch {
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
for (const line of text.split("\n")) {
|
|
367
|
+
if (line === "") continue;
|
|
368
|
+
try {
|
|
369
|
+
const event = JSON.parse(line);
|
|
370
|
+
if (event.type === "turn/start" || event.type === "turn/end" || event.type === "step/start" || event.type === "tool/call") {
|
|
371
|
+
events.push({ type: event.type });
|
|
372
|
+
}
|
|
373
|
+
} catch {}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return events;
|
|
377
|
+
} catch {
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/** Whether a session's log ends inside a turn (live elsewhere, or abandoned). */
|
|
383
|
+
function hasOpenTurn(sessionDir) {
|
|
384
|
+
const events = readSessionTail(sessionDir);
|
|
385
|
+
if (events === null || events.length === 0) return false;
|
|
386
|
+
let open = false;
|
|
387
|
+
for (const event of events) {
|
|
388
|
+
if (event.type === "turn/end") open = false;
|
|
389
|
+
else if (event.type === "turn/start" || event.type === "step/start" || event.type === "tool/call") open = true;
|
|
390
|
+
}
|
|
391
|
+
return open;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function listSessions(cwd = process.cwd()) {
|
|
395
|
+
const dir = join(resolveDshHome(), "sessions", projectKey(cwd));
|
|
396
|
+
let entries;
|
|
397
|
+
try {
|
|
398
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
399
|
+
} catch {
|
|
400
|
+
return [];
|
|
401
|
+
}
|
|
402
|
+
const sessions = [];
|
|
403
|
+
for (const entry of entries) {
|
|
404
|
+
if (!entry.isDirectory() || !entry.name.startsWith("session-")) continue;
|
|
405
|
+
const sessionDir = join(dir, entry.name);
|
|
406
|
+
let newest = 0;
|
|
407
|
+
try {
|
|
408
|
+
for (const file of readdirSync(sessionDir)) {
|
|
409
|
+
const st = statSync(join(sessionDir, file));
|
|
410
|
+
if (st.mtimeMs > newest) newest = st.mtimeMs;
|
|
411
|
+
}
|
|
412
|
+
} catch {
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
if (newest > 0) sessions.push({ id: entry.name, mtime: newest, openTurn: hasOpenTurn(sessionDir) });
|
|
416
|
+
}
|
|
417
|
+
sessions.sort((a, b) => b.mtime - a.mtime);
|
|
418
|
+
return sessions;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function timeAgo(ms) {
|
|
422
|
+
const diff = Date.now() - ms;
|
|
423
|
+
if (diff < 60000) return `${Math.max(1, Math.floor(diff / 1000))}s ago`;
|
|
424
|
+
if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`;
|
|
425
|
+
if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`;
|
|
426
|
+
return `${Math.floor(diff / 86400000)}d ago`;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function printSessionList(sessions, write = out) {
|
|
430
|
+
if (sessions.length === 0) {
|
|
431
|
+
write(paint(ansi.dim, " no sessions in this directory yet\n"));
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
write(` Recent sessions in ${process.cwd()}:\n`);
|
|
435
|
+
sessions.slice(0, 10).forEach((session, index) => {
|
|
436
|
+
const running = session.openTurn ? paint(ansi.yellow, " ▶ running") : "";
|
|
437
|
+
write(` ${paint(ansi.cyan, String(index + 1))}) ${paint(ansi.dim, session.id)} (${paint(ansi.dim, timeAgo(session.mtime))})${running}\n`);
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// ---------------------------------------------------------------------------
|
|
442
|
+
// Help / banner
|
|
443
|
+
// ---------------------------------------------------------------------------
|
|
444
|
+
function printBanner(model) {
|
|
445
|
+
const art = [
|
|
446
|
+
" ooo",
|
|
447
|
+
' " """',
|
|
448
|
+
' " "',
|
|
449
|
+
' ___:____ " "|',
|
|
450
|
+
" ,' \\. \\//",
|
|
451
|
+
" | O \\___/ |",
|
|
452
|
+
"~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~",
|
|
453
|
+
];
|
|
454
|
+
out("\n");
|
|
455
|
+
for (const line of art) out(paint(ansi.blue, line) + "\n");
|
|
456
|
+
out(`${paint(ansi.bold, "deepseek")} ${paint(ansi.dim, VERSION)} — DeepSeek Harness in your terminal\n`);
|
|
457
|
+
out(paint(ansi.dim, `model: ${model.provider}/${model.model}${model.reasoningEffort ? ` · ${model.reasoningEffort}` : ""}`) + "\n");
|
|
458
|
+
out(paint(ansi.dim, `cwd: ${process.cwd()}`) + "\n");
|
|
459
|
+
out(paint(ansi.dim, "type /help for commands, Ctrl+C to interrupt or exit") + "\n\n");
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function printHelp(write = out) {
|
|
463
|
+
write(`deepseek ${VERSION} — DeepSeek Harness in your terminal.
|
|
464
|
+
|
|
465
|
+
Usage:
|
|
466
|
+
deepseek start an interactive session
|
|
467
|
+
deepseek "task text..." answer one task and exit (scriptable)
|
|
468
|
+
deepseek -r, --resume [id] resume a session (newest in this directory by default)
|
|
469
|
+
deepseek -c, --continue continue the newest session without prompting
|
|
470
|
+
deepseek --list list recent sessions in this directory
|
|
471
|
+
deepseek -y, --yes auto-approve actions that would ask
|
|
472
|
+
deepseek --model <id> override the model for this run
|
|
473
|
+
deepseek --reasoning <effort> override the reasoning effort (off|high|max)
|
|
474
|
+
|
|
475
|
+
Interactive commands:
|
|
476
|
+
/help this list (plus the Harness commands below)
|
|
477
|
+
/new start a fresh session
|
|
478
|
+
/sessions list recent sessions in this directory
|
|
479
|
+
/resume <id> switch to another session
|
|
480
|
+
/model [id] show or switch the model (e.g. /model deepseek-v4-pro)
|
|
481
|
+
/reasoning [off|high|max] show or set the reasoning effort
|
|
482
|
+
/permissions [p] show or switch the permission preset (native: /permission)
|
|
483
|
+
/skills list the skills discovered in this workspace
|
|
484
|
+
/jobs list this session's background jobs
|
|
485
|
+
/todos show the current todo list
|
|
486
|
+
/image <path> attach an image to the next message (or mention @photo.png)
|
|
487
|
+
/export [file] write this run's transcript as markdown
|
|
488
|
+
/thinking show the last turn's reasoning
|
|
489
|
+
/session print the current session id (for --resume)
|
|
490
|
+
/clear clear the screen
|
|
491
|
+
/quit, /exit leave (also Ctrl+C at an empty prompt)
|
|
492
|
+
|
|
493
|
+
Harness commands (shared with the Web and Desktop apps):
|
|
494
|
+
/compact, /goal, /plan, and any command a plugin registers.
|
|
495
|
+
Type /help inside a session to see the live list.
|
|
496
|
+
`);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// ---------------------------------------------------------------------------
|
|
500
|
+
// Argument parsing
|
|
501
|
+
// ---------------------------------------------------------------------------
|
|
502
|
+
function parseArgs(argv) {
|
|
503
|
+
const options = {
|
|
504
|
+
mode: "interactive",
|
|
505
|
+
resume: undefined,
|
|
506
|
+
continue: false,
|
|
507
|
+
list: false,
|
|
508
|
+
yes: false,
|
|
509
|
+
model: undefined,
|
|
510
|
+
reasoning: undefined,
|
|
511
|
+
help: false,
|
|
512
|
+
version: false,
|
|
513
|
+
task: undefined,
|
|
514
|
+
};
|
|
515
|
+
const rest = [];
|
|
516
|
+
for (let i = 0; i < argv.length; i++) {
|
|
517
|
+
const arg = argv[i];
|
|
518
|
+
if (arg === "--help" || arg === "-h") options.help = true;
|
|
519
|
+
else if (arg === "--version" || arg === "-v") options.version = true;
|
|
520
|
+
else if (arg === "--list" || arg === "--sessions") options.list = true;
|
|
521
|
+
else if (arg === "--continue" || arg === "-c") options.continue = true;
|
|
522
|
+
else if (arg === "--yes" || arg === "-y") options.yes = true;
|
|
523
|
+
else if (arg === "--model") options.model = argv[++i];
|
|
524
|
+
else if (arg === "--reasoning") options.reasoning = argv[++i];
|
|
525
|
+
else if (arg === "--resume" || arg === "-r") {
|
|
526
|
+
const next = argv[i + 1];
|
|
527
|
+
if (next !== undefined && !next.startsWith("-")) {
|
|
528
|
+
options.resume = next;
|
|
529
|
+
i++;
|
|
530
|
+
} else {
|
|
531
|
+
options.resume = true;
|
|
532
|
+
}
|
|
533
|
+
} else if (arg === "--") {
|
|
534
|
+
rest.push(...argv.slice(i + 1));
|
|
535
|
+
break;
|
|
536
|
+
} else if (arg.startsWith("-")) {
|
|
537
|
+
rest.push(arg);
|
|
538
|
+
} else {
|
|
539
|
+
rest.push(arg);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
if (rest.length > 0) {
|
|
543
|
+
options.mode = "oneshot";
|
|
544
|
+
options.task = rest.join(" ");
|
|
545
|
+
}
|
|
546
|
+
return options;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// ---------------------------------------------------------------------------
|
|
550
|
+
// Core driver
|
|
551
|
+
// ---------------------------------------------------------------------------
|
|
552
|
+
async function run(ctx, options, exit) {
|
|
553
|
+
await ctx.get("loader")?.await();
|
|
554
|
+
const agents = ctx.get("agents");
|
|
555
|
+
const defaultModel = ctx.get("agentDefaultModel");
|
|
556
|
+
const persistence = ctx.get("sessionPersistence");
|
|
557
|
+
if (agents === undefined || defaultModel === undefined || persistence === undefined) {
|
|
558
|
+
throw new Error("cli-runner: agents / agentDefaultModel / sessionPersistence services missing");
|
|
559
|
+
}
|
|
560
|
+
// Optional Harness services: each one adds native parity when the profile
|
|
561
|
+
// composes it, and every use is guarded so a narrower profile still runs.
|
|
562
|
+
const commands = ctx.get("commands");
|
|
563
|
+
const skills = ctx.get("skills");
|
|
564
|
+
const jobs = ctx.get("jobs");
|
|
565
|
+
const planMode = ctx.get("planMode");
|
|
566
|
+
const permissionPresets = ctx.get("permissionPresets");
|
|
567
|
+
const attachments = ctx.get("attachments");
|
|
568
|
+
|
|
569
|
+
if (options.list) {
|
|
570
|
+
printSessionList(listSessions());
|
|
571
|
+
exit(0);
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const selection = { ...defaultModel.currentSelection() };
|
|
576
|
+
if (options.model) selection.model = options.model;
|
|
577
|
+
if (options.reasoning) selection.reasoningEffort = options.reasoning;
|
|
578
|
+
|
|
579
|
+
const interactive = options.mode === "interactive";
|
|
580
|
+
const autoYes = options.yes || process.env.DSH_CLI_YES === "1";
|
|
581
|
+
|
|
582
|
+
let handle = null;
|
|
583
|
+
let busy = false;
|
|
584
|
+
let quitting = false;
|
|
585
|
+
let cancelSent = false;
|
|
586
|
+
let selectionRef = { current: { ...selection }, assembled: undefined };
|
|
587
|
+
let currentAsk = null;
|
|
588
|
+
let pendingLine = null;
|
|
589
|
+
const lineQueue = [];
|
|
590
|
+
let paused = false;
|
|
591
|
+
let lastInterrupt = 0;
|
|
592
|
+
let ready = false;
|
|
593
|
+
let stdinClosed = false;
|
|
594
|
+
|
|
595
|
+
const write = (text) => process.stdout.write(text);
|
|
596
|
+
|
|
597
|
+
// ---- line input ---------------------------------------------------------
|
|
598
|
+
// One-shot mode must not own stdin: a piped/closed stdin would deliver EOF
|
|
599
|
+
// and tear the runtime down before the task runs.
|
|
600
|
+
const rlInterface = interactive
|
|
601
|
+
? createInterface({
|
|
602
|
+
input: process.stdin,
|
|
603
|
+
output: process.stdout,
|
|
604
|
+
terminal: process.stdin.isTTY,
|
|
605
|
+
historySize: 200,
|
|
606
|
+
removeHistoryDuplicates: true,
|
|
607
|
+
})
|
|
608
|
+
: null;
|
|
609
|
+
// Hold the input until the first agent is ready: buffered piped lines must
|
|
610
|
+
// not be consumed (and dropped) while the profile is still starting.
|
|
611
|
+
if (rlInterface) {
|
|
612
|
+
try {
|
|
613
|
+
rlInterface.pause();
|
|
614
|
+
paused = true;
|
|
615
|
+
} catch {}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function setPaused(value) {
|
|
619
|
+
if (!rlInterface || paused === value) return;
|
|
620
|
+
paused = value;
|
|
621
|
+
try {
|
|
622
|
+
if (value) rlInterface.pause();
|
|
623
|
+
else rlInterface.resume();
|
|
624
|
+
} catch {}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
rlInterface?.on("line", (line) => {
|
|
628
|
+
if (pendingLine) {
|
|
629
|
+
const deliver = pendingLine;
|
|
630
|
+
pendingLine = null;
|
|
631
|
+
deliver(line);
|
|
632
|
+
} else {
|
|
633
|
+
lineQueue.push(line);
|
|
634
|
+
}
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Ask one line. Resolves null when the question is interrupted (Ctrl+C) or
|
|
639
|
+
* stdin closes. A permanent listener queues ahead-of-time lines (piped input,
|
|
640
|
+
* or typing while the model works), so no line is ever dropped.
|
|
641
|
+
* @param {string} prompt
|
|
642
|
+
* @returns {Promise<string | null>}
|
|
643
|
+
*/
|
|
644
|
+
function ask(prompt) {
|
|
645
|
+
return new Promise((resolve) => {
|
|
646
|
+
if (!rlInterface) {
|
|
647
|
+
resolve(null);
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
if (lineQueue.length > 0) {
|
|
651
|
+
resolve(lineQueue.shift());
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
if (stdinClosed) {
|
|
655
|
+
resolve(null);
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
setPaused(false);
|
|
659
|
+
rlInterface.setPrompt(prompt);
|
|
660
|
+
rlInterface.prompt();
|
|
661
|
+
pendingLine = (value) => {
|
|
662
|
+
currentAsk = null;
|
|
663
|
+
if (busy) setPaused(true);
|
|
664
|
+
resolve(value);
|
|
665
|
+
};
|
|
666
|
+
currentAsk = {
|
|
667
|
+
done: (value) => {
|
|
668
|
+
const answer = pendingLine;
|
|
669
|
+
pendingLine = null;
|
|
670
|
+
answer?.(value);
|
|
671
|
+
},
|
|
672
|
+
};
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function closeInput() {
|
|
677
|
+
try {
|
|
678
|
+
rlInterface?.close();
|
|
679
|
+
} catch {}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const shutdown = async (code) => {
|
|
683
|
+
if (quitting) return;
|
|
684
|
+
quitting = true;
|
|
685
|
+
closeInput();
|
|
686
|
+
if (handle) {
|
|
687
|
+
try {
|
|
688
|
+
await Promise.race([persistence.flush(), sleep(2000)]);
|
|
689
|
+
} catch {}
|
|
690
|
+
try {
|
|
691
|
+
await Promise.race([handle.dispose(), sleep(3000)]);
|
|
692
|
+
} catch {}
|
|
693
|
+
handle = null;
|
|
694
|
+
}
|
|
695
|
+
exit(code);
|
|
696
|
+
};
|
|
697
|
+
|
|
698
|
+
rlInterface?.on("SIGINT", handleInterrupt);
|
|
699
|
+
process.on("SIGINT", handleInterrupt);
|
|
700
|
+
rlInterface?.on("close", () => {
|
|
701
|
+
stdinClosed = true;
|
|
702
|
+
if (currentAsk) {
|
|
703
|
+
const done = currentAsk.done;
|
|
704
|
+
done(lineQueue.length > 0 ? lineQueue.shift() : null);
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
if (quitting || busy) return;
|
|
708
|
+
if (lineQueue.length > 0) return; // the loop drains the buffered lines
|
|
709
|
+
if (ready) shutdown(0);
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
/** One Ctrl+C path shared by readline and the process signal. */
|
|
713
|
+
function handleInterrupt() {
|
|
714
|
+
const now = Date.now();
|
|
715
|
+
if (now - lastInterrupt < 150) return; // one physical keypress, one action
|
|
716
|
+
lastInterrupt = now;
|
|
717
|
+
if (currentAsk) {
|
|
718
|
+
const done = currentAsk.done;
|
|
719
|
+
process.stdout.write("\n");
|
|
720
|
+
done(null);
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
if (busy) {
|
|
724
|
+
if (!cancelSent) {
|
|
725
|
+
cancelSent = true;
|
|
726
|
+
out(paint(ansi.yellow, "\n ⏹ cancelling…\n"));
|
|
727
|
+
try {
|
|
728
|
+
handle?.agent.cancel({ kind: "user" });
|
|
729
|
+
} catch {}
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
shutdown(130);
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
shutdown(0);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// ---- terminal questions for tools (plan mode, ask_user_question) ---------
|
|
739
|
+
// Answered per agent through the same `user-questions/request` waterfall the
|
|
740
|
+
// Web client uses, so plan review and ask_user_question behave identically.
|
|
741
|
+
const answerQuestions = async (request) => {
|
|
742
|
+
const answers = [];
|
|
743
|
+
const questions = request?.questions ?? [];
|
|
744
|
+
for (const question of questions) {
|
|
745
|
+
out("\n");
|
|
746
|
+
if (question.header) out(paint(ansi.bold, ` ${question.header}`) + "\n");
|
|
747
|
+
out(` ${question.question}\n`);
|
|
748
|
+
if (question.detail) out(paint(ansi.dim, question.detail.replace(/^/gm, " | ")) + "\n");
|
|
749
|
+
const options = question.options ?? [];
|
|
750
|
+
options.forEach((option, index) => {
|
|
751
|
+
out(` ${paint(ansi.cyan, String(index + 1))}) ${option.label}${option.description ? paint(ansi.dim, ` — ${option.description}`) : ""}\n`);
|
|
752
|
+
});
|
|
753
|
+
if (!interactive) {
|
|
754
|
+
answers.push({ id: question.id, selected: options[0] ? [options[0].label] : [], custom: options[0] ? undefined : "one-shot mode: no interactive answer" });
|
|
755
|
+
continue;
|
|
756
|
+
}
|
|
757
|
+
const answer = await ask(paint(ansi.green, options.length > 0 ? " choose: " : " answer: "));
|
|
758
|
+
const text = (answer ?? "").trim();
|
|
759
|
+
const index = Number.parseInt(text, 10) - 1;
|
|
760
|
+
if (options.length > 0 && index >= 0 && index < options.length) answers.push({ id: question.id, selected: [options[index].label] });
|
|
761
|
+
else answers.push({ id: question.id, selected: [], custom: text });
|
|
762
|
+
}
|
|
763
|
+
return { answers };
|
|
764
|
+
};
|
|
765
|
+
|
|
766
|
+
// ---- agent lifecycle ----------------------------------------------------
|
|
767
|
+
const makeAgentOptions = () => ({ ...selectionRef.current });
|
|
768
|
+
|
|
769
|
+
const wire = (agent) => {
|
|
770
|
+
agent.ctx.on("session/event", (session, event) => {
|
|
771
|
+
if (session === agent.session) renderer.event(event);
|
|
772
|
+
});
|
|
773
|
+
agent.ctx.on("agent/assistant-stream", (payload) => {
|
|
774
|
+
if (payload.agent === agent) renderer.frame(payload.frame);
|
|
775
|
+
});
|
|
776
|
+
agent.ctx.on("user-questions/request", async (request) => answerQuestions(request));
|
|
777
|
+
agent.ctx.on("approval/request", async (request) => {
|
|
778
|
+
const toolName = request.toolName ?? "tool";
|
|
779
|
+
if (autoYes) return "allowed-once";
|
|
780
|
+
if (request.reason) {
|
|
781
|
+
out(paint(ansi.dim, paint(ansi.italic, ` ⚠ ${toolName} needs approval: ${request.reason}`)) + "\n");
|
|
782
|
+
}
|
|
783
|
+
if (!interactive || !process.stdin.isTTY) return "rejected";
|
|
784
|
+
const answer = await ask(paint(ansi.yellow, ` ⚠ Allow ${paint(ansi.cyan, toolName)}? `) + paint(ansi.dim, "[y/N] "));
|
|
785
|
+
if (answer === null) return "rejected";
|
|
786
|
+
return /^y(es)?$/i.test(answer.trim()) ? "allowed-once" : "rejected";
|
|
787
|
+
});
|
|
788
|
+
};
|
|
789
|
+
|
|
790
|
+
const startAgent = async (sessionId, resumeId) => {
|
|
791
|
+
if (handle) {
|
|
792
|
+
try {
|
|
793
|
+
await Promise.race([handle.dispose(), sleep(3000)]);
|
|
794
|
+
} catch {}
|
|
795
|
+
handle = null;
|
|
796
|
+
}
|
|
797
|
+
selectionRef = { current: { ...selectionRef.current }, assembled: undefined };
|
|
798
|
+
const setup = (agentCtx) => {
|
|
799
|
+
installModelSelection(agentCtx, selectionRef);
|
|
800
|
+
};
|
|
801
|
+
const created = resumeId
|
|
802
|
+
? await agents.resume({ resumeSessionId: SessionId(resumeId), agentOptions: makeAgentOptions(), setup })
|
|
803
|
+
: await agents.create({
|
|
804
|
+
sessionId,
|
|
805
|
+
meta: { cwd: process.cwd() },
|
|
806
|
+
agentOptions: makeAgentOptions(),
|
|
807
|
+
setup,
|
|
808
|
+
});
|
|
809
|
+
handle = created;
|
|
810
|
+
wire(created.agent);
|
|
811
|
+
await created.agent.whenIdle();
|
|
812
|
+
return created.agent;
|
|
813
|
+
};
|
|
814
|
+
|
|
815
|
+
const runTurn = async (agent, text) => {
|
|
816
|
+
const content = await buildUserContent(text);
|
|
817
|
+
transcript.push({ role: "user", text });
|
|
818
|
+
const previousAnswer = renderer.lastAssistantText;
|
|
819
|
+
busy = true;
|
|
820
|
+
cancelSent = false;
|
|
821
|
+
renderer.resetTurn();
|
|
822
|
+
setPaused(true);
|
|
823
|
+
try {
|
|
824
|
+
agent.followup(createUserMessage({ content, source: { kind: "user" } }));
|
|
825
|
+
await agent.whenIdle();
|
|
826
|
+
await persistence.flush();
|
|
827
|
+
} finally {
|
|
828
|
+
busy = false;
|
|
829
|
+
setPaused(false);
|
|
830
|
+
if (renderer.lastAssistantText && renderer.lastAssistantText !== previousAnswer) {
|
|
831
|
+
transcript.push({ role: "assistant", text: renderer.lastAssistantText });
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
};
|
|
835
|
+
|
|
836
|
+
const renderer = createRenderer({
|
|
837
|
+
write: (text) => {
|
|
838
|
+
if (options.mode !== "oneshot") write(text);
|
|
839
|
+
},
|
|
840
|
+
capture: options.mode === "oneshot",
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
const modelDesc = () => {
|
|
844
|
+
const current = selectionRef.current;
|
|
845
|
+
return `${current.provider}/${current.model}${current.reasoningEffort ? ` · ${current.reasoningEffort}` : ""}`;
|
|
846
|
+
};
|
|
847
|
+
|
|
848
|
+
// ---- parity helpers -----------------------------------------------------
|
|
849
|
+
const transcript = [];
|
|
850
|
+
const pendingImages = [];
|
|
851
|
+
const IMAGE_TYPES = { png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", webp: "image/webp", gif: "image/gif" };
|
|
852
|
+
|
|
853
|
+
/** Image paths mentioned with `@…` in a message. */
|
|
854
|
+
function mentionedImages(text) {
|
|
855
|
+
const paths = [];
|
|
856
|
+
for (const match of text.matchAll(/(?:^|\s)@([^\s@]+)/g)) {
|
|
857
|
+
const extension = match[1].split(".").pop()?.toLowerCase() ?? "";
|
|
858
|
+
if (!(extension in IMAGE_TYPES)) continue;
|
|
859
|
+
const target = resolvePath(process.cwd(), match[1]);
|
|
860
|
+
if (existsSync(target)) paths.push(target);
|
|
861
|
+
}
|
|
862
|
+
return paths;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/** Build the user message: text (with @file inlining) plus attached images. */
|
|
866
|
+
async function buildUserContent(text) {
|
|
867
|
+
const expanded = expandMentions(text);
|
|
868
|
+
const images = [...pendingImages, ...mentionedImages(text)];
|
|
869
|
+
pendingImages.length = 0;
|
|
870
|
+
const blocks = [{ type: "text", text: expanded }];
|
|
871
|
+
for (const imagePath of [...new Set(images)]) {
|
|
872
|
+
if (attachments === undefined) {
|
|
873
|
+
out(paint(ansi.yellow, " ⚠ this profile has no attachment store; image ignored\n"));
|
|
874
|
+
continue;
|
|
875
|
+
}
|
|
876
|
+
try {
|
|
877
|
+
const extension = imagePath.split(".").pop()?.toLowerCase() ?? "";
|
|
878
|
+
const refs = await attachments.saveImages([
|
|
879
|
+
{ data: readFileSync(imagePath), mediaType: IMAGE_TYPES[extension], name: basename(imagePath) },
|
|
880
|
+
]);
|
|
881
|
+
if (refs[0] !== undefined) blocks.push({ type: "image", attachment: refs[0] });
|
|
882
|
+
} catch (error) {
|
|
883
|
+
out(paint(ansi.yellow, ` ⚠ cannot attach ${imagePath}: ${describeError(error)}`) + "\n");
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
if (blocks.length > 1) out(paint(ansi.dim, ` ▣ ${blocks.length - 1} image(s) attached`) + "\n");
|
|
887
|
+
return blocks;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
/** Run one native Harness slash command; false when no such command exists. */
|
|
891
|
+
async function runNativeCommand(agent, line) {
|
|
892
|
+
if (commands === undefined) return false;
|
|
893
|
+
const controller = new AbortController();
|
|
894
|
+
try {
|
|
895
|
+
const execution = await commands.execute(agent, line, [], controller.signal);
|
|
896
|
+
if (execution === undefined) return false;
|
|
897
|
+
const result = execution.result;
|
|
898
|
+
if (result.kind === "error") out(paint(ansi.red, ` ✘ ${result.text ?? "command failed"}`) + "\n");
|
|
899
|
+
else if (result.text) out(paint(ansi.dim, ` ${result.text}`) + "\n");
|
|
900
|
+
else out(paint(ansi.dim, " ✓ done") + "\n");
|
|
901
|
+
return true;
|
|
902
|
+
} catch (error) {
|
|
903
|
+
out(paint(ansi.red, ` ✘ ${describeError(error)}`) + "\n");
|
|
904
|
+
return true;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
/** List the Harness commands this profile registers (/compact, /goal, …). */
|
|
909
|
+
function printNativeCommands(agent) {
|
|
910
|
+
if (commands === undefined) return;
|
|
911
|
+
let list = [];
|
|
912
|
+
try {
|
|
913
|
+
list = commands.list(agent);
|
|
914
|
+
} catch {}
|
|
915
|
+
if (list.length === 0) return;
|
|
916
|
+
out(paint(ansi.bold, "Harness commands:") + "\n");
|
|
917
|
+
for (const entry of list) out(` /${entry.name.padEnd(12)} ${paint(ansi.dim, entry.description)}\n`);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/**
|
|
921
|
+
* Expand `@path` mentions into attached file text, so the model can read a
|
|
922
|
+
* file the same way a Web client attaches one.
|
|
923
|
+
*/
|
|
924
|
+
function expandMentions(text) {
|
|
925
|
+
const mentions = [...new Set([...text.matchAll(/(?:^|\s)@([^\s@]+)/g)].map((match) => match[1]))];
|
|
926
|
+
if (mentions.length === 0) return text;
|
|
927
|
+
let extra = "";
|
|
928
|
+
for (const mention of mentions) {
|
|
929
|
+
try {
|
|
930
|
+
const target = resolvePath(process.cwd(), mention);
|
|
931
|
+
if (!existsSync(target)) continue;
|
|
932
|
+
const stat = statSync(target);
|
|
933
|
+
if (!stat.isFile()) continue;
|
|
934
|
+
if (stat.size > 200_000) {
|
|
935
|
+
extra += `\n\n--- attached: ${mention} (skipped, ${stat.size} bytes > 200 KB) ---\n`;
|
|
936
|
+
continue;
|
|
937
|
+
}
|
|
938
|
+
if (/\.(png|jpe?g|webp|gif)$/i.test(target)) continue; // handled as an image attachment
|
|
939
|
+
if (/\.(pdf|zip|exe|dll|bin)$/i.test(target)) {
|
|
940
|
+
extra += `\n\n--- attached: ${mention} (binary, not inlined) ---\n`;
|
|
941
|
+
continue;
|
|
942
|
+
}
|
|
943
|
+
const body = readFileSync(target, "utf8");
|
|
944
|
+
extra += `\n\n--- attached file: ${mention} ---\n${body}\n--- end ${mention} ---\n`;
|
|
945
|
+
} catch {}
|
|
946
|
+
}
|
|
947
|
+
return extra === "" ? text : text + extra;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
/** Write the current run's transcript as markdown. */
|
|
951
|
+
function exportTranscript(agent, target) {
|
|
952
|
+
const lines = [
|
|
953
|
+
`# DeepSeek Harness session ${agent.session.id}`,
|
|
954
|
+
"",
|
|
955
|
+
`- model: ${modelDesc()}`,
|
|
956
|
+
`- cwd: ${process.cwd()}`,
|
|
957
|
+
`- exported: ${new Date().toISOString()}`,
|
|
958
|
+
"",
|
|
959
|
+
];
|
|
960
|
+
if (transcript.length === 0) lines.push("_(no turns in this run)_", "");
|
|
961
|
+
for (const entry of transcript) {
|
|
962
|
+
lines.push(entry.role === "user" ? "## You" : "## Assistant", "", entry.text, "");
|
|
963
|
+
}
|
|
964
|
+
const file = target === "" ? `deepseek-${String(agent.session.id).replace(/^session-/, "").slice(0, 8)}.md` : target;
|
|
965
|
+
writeFileSync(file, lines.join("\n"), "utf8");
|
|
966
|
+
return file;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// ---- subagent progress --------------------------------------------------
|
|
970
|
+
// Root-scoped listeners: child agents created by this session are reported as
|
|
971
|
+
// one-line progress notes, like the Web UI's subagent cards.
|
|
972
|
+
ctx.on("agent/created", (payload) => {
|
|
973
|
+
try {
|
|
974
|
+
if (handle !== null && agents.isOwnedBy(payload.agent.id, handle.agent)) {
|
|
975
|
+
out(paint(ansi.dim, paint(ansi.italic, ` ↳ subagent ${String(payload.agent.id).slice(-8)} started`)) + "\n");
|
|
976
|
+
}
|
|
977
|
+
} catch {}
|
|
978
|
+
});
|
|
979
|
+
ctx.on("agent/disposed", (payload) => {
|
|
980
|
+
try {
|
|
981
|
+
if (handle !== null && agents.isOwnedBy(payload.agent.id, handle.agent)) {
|
|
982
|
+
out(paint(ansi.dim, paint(ansi.italic, ` ↳ subagent ${String(payload.agent.id).slice(-8)} finished`)) + "\n");
|
|
983
|
+
}
|
|
984
|
+
} catch {}
|
|
985
|
+
});
|
|
986
|
+
|
|
987
|
+
// ---- resolve a session to resume, if any --------------------------------
|
|
988
|
+
let resumeTarget = options.resume === true ? true : typeof options.resume === "string" ? options.resume : undefined;
|
|
989
|
+
if (options.continue) resumeTarget = true;
|
|
990
|
+
|
|
991
|
+
if (resumeTarget === true) {
|
|
992
|
+
const sessions = listSessions();
|
|
993
|
+
const candidate = sessions.find((session) => !session.openTurn);
|
|
994
|
+
if (candidate === undefined) {
|
|
995
|
+
out(paint(ansi.yellow, " no resumable sessions in this directory; starting fresh\n"));
|
|
996
|
+
resumeTarget = undefined;
|
|
997
|
+
} else {
|
|
998
|
+
resumeTarget = candidate.id;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
if (interactive && resumeTarget === undefined && process.stdin.isTTY) {
|
|
1003
|
+
const candidate = listSessions().find((session) => !session.openTurn);
|
|
1004
|
+
if (candidate !== undefined) {
|
|
1005
|
+
const answer = await ask(
|
|
1006
|
+
paint(ansi.yellow, ` Resume previous session (${candidate.id}, ${timeAgo(candidate.mtime)})? `) + paint(ansi.dim, "[y/N] ")
|
|
1007
|
+
);
|
|
1008
|
+
if (answer !== null && /^y(es)?$/i.test(answer.trim())) resumeTarget = candidate.id;
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// ---- one-shot -----------------------------------------------------------
|
|
1013
|
+
if (options.mode === "oneshot") {
|
|
1014
|
+
const task = options.task?.trim();
|
|
1015
|
+
if (!task) {
|
|
1016
|
+
err("deepseek: no task given\n");
|
|
1017
|
+
await shutdown(1);
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
let agent;
|
|
1021
|
+
try {
|
|
1022
|
+
agent = resumeTarget ? await startAgent(undefined, resumeTarget) : await startAgent(SessionId(`session-${randomUUID()}`), undefined);
|
|
1023
|
+
} catch (error) {
|
|
1024
|
+
err(`deepseek: ${describeError(error)}\n`);
|
|
1025
|
+
await shutdown(1);
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
await runTurn(agent, task);
|
|
1029
|
+
const answer = renderer.lastAssistantText;
|
|
1030
|
+
if (answer !== "") process.stdout.write(answer.endsWith("\n") ? answer : answer + "\n");
|
|
1031
|
+
await shutdown(renderer.lastTurnKind === "completed" ? 0 : 1);
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// ---- interactive --------------------------------------------------------
|
|
1036
|
+
let agent;
|
|
1037
|
+
try {
|
|
1038
|
+
agent = resumeTarget ? await startAgent(undefined, resumeTarget) : await startAgent(SessionId(`session-${randomUUID()}`), undefined);
|
|
1039
|
+
} catch (error) {
|
|
1040
|
+
err(`deepseek: cannot start a session: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
1041
|
+
await shutdown(1);
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
ready = true;
|
|
1046
|
+
if (stdinClosed) {
|
|
1047
|
+
await shutdown(0);
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
printBanner(selectionRef.current);
|
|
1052
|
+
if (resumeTarget) {
|
|
1053
|
+
out(paint(ansi.green, `▶ resumed `) + paint(ansi.dim, resumeTarget) + "\n");
|
|
1054
|
+
out(paint(ansi.dim, " (history is in the session log; continue the conversation below)") + "\n\n");
|
|
1055
|
+
} else {
|
|
1056
|
+
out(paint(ansi.dim, `session: ${agent.session.id} (resume with: deepseek --resume ${agent.session.id})`) + "\n\n");
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
while (!quitting) {
|
|
1060
|
+
const line = await ask(paint(ansi.green, "❯ "));
|
|
1061
|
+
if (line === null) break;
|
|
1062
|
+
const text = line.trim();
|
|
1063
|
+
if (text === "") continue;
|
|
1064
|
+
|
|
1065
|
+
if (text.startsWith("/")) {
|
|
1066
|
+
const [command, ...rest] = text.split(/\s+/);
|
|
1067
|
+
const argument = rest.join(" ");
|
|
1068
|
+
switch (command) {
|
|
1069
|
+
case "/help":
|
|
1070
|
+
printHelp(write);
|
|
1071
|
+
printNativeCommands(agent);
|
|
1072
|
+
continue;
|
|
1073
|
+
case "/quit":
|
|
1074
|
+
case "/exit":
|
|
1075
|
+
await shutdown(0);
|
|
1076
|
+
return;
|
|
1077
|
+
case "/new": {
|
|
1078
|
+
out(paint(ansi.dim, " ↻ new session\n"));
|
|
1079
|
+
try {
|
|
1080
|
+
agent = await startAgent(SessionId(`session-${randomUUID()}`), undefined);
|
|
1081
|
+
out(paint(ansi.dim, ` session: ${agent.session.id}\n`));
|
|
1082
|
+
} catch (error) {
|
|
1083
|
+
out(paint(ansi.red, ` ✘ ${error instanceof Error ? error.message : String(error)}\n`));
|
|
1084
|
+
}
|
|
1085
|
+
continue;
|
|
1086
|
+
}
|
|
1087
|
+
case "/sessions":
|
|
1088
|
+
printSessionList(listSessions(), write);
|
|
1089
|
+
continue;
|
|
1090
|
+
case "/resume": {
|
|
1091
|
+
if (argument === "") {
|
|
1092
|
+
out(paint(ansi.yellow, " usage: /resume <session-id> (see /sessions)\n"));
|
|
1093
|
+
continue;
|
|
1094
|
+
}
|
|
1095
|
+
try {
|
|
1096
|
+
out(paint(ansi.dim, ` ↻ resuming ${argument}\n`));
|
|
1097
|
+
agent = await startAgent(undefined, argument);
|
|
1098
|
+
} catch (error) {
|
|
1099
|
+
out(paint(ansi.red, ` ✘ ${error instanceof Error ? error.message : String(error)}\n`));
|
|
1100
|
+
}
|
|
1101
|
+
continue;
|
|
1102
|
+
}
|
|
1103
|
+
case "/session":
|
|
1104
|
+
out(paint(ansi.dim, ` ${agent.session.id}\n`));
|
|
1105
|
+
continue;
|
|
1106
|
+
case "/model": {
|
|
1107
|
+
if (argument === "") {
|
|
1108
|
+
out(paint(ansi.dim, ` model: ${modelDesc()}\n`));
|
|
1109
|
+
continue;
|
|
1110
|
+
}
|
|
1111
|
+
const next = { ...selectionRef.current, model: argument };
|
|
1112
|
+
try {
|
|
1113
|
+
await defaultModel.saveSelection(next);
|
|
1114
|
+
selectionRef.current = next;
|
|
1115
|
+
out(paint(ansi.green, ` ✓ model set: ${next.provider}/${next.model} (next message)\n`));
|
|
1116
|
+
} catch (error) {
|
|
1117
|
+
out(paint(ansi.red, ` ✘ ${error instanceof Error ? error.message : String(error)}\n`));
|
|
1118
|
+
}
|
|
1119
|
+
continue;
|
|
1120
|
+
}
|
|
1121
|
+
case "/reasoning": {
|
|
1122
|
+
if (argument === "") {
|
|
1123
|
+
out(paint(ansi.dim, ` reasoning: ${selectionRef.current.reasoningEffort ?? "default"}\n`));
|
|
1124
|
+
continue;
|
|
1125
|
+
}
|
|
1126
|
+
const next = { ...selectionRef.current, reasoningEffort: argument === "default" ? undefined : argument };
|
|
1127
|
+
try {
|
|
1128
|
+
await defaultModel.saveSelection(next);
|
|
1129
|
+
selectionRef.current = next;
|
|
1130
|
+
out(paint(ansi.green, ` ✓ reasoning set: ${argument} (next message)\n`));
|
|
1131
|
+
} catch (error) {
|
|
1132
|
+
out(paint(ansi.red, ` ✘ ${error instanceof Error ? error.message : String(error)}\n`));
|
|
1133
|
+
}
|
|
1134
|
+
continue;
|
|
1135
|
+
}
|
|
1136
|
+
case "/thinking": {
|
|
1137
|
+
const reasoning = renderer.lastReasoning;
|
|
1138
|
+
if (reasoning === "") out(paint(ansi.dim, " no reasoning recorded yet\n"));
|
|
1139
|
+
else out(paint(ansi.dim, paint(ansi.italic, reasoning)) + "\n");
|
|
1140
|
+
continue;
|
|
1141
|
+
}
|
|
1142
|
+
case "/image": {
|
|
1143
|
+
if (argument === "") {
|
|
1144
|
+
out(paint(ansi.yellow, " usage: /image <path> (or mention @photo.png in a message)\n"));
|
|
1145
|
+
continue;
|
|
1146
|
+
}
|
|
1147
|
+
const target = resolvePath(process.cwd(), argument);
|
|
1148
|
+
if (!existsSync(target)) {
|
|
1149
|
+
out(paint(ansi.red, ` ✘ no such file: ${argument}\n`));
|
|
1150
|
+
continue;
|
|
1151
|
+
}
|
|
1152
|
+
pendingImages.push(target);
|
|
1153
|
+
out(paint(ansi.green, ` ▣ attached ${basename(target)} to the next message\n`));
|
|
1154
|
+
continue;
|
|
1155
|
+
}
|
|
1156
|
+
case "/todos": {
|
|
1157
|
+
const todos = renderer.lastTodos;
|
|
1158
|
+
if (todos.length === 0) {
|
|
1159
|
+
out(paint(ansi.dim, " no todo list in this session yet\n"));
|
|
1160
|
+
continue;
|
|
1161
|
+
}
|
|
1162
|
+
const done = todos.filter((todo) => todo.status === "completed").length;
|
|
1163
|
+
out(paint(ansi.dim, ` ☑ todos ${done}/${todos.length}`) + "\n");
|
|
1164
|
+
for (const todo of todos) {
|
|
1165
|
+
const mark = todo.status === "completed" ? "✓" : todo.status === "in_progress" ? "▸" : "☐";
|
|
1166
|
+
out(` ${mark} ${todo.content}\n`);
|
|
1167
|
+
}
|
|
1168
|
+
continue;
|
|
1169
|
+
}
|
|
1170
|
+
case "/skills": {
|
|
1171
|
+
if (skills === undefined) {
|
|
1172
|
+
out(paint(ansi.yellow, " this profile does not compose the skill registry\n"));
|
|
1173
|
+
continue;
|
|
1174
|
+
}
|
|
1175
|
+
try {
|
|
1176
|
+
const list = await skills.list();
|
|
1177
|
+
if (list.length === 0) out(paint(ansi.dim, " no skills discovered in this workspace\n"));
|
|
1178
|
+
else {
|
|
1179
|
+
out(paint(ansi.bold, "Skills:") + "\n");
|
|
1180
|
+
for (const skill of list) out(` ${paint(ansi.cyan, skill.name)} — ${paint(ansi.dim, skill.description)}\n`);
|
|
1181
|
+
}
|
|
1182
|
+
} catch (error) {
|
|
1183
|
+
out(paint(ansi.red, ` ✘ ${describeError(error)}\n`));
|
|
1184
|
+
}
|
|
1185
|
+
continue;
|
|
1186
|
+
}
|
|
1187
|
+
case "/jobs": {
|
|
1188
|
+
if (jobs === undefined) {
|
|
1189
|
+
out(paint(ansi.yellow, " this profile does not compose the job registry\n"));
|
|
1190
|
+
continue;
|
|
1191
|
+
}
|
|
1192
|
+
let list = [];
|
|
1193
|
+
try {
|
|
1194
|
+
list = jobs.list(agent.session.id);
|
|
1195
|
+
} catch (error) {
|
|
1196
|
+
out(paint(ansi.red, ` ✘ ${describeError(error)}\n`));
|
|
1197
|
+
continue;
|
|
1198
|
+
}
|
|
1199
|
+
if (list.length === 0) out(paint(ansi.dim, " no background jobs\n"));
|
|
1200
|
+
else {
|
|
1201
|
+
out(paint(ansi.bold, "Background jobs:") + "\n");
|
|
1202
|
+
for (const job of list) {
|
|
1203
|
+
const state = job.status === "running" ? paint(ansi.yellow, job.status) : paint(ansi.dim, job.status);
|
|
1204
|
+
out(` ${job.id} ${state} ${job.label}${job.progress ? paint(ansi.dim, ` · ${job.progress}`) : ""}\n`);
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
continue;
|
|
1208
|
+
}
|
|
1209
|
+
case "/permissions": {
|
|
1210
|
+
if (permissionPresets === undefined) {
|
|
1211
|
+
out(paint(ansi.yellow, " this profile does not compose permission presets\n"));
|
|
1212
|
+
continue;
|
|
1213
|
+
}
|
|
1214
|
+
try {
|
|
1215
|
+
const catalog = permissionPresets.catalog();
|
|
1216
|
+
if (argument === "") {
|
|
1217
|
+
out(paint(ansi.bold, "Permission presets:") + "\n");
|
|
1218
|
+
for (const option of catalog.options) {
|
|
1219
|
+
out(` ${paint(ansi.cyan, option.value.padEnd(18))} ${option.name}${option.description ? paint(ansi.dim, ` — ${option.description}`) : ""}\n`);
|
|
1220
|
+
}
|
|
1221
|
+
out(paint(ansi.dim, ` default: ${catalog.defaultPreset}`) + "\n");
|
|
1222
|
+
} else {
|
|
1223
|
+
permissionPresets.set(agent.session, argument);
|
|
1224
|
+
out(paint(ansi.green, ` ✓ permissions: ${argument} (applies to the next tool call)\n`));
|
|
1225
|
+
}
|
|
1226
|
+
} catch (error) {
|
|
1227
|
+
out(paint(ansi.red, ` ✘ ${describeError(error)}\n`));
|
|
1228
|
+
}
|
|
1229
|
+
continue;
|
|
1230
|
+
}
|
|
1231
|
+
case "/export": {
|
|
1232
|
+
try {
|
|
1233
|
+
const file = exportTranscript(agent, argument);
|
|
1234
|
+
out(paint(ansi.green, ` ✓ exported ${transcript.length} message(s) to ${file}\n`));
|
|
1235
|
+
} catch (error) {
|
|
1236
|
+
out(paint(ansi.red, ` ✘ ${describeError(error)}\n`));
|
|
1237
|
+
}
|
|
1238
|
+
continue;
|
|
1239
|
+
}
|
|
1240
|
+
case "/clear":
|
|
1241
|
+
out("\x1b[2J\x1b[3J\x1b[H");
|
|
1242
|
+
continue;
|
|
1243
|
+
default: {
|
|
1244
|
+
// Anything the Harness itself registers (/compact, /goal, /plan, …)
|
|
1245
|
+
// runs through the shared command registry, so CLI and Web stay in sync.
|
|
1246
|
+
if (await runNativeCommand(agent, text)) continue;
|
|
1247
|
+
if (planMode !== undefined && command === "/plan") {
|
|
1248
|
+
try {
|
|
1249
|
+
const current = planMode.get(agent);
|
|
1250
|
+
const next = argument === "off" ? false : argument === "" ? !current.active : true;
|
|
1251
|
+
const outcome = planMode.set(agent, next);
|
|
1252
|
+
out(paint(ansi.green, ` ✓ plan mode ${next ? "on" : "off"} (${outcome})\n`));
|
|
1253
|
+
} catch (error) {
|
|
1254
|
+
out(paint(ansi.red, ` ✘ ${describeError(error)}\n`));
|
|
1255
|
+
}
|
|
1256
|
+
continue;
|
|
1257
|
+
}
|
|
1258
|
+
out(paint(ansi.yellow, ` unknown command ${command} — try /help\n`));
|
|
1259
|
+
continue;
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
out("\n" + paint(ansi.green, "❯ ") + text + "\n\n");
|
|
1265
|
+
await runTurn(agent, text);
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
await shutdown(0);
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
// ---------------------------------------------------------------------------
|
|
1272
|
+
// Cordis plugin entry
|
|
1273
|
+
// ---------------------------------------------------------------------------
|
|
1274
|
+
export function apply(ctx) {
|
|
1275
|
+
const exit = ctx.get("appExit");
|
|
1276
|
+
if (exit === undefined) {
|
|
1277
|
+
throw new Error("cli-runner: the launcher must provide ctx.appExit before the tree mounts");
|
|
1278
|
+
}
|
|
1279
|
+
const argv = ctx.get("cmdlineArgs")?.get() ?? [];
|
|
1280
|
+
const options = parseArgs(argv);
|
|
1281
|
+
if (options.help) {
|
|
1282
|
+
printHelp();
|
|
1283
|
+
exit(0);
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1286
|
+
if (options.version) {
|
|
1287
|
+
console.log(VERSION);
|
|
1288
|
+
exit(0);
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1291
|
+
run(ctx, options, exit).catch((error) => {
|
|
1292
|
+
const detail = process.env.DSH_CLI_DEBUG === "1" && error instanceof Error ? `\n${error.stack}` : "";
|
|
1293
|
+
process.stderr.write(`deepseek: ${error instanceof Error ? error.message : String(error)}${detail}\n`);
|
|
1294
|
+
try {
|
|
1295
|
+
exit(1);
|
|
1296
|
+
} catch {}
|
|
1297
|
+
});
|
|
1298
|
+
}
|