@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.
- package/CHANGELOG.md +86 -0
- package/LICENSE +21 -0
- package/README.md +173 -0
- package/README.zh-CN.md +148 -0
- package/dist/agent/approvals.d.ts +38 -0
- package/dist/agent/approvals.d.ts.map +1 -0
- package/dist/agent/approvals.js +53 -0
- package/dist/agent/command-policy.d.ts +36 -0
- package/dist/agent/command-policy.d.ts.map +1 -0
- package/dist/agent/command-policy.js +133 -0
- package/dist/agent/loop.d.ts +107 -0
- package/dist/agent/loop.d.ts.map +1 -0
- package/dist/agent/loop.js +242 -0
- package/dist/agent/session.d.ts +62 -0
- package/dist/agent/session.d.ts.map +1 -0
- package/dist/agent/session.js +290 -0
- package/dist/agent/tools/read-only.d.ts +15 -0
- package/dist/agent/tools/read-only.d.ts.map +1 -0
- package/dist/agent/tools/read-only.js +254 -0
- package/dist/agent/tools/run-command.d.ts +20 -0
- package/dist/agent/tools/run-command.d.ts.map +1 -0
- package/dist/agent/tools/run-command.js +116 -0
- package/dist/agent/tools/types.d.ts +46 -0
- package/dist/agent/tools/types.d.ts.map +1 -0
- package/dist/agent/tools/types.js +16 -0
- package/dist/agent/tools/write.d.ts +3 -0
- package/dist/agent/tools/write.d.ts.map +1 -0
- package/dist/agent/tools/write.js +68 -0
- package/dist/api/account.d.ts +39 -0
- package/dist/api/account.d.ts.map +1 -0
- package/dist/api/account.js +39 -0
- package/dist/api/errors.d.ts +38 -0
- package/dist/api/errors.d.ts.map +1 -0
- package/dist/api/errors.js +149 -0
- package/dist/api/http.d.ts +63 -0
- package/dist/api/http.d.ts.map +1 -0
- package/dist/api/http.js +207 -0
- package/dist/api/models.d.ts +68 -0
- package/dist/api/models.d.ts.map +1 -0
- package/dist/api/models.js +91 -0
- package/dist/api/sse-parser.d.ts +72 -0
- package/dist/api/sse-parser.d.ts.map +1 -0
- package/dist/api/sse-parser.js +163 -0
- package/dist/commands/account.d.ts +4 -0
- package/dist/commands/account.d.ts.map +1 -0
- package/dist/commands/account.js +103 -0
- package/dist/commands/agent.d.ts +10 -0
- package/dist/commands/agent.d.ts.map +1 -0
- package/dist/commands/agent.js +268 -0
- package/dist/commands/chat.d.ts +13 -0
- package/dist/commands/chat.d.ts.map +1 -0
- package/dist/commands/chat.js +191 -0
- package/dist/commands/config.d.ts +15 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +74 -0
- package/dist/commands/doctor.d.ts +15 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +157 -0
- package/dist/commands/embed.d.ts +7 -0
- package/dist/commands/embed.d.ts.map +1 -0
- package/dist/commands/embed.js +105 -0
- package/dist/commands/models.d.ts +4 -0
- package/dist/commands/models.d.ts.map +1 -0
- package/dist/commands/models.js +151 -0
- package/dist/commands/runtime.d.ts +29 -0
- package/dist/commands/runtime.d.ts.map +1 -0
- package/dist/commands/runtime.js +56 -0
- package/dist/commands/speech.d.ts +7 -0
- package/dist/commands/speech.d.ts.map +1 -0
- package/dist/commands/speech.js +111 -0
- package/dist/config/config.d.ts +57 -0
- package/dist/config/config.d.ts.map +1 -0
- package/dist/config/config.js +129 -0
- package/dist/config/load.d.ts +20 -0
- package/dist/config/load.d.ts.map +1 -0
- package/dist/config/load.js +48 -0
- package/dist/config/paths.d.ts +11 -0
- package/dist/config/paths.d.ts.map +1 -0
- package/dist/config/paths.js +26 -0
- package/dist/filesystem/diff.d.ts +12 -0
- package/dist/filesystem/diff.d.ts.map +1 -0
- package/dist/filesystem/diff.js +109 -0
- package/dist/filesystem/sensitive-files.d.ts +19 -0
- package/dist/filesystem/sensitive-files.d.ts.map +1 -0
- package/dist/filesystem/sensitive-files.js +46 -0
- package/dist/filesystem/workspace.d.ts +92 -0
- package/dist/filesystem/workspace.d.ts.map +1 -0
- package/dist/filesystem/workspace.js +264 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +109 -0
- package/dist/output/exit-codes.d.ts +25 -0
- package/dist/output/exit-codes.d.ts.map +1 -0
- package/dist/output/exit-codes.js +23 -0
- package/dist/output/format.d.ts +28 -0
- package/dist/output/format.d.ts.map +1 -0
- package/dist/output/format.js +56 -0
- package/dist/output/money.d.ts +16 -0
- package/dist/output/money.d.ts.map +1 -0
- package/dist/output/money.js +35 -0
- package/dist/output/write-file.d.ts +11 -0
- package/dist/output/write-file.d.ts.map +1 -0
- package/dist/output/write-file.js +38 -0
- package/dist/security/redact.d.ts +16 -0
- package/dist/security/redact.d.ts.map +1 -0
- package/dist/security/redact.js +61 -0
- 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 @@
|
|
|
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"}
|