@mafdet/cli 0.4.1 → 0.4.3
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 +63 -0
- package/README.md +79 -12
- package/README.zh-CN.md +69 -12
- package/dist/agent/context-budget.d.ts +37 -0
- package/dist/agent/context-budget.d.ts.map +1 -0
- package/dist/agent/context-budget.js +105 -0
- package/dist/agent/loop.d.ts +25 -0
- package/dist/agent/loop.d.ts.map +1 -1
- package/dist/agent/loop.js +36 -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/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/account.d.ts +43 -0
- package/dist/api/account.d.ts.map +1 -1
- package/dist/api/account.js +40 -0
- package/dist/commands/chat.d.ts.map +1 -1
- package/dist/commands/chat.js +118 -18
- 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 +23 -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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run-command.d.ts","sourceRoot":"","sources":["../../../src/agent/tools/run-command.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAA+B,cAAc,EAAE,MAAM,YAAY,CAAC;AAE9E,0DAA0D;AAC1D,eAAO,MAAM,kBAAkB,SAAU,CAAC;AAG1C,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED;;;;;;GAMG;AACH,wBAAsB,cAAc,CAClC,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,GAAG,EAAE,MAAM,EACX,SAAS,GAAE,MAA2B,GACrC,OAAO,CAAC,aAAa,CAAC,CAkDxB;
|
|
1
|
+
{"version":3,"file":"run-command.d.ts","sourceRoot":"","sources":["../../../src/agent/tools/run-command.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAA+B,cAAc,EAAE,MAAM,YAAY,CAAC;AAE9E,0DAA0D;AAC1D,eAAO,MAAM,kBAAkB,SAAU,CAAC;AAG1C,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED;;;;;;GAMG;AACH,wBAAsB,cAAc,CAClC,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,GAAG,EAAE,MAAM,EACX,SAAS,GAAE,MAA2B,GACrC,OAAO,CAAC,aAAa,CAAC,CAkDxB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,MAAM,QAAQ,GAChB;IACE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,OAAO,CAAC;CACjC,GACD;IACE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;CAClC,CAAC;AAEN,wBAAgB,QAAQ,CACtB,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,OAAO,EACd,QAAQ,GAAE,MAAyB,GAClC,QAAQ,CAaV;AA8BD,eAAO,MAAM,cAAc,EAAE,cAmC5B,CAAC;AAEF,eAAO,MAAM,aAAa,EAAE,cAAc,EAAqB,CAAC"}
|
|
@@ -48,8 +48,8 @@ export async function executeCommand(argv, cwd, timeoutMs = COMMAND_TIMEOUT_MS)
|
|
|
48
48
|
const timer = setTimeout(() => {
|
|
49
49
|
timedOut = true;
|
|
50
50
|
killTree(child.pid);
|
|
51
|
-
// A process ignoring
|
|
52
|
-
setTimeout(() => killTree(child.pid,
|
|
51
|
+
// A process ignoring the polite request still has to go.
|
|
52
|
+
setTimeout(() => killTree(child.pid, true), 2000).unref();
|
|
53
53
|
}, timeoutMs);
|
|
54
54
|
const finish = (code, signal) => {
|
|
55
55
|
clearTimeout(timer);
|
|
@@ -59,23 +59,44 @@ export async function executeCommand(argv, cwd, timeoutMs = COMMAND_TIMEOUT_MS)
|
|
|
59
59
|
child.on("close", (code, signal) => finish(code, signal));
|
|
60
60
|
});
|
|
61
61
|
}
|
|
62
|
+
export function killPlan(pid, force, platform = process.platform) {
|
|
63
|
+
if (platform === "win32") {
|
|
64
|
+
return {
|
|
65
|
+
kind: "spawn",
|
|
66
|
+
command: "taskkill",
|
|
67
|
+
args: ["/PID", String(pid), "/T", ...(force ? ["/F"] : [])],
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
kind: "signal",
|
|
72
|
+
target: -pid,
|
|
73
|
+
signal: force ? "SIGKILL" : "SIGTERM",
|
|
74
|
+
};
|
|
75
|
+
}
|
|
62
76
|
/**
|
|
63
|
-
* Kill the whole
|
|
77
|
+
* Kill the whole tree.
|
|
64
78
|
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
* child is killed and the limitation is stated rather than papered over
|
|
69
|
-
* (no Windows machine here to verify a taskkill /T path).
|
|
79
|
+
* ⚠️ The Windows branch is UNVERIFIED on a real machine. Its shape is tested;
|
|
80
|
+
* whether `taskkill /T` actually reaches every grandchild of a Node-spawned
|
|
81
|
+
* process is not something this project has been able to check.
|
|
70
82
|
*/
|
|
71
|
-
function killTree(pid,
|
|
83
|
+
function killTree(pid, force = false) {
|
|
72
84
|
if (pid === undefined)
|
|
73
85
|
return;
|
|
86
|
+
const plan = killPlan(pid, force);
|
|
74
87
|
try {
|
|
75
|
-
if (
|
|
76
|
-
process.kill(
|
|
77
|
-
|
|
78
|
-
|
|
88
|
+
if (plan.kind === "signal") {
|
|
89
|
+
process.kill(plan.target, plan.signal);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
// Detached and ignored: this is fire-and-forget cleanup, and a taskkill
|
|
93
|
+
// that fails must not become an unhandled error on the way out.
|
|
94
|
+
const killer = spawn(plan.command, [...plan.args], {
|
|
95
|
+
stdio: "ignore",
|
|
96
|
+
windowsHide: true,
|
|
97
|
+
});
|
|
98
|
+
killer.on("error", () => { });
|
|
99
|
+
killer.unref();
|
|
79
100
|
}
|
|
80
101
|
catch {
|
|
81
102
|
// Already exited, or never became a group leader.
|
package/dist/api/account.d.ts
CHANGED
|
@@ -36,4 +36,47 @@ export interface Subscription {
|
|
|
36
36
|
export declare function fetchWallet(client: HttpClient): Promise<Wallet>;
|
|
37
37
|
export declare function fetchUsage(client: HttpClient, days?: number): Promise<UsageSummary>;
|
|
38
38
|
export declare function fetchSubscription(client: HttpClient): Promise<Subscription>;
|
|
39
|
+
/**
|
|
40
|
+
* CLI-027 · One settled request as the ledger holds it.
|
|
41
|
+
*
|
|
42
|
+
* The whole whitelist the server returns (CLI-026): no prompt, no response, no
|
|
43
|
+
* tool arguments, no file content, and no session id - the CLI matches on
|
|
44
|
+
* `request_id` alone, which is why none of that has to leave the machine.
|
|
45
|
+
*/
|
|
46
|
+
export interface LedgerUsageEvent {
|
|
47
|
+
request_id: string;
|
|
48
|
+
model_id: string;
|
|
49
|
+
input_tokens: number;
|
|
50
|
+
output_tokens: number;
|
|
51
|
+
platform_charge_micro_usd: string;
|
|
52
|
+
status: string;
|
|
53
|
+
created_at: string;
|
|
54
|
+
}
|
|
55
|
+
export interface UsageEventsResult {
|
|
56
|
+
readonly events: readonly LedgerUsageEvent[];
|
|
57
|
+
readonly pages: number;
|
|
58
|
+
/**
|
|
59
|
+
* True when the walk reached the end of the window.
|
|
60
|
+
*
|
|
61
|
+
* When it is false the caller stopped early, and "not found" then means "not
|
|
62
|
+
* found yet" - reporting it as "the ledger has no such request" would be a
|
|
63
|
+
* claim the walk did not earn.
|
|
64
|
+
*/
|
|
65
|
+
readonly exhausted: boolean;
|
|
66
|
+
}
|
|
67
|
+
/** Hard stop on paging, so a busy key cannot turn one command into hundreds. */
|
|
68
|
+
export declare const MAX_USAGE_EVENT_PAGES = 20;
|
|
69
|
+
/**
|
|
70
|
+
* Walk this key's settled requests, newest first.
|
|
71
|
+
*
|
|
72
|
+
* Stops as soon as every id in `wanted` has been seen: a session of three
|
|
73
|
+
* requests should cost one page, not a full sweep of the window. Passing no
|
|
74
|
+
* `wanted` set walks until the pages run out.
|
|
75
|
+
*/
|
|
76
|
+
export declare function fetchUsageEvents(client: HttpClient, options?: {
|
|
77
|
+
days?: number;
|
|
78
|
+
wanted?: ReadonlySet<string>;
|
|
79
|
+
maxPages?: number;
|
|
80
|
+
signal?: AbortSignal;
|
|
81
|
+
}): Promise<UsageEventsResult>;
|
|
39
82
|
//# sourceMappingURL=account.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"account.d.ts","sourceRoot":"","sources":["../../src/api/account.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAE5C;;;GAGG;AAEH,MAAM,WAAW,MAAM;IACrB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,eAAe,EAAE,MAAM,CAAC;IACxB,gBAAgB,EAAE,MAAM,CAAC;IACzB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,UAAU,CAAC,EAAE;QACX,mBAAmB,EAAE,MAAM,CAAC;QAC5B,cAAc,EAAE,MAAM,CAAC;QACvB,mBAAmB,EAAE,MAAM,CAAC;QAC5B,aAAa,EAAE,MAAM,CAAC;QACtB,KAAK,EAAE,MAAM,CAAC;KACf,CAAC;CACH;AAyBD,wBAAsB,WAAW,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAKrE;AAED,wBAAsB,UAAU,CAC9B,MAAM,EAAE,UAAU,EAClB,IAAI,CAAC,EAAE,MAAM,GACZ,OAAO,CAAC,YAAY,CAAC,CAMvB;AAED,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,UAAU,GACjB,OAAO,CAAC,YAAY,CAAC,CAKvB"}
|
|
1
|
+
{"version":3,"file":"account.d.ts","sourceRoot":"","sources":["../../src/api/account.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAE5C;;;GAGG;AAEH,MAAM,WAAW,MAAM;IACrB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,eAAe,EAAE,MAAM,CAAC;IACxB,gBAAgB,EAAE,MAAM,CAAC;IACzB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,UAAU,CAAC,EAAE;QACX,mBAAmB,EAAE,MAAM,CAAC;QAC5B,cAAc,EAAE,MAAM,CAAC;QACvB,mBAAmB,EAAE,MAAM,CAAC;QAC5B,aAAa,EAAE,MAAM,CAAC;QACtB,KAAK,EAAE,MAAM,CAAC;KACf,CAAC;CACH;AAyBD,wBAAsB,WAAW,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAKrE;AAED,wBAAsB,UAAU,CAC9B,MAAM,EAAE,UAAU,EAClB,IAAI,CAAC,EAAE,MAAM,GACZ,OAAO,CAAC,YAAY,CAAC,CAMvB;AAED,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,UAAU,GACjB,OAAO,CAAC,YAAY,CAAC,CAKvB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,yBAAyB,EAAE,MAAM,CAAC;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;CACpB;AAOD,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,MAAM,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAC7C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB;;;;;;OAMG;IACH,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;CAC7B;AAED,gFAAgF;AAChF,eAAO,MAAM,qBAAqB,KAAK,CAAC;AAExC;;;;;;GAMG;AACH,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,UAAU,EAClB,OAAO,GAAE;IACP,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC7B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,WAAW,CAAC;CACjB,GACL,OAAO,CAAC,iBAAiB,CAAC,CAgC5B"}
|
package/dist/api/account.js
CHANGED
|
@@ -37,3 +37,43 @@ export async function fetchSubscription(client) {
|
|
|
37
37
|
.catch(explainScope);
|
|
38
38
|
return data;
|
|
39
39
|
}
|
|
40
|
+
/** Hard stop on paging, so a busy key cannot turn one command into hundreds. */
|
|
41
|
+
export const MAX_USAGE_EVENT_PAGES = 20;
|
|
42
|
+
/**
|
|
43
|
+
* Walk this key's settled requests, newest first.
|
|
44
|
+
*
|
|
45
|
+
* Stops as soon as every id in `wanted` has been seen: a session of three
|
|
46
|
+
* requests should cost one page, not a full sweep of the window. Passing no
|
|
47
|
+
* `wanted` set walks until the pages run out.
|
|
48
|
+
*/
|
|
49
|
+
export async function fetchUsageEvents(client, options = {}) {
|
|
50
|
+
const maxPages = options.maxPages ?? MAX_USAGE_EVENT_PAGES;
|
|
51
|
+
const remaining = options.wanted ? new Set(options.wanted) : undefined;
|
|
52
|
+
const events = [];
|
|
53
|
+
let cursor = null;
|
|
54
|
+
let pages = 0;
|
|
55
|
+
while (pages < maxPages) {
|
|
56
|
+
const query = new URLSearchParams();
|
|
57
|
+
if (options.days !== undefined)
|
|
58
|
+
query.set("days", String(options.days));
|
|
59
|
+
if (cursor !== null)
|
|
60
|
+
query.set("cursor", cursor);
|
|
61
|
+
const { data } = await client
|
|
62
|
+
.getJson(`/keyinfo/usage-events?${query.toString()}`, {
|
|
63
|
+
signal: options.signal,
|
|
64
|
+
})
|
|
65
|
+
.catch(explainScope);
|
|
66
|
+
pages += 1;
|
|
67
|
+
for (const event of data.data ?? []) {
|
|
68
|
+
events.push(event);
|
|
69
|
+
remaining?.delete(event.request_id);
|
|
70
|
+
}
|
|
71
|
+
if (remaining !== undefined && remaining.size === 0) {
|
|
72
|
+
return { events, pages, exhausted: true };
|
|
73
|
+
}
|
|
74
|
+
cursor = data.next_cursor ?? null;
|
|
75
|
+
if (cursor === null)
|
|
76
|
+
return { events, pages, exhausted: true };
|
|
77
|
+
}
|
|
78
|
+
return { events, pages, exhausted: false };
|
|
79
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chat.d.ts","sourceRoot":"","sources":["../../src/commands/chat.ts"],"names":[],"mappings":"AAEA,OAAO,EAAgB,KAAK,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACnE,OAAO,EAAkB,KAAK,WAAW,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"chat.d.ts","sourceRoot":"","sources":["../../src/commands/chat.ts"],"names":[],"mappings":"AAEA,OAAO,EAAgB,KAAK,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACnE,OAAO,EAAkB,KAAK,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAOxE,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAEnD,OAAO,EAIL,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAatB,MAAM,WAAW,MAAO,SAAQ,SAAS;IACvC,qDAAqD;IACrD,SAAS,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IAClC,gDAAgD;IAChD,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAcD,wBAAsB,WAAW,CAC/B,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,EAAE,EAAE,MAAM,GACT,OAAO,CAAC,QAAQ,CAAC,CA8BnB;AAoSD,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,YAAY,GAAG,SAAS,EAC/B,KAAK,EAAE,WAAW,GACjB,MAAM,GAAG,SAAS,CASpB"}
|
package/dist/commands/chat.js
CHANGED
|
@@ -2,15 +2,23 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
import { fetchCatalog } from "../api/models.js";
|
|
3
3
|
import { readChatStream } from "../api/sse-parser.js";
|
|
4
4
|
import { ApiError } from "../api/errors.js";
|
|
5
|
+
import { sessionsDir } from "../config/paths.js";
|
|
6
|
+
import { keyDigest } from "../security/redact.js";
|
|
7
|
+
import { upsertIndexEntry } from "../sessions/index-file.js";
|
|
8
|
+
import { decideRecording } from "../sessions/policy.js";
|
|
9
|
+
import { SessionWriter } from "../sessions/store.js";
|
|
5
10
|
import { ExitCode } from "../output/exit-codes.js";
|
|
6
11
|
import { formatUsd } from "../output/money.js";
|
|
7
12
|
import { buildRuntime, reportError, requireApiKey, } from "./runtime.js";
|
|
8
13
|
const USAGE = `Usage:
|
|
9
14
|
mafdet chat [prompt] [--model <id>] [--system <file>] [--input <file>]
|
|
10
|
-
[--no-stream] [--max-tokens <n>] [--quiet]
|
|
15
|
+
[--no-stream] [--max-tokens <n>] [--quiet] [--save]
|
|
11
16
|
|
|
12
17
|
The prompt comes from an argument, from --input <file>, or from stdin when
|
|
13
18
|
neither is given. Use "--input -" to force reading stdin.
|
|
19
|
+
|
|
20
|
+
A single chat is not saved unless --save is given. Saved conversations stay
|
|
21
|
+
on this computer; see 'mafdet sessions'.
|
|
14
22
|
`;
|
|
15
23
|
export async function chatCommand(argv, io) {
|
|
16
24
|
try {
|
|
@@ -51,6 +59,8 @@ async function parseArgs(argv, configuredModel, io) {
|
|
|
51
59
|
let inputFile;
|
|
52
60
|
let stream = true;
|
|
53
61
|
let quiet = false;
|
|
62
|
+
let save = false;
|
|
63
|
+
let noHistory = false;
|
|
54
64
|
let maxTokens;
|
|
55
65
|
for (let i = 0; i < argv.length; i += 1) {
|
|
56
66
|
const arg = argv[i];
|
|
@@ -65,6 +75,10 @@ async function parseArgs(argv, configuredModel, io) {
|
|
|
65
75
|
stream = false;
|
|
66
76
|
else if (arg === "--quiet")
|
|
67
77
|
quiet = true;
|
|
78
|
+
else if (arg === "--save")
|
|
79
|
+
save = true;
|
|
80
|
+
else if (arg === "--no-history")
|
|
81
|
+
noHistory = true;
|
|
68
82
|
else if (arg === "--max-tokens") {
|
|
69
83
|
const raw = next();
|
|
70
84
|
maxTokens = Number(raw);
|
|
@@ -123,6 +137,8 @@ async function parseArgs(argv, configuredModel, io) {
|
|
|
123
137
|
stream,
|
|
124
138
|
maxTokens,
|
|
125
139
|
quiet,
|
|
140
|
+
save,
|
|
141
|
+
noHistory,
|
|
126
142
|
};
|
|
127
143
|
}
|
|
128
144
|
function readTextFile(path, flag) {
|
|
@@ -137,26 +153,117 @@ function readTextFile(path, flag) {
|
|
|
137
153
|
});
|
|
138
154
|
}
|
|
139
155
|
}
|
|
156
|
+
/**
|
|
157
|
+
* CLI-024/§20.4 · Save a one-shot chat, when asked.
|
|
158
|
+
*
|
|
159
|
+
* A single `mafdet chat` is not a conversation anyone resumes, so it is not
|
|
160
|
+
* recorded by default; `--save` opts in and `--no-history` still wins over it.
|
|
161
|
+
* Everything written goes through the same store as an interactive session, so
|
|
162
|
+
* the same redaction, the same 0600 files and the same `mafdet sessions
|
|
163
|
+
* delete` apply - there is no second, weaker path to disk.
|
|
164
|
+
*
|
|
165
|
+
* Failures here are swallowed: losing the record of a chat is bad, losing the
|
|
166
|
+
* answer the user is waiting for is worse.
|
|
167
|
+
*/
|
|
168
|
+
function saveChat(args, io, answer, usage, requestId, model) {
|
|
169
|
+
const decision = decideRecording({
|
|
170
|
+
surface: "one-shot",
|
|
171
|
+
noHistory: args.noHistory,
|
|
172
|
+
save: args.save,
|
|
173
|
+
});
|
|
174
|
+
if (!decision.record)
|
|
175
|
+
return;
|
|
176
|
+
const env = io.env ?? process.env;
|
|
177
|
+
try {
|
|
178
|
+
const root = sessionsDir(env);
|
|
179
|
+
const writer = SessionWriter.create({
|
|
180
|
+
root,
|
|
181
|
+
title: chatTitle(args.prompt),
|
|
182
|
+
modelId: args.model,
|
|
183
|
+
workspace: io.cwd ?? process.cwd(),
|
|
184
|
+
mode: "chat",
|
|
185
|
+
});
|
|
186
|
+
if (args.system !== undefined) {
|
|
187
|
+
writer.append({ type: "user_message", content: args.system });
|
|
188
|
+
}
|
|
189
|
+
writer.append({ type: "user_message", content: args.prompt });
|
|
190
|
+
writer.append({ type: "assistant_message", content: answer });
|
|
191
|
+
if (usage) {
|
|
192
|
+
const micros = estimateChargeMicroUsd(model, usage);
|
|
193
|
+
writer.append({
|
|
194
|
+
type: "usage",
|
|
195
|
+
...(requestId === undefined ? {} : { request_id: requestId }),
|
|
196
|
+
model_id: args.model,
|
|
197
|
+
prompt_tokens: usage.prompt_tokens,
|
|
198
|
+
completion_tokens: usage.completion_tokens,
|
|
199
|
+
estimated_charge_micro_usd: (micros ?? 0n).toString(),
|
|
200
|
+
settlement_status: "pending",
|
|
201
|
+
api_key_ref: keyDigest(readKey(io)),
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
writer.close();
|
|
205
|
+
upsertIndexEntry(root, writer.metadata);
|
|
206
|
+
if (!args.quiet) {
|
|
207
|
+
io.stderr(`saved as ${writer.sessionId} (local only)\n`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
io.stderr(`mafdet chat: not saved: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/** The first line, trimmed to something that fits a listing. */
|
|
215
|
+
function chatTitle(prompt) {
|
|
216
|
+
const line = prompt.split("\n").find((l) => l.trim().length > 0) ?? "Chat";
|
|
217
|
+
const trimmed = line.trim();
|
|
218
|
+
return trimmed.length > 60 ? `${trimmed.slice(0, 57)}...` : trimmed;
|
|
219
|
+
}
|
|
220
|
+
function readKey(io) {
|
|
221
|
+
return (io.env ?? process.env).MAFDET_API_KEY ?? "";
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* The catalogue entry for pricing, fetched once and shared.
|
|
225
|
+
*
|
|
226
|
+
* A pricing lookup must never fail the call that already succeeded, so a
|
|
227
|
+
* failure here is an absent price rather than an error - the tokens are still
|
|
228
|
+
* reported, and `sessions usage --refresh` can still get the settled figure.
|
|
229
|
+
*/
|
|
230
|
+
async function priceModel(runtime, modelId, needed) {
|
|
231
|
+
if (!needed)
|
|
232
|
+
return undefined;
|
|
233
|
+
try {
|
|
234
|
+
return (await fetchCatalog(runtime.accountPlane)).find((m) => m.modelId === modelId);
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
return undefined;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
140
240
|
async function streamChat(runtime, body, args, io) {
|
|
141
|
-
const { response } = await runtime.dataPlane.postStream("/chat/completions", body, {
|
|
241
|
+
const { response, requestId } = await runtime.dataPlane.postStream("/chat/completions", body, {
|
|
142
242
|
signal: io.signal,
|
|
143
243
|
});
|
|
144
244
|
// Text goes to stdout as it arrives; everything else is stderr, so
|
|
145
245
|
// `mafdet chat ... > answer.txt` captures only the answer.
|
|
146
246
|
const result = await readChatStream(response, { onText: io.stdout }, io.signal);
|
|
147
247
|
io.stdout("\n");
|
|
248
|
+
const model = await priceModel(runtime, args.model, !args.quiet || args.save);
|
|
148
249
|
if (io.signal?.aborted) {
|
|
149
250
|
io.stderr("mafdet: cancelled - the connection was closed mid-generation\n");
|
|
150
|
-
|
|
251
|
+
summarise(args, result.usage, io, model, { partial: true });
|
|
252
|
+
// A cancelled chat is not saved: the answer is incomplete, and an archive
|
|
253
|
+
// of half a reply is a worse record than none.
|
|
151
254
|
return ExitCode.Cancelled;
|
|
152
255
|
}
|
|
153
|
-
|
|
256
|
+
summarise(args, result.usage, io, model);
|
|
257
|
+
saveChat(args, io, result.text, result.usage, requestId, model);
|
|
154
258
|
return ExitCode.Ok;
|
|
155
259
|
}
|
|
156
260
|
async function completeChat(runtime, body, args, io) {
|
|
157
|
-
const { data } = await runtime.dataPlane.postJson("/chat/completions", body, { signal: io.signal });
|
|
158
|
-
|
|
159
|
-
|
|
261
|
+
const { data, requestId } = await runtime.dataPlane.postJson("/chat/completions", body, { signal: io.signal });
|
|
262
|
+
const answer = data.choices?.[0]?.message?.content ?? "";
|
|
263
|
+
io.stdout(`${answer}\n`);
|
|
264
|
+
const model = await priceModel(runtime, args.model, !args.quiet || args.save);
|
|
265
|
+
summarise(args, data.usage, io, model);
|
|
266
|
+
saveChat(args, io, answer, data.usage, requestId, model);
|
|
160
267
|
return ExitCode.Ok;
|
|
161
268
|
}
|
|
162
269
|
/**
|
|
@@ -165,20 +272,13 @@ async function completeChat(runtime, body, args, io) {
|
|
|
165
272
|
* verified against the ledger to the micro-dollar. It is still labelled as an
|
|
166
273
|
* estimate: the wallet, not the client, is the authority.
|
|
167
274
|
*/
|
|
168
|
-
|
|
275
|
+
function summarise(args, usage, io, model, options = {}) {
|
|
169
276
|
if (args.quiet || !usage)
|
|
170
277
|
return;
|
|
171
278
|
let cost = "";
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
const micros = estimateChargeMicroUsd(model, usage);
|
|
176
|
-
if (micros !== undefined)
|
|
177
|
-
cost = `, ~${formatUsd(micros)}`;
|
|
178
|
-
}
|
|
179
|
-
catch {
|
|
180
|
-
// A pricing lookup must never fail the call that already succeeded.
|
|
181
|
-
}
|
|
279
|
+
const micros = estimateChargeMicroUsd(model, usage);
|
|
280
|
+
if (micros !== undefined)
|
|
281
|
+
cost = `, ~${formatUsd(micros)}`;
|
|
182
282
|
const prefix = options.partial ? "partial: " : "";
|
|
183
283
|
io.stderr(`${prefix}${usage.prompt_tokens} in / ${usage.completion_tokens} out tokens${cost}\n`);
|
|
184
284
|
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { ExitCode } from "../output/exit-codes.js";
|
|
2
|
+
import { type CommandIo } from "./runtime.js";
|
|
3
|
+
export declare function sessionsCommand(argv: readonly string[], io: CommandIo): Promise<ExitCode>;
|
|
4
|
+
export declare const DELETION_NOTICE: (id: string) => string;
|
|
5
|
+
//# sourceMappingURL=sessions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sessions.d.ts","sourceRoot":"","sources":["../../src/commands/sessions.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAuBnD,OAAO,EAIL,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAgBtB,wBAAsB,eAAe,CACnC,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,EAAE,EAAE,SAAS,GACZ,OAAO,CAAC,QAAQ,CAAC,CAenB;AAqID,eAAO,MAAM,eAAe,GAAI,IAAI,MAAM,KAAG,MAUX,CAAC"}
|