@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.
Files changed (80) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.md +79 -12
  3. package/README.zh-CN.md +69 -12
  4. package/dist/agent/context-budget.d.ts +37 -0
  5. package/dist/agent/context-budget.d.ts.map +1 -0
  6. package/dist/agent/context-budget.js +105 -0
  7. package/dist/agent/loop.d.ts +25 -0
  8. package/dist/agent/loop.d.ts.map +1 -1
  9. package/dist/agent/loop.js +36 -3
  10. package/dist/agent/session.d.ts +5 -0
  11. package/dist/agent/session.d.ts.map +1 -1
  12. package/dist/agent/session.js +1 -1
  13. package/dist/agent/tools/read-only.d.ts.map +1 -1
  14. package/dist/agent/tools/read-only.js +6 -0
  15. package/dist/agent/tools/run-command.d.ts +28 -0
  16. package/dist/agent/tools/run-command.d.ts.map +1 -1
  17. package/dist/agent/tools/run-command.js +34 -13
  18. package/dist/api/account.d.ts +43 -0
  19. package/dist/api/account.d.ts.map +1 -1
  20. package/dist/api/account.js +40 -0
  21. package/dist/commands/chat.d.ts.map +1 -1
  22. package/dist/commands/chat.js +118 -18
  23. package/dist/commands/sessions.d.ts +5 -0
  24. package/dist/commands/sessions.d.ts.map +1 -0
  25. package/dist/commands/sessions.js +379 -0
  26. package/dist/config/paths.d.ts +7 -0
  27. package/dist/config/paths.d.ts.map +1 -1
  28. package/dist/config/paths.js +24 -0
  29. package/dist/filesystem/protected-dirs.d.ts +26 -0
  30. package/dist/filesystem/protected-dirs.d.ts.map +1 -0
  31. package/dist/filesystem/protected-dirs.js +78 -0
  32. package/dist/filesystem/workspace.d.ts +12 -1
  33. package/dist/filesystem/workspace.d.ts.map +1 -1
  34. package/dist/filesystem/workspace.js +24 -3
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +8 -0
  37. package/dist/security/redact.d.ts +26 -3
  38. package/dist/security/redact.d.ts.map +1 -1
  39. package/dist/security/redact.js +48 -3
  40. package/dist/sessions/index-file.d.ts +38 -0
  41. package/dist/sessions/index-file.d.ts.map +1 -0
  42. package/dist/sessions/index-file.js +174 -0
  43. package/dist/sessions/lock.d.ts +39 -0
  44. package/dist/sessions/lock.d.ts.map +1 -0
  45. package/dist/sessions/lock.js +134 -0
  46. package/dist/sessions/open.d.ts +45 -0
  47. package/dist/sessions/open.d.ts.map +1 -0
  48. package/dist/sessions/open.js +96 -0
  49. package/dist/sessions/policy.d.ts +32 -0
  50. package/dist/sessions/policy.d.ts.map +1 -0
  51. package/dist/sessions/policy.js +57 -0
  52. package/dist/sessions/reconcile.d.ts +49 -0
  53. package/dist/sessions/reconcile.d.ts.map +1 -0
  54. package/dist/sessions/reconcile.js +115 -0
  55. package/dist/sessions/recorder.d.ts +42 -0
  56. package/dist/sessions/recorder.d.ts.map +1 -0
  57. package/dist/sessions/recorder.js +147 -0
  58. package/dist/sessions/resume.d.ts +67 -0
  59. package/dist/sessions/resume.d.ts.map +1 -0
  60. package/dist/sessions/resume.js +100 -0
  61. package/dist/sessions/store.d.ts +141 -0
  62. package/dist/sessions/store.d.ts.map +1 -0
  63. package/dist/sessions/store.js +272 -0
  64. package/dist/sessions/usage.d.ts +35 -0
  65. package/dist/sessions/usage.d.ts.map +1 -0
  66. package/dist/sessions/usage.js +157 -0
  67. package/dist/tui/agent-surface.d.ts +3 -0
  68. package/dist/tui/agent-surface.d.ts.map +1 -1
  69. package/dist/tui/agent-surface.js +23 -1
  70. package/dist/tui/app.d.ts.map +1 -1
  71. package/dist/tui/app.js +18 -1
  72. package/dist/tui/route.d.ts.map +1 -1
  73. package/dist/tui/route.js +19 -2
  74. package/dist/tui/session-centre.d.ts +15 -0
  75. package/dist/tui/session-centre.d.ts.map +1 -0
  76. package/dist/tui/session-centre.js +59 -0
  77. package/dist/tui/start-agent.d.ts +9 -0
  78. package/dist/tui/start-agent.d.ts.map +1 -1
  79. package/dist/tui/start-agent.js +122 -0
  80. 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;AAwBD,eAAO,MAAM,cAAc,EAAE,cAmC5B,CAAC;AAEF,eAAO,MAAM,aAAa,EAAE,cAAc,EAAqB,CAAC"}
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 SIGTERM still has to go.
52
- setTimeout(() => killTree(child.pid, "SIGKILL"), 2000).unref();
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 process group, not just the child.
77
+ * Kill the whole tree.
64
78
  *
65
- * Killing the parent alone leaves grandchildren running - the familiar case
66
- * where a "timed out" test runner keeps holding a port. The negative pid is
67
- * POSIX; Windows has no process groups in this sense, so there the direct
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, signal = "SIGTERM") {
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 (process.platform === "win32")
76
- process.kill(pid, signal);
77
- else
78
- process.kill(-pid, signal);
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.
@@ -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"}
@@ -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;AAExE,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAEnD,OAAO,EAIL,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAUtB,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;AAWD,wBAAsB,WAAW,CAC/B,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,EAAE,EAAE,MAAM,GACT,OAAO,CAAC,QAAQ,CAAC,CA8BnB;AAwLD,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,YAAY,GAAG,SAAS,EAC/B,KAAK,EAAE,WAAW,GACjB,MAAM,GAAG,SAAS,CASpB"}
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"}
@@ -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
- await summarise(runtime, args, result.usage, io, { partial: true });
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
- await summarise(runtime, args, result.usage, io);
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
- io.stdout(`${data.choices?.[0]?.message?.content ?? ""}\n`);
159
- await summarise(runtime, args, data.usage, io);
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
- async function summarise(runtime, args, usage, io, options = {}) {
275
+ function summarise(args, usage, io, model, options = {}) {
169
276
  if (args.quiet || !usage)
170
277
  return;
171
278
  let cost = "";
172
- try {
173
- const catalog = await fetchCatalog(runtime.accountPlane);
174
- const model = catalog.find((m) => m.modelId === args.model);
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"}