@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
package/dist/tui/app.js
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
// picker (CLI-020) and the agent surface (CLI-021) attach after onboarding.
|
|
7
7
|
import { existsSync } from "node:fs";
|
|
8
8
|
import { ExitCode } from "../output/exit-codes.js";
|
|
9
|
-
import {
|
|
9
|
+
import { loadConfig } from "../config/load.js";
|
|
10
|
+
import { loadCatalogue, saveDefaultModel, } from "./adapters/models.js";
|
|
10
11
|
import { renderBanner } from "./banner.js";
|
|
11
12
|
import { keyboard } from "./input.js";
|
|
12
13
|
import { FORCED_EXIT_NOTICE, InterruptTracker } from "./interrupt.js";
|
|
@@ -16,11 +17,24 @@ import { palette, detectCapabilities, } from "./theme.js";
|
|
|
16
17
|
import { followResize } from "./resize.js";
|
|
17
18
|
import { installTerminalGuard } from "./terminal.js";
|
|
18
19
|
import { inspectWorkspace, renderWorkspaceSummary, } from "./workspace-summary.js";
|
|
19
|
-
import { parsePermissions, parseSessionOptions, startAgent, } from "./start-agent.js";
|
|
20
|
+
import { checkTuiArguments, parsePermissions, parseSessionOptions, startAgent, TuiArgumentError, TUI_FLAGS, } from "./start-agent.js";
|
|
20
21
|
import { sessionsDir } from "../config/paths.js";
|
|
21
22
|
import { chooseSession } from "./session-centre.js";
|
|
22
23
|
import { select } from "./widgets.js";
|
|
23
24
|
export async function runTui(options) {
|
|
25
|
+
// F-014 · Before anything is drawn: a mistyped flag or a `--title` with no
|
|
26
|
+
// value used to start a session that quietly did something else.
|
|
27
|
+
try {
|
|
28
|
+
checkTuiArguments(options.argv);
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
if (error instanceof TuiArgumentError) {
|
|
32
|
+
options.stdout.write(`mafdet: ${error.message}\n`);
|
|
33
|
+
options.stdout.write(`Supported here: ${TUI_FLAGS.join(", ")}\n`);
|
|
34
|
+
return ExitCode.Usage;
|
|
35
|
+
}
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
24
38
|
// Live, not a snapshot: a window resized mid-session must not keep laying
|
|
25
39
|
// out for the width it used to be.
|
|
26
40
|
const live = followResize(detectCapabilities({
|
|
@@ -96,18 +110,21 @@ export async function runTui(options) {
|
|
|
96
110
|
options.stdout.write("commands that work without an interactive session.\n");
|
|
97
111
|
return ExitCode.Cancelled;
|
|
98
112
|
}
|
|
99
|
-
const
|
|
113
|
+
const chooseOptions = {
|
|
100
114
|
io,
|
|
101
115
|
commandIo: options.commandIo,
|
|
102
116
|
apiKey: auth.apiKey,
|
|
103
117
|
capabilities,
|
|
104
118
|
stdout: options.stdout,
|
|
105
|
-
}
|
|
106
|
-
|
|
119
|
+
};
|
|
120
|
+
const catalogue = await loadModels(chooseOptions);
|
|
121
|
+
if (catalogue === undefined)
|
|
107
122
|
return ExitCode.Cancelled;
|
|
108
123
|
options.stdout.write(renderWorkspaceSummary(inspectWorkspace(options.workspace, existsSync), capabilities));
|
|
109
|
-
//
|
|
110
|
-
//
|
|
124
|
+
// R-9 · The session comes first. Resuming runs on the model the session
|
|
125
|
+
// recorded, so asking for one before knowing whether we are resuming made
|
|
126
|
+
// the user choose - and possibly save as their default - a model the run
|
|
127
|
+
// then ignored, after printing it in the banner as though it were in use.
|
|
111
128
|
const sessionOptions = parseSessionOptions(options.argv);
|
|
112
129
|
let resumeId = sessionOptions.resumeId;
|
|
113
130
|
if (resumeId === undefined && !sessionOptions.noHistory) {
|
|
@@ -117,13 +134,26 @@ export async function runTui(options) {
|
|
|
117
134
|
if (choice.kind === "resume")
|
|
118
135
|
resumeId = choice.sessionId;
|
|
119
136
|
}
|
|
137
|
+
// Only a new conversation needs a model chosen for it.
|
|
138
|
+
const chosen = resumeId === undefined
|
|
139
|
+
? await chooseModel(chooseOptions, catalogue)
|
|
140
|
+
: {
|
|
141
|
+
model: catalogue.agentReady[0] ?? catalogue.chat[0],
|
|
142
|
+
catalogue: catalogue.chat,
|
|
143
|
+
agentCapable: catalogue.agentReady.map((m) => m.modelId),
|
|
144
|
+
};
|
|
145
|
+
if (chosen === undefined || chosen.model === undefined) {
|
|
146
|
+
return ExitCode.Cancelled;
|
|
147
|
+
}
|
|
120
148
|
return await startAgent({
|
|
121
149
|
io,
|
|
122
150
|
capabilities,
|
|
123
151
|
stdout: options.stdout,
|
|
124
152
|
commandIo: options.commandIo,
|
|
125
153
|
apiKey: auth.apiKey,
|
|
126
|
-
model: chosen,
|
|
154
|
+
model: chosen.model,
|
|
155
|
+
catalogue: chosen.catalogue,
|
|
156
|
+
agentCapableModels: chosen.agentCapable,
|
|
127
157
|
workspace: options.workspace,
|
|
128
158
|
permissions: parsePermissions(options.argv),
|
|
129
159
|
session: {
|
|
@@ -143,13 +173,30 @@ export async function runTui(options) {
|
|
|
143
173
|
guard.release();
|
|
144
174
|
}
|
|
145
175
|
}
|
|
176
|
+
/** The model the whole precedence chain settles on: flag > env > project > user. */
|
|
177
|
+
export function resolvedDefaultModel(options) {
|
|
178
|
+
try {
|
|
179
|
+
return loadConfig({
|
|
180
|
+
env: options.commandIo.env,
|
|
181
|
+
cwd: options.commandIo.cwd,
|
|
182
|
+
userConfigFile: options.commandIo.userConfigFile,
|
|
183
|
+
}).model;
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
// A broken config must not stop the picker from opening; it will be
|
|
187
|
+
// reported the moment anything actually needs the configuration.
|
|
188
|
+
return options.commandIo.env?.MAFDET_MODEL;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
146
191
|
/**
|
|
147
|
-
*
|
|
192
|
+
* R-9 · Load and vet the catalogue, without choosing anything yet.
|
|
148
193
|
*
|
|
149
|
-
*
|
|
150
|
-
* the
|
|
194
|
+
* Split from the picker because resuming does not need a choice: the session
|
|
195
|
+
* runs on the model it recorded. Asking for one first made the user pick - and
|
|
196
|
+
* possibly "remember" - a model that the run then ignored, and printed it in
|
|
197
|
+
* the banner as though it were in use.
|
|
151
198
|
*/
|
|
152
|
-
async function
|
|
199
|
+
async function loadModels(options) {
|
|
153
200
|
const { dim, bold } = palette(options.capabilities);
|
|
154
201
|
options.stdout.write(dim(" loading the model catalogue...\n"));
|
|
155
202
|
let catalogue;
|
|
@@ -177,14 +224,30 @@ async function chooseModel(options) {
|
|
|
177
224
|
return undefined;
|
|
178
225
|
}
|
|
179
226
|
if (catalogue.agentReady.length === 0) {
|
|
227
|
+
// R-1 · Refuse, rather than warn and open the gate wider.
|
|
228
|
+
//
|
|
229
|
+
// This printed a notice and then offered *every* chat model, because
|
|
230
|
+
// `agentOnly` was `agentReady.length > 0` - so the one case with nothing
|
|
231
|
+
// safe to offer was the case that offered everything. The whole of this
|
|
232
|
+
// surface is an agent session, and `mafdet agent` already refuses an
|
|
233
|
+
// unverified model outright; the two must not disagree about admission.
|
|
180
234
|
options.stdout.write(noAgentModelsNotice(catalogue.chat.length, options.capabilities));
|
|
235
|
+
return undefined;
|
|
181
236
|
}
|
|
237
|
+
return catalogue;
|
|
238
|
+
}
|
|
239
|
+
async function chooseModel(options, catalogue) {
|
|
240
|
+
const { dim } = palette(options.capabilities);
|
|
182
241
|
const picked = await pickModel(options.io, {
|
|
183
242
|
models: catalogue.chat,
|
|
184
|
-
// Fail closed:
|
|
185
|
-
//
|
|
186
|
-
agentOnly:
|
|
187
|
-
|
|
243
|
+
// Fail closed: only agent-capable models are ever offered here. The
|
|
244
|
+
// empty case is refused in loadModels, so this is never a way in.
|
|
245
|
+
agentOnly: true,
|
|
246
|
+
// F-005 · The resolved model, not just the environment variable. After
|
|
247
|
+
// "Remember X as your default?" wrote X to the user config, the next
|
|
248
|
+
// launch still highlighted whatever MAFDET_MODEL said - usually nothing -
|
|
249
|
+
// so the setting the user had just agreed to appeared not to have worked.
|
|
250
|
+
current: resolvedDefaultModel(options),
|
|
188
251
|
});
|
|
189
252
|
if (picked.kind === "cancelled" || picked.model === undefined)
|
|
190
253
|
return undefined;
|
|
@@ -213,5 +276,9 @@ async function chooseModel(options) {
|
|
|
213
276
|
? dim(` saved to ${saved.path}\n`)
|
|
214
277
|
: dim(` could not save to ${saved.path}: ${saved.problem}\n`));
|
|
215
278
|
}
|
|
216
|
-
return
|
|
279
|
+
return {
|
|
280
|
+
model: picked.model,
|
|
281
|
+
catalogue: catalogue.chat,
|
|
282
|
+
agentCapable: catalogue.agentReady.map((m) => m.modelId),
|
|
283
|
+
};
|
|
217
284
|
}
|
package/dist/tui/picker.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"picker.d.ts","sourceRoot":"","sources":["../../src/tui/picker.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEtD,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AACrC,OAAO,EAAW,KAAK,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAChE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAW7C,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,WAAW,CAAC;IACtC,QAAQ,CAAC,KAAK,CAAC,EAAE,aAAa,CAAC;CAChC;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,SAAS,aAAa,EAAE,CAAC;IAC1C,8CAA8C;IAC9C,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,iDAAiD;IACjD,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,wBAAsB,SAAS,CAC7B,EAAE,EAAE,QAAQ,EACZ,OAAO,EAAE,aAAa,GACrB,OAAO,CAAC,UAAU,CAAC,CAiHrB;AA6BD;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,oBAAoB,GACjC,MAAM,
|
|
1
|
+
{"version":3,"file":"picker.d.ts","sourceRoot":"","sources":["../../src/tui/picker.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEtD,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AACrC,OAAO,EAAW,KAAK,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAChE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAW7C,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,WAAW,CAAC;IACtC,QAAQ,CAAC,KAAK,CAAC,EAAE,aAAa,CAAC;CAChC;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,SAAS,aAAa,EAAE,CAAC;IAC1C,8CAA8C;IAC9C,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,iDAAiD;IACjD,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,wBAAsB,SAAS,CAC7B,EAAE,EAAE,QAAQ,EACZ,OAAO,EAAE,aAAa,GACrB,OAAO,CAAC,UAAU,CAAC,CAiHrB;AA6BD;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,oBAAoB,GACjC,MAAM,CAaR;AAED,iEAAiE;AACjE,YAAY,EAAE,GAAG,EAAE,CAAC"}
|
package/dist/tui/picker.js
CHANGED
|
@@ -138,7 +138,8 @@ export function noAgentModelsNotice(chatCount, capabilities) {
|
|
|
138
138
|
return ([
|
|
139
139
|
"",
|
|
140
140
|
bold(" No model on this key has verified tool calling."),
|
|
141
|
-
dim(
|
|
141
|
+
dim(" The agent needs one, so there is no session to open."),
|
|
142
|
+
dim(` ${chatCount} chat model(s) still work: try 'mafdet chat'.`),
|
|
142
143
|
dim(" Run 'mafdet models list --agent' to see which models qualify,"),
|
|
143
144
|
dim(" or check the key's model permissions in the console."),
|
|
144
145
|
"",
|
|
@@ -10,6 +10,11 @@ export type SessionChoice = {
|
|
|
10
10
|
readonly kind: "cancelled";
|
|
11
11
|
};
|
|
12
12
|
export declare function recentSessions(root: string): readonly IndexEntry[];
|
|
13
|
+
/**
|
|
14
|
+
* R-4 · Every string here came off disk, and this list is drawn on a terminal
|
|
15
|
+
* by definition. A title carrying an OSC sequence would retitle the window of
|
|
16
|
+
* anyone who merely opened the picker - before choosing anything.
|
|
17
|
+
*/
|
|
13
18
|
export declare function describeSession(entry: IndexEntry, capabilities: TerminalCapabilities): string;
|
|
14
19
|
export declare function chooseSession(io: WidgetIo, root: string): Promise<SessionChoice>;
|
|
15
20
|
//# sourceMappingURL=session-centre.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-centre.d.ts","sourceRoot":"","sources":["../../src/tui/session-centre.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"session-centre.d.ts","sourceRoot":"","sources":["../../src/tui/session-centre.ts"],"names":[],"mappings":"AAQA,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;;;;GAIG;AACH,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"}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// Offered only when there is something to resume: a menu whose only entry is
|
|
4
4
|
// "New conversation" is a keystroke charged for nothing. Everything here reads
|
|
5
5
|
// the local index and sends no request, so browsing costs nothing (plan 20.2).
|
|
6
|
+
import { stripAnsi } from "../agent/command-policy.js";
|
|
6
7
|
import { formatUsd } from "../output/money.js";
|
|
7
8
|
import { loadIndex } from "../sessions/index-file.js";
|
|
8
9
|
import { palette } from "./theme.js";
|
|
@@ -19,10 +20,15 @@ export function recentSessions(root) {
|
|
|
19
20
|
return [];
|
|
20
21
|
}
|
|
21
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* R-4 · Every string here came off disk, and this list is drawn on a terminal
|
|
25
|
+
* by definition. A title carrying an OSC sequence would retitle the window of
|
|
26
|
+
* anyone who merely opened the picker - before choosing anything.
|
|
27
|
+
*/
|
|
22
28
|
export function describeSession(entry, capabilities) {
|
|
23
29
|
const parts = [
|
|
24
|
-
entry.model_id,
|
|
25
|
-
workspaceName(entry.workspace),
|
|
30
|
+
stripAnsi(entry.model_id),
|
|
31
|
+
stripAnsi(workspaceName(entry.workspace)),
|
|
26
32
|
`${entry.model_requests} request(s)`,
|
|
27
33
|
`~${formatUsd(entry.estimated_charge_micro_usd)}`,
|
|
28
34
|
];
|
|
@@ -42,7 +48,7 @@ export async function chooseSession(io, root) {
|
|
|
42
48
|
{ value: "", label: "New conversation" },
|
|
43
49
|
...recent.map((entry) => ({
|
|
44
50
|
value: entry.session_id,
|
|
45
|
-
label: entry.title,
|
|
51
|
+
label: stripAnsi(entry.title),
|
|
46
52
|
detail: stripStyles(describeSession(entry, io.capabilities)),
|
|
47
53
|
})),
|
|
48
54
|
],
|
|
@@ -20,7 +20,27 @@ export interface AgentPermissions {
|
|
|
20
20
|
* granting these and no second one to keep in step.
|
|
21
21
|
*/
|
|
22
22
|
/** `--no-history` and `--title`, read off the bare invocation. */
|
|
23
|
+
/** Every flag the bare `mafdet` invocation understands. */
|
|
24
|
+
export declare const TUI_FLAGS: readonly ["--allow-write", "--read-only", "--allow-command", "--no-history", "--no-tui", "--resume", "--title"];
|
|
25
|
+
export declare class TuiArgumentError extends Error {
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* F-014 · Refuse arguments we do not understand.
|
|
29
|
+
*
|
|
30
|
+
* `--title` with no value, or a mistyped `--reusme`, used to be dropped in
|
|
31
|
+
* silence: the session started, looked fine, and did something other than what
|
|
32
|
+
* was asked. That is worse than an error, because nothing tells the user.
|
|
33
|
+
*/
|
|
34
|
+
export declare function checkTuiArguments(argv: readonly string[]): void;
|
|
23
35
|
export declare function parseSessionOptions(argv: readonly string[]): SessionOptions;
|
|
36
|
+
/**
|
|
37
|
+
* F-004 · The same permission syntax as the command surface.
|
|
38
|
+
*
|
|
39
|
+
* `--read-only` existed only in `mafdet agent`, so `mafdet --allow-write
|
|
40
|
+
* --read-only` was read-only there and write-enabled here. A flag whose whole
|
|
41
|
+
* purpose is to take permission away must not be the one the TUI ignores, and
|
|
42
|
+
* last-flag-wins is the only ordering a user can reason about.
|
|
43
|
+
*/
|
|
24
44
|
export declare function parsePermissions(argv: readonly string[]): AgentPermissions;
|
|
25
45
|
export interface StartAgentOptions {
|
|
26
46
|
readonly io: WidgetIo;
|
|
@@ -32,6 +52,17 @@ export interface StartAgentOptions {
|
|
|
32
52
|
readonly apiKey: string;
|
|
33
53
|
readonly model: ResolvedModel;
|
|
34
54
|
readonly workspace: string;
|
|
55
|
+
/**
|
|
56
|
+
* F-003 · Everything this key can reach.
|
|
57
|
+
*
|
|
58
|
+
* Passed in whole rather than as two id lists because a resumed session runs
|
|
59
|
+
* on its *own* recorded model, and that model needs its catalogue entry for
|
|
60
|
+
* pricing. Checking resume against only the model picked moments earlier was
|
|
61
|
+
* asking the wrong question: resume asks whether this key can still call the
|
|
62
|
+
* model the session was recorded with.
|
|
63
|
+
*/
|
|
64
|
+
readonly catalogue?: readonly ResolvedModel[];
|
|
65
|
+
readonly agentCapableModels?: readonly string[];
|
|
35
66
|
readonly permissions: AgentPermissions;
|
|
36
67
|
readonly session?: SessionOptions;
|
|
37
68
|
/**
|
|
@@ -42,5 +73,33 @@ export interface StartAgentOptions {
|
|
|
42
73
|
*/
|
|
43
74
|
readonly getSignal?: () => AbortSignal | undefined;
|
|
44
75
|
}
|
|
76
|
+
export interface RunContextInput {
|
|
77
|
+
/** The metadata of a resumed session, or undefined for a new one. */
|
|
78
|
+
readonly resumed?: {
|
|
79
|
+
readonly workspace: string;
|
|
80
|
+
readonly model_id: string;
|
|
81
|
+
};
|
|
82
|
+
readonly currentWorkspace: string;
|
|
83
|
+
readonly currentModel: ResolvedModel;
|
|
84
|
+
readonly catalogue: readonly ResolvedModel[];
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* F-003 · Where a run happens and which model it uses.
|
|
88
|
+
*
|
|
89
|
+
* A resumed session runs where it was recorded, on the model it was recorded
|
|
90
|
+
* with. It used to run in the current directory on the model picked moments
|
|
91
|
+
* earlier - so continuing project A's conversation from project B pointed the
|
|
92
|
+
* agent at B's files, and with `--allow-write` that meant editing the wrong
|
|
93
|
+
* repository.
|
|
94
|
+
*
|
|
95
|
+
* Falls back to the current model only when the catalogue has no entry for the
|
|
96
|
+
* recorded one. That cannot happen on a run that got this far - an uncallable
|
|
97
|
+
* model is a blocker and blockers now stop the run - but the fallback keeps
|
|
98
|
+
* this total rather than throwing on a case the type system allows.
|
|
99
|
+
*/
|
|
100
|
+
export declare function effectiveRunContext(input: RunContextInput): {
|
|
101
|
+
workspace: string;
|
|
102
|
+
model: ResolvedModel;
|
|
103
|
+
};
|
|
45
104
|
export declare function startAgent(options: StartAgentOptions): Promise<ExitCode>;
|
|
46
105
|
//# sourceMappingURL=start-agent.d.ts.map
|
|
@@ -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;AAgBnD,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,2DAA2D;AAC3D,eAAO,MAAM,SAAS,iHAQZ,CAAC;AASX,qBAAa,gBAAiB,SAAQ,KAAK;CAAG;AAE9C;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAgB/D;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,cAAc,CAiB3E;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,gBAAgB,CAgB1E;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;;;;;;;;OAQG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,aAAa,EAAE,CAAC;IAC9C,QAAQ,CAAC,kBAAkB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAChD,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,MAAM,WAAW,eAAe;IAC9B,qEAAqE;IACrE,QAAQ,CAAC,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IAC7E,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,YAAY,EAAE,aAAa,CAAC;IACrC,QAAQ,CAAC,SAAS,EAAE,SAAS,aAAa,EAAE,CAAC;CAC9C;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,eAAe,GAAG;IAC3D,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,aAAa,CAAC;CACtB,CAUA;AAED,wBAAsB,UAAU,CAC9B,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAAC,QAAQ,CAAC,CAgKnB"}
|
package/dist/tui/start-agent.js
CHANGED
|
@@ -15,7 +15,9 @@ import { keyDigest } from "../security/redact.js";
|
|
|
15
15
|
import { SessionRecorder } from "../sessions/recorder.js";
|
|
16
16
|
import { SessionWriter } from "../sessions/store.js";
|
|
17
17
|
import { NOTICE_FILE, decideRecording, firstSaveNotice, } from "../sessions/policy.js";
|
|
18
|
+
import { stripAnsi } from "../agent/command-policy.js";
|
|
18
19
|
import { describeResume, openForResume } from "../sessions/open.js";
|
|
20
|
+
import { describeBlocker } from "../sessions/resume.js";
|
|
19
21
|
import { runAgentSurface } from "./agent-surface.js";
|
|
20
22
|
import { palette } from "./theme.js";
|
|
21
23
|
/**
|
|
@@ -25,24 +27,64 @@ import { palette } from "./theme.js";
|
|
|
25
27
|
* granting these and no second one to keep in step.
|
|
26
28
|
*/
|
|
27
29
|
/** `--no-history` and `--title`, read off the bare invocation. */
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
/** Every flag the bare `mafdet` invocation understands. */
|
|
31
|
+
export const TUI_FLAGS = [
|
|
32
|
+
"--allow-write",
|
|
33
|
+
"--read-only",
|
|
34
|
+
"--allow-command",
|
|
35
|
+
"--no-history",
|
|
36
|
+
"--no-tui",
|
|
37
|
+
"--resume",
|
|
38
|
+
"--title",
|
|
39
|
+
];
|
|
40
|
+
/** The ones that consume the argument after them. */
|
|
41
|
+
const TUI_FLAGS_WITH_VALUE = new Set([
|
|
42
|
+
"--allow-command",
|
|
43
|
+
"--resume",
|
|
44
|
+
"--title",
|
|
45
|
+
]);
|
|
46
|
+
export class TuiArgumentError extends Error {
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* F-014 · Refuse arguments we do not understand.
|
|
50
|
+
*
|
|
51
|
+
* `--title` with no value, or a mistyped `--reusme`, used to be dropped in
|
|
52
|
+
* silence: the session started, looked fine, and did something other than what
|
|
53
|
+
* was asked. That is worse than an error, because nothing tells the user.
|
|
54
|
+
*/
|
|
55
|
+
export function checkTuiArguments(argv) {
|
|
30
56
|
for (let i = 0; i < argv.length; i += 1) {
|
|
31
|
-
|
|
57
|
+
const arg = argv[i];
|
|
58
|
+
if (!arg.startsWith("-")) {
|
|
59
|
+
throw new TuiArgumentError(`unexpected argument '${arg}'`);
|
|
60
|
+
}
|
|
61
|
+
if (!TUI_FLAGS.includes(arg)) {
|
|
62
|
+
throw new TuiArgumentError(`unknown option '${arg}'`);
|
|
63
|
+
}
|
|
64
|
+
if (!TUI_FLAGS_WITH_VALUE.has(arg))
|
|
32
65
|
continue;
|
|
33
66
|
const value = argv[i + 1];
|
|
34
|
-
if (value
|
|
35
|
-
|
|
36
|
-
i += 1;
|
|
67
|
+
if (value === undefined || value.startsWith("-")) {
|
|
68
|
+
throw new TuiArgumentError(`${arg} needs a value`);
|
|
37
69
|
}
|
|
70
|
+
i += 1;
|
|
38
71
|
}
|
|
72
|
+
}
|
|
73
|
+
export function parseSessionOptions(argv) {
|
|
74
|
+
let title;
|
|
39
75
|
let resumeId;
|
|
40
76
|
for (let i = 0; i < argv.length; i += 1) {
|
|
41
|
-
|
|
77
|
+
const arg = argv[i];
|
|
78
|
+
if (!TUI_FLAGS_WITH_VALUE.has(arg))
|
|
42
79
|
continue;
|
|
43
80
|
const value = argv[i + 1];
|
|
44
|
-
if (value
|
|
81
|
+
if (value === undefined || value.startsWith("-"))
|
|
82
|
+
continue;
|
|
83
|
+
if (arg === "--title")
|
|
84
|
+
title = value;
|
|
85
|
+
if (arg === "--resume")
|
|
45
86
|
resumeId = value;
|
|
87
|
+
i += 1;
|
|
46
88
|
}
|
|
47
89
|
return {
|
|
48
90
|
noHistory: argv.includes("--no-history"),
|
|
@@ -50,6 +92,14 @@ export function parseSessionOptions(argv) {
|
|
|
50
92
|
...(resumeId === undefined ? {} : { resumeId }),
|
|
51
93
|
};
|
|
52
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* F-004 · The same permission syntax as the command surface.
|
|
97
|
+
*
|
|
98
|
+
* `--read-only` existed only in `mafdet agent`, so `mafdet --allow-write
|
|
99
|
+
* --read-only` was read-only there and write-enabled here. A flag whose whole
|
|
100
|
+
* purpose is to take permission away must not be the one the TUI ignores, and
|
|
101
|
+
* last-flag-wins is the only ordering a user can reason about.
|
|
102
|
+
*/
|
|
53
103
|
export function parsePermissions(argv) {
|
|
54
104
|
let allowWrite = false;
|
|
55
105
|
const allowedCommands = [];
|
|
@@ -57,6 +107,8 @@ export function parsePermissions(argv) {
|
|
|
57
107
|
const arg = argv[i];
|
|
58
108
|
if (arg === "--allow-write")
|
|
59
109
|
allowWrite = true;
|
|
110
|
+
else if (arg === "--read-only")
|
|
111
|
+
allowWrite = false;
|
|
60
112
|
else if (arg === "--allow-command") {
|
|
61
113
|
const value = argv[i + 1];
|
|
62
114
|
if (value !== undefined && !value.startsWith("-")) {
|
|
@@ -67,17 +119,32 @@ export function parsePermissions(argv) {
|
|
|
67
119
|
}
|
|
68
120
|
return { allowWrite, allowedCommands };
|
|
69
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* F-003 · Where a run happens and which model it uses.
|
|
124
|
+
*
|
|
125
|
+
* A resumed session runs where it was recorded, on the model it was recorded
|
|
126
|
+
* with. It used to run in the current directory on the model picked moments
|
|
127
|
+
* earlier - so continuing project A's conversation from project B pointed the
|
|
128
|
+
* agent at B's files, and with `--allow-write` that meant editing the wrong
|
|
129
|
+
* repository.
|
|
130
|
+
*
|
|
131
|
+
* Falls back to the current model only when the catalogue has no entry for the
|
|
132
|
+
* recorded one. That cannot happen on a run that got this far - an uncallable
|
|
133
|
+
* model is a blocker and blockers now stop the run - but the fallback keeps
|
|
134
|
+
* this total rather than throwing on a case the type system allows.
|
|
135
|
+
*/
|
|
136
|
+
export function effectiveRunContext(input) {
|
|
137
|
+
if (input.resumed === undefined) {
|
|
138
|
+
return { workspace: input.currentWorkspace, model: input.currentModel };
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
workspace: input.resumed.workspace,
|
|
142
|
+
model: input.catalogue.find((m) => m.modelId === input.resumed?.model_id) ??
|
|
143
|
+
input.currentModel,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
70
146
|
export async function startAgent(options) {
|
|
71
147
|
const { dim, bold } = palette(options.capabilities);
|
|
72
|
-
let workspace;
|
|
73
|
-
try {
|
|
74
|
-
workspace = Workspace.open(options.workspace);
|
|
75
|
-
}
|
|
76
|
-
catch (error) {
|
|
77
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
78
|
-
options.stdout.write(`\n ${bold("cannot use this directory")}: ${message}\n`);
|
|
79
|
-
return ExitCode.General;
|
|
80
|
-
}
|
|
81
148
|
// The TUI is interactive by construction, so `--allow-write` can be honoured
|
|
82
149
|
// here without the non-interactive downgrade the command surface applies.
|
|
83
150
|
const mode = options.permissions.allowWrite ? "suggest" : "read-only";
|
|
@@ -86,20 +153,18 @@ export async function startAgent(options) {
|
|
|
86
153
|
...options.commandIo,
|
|
87
154
|
env: { ...options.commandIo.env, MAFDET_API_KEY: options.apiKey },
|
|
88
155
|
});
|
|
89
|
-
options.stdout.write(dim(`\n ${options.model.modelId} - ${mode}` +
|
|
90
|
-
(hasCommands
|
|
91
|
-
? ` - commands: ${options.permissions.allowedCommands.join(", ")}`
|
|
92
|
-
: "") +
|
|
93
|
-
"\n type a goal, or /help for the session commands\n\n"));
|
|
94
156
|
const opened = options.session?.resumeId
|
|
95
157
|
? openForResume({
|
|
96
158
|
root: sessionsDir(options.commandIo.env ?? process.env),
|
|
97
159
|
sessionId: options.session.resumeId,
|
|
98
160
|
systemPrompt: mode === "read-only" ? READ_ONLY_PROMPT : WRITE_PROMPT,
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
161
|
+
// F-003 · The whole set this key can call, not just the model that
|
|
162
|
+
// happened to be picked before the session was chosen. Checking
|
|
163
|
+
// against a one-element set made every resume of a different model
|
|
164
|
+
// look unavailable, and told the user the wrong thing about why.
|
|
165
|
+
callable: (options.catalogue ?? [options.model]).map((m) => m.modelId),
|
|
166
|
+
agentCapable: options.agentCapableModels ??
|
|
167
|
+
(options.model.agentEligible ? [options.model.modelId] : []),
|
|
103
168
|
allowedCommandsThisRun: options.permissions.allowedCommands,
|
|
104
169
|
})
|
|
105
170
|
: undefined;
|
|
@@ -111,6 +176,54 @@ export async function startAgent(options) {
|
|
|
111
176
|
}
|
|
112
177
|
if (opened !== undefined)
|
|
113
178
|
options.stdout.write(dim(describeResume(opened)));
|
|
179
|
+
// F-003 · A blocker used to be printed and then ignored, so a resume with an
|
|
180
|
+
// uncallable model or a deleted workspace started anyway. Resuming into a
|
|
181
|
+
// different directory is the worst of them: with writing enabled the agent
|
|
182
|
+
// would carry one project's context into another project's files.
|
|
183
|
+
if (opened !== undefined && opened.blockers.length > 0) {
|
|
184
|
+
options.stdout.write(`\n ${bold("cannot continue this conversation yet")}:\n` +
|
|
185
|
+
opened.blockers
|
|
186
|
+
.map((blocker) => ` - ${describeBlocker(blocker)}\n`)
|
|
187
|
+
.join("") +
|
|
188
|
+
`\n You can still read it with 'mafdet sessions show ${options.session?.resumeId ?? ""}'.\n\n`);
|
|
189
|
+
opened.writer.close();
|
|
190
|
+
opened.lock.release();
|
|
191
|
+
return ExitCode.General;
|
|
192
|
+
}
|
|
193
|
+
// F-003 · A resumed session runs where it was recorded and on the model it
|
|
194
|
+
// was recorded with. Continuing project A's conversation against project B's
|
|
195
|
+
// files is how an agent with write access edits the wrong repository - and
|
|
196
|
+
// it used to be the default, because the workspace came from the cwd.
|
|
197
|
+
const { workspace: effectiveWorkspace, model: effectiveModel } = effectiveRunContext({
|
|
198
|
+
resumed: opened?.metadata,
|
|
199
|
+
currentWorkspace: options.workspace,
|
|
200
|
+
currentModel: options.model,
|
|
201
|
+
catalogue: options.catalogue ?? [],
|
|
202
|
+
});
|
|
203
|
+
let workspace;
|
|
204
|
+
try {
|
|
205
|
+
workspace = Workspace.open(effectiveWorkspace);
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
209
|
+
options.stdout.write(`\n ${bold("cannot use this directory")}: ${message}\n`);
|
|
210
|
+
if (opened !== undefined) {
|
|
211
|
+
opened.writer.close();
|
|
212
|
+
opened.lock.release();
|
|
213
|
+
}
|
|
214
|
+
return ExitCode.General;
|
|
215
|
+
}
|
|
216
|
+
if (opened !== undefined && effectiveWorkspace !== options.workspace) {
|
|
217
|
+
options.stdout.write(dim(` continuing in ${effectiveWorkspace}, where it was recorded\n`));
|
|
218
|
+
}
|
|
219
|
+
// R-9 · Printed here, not before the session was resolved: on a resume the
|
|
220
|
+
// model in use is the one the session recorded, and announcing the model
|
|
221
|
+
// picked beforehand named something that was never going to be used.
|
|
222
|
+
options.stdout.write(dim(`\n ${stripAnsi(effectiveModel.modelId)} - ${mode}` +
|
|
223
|
+
(hasCommands
|
|
224
|
+
? ` - commands: ${options.permissions.allowedCommands.join(", ")}`
|
|
225
|
+
: "") +
|
|
226
|
+
"\n type a goal, or /help for the session commands\n\n"));
|
|
114
227
|
const recorder = opened !== undefined
|
|
115
228
|
? new SessionRecorder({
|
|
116
229
|
writer: opened.writer,
|
|
@@ -119,32 +232,51 @@ export async function startAgent(options) {
|
|
|
119
232
|
onProblem: (message) => void options.stdout.write(dim(` ${message}\n`)),
|
|
120
233
|
})
|
|
121
234
|
: startRecording(options, mode);
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
235
|
+
// F-015 · The resume lock lives for exactly the run that holds it. It used
|
|
236
|
+
// to be taken and never released, so the lock file survived until the next
|
|
237
|
+
// process judged the pid dead - and on a shared filesystem, where the
|
|
238
|
+
// recorded hostname differs, `isStale` never reclaims it at all and the
|
|
239
|
+
// session is locked forever. The finally covers a normal exit, a Ctrl+C that
|
|
240
|
+
// unwinds through here, and a throw.
|
|
241
|
+
try {
|
|
242
|
+
await runAgentSurface({
|
|
243
|
+
io: options.io,
|
|
244
|
+
...(recorder === undefined ? {} : { recorder }),
|
|
245
|
+
capabilities: options.capabilities,
|
|
246
|
+
stdout: options.stdout,
|
|
247
|
+
...(options.getSignal === undefined
|
|
248
|
+
? {}
|
|
249
|
+
: { getSignal: options.getSignal }),
|
|
250
|
+
session: {
|
|
251
|
+
client: runtime.dataPlane,
|
|
252
|
+
model: effectiveModel.modelId,
|
|
253
|
+
pricing: effectiveModel,
|
|
254
|
+
catalog: options.catalogue ?? [effectiveModel],
|
|
255
|
+
systemPrompt: mode === "read-only" ? READ_ONLY_PROMPT : WRITE_PROMPT,
|
|
256
|
+
tools: buildToolset(mode, hasCommands),
|
|
257
|
+
toolContext: {
|
|
258
|
+
workspace,
|
|
259
|
+
allowedCommands: options.permissions.allowedCommands,
|
|
260
|
+
},
|
|
261
|
+
mode,
|
|
262
|
+
// The rebuilt conversation, or nothing for a new session. runSession
|
|
263
|
+
// appends to this array in place, which is what continues the thread.
|
|
264
|
+
...(opened === undefined ? {} : { history: opened.messages }),
|
|
139
265
|
allowedCommands: options.permissions.allowedCommands,
|
|
140
266
|
},
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
finally {
|
|
270
|
+
if (opened !== undefined) {
|
|
271
|
+
try {
|
|
272
|
+
opened.writer.close();
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
// Closing is bookkeeping; nothing depends on it succeeding.
|
|
276
|
+
}
|
|
277
|
+
opened.lock.release();
|
|
278
|
+
}
|
|
279
|
+
}
|
|
148
280
|
return ExitCode.Ok;
|
|
149
281
|
}
|
|
150
282
|
/**
|