@mafdet/cli 0.4.2 → 0.4.4
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 +93 -0
- package/README.md +35 -19
- package/README.zh-CN.md +28 -16
- package/dist/agent/context-budget.d.ts +69 -0
- package/dist/agent/context-budget.d.ts.map +1 -0
- package/dist/agent/context-budget.js +169 -0
- package/dist/agent/loop.d.ts +19 -1
- package/dist/agent/loop.d.ts.map +1 -1
- package/dist/agent/loop.js +32 -0
- package/dist/agent/session.d.ts.map +1 -1
- package/dist/agent/session.js +7 -1
- package/dist/agent/tools/run-command.d.ts +28 -0
- package/dist/agent/tools/run-command.d.ts.map +1 -1
- package/dist/agent/tools/run-command.js +34 -13
- 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 +26 -1
- package/dist/api/sse-parser.d.ts.map +1 -1
- package/dist/api/sse-parser.js +62 -21
- package/dist/commands/agent.d.ts.map +1 -1
- package/dist/commands/agent.js +76 -0
- package/dist/commands/chat.d.ts.map +1 -1
- package/dist/commands/chat.js +136 -19
- 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 +129 -18
- 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/open.d.ts.map +1 -1
- package/dist/sessions/open.js +26 -2
- package/dist/sessions/recorder.d.ts.map +1 -1
- package/dist/sessions/recorder.js +6 -1
- 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 +13 -2
- package/dist/tui/app.d.ts.map +1 -1
- package/dist/tui/app.js +43 -10
- package/dist/tui/start-agent.d.ts +31 -0
- package/dist/tui/start-agent.d.ts.map +1 -1
- package/dist/tui/start-agent.js +147 -45
- package/package.json +1 -1
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
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"open.d.ts","sourceRoot":"","sources":["../../src/sessions/open.ts"],"names":[],"mappings":"AAWA,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;
|
|
1
|
+
{"version":3,"file":"open.d.ts","sourceRoot":"","sources":["../../src/sessions/open.ts"],"names":[],"mappings":"AAWA,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,CAwBR"}
|
package/dist/sessions/open.js
CHANGED
|
@@ -6,12 +6,24 @@
|
|
|
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 { join } from "node:path";
|
|
10
9
|
import { acquireSessionLock, SessionLockedError, } from "./lock.js";
|
|
11
10
|
import { describeBlocker, permissionsForResume, rebuildContext, resumeBlockers, } from "./resume.js";
|
|
11
|
+
import { InvalidSessionIdError, resolveSessionDirectory, } from "./session-id.js";
|
|
12
12
|
import { SessionWriter, readSession } from "./store.js";
|
|
13
13
|
export function openForResume(input) {
|
|
14
|
-
|
|
14
|
+
// F-001 · Through the same boundary as the `sessions` subcommands. This is
|
|
15
|
+
// the write path: before the check, `--resume ../../../x` opened a directory
|
|
16
|
+
// outside the store and appended to it.
|
|
17
|
+
let directory;
|
|
18
|
+
try {
|
|
19
|
+
directory = resolveSessionDirectory(input.root, input.sessionId);
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
if (error instanceof InvalidSessionIdError) {
|
|
23
|
+
return { kind: "unreadable", reason: error.message };
|
|
24
|
+
}
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
15
27
|
if (!existsSync(directory))
|
|
16
28
|
return { kind: "missing" };
|
|
17
29
|
let session;
|
|
@@ -24,6 +36,18 @@ export function openForResume(input) {
|
|
|
24
36
|
reason: error instanceof Error ? error.message : String(error),
|
|
25
37
|
};
|
|
26
38
|
}
|
|
39
|
+
// F-016 · Continuing a conversation with a hole in it sends the model a
|
|
40
|
+
// history that is missing pieces, and it has no way to know. Reading it is
|
|
41
|
+
// fine; carrying on from it needs to be a decision, not a default.
|
|
42
|
+
if (session.corruptLines.length > 0) {
|
|
43
|
+
return {
|
|
44
|
+
kind: "unreadable",
|
|
45
|
+
reason: `${session.corruptLines.length} unreadable line(s) in this transcript ` +
|
|
46
|
+
`(line ${session.corruptLines.join(", ")}) - part of the conversation ` +
|
|
47
|
+
`is missing, so continuing it would send the model an incomplete ` +
|
|
48
|
+
`history. 'mafdet sessions show' still reads what survived.`,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
27
51
|
// Taken before anything is written, so two processes cannot interleave into
|
|
28
52
|
// one transcript.
|
|
29
53
|
let lock;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"recorder.d.ts","sourceRoot":"","sources":["../../src/sessions/recorder.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,OAAO,EAAE,aAAa,EAAqB,MAAM,YAAY,CAAC;AAE9D,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,2DAA2D;IAC3D,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CAChD;AAED;;;;;GAKG;AACH,qBAAa,eAAe;IAId,OAAO,CAAC,QAAQ,CAAC,OAAO;IAHpC,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,KAAK,CAAS;gBAEO,OAAO,EAAE,eAAe;IAErD,IAAI,SAAS,IAAI,MAAM,CAEtB;IAED,wEAAwE;IACxE,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAIxB,OAAO,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI;
|
|
1
|
+
{"version":3,"file":"recorder.d.ts","sourceRoot":"","sources":["../../src/sessions/recorder.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,OAAO,EAAE,aAAa,EAAqB,MAAM,YAAY,CAAC;AAE9D,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,2DAA2D;IAC3D,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CAChD;AAED;;;;;GAKG;AACH,qBAAa,eAAe;IAId,OAAO,CAAC,QAAQ,CAAC,OAAO;IAHpC,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,KAAK,CAAS;gBAEO,OAAO,EAAE,eAAe;IAErD,IAAI,SAAS,IAAI,MAAM,CAEtB;IAED,wEAAwE;IACxE,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAIxB,OAAO,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI;IAyDhC,sEAAsE;IACtE,UAAU,IAAI,IAAI;IAUlB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAU7B,KAAK,IAAI,IAAI;IASb,OAAO,CAAC,KAAK;IAUb;;;;;OAKG;IACH,OAAO,CAAC,IAAI;CAOb"}
|
|
@@ -71,7 +71,12 @@ export class SessionRecorder {
|
|
|
71
71
|
model_id: event.model,
|
|
72
72
|
prompt_tokens: event.promptTokens,
|
|
73
73
|
completion_tokens: event.completionTokens,
|
|
74
|
-
|
|
74
|
+
// F-012 · The loop reports 0 when the catalogue had no price for the
|
|
75
|
+
// model, which is indistinguishable from a genuinely free request.
|
|
76
|
+
// `priced` says which it was, so an unknown is recorded as unknown.
|
|
77
|
+
...(event.priced === false
|
|
78
|
+
? {}
|
|
79
|
+
: { estimated_charge_micro_usd: event.costMicroUsd.toString() }),
|
|
75
80
|
...(this.options.apiKeyRef === undefined
|
|
76
81
|
? {}
|
|
77
82
|
: { api_key_ref: this.options.apiKeyRef }),
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare class InvalidSessionIdError extends Error {
|
|
2
|
+
}
|
|
3
|
+
/**
|
|
4
|
+
* Is this a plain directory name, rather than a path?
|
|
5
|
+
*
|
|
6
|
+
* Exported for tests and for anywhere that wants to check without resolving.
|
|
7
|
+
*/
|
|
8
|
+
export declare function isPlainSessionId(id: string): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* The directory for a session id, or a refusal.
|
|
11
|
+
*
|
|
12
|
+
* @throws InvalidSessionIdError when the id is not a bare name, or when the
|
|
13
|
+
* path it produces would land outside `root`.
|
|
14
|
+
*/
|
|
15
|
+
export declare function resolveSessionDirectory(root: string, id: string): string;
|
|
16
|
+
//# sourceMappingURL=session-id.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-id.d.ts","sourceRoot":"","sources":["../../src/sessions/session-id.ts"],"names":[],"mappings":"AA6BA,qBAAa,qBAAsB,SAAQ,KAAK;CAAG;AAOnD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAOpD;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAkBxE"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// F-001 · The one place a session id becomes a path.
|
|
2
|
+
//
|
|
3
|
+
// Every `sessions` subcommand and `--resume` used to do `join(root, id)` with
|
|
4
|
+
// whatever the user typed. `join` resolves `..`, so an id of `../../../x`
|
|
5
|
+
// pointed outside the store: `show` and `usage` read it, `--resume` appended
|
|
6
|
+
// to it, and `delete --yes` unlinked its files and removed the directory.
|
|
7
|
+
// That last one shipped in 0.3.0 and was a local destructive defect, not a
|
|
8
|
+
// theoretical one - it was reproduced before this was written.
|
|
9
|
+
//
|
|
10
|
+
// The fix is one function that every caller must go through, and two
|
|
11
|
+
// independent checks inside it:
|
|
12
|
+
//
|
|
13
|
+
// 1. Reject anything that is not a bare name. A session id is a directory
|
|
14
|
+
// name and nothing else - no separator, no `.` or `..`, no NUL, not
|
|
15
|
+
// absolute, not a Windows drive or UNC path.
|
|
16
|
+
// 2. Resolve it anyway and require the result to be inside the root. The
|
|
17
|
+
// first check should already be sufficient; this one is what holds if it
|
|
18
|
+
// ever is not, and it is the check that also copes with a root reached
|
|
19
|
+
// through a symlink.
|
|
20
|
+
//
|
|
21
|
+
// Deliberately NOT a `^ses_[0-9a-z]{19}$` format match. That would be tighter
|
|
22
|
+
// against this attack and worse overall: it breaks ids from a future
|
|
23
|
+
// generator, and it turns "no session called that" - the common typo - into a
|
|
24
|
+
// format error. Containment is the property that actually matters, so
|
|
25
|
+
// containment is what is enforced.
|
|
26
|
+
import { realpathSync } from "node:fs";
|
|
27
|
+
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
28
|
+
export class InvalidSessionIdError extends Error {
|
|
29
|
+
}
|
|
30
|
+
/** Separators on either platform, plus the ones a Windows path can carry. */
|
|
31
|
+
const SEPARATORS = /[/\\]/;
|
|
32
|
+
/** `C:` or `\\server\share`, which are absolute even where isAbsolute is not. */
|
|
33
|
+
const WINDOWS_ROOTED = /^(?:[A-Za-z]:|\\\\)/;
|
|
34
|
+
/**
|
|
35
|
+
* Is this a plain directory name, rather than a path?
|
|
36
|
+
*
|
|
37
|
+
* Exported for tests and for anywhere that wants to check without resolving.
|
|
38
|
+
*/
|
|
39
|
+
export function isPlainSessionId(id) {
|
|
40
|
+
if (id.length === 0)
|
|
41
|
+
return false;
|
|
42
|
+
if (id.includes("\0"))
|
|
43
|
+
return false;
|
|
44
|
+
if (SEPARATORS.test(id))
|
|
45
|
+
return false;
|
|
46
|
+
if (id === "." || id === "..")
|
|
47
|
+
return false;
|
|
48
|
+
if (isAbsolute(id) || WINDOWS_ROOTED.test(id))
|
|
49
|
+
return false;
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The directory for a session id, or a refusal.
|
|
54
|
+
*
|
|
55
|
+
* @throws InvalidSessionIdError when the id is not a bare name, or when the
|
|
56
|
+
* path it produces would land outside `root`.
|
|
57
|
+
*/
|
|
58
|
+
export function resolveSessionDirectory(root, id) {
|
|
59
|
+
if (!isPlainSessionId(id)) {
|
|
60
|
+
throw new InvalidSessionIdError(`'${id}' is not a session id. Session ids are plain names like ` +
|
|
61
|
+
`ses_0msod7qr3y83x5k7t7t; run 'mafdet sessions list' to see them.`);
|
|
62
|
+
}
|
|
63
|
+
const directory = join(root, id);
|
|
64
|
+
if (!isInside(root, directory)) {
|
|
65
|
+
// Unreachable given the check above, and kept anyway: this is the check
|
|
66
|
+
// that states the actual property, and the one a future caller that
|
|
67
|
+
// bypasses `isPlainSessionId` would still hit.
|
|
68
|
+
throw new InvalidSessionIdError(`'${id}' would resolve outside the sessions directory`);
|
|
69
|
+
}
|
|
70
|
+
return directory;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Containment against the real path of the root, so a symlinked state
|
|
74
|
+
* directory - `XDG_STATE_HOME` pointing through one is ordinary - does not
|
|
75
|
+
* make an inside path look outside.
|
|
76
|
+
*/
|
|
77
|
+
function isInside(root, candidate) {
|
|
78
|
+
const realRoot = realOrSelf(root);
|
|
79
|
+
const rel = relative(realRoot, realOrSelf(candidate));
|
|
80
|
+
if (rel === "")
|
|
81
|
+
return true;
|
|
82
|
+
return !rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute(rel);
|
|
83
|
+
}
|
|
84
|
+
function realOrSelf(path) {
|
|
85
|
+
try {
|
|
86
|
+
return realpathSync(resolve(path));
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// Not created yet - a fresh machine has no sessions directory, and the
|
|
90
|
+
// lexical form is still the right thing to compare.
|
|
91
|
+
return resolve(path);
|
|
92
|
+
}
|
|
93
|
+
}
|
package/dist/sessions/store.d.ts
CHANGED
|
@@ -25,7 +25,15 @@ export type SessionEvent = {
|
|
|
25
25
|
readonly model_id: string;
|
|
26
26
|
readonly prompt_tokens: number;
|
|
27
27
|
readonly completion_tokens: number;
|
|
28
|
-
|
|
28
|
+
/**
|
|
29
|
+
* F-012 · Integer micro-USD, or absent when we could not price it.
|
|
30
|
+
*
|
|
31
|
+
* Absent and "0" are different claims: one is "we do not know", the
|
|
32
|
+
* other is "it was free". Writing 0 for an unpriced request - a
|
|
33
|
+
* catalogue that was briefly unreachable, or a model the catalogue does
|
|
34
|
+
* not list - made an unknown look like a gift.
|
|
35
|
+
*/
|
|
36
|
+
readonly estimated_charge_micro_usd?: string;
|
|
29
37
|
readonly settlement_status: "pending" | "settled" | "unknown";
|
|
30
38
|
/**
|
|
31
39
|
* Which key paid, as an irreversible digest. Plan §20.3 requires
|
|
@@ -69,6 +77,8 @@ export interface SessionMetadata {
|
|
|
69
77
|
prompt_tokens: number;
|
|
70
78
|
completion_tokens: number;
|
|
71
79
|
estimated_charge_micro_usd: string;
|
|
80
|
+
/** F-012 · How many requests the estimate above does not include. */
|
|
81
|
+
unpriced_requests?: number;
|
|
72
82
|
}
|
|
73
83
|
export declare class SessionStoreError extends Error {
|
|
74
84
|
}
|
|
@@ -126,6 +136,16 @@ export interface ReadSession {
|
|
|
126
136
|
readonly events: readonly StoredEvent[];
|
|
127
137
|
/** Set when the last line was incomplete, which a crash mid-append leaves. */
|
|
128
138
|
readonly truncatedTail: boolean;
|
|
139
|
+
/**
|
|
140
|
+
* F-016 · Line numbers, 1-based, that could not be parsed - not counting the
|
|
141
|
+
* truncated tail above, which has its own well-understood cause.
|
|
142
|
+
*
|
|
143
|
+
* A damaged line in the *middle* of a file is a different thing entirely: it
|
|
144
|
+
* means disk corruption or interleaved writes, and history is missing from
|
|
145
|
+
* the middle of the record. Skipping those in silence let `sessions show`
|
|
146
|
+
* and resume present an incomplete conversation as a complete one.
|
|
147
|
+
*/
|
|
148
|
+
readonly corruptLines: readonly number[];
|
|
129
149
|
}
|
|
130
150
|
/**
|
|
131
151
|
* Read a session back.
|
|
@@ -137,5 +157,24 @@ export interface ReadSession {
|
|
|
137
157
|
export declare function readSession(directory: string): ReadSession;
|
|
138
158
|
/** Session directories present on disk, newest first by mtime of metadata. */
|
|
139
159
|
export declare function listSessionDirectories(root: string): string[];
|
|
140
|
-
|
|
160
|
+
/**
|
|
161
|
+
* F-002 · What actually happened when a session was deleted.
|
|
162
|
+
*
|
|
163
|
+
* The previous version swallowed every `unlink`/`rmdir` failure and returned
|
|
164
|
+
* nothing, so the caller printed "deleted <id>" whether or not anything went.
|
|
165
|
+
* A read-only file, a permission problem or another process holding the
|
|
166
|
+
* directory all produced a cheerful success and a session still on disk.
|
|
167
|
+
*
|
|
168
|
+
* Errors are still not thrown - a partial delete should report what is left,
|
|
169
|
+
* not abort halfway - but they are now returned, and `removed` is the truth.
|
|
170
|
+
*/
|
|
171
|
+
export interface DeleteResult {
|
|
172
|
+
readonly removed: boolean;
|
|
173
|
+
/** Paths that could not be removed, with the reason. */
|
|
174
|
+
readonly failures: readonly {
|
|
175
|
+
path: string;
|
|
176
|
+
reason: string;
|
|
177
|
+
}[];
|
|
178
|
+
}
|
|
179
|
+
export declare function deleteSession(directory: string): DeleteResult;
|
|
141
180
|
//# sourceMappingURL=store.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/sessions/store.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/sessions/store.ts"],"names":[],"mappings":"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"}
|
package/dist/sessions/store.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// - Nothing written here may carry a credential. Everything goes through
|
|
12
12
|
// `redact()` on the way to disk - the forbidden list in §20.5 covers what
|
|
13
13
|
// the CLI holds, not a password the user typed into a prompt.
|
|
14
|
-
import { appendFileSync, lstatSync, mkdirSync, readFileSync, readdirSync, renameSync, rmdirSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
14
|
+
import { appendFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, renameSync, rmdirSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
15
15
|
import { join } from "node:path";
|
|
16
16
|
import { redactDeep } from "../security/redact.js";
|
|
17
17
|
export const SCHEMA_VERSION = 1;
|
|
@@ -150,8 +150,16 @@ export class SessionWriter {
|
|
|
150
150
|
this.meta.prompt_tokens += event.prompt_tokens;
|
|
151
151
|
this.meta.completion_tokens += event.completion_tokens;
|
|
152
152
|
// Money stays integral: strings in, BigInt to add, string out.
|
|
153
|
-
|
|
154
|
-
|
|
153
|
+
// F-012 · An unpriced request adds nothing rather than adding zero, and
|
|
154
|
+
// the count below records that the total is short - so a reader can see
|
|
155
|
+
// the difference between "free" and "we could not work it out".
|
|
156
|
+
if (event.estimated_charge_micro_usd === undefined) {
|
|
157
|
+
this.meta.unpriced_requests = (this.meta.unpriced_requests ?? 0) + 1;
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
this.meta.estimated_charge_micro_usd = (BigInt(this.meta.estimated_charge_micro_usd) +
|
|
161
|
+
BigInt(event.estimated_charge_micro_usd)).toString();
|
|
162
|
+
}
|
|
155
163
|
}
|
|
156
164
|
this.meta.updated_at = this.now().toISOString();
|
|
157
165
|
this.writeMetadata();
|
|
@@ -212,23 +220,27 @@ export function readSession(directory) {
|
|
|
212
220
|
raw = readFileSync(join(directory, "events.jsonl"), "utf8");
|
|
213
221
|
}
|
|
214
222
|
catch {
|
|
215
|
-
return { metadata, events: [], truncatedTail: false };
|
|
223
|
+
return { metadata, events: [], truncatedTail: false, corruptLines: [] };
|
|
216
224
|
}
|
|
217
225
|
const lines = raw.split("\n");
|
|
218
226
|
const trailing = lines.pop();
|
|
219
227
|
const truncatedTail = trailing !== undefined && trailing.length > 0;
|
|
220
228
|
const events = [];
|
|
221
|
-
|
|
229
|
+
const corruptLines = [];
|
|
230
|
+
for (const [index, line] of lines.entries()) {
|
|
222
231
|
if (line.length === 0)
|
|
223
232
|
continue;
|
|
224
233
|
try {
|
|
225
234
|
events.push(JSON.parse(line));
|
|
226
235
|
}
|
|
227
236
|
catch {
|
|
228
|
-
// One unreadable line does not invalidate the rest of the transcript
|
|
237
|
+
// One unreadable line does not invalidate the rest of the transcript,
|
|
238
|
+
// but it is reported: the reader is entitled to know the record has a
|
|
239
|
+
// hole in it before deciding to continue the conversation.
|
|
240
|
+
corruptLines.push(index + 1);
|
|
229
241
|
}
|
|
230
242
|
}
|
|
231
|
-
return { metadata, events, truncatedTail };
|
|
243
|
+
return { metadata, events, truncatedTail, corruptLines };
|
|
232
244
|
}
|
|
233
245
|
/** Session directories present on disk, newest first by mtime of metadata. */
|
|
234
246
|
export function listSessionDirectories(root) {
|
|
@@ -246,27 +258,39 @@ export function listSessionDirectories(root) {
|
|
|
246
258
|
}
|
|
247
259
|
export function deleteSession(directory) {
|
|
248
260
|
assertNotLink(directory);
|
|
249
|
-
|
|
261
|
+
const failures = [];
|
|
262
|
+
const remove = (path, unlink) => {
|
|
250
263
|
try {
|
|
251
|
-
|
|
264
|
+
unlink(path);
|
|
252
265
|
}
|
|
253
|
-
catch {
|
|
254
|
-
|
|
266
|
+
catch (error) {
|
|
267
|
+
const code = error.code;
|
|
268
|
+
// Already gone is the goal, not a failure.
|
|
269
|
+
if (code === "ENOENT")
|
|
270
|
+
return;
|
|
271
|
+
failures.push({
|
|
272
|
+
path,
|
|
273
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
274
|
+
});
|
|
255
275
|
}
|
|
256
|
-
}
|
|
276
|
+
};
|
|
277
|
+
let entries = [];
|
|
257
278
|
try {
|
|
258
|
-
|
|
259
|
-
for (const entry of readdirSync(directory)) {
|
|
260
|
-
unlinkSync(join(directory, entry));
|
|
261
|
-
}
|
|
279
|
+
entries = readdirSync(directory);
|
|
262
280
|
}
|
|
263
|
-
catch {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
281
|
+
catch (error) {
|
|
282
|
+
const code = error.code;
|
|
283
|
+
if (code !== "ENOENT") {
|
|
284
|
+
failures.push({
|
|
285
|
+
path: directory,
|
|
286
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
287
|
+
});
|
|
288
|
+
}
|
|
271
289
|
}
|
|
290
|
+
for (const entry of entries)
|
|
291
|
+
remove(join(directory, entry), unlinkSync);
|
|
292
|
+
remove(directory, rmdirSync);
|
|
293
|
+
// The only claim worth making: is it actually gone?
|
|
294
|
+
const removed = !existsSync(directory);
|
|
295
|
+
return { removed, failures };
|
|
272
296
|
}
|
package/dist/sessions/usage.d.ts
CHANGED
|
@@ -2,6 +2,12 @@ import type { StoredEvent } from "./store.js";
|
|
|
2
2
|
export type SettlementStatus = "settled" | "pending" | "unknown";
|
|
3
3
|
export interface UsageTotals {
|
|
4
4
|
readonly modelRequests: number;
|
|
5
|
+
/**
|
|
6
|
+
* F-012 · Requests whose local estimate could not be computed. Counted so a
|
|
7
|
+
* caller can say the estimate is partial instead of presenting a total that
|
|
8
|
+
* quietly omits them.
|
|
9
|
+
*/
|
|
10
|
+
readonly unpricedRequests: number;
|
|
5
11
|
readonly promptTokens: number;
|
|
6
12
|
readonly completionTokens: number;
|
|
7
13
|
readonly estimatedMicroUsd: bigint;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"usage.d.ts","sourceRoot":"","sources":["../../src/sessions/usage.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,CAAC;AAEjE,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;IACnC,gEAAgE;IAChE,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,YAAY,EAAE,QAAQ,CAAC,MAAM,CAAC,gBAAgB,EAAE,MAAM,CAAC,CAAC,CAAC;CACnE;AAED,MAAM,WAAW,cAAe,SAAQ,WAAW;IACjD,8EAA8E;IAC9E,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,yEAAyE;IACzE,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,OAAO,EAAE,SAAS,CAAC,WAAW,GAAG;QAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,EAAE,CAAC;IAC1E,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC,WAAW,GAAG;QAAE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,EAAE,CAAC;CAC3E;
|
|
1
|
+
{"version":3,"file":"usage.d.ts","sourceRoot":"","sources":["../../src/sessions/usage.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,CAAC;AAEjE,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B;;;;OAIG;IACH,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;IACnC,gEAAgE;IAChE,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,YAAY,EAAE,QAAQ,CAAC,MAAM,CAAC,gBAAgB,EAAE,MAAM,CAAC,CAAC,CAAC;CACnE;AAED,MAAM,WAAW,cAAe,SAAQ,WAAW;IACjD,8EAA8E;IAC9E,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,yEAAyE;IACzE,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,OAAO,EAAE,SAAS,CAAC,WAAW,GAAG;QAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,EAAE,CAAC;IAC1E,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC,WAAW,GAAG;QAAE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,EAAE,CAAC;CAC3E;AAoGD,wBAAgB,cAAc,CAAC,MAAM,EAAE,SAAS,WAAW,EAAE,GAAG,cAAc,CA6D7E;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,GAAG,SAAS,CAcrE"}
|
package/dist/sessions/usage.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
// final charge.
|
|
15
15
|
const ZERO = {
|
|
16
16
|
modelRequests: 0,
|
|
17
|
+
unpricedRequests: 0,
|
|
17
18
|
promptTokens: 0,
|
|
18
19
|
completionTokens: 0,
|
|
19
20
|
estimatedMicroUsd: 0n,
|
|
@@ -42,6 +43,8 @@ function add(base, event, settledMicro) {
|
|
|
42
43
|
const status = statusOf(event, settledMicro !== undefined);
|
|
43
44
|
return {
|
|
44
45
|
modelRequests: base.modelRequests + 1,
|
|
46
|
+
unpricedRequests: base.unpricedRequests +
|
|
47
|
+
(isPriced(event.estimated_charge_micro_usd) ? 0 : 1),
|
|
45
48
|
promptTokens: base.promptTokens + count(event.prompt_tokens),
|
|
46
49
|
completionTokens: base.completionTokens + count(event.completion_tokens),
|
|
47
50
|
estimatedMicroUsd: base.estimatedMicroUsd + micro(event.estimated_charge_micro_usd),
|
|
@@ -76,6 +79,10 @@ function settlementsIn(events) {
|
|
|
76
79
|
function count(value) {
|
|
77
80
|
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
78
81
|
}
|
|
82
|
+
/** Was this request priced at all? Absent is not the same as zero. */
|
|
83
|
+
function isPriced(value) {
|
|
84
|
+
return typeof value === "string" && /^-?\d+$/.test(value);
|
|
85
|
+
}
|
|
79
86
|
/** Money is integral end to end; a malformed figure counts as zero, not NaN. */
|
|
80
87
|
function micro(value) {
|
|
81
88
|
if (typeof value !== "string" || !/^-?\d+$/.test(value))
|
|
@@ -151,7 +158,11 @@ export function settledCaveat(totals) {
|
|
|
151
158
|
parts.push(`${pending} not settled yet`);
|
|
152
159
|
if (unknown > 0)
|
|
153
160
|
parts.push(`${unknown} with no request id, so unknowable`);
|
|
161
|
+
if (totals.unpricedRequests > 0) {
|
|
162
|
+
parts.push(`${totals.unpricedRequests} could not be priced locally, so the estimate is short`);
|
|
163
|
+
}
|
|
154
164
|
if (parts.length === 0)
|
|
155
165
|
return undefined;
|
|
156
|
-
|
|
166
|
+
const covered = totals.modelRequests - totals.unpricedRequests;
|
|
167
|
+
return `${parts.join(", ")}; the estimate covers ${covered} of ${totals.modelRequests}`;
|
|
157
168
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-surface.d.ts","sourceRoot":"","sources":["../../src/tui/agent-surface.ts"],"names":[],"mappings":"AAQA,OAAO,EACL,UAAU,EACV,KAAK,cAAc,EACnB,KAAK,cAAc,EACpB,MAAM,qBAAqB,CAAC;AAI7B,OAAO,EAAW,KAAK,oBAAoB,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"agent-surface.d.ts","sourceRoot":"","sources":["../../src/tui/agent-surface.ts"],"names":[],"mappings":"AAQA,OAAO,EACL,UAAU,EACV,KAAK,cAAc,EACnB,KAAK,cAAc,EACpB,MAAM,qBAAqB,CAAC;AAI7B,OAAO,EAAW,KAAK,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAGhE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC/D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAE7C,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,EAAE,EAAE,QAAQ,CAAC;IACtB,QAAQ,CAAC,YAAY,EAAE,oBAAoB,CAAC;IAC5C,QAAQ,CAAC,MAAM,EAAE;QAAE,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAC;IAClD,0EAA0E;IAC1E,QAAQ,CAAC,OAAO,EAAE,IAAI,CACpB,cAAc,EACd,QAAQ,GAAG,QAAQ,GAAG,UAAU,GAAG,UAAU,GAAG,SAAS,CAC1D,CAAC;IACF;;;;;OAKG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,WAAW,GAAG,SAAS,CAAC;IACnD,wEAAwE;IACxE,QAAQ,CAAC,QAAQ,CAAC,EAAE,eAAe,CAAC;IACpC,yDAAyD;IACzD,QAAQ,CAAC,GAAG,CAAC,EAAE,OAAO,UAAU,CAAC;CAClC;AAED;;;;;;GAMG;AACH,wBAAsB,UAAU,CAC9B,EAAE,EAAE,QAAQ,EACZ,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CA+B7B;AAED,wBAAsB,eAAe,CACnC,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,cAAc,CAAC,CA4EzB;AAED,wBAAgB,aAAa,CAC3B,OAAO,EAAE,cAAc,EACvB,YAAY,EAAE,oBAAoB,GACjC,MAAM,CAgBR"}
|
|
@@ -10,6 +10,7 @@ import { formatUsd } from "../output/money.js";
|
|
|
10
10
|
import { tuiApprover } from "./approval.js";
|
|
11
11
|
import { palette } from "./theme.js";
|
|
12
12
|
import { renderCard, ToolCardLog } from "./tool-card.js";
|
|
13
|
+
import { stripAnsi } from "../agent/command-policy.js";
|
|
13
14
|
/**
|
|
14
15
|
* Read one line of a goal or slash command.
|
|
15
16
|
*
|
|
@@ -67,13 +68,23 @@ export async function runAgentSurface(options) {
|
|
|
67
68
|
// when someone is deciding whether to press Ctrl+C.
|
|
68
69
|
options.stdout.write(dim(` rate limited - waiting ${Math.round(event.waitMs / 1000)}s\n`));
|
|
69
70
|
}
|
|
71
|
+
if (event.type === "compacted") {
|
|
72
|
+
// Said out loud: from here the model can no longer see those results,
|
|
73
|
+
// which is the sort of thing that otherwise looks like it forgot.
|
|
74
|
+
// The saved transcript still has them - only what we send got shorter.
|
|
75
|
+
options.stdout.write(dim(` context near the limit (${event.promptTokens} tokens) - dropped ` +
|
|
76
|
+
`${event.exchanges} earlier tool exchange(s) from what is sent\n`));
|
|
77
|
+
}
|
|
70
78
|
};
|
|
71
79
|
const sessionOptions = {
|
|
72
80
|
...options.session,
|
|
73
|
-
|
|
81
|
+
// F-010 · The model's reply passes through here. The TUI is a terminal by
|
|
82
|
+
// construction, so an OSC or CSI in the answer would be executed rather
|
|
83
|
+
// than shown - retitling the window or clearing what came before.
|
|
84
|
+
stdout: (text) => void options.stdout.write(stripAnsi(text)),
|
|
74
85
|
// The line session sends progress to stderr so it survives redirection.
|
|
75
86
|
// Here everything is on one terminal, so it goes to the same place.
|
|
76
|
-
stderr: (text) => void options.stdout.write(dim(text)),
|
|
87
|
+
stderr: (text) => void options.stdout.write(dim(stripAnsi(text))),
|
|
77
88
|
approver: tuiApprover(io),
|
|
78
89
|
readLine: async () => {
|
|
79
90
|
// Checkpoint between goals rather than per event: the listing only needs
|
package/dist/tui/app.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../../src/tui/app.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AACnD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../../src/tui/app.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AACnD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAcxD,OAAO,EAAwB,KAAK,SAAS,EAAE,MAAM,eAAe,CAAC;AAiBrE,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE;QAAE,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAC;IAClD,QAAQ,CAAC,MAAM,EAAE;QAAE,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAC;IAClD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,UAAU,GAAG;QAAE,KAAK,CAAC,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;IACzE,QAAQ,CAAC,GAAG,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC;IAC3D,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;IACrC,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;IAC9B,wEAAwE;IACxE,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;CAClC;AAED,wBAAsB,MAAM,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,QAAQ,CAAC,CAgKnE"}
|