@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,74 @@
1
+ import { ConfigError } from "../config/config.js";
2
+ import { loadConfig } from "../config/load.js";
3
+ import { ExitCode } from "../output/exit-codes.js";
4
+ import { keyFingerprint } from "../security/redact.js";
5
+ const USAGE = `Usage:
6
+ mafdet config list [--json]
7
+ `;
8
+ /**
9
+ * `config list` answers "what settings is the CLI actually using, and where
10
+ * did each come from" — the question a user asks when a run picks a model they
11
+ * did not expect. The key is only ever shown as a fingerprint.
12
+ */
13
+ export function configCommand(argv, io) {
14
+ const [subcommand, ...rest] = argv;
15
+ if (subcommand !== "list") {
16
+ io.stderr(subcommand === undefined
17
+ ? USAGE
18
+ : `mafdet config: unknown subcommand '${subcommand}'\n${USAGE}`);
19
+ return ExitCode.Usage;
20
+ }
21
+ const json = rest.includes("--json");
22
+ const unknown = rest.filter((arg) => arg !== "--json");
23
+ if (unknown.length > 0) {
24
+ io.stderr(`mafdet config list: unexpected argument '${unknown[0]}'\n${USAGE}`);
25
+ return ExitCode.Usage;
26
+ }
27
+ let config;
28
+ try {
29
+ config = loadConfig({
30
+ env: io.env,
31
+ cwd: io.cwd,
32
+ userConfigFile: io.userConfigFile,
33
+ });
34
+ }
35
+ catch (error) {
36
+ if (error instanceof ConfigError) {
37
+ io.stderr(`mafdet config: ${error.message}\n`);
38
+ return ExitCode.Usage;
39
+ }
40
+ throw error;
41
+ }
42
+ const view = {
43
+ model: config.model ?? null,
44
+ model_source: config.modelSource,
45
+ base_url: config.baseUrl,
46
+ backend_url: config.backendUrl,
47
+ approval_mode: config.approvalMode,
48
+ api_key: config.apiKey ? keyFingerprint(config.apiKey) : null,
49
+ user_config: config.sources.userConfigFound
50
+ ? config.sources.userConfigPath
51
+ : null,
52
+ project_config: config.sources.projectConfigFound
53
+ ? config.sources.projectConfigPath
54
+ : null,
55
+ };
56
+ if (json) {
57
+ io.stdout(`${JSON.stringify(view, null, 2)}\n`);
58
+ return ExitCode.Ok;
59
+ }
60
+ const rows = [
61
+ ["model", view.model ? `${view.model} (${view.model_source})` : "<unset>"],
62
+ ["base url", view.base_url],
63
+ ["backend url", view.backend_url],
64
+ ["approval mode", view.approval_mode],
65
+ ["api key", view.api_key ?? "<unset> (set MAFDET_API_KEY)"],
66
+ ["user config", view.user_config ?? "<none>"],
67
+ ["project config", view.project_config ?? "<none>"],
68
+ ];
69
+ const width = Math.max(...rows.map(([label]) => label.length));
70
+ io.stdout(rows
71
+ .map(([label, value]) => `${label.padEnd(width)} ${value}`)
72
+ .join("\n") + "\n");
73
+ return ExitCode.Ok;
74
+ }
@@ -0,0 +1,15 @@
1
+ import { ExitCode } from "../output/exit-codes.js";
2
+ import { type CommandIo } from "./runtime.js";
3
+ export interface DoctorIo extends CommandIo {
4
+ /** Injected by tests so the git probe stays hermetic. */
5
+ hasGit?: () => boolean;
6
+ nodeVersion?: string;
7
+ }
8
+ /**
9
+ * One pass over everything that has to be true before a real call works, so a
10
+ * failure is diagnosed once here instead of surfacing as a confusing error in
11
+ * the middle of a task. Every line is redacted; the key appears only as a
12
+ * fingerprint.
13
+ */
14
+ export declare function doctorCommand(argv: readonly string[], io: DoctorIo): Promise<ExitCode>;
15
+ //# sourceMappingURL=doctor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"AAYA,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAInD,OAAO,EAAgB,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAY5D,MAAM,WAAW,QAAS,SAAQ,SAAS;IACzC,yDAAyD;IACzD,MAAM,CAAC,EAAE,MAAM,OAAO,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;GAKG;AACH,wBAAsB,aAAa,CACjC,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,EAAE,EAAE,QAAQ,GACX,OAAO,CAAC,QAAQ,CAAC,CAyFnB"}
@@ -0,0 +1,157 @@
1
+ import { accessSync, constants } from "node:fs";
2
+ import { execFileSync } from "node:child_process";
3
+ import { fetchWallet } from "../api/account.js";
4
+ import { ApiError } from "../api/errors.js";
5
+ import { fetchAccessibleModels, fetchCatalog, intersectModels, } from "../api/models.js";
6
+ import { ConfigError } from "../config/config.js";
7
+ import { ExitCode } from "../output/exit-codes.js";
8
+ import { parseFormatFlags, renderJson, renderTable } from "../output/format.js";
9
+ import { formatUsd } from "../output/money.js";
10
+ import { keyFingerprint, redact } from "../security/redact.js";
11
+ import { buildRuntime } from "./runtime.js";
12
+ const MINIMUM_NODE_MAJOR = 20;
13
+ /**
14
+ * One pass over everything that has to be true before a real call works, so a
15
+ * failure is diagnosed once here instead of surfacing as a confusing error in
16
+ * the middle of a task. Every line is redacted; the key appears only as a
17
+ * fingerprint.
18
+ */
19
+ export async function doctorCommand(argv, io) {
20
+ let format;
21
+ try {
22
+ ({ format } = parseFormatFlags(argv));
23
+ }
24
+ catch (error) {
25
+ io.stderr(`mafdet doctor: ${error.message}\n`);
26
+ return ExitCode.Usage;
27
+ }
28
+ const checks = [];
29
+ const add = (name, status, detail) => checks.push({ name, status, detail: redact(detail) });
30
+ const nodeVersion = io.nodeVersion ?? process.version;
31
+ const major = Number(nodeVersion.replace(/^v/, "").split(".")[0]);
32
+ add("node", Number.isFinite(major) && major >= MINIMUM_NODE_MAJOR ? "ok" : "fail", `${nodeVersion} (needs ${MINIMUM_NODE_MAJOR}+)`);
33
+ // Configuration first: an unreadable config or a downgraded endpoint stops
34
+ // everything else from meaning anything.
35
+ let runtime;
36
+ try {
37
+ runtime = buildRuntime(io);
38
+ }
39
+ catch (error) {
40
+ add("config", "fail", error instanceof ConfigError ? error.message : String(error));
41
+ return report(checks, format, io);
42
+ }
43
+ const { config } = runtime;
44
+ add("base url", "ok", config.baseUrl);
45
+ add("backend url", "ok", config.backendUrl);
46
+ add("api key", config.apiKey ? "ok" : "fail", config.apiKey
47
+ ? keyFingerprint(config.apiKey)
48
+ : "unset - set MAFDET_API_KEY");
49
+ await addWorkspaceChecks(add, io);
50
+ if (!config.apiKey)
51
+ return report(checks, format, io);
52
+ // Data plane: does the key work at all, and what may it call?
53
+ let accessible = [];
54
+ try {
55
+ accessible = await fetchAccessibleModels(runtime.dataPlane);
56
+ add("data plane", "ok", `${accessible.length} model(s) callable`);
57
+ }
58
+ catch (error) {
59
+ add("data plane", "fail", describe(error));
60
+ }
61
+ let catalog = [];
62
+ try {
63
+ catalog = await fetchCatalog(runtime.accountPlane);
64
+ add("catalogue", "ok", `${catalog.length} model(s) published`);
65
+ }
66
+ catch (error) {
67
+ add("catalogue", "warn", describe(error));
68
+ }
69
+ addModelChecks(add, config.model, accessible, catalog);
70
+ // account:read is optional: without it model calls still work, so a missing
71
+ // scope is a warning, not a failure.
72
+ try {
73
+ const wallet = await fetchWallet(runtime.accountPlane);
74
+ const available = BigInt(wallet.available_micro_usd);
75
+ add("balance", available > 0n ? "ok" : "fail", `${formatUsd(wallet.available_micro_usd)} available`);
76
+ }
77
+ catch (error) {
78
+ const scopeProblem = error instanceof ApiError && error.kind === "auth";
79
+ add("account:read", scopeProblem ? "warn" : "fail", scopeProblem ? "not granted - model calls still work" : describe(error));
80
+ }
81
+ return report(checks, format, io);
82
+ }
83
+ function addModelChecks(add, model, accessible, catalog) {
84
+ if (!model) {
85
+ add("model", "warn", "none selected - pass --model or set MAFDET_MODEL");
86
+ return;
87
+ }
88
+ const sets = intersectModels(accessible, catalog);
89
+ const resolved = sets.available.find((m) => m.modelId === model);
90
+ if (!resolved) {
91
+ // Say which of the two reasons it is; the server's 403 cannot.
92
+ if (sets.inaccessible.includes(model)) {
93
+ add("model", "fail", `${model} exists but this key may not call it`);
94
+ }
95
+ else if (sets.unlisted.includes(model)) {
96
+ add("model", "warn", `${model} is callable but the catalogue does not describe it`);
97
+ }
98
+ else {
99
+ add("model", "fail", `${model} is not callable with this key`);
100
+ }
101
+ return;
102
+ }
103
+ add("model", "ok", `${model} (${resolved.apiSurface ?? "CHAT_COMPLETIONS"})`);
104
+ add("tool calling", resolved.agentEligible ? "ok" : "warn", resolved.agentEligible
105
+ ? "verified - usable by the agent"
106
+ : "not verified for this model; the agent will refuse it");
107
+ }
108
+ async function addWorkspaceChecks(add, io) {
109
+ const cwd = io.cwd ?? process.cwd();
110
+ try {
111
+ accessSync(cwd, constants.R_OK);
112
+ let writable = true;
113
+ try {
114
+ accessSync(cwd, constants.W_OK);
115
+ }
116
+ catch {
117
+ writable = false;
118
+ }
119
+ add("workspace", writable ? "ok" : "warn", `${cwd}${writable ? "" : " (read-only)"}`);
120
+ }
121
+ catch {
122
+ add("workspace", "fail", `${cwd} is not readable`);
123
+ }
124
+ const hasGit = io.hasGit ?? defaultHasGit;
125
+ add("git", hasGit() ? "ok" : "warn", hasGit()
126
+ ? "available"
127
+ : "not found - diff and status tools will be unavailable");
128
+ }
129
+ function defaultHasGit() {
130
+ try {
131
+ execFileSync("git", ["--version"], { stdio: "ignore" });
132
+ return true;
133
+ }
134
+ catch {
135
+ return false;
136
+ }
137
+ }
138
+ function describe(error) {
139
+ return error instanceof Error ? error.message : String(error);
140
+ }
141
+ function report(checks, format, io) {
142
+ const failed = checks.filter((check) => check.status === "fail").length;
143
+ if (format === "json") {
144
+ io.stdout(renderJson({ ok: failed === 0, checks }));
145
+ }
146
+ else {
147
+ const mark = { ok: "ok", warn: "warn", fail: "FAIL" };
148
+ io.stdout(renderTable(checks, [
149
+ { header: "", value: (c) => mark[c.status] },
150
+ { header: "CHECK", value: (c) => c.name },
151
+ { header: "DETAIL", value: (c) => c.detail },
152
+ ]));
153
+ }
154
+ // Warnings are survivable — a missing scope or an unverified model does not
155
+ // stop a chat — so only a real failure changes the exit code.
156
+ return failed === 0 ? ExitCode.Ok : ExitCode.General;
157
+ }
@@ -0,0 +1,7 @@
1
+ import { ExitCode } from "../output/exit-codes.js";
2
+ import { type CommandIo } from "./runtime.js";
3
+ export interface EmbedIo extends CommandIo {
4
+ readStdin?: () => Promise<string>;
5
+ }
6
+ export declare function embedCommand(argv: readonly string[], io: EmbedIo): Promise<ExitCode>;
7
+ //# sourceMappingURL=embed.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"embed.d.ts","sourceRoot":"","sources":["../../src/commands/embed.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAEnD,OAAO,EAIL,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAQtB,MAAM,WAAW,OAAQ,SAAQ,SAAS;IACxC,SAAS,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;CACnC;AAED,wBAAsB,YAAY,CAChC,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,EAAE,EAAE,OAAO,GACV,OAAO,CAAC,QAAQ,CAAC,CA8DnB"}
@@ -0,0 +1,105 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { ApiError } from "../api/errors.js";
3
+ import { assertSurface, fetchCatalog, SurfaceMismatchError, } from "../api/models.js";
4
+ import { ExitCode } from "../output/exit-codes.js";
5
+ import { writeOutputFile } from "../output/write-file.js";
6
+ import { buildRuntime, reportError, requireApiKey, } from "./runtime.js";
7
+ const USAGE = `Usage:
8
+ mafdet embed [text | <file>] --model <id> [--output <file>] [--force]
9
+
10
+ Reads the text from an argument, a file, or stdin.
11
+ `;
12
+ export async function embedCommand(argv, io) {
13
+ try {
14
+ const runtime = buildRuntime(io);
15
+ requireApiKey(runtime.config);
16
+ let model = runtime.config.model;
17
+ let output;
18
+ let force = false;
19
+ const positional = [];
20
+ for (let i = 0; i < argv.length; i += 1) {
21
+ const arg = argv[i];
22
+ const next = () => argv[(i += 1)];
23
+ if (arg === "--model")
24
+ model = next();
25
+ else if (arg === "--output" || arg === "-o")
26
+ output = next();
27
+ else if (arg === "--force")
28
+ force = true;
29
+ else if (arg.startsWith("--")) {
30
+ io.stderr(`mafdet embed: unknown option '${arg}'\n${USAGE}`);
31
+ return ExitCode.Usage;
32
+ }
33
+ else
34
+ positional.push(arg);
35
+ }
36
+ if (!model) {
37
+ io.stderr("mafdet embed: no model. Pass --model or set MAFDET_MODEL.\n");
38
+ return ExitCode.Usage;
39
+ }
40
+ const input = await resolveInput(positional, io);
41
+ if (input === undefined)
42
+ return ExitCode.Usage;
43
+ // Catch the wrong-surface mistake locally, before it costs a request.
44
+ const catalog = await fetchCatalog(runtime.accountPlane).catch(() => []);
45
+ assertSurface(catalog, model, "EMBEDDINGS");
46
+ const { data } = await runtime.dataPlane.postJson("/embeddings", {
47
+ model,
48
+ input,
49
+ });
50
+ // Re-serialising a parsed double is exact in JavaScript, but the vector is
51
+ // the product here — pretty-print it and nothing else, so the file is both
52
+ // valid JSON and byte-for-byte reproducible.
53
+ const body = `${JSON.stringify(data, null, 2)}\n`;
54
+ if (output) {
55
+ writeOutputFile(output, body, { force });
56
+ const dimensions = firstVectorLength(data);
57
+ io.stderr(`wrote ${output}${dimensions === undefined ? "" : ` (${dimensions} dimensions)`}\n`);
58
+ return ExitCode.Ok;
59
+ }
60
+ io.stdout(body);
61
+ return ExitCode.Ok;
62
+ }
63
+ catch (error) {
64
+ if (error instanceof SurfaceMismatchError) {
65
+ io.stderr(`mafdet embed: ${error.message}\n`);
66
+ return ExitCode.Usage;
67
+ }
68
+ return reportError(error, io);
69
+ }
70
+ }
71
+ async function resolveInput(positional, io) {
72
+ if (positional.length > 1) {
73
+ io.stderr(`mafdet embed: expected one input, got ${positional.length}\n${USAGE}`);
74
+ return undefined;
75
+ }
76
+ if (positional.length === 1) {
77
+ const value = positional[0];
78
+ // A path is read; anything else is the text itself. Reading a file that
79
+ // happens to exist is the useful default for `mafdet embed input.txt`.
80
+ try {
81
+ return readFileSync(value, "utf8");
82
+ }
83
+ catch (error) {
84
+ if (error.code === "ENOENT")
85
+ return value;
86
+ throw new ApiError({
87
+ kind: "request",
88
+ exitCode: ExitCode.Usage,
89
+ message: `cannot read ${value}: ${error.message}`,
90
+ });
91
+ }
92
+ }
93
+ if (io.readStdin && !process.stdin.isTTY) {
94
+ const piped = (await io.readStdin()).trim();
95
+ if (piped.length > 0)
96
+ return piped;
97
+ }
98
+ io.stderr(`mafdet embed: no input\n${USAGE}`);
99
+ return undefined;
100
+ }
101
+ function firstVectorLength(body) {
102
+ const vector = body?.data?.[0]
103
+ ?.embedding;
104
+ return Array.isArray(vector) ? vector.length : undefined;
105
+ }
@@ -0,0 +1,4 @@
1
+ import { ExitCode } from "../output/exit-codes.js";
2
+ import { type CommandIo } from "./runtime.js";
3
+ export declare function modelsCommand(argv: readonly string[], io: CommandIo): Promise<ExitCode>;
4
+ //# sourceMappingURL=models.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"models.d.ts","sourceRoot":"","sources":["../../src/commands/models.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAUnD,OAAO,EAIL,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AA0BtB,wBAAsB,aAAa,CACjC,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,EAAE,EAAE,SAAS,GACZ,OAAO,CAAC,QAAQ,CAAC,CA6BnB"}
@@ -0,0 +1,151 @@
1
+ import { fetchAccessibleModels, fetchCatalog, intersectModels, } from "../api/models.js";
2
+ import { ExitCode } from "../output/exit-codes.js";
3
+ import { parseFormatFlags, renderJson, renderPlain, renderRecord, renderTable, } from "../output/format.js";
4
+ import { formatUsd } from "../output/money.js";
5
+ import { buildRuntime, reportError, requireApiKey, } from "./runtime.js";
6
+ const USAGE = `Usage:
7
+ mafdet models list [--surface <chat|embeddings|speech>] [--agent] [--json|--plain]
8
+ mafdet models show <model-id> [--json]
9
+ `;
10
+ const SURFACES = {
11
+ chat: "CHAT_COMPLETIONS",
12
+ embeddings: "EMBEDDINGS",
13
+ speech: "AUDIO_SPEECH",
14
+ realtime: "REALTIME",
15
+ };
16
+ const columns = [
17
+ { header: "MODEL", value: (m) => m.modelId },
18
+ { header: "PROVIDER", value: (m) => m.provider ?? "-" },
19
+ { header: "SURFACE", value: (m) => m.apiSurface ?? "-" },
20
+ {
21
+ header: "CONTEXT",
22
+ value: (m) => String(m.contextLength ?? "-"),
23
+ align: "right",
24
+ },
25
+ { header: "AGENT", value: (m) => (m.agentEligible ? "yes" : "-") },
26
+ ];
27
+ export async function modelsCommand(argv, io) {
28
+ const [subcommand, ...rest] = argv;
29
+ if (subcommand !== "list" && subcommand !== "show") {
30
+ io.stderr(subcommand === undefined
31
+ ? USAGE
32
+ : `mafdet models: unknown subcommand '${subcommand}'\n${USAGE}`);
33
+ return ExitCode.Usage;
34
+ }
35
+ try {
36
+ const { format, rest: args } = parseFormatFlags(rest);
37
+ const runtime = buildRuntime(io);
38
+ requireApiKey(runtime.config);
39
+ // Both planes are needed: one proves access, the other explains the model.
40
+ const [accessible, catalog] = await Promise.all([
41
+ fetchAccessibleModels(runtime.dataPlane),
42
+ fetchCatalog(runtime.accountPlane),
43
+ ]);
44
+ const sets = intersectModels(accessible, catalog);
45
+ return subcommand === "list"
46
+ ? listModels(sets, args, format, io)
47
+ : showModel(sets, args, format, io);
48
+ }
49
+ catch (error) {
50
+ return reportError(error, io);
51
+ }
52
+ }
53
+ function listModels(sets, args, format, io) {
54
+ let rows = sets.available;
55
+ const remaining = [...args];
56
+ const agentOnly = remaining.includes("--agent");
57
+ if (agentOnly)
58
+ remaining.splice(remaining.indexOf("--agent"), 1);
59
+ const surfaceAt = remaining.indexOf("--surface");
60
+ if (surfaceAt !== -1) {
61
+ const name = remaining[surfaceAt + 1];
62
+ const surface = name ? SURFACES[name] : undefined;
63
+ if (!surface) {
64
+ io.stderr(`mafdet models: unknown surface '${name ?? ""}' (expected ${Object.keys(SURFACES).join(", ")})\n`);
65
+ return ExitCode.Usage;
66
+ }
67
+ rows = rows.filter((m) => (m.apiSurface ?? "CHAT_COMPLETIONS") === surface);
68
+ remaining.splice(surfaceAt, 2);
69
+ }
70
+ if (agentOnly)
71
+ rows = rows.filter((m) => m.agentEligible);
72
+ if (remaining.length > 0) {
73
+ io.stderr(`mafdet models list: unexpected argument '${remaining[0]}'\n${USAGE}`);
74
+ return ExitCode.Usage;
75
+ }
76
+ if (format === "json") {
77
+ io.stdout(renderJson({
78
+ models: rows,
79
+ // Callable but undescribed. Surfaced rather than hidden, because the
80
+ // user can still use them with an explicit --model.
81
+ unlisted: sets.unlisted,
82
+ inaccessible: sets.inaccessible,
83
+ }));
84
+ return ExitCode.Ok;
85
+ }
86
+ if (rows.length === 0) {
87
+ io.stderr("mafdet: no models match\n");
88
+ return ExitCode.Ok;
89
+ }
90
+ io.stdout(format === "plain"
91
+ ? renderPlain(rows, columns)
92
+ : renderTable(rows, columns));
93
+ if (format === "table" && sets.unlisted.length > 0) {
94
+ io.stderr(`note: ${sets.unlisted.length} model(s) callable but missing from the catalogue: ${sets.unlisted.join(", ")}\n`);
95
+ }
96
+ return ExitCode.Ok;
97
+ }
98
+ function showModel(sets, args, format, io) {
99
+ const [modelId, ...extra] = args;
100
+ if (!modelId || extra.length > 0) {
101
+ io.stderr(USAGE);
102
+ return ExitCode.Usage;
103
+ }
104
+ const model = sets.available.find((m) => m.modelId === modelId);
105
+ if (!model) {
106
+ // Distinguish the two reasons the server cannot: a 403 looks identical for
107
+ // "not allowed" and "no such model" (CLI-001).
108
+ if (sets.inaccessible.includes(modelId)) {
109
+ io.stderr(`mafdet: '${modelId}' exists but this key may not call it\n`);
110
+ }
111
+ else if (sets.unlisted.includes(modelId)) {
112
+ io.stderr(`mafdet: '${modelId}' is callable but the catalogue does not describe it\n`);
113
+ }
114
+ else {
115
+ io.stderr(`mafdet: no such model '${modelId}'\n`);
116
+ }
117
+ return ExitCode.Usage;
118
+ }
119
+ if (format === "json") {
120
+ io.stdout(renderJson(model));
121
+ return ExitCode.Ok;
122
+ }
123
+ io.stdout(renderRecord([
124
+ ["model", model.modelId],
125
+ ["name", model.displayName ?? "-"],
126
+ ["provider", model.provider ?? "-"],
127
+ ["surface", model.apiSurface ?? "-"],
128
+ ["context", String(model.contextLength ?? "-")],
129
+ ["capabilities", (model.capabilities ?? []).join(", ") || "-"],
130
+ ["agent eligible", model.agentEligible ? "yes" : "no"],
131
+ [
132
+ "input / 1M",
133
+ model.inputPricePer1mMicroUsd
134
+ ? formatUsd(model.inputPricePer1mMicroUsd)
135
+ : "-",
136
+ ],
137
+ [
138
+ "output / 1M",
139
+ model.outputPricePer1mMicroUsd
140
+ ? formatUsd(model.outputPricePer1mMicroUsd)
141
+ : "-",
142
+ ],
143
+ [
144
+ "deprecated",
145
+ model.deprecated
146
+ ? `yes${model.retires_at ? ` (retires ${model.retires_at})` : ""}`
147
+ : "no",
148
+ ],
149
+ ]));
150
+ return ExitCode.Ok;
151
+ }
@@ -0,0 +1,29 @@
1
+ import { type LoadedConfig } from "../config/load.js";
2
+ import { HttpClient } from "../api/http.js";
3
+ import { ExitCode } from "../output/exit-codes.js";
4
+ export interface CommandIo {
5
+ stdout: (text: string) => void;
6
+ stderr: (text: string) => void;
7
+ env?: NodeJS.ProcessEnv;
8
+ cwd?: string;
9
+ userConfigFile?: string;
10
+ /** Injected by tests; production uses global fetch. */
11
+ fetchImpl?: typeof fetch;
12
+ version?: string;
13
+ }
14
+ export interface Runtime {
15
+ config: LoadedConfig;
16
+ /** api.mafdet.ai/v1 — model calls. */
17
+ dataPlane: HttpClient;
18
+ /** backend.mafdet.ai — account reads. */
19
+ accountPlane: HttpClient;
20
+ }
21
+ export declare function userAgent(version: string): string;
22
+ export declare function buildRuntime(io: CommandIo): Runtime;
23
+ export declare function requireApiKey(config: LoadedConfig): void;
24
+ /**
25
+ * Turn any failure into an exit code and a single stderr line. Nothing here
26
+ * prints a raw upstream message: ApiError redacted it at construction.
27
+ */
28
+ export declare function reportError(error: unknown, io: CommandIo): ExitCode;
29
+ //# sourceMappingURL=runtime.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,56 @@
1
+ import { ConfigError } from "../config/config.js";
2
+ import { loadConfig } from "../config/load.js";
3
+ import { HttpClient } from "../api/http.js";
4
+ import { ApiError } from "../api/errors.js";
5
+ import { ExitCode } from "../output/exit-codes.js";
6
+ export function userAgent(version) {
7
+ // Identifiable, and carrying nothing about the user (plan §9.2).
8
+ return `mafdet-cli/${version}`;
9
+ }
10
+ export function buildRuntime(io) {
11
+ const config = loadConfig({
12
+ env: io.env,
13
+ cwd: io.cwd,
14
+ userConfigFile: io.userConfigFile,
15
+ });
16
+ const shared = {
17
+ apiKey: config.apiKey,
18
+ userAgent: userAgent(io.version ?? "0.0.0"),
19
+ fetchImpl: io.fetchImpl,
20
+ };
21
+ return {
22
+ config,
23
+ dataPlane: new HttpClient({ baseUrl: config.baseUrl, ...shared }),
24
+ accountPlane: new HttpClient({ baseUrl: config.backendUrl, ...shared }),
25
+ };
26
+ }
27
+ export function requireApiKey(config) {
28
+ if (!config.apiKey) {
29
+ throw new ApiError({
30
+ kind: "auth",
31
+ exitCode: ExitCode.Auth,
32
+ message: "no API key. Set MAFDET_API_KEY (there is deliberately no --api-key flag).",
33
+ });
34
+ }
35
+ }
36
+ /**
37
+ * Turn any failure into an exit code and a single stderr line. Nothing here
38
+ * prints a raw upstream message: ApiError redacted it at construction.
39
+ */
40
+ export function reportError(error, io) {
41
+ if (error instanceof ApiError) {
42
+ const id = error.requestId ? ` (request ${error.requestId})` : "";
43
+ io.stderr(`mafdet: ${error.message}${id}\n`);
44
+ if (error.kind === "rate_limited" &&
45
+ error.retryAfterSeconds !== undefined) {
46
+ io.stderr(`mafdet: retry after ${error.retryAfterSeconds}s\n`);
47
+ }
48
+ return error.exitCode;
49
+ }
50
+ if (error instanceof ConfigError) {
51
+ io.stderr(`mafdet: ${error.message}\n`);
52
+ return ExitCode.Usage;
53
+ }
54
+ io.stderr(`mafdet: ${error.message}\n`);
55
+ return ExitCode.General;
56
+ }
@@ -0,0 +1,7 @@
1
+ import { ExitCode } from "../output/exit-codes.js";
2
+ import { type CommandIo } from "./runtime.js";
3
+ export interface SpeechIo extends CommandIo {
4
+ readStdin?: () => Promise<string>;
5
+ }
6
+ export declare function speechCommand(argv: readonly string[], io: SpeechIo): Promise<ExitCode>;
7
+ //# sourceMappingURL=speech.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"speech.d.ts","sourceRoot":"","sources":["../../src/commands/speech.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAEnD,OAAO,EAIL,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAStB,MAAM,WAAW,QAAS,SAAQ,SAAS;IACzC,SAAS,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;CACnC;AAED,wBAAsB,aAAa,CACjC,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,EAAE,EAAE,QAAQ,GACX,OAAO,CAAC,QAAQ,CAAC,CAyEnB"}