@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/tui/app.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
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 { loadConfig } from "../config/load.js";
|
|
9
10
|
import { loadCatalogue, saveDefaultModel } from "./adapters/models.js";
|
|
10
11
|
import { renderBanner } from "./banner.js";
|
|
11
12
|
import { keyboard } from "./input.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({
|
|
@@ -123,7 +137,9 @@ export async function runTui(options) {
|
|
|
123
137
|
stdout: options.stdout,
|
|
124
138
|
commandIo: options.commandIo,
|
|
125
139
|
apiKey: auth.apiKey,
|
|
126
|
-
model: chosen,
|
|
140
|
+
model: chosen.model,
|
|
141
|
+
catalogue: chosen.catalogue,
|
|
142
|
+
agentCapableModels: chosen.agentCapable,
|
|
127
143
|
workspace: options.workspace,
|
|
128
144
|
permissions: parsePermissions(options.argv),
|
|
129
145
|
session: {
|
|
@@ -143,12 +159,21 @@ export async function runTui(options) {
|
|
|
143
159
|
guard.release();
|
|
144
160
|
}
|
|
145
161
|
}
|
|
146
|
-
/**
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
162
|
+
/** The model the whole precedence chain settles on: flag > env > project > user. */
|
|
163
|
+
function resolvedDefaultModel(options) {
|
|
164
|
+
try {
|
|
165
|
+
return loadConfig({
|
|
166
|
+
env: options.commandIo.env,
|
|
167
|
+
cwd: options.commandIo.cwd,
|
|
168
|
+
userConfigFile: options.commandIo.userConfigFile,
|
|
169
|
+
}).model;
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
// A broken config must not stop the picker from opening; it will be
|
|
173
|
+
// reported the moment anything actually needs the configuration.
|
|
174
|
+
return options.commandIo.env?.MAFDET_MODEL;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
152
177
|
async function chooseModel(options) {
|
|
153
178
|
const { dim, bold } = palette(options.capabilities);
|
|
154
179
|
options.stdout.write(dim(" loading the model catalogue...\n"));
|
|
@@ -184,7 +209,11 @@ async function chooseModel(options) {
|
|
|
184
209
|
// Fail closed: offer only agent-capable models when any exist, so a
|
|
185
210
|
// model the agent cannot drive never becomes the session's default.
|
|
186
211
|
agentOnly: catalogue.agentReady.length > 0,
|
|
187
|
-
|
|
212
|
+
// F-005 · The resolved model, not just the environment variable. After
|
|
213
|
+
// "Remember X as your default?" wrote X to the user config, the next
|
|
214
|
+
// launch still highlighted whatever MAFDET_MODEL said - usually nothing -
|
|
215
|
+
// so the setting the user had just agreed to appeared not to have worked.
|
|
216
|
+
current: resolvedDefaultModel(options),
|
|
188
217
|
});
|
|
189
218
|
if (picked.kind === "cancelled" || picked.model === undefined)
|
|
190
219
|
return undefined;
|
|
@@ -213,5 +242,9 @@ async function chooseModel(options) {
|
|
|
213
242
|
? dim(` saved to ${saved.path}\n`)
|
|
214
243
|
: dim(` could not save to ${saved.path}: ${saved.problem}\n`));
|
|
215
244
|
}
|
|
216
|
-
return
|
|
245
|
+
return {
|
|
246
|
+
model: picked.model,
|
|
247
|
+
catalogue: catalogue.chat,
|
|
248
|
+
agentCapable: catalogue.agentReady.map((m) => m.modelId),
|
|
249
|
+
};
|
|
217
250
|
}
|
|
@@ -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
|
/**
|
|
@@ -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;AAenD,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,wBAAsB,UAAU,CAC9B,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAAC,QAAQ,CAAC,CA8JnB"}
|
package/dist/tui/start-agent.js
CHANGED
|
@@ -16,6 +16,7 @@ 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
18
|
import { describeResume, openForResume } from "../sessions/open.js";
|
|
19
|
+
import { describeBlocker } from "../sessions/resume.js";
|
|
19
20
|
import { runAgentSurface } from "./agent-surface.js";
|
|
20
21
|
import { palette } from "./theme.js";
|
|
21
22
|
/**
|
|
@@ -25,24 +26,64 @@ import { palette } from "./theme.js";
|
|
|
25
26
|
* granting these and no second one to keep in step.
|
|
26
27
|
*/
|
|
27
28
|
/** `--no-history` and `--title`, read off the bare invocation. */
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
/** Every flag the bare `mafdet` invocation understands. */
|
|
30
|
+
export const TUI_FLAGS = [
|
|
31
|
+
"--allow-write",
|
|
32
|
+
"--read-only",
|
|
33
|
+
"--allow-command",
|
|
34
|
+
"--no-history",
|
|
35
|
+
"--no-tui",
|
|
36
|
+
"--resume",
|
|
37
|
+
"--title",
|
|
38
|
+
];
|
|
39
|
+
/** The ones that consume the argument after them. */
|
|
40
|
+
const TUI_FLAGS_WITH_VALUE = new Set([
|
|
41
|
+
"--allow-command",
|
|
42
|
+
"--resume",
|
|
43
|
+
"--title",
|
|
44
|
+
]);
|
|
45
|
+
export class TuiArgumentError extends Error {
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* F-014 · Refuse arguments we do not understand.
|
|
49
|
+
*
|
|
50
|
+
* `--title` with no value, or a mistyped `--reusme`, used to be dropped in
|
|
51
|
+
* silence: the session started, looked fine, and did something other than what
|
|
52
|
+
* was asked. That is worse than an error, because nothing tells the user.
|
|
53
|
+
*/
|
|
54
|
+
export function checkTuiArguments(argv) {
|
|
30
55
|
for (let i = 0; i < argv.length; i += 1) {
|
|
31
|
-
|
|
56
|
+
const arg = argv[i];
|
|
57
|
+
if (!arg.startsWith("-")) {
|
|
58
|
+
throw new TuiArgumentError(`unexpected argument '${arg}'`);
|
|
59
|
+
}
|
|
60
|
+
if (!TUI_FLAGS.includes(arg)) {
|
|
61
|
+
throw new TuiArgumentError(`unknown option '${arg}'`);
|
|
62
|
+
}
|
|
63
|
+
if (!TUI_FLAGS_WITH_VALUE.has(arg))
|
|
32
64
|
continue;
|
|
33
65
|
const value = argv[i + 1];
|
|
34
|
-
if (value
|
|
35
|
-
|
|
36
|
-
i += 1;
|
|
66
|
+
if (value === undefined || value.startsWith("-")) {
|
|
67
|
+
throw new TuiArgumentError(`${arg} needs a value`);
|
|
37
68
|
}
|
|
69
|
+
i += 1;
|
|
38
70
|
}
|
|
71
|
+
}
|
|
72
|
+
export function parseSessionOptions(argv) {
|
|
73
|
+
let title;
|
|
39
74
|
let resumeId;
|
|
40
75
|
for (let i = 0; i < argv.length; i += 1) {
|
|
41
|
-
|
|
76
|
+
const arg = argv[i];
|
|
77
|
+
if (!TUI_FLAGS_WITH_VALUE.has(arg))
|
|
42
78
|
continue;
|
|
43
79
|
const value = argv[i + 1];
|
|
44
|
-
if (value
|
|
80
|
+
if (value === undefined || value.startsWith("-"))
|
|
81
|
+
continue;
|
|
82
|
+
if (arg === "--title")
|
|
83
|
+
title = value;
|
|
84
|
+
if (arg === "--resume")
|
|
45
85
|
resumeId = value;
|
|
86
|
+
i += 1;
|
|
46
87
|
}
|
|
47
88
|
return {
|
|
48
89
|
noHistory: argv.includes("--no-history"),
|
|
@@ -50,6 +91,14 @@ export function parseSessionOptions(argv) {
|
|
|
50
91
|
...(resumeId === undefined ? {} : { resumeId }),
|
|
51
92
|
};
|
|
52
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* F-004 · The same permission syntax as the command surface.
|
|
96
|
+
*
|
|
97
|
+
* `--read-only` existed only in `mafdet agent`, so `mafdet --allow-write
|
|
98
|
+
* --read-only` was read-only there and write-enabled here. A flag whose whole
|
|
99
|
+
* purpose is to take permission away must not be the one the TUI ignores, and
|
|
100
|
+
* last-flag-wins is the only ordering a user can reason about.
|
|
101
|
+
*/
|
|
53
102
|
export function parsePermissions(argv) {
|
|
54
103
|
let allowWrite = false;
|
|
55
104
|
const allowedCommands = [];
|
|
@@ -57,6 +106,8 @@ export function parsePermissions(argv) {
|
|
|
57
106
|
const arg = argv[i];
|
|
58
107
|
if (arg === "--allow-write")
|
|
59
108
|
allowWrite = true;
|
|
109
|
+
else if (arg === "--read-only")
|
|
110
|
+
allowWrite = false;
|
|
60
111
|
else if (arg === "--allow-command") {
|
|
61
112
|
const value = argv[i + 1];
|
|
62
113
|
if (value !== undefined && !value.startsWith("-")) {
|
|
@@ -69,15 +120,6 @@ export function parsePermissions(argv) {
|
|
|
69
120
|
}
|
|
70
121
|
export async function startAgent(options) {
|
|
71
122
|
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
123
|
// The TUI is interactive by construction, so `--allow-write` can be honoured
|
|
82
124
|
// here without the non-interactive downgrade the command surface applies.
|
|
83
125
|
const mode = options.permissions.allowWrite ? "suggest" : "read-only";
|
|
@@ -96,10 +138,13 @@ export async function startAgent(options) {
|
|
|
96
138
|
root: sessionsDir(options.commandIo.env ?? process.env),
|
|
97
139
|
sessionId: options.session.resumeId,
|
|
98
140
|
systemPrompt: mode === "read-only" ? READ_ONLY_PROMPT : WRITE_PROMPT,
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
141
|
+
// F-003 · The whole set this key can call, not just the model that
|
|
142
|
+
// happened to be picked before the session was chosen. Checking
|
|
143
|
+
// against a one-element set made every resume of a different model
|
|
144
|
+
// look unavailable, and told the user the wrong thing about why.
|
|
145
|
+
callable: (options.catalogue ?? [options.model]).map((m) => m.modelId),
|
|
146
|
+
agentCapable: options.agentCapableModels ??
|
|
147
|
+
(options.model.agentEligible ? [options.model.modelId] : []),
|
|
103
148
|
allowedCommandsThisRun: options.permissions.allowedCommands,
|
|
104
149
|
})
|
|
105
150
|
: undefined;
|
|
@@ -111,6 +156,44 @@ export async function startAgent(options) {
|
|
|
111
156
|
}
|
|
112
157
|
if (opened !== undefined)
|
|
113
158
|
options.stdout.write(dim(describeResume(opened)));
|
|
159
|
+
// F-003 · A blocker used to be printed and then ignored, so a resume with an
|
|
160
|
+
// uncallable model or a deleted workspace started anyway. Resuming into a
|
|
161
|
+
// different directory is the worst of them: with writing enabled the agent
|
|
162
|
+
// would carry one project's context into another project's files.
|
|
163
|
+
if (opened !== undefined && opened.blockers.length > 0) {
|
|
164
|
+
options.stdout.write(`\n ${bold("cannot continue this conversation yet")}:\n` +
|
|
165
|
+
opened.blockers
|
|
166
|
+
.map((blocker) => ` - ${describeBlocker(blocker)}\n`)
|
|
167
|
+
.join("") +
|
|
168
|
+
`\n You can still read it with 'mafdet sessions show ${options.session?.resumeId ?? ""}'.\n\n`);
|
|
169
|
+
opened.writer.close();
|
|
170
|
+
opened.lock.release();
|
|
171
|
+
return ExitCode.General;
|
|
172
|
+
}
|
|
173
|
+
// F-003 · A resumed session runs where it was recorded and on the model it
|
|
174
|
+
// was recorded with. Continuing project A's conversation against project B's
|
|
175
|
+
// files is how an agent with write access edits the wrong repository - and
|
|
176
|
+
// it used to be the default, because the workspace came from the cwd.
|
|
177
|
+
const effectiveWorkspace = opened === undefined ? options.workspace : opened.metadata.workspace;
|
|
178
|
+
const effectiveModel = opened === undefined
|
|
179
|
+
? options.model
|
|
180
|
+
: ((options.catalogue ?? []).find((m) => m.modelId === opened.metadata.model_id) ?? options.model);
|
|
181
|
+
let workspace;
|
|
182
|
+
try {
|
|
183
|
+
workspace = Workspace.open(effectiveWorkspace);
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
187
|
+
options.stdout.write(`\n ${bold("cannot use this directory")}: ${message}\n`);
|
|
188
|
+
if (opened !== undefined) {
|
|
189
|
+
opened.writer.close();
|
|
190
|
+
opened.lock.release();
|
|
191
|
+
}
|
|
192
|
+
return ExitCode.General;
|
|
193
|
+
}
|
|
194
|
+
if (opened !== undefined && effectiveWorkspace !== options.workspace) {
|
|
195
|
+
options.stdout.write(dim(` continuing in ${effectiveWorkspace}, where it was recorded\n`));
|
|
196
|
+
}
|
|
114
197
|
const recorder = opened !== undefined
|
|
115
198
|
? new SessionRecorder({
|
|
116
199
|
writer: opened.writer,
|
|
@@ -119,32 +202,51 @@ export async function startAgent(options) {
|
|
|
119
202
|
onProblem: (message) => void options.stdout.write(dim(` ${message}\n`)),
|
|
120
203
|
})
|
|
121
204
|
: startRecording(options, mode);
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
205
|
+
// F-015 · The resume lock lives for exactly the run that holds it. It used
|
|
206
|
+
// to be taken and never released, so the lock file survived until the next
|
|
207
|
+
// process judged the pid dead - and on a shared filesystem, where the
|
|
208
|
+
// recorded hostname differs, `isStale` never reclaims it at all and the
|
|
209
|
+
// session is locked forever. The finally covers a normal exit, a Ctrl+C that
|
|
210
|
+
// unwinds through here, and a throw.
|
|
211
|
+
try {
|
|
212
|
+
await runAgentSurface({
|
|
213
|
+
io: options.io,
|
|
214
|
+
...(recorder === undefined ? {} : { recorder }),
|
|
215
|
+
capabilities: options.capabilities,
|
|
216
|
+
stdout: options.stdout,
|
|
217
|
+
...(options.getSignal === undefined
|
|
218
|
+
? {}
|
|
219
|
+
: { getSignal: options.getSignal }),
|
|
220
|
+
session: {
|
|
221
|
+
client: runtime.dataPlane,
|
|
222
|
+
model: effectiveModel.modelId,
|
|
223
|
+
pricing: effectiveModel,
|
|
224
|
+
catalog: options.catalogue ?? [effectiveModel],
|
|
225
|
+
systemPrompt: mode === "read-only" ? READ_ONLY_PROMPT : WRITE_PROMPT,
|
|
226
|
+
tools: buildToolset(mode, hasCommands),
|
|
227
|
+
toolContext: {
|
|
228
|
+
workspace,
|
|
229
|
+
allowedCommands: options.permissions.allowedCommands,
|
|
230
|
+
},
|
|
231
|
+
mode,
|
|
232
|
+
// The rebuilt conversation, or nothing for a new session. runSession
|
|
233
|
+
// appends to this array in place, which is what continues the thread.
|
|
234
|
+
...(opened === undefined ? {} : { history: opened.messages }),
|
|
139
235
|
allowedCommands: options.permissions.allowedCommands,
|
|
140
236
|
},
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
finally {
|
|
240
|
+
if (opened !== undefined) {
|
|
241
|
+
try {
|
|
242
|
+
opened.writer.close();
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
// Closing is bookkeeping; nothing depends on it succeeding.
|
|
246
|
+
}
|
|
247
|
+
opened.lock.release();
|
|
248
|
+
}
|
|
249
|
+
}
|
|
148
250
|
return ExitCode.Ok;
|
|
149
251
|
}
|
|
150
252
|
/**
|