@mafdet/cli 0.4.3 → 0.4.5
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 +107 -0
- package/README.md +16 -8
- package/README.zh-CN.md +12 -4
- package/dist/agent/command-policy.d.ts.map +1 -1
- package/dist/agent/command-policy.js +10 -1
- package/dist/agent/context-budget.d.ts +47 -0
- package/dist/agent/context-budget.d.ts.map +1 -1
- package/dist/agent/context-budget.js +104 -0
- package/dist/agent/loop.d.ts +16 -1
- package/dist/agent/loop.d.ts.map +1 -1
- package/dist/agent/loop.js +44 -13
- package/dist/agent/session.d.ts +2 -0
- package/dist/agent/session.d.ts.map +1 -1
- package/dist/agent/session.js +14 -3
- package/dist/api/http.d.ts +15 -1
- package/dist/api/http.d.ts.map +1 -1
- package/dist/api/http.js +26 -5
- package/dist/api/sse-parser.d.ts +41 -1
- package/dist/api/sse-parser.d.ts.map +1 -1
- package/dist/api/sse-parser.js +90 -22
- package/dist/commands/agent.d.ts.map +1 -1
- package/dist/commands/agent.js +146 -21
- package/dist/commands/chat.d.ts.map +1 -1
- package/dist/commands/chat.js +21 -4
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +0 -2
- package/dist/commands/runtime.d.ts +6 -0
- package/dist/commands/runtime.d.ts.map +1 -1
- package/dist/commands/sessions.d.ts.map +1 -1
- package/dist/commands/sessions.js +163 -26
- package/dist/config/config.d.ts +0 -12
- package/dist/config/config.d.ts.map +1 -1
- package/dist/config/config.js +31 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -1
- package/dist/output/format.d.ts.map +1 -1
- package/dist/output/format.js +10 -5
- package/dist/output/terminal-safe.d.ts +12 -0
- package/dist/output/terminal-safe.d.ts.map +1 -0
- package/dist/output/terminal-safe.js +38 -0
- package/dist/sessions/index-file.d.ts +2 -0
- package/dist/sessions/index-file.d.ts.map +1 -1
- package/dist/sessions/index-file.js +3 -0
- package/dist/sessions/open.d.ts.map +1 -1
- package/dist/sessions/open.js +29 -3
- package/dist/sessions/recorder.d.ts.map +1 -1
- package/dist/sessions/recorder.js +6 -1
- package/dist/sessions/resume.d.ts +1 -0
- package/dist/sessions/resume.d.ts.map +1 -1
- package/dist/sessions/resume.js +5 -3
- package/dist/sessions/session-id.d.ts +16 -0
- package/dist/sessions/session-id.d.ts.map +1 -0
- package/dist/sessions/session-id.js +93 -0
- package/dist/sessions/store.d.ts +41 -2
- package/dist/sessions/store.d.ts.map +1 -1
- package/dist/sessions/store.js +48 -24
- package/dist/sessions/usage.d.ts +6 -0
- package/dist/sessions/usage.d.ts.map +1 -1
- package/dist/sessions/usage.js +12 -1
- package/dist/tui/agent-surface.d.ts.map +1 -1
- package/dist/tui/agent-surface.js +16 -3
- package/dist/tui/app.d.ts +14 -0
- package/dist/tui/app.d.ts.map +1 -1
- package/dist/tui/app.js +84 -17
- package/dist/tui/picker.d.ts.map +1 -1
- package/dist/tui/picker.js +2 -1
- package/dist/tui/session-centre.d.ts +5 -0
- package/dist/tui/session-centre.d.ts.map +1 -1
- package/dist/tui/session-centre.js +9 -3
- package/dist/tui/start-agent.d.ts +59 -0
- package/dist/tui/start-agent.d.ts.map +1 -1
- package/dist/tui/start-agent.js +182 -50
- package/package.json +1 -1
|
@@ -11,14 +11,15 @@ import { sessionsDir } from "../config/paths.js";
|
|
|
11
11
|
import { formatUsd } from "../output/money.js";
|
|
12
12
|
import { ExitCode } from "../output/exit-codes.js";
|
|
13
13
|
import { parseFormatFlags, renderJson, renderRecord, renderTable, } from "../output/format.js";
|
|
14
|
+
import { safeForTerminal } from "../output/terminal-safe.js";
|
|
14
15
|
import { keyDigest } from "../security/redact.js";
|
|
15
16
|
import { loadIndex, removeIndexEntry } from "../sessions/index-file.js";
|
|
16
17
|
import { acquireSessionLock, SessionLockedError } from "../sessions/lock.js";
|
|
17
18
|
import { daysToCover, matchSettlements, planReconciliation, } from "../sessions/reconcile.js";
|
|
19
|
+
import { InvalidSessionIdError, resolveSessionDirectory, } from "../sessions/session-id.js";
|
|
18
20
|
import { deleteSession, readSession, SessionWriter, } from "../sessions/store.js";
|
|
19
21
|
import { settledCaveat, summariseUsage } from "../sessions/usage.js";
|
|
20
22
|
import { buildRuntime, reportError, requireApiKey, } from "./runtime.js";
|
|
21
|
-
import { join } from "node:path";
|
|
22
23
|
const USAGE = `Usage:
|
|
23
24
|
mafdet sessions list [--json|--plain]
|
|
24
25
|
mafdet sessions show <session-id> [--json]
|
|
@@ -32,6 +33,18 @@ calls a model and is never charged.
|
|
|
32
33
|
cost. It sends request IDs and nothing else - no messages, no titles.
|
|
33
34
|
`;
|
|
34
35
|
export async function sessionsCommand(argv, io) {
|
|
36
|
+
// F-013 · Nothing here may reach the top level as a raw Error. `--json
|
|
37
|
+
// --plain` used to print an ESM stack trace and exit 1 instead of a usage
|
|
38
|
+
// message, and every other command surface already funnels through
|
|
39
|
+
// reportError.
|
|
40
|
+
try {
|
|
41
|
+
return await run(argv, io);
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
return reportError(error, io);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async function run(argv, io) {
|
|
35
48
|
const [sub, ...rest] = argv;
|
|
36
49
|
const root = sessionsDir(io.env ?? process.env);
|
|
37
50
|
if (sub === undefined || sub === "help" || sub === "--help") {
|
|
@@ -50,7 +63,13 @@ export async function sessionsCommand(argv, io) {
|
|
|
50
63
|
return ExitCode.Usage;
|
|
51
64
|
}
|
|
52
65
|
function list(argv, io, root) {
|
|
53
|
-
const { format } = parseFormatFlags(argv);
|
|
66
|
+
const { format, rest } = parseFormatFlags(argv);
|
|
67
|
+
// R-7 · `list` was the one subcommand that threw its leftovers away, so
|
|
68
|
+
// `sessions list --dry-run` exited 0 and listed everything.
|
|
69
|
+
if (rest.length > 0) {
|
|
70
|
+
io.stderr(`mafdet sessions list: unexpected argument '${rest[0]}'\n${USAGE}`);
|
|
71
|
+
return ExitCode.Usage;
|
|
72
|
+
}
|
|
54
73
|
const { sessions } = loadIndex(root);
|
|
55
74
|
if (format === "json") {
|
|
56
75
|
io.stdout(renderJson({
|
|
@@ -67,26 +86,38 @@ function list(argv, io, root) {
|
|
|
67
86
|
return ExitCode.Ok;
|
|
68
87
|
}
|
|
69
88
|
io.stdout(renderTable(sessions, [
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
89
|
+
// F-010 · Every one of these came off disk. A transcript is a file, and
|
|
90
|
+
// a title carrying an OSC sequence would retitle the window of anyone
|
|
91
|
+
// who ran `sessions list`.
|
|
92
|
+
{ header: "SESSION", value: (s) => safe(s.session_id, io) },
|
|
93
|
+
{ header: "TITLE", value: (s) => safe(s.title, io) },
|
|
94
|
+
{ header: "MODEL", value: (s) => safe(s.model_id, io) },
|
|
73
95
|
{ header: "REQUESTS", value: (s) => `${s.model_requests}` },
|
|
74
96
|
{
|
|
75
97
|
header: "EST. COST",
|
|
76
|
-
|
|
98
|
+
// R-6 · An unpriced session is not a free one. `$0.00` in the main
|
|
99
|
+
// column is the claim that matters; a note on stderr cannot correct
|
|
100
|
+
// a number the user has already read.
|
|
101
|
+
value: (s) => s.unpriced_requests
|
|
102
|
+
? "unknown"
|
|
103
|
+
: formatUsd(s.estimated_charge_micro_usd),
|
|
77
104
|
},
|
|
78
105
|
{ header: "UPDATED", value: (s) => s.updated_at },
|
|
79
106
|
]));
|
|
80
107
|
return ExitCode.Ok;
|
|
81
108
|
}
|
|
82
109
|
function show(argv, io, root) {
|
|
83
|
-
const { format } = parseFormatFlags(argv);
|
|
84
|
-
const id =
|
|
110
|
+
const { format, rest } = parseFormatFlags(argv);
|
|
111
|
+
const id = rest[0];
|
|
85
112
|
if (id === undefined) {
|
|
86
113
|
io.stderr(`mafdet sessions show: no session id\n${USAGE}`);
|
|
87
114
|
return ExitCode.Usage;
|
|
88
115
|
}
|
|
89
|
-
|
|
116
|
+
if (!checkExtra("show", rest, io))
|
|
117
|
+
return ExitCode.Usage;
|
|
118
|
+
const directory = locate(root, id, io);
|
|
119
|
+
if (directory === undefined)
|
|
120
|
+
return ExitCode.Usage;
|
|
90
121
|
if (!existsSync(directory)) {
|
|
91
122
|
io.stderr(`mafdet sessions: no session '${id}' on this computer\n`);
|
|
92
123
|
return ExitCode.General;
|
|
@@ -104,37 +135,59 @@ function show(argv, io, root) {
|
|
|
104
135
|
metadata: session.metadata,
|
|
105
136
|
events: session.events,
|
|
106
137
|
truncated_tail: session.truncatedTail,
|
|
138
|
+
corrupt_lines: session.corruptLines,
|
|
107
139
|
}));
|
|
108
140
|
return ExitCode.Ok;
|
|
109
141
|
}
|
|
110
142
|
const m = session.metadata;
|
|
111
143
|
io.stdout(renderRecord([
|
|
112
|
-
["session", m.session_id],
|
|
113
|
-
["title", m.title],
|
|
114
|
-
["model", m.model_id],
|
|
115
|
-
["workspace", m.workspace],
|
|
116
|
-
["mode", m.mode],
|
|
117
|
-
|
|
118
|
-
|
|
144
|
+
["session", safe(m.session_id, io)],
|
|
145
|
+
["title", safe(m.title, io)],
|
|
146
|
+
["model", safe(m.model_id, io)],
|
|
147
|
+
["workspace", safe(m.workspace, io)],
|
|
148
|
+
["mode", safe(m.mode, io)],
|
|
149
|
+
// R-4 · Timestamps come off disk like everything else here; a file that
|
|
150
|
+
// has been edited can carry a sequence in any field, not only the
|
|
151
|
+
// free-text ones.
|
|
152
|
+
["created", safe(m.created_at, io)],
|
|
153
|
+
["updated", safe(m.updated_at, io)],
|
|
119
154
|
["messages", `${m.message_count}`],
|
|
120
155
|
["model requests", `${m.model_requests}`],
|
|
121
156
|
["tokens", `${m.prompt_tokens} in / ${m.completion_tokens} out`],
|
|
122
|
-
[
|
|
157
|
+
[
|
|
158
|
+
"estimated cost",
|
|
159
|
+
m.unpriced_requests
|
|
160
|
+
? `${formatUsd(m.estimated_charge_micro_usd)} + ${m.unpriced_requests} unpriced`
|
|
161
|
+
: formatUsd(m.estimated_charge_micro_usd),
|
|
162
|
+
],
|
|
123
163
|
]));
|
|
124
164
|
if (session.truncatedTail) {
|
|
125
165
|
// Worth saying plainly: the user should know a crash cost them the tail.
|
|
126
166
|
io.stderr("\nnote: the last event was incomplete, most likely an interrupted run.\n");
|
|
127
167
|
}
|
|
168
|
+
if (session.corruptLines.length > 0) {
|
|
169
|
+
// F-016 · A hole in the middle is not an interrupted run - it is
|
|
170
|
+
// corruption or interleaved writes, and part of the conversation is
|
|
171
|
+
// simply gone. Presenting the rest as complete would be a lie.
|
|
172
|
+
io.stderr(`\nwarning: ${session.corruptLines.length} unreadable line(s) in this ` +
|
|
173
|
+
`transcript (line ${session.corruptLines.join(", ")}); ` +
|
|
174
|
+
`some of the conversation is missing.\n`);
|
|
175
|
+
}
|
|
128
176
|
return ExitCode.Ok;
|
|
129
177
|
}
|
|
130
178
|
function remove(argv, io, root) {
|
|
131
|
-
const
|
|
179
|
+
const { rest } = parseFormatFlags(argv);
|
|
180
|
+
const id = rest[0];
|
|
132
181
|
const confirmed = argv.includes("--yes");
|
|
133
182
|
if (id === undefined) {
|
|
134
183
|
io.stderr(`mafdet sessions delete: no session id\n${USAGE}`);
|
|
135
184
|
return ExitCode.Usage;
|
|
136
185
|
}
|
|
137
|
-
|
|
186
|
+
if (!checkExtra("delete", rest, io, ["--yes"]))
|
|
187
|
+
return ExitCode.Usage;
|
|
188
|
+
const directory = locate(root, id, io);
|
|
189
|
+
if (directory === undefined)
|
|
190
|
+
return ExitCode.Usage;
|
|
138
191
|
if (!existsSync(directory)) {
|
|
139
192
|
io.stderr(`mafdet sessions: no session '${id}' on this computer\n`);
|
|
140
193
|
return ExitCode.General;
|
|
@@ -146,11 +199,84 @@ function remove(argv, io, root) {
|
|
|
146
199
|
io.stderr(DELETION_NOTICE(id));
|
|
147
200
|
return ExitCode.Usage;
|
|
148
201
|
}
|
|
149
|
-
|
|
202
|
+
// F-002 · Under the lock: a session open in another window is still writing
|
|
203
|
+
// to this directory, and deleting underneath it leaves the live process to
|
|
204
|
+
// recreate half a session.
|
|
205
|
+
let lock;
|
|
206
|
+
try {
|
|
207
|
+
lock = acquireSessionLock(directory);
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
// Not only SessionLockedError: an unwritable directory throws EACCES here,
|
|
211
|
+
// and letting that escape would print a Node stack trace at the user.
|
|
212
|
+
io.stderr(`mafdet sessions: cannot delete ${id}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
213
|
+
return ExitCode.General;
|
|
214
|
+
}
|
|
215
|
+
let result;
|
|
216
|
+
try {
|
|
217
|
+
result = deleteSession(directory);
|
|
218
|
+
}
|
|
219
|
+
finally {
|
|
220
|
+
// The lock file lives inside the directory, so a successful delete already
|
|
221
|
+
// took it with it; releasing is for the failure path.
|
|
222
|
+
try {
|
|
223
|
+
lock.release();
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
// Gone with the directory.
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (!result.removed) {
|
|
230
|
+
// F-002 · Never print "deleted" for something still on disk.
|
|
231
|
+
io.stderr(`mafdet sessions: ${id} was not fully deleted\n`);
|
|
232
|
+
for (const failure of result.failures) {
|
|
233
|
+
io.stderr(` ${failure.path}: ${failure.reason}\n`);
|
|
234
|
+
}
|
|
235
|
+
// The index is rebuilt from the directories, so leaving the entry keeps
|
|
236
|
+
// the listing honest about what is still there.
|
|
237
|
+
return ExitCode.General;
|
|
238
|
+
}
|
|
150
239
|
removeIndexEntry(root, id);
|
|
151
240
|
io.stdout(`deleted ${id}\n`);
|
|
152
241
|
return ExitCode.Ok;
|
|
153
242
|
}
|
|
243
|
+
/**
|
|
244
|
+
* F-001 · Turn an id into a directory, or say why not.
|
|
245
|
+
*
|
|
246
|
+
* The single boundary: `show`, `usage` and `delete` all come through here, so
|
|
247
|
+
* there is no subcommand left that can build the path its own way. Returns
|
|
248
|
+
* undefined after reporting, because every caller's response is the same.
|
|
249
|
+
*/
|
|
250
|
+
/**
|
|
251
|
+
* F-014 · Refuse what we were not asked to do.
|
|
252
|
+
*
|
|
253
|
+
* A mistyped flag or a stray second argument used to be dropped silently, so
|
|
254
|
+
* `sessions delete a b --yes` deleted `a` and said nothing about `b`, and
|
|
255
|
+
* `--dry-run` looked like it worked. Anything not recognised is an error.
|
|
256
|
+
*/
|
|
257
|
+
/** F-010 · Terminal-safe when a terminal is reading, exact bytes otherwise. */
|
|
258
|
+
function safe(text, io) {
|
|
259
|
+
return safeForTerminal(text, io.isTty ?? process.stdout.isTTY === true);
|
|
260
|
+
}
|
|
261
|
+
function checkExtra(name, rest, io, known = []) {
|
|
262
|
+
const leftover = rest.slice(1).filter((arg) => !known.includes(arg));
|
|
263
|
+
if (leftover.length === 0)
|
|
264
|
+
return true;
|
|
265
|
+
io.stderr(`mafdet sessions ${name}: unexpected argument '${leftover[0]}'\n${USAGE}`);
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
function locate(root, id, io) {
|
|
269
|
+
try {
|
|
270
|
+
return resolveSessionDirectory(root, id);
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
if (error instanceof InvalidSessionIdError) {
|
|
274
|
+
io.stderr(`mafdet sessions: ${error.message}\n`);
|
|
275
|
+
return undefined;
|
|
276
|
+
}
|
|
277
|
+
throw error;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
154
280
|
export const DELETION_NOTICE = (id) => `Deleting ${id} removes local messages and its per-session usage mapping\n` +
|
|
155
281
|
`from this computer.\n\n` +
|
|
156
282
|
`It does not remove API usage, billing records, or charges.\n\n` +
|
|
@@ -170,13 +296,17 @@ export const DELETION_NOTICE = (id) => `Deleting ${id} removes local messages an
|
|
|
170
296
|
* would be a smaller number than the truth.
|
|
171
297
|
*/
|
|
172
298
|
async function usage(argv, io, root) {
|
|
173
|
-
const { format } = parseFormatFlags(argv);
|
|
174
|
-
const id =
|
|
299
|
+
const { format, rest } = parseFormatFlags(argv);
|
|
300
|
+
const id = rest[0];
|
|
175
301
|
if (id === undefined) {
|
|
176
302
|
io.stderr(`mafdet sessions usage: no session id\n${USAGE}`);
|
|
177
303
|
return ExitCode.Usage;
|
|
178
304
|
}
|
|
179
|
-
|
|
305
|
+
if (!checkExtra("usage", rest, io, ["--refresh"]))
|
|
306
|
+
return ExitCode.Usage;
|
|
307
|
+
const directory = locate(root, id, io);
|
|
308
|
+
if (directory === undefined)
|
|
309
|
+
return ExitCode.Usage;
|
|
180
310
|
if (!existsSync(directory)) {
|
|
181
311
|
io.stderr(`mafdet sessions: no session '${id}' on this computer\n`);
|
|
182
312
|
return ExitCode.General;
|
|
@@ -211,6 +341,8 @@ async function usage(argv, io, root) {
|
|
|
211
341
|
completion_tokens: totals.completionTokens,
|
|
212
342
|
// Strings, so no parser turns a charge into a float.
|
|
213
343
|
estimated_charge_micro_usd: totals.estimatedMicroUsd.toString(),
|
|
344
|
+
// R-6 · Without this a consumer reads the total above as complete.
|
|
345
|
+
unpriced_requests: totals.unpricedRequests,
|
|
214
346
|
settled_charge_micro_usd: totals.settledMicroUsd.toString(),
|
|
215
347
|
by_settlement: totals.bySettlement,
|
|
216
348
|
by_model: totals.byModel.map((m) => ({
|
|
@@ -237,7 +369,12 @@ async function usage(argv, io, root) {
|
|
|
237
369
|
`${totals.toolRuns}${totals.toolRunsRefused > 0 ? ` (${totals.toolRunsRefused} refused)` : ""}`,
|
|
238
370
|
],
|
|
239
371
|
["tokens", `${totals.promptTokens} in / ${totals.completionTokens} out`],
|
|
240
|
-
[
|
|
372
|
+
[
|
|
373
|
+
"local estimate",
|
|
374
|
+
totals.unpricedRequests > 0
|
|
375
|
+
? `${formatUsd(totals.estimatedMicroUsd.toString())} + ${totals.unpricedRequests} unpriced`
|
|
376
|
+
: formatUsd(totals.estimatedMicroUsd.toString()),
|
|
377
|
+
],
|
|
241
378
|
];
|
|
242
379
|
if (totals.bySettlement.settled > 0) {
|
|
243
380
|
rows.push([
|
|
@@ -255,7 +392,7 @@ async function usage(argv, io, root) {
|
|
|
255
392
|
if (totals.byModel.length > 1) {
|
|
256
393
|
io.stdout("\nBy model\n");
|
|
257
394
|
io.stdout(renderTable(totals.byModel, [
|
|
258
|
-
{ header: "MODEL", value: (m) => m.modelId },
|
|
395
|
+
{ header: "MODEL", value: (m) => safe(m.modelId, io) },
|
|
259
396
|
{ header: "REQUESTS", value: (m) => `${m.modelRequests}` },
|
|
260
397
|
{
|
|
261
398
|
header: "ESTIMATE",
|
|
@@ -267,7 +404,7 @@ async function usage(argv, io, root) {
|
|
|
267
404
|
// Split per key so two accounts' spending is never presented as one.
|
|
268
405
|
io.stdout("\nBy API key\n");
|
|
269
406
|
io.stdout(renderTable(totals.byKey, [
|
|
270
|
-
{ header: "KEY", value: (k) => k.apiKeyRef },
|
|
407
|
+
{ header: "KEY", value: (k) => safe(k.apiKeyRef, io) },
|
|
271
408
|
{ header: "REQUESTS", value: (k) => `${k.modelRequests}` },
|
|
272
409
|
{
|
|
273
410
|
header: "ESTIMATE",
|
package/dist/config/config.d.ts
CHANGED
|
@@ -6,20 +6,9 @@ export declare class ConfigError extends Error {
|
|
|
6
6
|
}
|
|
7
7
|
declare const projectConfigSchema: z.ZodObject<{
|
|
8
8
|
model: z.ZodOptional<z.ZodString>;
|
|
9
|
-
approvalMode: z.ZodOptional<z.ZodEnum<{
|
|
10
|
-
"read-only": "read-only";
|
|
11
|
-
suggest: "suggest";
|
|
12
|
-
"auto-edit": "auto-edit";
|
|
13
|
-
}>>;
|
|
14
|
-
testCommands: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
15
9
|
}, z.core.$strict>;
|
|
16
10
|
declare const userConfigSchema: z.ZodObject<{
|
|
17
11
|
model: z.ZodOptional<z.ZodString>;
|
|
18
|
-
approvalMode: z.ZodOptional<z.ZodEnum<{
|
|
19
|
-
"read-only": "read-only";
|
|
20
|
-
suggest: "suggest";
|
|
21
|
-
"auto-edit": "auto-edit";
|
|
22
|
-
}>>;
|
|
23
12
|
baseUrl: z.ZodOptional<z.ZodString>;
|
|
24
13
|
backendUrl: z.ZodOptional<z.ZodString>;
|
|
25
14
|
}, z.core.$strict>;
|
|
@@ -48,7 +37,6 @@ export interface ResolvedConfig {
|
|
|
48
37
|
modelSource: "flag" | "env" | "project" | "user" | "none";
|
|
49
38
|
baseUrl: string;
|
|
50
39
|
backendUrl: string;
|
|
51
|
-
approvalMode: "read-only" | "suggest" | "auto-edit";
|
|
52
40
|
apiKey?: string;
|
|
53
41
|
}
|
|
54
42
|
/** Plan §5.1: flag > env > project > user > default. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/config/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,eAAO,MAAM,gBAAgB,6BAA6B,CAAC;AAC3D,eAAO,MAAM,mBAAmB,8BAA8B,CAAC;AAE/D,qBAAa,WAAY,SAAQ,KAAK;gBACxB,OAAO,EAAE,MAAM;CAK5B;
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/config/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,eAAO,MAAM,gBAAgB,6BAA6B,CAAC;AAC3D,eAAO,MAAM,mBAAmB,8BAA8B,CAAC;AAE/D,qBAAa,WAAY,SAAQ,KAAK;gBACxB,OAAO,EAAE,MAAM;CAK5B;AAoCD,QAAA,MAAM,mBAAmB;;kBAId,CAAC;AAEZ,QAAA,MAAM,gBAAgB;;;;kBAMX,CAAC;AAUZ,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAChE,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAE1D;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,GAAG,CAczE;AAOD,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,UAAU,CAWxE;AAED,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,OAAO,EACZ,MAAM,EAAE,MAAM,GACb,aAAa,CAef;AAED,MAAM,WAAW,YAAY;IAC3B,4CAA4C;IAC5C,KAAK,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC3B,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,IAAI,CAAC,EAAE,UAAU,CAAC;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iEAAiE;IACjE,WAAW,EAAE,MAAM,GAAG,KAAK,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,CAAC;IAC1D,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wDAAwD;AACxD,wBAAgB,aAAa,CAAC,KAAK,GAAE,YAAiB,GAAG,cAAc,CA8BtE"}
|
package/dist/config/config.js
CHANGED
|
@@ -16,22 +16,48 @@ export class ConfigError extends Error {
|
|
|
16
16
|
* silently dropping them would hide an attempt to redirect traffic.
|
|
17
17
|
*/
|
|
18
18
|
const FORBIDDEN_IN_PROJECT = ["apiKey", "baseUrl", "backendUrl"];
|
|
19
|
-
|
|
19
|
+
/**
|
|
20
|
+
* F-006 · Accepted, displayed, and never read by anything.
|
|
21
|
+
*
|
|
22
|
+
* `approvalMode` and `testCommands` were in the schema, in `config list` and
|
|
23
|
+
* in the README, and no code consulted either - so a user could set
|
|
24
|
+
* `approvalMode: "auto-edit"` and believe approval had been turned off.
|
|
25
|
+
*
|
|
26
|
+
* They are removed rather than wired up, and the reason is not effort. Both
|
|
27
|
+
* are permission-granting settings, and in a project file that means a cloned
|
|
28
|
+
* repository raising its own permission level: `.mafdet.json` asking for
|
|
29
|
+
* `auto-edit`, or listing its own `testCommands` to run. §8.3 already refuses
|
|
30
|
+
* a global approval flag, and CLI-003 already treats a repository as
|
|
31
|
+
* untrusted input - honouring these would undo both.
|
|
32
|
+
*
|
|
33
|
+
* Rejected with an explanation rather than ignored, so anyone who set one
|
|
34
|
+
* finds out it never did what they thought.
|
|
35
|
+
*/
|
|
36
|
+
const REMOVED_KEYS = {
|
|
37
|
+
approvalMode: "it was never implemented, and a config file may not raise the approval " +
|
|
38
|
+
"level - use --allow-write for this run",
|
|
39
|
+
testCommands: "it was never implemented, and a config file may not grant command " +
|
|
40
|
+
"execution - use --allow-command for this run",
|
|
41
|
+
};
|
|
20
42
|
const projectConfigSchema = z
|
|
21
43
|
.object({
|
|
22
44
|
model: z.string().min(1).optional(),
|
|
23
|
-
approvalMode: approvalMode.optional(),
|
|
24
|
-
testCommands: z.array(z.string().min(1)).optional(),
|
|
25
45
|
})
|
|
26
46
|
.strict();
|
|
27
47
|
const userConfigSchema = z
|
|
28
48
|
.object({
|
|
29
49
|
model: z.string().min(1).optional(),
|
|
30
|
-
approvalMode: approvalMode.optional(),
|
|
31
50
|
baseUrl: z.string().url().optional(),
|
|
32
51
|
backendUrl: z.string().url().optional(),
|
|
33
52
|
})
|
|
34
53
|
.strict();
|
|
54
|
+
function rejectRemoved(raw, source) {
|
|
55
|
+
for (const [key, why] of Object.entries(REMOVED_KEYS)) {
|
|
56
|
+
if (hasKey(raw, key)) {
|
|
57
|
+
throw new ConfigError(`${source} sets '${key}': ${why}.`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
35
61
|
/**
|
|
36
62
|
* Remote endpoints must be HTTPS. Plain HTTP is allowed only against the local
|
|
37
63
|
* machine, where there is no network to intercept — that exception exists so a
|
|
@@ -72,6 +98,7 @@ export function parseProjectConfig(raw, source) {
|
|
|
72
98
|
`Credentials and endpoints come from the environment or your user config only.`);
|
|
73
99
|
}
|
|
74
100
|
}
|
|
101
|
+
rejectRemoved(raw, source);
|
|
75
102
|
const parsed = projectConfigSchema.safeParse(raw);
|
|
76
103
|
if (!parsed.success) {
|
|
77
104
|
throw new ConfigError(`${source} is invalid: ${issueText(parsed.error)}`);
|
|
@@ -99,8 +126,6 @@ export function resolveConfig(input = {}) {
|
|
|
99
126
|
modelSource,
|
|
100
127
|
baseUrl,
|
|
101
128
|
backendUrl,
|
|
102
|
-
// read-only is the safe default; a caller raises it explicitly.
|
|
103
|
-
approvalMode: input.project?.approvalMode ?? input.user?.approvalMode ?? "read-only",
|
|
104
129
|
// The key is environment-only, by design (plan §5.4): a flag would put it
|
|
105
130
|
// in shell history and the process list, a file would outlive its use.
|
|
106
131
|
apiKey: trimmed(env.MAFDET_API_KEY),
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAeA,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAeA,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAmElD,wBAAsB,GAAG,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,QAAQ,CAAC,CAyGpE"}
|
package/dist/index.js
CHANGED
|
@@ -27,8 +27,10 @@ const HELP = `mafdet - Mafdet AI command line
|
|
|
27
27
|
Usage:
|
|
28
28
|
mafdet <command> [options]
|
|
29
29
|
|
|
30
|
+
Run with no arguments in a terminal to open an interactive session.
|
|
31
|
+
|
|
30
32
|
Commands:
|
|
31
|
-
agent
|
|
33
|
+
agent Work in this directory with a model
|
|
32
34
|
chat Send a prompt to a model
|
|
33
35
|
embed Turn text into a vector
|
|
34
36
|
speech Turn text into an audio file
|
|
@@ -39,6 +41,7 @@ Commands:
|
|
|
39
41
|
account subscription Current plan and Playground allowance
|
|
40
42
|
sessions list Conversations saved on this computer
|
|
41
43
|
sessions show One saved conversation in detail
|
|
44
|
+
sessions usage What one conversation used, and what it was charged
|
|
42
45
|
sessions delete Remove a saved conversation from this computer
|
|
43
46
|
config list Show the resolved settings and where each came from
|
|
44
47
|
doctor Check key, endpoints, model, balance and workspace
|
|
@@ -49,6 +52,19 @@ Options:
|
|
|
49
52
|
-h, --help Show this help
|
|
50
53
|
-v, --version Print the version
|
|
51
54
|
|
|
55
|
+
Interactive session (bare 'mafdet'):
|
|
56
|
+
--allow-write Offer file changes, each shown as a diff and approved
|
|
57
|
+
--allow-command <prefix> Allow one command prefix, approved per run
|
|
58
|
+
--read-only Cancel an earlier --allow-write
|
|
59
|
+
--resume <id> Continue a saved conversation
|
|
60
|
+
--title <text> Name this conversation in the listing
|
|
61
|
+
--no-history Do not record this run at all
|
|
62
|
+
--no-tui Do not open the interactive session
|
|
63
|
+
|
|
64
|
+
The agent is read-only unless told otherwise, every file change is shown as a
|
|
65
|
+
diff and approved one at a time, and there is no flag that approves
|
|
66
|
+
everything in advance.
|
|
67
|
+
|
|
52
68
|
Docs: https://docs.mafdet.ai
|
|
53
69
|
`;
|
|
54
70
|
async function readStdin() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../src/output/format.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../src/output/format.ts"],"names":[],"mappings":"AAGA;;;GAGG;AAEH,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,OAAO,GAAG,MAAM,CAAC;AAEtD,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,YAAY,CAAC;IACrB,+DAA+D;IAC/D,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED,2EAA2E;AAC3E,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,WAAW,CAwBrE;AAED,sDAAsD;AACtD,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEjD;AAED,MAAM,WAAW,MAAM,CAAC,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,MAAM,CAAC;IAC1B,+DAA+D;IAC/D,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CAC1B;AAED,wBAAgB,WAAW,CAAC,CAAC,EAC3B,IAAI,EAAE,SAAS,CAAC,EAAE,EAClB,OAAO,EAAE,SAAS,MAAM,CAAC,CAAC,CAAC,EAAE,GAC5B,MAAM,CAoBR;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAC3B,IAAI,EAAE,SAAS,CAAC,EAAE,EAClB,OAAO,EAAE,SAAS,MAAM,CAAC,CAAC,CAAC,EAAE,GAC5B,MAAM,CAOR;AAED,wBAAgB,YAAY,CAC1B,OAAO,EAAE,SAAS,CAAC,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE,GAC9C,MAAM,CAOR"}
|
package/dist/output/format.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* result and nothing else, so `mafdet ... --json | jq` is always valid.
|
|
4
|
-
*/
|
|
1
|
+
import { ApiError } from "../api/errors.js";
|
|
2
|
+
import { ExitCode } from "./exit-codes.js";
|
|
5
3
|
/** `--json` and `--plain` are mutually exclusive; table is the default. */
|
|
6
4
|
export function parseFormatFlags(argv) {
|
|
7
5
|
const rest = [];
|
|
@@ -10,7 +8,14 @@ export function parseFormatFlags(argv) {
|
|
|
10
8
|
for (const arg of argv) {
|
|
11
9
|
if (arg === "--json" || arg === "--plain") {
|
|
12
10
|
if (chosen && chosen !== arg) {
|
|
13
|
-
|
|
11
|
+
// F-013 · A typed error, so the command surface turns this into a
|
|
12
|
+
// usage message and exit 2. A bare Error escaped to the top level and
|
|
13
|
+
// printed a Node stack trace at the user.
|
|
14
|
+
throw new ApiError({
|
|
15
|
+
kind: "request",
|
|
16
|
+
exitCode: ExitCode.Usage,
|
|
17
|
+
message: `${chosen} and ${arg} cannot be combined`,
|
|
18
|
+
});
|
|
14
19
|
}
|
|
15
20
|
chosen = arg;
|
|
16
21
|
format = arg === "--json" ? "json" : "plain";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Make untrusted text safe to print, when and only when a terminal will read
|
|
3
|
+
* it.
|
|
4
|
+
*
|
|
5
|
+
* `isTty` is passed in rather than read from `process.stdout` so that the
|
|
6
|
+
* decision is testable and so a caller writing somewhere else cannot get the
|
|
7
|
+
* wrong answer by accident.
|
|
8
|
+
*/
|
|
9
|
+
export declare function safeForTerminal(text: string, isTty: boolean): string;
|
|
10
|
+
/** Wrap a writer so everything through it is sanitised for a terminal. */
|
|
11
|
+
export declare function terminalSafeWriter(write: (text: string) => void, isTty: boolean): (text: string) => void;
|
|
12
|
+
//# sourceMappingURL=terminal-safe.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"terminal-safe.d.ts","sourceRoot":"","sources":["../../src/output/terminal-safe.ts"],"names":[],"mappings":"AAuBA;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,MAAM,CAEpE;AAED,0EAA0E;AAC1E,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,EAC7B,KAAK,EAAE,OAAO,GACb,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAGxB"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// F-010 · Text we did not write, on its way to a terminal.
|
|
2
|
+
//
|
|
3
|
+
// Control sequences were already stripped from file contents, command output,
|
|
4
|
+
// tool cards and diff previews. Three sources were not covered, and they are
|
|
5
|
+
// the ones a remote party controls most directly:
|
|
6
|
+
//
|
|
7
|
+
// - the model's own reply, streamed straight to stdout;
|
|
8
|
+
// - a session's title, workspace and model id, rendered into tables and
|
|
9
|
+
// pickers - a transcript is a file, and files can be edited;
|
|
10
|
+
// - anything else read back out of the local store.
|
|
11
|
+
//
|
|
12
|
+
// An OSC sequence retitles the window, a CSI can clear the screen or move the
|
|
13
|
+
// cursor to overwrite what came before, and either makes the terminal show
|
|
14
|
+
// something other than what happened.
|
|
15
|
+
//
|
|
16
|
+
// The rule is deliberately about the *destination*, not the source: escapes
|
|
17
|
+
// only do damage when something interprets them. Stripping on the way to a
|
|
18
|
+
// pipe or a file would corrupt `mafdet chat > answer.txt` and any JSON a
|
|
19
|
+
// script parses, for no safety gain - a byte in a file is inert until it is
|
|
20
|
+
// cat'd, and at that point it is that terminal's problem, not a hole we made.
|
|
21
|
+
import { stripAnsi } from "../agent/command-policy.js";
|
|
22
|
+
/**
|
|
23
|
+
* Make untrusted text safe to print, when and only when a terminal will read
|
|
24
|
+
* it.
|
|
25
|
+
*
|
|
26
|
+
* `isTty` is passed in rather than read from `process.stdout` so that the
|
|
27
|
+
* decision is testable and so a caller writing somewhere else cannot get the
|
|
28
|
+
* wrong answer by accident.
|
|
29
|
+
*/
|
|
30
|
+
export function safeForTerminal(text, isTty) {
|
|
31
|
+
return isTty ? stripAnsi(text) : text;
|
|
32
|
+
}
|
|
33
|
+
/** Wrap a writer so everything through it is sanitised for a terminal. */
|
|
34
|
+
export function terminalSafeWriter(write, isTty) {
|
|
35
|
+
if (!isTty)
|
|
36
|
+
return write;
|
|
37
|
+
return (text) => write(stripAnsi(text));
|
|
38
|
+
}
|
|
@@ -7,6 +7,8 @@ export interface IndexEntry {
|
|
|
7
7
|
readonly workspace: string;
|
|
8
8
|
readonly model_requests: number;
|
|
9
9
|
readonly estimated_charge_micro_usd: string;
|
|
10
|
+
/** R-6 · Requests the estimate above does not include; see F-012. */
|
|
11
|
+
readonly unpriced_requests?: number;
|
|
10
12
|
}
|
|
11
13
|
export interface SessionIndex {
|
|
12
14
|
readonly schema_version: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index-file.d.ts","sourceRoot":"","sources":["../../src/sessions/index-file.ts"],"names":[],"mappings":"AAqBA,OAAO,EAIL,KAAK,eAAe,EACrB,MAAM,YAAY,CAAC;AAKpB,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,0BAA0B,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"index-file.d.ts","sourceRoot":"","sources":["../../src/sessions/index-file.ts"],"names":[],"mappings":"AAqBA,OAAO,EAIL,KAAK,eAAe,EACrB,MAAM,YAAY,CAAC;AAKpB,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,0BAA0B,EAAE,MAAM,CAAC;IAC5C,qEAAqE;IACrE,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;CACrC;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,QAAQ,EAAE,SAAS,UAAU,EAAE,CAAC;CAC1C;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE9C;AAsBD;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,CAoBvD;AAED,oEAAoE;AACpE,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,GAAG,IAAI,CAiBlE;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,CAapD;AAwBD,yEAAyE;AACzE,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,eAAe,GACxB,YAAY,CAUd;AAED,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,GAChB,YAAY,CAed;AAED,8EAA8E;AAC9E,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAkBnD"}
|
|
@@ -25,6 +25,9 @@ function entryOf(metadata) {
|
|
|
25
25
|
workspace: metadata.workspace,
|
|
26
26
|
model_requests: metadata.model_requests,
|
|
27
27
|
estimated_charge_micro_usd: metadata.estimated_charge_micro_usd,
|
|
28
|
+
...(metadata.unpriced_requests
|
|
29
|
+
? { unpriced_requests: metadata.unpriced_requests }
|
|
30
|
+
: {}),
|
|
28
31
|
};
|
|
29
32
|
}
|
|
30
33
|
/** Newest first, which is the order a session list wants. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"open.d.ts","sourceRoot":"","sources":["../../src/sessions/open.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"open.d.ts","sourceRoot":"","sources":["../../src/sessions/open.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAGL,KAAK,WAAW,EACjB,MAAM,WAAW,CAAC;AACnB,OAAO,EAEL,oBAAoB,EAGpB,KAAK,aAAa,EACnB,MAAM,aAAa,CAAC;AAKrB,OAAO,EAAE,aAAa,EAAe,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AAE9E,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,0CAA0C;IAC1C,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,QAAQ,CAAC,sBAAsB,EAAE,SAAS,MAAM,EAAE,CAAC;CACpD;AAED,MAAM,MAAM,UAAU,GAClB;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;CAAE,GAC5B;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACpD;IAAE,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACxD;IACE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,eAAe,CAAC;IACnC,QAAQ,CAAC,QAAQ,EAAE,WAAW,EAAE,CAAC;IACjC,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAC3B,0DAA0D;IAC1D,QAAQ,CAAC,QAAQ,EAAE,SAAS,aAAa,EAAE,CAAC;IAC5C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC,QAAQ,CAAC,WAAW,EAAE,UAAU,CAAC,OAAO,oBAAoB,CAAC,CAAC;CAC/D,CAAC;AAEN,wBAAgB,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,UAAU,CAqFnE;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,OAAO,CAAC,UAAU,EAAE;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,CAAC,GAC9C,MAAM,CAyBR"}
|
package/dist/sessions/open.js
CHANGED
|
@@ -6,12 +6,25 @@
|
|
|
6
6
|
// and the checks are all re-derived from the key in use *now*, never from
|
|
7
7
|
// what the session was recorded with.
|
|
8
8
|
import { existsSync } from "node:fs";
|
|
9
|
-
import {
|
|
9
|
+
import { stripAnsi } from "../agent/command-policy.js";
|
|
10
10
|
import { acquireSessionLock, SessionLockedError, } from "./lock.js";
|
|
11
11
|
import { describeBlocker, permissionsForResume, rebuildContext, resumeBlockers, } from "./resume.js";
|
|
12
|
+
import { InvalidSessionIdError, resolveSessionDirectory, } from "./session-id.js";
|
|
12
13
|
import { SessionWriter, readSession } from "./store.js";
|
|
13
14
|
export function openForResume(input) {
|
|
14
|
-
|
|
15
|
+
// F-001 · Through the same boundary as the `sessions` subcommands. This is
|
|
16
|
+
// the write path: before the check, `--resume ../../../x` opened a directory
|
|
17
|
+
// outside the store and appended to it.
|
|
18
|
+
let directory;
|
|
19
|
+
try {
|
|
20
|
+
directory = resolveSessionDirectory(input.root, input.sessionId);
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
if (error instanceof InvalidSessionIdError) {
|
|
24
|
+
return { kind: "unreadable", reason: error.message };
|
|
25
|
+
}
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
15
28
|
if (!existsSync(directory))
|
|
16
29
|
return { kind: "missing" };
|
|
17
30
|
let session;
|
|
@@ -24,6 +37,18 @@ export function openForResume(input) {
|
|
|
24
37
|
reason: error instanceof Error ? error.message : String(error),
|
|
25
38
|
};
|
|
26
39
|
}
|
|
40
|
+
// F-016 · Continuing a conversation with a hole in it sends the model a
|
|
41
|
+
// history that is missing pieces, and it has no way to know. Reading it is
|
|
42
|
+
// fine; carrying on from it needs to be a decision, not a default.
|
|
43
|
+
if (session.corruptLines.length > 0) {
|
|
44
|
+
return {
|
|
45
|
+
kind: "unreadable",
|
|
46
|
+
reason: `${session.corruptLines.length} unreadable line(s) in this transcript ` +
|
|
47
|
+
`(line ${session.corruptLines.join(", ")}) - part of the conversation ` +
|
|
48
|
+
`is missing, so continuing it would send the model an incomplete ` +
|
|
49
|
+
`history. 'mafdet sessions show' still reads what survived.`,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
27
52
|
// Taken before anything is written, so two processes cannot interleave into
|
|
28
53
|
// one transcript.
|
|
29
54
|
let lock;
|
|
@@ -78,8 +103,9 @@ export function openForResume(input) {
|
|
|
78
103
|
* know that will be surprised by the bill rather than by the feature.
|
|
79
104
|
*/
|
|
80
105
|
export function describeResume(result) {
|
|
106
|
+
// R-4 · Title and model come off disk and this is drawn on a terminal.
|
|
81
107
|
const lines = [
|
|
82
|
-
`resuming "${result.metadata.title}" (${result.metadata.model_id})`,
|
|
108
|
+
`resuming "${stripAnsi(result.metadata.title)}" (${stripAnsi(result.metadata.model_id)})`,
|
|
83
109
|
];
|
|
84
110
|
if (result.toolRunsSummarised > 0) {
|
|
85
111
|
lines.push(` ${result.toolRunsSummarised} earlier tool run(s) are summarised, not repeated`);
|