@este.systems/dsc 0.2.2 → 0.4.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/CHANGELOG.md +166 -0
- package/README.md +85 -43
- package/bin/dsc.mjs +3 -12
- package/dist/agent.js +5 -5
- package/dist/agent.js.map +1 -1
- package/dist/api.js +108 -0
- package/dist/api.js.map +1 -1
- package/dist/approval.js +58 -27
- package/dist/approval.js.map +1 -1
- package/dist/clipboard.js +59 -0
- package/dist/clipboard.js.map +1 -0
- package/dist/history.js +106 -0
- package/dist/history.js.map +1 -1
- package/dist/prompt.js +2 -1
- package/dist/prompt.js.map +1 -1
- package/dist/slash.js +6 -0
- package/dist/slash.js.map +1 -1
- package/dist/store.js.map +1 -1
- package/dist/tools.js +74 -33
- package/dist/tools.js.map +1 -1
- package/dist/tui/ApprovalDialog.js +41 -1
- package/dist/tui/ApprovalDialog.js.map +1 -1
- package/dist/tui/History.js +14 -1
- package/dist/tui/History.js.map +1 -1
- package/dist/tui/Input.js +17 -0
- package/dist/tui/Input.js.map +1 -1
- package/dist/tui.js +357 -34
- package/dist/tui.js.map +1 -1
- package/dist/ui.js +7 -94
- package/dist/ui.js.map +1 -1
- package/dist/update.js +163 -0
- package/dist/update.js.map +1 -0
- package/package.json +5 -5
- package/dist/index.js +0 -916
- package/dist/index.js.map +0 -1
- package/dist/markdown.js +0 -543
- package/dist/markdown.js.map +0 -1
package/dist/index.js
DELETED
|
@@ -1,916 +0,0 @@
|
|
|
1
|
-
import * as readline from "node:readline/promises";
|
|
2
|
-
import { stdin as input, stdout as output } from "node:process";
|
|
3
|
-
import { promises as fsp } from "node:fs";
|
|
4
|
-
import { openEditor } from "./editor.js";
|
|
5
|
-
import { completeSlash } from "./slash.js";
|
|
6
|
-
import { AVAILABLE_MODELS, DEFAULT_MODEL, DeepSeekError, configPath, hasApiKey, recordUsage, } from "./api.js";
|
|
7
|
-
import { runAgent, formatCost, formatStatus, estimateContextTokens } from "./agent.js";
|
|
8
|
-
import * as history from "./history.js";
|
|
9
|
-
import * as approval from "./approval.js";
|
|
10
|
-
import * as replHistory from "./repl_history.js";
|
|
11
|
-
import * as audit from "./audit.js";
|
|
12
|
-
import { compactSession } from "./compact.js";
|
|
13
|
-
import { Spinner, StatusBar } from "./ui.js";
|
|
14
|
-
import { formatVersionInfo } from "./version.js";
|
|
15
|
-
const RESET = "\x1b[0m";
|
|
16
|
-
const DIM = "\x1b[2m";
|
|
17
|
-
const BOLD = "\x1b[1m";
|
|
18
|
-
const RED = "\x1b[31m";
|
|
19
|
-
function parseArgs(argv) {
|
|
20
|
-
const out = { model: DEFAULT_MODEL, yolo: false, resume: true, modelExplicit: false };
|
|
21
|
-
const positional = [];
|
|
22
|
-
for (let i = 0; i < argv.length; i++) {
|
|
23
|
-
const a = argv[i];
|
|
24
|
-
if (a === "--yolo" || a === "-y") {
|
|
25
|
-
out.yolo = true;
|
|
26
|
-
}
|
|
27
|
-
else if (a === "--model" || a === "-m") {
|
|
28
|
-
const v = argv[++i];
|
|
29
|
-
if (!AVAILABLE_MODELS.includes(v)) {
|
|
30
|
-
throw new Error(`unknown model: ${v} (available: ${AVAILABLE_MODELS.join(", ")})`);
|
|
31
|
-
}
|
|
32
|
-
out.model = v;
|
|
33
|
-
out.modelExplicit = true;
|
|
34
|
-
}
|
|
35
|
-
else if (a === "--no-resume") {
|
|
36
|
-
out.resume = false;
|
|
37
|
-
}
|
|
38
|
-
else if (a === "--resume") {
|
|
39
|
-
const v = argv[i + 1];
|
|
40
|
-
if (v && !v.startsWith("-")) {
|
|
41
|
-
out.resumeId = v;
|
|
42
|
-
i++;
|
|
43
|
-
}
|
|
44
|
-
out.resume = true;
|
|
45
|
-
}
|
|
46
|
-
else if (a === "--help" || a === "-h") {
|
|
47
|
-
out.help = true;
|
|
48
|
-
}
|
|
49
|
-
else if (a === "--version" || a === "-v") {
|
|
50
|
-
out.version = true;
|
|
51
|
-
}
|
|
52
|
-
else if (a.startsWith("-")) {
|
|
53
|
-
throw new Error(`unknown flag: ${a}`);
|
|
54
|
-
}
|
|
55
|
-
else {
|
|
56
|
-
positional.push(a);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
if (positional.length)
|
|
60
|
-
out.prompt = positional.join(" ");
|
|
61
|
-
return out;
|
|
62
|
-
}
|
|
63
|
-
function printHelp() {
|
|
64
|
-
process.stdout.write(`dsc — CLI coding agent for DeepSeek
|
|
65
|
-
|
|
66
|
-
Usage:
|
|
67
|
-
dsc Start interactive REPL
|
|
68
|
-
dsc "your prompt here" One-shot mode: run agent on prompt and exit
|
|
69
|
-
|
|
70
|
-
Flags:
|
|
71
|
-
-m, --model <name> Model: ${AVAILABLE_MODELS.join(" | ")} (default: ${DEFAULT_MODEL})
|
|
72
|
-
-y, --yolo Skip approval prompts for write/edit/bash
|
|
73
|
-
--no-resume Start a fresh session instead of resuming
|
|
74
|
-
--resume [id] Resume a session (default: most recent for this cwd)
|
|
75
|
-
-v, --version Print version (dsc, Node, platform/arch) and exit
|
|
76
|
-
-h, --help Show this help
|
|
77
|
-
|
|
78
|
-
API key (in priority order):
|
|
79
|
-
$DEEPSEEK_API_KEY Env var, takes precedence if set
|
|
80
|
-
${configPath()}
|
|
81
|
-
JSON file. Accepted shapes:
|
|
82
|
-
{"api_key": "sk-..."}
|
|
83
|
-
{"env": {"DEEPSEEK_API_KEY": "sk-..."}}
|
|
84
|
-
{"env": {"ANTHROPIC_AUTH_TOKEN": "sk-..."}} (claude-switcher compat)
|
|
85
|
-
|
|
86
|
-
REPL commands:
|
|
87
|
-
/clear Start a new session (current one stays on disk)
|
|
88
|
-
/cost Show token usage and estimated cost
|
|
89
|
-
/model Show or switch model (e.g. /model deepseek-v4-flash)
|
|
90
|
-
/yolo Toggle approval mode
|
|
91
|
-
/reasoning [on|off]
|
|
92
|
-
Show or hide reasoning_content streamed by thinking models
|
|
93
|
-
(toggle when no arg)
|
|
94
|
-
/lang [name|off]
|
|
95
|
-
Force the model to reply exclusively in <name> (e.g. en,
|
|
96
|
-
ro, fr, Chinese, "formal English"). No arg prints the
|
|
97
|
-
current setting; 'off' clears the directive.
|
|
98
|
-
/auto-continue [N|off]
|
|
99
|
-
When the agent hits MAX_TOOL_DEPTH without finishing, auto-grant
|
|
100
|
-
up to N more budgets instead of stopping. No arg prints the
|
|
101
|
-
current setting. Initial value comes from DSC_AUTO_CONTINUE
|
|
102
|
-
env var; default 0 (manual continue).
|
|
103
|
-
/list List sessions in this cwd
|
|
104
|
-
/save <name> Give the current session a friendly name (used by /resume
|
|
105
|
-
and shown in /list).
|
|
106
|
-
/rename <text> Replace the "assistant:" prefix on streamed turns with
|
|
107
|
-
<text> (a name, glyph, or anything you like). No arg
|
|
108
|
-
prints the current label; --reset / default restores it.
|
|
109
|
-
/resume <ref> Resume a session by index from /list, by /save'd name,
|
|
110
|
-
by id, or 'last' for the most recent.
|
|
111
|
-
/audit [path|show [N]]
|
|
112
|
-
Default / 'path': print the JSONL audit log path.
|
|
113
|
-
'show [N]': render the last N entries inline (default 10).
|
|
114
|
-
/queue [clear] Show or clear the type-ahead queue (lines you typed while
|
|
115
|
-
a turn was running).
|
|
116
|
-
/version Print dsc + Node + platform versions (useful for bug reports).
|
|
117
|
-
/transcript Print the full conversation, including any messages that
|
|
118
|
-
/compact previously archived (kept on disk, not sent to
|
|
119
|
-
the API).
|
|
120
|
-
/compact [N] Summarize older turns into a synthetic block (kept in the
|
|
121
|
-
system prompt) and move them to the archive (visible via
|
|
122
|
-
/transcript). Keeps the last N user turns verbatim
|
|
123
|
-
(default N=4). Cumulative across re-runs.
|
|
124
|
-
/edit [text] Compose the next prompt in $EDITOR (good for paste-heavy
|
|
125
|
-
or multi-line input). Optional initial text seeds the buffer.
|
|
126
|
-
/exit Quit
|
|
127
|
-
|
|
128
|
-
Multi-line input:
|
|
129
|
-
End a line with a single \\ to continue on the next line (bash-style).
|
|
130
|
-
An even number of trailing backslashes is treated as literal.
|
|
131
|
-
For paste-heavy or longer drafts, use /edit.
|
|
132
|
-
|
|
133
|
-
Audit log:
|
|
134
|
-
Every tool call (bash, edits, reads, fetches, rejections) is recorded
|
|
135
|
-
as one JSON line at ${audit.auditLogPath()}.
|
|
136
|
-
Disable with DSC_NO_AUDIT=1.
|
|
137
|
-
|
|
138
|
-
Auto-compact:
|
|
139
|
-
When estimated context tokens exceed DSC_AUTO_COMPACT_AT (default 50000;
|
|
140
|
-
set to 0 / off / false to disable), dsc runs /compact 4 automatically
|
|
141
|
-
after the current turn. Manual /compact still works regardless.
|
|
142
|
-
|
|
143
|
-
Auto-continue:
|
|
144
|
-
When the agent hits MAX_TOOL_DEPTH=24 tool calls in a turn without
|
|
145
|
-
finishing, dsc stops and asks you to type 'continue' to grant another
|
|
146
|
-
budget. Set DSC_AUTO_CONTINUE=N to silently grant up to N extra budgets
|
|
147
|
-
(≈24×N more tool calls) before stopping. Same toggle via /auto-continue.
|
|
148
|
-
`);
|
|
149
|
-
}
|
|
150
|
-
async function main() {
|
|
151
|
-
let cli;
|
|
152
|
-
try {
|
|
153
|
-
cli = parseArgs(process.argv.slice(2));
|
|
154
|
-
}
|
|
155
|
-
catch (e) {
|
|
156
|
-
process.stderr.write(`${RED}${e.message}${RESET}\n`);
|
|
157
|
-
process.exit(2);
|
|
158
|
-
}
|
|
159
|
-
if (cli.help) {
|
|
160
|
-
printHelp();
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
if (cli.version) {
|
|
164
|
-
process.stdout.write(formatVersionInfo() + "\n");
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
// Auto-compact threshold (token-count of estimated context). Set to 0 to
|
|
168
|
-
// disable. Default 50K — well below the 1M model limit, but tuned to keep
|
|
169
|
-
// per-turn input cost from creeping. Override via DSC_AUTO_COMPACT_AT.
|
|
170
|
-
const AUTO_COMPACT_AT_TOKENS = (() => {
|
|
171
|
-
const raw = process.env.DSC_AUTO_COMPACT_AT;
|
|
172
|
-
if (!raw)
|
|
173
|
-
return 50_000;
|
|
174
|
-
if (raw === "0" || raw === "off" || raw === "false")
|
|
175
|
-
return 0;
|
|
176
|
-
const n = parseInt(raw, 10);
|
|
177
|
-
return Number.isFinite(n) && n > 0 ? n : 50_000;
|
|
178
|
-
})();
|
|
179
|
-
const AUTO_COMPACT_KEEP = 4;
|
|
180
|
-
// Auto-continue: how many times to silently grant another tool-call
|
|
181
|
-
// budget when the agent hits MAX_TOOL_DEPTH without converging. 0 (default)
|
|
182
|
-
// keeps today's behavior — stop and ask the user to type 'continue'.
|
|
183
|
-
const initialAutoContinue = (() => {
|
|
184
|
-
const raw = process.env.DSC_AUTO_CONTINUE;
|
|
185
|
-
if (!raw)
|
|
186
|
-
return 0;
|
|
187
|
-
if (raw === "0" || raw === "off" || raw === "false")
|
|
188
|
-
return 0;
|
|
189
|
-
const n = parseInt(raw, 10);
|
|
190
|
-
return Number.isFinite(n) && n > 0 ? n : 0;
|
|
191
|
-
})();
|
|
192
|
-
if (!hasApiKey()) {
|
|
193
|
-
process.stderr.write(`${RED}No DeepSeek API key found.${RESET}\n`);
|
|
194
|
-
process.stderr.write(`Either export DEEPSEEK_API_KEY, or create ${configPath()} containing:\n`);
|
|
195
|
-
process.stderr.write(` {"api_key": "sk-..."}\n`);
|
|
196
|
-
process.stderr.write(`(also accepts {"env": {"ANTHROPIC_AUTH_TOKEN": "sk-..."}} for claude-switcher compat)\n`);
|
|
197
|
-
process.exit(1);
|
|
198
|
-
}
|
|
199
|
-
const cwd = process.cwd();
|
|
200
|
-
// Migrate old per-cwd file to the new sessions dir if present.
|
|
201
|
-
await history.migrateLegacyIfPresent(cwd, cli.model);
|
|
202
|
-
let session = history.newSession(cwd, cli.model);
|
|
203
|
-
// The system prompt is rebuilt per turn inside runAgent so cwd/date/status
|
|
204
|
-
// are always current — we no longer persist a stale copy at messages[0].
|
|
205
|
-
let restoredTurns = 0;
|
|
206
|
-
if (cli.resume) {
|
|
207
|
-
let target = null;
|
|
208
|
-
if (cli.resumeId) {
|
|
209
|
-
const loaded = await history.loadSession(cli.resumeId);
|
|
210
|
-
if (loaded) {
|
|
211
|
-
session = loaded;
|
|
212
|
-
target = { id: loaded.id };
|
|
213
|
-
}
|
|
214
|
-
else {
|
|
215
|
-
process.stderr.write(`${RED}session not found: ${cli.resumeId}${RESET}\n`);
|
|
216
|
-
process.exit(1);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
else {
|
|
220
|
-
target = await history.mostRecentForCwd(cwd);
|
|
221
|
-
if (target) {
|
|
222
|
-
const loaded = await history.loadSession(target.id);
|
|
223
|
-
if (loaded)
|
|
224
|
-
session = loaded;
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
restoredTurns = session.messages.filter((m) => m.role === "user").length;
|
|
228
|
-
}
|
|
229
|
-
let messages = session.messages;
|
|
230
|
-
let stats = session.stats;
|
|
231
|
-
let model = cli.modelExplicit ? cli.model : session.model;
|
|
232
|
-
const toolCtx = {
|
|
233
|
-
cwd,
|
|
234
|
-
yolo: cli.yolo,
|
|
235
|
-
filesTouched: stats.files_touched,
|
|
236
|
-
sessionId: session.id,
|
|
237
|
-
};
|
|
238
|
-
// Single-in-flight, coalescing save. Multiple persist() calls during one
|
|
239
|
-
// save's RTT collapse into one re-save at the end with the latest state.
|
|
240
|
-
// Awaiting persist() returns when *all* queued state is on disk.
|
|
241
|
-
let savePromise = null;
|
|
242
|
-
let savePending = false;
|
|
243
|
-
const persist = () => {
|
|
244
|
-
savePending = true;
|
|
245
|
-
if (savePromise)
|
|
246
|
-
return savePromise;
|
|
247
|
-
savePromise = (async () => {
|
|
248
|
-
while (savePending) {
|
|
249
|
-
savePending = false;
|
|
250
|
-
try {
|
|
251
|
-
session.model = model;
|
|
252
|
-
session.messages = messages;
|
|
253
|
-
session.stats = stats;
|
|
254
|
-
await history.saveSession(session);
|
|
255
|
-
}
|
|
256
|
-
catch (e) {
|
|
257
|
-
process.stderr.write(`${DIM}(history save failed: ${e.message})${RESET}\n`);
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
savePromise = null;
|
|
261
|
-
})();
|
|
262
|
-
return savePromise;
|
|
263
|
-
};
|
|
264
|
-
const statusBar = new StatusBar();
|
|
265
|
-
const sessionStart = Date.now();
|
|
266
|
-
let showReasoning = true;
|
|
267
|
-
let autoContinue = initialAutoContinue;
|
|
268
|
-
// Declared up here (before currentStatusLine references it) so the early
|
|
269
|
-
// refreshStatus() right after statusBar.enable doesn't hit the TDZ.
|
|
270
|
-
const promptQueue = [];
|
|
271
|
-
const currentStatusLine = () => formatStatus(stats, model, {
|
|
272
|
-
yolo: toolCtx.yolo,
|
|
273
|
-
reasoning: showReasoning,
|
|
274
|
-
contextTokens: estimateContextTokens(messages),
|
|
275
|
-
sessionSeconds: Math.floor((Date.now() - sessionStart) / 1000),
|
|
276
|
-
compacted: !!session.compaction,
|
|
277
|
-
queued: promptQueue.length,
|
|
278
|
-
});
|
|
279
|
-
const refreshStatus = () => statusBar.render(currentStatusLine());
|
|
280
|
-
let pendingAbort = null;
|
|
281
|
-
const formatApiError = (e) => {
|
|
282
|
-
if (e.status === 401) {
|
|
283
|
-
return `API key rejected (401). Check $DEEPSEEK_API_KEY or ${configPath()}.`;
|
|
284
|
-
}
|
|
285
|
-
if (e.status === 429)
|
|
286
|
-
return "Rate-limited (429). Try again in a moment.";
|
|
287
|
-
if (e.status === 400 && e.body) {
|
|
288
|
-
let detail = e.body;
|
|
289
|
-
try {
|
|
290
|
-
const parsed = JSON.parse(e.body);
|
|
291
|
-
detail = parsed?.error?.message ?? detail;
|
|
292
|
-
}
|
|
293
|
-
catch { }
|
|
294
|
-
return `Bad request (400): ${detail}`;
|
|
295
|
-
}
|
|
296
|
-
return e.message;
|
|
297
|
-
};
|
|
298
|
-
const runTurn = async (userText) => {
|
|
299
|
-
messages.push({ role: "user", content: userText });
|
|
300
|
-
pendingAbort = new AbortController();
|
|
301
|
-
try {
|
|
302
|
-
await runAgent({
|
|
303
|
-
model,
|
|
304
|
-
stats,
|
|
305
|
-
toolCtx,
|
|
306
|
-
messages,
|
|
307
|
-
signal: pendingAbort.signal,
|
|
308
|
-
onTurn: () => {
|
|
309
|
-
refreshStatus();
|
|
310
|
-
// Make every committed message durable. Coalesces if a save is in
|
|
311
|
-
// flight, so back-to-back tool turns don't queue N saves.
|
|
312
|
-
void persist();
|
|
313
|
-
},
|
|
314
|
-
showReasoning,
|
|
315
|
-
getSummary: () => session.compaction?.summary,
|
|
316
|
-
assistantLabel: session.assistantLabel,
|
|
317
|
-
maxAutoContinue: autoContinue,
|
|
318
|
-
language: session.language,
|
|
319
|
-
});
|
|
320
|
-
}
|
|
321
|
-
catch (e) {
|
|
322
|
-
if (e.name === "AbortError" || pendingAbort?.signal.aborted) {
|
|
323
|
-
process.stderr.write(`\n${DIM}(interrupted)${RESET}\n`);
|
|
324
|
-
}
|
|
325
|
-
else if (e instanceof DeepSeekError) {
|
|
326
|
-
process.stderr.write(`\n${RED}${formatApiError(e)}${RESET}\n`);
|
|
327
|
-
if (e.body && e.status !== 400) {
|
|
328
|
-
process.stderr.write(`${DIM}${e.body.slice(0, 1000)}${RESET}\n`);
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
else {
|
|
332
|
-
process.stderr.write(`\n${RED}${e.message}${RESET}\n`);
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
finally {
|
|
336
|
-
pendingAbort = null;
|
|
337
|
-
}
|
|
338
|
-
// No final refreshStatus here — the last onTurn inside runAgent already
|
|
339
|
-
// printed the post-push status; re-printing would just double the bar.
|
|
340
|
-
await persist();
|
|
341
|
-
};
|
|
342
|
-
// One-shot mode
|
|
343
|
-
if (cli.prompt) {
|
|
344
|
-
await runTurn(cli.prompt);
|
|
345
|
-
process.stdout.write(`\n${DIM}${formatCost(stats, model)}${RESET}\n`);
|
|
346
|
-
return;
|
|
347
|
-
}
|
|
348
|
-
// REPL
|
|
349
|
-
process.stdout.write(`${BOLD}dsc${RESET} ${DIM}(${model}${cli.yolo ? ", yolo" : ""})${RESET} `);
|
|
350
|
-
process.stdout.write(`${DIM}type /help for commands, ESC to interrupt a turn, Ctrl+D to exit${RESET}\n`);
|
|
351
|
-
if (restoredTurns > 0) {
|
|
352
|
-
process.stdout.write(`${DIM}restored ${restoredTurns}-turn history (use /clear to reset)${RESET}\n`);
|
|
353
|
-
}
|
|
354
|
-
statusBar.enable();
|
|
355
|
-
refreshStatus();
|
|
356
|
-
const cleanup = () => statusBar.disable();
|
|
357
|
-
process.on("exit", cleanup);
|
|
358
|
-
let lastSigintMs = 0;
|
|
359
|
-
process.on("SIGINT", () => {
|
|
360
|
-
if (pendingAbort && !pendingAbort.signal.aborted) {
|
|
361
|
-
pendingAbort.abort();
|
|
362
|
-
return;
|
|
363
|
-
}
|
|
364
|
-
const now = Date.now();
|
|
365
|
-
if (now - lastSigintMs < 1000) {
|
|
366
|
-
cleanup();
|
|
367
|
-
process.exit(130);
|
|
368
|
-
}
|
|
369
|
-
lastSigintMs = now;
|
|
370
|
-
process.stdout.write(`\n${DIM}(press Ctrl+C again within 1s to exit)${RESET}\n`);
|
|
371
|
-
});
|
|
372
|
-
const rl = readline.createInterface({
|
|
373
|
-
input,
|
|
374
|
-
output,
|
|
375
|
-
historySize: 1000,
|
|
376
|
-
completer: completeSlashCommand,
|
|
377
|
-
});
|
|
378
|
-
// Approval calls rl.question — while one is pending, lines that come back
|
|
379
|
-
// are the user's y/N answer, not new prompts. The queue listener checks this
|
|
380
|
-
// flag and stays out of the way so the answer doesn't get queued.
|
|
381
|
-
let approvalActive = false;
|
|
382
|
-
approval.setAsker(async (q) => {
|
|
383
|
-
approvalActive = true;
|
|
384
|
-
try {
|
|
385
|
-
return await rl.question(q);
|
|
386
|
-
}
|
|
387
|
-
finally {
|
|
388
|
-
approvalActive = false;
|
|
389
|
-
}
|
|
390
|
-
});
|
|
391
|
-
// Type-ahead queue listener: while a turn is running, captured 'line'
|
|
392
|
-
// events get pushed into promptQueue (declared earlier in this function).
|
|
393
|
-
const bufferDuringTurn = (line) => {
|
|
394
|
-
if (approvalActive)
|
|
395
|
-
return;
|
|
396
|
-
const trimmed = line.trim();
|
|
397
|
-
if (!trimmed)
|
|
398
|
-
return;
|
|
399
|
-
promptQueue.push(trimmed);
|
|
400
|
-
process.stdout.write(`${DIM}(queued — ${promptQueue.length} pending)${RESET}\n`);
|
|
401
|
-
refreshStatus();
|
|
402
|
-
};
|
|
403
|
-
// ESC interrupts the current turn — more intuitive than Ctrl+C for "stop
|
|
404
|
-
// what the agent is doing right now". readline already puts stdin in raw
|
|
405
|
-
// mode and emits 'keypress' events for terminal input, so we just listen.
|
|
406
|
-
// Standalone ESC fires after a short disambiguation delay; ESC-prefixed
|
|
407
|
-
// sequences (arrow keys etc) come through as their named keys instead.
|
|
408
|
-
const onKeypress = (_str, key) => {
|
|
409
|
-
if (key?.name === "escape" &&
|
|
410
|
-
pendingAbort &&
|
|
411
|
-
!pendingAbort.signal.aborted) {
|
|
412
|
-
pendingAbort.abort();
|
|
413
|
-
}
|
|
414
|
-
};
|
|
415
|
-
process.stdin.on("keypress", onKeypress);
|
|
416
|
-
// Also catch readline's own SIGINT path for Ctrl+C — defends against the
|
|
417
|
-
// case where readline intercepts the signal before our process.on handler.
|
|
418
|
-
rl.on("SIGINT", () => {
|
|
419
|
-
if (pendingAbort && !pendingAbort.signal.aborted) {
|
|
420
|
-
pendingAbort.abort();
|
|
421
|
-
}
|
|
422
|
-
});
|
|
423
|
-
// Seed up/down history from disk (newest first per readline's convention).
|
|
424
|
-
void replHistory.compact();
|
|
425
|
-
const past = await replHistory.load();
|
|
426
|
-
const rlAny = rl;
|
|
427
|
-
rlAny.history.length = 0;
|
|
428
|
-
rlAny.history.push(...past.slice().reverse());
|
|
429
|
-
while (true) {
|
|
430
|
-
let line;
|
|
431
|
-
if (promptQueue.length) {
|
|
432
|
-
// Drain the type-ahead queue before prompting again. Show what we're
|
|
433
|
-
// about to run so the user can tell it's not a fresh prompt.
|
|
434
|
-
line = promptQueue.shift();
|
|
435
|
-
process.stdout.write(`${DIM}── from queue:${RESET} ${line}\n`);
|
|
436
|
-
refreshStatus();
|
|
437
|
-
}
|
|
438
|
-
else {
|
|
439
|
-
try {
|
|
440
|
-
line = await readPromptInput(rl);
|
|
441
|
-
}
|
|
442
|
-
catch {
|
|
443
|
-
break; // Ctrl+D
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
const trimmed = line.trim();
|
|
447
|
-
if (!trimmed)
|
|
448
|
-
continue;
|
|
449
|
-
// Persist the user's submitted line to the on-disk history file. Slash
|
|
450
|
-
// commands are recorded too — recalling "/resume 3" is useful.
|
|
451
|
-
void replHistory.append(trimmed);
|
|
452
|
-
if (trimmed.startsWith("/")) {
|
|
453
|
-
const [cmd, ...rest] = trimmed.slice(1).split(/\s+/);
|
|
454
|
-
const arg = rest.join(" ");
|
|
455
|
-
if (cmd === "exit" || cmd === "quit") {
|
|
456
|
-
break;
|
|
457
|
-
}
|
|
458
|
-
else if (cmd === "help") {
|
|
459
|
-
printHelp();
|
|
460
|
-
}
|
|
461
|
-
else if (cmd === "clear") {
|
|
462
|
-
// Start a brand-new session; old session stays on disk. System prompt
|
|
463
|
-
// is rebuilt per turn so we don't seed messages with one.
|
|
464
|
-
session = history.newSession(cwd, model);
|
|
465
|
-
messages = session.messages;
|
|
466
|
-
stats = session.stats;
|
|
467
|
-
toolCtx.filesTouched = stats.files_touched;
|
|
468
|
-
toolCtx.sessionId = session.id;
|
|
469
|
-
refreshStatus();
|
|
470
|
-
process.stdout.write(`${DIM}new session started (${session.id})${RESET}\n`);
|
|
471
|
-
}
|
|
472
|
-
else if (cmd === "list") {
|
|
473
|
-
const all = await history.listSessions(cwd);
|
|
474
|
-
if (!all.length) {
|
|
475
|
-
process.stdout.write(`${DIM}no sessions for ${cwd}${RESET}\n`);
|
|
476
|
-
}
|
|
477
|
-
else {
|
|
478
|
-
all.forEach((s, i) => {
|
|
479
|
-
const ago = formatRelative(s.updated_at);
|
|
480
|
-
const here = s.id === session.id ? `${BOLD}*${RESET} ` : " ";
|
|
481
|
-
const label = s.name ? `${BOLD}${s.name}${RESET} (${s.model})` : s.model;
|
|
482
|
-
process.stdout.write(`${here}${String(i + 1).padStart(2, " ")}. ${label} ${ago} (${s.message_count} msgs) ${DIM}${s.first_user_message || "—"}${RESET}\n`);
|
|
483
|
-
});
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
else if (cmd === "save") {
|
|
487
|
-
const name = arg.trim();
|
|
488
|
-
if (!name) {
|
|
489
|
-
process.stdout.write(`${RED}usage: /save <name>${RESET}\n`);
|
|
490
|
-
}
|
|
491
|
-
else {
|
|
492
|
-
session.name = name;
|
|
493
|
-
await persist();
|
|
494
|
-
process.stdout.write(`${DIM}session saved as "${name}" (id ${session.id})${RESET}\n`);
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
else if (cmd === "rename") {
|
|
498
|
-
const text = arg.trim();
|
|
499
|
-
if (!text) {
|
|
500
|
-
const cur = session.assistantLabel ?? "assistant:";
|
|
501
|
-
process.stdout.write(`${DIM}assistant label: "${cur}"${RESET}\n`);
|
|
502
|
-
}
|
|
503
|
-
else if (text === "--reset" || text === "default") {
|
|
504
|
-
delete session.assistantLabel;
|
|
505
|
-
await persist();
|
|
506
|
-
process.stdout.write(`${DIM}assistant label reset to default${RESET}\n`);
|
|
507
|
-
}
|
|
508
|
-
else {
|
|
509
|
-
session.assistantLabel = text;
|
|
510
|
-
await persist();
|
|
511
|
-
process.stdout.write(`${DIM}assistant label → "${text}"${RESET}\n`);
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
else if (cmd === "resume") {
|
|
515
|
-
const all = await history.listSessions(cwd);
|
|
516
|
-
if (!all.length) {
|
|
517
|
-
process.stdout.write(`${DIM}no sessions to resume${RESET}\n`);
|
|
518
|
-
}
|
|
519
|
-
else {
|
|
520
|
-
let target = null;
|
|
521
|
-
if (!arg || arg === "last") {
|
|
522
|
-
target = all[0];
|
|
523
|
-
}
|
|
524
|
-
else if (/^\d+$/.test(arg)) {
|
|
525
|
-
const idx = parseInt(arg, 10) - 1;
|
|
526
|
-
target = all[idx] ?? null;
|
|
527
|
-
if (!target)
|
|
528
|
-
process.stdout.write(`${RED}no session at index ${arg} (have ${all.length})${RESET}\n`);
|
|
529
|
-
}
|
|
530
|
-
else {
|
|
531
|
-
// Match by name first (most-recent wins on tie since `all` is
|
|
532
|
-
// sorted desc by updated_at), then fall back to id.
|
|
533
|
-
target =
|
|
534
|
-
all.find((s) => s.name === arg) ??
|
|
535
|
-
all.find((s) => s.id === arg) ??
|
|
536
|
-
null;
|
|
537
|
-
if (!target)
|
|
538
|
-
process.stdout.write(`${RED}no session with name or id ${arg}${RESET}\n`);
|
|
539
|
-
}
|
|
540
|
-
if (target) {
|
|
541
|
-
const loaded = await history.loadSession(target.id);
|
|
542
|
-
if (!loaded) {
|
|
543
|
-
process.stdout.write(`${RED}failed to load session ${target.id}${RESET}\n`);
|
|
544
|
-
}
|
|
545
|
-
else {
|
|
546
|
-
session = loaded;
|
|
547
|
-
messages = session.messages;
|
|
548
|
-
stats = session.stats;
|
|
549
|
-
model = session.model;
|
|
550
|
-
toolCtx.filesTouched = stats.files_touched;
|
|
551
|
-
toolCtx.sessionId = session.id;
|
|
552
|
-
refreshStatus();
|
|
553
|
-
const userTurns = messages.filter((m) => m.role === "user").length;
|
|
554
|
-
process.stdout.write(`${DIM}resumed ${session.id} (${userTurns} turns, model ${model})${RESET}\n`);
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
else if (cmd === "cost") {
|
|
560
|
-
process.stdout.write(`${DIM}${formatCost(stats, model)}${RESET}\n`);
|
|
561
|
-
}
|
|
562
|
-
else if (cmd === "model") {
|
|
563
|
-
if (!arg) {
|
|
564
|
-
process.stdout.write(`${DIM}current model: ${model}${RESET}\n`);
|
|
565
|
-
}
|
|
566
|
-
else if (!AVAILABLE_MODELS.includes(arg)) {
|
|
567
|
-
process.stdout.write(`${RED}unknown model: ${arg} (available: ${AVAILABLE_MODELS.join(", ")})${RESET}\n`);
|
|
568
|
-
}
|
|
569
|
-
else {
|
|
570
|
-
model = arg;
|
|
571
|
-
refreshStatus();
|
|
572
|
-
process.stdout.write(`${DIM}model -> ${model}${RESET}\n`);
|
|
573
|
-
await persist();
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
else if (cmd === "yolo") {
|
|
577
|
-
toolCtx.yolo = !toolCtx.yolo;
|
|
578
|
-
refreshStatus();
|
|
579
|
-
process.stdout.write(`${DIM}yolo: ${toolCtx.yolo}${RESET}\n`);
|
|
580
|
-
}
|
|
581
|
-
else if (cmd === "reasoning") {
|
|
582
|
-
if (arg === "on")
|
|
583
|
-
showReasoning = true;
|
|
584
|
-
else if (arg === "off")
|
|
585
|
-
showReasoning = false;
|
|
586
|
-
else
|
|
587
|
-
showReasoning = !showReasoning; // toggle when no arg
|
|
588
|
-
refreshStatus();
|
|
589
|
-
process.stdout.write(`${DIM}reasoning: ${showReasoning ? "on" : "off"}${RESET}\n`);
|
|
590
|
-
}
|
|
591
|
-
else if (cmd === "queue") {
|
|
592
|
-
const sub = arg.trim().toLowerCase();
|
|
593
|
-
if (sub === "clear" || sub === "drop") {
|
|
594
|
-
const n = promptQueue.length;
|
|
595
|
-
promptQueue.length = 0;
|
|
596
|
-
refreshStatus();
|
|
597
|
-
process.stdout.write(`${DIM}cleared ${n} queued prompt(s)${RESET}\n`);
|
|
598
|
-
}
|
|
599
|
-
else if (promptQueue.length === 0) {
|
|
600
|
-
process.stdout.write(`${DIM}queue is empty${RESET}\n`);
|
|
601
|
-
}
|
|
602
|
-
else {
|
|
603
|
-
promptQueue.forEach((p, i) => process.stdout.write(`${DIM}${String(i + 1).padStart(2, " ")}.${RESET} ${p}\n`));
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
else if (cmd === "lang") {
|
|
607
|
-
const text = arg.trim();
|
|
608
|
-
if (!text) {
|
|
609
|
-
process.stdout.write(`${DIM}language: ${session.language ? `"${session.language}"` : "off (any language)"}${RESET}\n`);
|
|
610
|
-
}
|
|
611
|
-
else if (text === "off" || text === "default" || text === "any") {
|
|
612
|
-
delete session.language;
|
|
613
|
-
await persist();
|
|
614
|
-
process.stdout.write(`${DIM}language directive cleared${RESET}\n`);
|
|
615
|
-
}
|
|
616
|
-
else {
|
|
617
|
-
session.language = text;
|
|
618
|
-
await persist();
|
|
619
|
-
process.stdout.write(`${DIM}language → "${text}" (replies will be exclusively in this language)${RESET}\n`);
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
else if (cmd === "auto-continue") {
|
|
623
|
-
const trimmed = arg.trim();
|
|
624
|
-
if (!trimmed) {
|
|
625
|
-
process.stdout.write(`${DIM}auto-continue: ${autoContinue === 0 ? "off" : `up to ${autoContinue} extra budget(s)`}${RESET}\n`);
|
|
626
|
-
}
|
|
627
|
-
else if (trimmed === "off" || trimmed === "0" || trimmed === "false") {
|
|
628
|
-
autoContinue = 0;
|
|
629
|
-
process.stdout.write(`${DIM}auto-continue: off${RESET}\n`);
|
|
630
|
-
}
|
|
631
|
-
else {
|
|
632
|
-
const n = parseInt(trimmed, 10);
|
|
633
|
-
if (!Number.isFinite(n) || n < 0) {
|
|
634
|
-
process.stdout.write(`${RED}usage: /auto-continue [N|off]${RESET}\n`);
|
|
635
|
-
}
|
|
636
|
-
else {
|
|
637
|
-
autoContinue = n;
|
|
638
|
-
process.stdout.write(`${DIM}auto-continue: ${n === 0 ? "off" : `up to ${n} extra budget(s)`}${RESET}\n`);
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
else if (cmd === "version") {
|
|
643
|
-
process.stdout.write(`${DIM}${formatVersionInfo()}${RESET}\n`);
|
|
644
|
-
}
|
|
645
|
-
else if (cmd === "audit") {
|
|
646
|
-
const sub = arg.trim();
|
|
647
|
-
if (sub.startsWith("show")) {
|
|
648
|
-
const nRaw = sub.replace(/^show\s*/, "").trim();
|
|
649
|
-
const n = nRaw ? parseInt(nRaw, 10) : NaN;
|
|
650
|
-
const limit = Number.isFinite(n) && n > 0 ? n : 10;
|
|
651
|
-
await renderAuditEntries(limit);
|
|
652
|
-
}
|
|
653
|
-
else if (!sub || sub === "path") {
|
|
654
|
-
process.stdout.write(`${DIM}${audit.auditLogPath()}${RESET}\n`);
|
|
655
|
-
}
|
|
656
|
-
else {
|
|
657
|
-
process.stdout.write(`${RED}usage: /audit | /audit path | /audit show [N]${RESET}\n`);
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
else if (cmd === "transcript") {
|
|
661
|
-
const archived = session.archivedMessages ?? [];
|
|
662
|
-
if (archived.length === 0 && messages.length === 0) {
|
|
663
|
-
process.stdout.write(`${DIM}(no messages)${RESET}\n`);
|
|
664
|
-
}
|
|
665
|
-
else {
|
|
666
|
-
if (archived.length > 0) {
|
|
667
|
-
process.stdout.write(`${DIM}── archived (${archived.length} messages)${RESET}\n`);
|
|
668
|
-
for (const m of archived)
|
|
669
|
-
renderTranscriptMessage(m, true);
|
|
670
|
-
}
|
|
671
|
-
if (messages.length > 0) {
|
|
672
|
-
process.stdout.write(`${DIM}── active (${messages.length} messages)${RESET}\n`);
|
|
673
|
-
for (const m of messages)
|
|
674
|
-
renderTranscriptMessage(m, false);
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
else if (cmd === "compact") {
|
|
679
|
-
const keepRaw = arg ? parseInt(arg, 10) : NaN;
|
|
680
|
-
const keep = Number.isFinite(keepRaw) ? Math.max(0, keepRaw) : 4;
|
|
681
|
-
await runCompaction(keep, false);
|
|
682
|
-
}
|
|
683
|
-
else if (cmd === "edit") {
|
|
684
|
-
const draft = openEditor(arg ? arg + "\n" : "");
|
|
685
|
-
if (draft === null) {
|
|
686
|
-
process.stdout.write(`${RED}editor failed${RESET}\n`);
|
|
687
|
-
}
|
|
688
|
-
else if (!draft.trim()) {
|
|
689
|
-
process.stdout.write(`${DIM}(empty draft, not sent)${RESET}\n`);
|
|
690
|
-
}
|
|
691
|
-
else {
|
|
692
|
-
process.stdout.write(`${DIM}── editor draft (${draft.length} chars):${RESET}\n`);
|
|
693
|
-
process.stdout.write(draft.replace(/\n/g, "\n ") + "\n");
|
|
694
|
-
process.stdout.write(`${DIM}──${RESET}\n`);
|
|
695
|
-
void replHistory.append(draft);
|
|
696
|
-
await runTurnWithHistorySnapshot(draft);
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
else {
|
|
700
|
-
process.stdout.write(`${RED}unknown command: /${cmd}${RESET}\n`);
|
|
701
|
-
}
|
|
702
|
-
continue;
|
|
703
|
-
}
|
|
704
|
-
await runTurnWithHistorySnapshot(trimmed);
|
|
705
|
-
}
|
|
706
|
-
async function runTurnWithHistorySnapshot(text) {
|
|
707
|
-
// Snapshot rl.history so approval y/N answers (which readline auto-adds)
|
|
708
|
-
// don't leak into up-arrow recall.
|
|
709
|
-
const histSnapshot = rlAny.history.slice();
|
|
710
|
-
// Listen for 'line' events while the turn runs so type-ahead lands in
|
|
711
|
-
// promptQueue instead of being lost.
|
|
712
|
-
rl.on("line", bufferDuringTurn);
|
|
713
|
-
try {
|
|
714
|
-
await runTurn(text);
|
|
715
|
-
}
|
|
716
|
-
finally {
|
|
717
|
-
rl.off("line", bufferDuringTurn);
|
|
718
|
-
rlAny.history.length = 0;
|
|
719
|
-
rlAny.history.push(...histSnapshot);
|
|
720
|
-
}
|
|
721
|
-
// After every turn, kick off an auto-compaction if ctx is over budget.
|
|
722
|
-
if (AUTO_COMPACT_AT_TOKENS > 0) {
|
|
723
|
-
const ctx = estimateContextTokens(messages);
|
|
724
|
-
if (ctx > AUTO_COMPACT_AT_TOKENS) {
|
|
725
|
-
await runCompaction(AUTO_COMPACT_KEEP, true);
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
async function runCompaction(keep, auto) {
|
|
730
|
-
const beforeMessages = messages.length;
|
|
731
|
-
const beforeChars = messages.reduce((n, m) => n + (typeof m.content === "string" ? m.content.length : 0), 0);
|
|
732
|
-
if (auto) {
|
|
733
|
-
process.stdout.write(`${DIM}── auto-compact (ctx > ${AUTO_COMPACT_AT_TOKENS} tokens)${RESET}\n`);
|
|
734
|
-
}
|
|
735
|
-
const spinner = new Spinner("compacting");
|
|
736
|
-
spinner.start();
|
|
737
|
-
try {
|
|
738
|
-
const result = await compactSession(session, keep, model);
|
|
739
|
-
spinner.stop();
|
|
740
|
-
if (!result) {
|
|
741
|
-
if (!auto) {
|
|
742
|
-
process.stdout.write(`${DIM}nothing to compact (need more than ${keep} user turns)${RESET}\n`);
|
|
743
|
-
}
|
|
744
|
-
return;
|
|
745
|
-
}
|
|
746
|
-
// Move the summarized messages into the on-disk archive so /transcript
|
|
747
|
-
// can still show them. They're no longer sent to the API.
|
|
748
|
-
session.archivedMessages = [
|
|
749
|
-
...(session.archivedMessages ?? []),
|
|
750
|
-
...result.droppedMessages,
|
|
751
|
-
];
|
|
752
|
-
session.messages = result.remainingMessages;
|
|
753
|
-
messages = session.messages;
|
|
754
|
-
session.compaction = {
|
|
755
|
-
summary: result.summary,
|
|
756
|
-
compacted_at: Date.now(),
|
|
757
|
-
turns_removed: (session.compaction?.turns_removed ?? 0) + result.turnsRemoved,
|
|
758
|
-
};
|
|
759
|
-
stats.prompts += 1;
|
|
760
|
-
recordUsage(stats, result.usage);
|
|
761
|
-
const afterChars = messages.reduce((n, m) => n + (typeof m.content === "string" ? m.content.length : 0), 0);
|
|
762
|
-
const summaryChars = result.summary.length;
|
|
763
|
-
process.stdout.write(`${DIM}compacted ${result.turnsRemoved} user turn(s); messages ${beforeMessages} → ${messages.length}; chars ${beforeChars} → ${afterChars} + ${summaryChars} summary${RESET}\n`);
|
|
764
|
-
refreshStatus();
|
|
765
|
-
await persist();
|
|
766
|
-
}
|
|
767
|
-
catch (e) {
|
|
768
|
-
spinner.stop();
|
|
769
|
-
if (e instanceof DeepSeekError) {
|
|
770
|
-
process.stderr.write(`${RED}compaction failed: ${formatApiError(e)}${RESET}\n`);
|
|
771
|
-
}
|
|
772
|
-
else {
|
|
773
|
-
process.stderr.write(`${RED}compaction failed: ${e.message}${RESET}\n`);
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
approval.setAsker(null);
|
|
778
|
-
process.stdin.removeListener("keypress", onKeypress);
|
|
779
|
-
rl.close();
|
|
780
|
-
statusBar.disable();
|
|
781
|
-
process.stdout.write(`\n${DIM}${formatCost(stats, model)}${RESET}\n`);
|
|
782
|
-
}
|
|
783
|
-
async function renderAuditEntries(limit) {
|
|
784
|
-
let text;
|
|
785
|
-
try {
|
|
786
|
-
text = await fsp.readFile(audit.auditLogPath(), "utf8");
|
|
787
|
-
}
|
|
788
|
-
catch {
|
|
789
|
-
process.stdout.write(`${DIM}(no audit log yet)${RESET}\n`);
|
|
790
|
-
return;
|
|
791
|
-
}
|
|
792
|
-
const lines = text.split("\n").filter((l) => l.length > 0).slice(-limit);
|
|
793
|
-
if (lines.length === 0) {
|
|
794
|
-
process.stdout.write(`${DIM}(empty)${RESET}\n`);
|
|
795
|
-
return;
|
|
796
|
-
}
|
|
797
|
-
for (const line of lines) {
|
|
798
|
-
let entry;
|
|
799
|
-
try {
|
|
800
|
-
entry = JSON.parse(line);
|
|
801
|
-
}
|
|
802
|
-
catch {
|
|
803
|
-
continue;
|
|
804
|
-
}
|
|
805
|
-
const ts = typeof entry.ts === "string" ? entry.ts.slice(11, 19) : "";
|
|
806
|
-
const ok = entry.approved === false
|
|
807
|
-
? `${RED}✗${RESET}`
|
|
808
|
-
: entry.error
|
|
809
|
-
? `${RED}!${RESET}`
|
|
810
|
-
: `${DIM}✓${RESET}`;
|
|
811
|
-
const tool = String(entry.tool ?? "?");
|
|
812
|
-
const summary = summarizeAuditEntry(entry);
|
|
813
|
-
process.stdout.write(`${DIM}${ts}${RESET} ${ok} ${BOLD}${tool.padEnd(11)}${RESET} ${summary}\n`);
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
function summarizeAuditEntry(e) {
|
|
817
|
-
const trim = (v, n = 80) => {
|
|
818
|
-
const s = typeof v === "string" ? v : "";
|
|
819
|
-
return s.length <= n ? s : s.slice(0, n) + "…";
|
|
820
|
-
};
|
|
821
|
-
switch (e.tool) {
|
|
822
|
-
case "bash":
|
|
823
|
-
return trim(e.command, 100);
|
|
824
|
-
case "write_file":
|
|
825
|
-
case "edit_file":
|
|
826
|
-
case "read_file":
|
|
827
|
-
return trim(e.path);
|
|
828
|
-
case "grep":
|
|
829
|
-
return `"${trim(e.pattern, 40)}" in ${trim(e.path, 40)}`;
|
|
830
|
-
case "glob":
|
|
831
|
-
return trim(e.pattern);
|
|
832
|
-
case "web_search":
|
|
833
|
-
return `"${trim(e.query, 80)}"`;
|
|
834
|
-
case "web_fetch":
|
|
835
|
-
return trim(e.url, 100);
|
|
836
|
-
default:
|
|
837
|
-
return "";
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
// Slash commands the REPL recognizes. Used both for tab completion and as
|
|
841
|
-
// a single source of truth for what's accepted (kept in sync with the if/
|
|
842
|
-
// else chain in main()).
|
|
843
|
-
// readline-style completer: returns [matches, substringMatched]. Backs into
|
|
844
|
-
// the shared completeSlash helper so the REPL and TUI complete the same set.
|
|
845
|
-
function completeSlashCommand(line) {
|
|
846
|
-
const { matches } = completeSlash(line);
|
|
847
|
-
return [matches, line];
|
|
848
|
-
}
|
|
849
|
-
function renderTranscriptMessage(m, archived) {
|
|
850
|
-
const tag = archived ? `${DIM}archived${RESET} ` : "";
|
|
851
|
-
const role = m.role;
|
|
852
|
-
const color = role === "user" ? "\x1b[36m" : role === "assistant" ? "\x1b[35m" : DIM;
|
|
853
|
-
const content = typeof m.content === "string" ? m.content : "";
|
|
854
|
-
process.stdout.write(`\n${tag}${BOLD}${color}${role}${RESET}\n`);
|
|
855
|
-
if (m.tool_calls && m.tool_calls.length) {
|
|
856
|
-
for (const tc of m.tool_calls) {
|
|
857
|
-
process.stdout.write(`${DIM} → ${tc.function.name}(${truncateForTranscript(tc.function.arguments, 200)})${RESET}\n`);
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
if (m.tool_call_id) {
|
|
861
|
-
process.stdout.write(`${DIM} ← tool_call_id: ${m.tool_call_id}${RESET}\n`);
|
|
862
|
-
}
|
|
863
|
-
if (content)
|
|
864
|
-
process.stdout.write(content + "\n");
|
|
865
|
-
}
|
|
866
|
-
function truncateForTranscript(s, n) {
|
|
867
|
-
return s.length <= n ? s : s.slice(0, n) + "…";
|
|
868
|
-
}
|
|
869
|
-
function formatRelative(ts) {
|
|
870
|
-
const s = Math.max(0, Math.floor((Date.now() - ts) / 1000));
|
|
871
|
-
if (s < 60)
|
|
872
|
-
return `${s}s ago`;
|
|
873
|
-
const m = Math.floor(s / 60);
|
|
874
|
-
if (m < 60)
|
|
875
|
-
return `${m}m ago`;
|
|
876
|
-
const h = Math.floor(m / 60);
|
|
877
|
-
if (h < 24)
|
|
878
|
-
return `${h}h ago`;
|
|
879
|
-
const d = Math.floor(h / 24);
|
|
880
|
-
return `${d}d ago`;
|
|
881
|
-
}
|
|
882
|
-
/**
|
|
883
|
-
* Read one logical user prompt, supporting bash-style backslash continuation:
|
|
884
|
-
* an odd number of trailing backslashes on a submitted line means "continue
|
|
885
|
-
* on the next line"; the last backslash is consumed and replaced with a
|
|
886
|
-
* literal newline. An even count (e.g. \\) is treated as literal trailing
|
|
887
|
-
* backslashes and the line submits.
|
|
888
|
-
*/
|
|
889
|
-
async function readPromptInput(rl) {
|
|
890
|
-
const FIRST = `${BOLD}> ${RESET}`;
|
|
891
|
-
const CONT = `${DIM}… ${RESET}`;
|
|
892
|
-
let buf = "";
|
|
893
|
-
let prompt = FIRST;
|
|
894
|
-
while (true) {
|
|
895
|
-
const line = await rl.question(prompt);
|
|
896
|
-
const trailing = (line.match(/\\+$/) || [""])[0].length;
|
|
897
|
-
if (trailing % 2 === 1) {
|
|
898
|
-
buf += line.slice(0, -1) + "\n";
|
|
899
|
-
prompt = CONT;
|
|
900
|
-
continue;
|
|
901
|
-
}
|
|
902
|
-
buf += line;
|
|
903
|
-
return buf;
|
|
904
|
-
}
|
|
905
|
-
}
|
|
906
|
-
/**
|
|
907
|
-
* Open $EDITOR (preferring $VISUAL) on a temp .md file seeded with `initial`.
|
|
908
|
-
* Returns the file's contents on save, or null if the editor failed to launch.
|
|
909
|
-
* Empty/whitespace-only results are returned as "" so the caller can decide
|
|
910
|
-
* whether to skip the turn.
|
|
911
|
-
*/
|
|
912
|
-
main().catch((e) => {
|
|
913
|
-
process.stderr.write(`${RED}fatal: ${e.message}${RESET}\n`);
|
|
914
|
-
process.exit(1);
|
|
915
|
-
});
|
|
916
|
-
//# sourceMappingURL=index.js.map
|