@mafdet/cli 0.3.0

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 (107) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/LICENSE +21 -0
  3. package/README.md +173 -0
  4. package/README.zh-CN.md +148 -0
  5. package/dist/agent/approvals.d.ts +38 -0
  6. package/dist/agent/approvals.d.ts.map +1 -0
  7. package/dist/agent/approvals.js +53 -0
  8. package/dist/agent/command-policy.d.ts +36 -0
  9. package/dist/agent/command-policy.d.ts.map +1 -0
  10. package/dist/agent/command-policy.js +133 -0
  11. package/dist/agent/loop.d.ts +107 -0
  12. package/dist/agent/loop.d.ts.map +1 -0
  13. package/dist/agent/loop.js +242 -0
  14. package/dist/agent/session.d.ts +62 -0
  15. package/dist/agent/session.d.ts.map +1 -0
  16. package/dist/agent/session.js +290 -0
  17. package/dist/agent/tools/read-only.d.ts +15 -0
  18. package/dist/agent/tools/read-only.d.ts.map +1 -0
  19. package/dist/agent/tools/read-only.js +254 -0
  20. package/dist/agent/tools/run-command.d.ts +20 -0
  21. package/dist/agent/tools/run-command.d.ts.map +1 -0
  22. package/dist/agent/tools/run-command.js +116 -0
  23. package/dist/agent/tools/types.d.ts +46 -0
  24. package/dist/agent/tools/types.d.ts.map +1 -0
  25. package/dist/agent/tools/types.js +16 -0
  26. package/dist/agent/tools/write.d.ts +3 -0
  27. package/dist/agent/tools/write.d.ts.map +1 -0
  28. package/dist/agent/tools/write.js +68 -0
  29. package/dist/api/account.d.ts +39 -0
  30. package/dist/api/account.d.ts.map +1 -0
  31. package/dist/api/account.js +39 -0
  32. package/dist/api/errors.d.ts +38 -0
  33. package/dist/api/errors.d.ts.map +1 -0
  34. package/dist/api/errors.js +149 -0
  35. package/dist/api/http.d.ts +63 -0
  36. package/dist/api/http.d.ts.map +1 -0
  37. package/dist/api/http.js +207 -0
  38. package/dist/api/models.d.ts +68 -0
  39. package/dist/api/models.d.ts.map +1 -0
  40. package/dist/api/models.js +91 -0
  41. package/dist/api/sse-parser.d.ts +72 -0
  42. package/dist/api/sse-parser.d.ts.map +1 -0
  43. package/dist/api/sse-parser.js +163 -0
  44. package/dist/commands/account.d.ts +4 -0
  45. package/dist/commands/account.d.ts.map +1 -0
  46. package/dist/commands/account.js +103 -0
  47. package/dist/commands/agent.d.ts +10 -0
  48. package/dist/commands/agent.d.ts.map +1 -0
  49. package/dist/commands/agent.js +268 -0
  50. package/dist/commands/chat.d.ts +13 -0
  51. package/dist/commands/chat.d.ts.map +1 -0
  52. package/dist/commands/chat.js +191 -0
  53. package/dist/commands/config.d.ts +15 -0
  54. package/dist/commands/config.d.ts.map +1 -0
  55. package/dist/commands/config.js +74 -0
  56. package/dist/commands/doctor.d.ts +15 -0
  57. package/dist/commands/doctor.d.ts.map +1 -0
  58. package/dist/commands/doctor.js +157 -0
  59. package/dist/commands/embed.d.ts +7 -0
  60. package/dist/commands/embed.d.ts.map +1 -0
  61. package/dist/commands/embed.js +105 -0
  62. package/dist/commands/models.d.ts +4 -0
  63. package/dist/commands/models.d.ts.map +1 -0
  64. package/dist/commands/models.js +151 -0
  65. package/dist/commands/runtime.d.ts +29 -0
  66. package/dist/commands/runtime.d.ts.map +1 -0
  67. package/dist/commands/runtime.js +56 -0
  68. package/dist/commands/speech.d.ts +7 -0
  69. package/dist/commands/speech.d.ts.map +1 -0
  70. package/dist/commands/speech.js +111 -0
  71. package/dist/config/config.d.ts +57 -0
  72. package/dist/config/config.d.ts.map +1 -0
  73. package/dist/config/config.js +129 -0
  74. package/dist/config/load.d.ts +20 -0
  75. package/dist/config/load.d.ts.map +1 -0
  76. package/dist/config/load.js +48 -0
  77. package/dist/config/paths.d.ts +11 -0
  78. package/dist/config/paths.d.ts.map +1 -0
  79. package/dist/config/paths.js +26 -0
  80. package/dist/filesystem/diff.d.ts +12 -0
  81. package/dist/filesystem/diff.d.ts.map +1 -0
  82. package/dist/filesystem/diff.js +109 -0
  83. package/dist/filesystem/sensitive-files.d.ts +19 -0
  84. package/dist/filesystem/sensitive-files.d.ts.map +1 -0
  85. package/dist/filesystem/sensitive-files.js +46 -0
  86. package/dist/filesystem/workspace.d.ts +92 -0
  87. package/dist/filesystem/workspace.d.ts.map +1 -0
  88. package/dist/filesystem/workspace.js +264 -0
  89. package/dist/index.d.ts +4 -0
  90. package/dist/index.d.ts.map +1 -0
  91. package/dist/index.js +109 -0
  92. package/dist/output/exit-codes.d.ts +25 -0
  93. package/dist/output/exit-codes.d.ts.map +1 -0
  94. package/dist/output/exit-codes.js +23 -0
  95. package/dist/output/format.d.ts +28 -0
  96. package/dist/output/format.d.ts.map +1 -0
  97. package/dist/output/format.js +56 -0
  98. package/dist/output/money.d.ts +16 -0
  99. package/dist/output/money.d.ts.map +1 -0
  100. package/dist/output/money.js +35 -0
  101. package/dist/output/write-file.d.ts +11 -0
  102. package/dist/output/write-file.d.ts.map +1 -0
  103. package/dist/output/write-file.js +38 -0
  104. package/dist/security/redact.d.ts +16 -0
  105. package/dist/security/redact.d.ts.map +1 -0
  106. package/dist/security/redact.js +61 -0
  107. package/package.json +51 -0
@@ -0,0 +1,116 @@
1
+ import { spawn } from "node:child_process";
2
+ import { z } from "zod";
3
+ import { assessCommand, stripAnsi } from "../command-policy.js";
4
+ import { finishOutput } from "./read-only.js";
5
+ /** Plan §7.4: a single local command gets two minutes. */
6
+ export const COMMAND_TIMEOUT_MS = 120_000;
7
+ const MAX_COMMAND_OUTPUT_BYTES = 256 * 1024;
8
+ /**
9
+ * Run one program, with no shell and nothing inherited that it does not need.
10
+ *
11
+ * `detached` puts the child in its own process group so a timeout can kill the
12
+ * whole tree. Killing only the parent leaves the grandchildren running — the
13
+ * failure mode where a "timed out" test run keeps holding a port.
14
+ */
15
+ export async function executeCommand(argv, cwd, timeoutMs = COMMAND_TIMEOUT_MS) {
16
+ return new Promise((resolve) => {
17
+ const child = spawn(argv[0], [...argv.slice(1)], {
18
+ cwd,
19
+ shell: false,
20
+ // Its own process group, so the timeout below can reach the whole tree.
21
+ // spawnSync cannot do this at all, which is why this is asynchronous.
22
+ detached: process.platform !== "win32",
23
+ // A deliberately small environment. The API key is the point: a test
24
+ // script, a postinstall hook or a compromised dependency must not read
25
+ // the user's credentials out of its own environment (plan §8.4).
26
+ env: {
27
+ PATH: process.env.PATH ?? "",
28
+ HOME: process.env.HOME ?? "",
29
+ LANG: process.env.LANG ?? "C",
30
+ CI: "1",
31
+ },
32
+ });
33
+ let output = "";
34
+ let truncated = false;
35
+ let timedOut = false;
36
+ const collect = (chunk) => {
37
+ if (truncated)
38
+ return;
39
+ output += chunk.toString("utf8");
40
+ if (Buffer.byteLength(output, "utf8") > MAX_COMMAND_OUTPUT_BYTES) {
41
+ output =
42
+ output.slice(0, MAX_COMMAND_OUTPUT_BYTES) + "\n[output truncated]";
43
+ truncated = true;
44
+ }
45
+ };
46
+ child.stdout?.on("data", collect);
47
+ child.stderr?.on("data", collect);
48
+ const timer = setTimeout(() => {
49
+ timedOut = true;
50
+ killTree(child.pid);
51
+ // A process ignoring SIGTERM still has to go.
52
+ setTimeout(() => killTree(child.pid, "SIGKILL"), 2000).unref();
53
+ }, timeoutMs);
54
+ const finish = (code, signal) => {
55
+ clearTimeout(timer);
56
+ resolve({ code, signal, output: stripAnsi(output), timedOut });
57
+ };
58
+ child.on("error", (error) => finish(null, null));
59
+ child.on("close", (code, signal) => finish(code, signal));
60
+ });
61
+ }
62
+ /**
63
+ * Kill the whole process group, not just the child.
64
+ *
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).
70
+ */
71
+ function killTree(pid, signal = "SIGTERM") {
72
+ if (pid === undefined)
73
+ return;
74
+ try {
75
+ if (process.platform === "win32")
76
+ process.kill(pid, signal);
77
+ else
78
+ process.kill(-pid, signal);
79
+ }
80
+ catch {
81
+ // Already exited, or never became a group leader.
82
+ }
83
+ }
84
+ export const runCommandTool = {
85
+ name: "run_command",
86
+ description: "Run one program in the workspace, without a shell. No pipes, redirects or chaining; run a single program with arguments.",
87
+ autoApproved: false,
88
+ schema: z.object({
89
+ command: z
90
+ .string()
91
+ .min(1)
92
+ .describe("A single program with arguments, e.g. 'pnpm test --filter web'"),
93
+ }),
94
+ prepare(args, context) {
95
+ const verdict = assessCommand(args.command, context.allowedCommands ?? []);
96
+ if (!verdict.ok || !verdict.argv) {
97
+ throw new Error(verdict.reason ?? "command refused");
98
+ }
99
+ const argv = verdict.argv;
100
+ return {
101
+ summary: `run: ${argv.join(" ")}`,
102
+ preview: `cwd: ${context.workspace.root}\nenv: PATH, HOME, LANG, CI (no credentials)\ntimeout: ${COMMAND_TIMEOUT_MS / 1000}s`,
103
+ // An --allow-command entry is the user's advance approval; anything else
104
+ // is asked about every single time (plan §7.1: run_command is "每次确认").
105
+ preApproved: verdict.preApproved === true,
106
+ commit: async () => {
107
+ const result = await executeCommand(argv, context.workspace.root);
108
+ const status = result.timedOut
109
+ ? `timed out after ${COMMAND_TIMEOUT_MS / 1000}s (process group killed)`
110
+ : `exit ${result.code ?? "signal " + result.signal}`;
111
+ return finishOutput(`$ ${argv.join(" ")}\n${status}\n${result.output}`);
112
+ },
113
+ };
114
+ },
115
+ };
116
+ export const COMMAND_TOOLS = [runCommandTool];
@@ -0,0 +1,46 @@
1
+ import { z } from "zod";
2
+ import type { Workspace } from "../../filesystem/workspace.js";
3
+ export interface ToolContext {
4
+ workspace: Workspace;
5
+ /** Injected in tests; production shells out to git. */
6
+ runGit?: (args: readonly string[], cwd: string) => string;
7
+ /** `--allow-command` entries: advance approval for exactly these commands. */
8
+ allowedCommands?: readonly string[];
9
+ }
10
+ /**
11
+ * What a non-auto-approved tool produces instead of acting: a description, a
12
+ * preview to show the user, and a commit closure the loop calls only after an
13
+ * approval. Nothing has touched the disk when this is returned.
14
+ */
15
+ export interface PreparedAction {
16
+ summary: string;
17
+ preview: string;
18
+ /** The user approved this in advance (an allowlist entry); do not prompt. */
19
+ preApproved?: boolean;
20
+ commit(): string | Promise<string>;
21
+ }
22
+ export interface ToolDefinition<Schema extends z.ZodType = z.ZodType> {
23
+ name: string;
24
+ description: string;
25
+ schema: Schema;
26
+ /** Read-only tools run without asking; writes and shells never will. */
27
+ autoApproved: boolean;
28
+ /** Used for auto-approved tools. */
29
+ run?(args: z.infer<Schema>, context: ToolContext): string;
30
+ /** Required when `autoApproved` is false: plan, show, then commit. */
31
+ prepare?(args: z.infer<Schema>, context: ToolContext): PreparedAction;
32
+ }
33
+ /**
34
+ * The JSON Schema handed to the model, generated from the same Zod schema that
35
+ * validates what comes back — so the contract the model is shown and the
36
+ * contract enforced locally cannot drift apart.
37
+ */
38
+ export declare function toolToJsonSchema(tool: ToolDefinition): {
39
+ type: "function";
40
+ function: {
41
+ name: string;
42
+ description: string;
43
+ parameters: unknown;
44
+ };
45
+ };
46
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/agent/tools/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,+BAA+B,CAAC;AAE/D,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,SAAS,CAAC;IACrB,uDAAuD;IACvD,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC;IAC1D,8EAA8E;IAC9E,eAAe,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC;AAED;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,6EAA6E;IAC7E,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,MAAM,IAAI,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,cAAc,CAAC,MAAM,SAAS,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC,OAAO;IAClE,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,wEAAwE;IACxE,YAAY,EAAE,OAAO,CAAC;IACtB,oCAAoC;IACpC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,WAAW,GAAG,MAAM,CAAC;IAC1D,sEAAsE;IACtE,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,WAAW,GAAG,cAAc,CAAC;CACvE;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,cAAc,GAAG;IACtD,IAAI,EAAE,UAAU,CAAC;IACjB,QAAQ,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,OAAO,CAAA;KAAE,CAAC;CACtE,CASA"}
@@ -0,0 +1,16 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * The JSON Schema handed to the model, generated from the same Zod schema that
4
+ * validates what comes back — so the contract the model is shown and the
5
+ * contract enforced locally cannot drift apart.
6
+ */
7
+ export function toolToJsonSchema(tool) {
8
+ return {
9
+ type: "function",
10
+ function: {
11
+ name: tool.name,
12
+ description: tool.description,
13
+ parameters: z.toJSONSchema(tool.schema, { io: "input" }),
14
+ },
15
+ };
16
+ }
@@ -0,0 +1,3 @@
1
+ import type { ToolDefinition } from "./types.js";
2
+ export declare const WRITE_TOOLS: ToolDefinition[];
3
+ //# sourceMappingURL=write.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"write.d.ts","sourceRoot":"","sources":["../../../src/agent/tools/write.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAA+B,cAAc,EAAE,MAAM,YAAY,CAAC;AA8E9E,eAAO,MAAM,WAAW,EAAE,cAAc,EAA2B,CAAC"}
@@ -0,0 +1,68 @@
1
+ import { z } from "zod";
2
+ import { planCreate, planEdit } from "../../filesystem/workspace.js";
3
+ import { finishOutput } from "./read-only.js";
4
+ /**
5
+ * The two write tools. Neither is auto-approved: the loop must route them
6
+ * through an approver, and the approver's default answer is no.
7
+ *
8
+ * They deliberately cannot overwrite a whole file. A model that has drifted
9
+ * from reality then fails on an unmatched `old_text` instead of silently
10
+ * replacing work it never read (plan §7.2).
11
+ */
12
+ const editFile = {
13
+ name: "edit_file",
14
+ description: "Replace an exact, unique snippet in an existing file. old_text must appear exactly once; include surrounding lines to make it unique.",
15
+ autoApproved: false,
16
+ schema: z.object({
17
+ path: z.string().min(1).describe("Workspace-relative path"),
18
+ old_text: z
19
+ .string()
20
+ .min(1)
21
+ .describe("Exact text to replace, quoted from the file"),
22
+ new_text: z
23
+ .string()
24
+ .describe("Replacement text (may be empty to delete the snippet)"),
25
+ }),
26
+ prepare(args, context) {
27
+ const planned = planEdit(context.workspace, {
28
+ path: args.path,
29
+ oldText: args.old_text,
30
+ newText: args.new_text,
31
+ });
32
+ return {
33
+ summary: `edit ${planned.path}`,
34
+ preview: planned.diff,
35
+ commit: () => {
36
+ planned.apply();
37
+ return finishOutput(`edited ${planned.path}\n${planned.diff}`);
38
+ },
39
+ };
40
+ },
41
+ };
42
+ const createFile = {
43
+ name: "create_file",
44
+ description: "Create a new file. Fails if the path already exists.",
45
+ autoApproved: false,
46
+ schema: z.object({
47
+ path: z
48
+ .string()
49
+ .min(1)
50
+ .describe("Workspace-relative path that does not exist yet"),
51
+ content: z.string().describe("Full contents of the new file"),
52
+ }),
53
+ prepare(args, context) {
54
+ const planned = planCreate(context.workspace, {
55
+ path: args.path,
56
+ content: args.content,
57
+ });
58
+ return {
59
+ summary: `create ${planned.path}`,
60
+ preview: planned.diff,
61
+ commit: () => {
62
+ planned.apply();
63
+ return finishOutput(`created ${planned.path} (${args.content.length} characters)`);
64
+ },
65
+ };
66
+ },
67
+ };
68
+ export const WRITE_TOOLS = [editFile, createFile];
@@ -0,0 +1,39 @@
1
+ import type { HttpClient } from "./http.js";
2
+ /**
3
+ * The account plane (`backend.mafdet.ai/keyinfo/*`). Amounts arrive as integer
4
+ * micro-USD strings and stay strings all the way to the renderer.
5
+ */
6
+ export interface Wallet {
7
+ balance_micro_usd: string;
8
+ bonus_micro_usd: string;
9
+ frozen_micro_usd: string;
10
+ available_micro_usd: string;
11
+ currency_anchor: string;
12
+ }
13
+ export interface UsageSummary {
14
+ days: number;
15
+ from: string;
16
+ to: string;
17
+ count: number;
18
+ input_tokens: number;
19
+ output_tokens: number;
20
+ total_tokens: number;
21
+ charge_micro_usd: string;
22
+ }
23
+ export interface Subscription {
24
+ status: string;
25
+ plan_slug?: string;
26
+ plan_name?: string;
27
+ current_period_end?: string;
28
+ playground?: {
29
+ allowance_micro_usd: string;
30
+ used_micro_usd: string;
31
+ remaining_micro_usd: string;
32
+ usage_percent: number;
33
+ state: string;
34
+ };
35
+ }
36
+ export declare function fetchWallet(client: HttpClient): Promise<Wallet>;
37
+ export declare function fetchUsage(client: HttpClient, days?: number): Promise<UsageSummary>;
38
+ export declare function fetchSubscription(client: HttpClient): Promise<Subscription>;
39
+ //# sourceMappingURL=account.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,39 @@
1
+ import { ApiError } from "./errors.js";
2
+ /**
3
+ * The account plane answers 401 with its own error shape when the key lacks
4
+ * `account:read`. That is a scope problem, not a bad key, and saying so saves
5
+ * the user from regenerating a key that was fine.
6
+ */
7
+ function explainScope(error) {
8
+ if (error instanceof ApiError &&
9
+ (error.kind === "auth" || error.kind === "forbidden")) {
10
+ throw new ApiError({
11
+ kind: error.kind,
12
+ exitCode: error.exitCode,
13
+ status: error.status,
14
+ requestId: error.requestId,
15
+ message: "this key cannot read account data - it needs the 'account:read' scope. " +
16
+ "Model calls are unaffected; create a key with that scope to use `mafdet account`.",
17
+ });
18
+ }
19
+ throw error;
20
+ }
21
+ export async function fetchWallet(client) {
22
+ const { data } = await client
23
+ .getJson("/keyinfo/wallet")
24
+ .catch(explainScope);
25
+ return data;
26
+ }
27
+ export async function fetchUsage(client, days) {
28
+ const query = days === undefined ? "" : `?days=${days}`;
29
+ const { data } = await client
30
+ .getJson(`/keyinfo/usage-summary${query}`)
31
+ .catch(explainScope);
32
+ return data;
33
+ }
34
+ export async function fetchSubscription(client) {
35
+ const { data } = await client
36
+ .getJson("/keyinfo/subscription")
37
+ .catch(explainScope);
38
+ return data;
39
+ }
@@ -0,0 +1,38 @@
1
+ import { ExitCode } from "../output/exit-codes.js";
2
+ export type ApiErrorKind = "auth" | "forbidden" | "payment" | "rate_limited" | "request" | "server" | "timeout" | "cancelled" | "network" | "too_large";
3
+ /**
4
+ * One error type for everything the API can do to us, carrying the decision a
5
+ * caller needs: which exit code, and whether replaying is safe.
6
+ */
7
+ export declare class ApiError extends Error {
8
+ readonly kind: ApiErrorKind;
9
+ readonly status?: number;
10
+ readonly exitCode: ExitCode;
11
+ /** Only ever true for an explicit 429 (plan §7.5). */
12
+ readonly retryable: boolean;
13
+ readonly retryAfterSeconds?: number;
14
+ /** Mafdet's own error code, dug out of the nested payload when present. */
15
+ readonly code?: string;
16
+ /** Upstream correlation id, for support tickets. */
17
+ readonly requestId?: string;
18
+ constructor(init: {
19
+ kind: ApiErrorKind;
20
+ message: string;
21
+ status?: number;
22
+ exitCode: ExitCode;
23
+ retryable?: boolean;
24
+ retryAfterSeconds?: number;
25
+ code?: string;
26
+ requestId?: string;
27
+ });
28
+ }
29
+ /**
30
+ * Mafdet's guards answer with a JSON document stringified *into*
31
+ * `error.message`, so the machine-readable code is one parse deeper than the
32
+ * OpenAI shape suggests (CLI-001 §3).
33
+ */
34
+ export declare function mafdetErrorCode(body: unknown): string | undefined;
35
+ /** Seconds to wait, from a `Retry-After` that may be seconds or an HTTP date. */
36
+ export declare function parseRetryAfter(header: string | null, now?: number): number | undefined;
37
+ export declare function errorForResponse(status: number, body: unknown, headers: Headers, requestId?: string): ApiError;
38
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/api/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAGnD,MAAM,MAAM,YAAY,GACpB,MAAM,GACN,WAAW,GACX,SAAS,GACT,cAAc,GACd,SAAS,GACT,QAAQ,GACR,SAAS,GACT,WAAW,GACX,SAAS,GACT,WAAW,CAAC;AAEhB;;;GAGG;AACH,qBAAa,QAAS,SAAQ,KAAK;IACjC,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAC5B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B,sDAAsD;IACtD,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IACpC,2EAA2E;IAC3E,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,oDAAoD;IACpD,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;gBAEhB,IAAI,EAAE;QAChB,IAAI,EAAE,YAAY,CAAC;QACnB,OAAO,EAAE,MAAM,CAAC;QAChB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,QAAQ,EAAE,QAAQ,CAAC;QACnB,SAAS,CAAC,EAAE,OAAO,CAAC;QACpB,iBAAiB,CAAC,EAAE,MAAM,CAAC;QAC3B,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB;CAcF;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAqBjE;AAmBD,iFAAiF;AACjF,wBAAgB,eAAe,CAC7B,MAAM,EAAE,MAAM,GAAG,IAAI,EACrB,GAAG,GAAE,MAAmB,GACvB,MAAM,GAAG,SAAS,CAOpB;AAED,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,OAAO,EACb,OAAO,EAAE,OAAO,EAChB,SAAS,CAAC,EAAE,MAAM,GACjB,QAAQ,CAyDV"}
@@ -0,0 +1,149 @@
1
+ import { ExitCode } from "../output/exit-codes.js";
2
+ import { redact } from "../security/redact.js";
3
+ /**
4
+ * One error type for everything the API can do to us, carrying the decision a
5
+ * caller needs: which exit code, and whether replaying is safe.
6
+ */
7
+ export class ApiError extends Error {
8
+ kind;
9
+ status;
10
+ exitCode;
11
+ /** Only ever true for an explicit 429 (plan §7.5). */
12
+ retryable;
13
+ retryAfterSeconds;
14
+ /** Mafdet's own error code, dug out of the nested payload when present. */
15
+ code;
16
+ /** Upstream correlation id, for support tickets. */
17
+ requestId;
18
+ constructor(init) {
19
+ // Upstream messages quote the key prefix and its hash (CLI-001), so the
20
+ // message is redacted at construction — before it can be logged or thrown
21
+ // past a handler that forgets.
22
+ super(redact(init.message));
23
+ this.name = "ApiError";
24
+ this.kind = init.kind;
25
+ this.status = init.status;
26
+ this.exitCode = init.exitCode;
27
+ this.retryable = init.retryable ?? false;
28
+ this.retryAfterSeconds = init.retryAfterSeconds;
29
+ this.code = init.code;
30
+ this.requestId = init.requestId;
31
+ }
32
+ }
33
+ /**
34
+ * Mafdet's guards answer with a JSON document stringified *into*
35
+ * `error.message`, so the machine-readable code is one parse deeper than the
36
+ * OpenAI shape suggests (CLI-001 §3).
37
+ */
38
+ export function mafdetErrorCode(body) {
39
+ // Third shape, found by running the real thing: mafdet's own request guards
40
+ // answer FastAPI-style, `{"detail": {"code": "TTS_VOICE_REQUIRED"}}` — no
41
+ // `error` object at all. Decoding only the OpenAI shape turned a precise,
42
+ // actionable reason into "request rejected (400)".
43
+ const detail = body?.detail;
44
+ if (detail && typeof detail === "object") {
45
+ const code = detail.code;
46
+ if (typeof code === "string")
47
+ return code;
48
+ }
49
+ const message = body?.error?.message;
50
+ if (typeof message !== "string" || !message.trimStart().startsWith("{")) {
51
+ return undefined;
52
+ }
53
+ try {
54
+ const inner = JSON.parse(message);
55
+ return typeof inner.code === "string" ? inner.code : undefined;
56
+ }
57
+ catch {
58
+ return undefined;
59
+ }
60
+ }
61
+ function messageOf(body, fallback) {
62
+ const openai = body?.error?.message;
63
+ if (typeof openai === "string" && openai.length > 0)
64
+ return openai;
65
+ // FastAPI-shaped guard rejections: a bare string, or an object whose code is
66
+ // the whole story.
67
+ const detail = body?.detail;
68
+ if (typeof detail === "string" && detail.length > 0)
69
+ return detail;
70
+ if (detail && typeof detail === "object") {
71
+ const code = detail.code;
72
+ if (typeof code === "string")
73
+ return code;
74
+ }
75
+ // The account plane uses its own shape: { error_code, message }.
76
+ const business = body?.message;
77
+ if (typeof business === "string" && business.length > 0)
78
+ return business;
79
+ return fallback;
80
+ }
81
+ /** Seconds to wait, from a `Retry-After` that may be seconds or an HTTP date. */
82
+ export function parseRetryAfter(header, now = Date.now()) {
83
+ if (!header)
84
+ return undefined;
85
+ const seconds = Number(header.trim());
86
+ if (Number.isFinite(seconds) && seconds >= 0)
87
+ return seconds;
88
+ const at = Date.parse(header);
89
+ if (Number.isNaN(at))
90
+ return undefined;
91
+ return Math.max(0, Math.round((at - now) / 1000));
92
+ }
93
+ export function errorForResponse(status, body, headers, requestId) {
94
+ const code = mafdetErrorCode(body);
95
+ const base = { status, code, requestId };
96
+ if (status === 401) {
97
+ return new ApiError({
98
+ ...base,
99
+ kind: "auth",
100
+ exitCode: ExitCode.Auth,
101
+ message: messageOf(body, "authentication failed - check MAFDET_API_KEY"),
102
+ });
103
+ }
104
+ if (status === 403) {
105
+ return new ApiError({
106
+ ...base,
107
+ kind: "forbidden",
108
+ exitCode: ExitCode.Auth,
109
+ // 403 covers both "your key may not call this model" and a mistyped id:
110
+ // the allowlist is checked before the id is resolved (CLI-001), so the
111
+ // caller must compare against its own model list before blaming either.
112
+ message: messageOf(body, "this key may not use that model"),
113
+ });
114
+ }
115
+ if (status === 402) {
116
+ return new ApiError({
117
+ ...base,
118
+ kind: "payment",
119
+ exitCode: ExitCode.Auth,
120
+ message: messageOf(body, "insufficient balance"),
121
+ });
122
+ }
123
+ if (status === 429) {
124
+ return new ApiError({
125
+ ...base,
126
+ kind: "rate_limited",
127
+ exitCode: ExitCode.Unavailable,
128
+ retryable: true,
129
+ retryAfterSeconds: parseRetryAfter(headers.get("retry-after")),
130
+ message: messageOf(body, "rate limited"),
131
+ });
132
+ }
133
+ if (status >= 500) {
134
+ return new ApiError({
135
+ ...base,
136
+ kind: "server",
137
+ exitCode: ExitCode.Unavailable,
138
+ // Deliberately not retryable: the upstream may already be generating,
139
+ // and generation costs money (plan §7.5).
140
+ message: messageOf(body, `upstream error (${status})`),
141
+ });
142
+ }
143
+ return new ApiError({
144
+ ...base,
145
+ kind: "request",
146
+ exitCode: ExitCode.Usage,
147
+ message: messageOf(body, `request rejected (${status})`),
148
+ });
149
+ }
@@ -0,0 +1,63 @@
1
+ export declare const DEFAULT_TIMEOUT_MS = 120000;
2
+ /** A JSON answer this large is a bug or an attack, not a model reply. */
3
+ export declare const DEFAULT_MAX_BYTES: number;
4
+ export interface HttpClientOptions {
5
+ baseUrl: string;
6
+ apiKey?: string;
7
+ userAgent: string;
8
+ timeoutMs?: number;
9
+ maxBytes?: number;
10
+ /** Injectable for tests. */
11
+ fetchImpl?: typeof fetch;
12
+ }
13
+ export interface RequestOptions {
14
+ /** Caller's cancellation (Ctrl+C); combined with the timeout. */
15
+ signal?: AbortSignal;
16
+ timeoutMs?: number;
17
+ maxBytes?: number;
18
+ headers?: Record<string, string>;
19
+ }
20
+ export interface HttpResponse<T> {
21
+ data: T;
22
+ status: number;
23
+ headers: Headers;
24
+ /** Server-side correlation id when the gateway returns one. */
25
+ requestId?: string;
26
+ }
27
+ /**
28
+ * The one place that talks to the network.
29
+ *
30
+ * It deliberately does NOT retry. Only an explicit 429 is even retryable, and
31
+ * the decision belongs to the caller that knows whether replaying is safe —
32
+ * a timeout or a 5xx may mean the model already started generating, and
33
+ * generation is already billed (plan §7.5).
34
+ */
35
+ export declare class HttpClient {
36
+ private readonly baseUrl;
37
+ private readonly apiKey?;
38
+ private readonly userAgent;
39
+ private readonly timeoutMs;
40
+ private readonly maxBytes;
41
+ private readonly fetchImpl;
42
+ constructor(options: HttpClientOptions);
43
+ getJson<T>(path: string, options?: RequestOptions): Promise<HttpResponse<T>>;
44
+ postJson<T>(path: string, body: unknown, options?: RequestOptions): Promise<HttpResponse<T>>;
45
+ /** For `/v1/audio/speech`, which answers with audio bytes. */
46
+ postBinary(path: string, body: unknown, options?: RequestOptions): Promise<HttpResponse<Uint8Array>>;
47
+ /** Raw streaming response, for the SSE reader to consume. */
48
+ postStream(path: string, body: unknown, options?: RequestOptions): Promise<{
49
+ response: Response;
50
+ requestId?: string;
51
+ }>;
52
+ private send;
53
+ private readJson;
54
+ private errorFrom;
55
+ }
56
+ /**
57
+ * Read a body while counting bytes, so an endless or oversized response is cut
58
+ * off rather than filling memory. Content-Length is checked first when it is
59
+ * present, but the running count is what actually enforces the cap — a
60
+ * streaming body may not declare a length.
61
+ */
62
+ export declare function readCapped(response: Response, maxBytes: number, requestId?: string): Promise<Uint8Array>;
63
+ //# sourceMappingURL=http.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/api/http.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,kBAAkB,SAAU,CAAC;AAC1C,yEAAyE;AACzE,eAAO,MAAM,iBAAiB,QAAmB,CAAC;AAElD,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,4BAA4B;IAC5B,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAC1B;AAED,MAAM,WAAW,cAAc;IAC7B,iEAAiE;IACjE,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAED,MAAM,WAAW,YAAY,CAAC,CAAC;IAC7B,IAAI,EAAE,CAAC,CAAC;IACR,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;IACjB,+DAA+D;IAC/D,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;GAOG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAe;gBAE7B,OAAO,EAAE,iBAAiB;IAShC,OAAO,CAAC,CAAC,EACb,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IAKrB,QAAQ,CAAC,CAAC,EACd,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,OAAO,EACb,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IAK3B,8DAA8D;IACxD,UAAU,CACd,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,OAAO,EACb,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC,YAAY,CAAC,UAAU,CAAC,CAAC;IAiBpC,6DAA6D;IACvD,UAAU,CACd,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,OAAO,EACb,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC;QAAE,QAAQ,EAAE,QAAQ,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;YASxC,IAAI;YA4DJ,QAAQ;YAuCR,SAAS;CAexB;AAED;;;;;GAKG;AACH,wBAAsB,UAAU,CAC9B,QAAQ,EAAE,QAAQ,EAClB,QAAQ,EAAE,MAAM,EAChB,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,UAAU,CAAC,CAkCrB"}