@mafdet/cli 0.4.1 → 0.4.2
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 +34 -0
- package/README.md +61 -0
- package/README.zh-CN.md +52 -0
- package/dist/agent/loop.d.ts +14 -0
- package/dist/agent/loop.d.ts.map +1 -1
- package/dist/agent/loop.js +16 -3
- package/dist/agent/session.d.ts +5 -0
- package/dist/agent/session.d.ts.map +1 -1
- package/dist/agent/session.js +1 -1
- package/dist/agent/tools/read-only.d.ts.map +1 -1
- package/dist/agent/tools/read-only.js +6 -0
- package/dist/api/account.d.ts +43 -0
- package/dist/api/account.d.ts.map +1 -1
- package/dist/api/account.js +40 -0
- package/dist/commands/sessions.d.ts +5 -0
- package/dist/commands/sessions.d.ts.map +1 -0
- package/dist/commands/sessions.js +379 -0
- package/dist/config/paths.d.ts +7 -0
- package/dist/config/paths.d.ts.map +1 -1
- package/dist/config/paths.js +24 -0
- package/dist/filesystem/protected-dirs.d.ts +26 -0
- package/dist/filesystem/protected-dirs.d.ts.map +1 -0
- package/dist/filesystem/protected-dirs.js +78 -0
- package/dist/filesystem/workspace.d.ts +12 -1
- package/dist/filesystem/workspace.d.ts.map +1 -1
- package/dist/filesystem/workspace.js +24 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/security/redact.d.ts +26 -3
- package/dist/security/redact.d.ts.map +1 -1
- package/dist/security/redact.js +48 -3
- package/dist/sessions/index-file.d.ts +38 -0
- package/dist/sessions/index-file.d.ts.map +1 -0
- package/dist/sessions/index-file.js +174 -0
- package/dist/sessions/lock.d.ts +39 -0
- package/dist/sessions/lock.d.ts.map +1 -0
- package/dist/sessions/lock.js +134 -0
- package/dist/sessions/open.d.ts +45 -0
- package/dist/sessions/open.d.ts.map +1 -0
- package/dist/sessions/open.js +96 -0
- package/dist/sessions/policy.d.ts +32 -0
- package/dist/sessions/policy.d.ts.map +1 -0
- package/dist/sessions/policy.js +57 -0
- package/dist/sessions/reconcile.d.ts +49 -0
- package/dist/sessions/reconcile.d.ts.map +1 -0
- package/dist/sessions/reconcile.js +115 -0
- package/dist/sessions/recorder.d.ts +42 -0
- package/dist/sessions/recorder.d.ts.map +1 -0
- package/dist/sessions/recorder.js +147 -0
- package/dist/sessions/resume.d.ts +67 -0
- package/dist/sessions/resume.d.ts.map +1 -0
- package/dist/sessions/resume.js +100 -0
- package/dist/sessions/store.d.ts +141 -0
- package/dist/sessions/store.d.ts.map +1 -0
- package/dist/sessions/store.js +272 -0
- package/dist/sessions/usage.d.ts +35 -0
- package/dist/sessions/usage.d.ts.map +1 -0
- package/dist/sessions/usage.js +157 -0
- package/dist/tui/agent-surface.d.ts +3 -0
- package/dist/tui/agent-surface.d.ts.map +1 -1
- package/dist/tui/agent-surface.js +16 -1
- package/dist/tui/app.d.ts.map +1 -1
- package/dist/tui/app.js +18 -1
- package/dist/tui/route.d.ts.map +1 -1
- package/dist/tui/route.js +19 -2
- package/dist/tui/session-centre.d.ts +15 -0
- package/dist/tui/session-centre.d.ts.map +1 -0
- package/dist/tui/session-centre.js +59 -0
- package/dist/tui/start-agent.d.ts +9 -0
- package/dist/tui/start-agent.d.ts.map +1 -1
- package/dist/tui/start-agent.js +122 -0
- package/package.json +1 -1
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// CLI-025 · Adding up what a session used.
|
|
2
|
+
//
|
|
3
|
+
// This module does arithmetic and nothing else. In particular it never prices
|
|
4
|
+
// anything: it sums the `estimated_charge_micro_usd` each request already
|
|
5
|
+
// recorded, which the agent loop computed with the gateway's own arithmetic.
|
|
6
|
+
// A second price implementation here would drift from the first one, and the
|
|
7
|
+
// first one is the one that was reconciled against the ledger.
|
|
8
|
+
//
|
|
9
|
+
// The four states in §20.7 are the other half. `estimated` is what we worked
|
|
10
|
+
// out locally; `settled` is what the server's ledger says; `pending` means the
|
|
11
|
+
// answer exists but has not been fetched; `unknown` means we never got a
|
|
12
|
+
// request id and so can never ask. They are counted separately because
|
|
13
|
+
// collapsing them would let a display show an estimate as though it were the
|
|
14
|
+
// final charge.
|
|
15
|
+
const ZERO = {
|
|
16
|
+
modelRequests: 0,
|
|
17
|
+
promptTokens: 0,
|
|
18
|
+
completionTokens: 0,
|
|
19
|
+
estimatedMicroUsd: 0n,
|
|
20
|
+
settledMicroUsd: 0n,
|
|
21
|
+
bySettlement: { settled: 0, pending: 0, unknown: 0 },
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* A request with no id can never be reconciled, whatever it says.
|
|
25
|
+
*
|
|
26
|
+
* CLI-001 found that a cancelled or timed-out request may not return one, and
|
|
27
|
+
* calling such a request `pending` would promise an answer that will never
|
|
28
|
+
* arrive.
|
|
29
|
+
*
|
|
30
|
+
* Note what is *not* consulted: the `settlement_status` written on the usage
|
|
31
|
+
* line itself. That records what was known at the moment of the request, which
|
|
32
|
+
* is always "pending" - only a `settlement` event, appended by CLI-027 after
|
|
33
|
+
* the ledger was actually asked, can make a request settled.
|
|
34
|
+
*/
|
|
35
|
+
function statusOf(event, settled) {
|
|
36
|
+
if (typeof event.request_id !== "string" || event.request_id.length === 0) {
|
|
37
|
+
return "unknown";
|
|
38
|
+
}
|
|
39
|
+
return settled ? "settled" : "pending";
|
|
40
|
+
}
|
|
41
|
+
function add(base, event, settledMicro) {
|
|
42
|
+
const status = statusOf(event, settledMicro !== undefined);
|
|
43
|
+
return {
|
|
44
|
+
modelRequests: base.modelRequests + 1,
|
|
45
|
+
promptTokens: base.promptTokens + count(event.prompt_tokens),
|
|
46
|
+
completionTokens: base.completionTokens + count(event.completion_tokens),
|
|
47
|
+
estimatedMicroUsd: base.estimatedMicroUsd + micro(event.estimated_charge_micro_usd),
|
|
48
|
+
settledMicroUsd: status === "settled"
|
|
49
|
+
? base.settledMicroUsd + (settledMicro ?? 0n)
|
|
50
|
+
: base.settledMicroUsd,
|
|
51
|
+
bySettlement: {
|
|
52
|
+
...base.bySettlement,
|
|
53
|
+
[status]: base.bySettlement[status] + 1,
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The settled amount per request id, from the `settlement` events.
|
|
59
|
+
*
|
|
60
|
+
* Read in a first pass because a settlement is appended long after the request
|
|
61
|
+
* it settles. Later wins: re-running `--refresh` after the ledger corrected a
|
|
62
|
+
* charge should show the correction, not the first answer we happened to get.
|
|
63
|
+
*/
|
|
64
|
+
function settlementsIn(events) {
|
|
65
|
+
const found = new Map();
|
|
66
|
+
for (const event of events) {
|
|
67
|
+
if (event.type !== "settlement")
|
|
68
|
+
continue;
|
|
69
|
+
const id = event.request_id;
|
|
70
|
+
if (typeof id !== "string" || id.length === 0)
|
|
71
|
+
continue;
|
|
72
|
+
found.set(id, micro(event.settled_charge_micro_usd));
|
|
73
|
+
}
|
|
74
|
+
return found;
|
|
75
|
+
}
|
|
76
|
+
function count(value) {
|
|
77
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
78
|
+
}
|
|
79
|
+
/** Money is integral end to end; a malformed figure counts as zero, not NaN. */
|
|
80
|
+
function micro(value) {
|
|
81
|
+
if (typeof value !== "string" || !/^-?\d+$/.test(value))
|
|
82
|
+
return 0n;
|
|
83
|
+
return BigInt(value);
|
|
84
|
+
}
|
|
85
|
+
export function summariseUsage(events) {
|
|
86
|
+
let totals = ZERO;
|
|
87
|
+
const byModel = new Map();
|
|
88
|
+
const byKey = new Map();
|
|
89
|
+
const settlements = settlementsIn(events);
|
|
90
|
+
let userMessages = 0;
|
|
91
|
+
let toolRuns = 0;
|
|
92
|
+
let toolRunsRefused = 0;
|
|
93
|
+
let rateLimitHits = 0;
|
|
94
|
+
let rateLimitWaitMs = 0;
|
|
95
|
+
for (const event of events) {
|
|
96
|
+
if (event.type === "user_message") {
|
|
97
|
+
userMessages += 1;
|
|
98
|
+
}
|
|
99
|
+
else if (event.type === "tool_result") {
|
|
100
|
+
toolRuns += 1;
|
|
101
|
+
if (event.status === "refused")
|
|
102
|
+
toolRunsRefused += 1;
|
|
103
|
+
}
|
|
104
|
+
else if (event.type === "rate_limited") {
|
|
105
|
+
rateLimitHits += 1;
|
|
106
|
+
rateLimitWaitMs += count(event.wait_ms);
|
|
107
|
+
}
|
|
108
|
+
else if (event.type === "usage") {
|
|
109
|
+
const usage = event;
|
|
110
|
+
const settled = typeof usage.request_id === "string"
|
|
111
|
+
? settlements.get(usage.request_id)
|
|
112
|
+
: undefined;
|
|
113
|
+
totals = add(totals, usage, settled);
|
|
114
|
+
const model = typeof usage.model_id === "string" ? usage.model_id : "(unknown model)";
|
|
115
|
+
byModel.set(model, add(byModel.get(model) ?? ZERO, usage, settled));
|
|
116
|
+
// Grouped only when the key is known: inventing a bucket for requests
|
|
117
|
+
// recorded before per-key attribution existed would misattribute them.
|
|
118
|
+
if (typeof usage.api_key_ref === "string" &&
|
|
119
|
+
usage.api_key_ref.length > 0) {
|
|
120
|
+
byKey.set(usage.api_key_ref, add(byKey.get(usage.api_key_ref) ?? ZERO, usage, settled));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
...totals,
|
|
126
|
+
userMessages,
|
|
127
|
+
toolRuns,
|
|
128
|
+
toolRunsRefused,
|
|
129
|
+
rateLimitHits,
|
|
130
|
+
rateLimitWaitMs,
|
|
131
|
+
byModel: [...byModel.entries()]
|
|
132
|
+
.map(([modelId, t]) => ({ modelId, ...t }))
|
|
133
|
+
.sort((a, b) => b.modelRequests - a.modelRequests),
|
|
134
|
+
byKey: [...byKey.entries()]
|
|
135
|
+
.map(([apiKeyRef, t]) => ({ apiKeyRef, ...t }))
|
|
136
|
+
.sort((a, b) => b.modelRequests - a.modelRequests),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* How to describe the settled column without overclaiming.
|
|
141
|
+
*
|
|
142
|
+
* Returns undefined when there is nothing honest to say, so a caller renders
|
|
143
|
+
* no figure rather than a zero - a zero reads as "it was free".
|
|
144
|
+
*/
|
|
145
|
+
export function settledCaveat(totals) {
|
|
146
|
+
const { settled, pending, unknown } = totals.bySettlement;
|
|
147
|
+
if (settled === 0 && pending === 0 && unknown === 0)
|
|
148
|
+
return undefined;
|
|
149
|
+
const parts = [];
|
|
150
|
+
if (pending > 0)
|
|
151
|
+
parts.push(`${pending} not settled yet`);
|
|
152
|
+
if (unknown > 0)
|
|
153
|
+
parts.push(`${unknown} with no request id, so unknowable`);
|
|
154
|
+
if (parts.length === 0)
|
|
155
|
+
return undefined;
|
|
156
|
+
return `${parts.join(", ")}; the estimate covers all ${totals.modelRequests}`;
|
|
157
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { runSession, type SessionOptions, type SessionSummary } from "../agent/session.js";
|
|
2
2
|
import { type TerminalCapabilities } from "./theme.js";
|
|
3
|
+
import type { SessionRecorder } from "../sessions/recorder.js";
|
|
3
4
|
import type { WidgetIo } from "./widgets.js";
|
|
4
5
|
export interface AgentSurfaceOptions {
|
|
5
6
|
readonly io: WidgetIo;
|
|
@@ -16,6 +17,8 @@ export interface AgentSurfaceOptions {
|
|
|
16
17
|
* the signal to whatever controller existed at startup.
|
|
17
18
|
*/
|
|
18
19
|
readonly getSignal?: () => AbortSignal | undefined;
|
|
20
|
+
/** Present when this run is being recorded; absent for --no-history. */
|
|
21
|
+
readonly recorder?: SessionRecorder;
|
|
19
22
|
/** Injected by the tests; production uses runSession. */
|
|
20
23
|
readonly run?: typeof runSession;
|
|
21
24
|
}
|
|
@@ -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;AAEhE,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,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,
|
|
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;AAEhE,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,CA8DzB;AAED,wBAAgB,aAAa,CAC3B,OAAO,EAAE,cAAc,EACvB,YAAY,EAAE,oBAAoB,GACjC,MAAM,CAgBR"}
|
|
@@ -56,6 +56,7 @@ export async function runAgentSurface(options) {
|
|
|
56
56
|
const cards = new ToolCardLog();
|
|
57
57
|
const run = options.run ?? runSession;
|
|
58
58
|
const onEvent = (event) => {
|
|
59
|
+
options.recorder?.observe(event);
|
|
59
60
|
const changed = cards.apply(event);
|
|
60
61
|
if (changed !== undefined) {
|
|
61
62
|
options.stdout.write(renderCard(changed, capabilities));
|
|
@@ -74,7 +75,20 @@ export async function runAgentSurface(options) {
|
|
|
74
75
|
// Here everything is on one terminal, so it goes to the same place.
|
|
75
76
|
stderr: (text) => void options.stdout.write(dim(text)),
|
|
76
77
|
approver: tuiApprover(io),
|
|
77
|
-
readLine: () =>
|
|
78
|
+
readLine: async () => {
|
|
79
|
+
// Checkpoint between goals rather than per event: the listing only needs
|
|
80
|
+
// to be current when someone might go and look at it.
|
|
81
|
+
options.recorder?.checkpoint();
|
|
82
|
+
const line = await promptLine(io, "> ");
|
|
83
|
+
// The user's own words never appear in the agent event stream, so they
|
|
84
|
+
// are recorded here. Slash commands are control, not conversation.
|
|
85
|
+
if (line !== undefined &&
|
|
86
|
+
line.trim().length > 0 &&
|
|
87
|
+
!line.startsWith("/")) {
|
|
88
|
+
options.recorder?.goal(line);
|
|
89
|
+
}
|
|
90
|
+
return line;
|
|
91
|
+
},
|
|
78
92
|
onEvent,
|
|
79
93
|
};
|
|
80
94
|
// Installed after the spread, on purpose - see getSignal above.
|
|
@@ -86,6 +100,7 @@ export async function runAgentSurface(options) {
|
|
|
86
100
|
});
|
|
87
101
|
}
|
|
88
102
|
const summary = await run(sessionOptions);
|
|
103
|
+
options.recorder?.close();
|
|
89
104
|
options.stdout.write(renderSummary(summary, capabilities));
|
|
90
105
|
return summary;
|
|
91
106
|
}
|
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;AAaxD,OAAO,EAAwB,KAAK,SAAS,EAAE,MAAM,eAAe,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;AAaxD,OAAO,EAAwB,KAAK,SAAS,EAAE,MAAM,eAAe,CAAC;AAcrE,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,CAiJnE"}
|
package/dist/tui/app.js
CHANGED
|
@@ -16,7 +16,9 @@ import { palette, detectCapabilities, } from "./theme.js";
|
|
|
16
16
|
import { followResize } from "./resize.js";
|
|
17
17
|
import { installTerminalGuard } from "./terminal.js";
|
|
18
18
|
import { inspectWorkspace, renderWorkspaceSummary, } from "./workspace-summary.js";
|
|
19
|
-
import { parsePermissions, startAgent } from "./start-agent.js";
|
|
19
|
+
import { parsePermissions, parseSessionOptions, startAgent, } from "./start-agent.js";
|
|
20
|
+
import { sessionsDir } from "../config/paths.js";
|
|
21
|
+
import { chooseSession } from "./session-centre.js";
|
|
20
22
|
import { select } from "./widgets.js";
|
|
21
23
|
export async function runTui(options) {
|
|
22
24
|
// Live, not a snapshot: a window resized mid-session must not keep laying
|
|
@@ -104,6 +106,17 @@ export async function runTui(options) {
|
|
|
104
106
|
if (chosen === undefined)
|
|
105
107
|
return ExitCode.Cancelled;
|
|
106
108
|
options.stdout.write(renderWorkspaceSummary(inspectWorkspace(options.workspace, existsSync), capabilities));
|
|
109
|
+
// Offered only when there is something to resume, and only when the user
|
|
110
|
+
// did not already say what they wanted on the command line.
|
|
111
|
+
const sessionOptions = parseSessionOptions(options.argv);
|
|
112
|
+
let resumeId = sessionOptions.resumeId;
|
|
113
|
+
if (resumeId === undefined && !sessionOptions.noHistory) {
|
|
114
|
+
const choice = await chooseSession(io, sessionsDir(options.commandIo.env ?? process.env));
|
|
115
|
+
if (choice.kind === "cancelled")
|
|
116
|
+
return ExitCode.Cancelled;
|
|
117
|
+
if (choice.kind === "resume")
|
|
118
|
+
resumeId = choice.sessionId;
|
|
119
|
+
}
|
|
107
120
|
return await startAgent({
|
|
108
121
|
io,
|
|
109
122
|
capabilities,
|
|
@@ -113,6 +126,10 @@ export async function runTui(options) {
|
|
|
113
126
|
model: chosen,
|
|
114
127
|
workspace: options.workspace,
|
|
115
128
|
permissions: parsePermissions(options.argv),
|
|
129
|
+
session: {
|
|
130
|
+
...sessionOptions,
|
|
131
|
+
...(resumeId === undefined ? {} : { resumeId }),
|
|
132
|
+
},
|
|
116
133
|
getSignal: () => {
|
|
117
134
|
inFlight ??= new AbortController();
|
|
118
135
|
return inFlight.signal;
|
package/dist/tui/route.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../../src/tui/route.ts"],"names":[],"mappings":"AAOA,0EAA0E;AAC1E,MAAM,MAAM,UAAU,GAClB,qBAAqB,GACrB,aAAa,GACb,eAAe,GACf,gBAAgB,GAChB,WAAW,GACX,IAAI,CAAC;AAET,MAAM,MAAM,aAAa;AACvB,gDAAgD;AAC9C;IAAE,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAA;CAAE;AAC1B,4EAA4E;GAC1E;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;CAAE;AAC9B,yEAAyE;GACvE;IAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,cAAc,EAAE,UAAU,CAAA;CAAE,CAAC;AAEpE,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,GAAG,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC;CAC5D;
|
|
1
|
+
{"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../../src/tui/route.ts"],"names":[],"mappings":"AAOA,0EAA0E;AAC1E,MAAM,MAAM,UAAU,GAClB,qBAAqB,GACrB,aAAa,GACb,eAAe,GACf,gBAAgB,GAChB,WAAW,GACX,IAAI,CAAC;AAET,MAAM,MAAM,aAAa;AACvB,gDAAgD;AAC9C;IAAE,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAA;CAAE;AAC1B,4EAA4E;GAC1E;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;CAAE;AAC9B,yEAAyE;GACvE;IAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,cAAc,EAAE,UAAU,CAAA;CAAE,CAAC;AAEpE,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,GAAG,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC;CAC5D;AA0CD,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,GAAG,aAAa,CAoBlE;AAED,yEAAyE;AACzE,wBAAgB,cAAc,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,CAezD"}
|
package/dist/tui/route.js
CHANGED
|
@@ -9,9 +9,26 @@
|
|
|
9
9
|
* `mafdet` so that `mafdet --json` can never be answered with a drawing.
|
|
10
10
|
*/
|
|
11
11
|
const MACHINE_FLAGS = new Set(["--json", "--quiet", "-q"]);
|
|
12
|
-
/**
|
|
12
|
+
/** Flags whose next argument is a value, not a command. */
|
|
13
|
+
const FLAGS_TAKING_A_VALUE = new Set([
|
|
14
|
+
"--title",
|
|
15
|
+
"--allow-command",
|
|
16
|
+
"--resume",
|
|
17
|
+
]);
|
|
13
18
|
function hasCommand(argv) {
|
|
14
|
-
|
|
19
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
20
|
+
const arg = argv[i];
|
|
21
|
+
if (arg.startsWith("-")) {
|
|
22
|
+
// `mafdet --title "Fix login"` is still a bare invocation: the value
|
|
23
|
+
// belongs to the flag, and reading it as a command would send the user
|
|
24
|
+
// to "unknown command 'Fix login'".
|
|
25
|
+
if (FLAGS_TAKING_A_VALUE.has(arg))
|
|
26
|
+
i += 1;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
return false;
|
|
15
32
|
}
|
|
16
33
|
/**
|
|
17
34
|
* CI systems attach a pipe often enough that the TTY checks usually catch them,
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type IndexEntry } from "../sessions/index-file.js";
|
|
2
|
+
import { type TerminalCapabilities } from "./theme.js";
|
|
3
|
+
import { type WidgetIo } from "./widgets.js";
|
|
4
|
+
export type SessionChoice = {
|
|
5
|
+
readonly kind: "new";
|
|
6
|
+
} | {
|
|
7
|
+
readonly kind: "resume";
|
|
8
|
+
readonly sessionId: string;
|
|
9
|
+
} | {
|
|
10
|
+
readonly kind: "cancelled";
|
|
11
|
+
};
|
|
12
|
+
export declare function recentSessions(root: string): readonly IndexEntry[];
|
|
13
|
+
export declare function describeSession(entry: IndexEntry, capabilities: TerminalCapabilities): string;
|
|
14
|
+
export declare function chooseSession(io: WidgetIo, root: string): Promise<SessionChoice>;
|
|
15
|
+
//# sourceMappingURL=session-centre.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-centre.d.ts","sourceRoot":"","sources":["../../src/tui/session-centre.ts"],"names":[],"mappings":"AAOA,OAAO,EAAa,KAAK,UAAU,EAAE,MAAM,2BAA2B,CAAC;AACvE,OAAO,EAAW,KAAK,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAChE,OAAO,EAAU,KAAK,QAAQ,EAAE,MAAM,cAAc,CAAC;AAKrD,MAAM,MAAM,aAAa,GACrB;IAAE,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAA;CAAE,GACxB;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GACvD;IAAE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAA;CAAE,CAAC;AAEnC,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,UAAU,EAAE,CAQlE;AAED,wBAAgB,eAAe,CAC7B,KAAK,EAAE,UAAU,EACjB,YAAY,EAAE,oBAAoB,GACjC,MAAM,CAQR;AAOD,wBAAsB,aAAa,CACjC,EAAE,EAAE,QAAQ,EACZ,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,aAAa,CAAC,CAoBxB"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// CLI-024 · New conversation, or one of the recent ones.
|
|
2
|
+
//
|
|
3
|
+
// Offered only when there is something to resume: a menu whose only entry is
|
|
4
|
+
// "New conversation" is a keystroke charged for nothing. Everything here reads
|
|
5
|
+
// the local index and sends no request, so browsing costs nothing (plan 20.2).
|
|
6
|
+
import { formatUsd } from "../output/money.js";
|
|
7
|
+
import { loadIndex } from "../sessions/index-file.js";
|
|
8
|
+
import { palette } from "./theme.js";
|
|
9
|
+
import { select } from "./widgets.js";
|
|
10
|
+
/** How many recent conversations to offer before it stops being a list. */
|
|
11
|
+
const RECENT_LIMIT = 8;
|
|
12
|
+
export function recentSessions(root) {
|
|
13
|
+
try {
|
|
14
|
+
return loadIndex(root).sessions.slice(0, RECENT_LIMIT);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
// A store we cannot read is a store with nothing to offer, not a failure
|
|
18
|
+
// that should stop someone starting a new conversation.
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function describeSession(entry, capabilities) {
|
|
23
|
+
const parts = [
|
|
24
|
+
entry.model_id,
|
|
25
|
+
workspaceName(entry.workspace),
|
|
26
|
+
`${entry.model_requests} request(s)`,
|
|
27
|
+
`~${formatUsd(entry.estimated_charge_micro_usd)}`,
|
|
28
|
+
];
|
|
29
|
+
return palette(capabilities).dim(parts.join(" - "));
|
|
30
|
+
}
|
|
31
|
+
function workspaceName(workspace) {
|
|
32
|
+
const name = workspace.split(/[\\/]/).filter(Boolean).at(-1);
|
|
33
|
+
return name === undefined || name.length === 0 ? workspace : name;
|
|
34
|
+
}
|
|
35
|
+
export async function chooseSession(io, root) {
|
|
36
|
+
const recent = recentSessions(root);
|
|
37
|
+
if (recent.length === 0)
|
|
38
|
+
return { kind: "new" };
|
|
39
|
+
const answer = await select(io, {
|
|
40
|
+
title: "What would you like to do?",
|
|
41
|
+
items: [
|
|
42
|
+
{ value: "", label: "New conversation" },
|
|
43
|
+
...recent.map((entry) => ({
|
|
44
|
+
value: entry.session_id,
|
|
45
|
+
label: entry.title,
|
|
46
|
+
detail: stripStyles(describeSession(entry, io.capabilities)),
|
|
47
|
+
})),
|
|
48
|
+
],
|
|
49
|
+
});
|
|
50
|
+
if (answer.kind === "cancelled")
|
|
51
|
+
return { kind: "cancelled" };
|
|
52
|
+
return answer.value === ""
|
|
53
|
+
? { kind: "new" }
|
|
54
|
+
: { kind: "resume", sessionId: answer.value };
|
|
55
|
+
}
|
|
56
|
+
/** The select widget dims details itself; doubling it up reads as a bug. */
|
|
57
|
+
function stripStyles(text) {
|
|
58
|
+
return text.replace(new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g"), "");
|
|
59
|
+
}
|
|
@@ -3,6 +3,12 @@ import { type CommandIo } from "../commands/runtime.js";
|
|
|
3
3
|
import { ExitCode } from "../output/exit-codes.js";
|
|
4
4
|
import { type TerminalCapabilities } from "./theme.js";
|
|
5
5
|
import type { WidgetIo } from "./widgets.js";
|
|
6
|
+
export interface SessionOptions {
|
|
7
|
+
readonly noHistory: boolean;
|
|
8
|
+
readonly title?: string;
|
|
9
|
+
/** Continue this session rather than starting a new one. */
|
|
10
|
+
readonly resumeId?: string;
|
|
11
|
+
}
|
|
6
12
|
export interface AgentPermissions {
|
|
7
13
|
readonly allowWrite: boolean;
|
|
8
14
|
readonly allowedCommands: readonly string[];
|
|
@@ -13,6 +19,8 @@ export interface AgentPermissions {
|
|
|
13
19
|
* The same spellings the agent command uses, so there is one grammar for
|
|
14
20
|
* granting these and no second one to keep in step.
|
|
15
21
|
*/
|
|
22
|
+
/** `--no-history` and `--title`, read off the bare invocation. */
|
|
23
|
+
export declare function parseSessionOptions(argv: readonly string[]): SessionOptions;
|
|
16
24
|
export declare function parsePermissions(argv: readonly string[]): AgentPermissions;
|
|
17
25
|
export interface StartAgentOptions {
|
|
18
26
|
readonly io: WidgetIo;
|
|
@@ -25,6 +33,7 @@ export interface StartAgentOptions {
|
|
|
25
33
|
readonly model: ResolvedModel;
|
|
26
34
|
readonly workspace: string;
|
|
27
35
|
readonly permissions: AgentPermissions;
|
|
36
|
+
readonly session?: SessionOptions;
|
|
28
37
|
/**
|
|
29
38
|
* Read fresh for every goal. A cancel aborts the request in flight and then
|
|
30
39
|
* swaps in a new controller, so the session survives Ctrl+C instead of
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"start-agent.d.ts","sourceRoot":"","sources":["../../src/tui/start-agent.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAgB,KAAK,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAEtE,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"start-agent.d.ts","sourceRoot":"","sources":["../../src/tui/start-agent.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAgB,KAAK,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAEtE,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAcnD,OAAO,EAAW,KAAK,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAChE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAE7C,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,4DAA4D;IAC5D,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,eAAe,EAAE,SAAS,MAAM,EAAE,CAAC;CAC7C;AAED;;;;;GAKG;AACH,kEAAkE;AAClE,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,cAAc,CAqB3E;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,gBAAgB,CAe1E;AAED,MAAM,WAAW,iBAAiB;IAChC,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,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC;IAC9B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,WAAW,EAAE,gBAAgB,CAAC;IACvC,QAAQ,CAAC,OAAO,CAAC,EAAE,cAAc,CAAC;IAClC;;;;;OAKG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,WAAW,GAAG,SAAS,CAAC;CACpD;AAED,wBAAsB,UAAU,CAC9B,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAAC,QAAQ,CAAC,CAkGnB"}
|
package/dist/tui/start-agent.js
CHANGED
|
@@ -9,6 +9,13 @@ import { buildToolset, READ_ONLY_PROMPT, WRITE_PROMPT, } from "../commands/agent
|
|
|
9
9
|
import { buildRuntime } from "../commands/runtime.js";
|
|
10
10
|
import { Workspace } from "../filesystem/workspace.js";
|
|
11
11
|
import { ExitCode } from "../output/exit-codes.js";
|
|
12
|
+
import { existsSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { sessionsDir } from "../config/paths.js";
|
|
14
|
+
import { keyDigest } from "../security/redact.js";
|
|
15
|
+
import { SessionRecorder } from "../sessions/recorder.js";
|
|
16
|
+
import { SessionWriter } from "../sessions/store.js";
|
|
17
|
+
import { NOTICE_FILE, decideRecording, firstSaveNotice, } from "../sessions/policy.js";
|
|
18
|
+
import { describeResume, openForResume } from "../sessions/open.js";
|
|
12
19
|
import { runAgentSurface } from "./agent-surface.js";
|
|
13
20
|
import { palette } from "./theme.js";
|
|
14
21
|
/**
|
|
@@ -17,6 +24,32 @@ import { palette } from "./theme.js";
|
|
|
17
24
|
* The same spellings the agent command uses, so there is one grammar for
|
|
18
25
|
* granting these and no second one to keep in step.
|
|
19
26
|
*/
|
|
27
|
+
/** `--no-history` and `--title`, read off the bare invocation. */
|
|
28
|
+
export function parseSessionOptions(argv) {
|
|
29
|
+
let title;
|
|
30
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
31
|
+
if (argv[i] !== "--title")
|
|
32
|
+
continue;
|
|
33
|
+
const value = argv[i + 1];
|
|
34
|
+
if (value !== undefined && !value.startsWith("-")) {
|
|
35
|
+
title = value;
|
|
36
|
+
i += 1;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
let resumeId;
|
|
40
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
41
|
+
if (argv[i] !== "--resume")
|
|
42
|
+
continue;
|
|
43
|
+
const value = argv[i + 1];
|
|
44
|
+
if (value !== undefined && !value.startsWith("-"))
|
|
45
|
+
resumeId = value;
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
noHistory: argv.includes("--no-history"),
|
|
49
|
+
...(title === undefined ? {} : { title }),
|
|
50
|
+
...(resumeId === undefined ? {} : { resumeId }),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
20
53
|
export function parsePermissions(argv) {
|
|
21
54
|
let allowWrite = false;
|
|
22
55
|
const allowedCommands = [];
|
|
@@ -58,8 +91,37 @@ export async function startAgent(options) {
|
|
|
58
91
|
? ` - commands: ${options.permissions.allowedCommands.join(", ")}`
|
|
59
92
|
: "") +
|
|
60
93
|
"\n type a goal, or /help for the session commands\n\n"));
|
|
94
|
+
const opened = options.session?.resumeId
|
|
95
|
+
? openForResume({
|
|
96
|
+
root: sessionsDir(options.commandIo.env ?? process.env),
|
|
97
|
+
sessionId: options.session.resumeId,
|
|
98
|
+
systemPrompt: mode === "read-only" ? READ_ONLY_PROMPT : WRITE_PROMPT,
|
|
99
|
+
callable: [options.model.modelId],
|
|
100
|
+
agentCapable: options.model.agentEligible
|
|
101
|
+
? [options.model.modelId]
|
|
102
|
+
: [],
|
|
103
|
+
allowedCommandsThisRun: options.permissions.allowedCommands,
|
|
104
|
+
})
|
|
105
|
+
: undefined;
|
|
106
|
+
if (opened !== undefined && opened.kind !== "opened") {
|
|
107
|
+
options.stdout.write(`\n ${bold("cannot resume that conversation")}: ${opened.kind === "missing"
|
|
108
|
+
? "no session with that id on this computer"
|
|
109
|
+
: opened.reason}\n\n`);
|
|
110
|
+
return ExitCode.General;
|
|
111
|
+
}
|
|
112
|
+
if (opened !== undefined)
|
|
113
|
+
options.stdout.write(dim(describeResume(opened)));
|
|
114
|
+
const recorder = opened !== undefined
|
|
115
|
+
? new SessionRecorder({
|
|
116
|
+
writer: opened.writer,
|
|
117
|
+
root: sessionsDir(options.commandIo.env ?? process.env),
|
|
118
|
+
apiKeyRef: keyDigest(options.apiKey),
|
|
119
|
+
onProblem: (message) => void options.stdout.write(dim(` ${message}\n`)),
|
|
120
|
+
})
|
|
121
|
+
: startRecording(options, mode);
|
|
61
122
|
await runAgentSurface({
|
|
62
123
|
io: options.io,
|
|
124
|
+
...(recorder === undefined ? {} : { recorder }),
|
|
63
125
|
capabilities: options.capabilities,
|
|
64
126
|
stdout: options.stdout,
|
|
65
127
|
...(options.getSignal === undefined
|
|
@@ -77,8 +139,68 @@ export async function startAgent(options) {
|
|
|
77
139
|
allowedCommands: options.permissions.allowedCommands,
|
|
78
140
|
},
|
|
79
141
|
mode,
|
|
142
|
+
// The rebuilt conversation, or nothing for a new session. runSession
|
|
143
|
+
// appends to this array in place, which is what continues the thread.
|
|
144
|
+
...(opened === undefined ? {} : { history: opened.messages }),
|
|
80
145
|
allowedCommands: options.permissions.allowedCommands,
|
|
81
146
|
},
|
|
82
147
|
});
|
|
83
148
|
return ExitCode.Ok;
|
|
84
149
|
}
|
|
150
|
+
/**
|
|
151
|
+
* Open a recorder for this run, or not.
|
|
152
|
+
*
|
|
153
|
+
* Returns undefined for `--no-history`, and - importantly - reaches the
|
|
154
|
+
* filesystem only when it is going to record. A flag that still created an
|
|
155
|
+
* empty directory would be a flag the user was right not to trust.
|
|
156
|
+
*/
|
|
157
|
+
function startRecording(options, mode) {
|
|
158
|
+
const decision = decideRecording({
|
|
159
|
+
surface: "interactive",
|
|
160
|
+
noHistory: options.session?.noHistory ?? false,
|
|
161
|
+
save: false,
|
|
162
|
+
});
|
|
163
|
+
if (!decision.record)
|
|
164
|
+
return undefined;
|
|
165
|
+
const env = options.commandIo.env ?? process.env;
|
|
166
|
+
const root = sessionsDir(env);
|
|
167
|
+
try {
|
|
168
|
+
const writer = SessionWriter.create({
|
|
169
|
+
root,
|
|
170
|
+
title: options.session?.title ?? defaultTitle(options.workspace),
|
|
171
|
+
modelId: options.model.modelId,
|
|
172
|
+
workspace: options.workspace,
|
|
173
|
+
mode,
|
|
174
|
+
});
|
|
175
|
+
showNoticeOnce(root, options);
|
|
176
|
+
return new SessionRecorder({
|
|
177
|
+
writer,
|
|
178
|
+
root,
|
|
179
|
+
apiKeyRef: keyDigest(options.apiKey),
|
|
180
|
+
onProblem: (message) => void options.stdout.write(palette(options.capabilities).dim(` ${message}\n`)),
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
// A store that cannot be opened is worth one line, not a dead session.
|
|
185
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
186
|
+
options.stdout.write(palette(options.capabilities).dim(` not recording this conversation: ${message}\n`));
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/** The directory name is the most useful thing we know before anyone speaks. */
|
|
191
|
+
function defaultTitle(workspace) {
|
|
192
|
+
const name = workspace.split(/[\\/]/).filter(Boolean).at(-1);
|
|
193
|
+
return name === undefined || name.length === 0 ? "Conversation" : name;
|
|
194
|
+
}
|
|
195
|
+
function showNoticeOnce(root, options) {
|
|
196
|
+
const marker = `${root}/${NOTICE_FILE}`;
|
|
197
|
+
try {
|
|
198
|
+
if (existsSync(marker))
|
|
199
|
+
return;
|
|
200
|
+
options.stdout.write(firstSaveNotice(root));
|
|
201
|
+
writeFileSync(marker, `${new Date().toISOString()}\n`, { mode: 0o600 });
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
// Showing it twice is a much smaller problem than failing to start.
|
|
205
|
+
}
|
|
206
|
+
}
|