@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.
Files changed (61) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/README.md +35 -19
  3. package/README.zh-CN.md +28 -16
  4. package/dist/agent/context-budget.d.ts +69 -0
  5. package/dist/agent/context-budget.d.ts.map +1 -0
  6. package/dist/agent/context-budget.js +169 -0
  7. package/dist/agent/loop.d.ts +19 -1
  8. package/dist/agent/loop.d.ts.map +1 -1
  9. package/dist/agent/loop.js +32 -0
  10. package/dist/agent/session.d.ts.map +1 -1
  11. package/dist/agent/session.js +7 -1
  12. package/dist/agent/tools/run-command.d.ts +28 -0
  13. package/dist/agent/tools/run-command.d.ts.map +1 -1
  14. package/dist/agent/tools/run-command.js +34 -13
  15. package/dist/api/http.d.ts +15 -1
  16. package/dist/api/http.d.ts.map +1 -1
  17. package/dist/api/http.js +26 -5
  18. package/dist/api/sse-parser.d.ts +26 -1
  19. package/dist/api/sse-parser.d.ts.map +1 -1
  20. package/dist/api/sse-parser.js +62 -21
  21. package/dist/commands/agent.d.ts.map +1 -1
  22. package/dist/commands/agent.js +76 -0
  23. package/dist/commands/chat.d.ts.map +1 -1
  24. package/dist/commands/chat.js +136 -19
  25. package/dist/commands/config.d.ts.map +1 -1
  26. package/dist/commands/config.js +0 -2
  27. package/dist/commands/runtime.d.ts +6 -0
  28. package/dist/commands/runtime.d.ts.map +1 -1
  29. package/dist/commands/sessions.d.ts.map +1 -1
  30. package/dist/commands/sessions.js +129 -18
  31. package/dist/config/config.d.ts +0 -12
  32. package/dist/config/config.d.ts.map +1 -1
  33. package/dist/config/config.js +31 -6
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +17 -1
  36. package/dist/output/format.d.ts.map +1 -1
  37. package/dist/output/format.js +10 -5
  38. package/dist/output/terminal-safe.d.ts +12 -0
  39. package/dist/output/terminal-safe.d.ts.map +1 -0
  40. package/dist/output/terminal-safe.js +38 -0
  41. package/dist/sessions/open.d.ts.map +1 -1
  42. package/dist/sessions/open.js +26 -2
  43. package/dist/sessions/recorder.d.ts.map +1 -1
  44. package/dist/sessions/recorder.js +6 -1
  45. package/dist/sessions/session-id.d.ts +16 -0
  46. package/dist/sessions/session-id.d.ts.map +1 -0
  47. package/dist/sessions/session-id.js +93 -0
  48. package/dist/sessions/store.d.ts +41 -2
  49. package/dist/sessions/store.d.ts.map +1 -1
  50. package/dist/sessions/store.js +48 -24
  51. package/dist/sessions/usage.d.ts +6 -0
  52. package/dist/sessions/usage.d.ts.map +1 -1
  53. package/dist/sessions/usage.js +12 -1
  54. package/dist/tui/agent-surface.d.ts.map +1 -1
  55. package/dist/tui/agent-surface.js +13 -2
  56. package/dist/tui/app.d.ts.map +1 -1
  57. package/dist/tui/app.js +43 -10
  58. package/dist/tui/start-agent.d.ts +31 -0
  59. package/dist/tui/start-agent.d.ts.map +1 -1
  60. package/dist/tui/start-agent.js +147 -45
  61. package/package.json +1 -1
@@ -2,15 +2,24 @@ 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 { safeForTerminal, terminalSafeWriter, } from "../output/terminal-safe.js";
6
+ import { sessionsDir } from "../config/paths.js";
7
+ import { keyDigest } from "../security/redact.js";
8
+ import { upsertIndexEntry } from "../sessions/index-file.js";
9
+ import { decideRecording } from "../sessions/policy.js";
10
+ import { SessionWriter } from "../sessions/store.js";
5
11
  import { ExitCode } from "../output/exit-codes.js";
6
12
  import { formatUsd } from "../output/money.js";
7
13
  import { buildRuntime, reportError, requireApiKey, } from "./runtime.js";
8
14
  const USAGE = `Usage:
9
15
  mafdet chat [prompt] [--model <id>] [--system <file>] [--input <file>]
10
- [--no-stream] [--max-tokens <n>] [--quiet]
16
+ [--no-stream] [--max-tokens <n>] [--quiet] [--save]
11
17
 
12
18
  The prompt comes from an argument, from --input <file>, or from stdin when
13
19
  neither is given. Use "--input -" to force reading stdin.
20
+
21
+ A single chat is not saved unless --save is given. Saved conversations stay
22
+ on this computer; see 'mafdet sessions'.
14
23
  `;
15
24
  export async function chatCommand(argv, io) {
16
25
  try {
@@ -51,6 +60,8 @@ async function parseArgs(argv, configuredModel, io) {
51
60
  let inputFile;
52
61
  let stream = true;
53
62
  let quiet = false;
63
+ let save = false;
64
+ let noHistory = false;
54
65
  let maxTokens;
55
66
  for (let i = 0; i < argv.length; i += 1) {
56
67
  const arg = argv[i];
@@ -65,6 +76,10 @@ async function parseArgs(argv, configuredModel, io) {
65
76
  stream = false;
66
77
  else if (arg === "--quiet")
67
78
  quiet = true;
79
+ else if (arg === "--save")
80
+ save = true;
81
+ else if (arg === "--no-history")
82
+ noHistory = true;
68
83
  else if (arg === "--max-tokens") {
69
84
  const raw = next();
70
85
  maxTokens = Number(raw);
@@ -123,6 +138,8 @@ async function parseArgs(argv, configuredModel, io) {
123
138
  stream,
124
139
  maxTokens,
125
140
  quiet,
141
+ save,
142
+ noHistory,
126
143
  };
127
144
  }
128
145
  function readTextFile(path, flag) {
@@ -137,26 +154,133 @@ function readTextFile(path, flag) {
137
154
  });
138
155
  }
139
156
  }
157
+ /**
158
+ * CLI-024/§20.4 · Save a one-shot chat, when asked.
159
+ *
160
+ * A single `mafdet chat` is not a conversation anyone resumes, so it is not
161
+ * recorded by default; `--save` opts in and `--no-history` still wins over it.
162
+ * Everything written goes through the same store as an interactive session, so
163
+ * the same redaction, the same 0600 files and the same `mafdet sessions
164
+ * delete` apply - there is no second, weaker path to disk.
165
+ *
166
+ * Failures here are swallowed: losing the record of a chat is bad, losing the
167
+ * answer the user is waiting for is worse.
168
+ */
169
+ function saveChat(args, io, answer, usage, requestId, model) {
170
+ const decision = decideRecording({
171
+ surface: "one-shot",
172
+ noHistory: args.noHistory,
173
+ save: args.save,
174
+ });
175
+ if (!decision.record)
176
+ return;
177
+ const env = io.env ?? process.env;
178
+ try {
179
+ const root = sessionsDir(env);
180
+ const writer = SessionWriter.create({
181
+ root,
182
+ title: chatTitle(args.prompt),
183
+ modelId: args.model,
184
+ workspace: io.cwd ?? process.cwd(),
185
+ mode: "chat",
186
+ });
187
+ if (args.system !== undefined) {
188
+ writer.append({ type: "user_message", content: args.system });
189
+ }
190
+ writer.append({ type: "user_message", content: args.prompt });
191
+ writer.append({ type: "assistant_message", content: answer });
192
+ if (usage) {
193
+ const micros = estimateChargeMicroUsd(model, usage);
194
+ writer.append({
195
+ type: "usage",
196
+ ...(requestId === undefined ? {} : { request_id: requestId }),
197
+ model_id: args.model,
198
+ prompt_tokens: usage.prompt_tokens,
199
+ completion_tokens: usage.completion_tokens,
200
+ // F-012 · Omitted rather than zeroed when the catalogue could not
201
+ // price it: "we do not know" must not be recorded as "it was free".
202
+ ...(micros === undefined
203
+ ? {}
204
+ : { estimated_charge_micro_usd: micros.toString() }),
205
+ settlement_status: "pending",
206
+ api_key_ref: keyDigest(readKey(io)),
207
+ });
208
+ }
209
+ writer.close();
210
+ upsertIndexEntry(root, writer.metadata);
211
+ if (!args.quiet) {
212
+ io.stderr(`saved as ${writer.sessionId} (local only)\n`);
213
+ }
214
+ }
215
+ catch (error) {
216
+ io.stderr(`mafdet chat: not saved: ${error instanceof Error ? error.message : String(error)}\n`);
217
+ }
218
+ }
219
+ /** The first line, trimmed to something that fits a listing. */
220
+ function chatTitle(prompt) {
221
+ const line = prompt.split("\n").find((l) => l.trim().length > 0) ?? "Chat";
222
+ const trimmed = line.trim();
223
+ return trimmed.length > 60 ? `${trimmed.slice(0, 57)}...` : trimmed;
224
+ }
225
+ /** Whether a terminal is on the other end of stdout. Injectable for tests. */
226
+ function isTty(io) {
227
+ return io.isTty ?? process.stdout.isTTY === true;
228
+ }
229
+ function readKey(io) {
230
+ return (io.env ?? process.env).MAFDET_API_KEY ?? "";
231
+ }
232
+ /**
233
+ * The catalogue entry for pricing, fetched once and shared.
234
+ *
235
+ * A pricing lookup must never fail the call that already succeeded, so a
236
+ * failure here is an absent price rather than an error - the tokens are still
237
+ * reported, and `sessions usage --refresh` can still get the settled figure.
238
+ */
239
+ async function priceModel(runtime, modelId, needed) {
240
+ if (!needed)
241
+ return undefined;
242
+ try {
243
+ return (await fetchCatalog(runtime.accountPlane)).find((m) => m.modelId === modelId);
244
+ }
245
+ catch {
246
+ return undefined;
247
+ }
248
+ }
140
249
  async function streamChat(runtime, body, args, io) {
141
- const { response } = await runtime.dataPlane.postStream("/chat/completions", body, {
250
+ const { response, requestId, abort, limits } = await runtime.dataPlane.postStream("/chat/completions", body, {
142
251
  signal: io.signal,
143
252
  });
144
253
  // Text goes to stdout as it arrives; everything else is stderr, so
145
254
  // `mafdet chat ... > answer.txt` captures only the answer.
146
- const result = await readChatStream(response, { onText: io.stdout }, io.signal);
255
+ // F-009 · The limits travel with the stream: without them a gateway that
256
+ // sent headers and then went quiet would hang here indefinitely.
257
+ // F-010 · Sanitised only when a terminal is reading: `mafdet chat >
258
+ // answer.txt` and `| jq` must get the bytes the model sent.
259
+ const write = terminalSafeWriter(io.stdout, isTty(io));
260
+ const result = await readChatStream(response, { onText: write }, io.signal, {
261
+ ...limits,
262
+ abort,
263
+ });
147
264
  io.stdout("\n");
265
+ const model = await priceModel(runtime, args.model, !args.quiet || args.save);
148
266
  if (io.signal?.aborted) {
149
267
  io.stderr("mafdet: cancelled - the connection was closed mid-generation\n");
150
- await summarise(runtime, args, result.usage, io, { partial: true });
268
+ summarise(args, result.usage, io, model, { partial: true });
269
+ // A cancelled chat is not saved: the answer is incomplete, and an archive
270
+ // of half a reply is a worse record than none.
151
271
  return ExitCode.Cancelled;
152
272
  }
153
- await summarise(runtime, args, result.usage, io);
273
+ summarise(args, result.usage, io, model);
274
+ saveChat(args, io, result.text, result.usage, requestId, model);
154
275
  return ExitCode.Ok;
155
276
  }
156
277
  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);
278
+ const { data, requestId } = await runtime.dataPlane.postJson("/chat/completions", body, { signal: io.signal });
279
+ const answer = data.choices?.[0]?.message?.content ?? "";
280
+ io.stdout(`${safeForTerminal(answer, isTty(io))}\n`);
281
+ const model = await priceModel(runtime, args.model, !args.quiet || args.save);
282
+ summarise(args, data.usage, io, model);
283
+ saveChat(args, io, answer, data.usage, requestId, model);
160
284
  return ExitCode.Ok;
161
285
  }
162
286
  /**
@@ -165,20 +289,13 @@ async function completeChat(runtime, body, args, io) {
165
289
  * verified against the ledger to the micro-dollar. It is still labelled as an
166
290
  * estimate: the wallet, not the client, is the authority.
167
291
  */
168
- async function summarise(runtime, args, usage, io, options = {}) {
292
+ function summarise(args, usage, io, model, options = {}) {
169
293
  if (args.quiet || !usage)
170
294
  return;
171
295
  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
- }
296
+ const micros = estimateChargeMicroUsd(model, usage);
297
+ if (micros !== undefined)
298
+ cost = `, ~${formatUsd(micros)}`;
182
299
  const prefix = options.partial ? "partial: " : "";
183
300
  io.stderr(`${prefix}${usage.prompt_tokens} in / ${usage.completion_tokens} out tokens${cost}\n`);
184
301
  }
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/commands/config.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAOnD,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,EAAE,EAAE,SAAS,GACZ,QAAQ,CAwEV"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/commands/config.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAOnD,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,EAAE,EAAE,SAAS,GACZ,QAAQ,CAsEV"}
@@ -44,7 +44,6 @@ export function configCommand(argv, io) {
44
44
  model_source: config.modelSource,
45
45
  base_url: config.baseUrl,
46
46
  backend_url: config.backendUrl,
47
- approval_mode: config.approvalMode,
48
47
  api_key: config.apiKey ? keyFingerprint(config.apiKey) : null,
49
48
  user_config: config.sources.userConfigFound
50
49
  ? config.sources.userConfigPath
@@ -61,7 +60,6 @@ export function configCommand(argv, io) {
61
60
  ["model", view.model ? `${view.model} (${view.model_source})` : "<unset>"],
62
61
  ["base url", view.base_url],
63
62
  ["backend url", view.backend_url],
64
- ["approval mode", view.approval_mode],
65
63
  ["api key", view.api_key ?? "<unset> (set MAFDET_API_KEY)"],
66
64
  ["user config", view.user_config ?? "<none>"],
67
65
  ["project config", view.project_config ?? "<none>"],
@@ -10,6 +10,12 @@ export interface CommandIo {
10
10
  /** Injected by tests; production uses global fetch. */
11
11
  fetchImpl?: typeof fetch;
12
12
  version?: string;
13
+ /**
14
+ * F-010 · Whether stdout is a terminal. Untrusted text is stripped of
15
+ * control sequences only then: a pipe or a file must receive exact bytes.
16
+ * Defaults to `process.stdout.isTTY`; tests set it explicitly.
17
+ */
18
+ isTty?: boolean;
13
19
  }
14
20
  export interface Runtime {
15
21
  config: LoadedConfig;
@@ -1 +1 @@
1
- {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/commands/runtime.ts"],"names":[],"mappings":"AACA,OAAO,EAAc,KAAK,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAClE,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAE5C,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAEnD,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,uDAAuD;IACvD,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,OAAO;IACtB,MAAM,EAAE,YAAY,CAAC;IACrB,sCAAsC;IACtC,SAAS,EAAE,UAAU,CAAC;IACtB,yCAAyC;IACzC,YAAY,EAAE,UAAU,CAAC;CAC1B;AAED,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAGjD;AAED,wBAAgB,YAAY,CAAC,EAAE,EAAE,SAAS,GAAG,OAAO,CAgBnD;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI,CASxD;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE,SAAS,GAAG,QAAQ,CAkBnE"}
1
+ {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/commands/runtime.ts"],"names":[],"mappings":"AACA,OAAO,EAAc,KAAK,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAClE,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAE5C,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAEnD,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,uDAAuD;IACvD,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,OAAO;IACtB,MAAM,EAAE,YAAY,CAAC;IACrB,sCAAsC;IACtC,SAAS,EAAE,UAAU,CAAC;IACtB,yCAAyC;IACzC,YAAY,EAAE,UAAU,CAAC;CAC1B;AAED,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAGjD;AAED,wBAAgB,YAAY,CAAC,EAAE,EAAE,SAAS,GAAG,OAAO,CAgBnD;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI,CASxD;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE,SAAS,GAAG,QAAQ,CAkBnE"}
@@ -1 +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"}
1
+ {"version":3,"file":"sessions.d.ts","sourceRoot":"","sources":["../../src/commands/sessions.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AA4BnD,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,CAUnB;AA8PD,eAAO,MAAM,eAAe,GAAI,IAAI,MAAM,KAAG,MAUX,CAAC"}
@@ -11,14 +11,15 @@ import { sessionsDir } from "../config/paths.js";
11
11
  import { formatUsd } from "../output/money.js";
12
12
  import { ExitCode } from "../output/exit-codes.js";
13
13
  import { parseFormatFlags, renderJson, renderRecord, renderTable, } from "../output/format.js";
14
+ import { safeForTerminal } from "../output/terminal-safe.js";
14
15
  import { keyDigest } from "../security/redact.js";
15
16
  import { loadIndex, removeIndexEntry } from "../sessions/index-file.js";
16
17
  import { acquireSessionLock, SessionLockedError } from "../sessions/lock.js";
17
18
  import { daysToCover, matchSettlements, planReconciliation, } from "../sessions/reconcile.js";
19
+ import { InvalidSessionIdError, resolveSessionDirectory, } from "../sessions/session-id.js";
18
20
  import { deleteSession, readSession, SessionWriter, } from "../sessions/store.js";
19
21
  import { settledCaveat, summariseUsage } from "../sessions/usage.js";
20
22
  import { buildRuntime, reportError, requireApiKey, } from "./runtime.js";
21
- import { join } from "node:path";
22
23
  const USAGE = `Usage:
23
24
  mafdet sessions list [--json|--plain]
24
25
  mafdet sessions show <session-id> [--json]
@@ -32,6 +33,18 @@ calls a model and is never charged.
32
33
  cost. It sends request IDs and nothing else - no messages, no titles.
33
34
  `;
34
35
  export async function sessionsCommand(argv, io) {
36
+ // F-013 · Nothing here may reach the top level as a raw Error. `--json
37
+ // --plain` used to print an ESM stack trace and exit 1 instead of a usage
38
+ // message, and every other command surface already funnels through
39
+ // reportError.
40
+ try {
41
+ return await run(argv, io);
42
+ }
43
+ catch (error) {
44
+ return reportError(error, io);
45
+ }
46
+ }
47
+ async function run(argv, io) {
35
48
  const [sub, ...rest] = argv;
36
49
  const root = sessionsDir(io.env ?? process.env);
37
50
  if (sub === undefined || sub === "help" || sub === "--help") {
@@ -67,9 +80,12 @@ function list(argv, io, root) {
67
80
  return ExitCode.Ok;
68
81
  }
69
82
  io.stdout(renderTable(sessions, [
70
- { header: "SESSION", value: (s) => s.session_id },
71
- { header: "TITLE", value: (s) => s.title },
72
- { header: "MODEL", value: (s) => s.model_id },
83
+ // F-010 · Every one of these came off disk. A transcript is a file, and
84
+ // a title carrying an OSC sequence would retitle the window of anyone
85
+ // who ran `sessions list`.
86
+ { header: "SESSION", value: (s) => safe(s.session_id, io) },
87
+ { header: "TITLE", value: (s) => safe(s.title, io) },
88
+ { header: "MODEL", value: (s) => safe(s.model_id, io) },
73
89
  { header: "REQUESTS", value: (s) => `${s.model_requests}` },
74
90
  {
75
91
  header: "EST. COST",
@@ -80,13 +96,17 @@ function list(argv, io, root) {
80
96
  return ExitCode.Ok;
81
97
  }
82
98
  function show(argv, io, root) {
83
- const { format } = parseFormatFlags(argv);
84
- const id = argv.find((arg) => !arg.startsWith("-"));
99
+ const { format, rest } = parseFormatFlags(argv);
100
+ const id = rest[0];
85
101
  if (id === undefined) {
86
102
  io.stderr(`mafdet sessions show: no session id\n${USAGE}`);
87
103
  return ExitCode.Usage;
88
104
  }
89
- const directory = join(root, id);
105
+ if (!checkExtra("show", rest, io))
106
+ return ExitCode.Usage;
107
+ const directory = locate(root, id, io);
108
+ if (directory === undefined)
109
+ return ExitCode.Usage;
90
110
  if (!existsSync(directory)) {
91
111
  io.stderr(`mafdet sessions: no session '${id}' on this computer\n`);
92
112
  return ExitCode.General;
@@ -104,16 +124,17 @@ function show(argv, io, root) {
104
124
  metadata: session.metadata,
105
125
  events: session.events,
106
126
  truncated_tail: session.truncatedTail,
127
+ corrupt_lines: session.corruptLines,
107
128
  }));
108
129
  return ExitCode.Ok;
109
130
  }
110
131
  const m = session.metadata;
111
132
  io.stdout(renderRecord([
112
- ["session", m.session_id],
113
- ["title", m.title],
114
- ["model", m.model_id],
115
- ["workspace", m.workspace],
116
- ["mode", m.mode],
133
+ ["session", safe(m.session_id, io)],
134
+ ["title", safe(m.title, io)],
135
+ ["model", safe(m.model_id, io)],
136
+ ["workspace", safe(m.workspace, io)],
137
+ ["mode", safe(m.mode, io)],
117
138
  ["created", m.created_at],
118
139
  ["updated", m.updated_at],
119
140
  ["messages", `${m.message_count}`],
@@ -125,16 +146,29 @@ function show(argv, io, root) {
125
146
  // Worth saying plainly: the user should know a crash cost them the tail.
126
147
  io.stderr("\nnote: the last event was incomplete, most likely an interrupted run.\n");
127
148
  }
149
+ if (session.corruptLines.length > 0) {
150
+ // F-016 · A hole in the middle is not an interrupted run - it is
151
+ // corruption or interleaved writes, and part of the conversation is
152
+ // simply gone. Presenting the rest as complete would be a lie.
153
+ io.stderr(`\nwarning: ${session.corruptLines.length} unreadable line(s) in this ` +
154
+ `transcript (line ${session.corruptLines.join(", ")}); ` +
155
+ `some of the conversation is missing.\n`);
156
+ }
128
157
  return ExitCode.Ok;
129
158
  }
130
159
  function remove(argv, io, root) {
131
- const id = argv.find((arg) => !arg.startsWith("-"));
160
+ const { rest } = parseFormatFlags(argv);
161
+ const id = rest[0];
132
162
  const confirmed = argv.includes("--yes");
133
163
  if (id === undefined) {
134
164
  io.stderr(`mafdet sessions delete: no session id\n${USAGE}`);
135
165
  return ExitCode.Usage;
136
166
  }
137
- const directory = join(root, id);
167
+ if (!checkExtra("delete", rest, io, ["--yes"]))
168
+ return ExitCode.Usage;
169
+ const directory = locate(root, id, io);
170
+ if (directory === undefined)
171
+ return ExitCode.Usage;
138
172
  if (!existsSync(directory)) {
139
173
  io.stderr(`mafdet sessions: no session '${id}' on this computer\n`);
140
174
  return ExitCode.General;
@@ -146,11 +180,84 @@ function remove(argv, io, root) {
146
180
  io.stderr(DELETION_NOTICE(id));
147
181
  return ExitCode.Usage;
148
182
  }
149
- deleteSession(directory);
183
+ // F-002 · Under the lock: a session open in another window is still writing
184
+ // to this directory, and deleting underneath it leaves the live process to
185
+ // recreate half a session.
186
+ let lock;
187
+ try {
188
+ lock = acquireSessionLock(directory);
189
+ }
190
+ catch (error) {
191
+ // Not only SessionLockedError: an unwritable directory throws EACCES here,
192
+ // and letting that escape would print a Node stack trace at the user.
193
+ io.stderr(`mafdet sessions: cannot delete ${id}: ${error instanceof Error ? error.message : String(error)}\n`);
194
+ return ExitCode.General;
195
+ }
196
+ let result;
197
+ try {
198
+ result = deleteSession(directory);
199
+ }
200
+ finally {
201
+ // The lock file lives inside the directory, so a successful delete already
202
+ // took it with it; releasing is for the failure path.
203
+ try {
204
+ lock.release();
205
+ }
206
+ catch {
207
+ // Gone with the directory.
208
+ }
209
+ }
210
+ if (!result.removed) {
211
+ // F-002 · Never print "deleted" for something still on disk.
212
+ io.stderr(`mafdet sessions: ${id} was not fully deleted\n`);
213
+ for (const failure of result.failures) {
214
+ io.stderr(` ${failure.path}: ${failure.reason}\n`);
215
+ }
216
+ // The index is rebuilt from the directories, so leaving the entry keeps
217
+ // the listing honest about what is still there.
218
+ return ExitCode.General;
219
+ }
150
220
  removeIndexEntry(root, id);
151
221
  io.stdout(`deleted ${id}\n`);
152
222
  return ExitCode.Ok;
153
223
  }
224
+ /**
225
+ * F-001 · Turn an id into a directory, or say why not.
226
+ *
227
+ * The single boundary: `show`, `usage` and `delete` all come through here, so
228
+ * there is no subcommand left that can build the path its own way. Returns
229
+ * undefined after reporting, because every caller's response is the same.
230
+ */
231
+ /**
232
+ * F-014 · Refuse what we were not asked to do.
233
+ *
234
+ * A mistyped flag or a stray second argument used to be dropped silently, so
235
+ * `sessions delete a b --yes` deleted `a` and said nothing about `b`, and
236
+ * `--dry-run` looked like it worked. Anything not recognised is an error.
237
+ */
238
+ /** F-010 · Terminal-safe when a terminal is reading, exact bytes otherwise. */
239
+ function safe(text, io) {
240
+ return safeForTerminal(text, io.isTty ?? process.stdout.isTTY === true);
241
+ }
242
+ function checkExtra(name, rest, io, known = []) {
243
+ const leftover = rest.slice(1).filter((arg) => !known.includes(arg));
244
+ if (leftover.length === 0)
245
+ return true;
246
+ io.stderr(`mafdet sessions ${name}: unexpected argument '${leftover[0]}'\n${USAGE}`);
247
+ return false;
248
+ }
249
+ function locate(root, id, io) {
250
+ try {
251
+ return resolveSessionDirectory(root, id);
252
+ }
253
+ catch (error) {
254
+ if (error instanceof InvalidSessionIdError) {
255
+ io.stderr(`mafdet sessions: ${error.message}\n`);
256
+ return undefined;
257
+ }
258
+ throw error;
259
+ }
260
+ }
154
261
  export const DELETION_NOTICE = (id) => `Deleting ${id} removes local messages and its per-session usage mapping\n` +
155
262
  `from this computer.\n\n` +
156
263
  `It does not remove API usage, billing records, or charges.\n\n` +
@@ -170,13 +277,17 @@ export const DELETION_NOTICE = (id) => `Deleting ${id} removes local messages an
170
277
  * would be a smaller number than the truth.
171
278
  */
172
279
  async function usage(argv, io, root) {
173
- const { format } = parseFormatFlags(argv);
174
- const id = argv.find((arg) => !arg.startsWith("-"));
280
+ const { format, rest } = parseFormatFlags(argv);
281
+ const id = rest[0];
175
282
  if (id === undefined) {
176
283
  io.stderr(`mafdet sessions usage: no session id\n${USAGE}`);
177
284
  return ExitCode.Usage;
178
285
  }
179
- const directory = join(root, id);
286
+ if (!checkExtra("usage", rest, io, ["--refresh"]))
287
+ return ExitCode.Usage;
288
+ const directory = locate(root, id, io);
289
+ if (directory === undefined)
290
+ return ExitCode.Usage;
180
291
  if (!existsSync(directory)) {
181
292
  io.stderr(`mafdet sessions: no session '${id}' on this computer\n`);
182
293
  return ExitCode.General;
@@ -6,20 +6,9 @@ export declare class ConfigError extends Error {
6
6
  }
7
7
  declare const projectConfigSchema: z.ZodObject<{
8
8
  model: z.ZodOptional<z.ZodString>;
9
- approvalMode: z.ZodOptional<z.ZodEnum<{
10
- "read-only": "read-only";
11
- suggest: "suggest";
12
- "auto-edit": "auto-edit";
13
- }>>;
14
- testCommands: z.ZodOptional<z.ZodArray<z.ZodString>>;
15
9
  }, z.core.$strict>;
16
10
  declare const userConfigSchema: z.ZodObject<{
17
11
  model: z.ZodOptional<z.ZodString>;
18
- approvalMode: z.ZodOptional<z.ZodEnum<{
19
- "read-only": "read-only";
20
- suggest: "suggest";
21
- "auto-edit": "auto-edit";
22
- }>>;
23
12
  baseUrl: z.ZodOptional<z.ZodString>;
24
13
  backendUrl: z.ZodOptional<z.ZodString>;
25
14
  }, z.core.$strict>;
@@ -48,7 +37,6 @@ export interface ResolvedConfig {
48
37
  modelSource: "flag" | "env" | "project" | "user" | "none";
49
38
  baseUrl: string;
50
39
  backendUrl: string;
51
- approvalMode: "read-only" | "suggest" | "auto-edit";
52
40
  apiKey?: string;
53
41
  }
54
42
  /** Plan §5.1: flag > env > project > user > default. */
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/config/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,eAAO,MAAM,gBAAgB,6BAA6B,CAAC;AAC3D,eAAO,MAAM,mBAAmB,8BAA8B,CAAC;AAE/D,qBAAa,WAAY,SAAQ,KAAK;gBACxB,OAAO,EAAE,MAAM;CAK5B;AAYD,QAAA,MAAM,mBAAmB;;;;;;;;kBAMd,CAAC;AAEZ,QAAA,MAAM,gBAAgB;;;;;;;;;kBAOX,CAAC;AAEZ,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAChE,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAE1D;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,GAAG,CAczE;AAOD,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,UAAU,CAWxE;AAED,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,OAAO,EACZ,MAAM,EAAE,MAAM,GACb,aAAa,CAcf;AAED,MAAM,WAAW,YAAY;IAC3B,4CAA4C;IAC5C,KAAK,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC3B,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,IAAI,CAAC,EAAE,UAAU,CAAC;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iEAAiE;IACjE,WAAW,EAAE,MAAM,GAAG,KAAK,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,CAAC;IAC1D,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,WAAW,GAAG,SAAS,GAAG,WAAW,CAAC;IACpD,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wDAAwD;AACxD,wBAAgB,aAAa,CAAC,KAAK,GAAE,YAAiB,GAAG,cAAc,CAiCtE"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/config/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,eAAO,MAAM,gBAAgB,6BAA6B,CAAC;AAC3D,eAAO,MAAM,mBAAmB,8BAA8B,CAAC;AAE/D,qBAAa,WAAY,SAAQ,KAAK;gBACxB,OAAO,EAAE,MAAM;CAK5B;AAoCD,QAAA,MAAM,mBAAmB;;kBAId,CAAC;AAEZ,QAAA,MAAM,gBAAgB;;;;kBAMX,CAAC;AAUZ,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAChE,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAE1D;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,GAAG,CAczE;AAOD,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,UAAU,CAWxE;AAED,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,OAAO,EACZ,MAAM,EAAE,MAAM,GACb,aAAa,CAef;AAED,MAAM,WAAW,YAAY;IAC3B,4CAA4C;IAC5C,KAAK,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC3B,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,IAAI,CAAC,EAAE,UAAU,CAAC;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iEAAiE;IACjE,WAAW,EAAE,MAAM,GAAG,KAAK,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,CAAC;IAC1D,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wDAAwD;AACxD,wBAAgB,aAAa,CAAC,KAAK,GAAE,YAAiB,GAAG,cAAc,CA8BtE"}
@@ -16,22 +16,48 @@ export class ConfigError extends Error {
16
16
  * silently dropping them would hide an attempt to redirect traffic.
17
17
  */
18
18
  const FORBIDDEN_IN_PROJECT = ["apiKey", "baseUrl", "backendUrl"];
19
- const approvalMode = z.enum(["read-only", "suggest", "auto-edit"]);
19
+ /**
20
+ * F-006 · Accepted, displayed, and never read by anything.
21
+ *
22
+ * `approvalMode` and `testCommands` were in the schema, in `config list` and
23
+ * in the README, and no code consulted either - so a user could set
24
+ * `approvalMode: "auto-edit"` and believe approval had been turned off.
25
+ *
26
+ * They are removed rather than wired up, and the reason is not effort. Both
27
+ * are permission-granting settings, and in a project file that means a cloned
28
+ * repository raising its own permission level: `.mafdet.json` asking for
29
+ * `auto-edit`, or listing its own `testCommands` to run. §8.3 already refuses
30
+ * a global approval flag, and CLI-003 already treats a repository as
31
+ * untrusted input - honouring these would undo both.
32
+ *
33
+ * Rejected with an explanation rather than ignored, so anyone who set one
34
+ * finds out it never did what they thought.
35
+ */
36
+ const REMOVED_KEYS = {
37
+ approvalMode: "it was never implemented, and a config file may not raise the approval " +
38
+ "level - use --allow-write for this run",
39
+ testCommands: "it was never implemented, and a config file may not grant command " +
40
+ "execution - use --allow-command for this run",
41
+ };
20
42
  const projectConfigSchema = z
21
43
  .object({
22
44
  model: z.string().min(1).optional(),
23
- approvalMode: approvalMode.optional(),
24
- testCommands: z.array(z.string().min(1)).optional(),
25
45
  })
26
46
  .strict();
27
47
  const userConfigSchema = z
28
48
  .object({
29
49
  model: z.string().min(1).optional(),
30
- approvalMode: approvalMode.optional(),
31
50
  baseUrl: z.string().url().optional(),
32
51
  backendUrl: z.string().url().optional(),
33
52
  })
34
53
  .strict();
54
+ function rejectRemoved(raw, source) {
55
+ for (const [key, why] of Object.entries(REMOVED_KEYS)) {
56
+ if (hasKey(raw, key)) {
57
+ throw new ConfigError(`${source} sets '${key}': ${why}.`);
58
+ }
59
+ }
60
+ }
35
61
  /**
36
62
  * Remote endpoints must be HTTPS. Plain HTTP is allowed only against the local
37
63
  * machine, where there is no network to intercept — that exception exists so a
@@ -72,6 +98,7 @@ export function parseProjectConfig(raw, source) {
72
98
  `Credentials and endpoints come from the environment or your user config only.`);
73
99
  }
74
100
  }
101
+ rejectRemoved(raw, source);
75
102
  const parsed = projectConfigSchema.safeParse(raw);
76
103
  if (!parsed.success) {
77
104
  throw new ConfigError(`${source} is invalid: ${issueText(parsed.error)}`);
@@ -99,8 +126,6 @@ export function resolveConfig(input = {}) {
99
126
  modelSource,
100
127
  baseUrl,
101
128
  backendUrl,
102
- // read-only is the safe default; a caller raises it explicitly.
103
- approvalMode: input.project?.approvalMode ?? input.user?.approvalMode ?? "read-only",
104
129
  // The key is environment-only, by design (plan §5.4): a flag would put it
105
130
  // in shell history and the process list, a file would outlive its use.
106
131
  apiKey: trimmed(env.MAFDET_API_KEY),
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAeA,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAmDlD,wBAAsB,GAAG,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,QAAQ,CAAC,CAyGpE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAeA,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAmElD,wBAAsB,GAAG,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,QAAQ,CAAC,CAyGpE"}
package/dist/index.js CHANGED
@@ -27,8 +27,10 @@ const HELP = `mafdet - Mafdet AI command line
27
27
  Usage:
28
28
  mafdet <command> [options]
29
29
 
30
+ Run with no arguments in a terminal to open an interactive session.
31
+
30
32
  Commands:
31
- agent Inspect the workspace with a model (read-only)
33
+ agent Work in this directory with a model
32
34
  chat Send a prompt to a model
33
35
  embed Turn text into a vector
34
36
  speech Turn text into an audio file
@@ -39,6 +41,7 @@ Commands:
39
41
  account subscription Current plan and Playground allowance
40
42
  sessions list Conversations saved on this computer
41
43
  sessions show One saved conversation in detail
44
+ sessions usage What one conversation used, and what it was charged
42
45
  sessions delete Remove a saved conversation from this computer
43
46
  config list Show the resolved settings and where each came from
44
47
  doctor Check key, endpoints, model, balance and workspace
@@ -49,6 +52,19 @@ Options:
49
52
  -h, --help Show this help
50
53
  -v, --version Print the version
51
54
 
55
+ Interactive session (bare 'mafdet'):
56
+ --allow-write Offer file changes, each shown as a diff and approved
57
+ --allow-command <prefix> Allow one command prefix, approved per run
58
+ --read-only Cancel an earlier --allow-write
59
+ --resume <id> Continue a saved conversation
60
+ --title <text> Name this conversation in the listing
61
+ --no-history Do not record this run at all
62
+ --no-tui Do not open the interactive session
63
+
64
+ The agent is read-only unless told otherwise, every file change is shown as a
65
+ diff and approved one at a time, and there is no flag that approves
66
+ everything in advance.
67
+
52
68
  Docs: https://docs.mafdet.ai
53
69
  `;
54
70
  async function readStdin() {
@@ -1 +1 @@
1
- {"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../src/output/format.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,OAAO,GAAG,MAAM,CAAC;AAEtD,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,YAAY,CAAC;IACrB,+DAA+D;IAC/D,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED,2EAA2E;AAC3E,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,WAAW,CAiBrE;AAED,sDAAsD;AACtD,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEjD;AAED,MAAM,WAAW,MAAM,CAAC,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,MAAM,CAAC;IAC1B,+DAA+D;IAC/D,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CAC1B;AAED,wBAAgB,WAAW,CAAC,CAAC,EAC3B,IAAI,EAAE,SAAS,CAAC,EAAE,EAClB,OAAO,EAAE,SAAS,MAAM,CAAC,CAAC,CAAC,EAAE,GAC5B,MAAM,CAoBR;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAC3B,IAAI,EAAE,SAAS,CAAC,EAAE,EAClB,OAAO,EAAE,SAAS,MAAM,CAAC,CAAC,CAAC,EAAE,GAC5B,MAAM,CAOR;AAED,wBAAgB,YAAY,CAC1B,OAAO,EAAE,SAAS,CAAC,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE,GAC9C,MAAM,CAOR"}
1
+ {"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../src/output/format.ts"],"names":[],"mappings":"AAGA;;;GAGG;AAEH,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,OAAO,GAAG,MAAM,CAAC;AAEtD,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,YAAY,CAAC;IACrB,+DAA+D;IAC/D,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED,2EAA2E;AAC3E,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,WAAW,CAwBrE;AAED,sDAAsD;AACtD,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEjD;AAED,MAAM,WAAW,MAAM,CAAC,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,MAAM,CAAC;IAC1B,+DAA+D;IAC/D,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CAC1B;AAED,wBAAgB,WAAW,CAAC,CAAC,EAC3B,IAAI,EAAE,SAAS,CAAC,EAAE,EAClB,OAAO,EAAE,SAAS,MAAM,CAAC,CAAC,CAAC,EAAE,GAC5B,MAAM,CAoBR;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAC3B,IAAI,EAAE,SAAS,CAAC,EAAE,EAClB,OAAO,EAAE,SAAS,MAAM,CAAC,CAAC,CAAC,EAAE,GAC5B,MAAM,CAOR;AAED,wBAAgB,YAAY,CAC1B,OAAO,EAAE,SAAS,CAAC,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE,GAC9C,MAAM,CAOR"}