@0dai-dev/cli 4.3.6 → 4.3.7
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/README.md +12 -11
- package/bin/0dai.js +127 -30
- package/lib/ai/manifest/mcp-exposure-contract.json +121 -0
- package/lib/ai/meta/manifest/mcp-tool-tiers.json +435 -0
- package/lib/ai/registry/mcp-catalog.json +98 -0
- package/lib/commands/auth.js +2 -1
- package/lib/commands/compliance.js +1 -1
- package/lib/commands/doctor.js +506 -12
- package/lib/commands/experience.js +40 -5
- package/lib/commands/feedback.js +157 -15
- package/lib/commands/gh.js +26 -0
- package/lib/commands/graph.js +9 -4
- package/lib/commands/heatmap.js +1 -1
- package/lib/commands/init.js +209 -27
- package/lib/commands/mcp.js +111 -33
- package/lib/commands/models.js +138 -41
- package/lib/commands/provider.js +30 -59
- package/lib/commands/quota.js +1 -1
- package/lib/commands/receipt.js +1 -1
- package/lib/commands/run.js +14 -6
- package/lib/commands/runner.js +31 -1
- package/lib/commands/status.js +38 -10
- package/lib/commands/swarm.js +130 -12
- package/lib/commands/update.js +184 -38
- package/lib/commands/usage.js +1 -1
- package/lib/commands/validate.js +32 -3
- package/lib/commands/vault.js +43 -8
- package/lib/python/__init__.py +0 -0
- package/lib/python/agent_quotas.py +525 -0
- package/lib/python/anomaly_alert.py +397 -0
- package/lib/python/anti_pattern_detector.py +799 -0
- package/lib/python/auth.py +443 -0
- package/lib/python/capi_profile_guard.py +477 -0
- package/lib/python/compliance_report.py +581 -0
- package/lib/python/drift_detector.py +388 -0
- package/lib/python/experience_pipeline.py +1130 -0
- package/lib/python/graph.py +19 -0
- package/lib/python/graph_core.py +293 -0
- package/lib/python/graph_io.py +179 -0
- package/lib/python/graph_legacy.py +2052 -0
- package/lib/python/graph_legacy_helpers.py +221 -0
- package/lib/python/graph_outcomes_core.py +85 -0
- package/lib/python/graph_queries.py +171 -0
- package/lib/python/graph_slice.py +198 -0
- package/lib/python/graph_slicer.py +576 -0
- package/lib/python/graph_slicer_cli.py +60 -0
- package/lib/python/graph_validation.py +64 -0
- package/lib/python/heatmap.py +934 -0
- package/lib/python/json_utils.py +193 -0
- package/lib/python/mcp_exposure_check.py +247 -0
- package/lib/python/model_router.py +1434 -0
- package/lib/python/project_manager.py +621 -0
- package/lib/python/provider_profiles.py +1618 -0
- package/lib/python/provider_registry.py +1211 -0
- package/lib/python/provider_registry_cli.py +125 -0
- package/lib/python/receipt_png.py +727 -0
- package/lib/python/structural_memory.py +325 -0
- package/lib/python/swarm_cost.py +177 -0
- package/lib/python/usage_ledger.py +569 -0
- package/lib/scripts/mcp_tier_config.py +240 -0
- package/lib/shared.js +95 -12
- package/lib/tui/index.mjs +35174 -0
- package/lib/utils/activation_telemetry.js +1 -4
- package/lib/utils/constants.js +7 -1
- package/lib/utils/identity.js +184 -0
- package/lib/utils/mcp-auth.js +81 -15
- package/lib/utils/plan.js +1 -1
- package/lib/vault/index.js +19 -3
- package/lib/vault/storage.js +21 -2
- package/lib/wizard.js +5 -2
- package/package.json +9 -3
- package/scripts/build-python-bundle.js +106 -0
- package/scripts/build-tui.js +14 -1
- package/scripts/harvest_experience.py +523 -0
- package/scripts/postinstall.js +15 -9
package/lib/commands/mcp.js
CHANGED
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Subcommands (all read-only / inspection):
|
|
7
7
|
* - list [--plan free|pro|team|enterprise] [--json]
|
|
8
|
-
* Enumerate MCP tools exposed by
|
|
8
|
+
* Enumerate MCP tools exposed by the MCP tier manifest.
|
|
9
9
|
* Resolves from ai/meta/manifest/mcp-tool-tiers.json when present,
|
|
10
|
-
* falls back to
|
|
10
|
+
* falls back to package metadata, then scans @mcp.tool decorators in
|
|
11
|
+
* scripts/mcp_server.py for repo checkouts.
|
|
11
12
|
* - catalog [--stack NAME] [--json]
|
|
12
13
|
* Show the per-stack MCP server catalog (common + stack servers)
|
|
13
14
|
* from templates/layer/ai/registry/mcp-catalog.json.
|
|
@@ -16,6 +17,8 @@
|
|
|
16
17
|
* - call <tool> [--json '<args>'] [--json-out]
|
|
17
18
|
* Invoke a single MCP tool synchronously via scripts/mcp_server.py
|
|
18
19
|
* (no daemon spawn — direct in-process call inside the helper subprocess).
|
|
20
|
+
* This is a repo-checkout helper; npm installs use the OAuth HTTP /mcp
|
|
21
|
+
* entry for live MCP calls.
|
|
19
22
|
*
|
|
20
23
|
* This command never mutates project state. It is the documented
|
|
21
24
|
* counterpart of the `mcp list` / `mcp call` / `mcp catalog` docs and the
|
|
@@ -31,9 +34,9 @@ function _printHelp() {
|
|
|
31
34
|
console.log(" 0dai mcp list [--plan free|pro|team|enterprise] [--json]");
|
|
32
35
|
console.log(" 0dai mcp catalog [--stack NAME] [--json]");
|
|
33
36
|
console.log(" 0dai mcp doctor [--json]");
|
|
34
|
-
console.log(" 0dai mcp call <tool> [--json '<json-args>'] [--json-out]");
|
|
37
|
+
console.log(" 0dai mcp call <tool> [--json '<json-args>'] [--json-out] (repo-checkout helper)");
|
|
35
38
|
console.log("");
|
|
36
|
-
console.log("Read-only inspection of
|
|
39
|
+
console.log("Read-only inspection of MCP metadata; live npm MCP calls use the claude_ai_0dai HTTP /mcp entry.");
|
|
37
40
|
console.log("");
|
|
38
41
|
}
|
|
39
42
|
|
|
@@ -59,12 +62,20 @@ function _readJsonFile(filePath) {
|
|
|
59
62
|
}
|
|
60
63
|
}
|
|
61
64
|
|
|
62
|
-
function
|
|
63
|
-
|
|
65
|
+
function _packageDataPath(...parts) {
|
|
66
|
+
return path.join(__dirname, "..", ...parts);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function _tierManifestCandidates(target) {
|
|
70
|
+
return [
|
|
64
71
|
path.join(target, "ai", "meta", "manifest", "mcp-tool-tiers.json"),
|
|
65
72
|
path.join(target, "ai", "manifest", "mcp-tool-tiers.json"),
|
|
73
|
+
_packageDataPath("ai", "meta", "manifest", "mcp-tool-tiers.json"),
|
|
66
74
|
];
|
|
67
|
-
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function _toolsFromTierManifest(target, plan) {
|
|
78
|
+
for (const candidate of _tierManifestCandidates(target)) {
|
|
68
79
|
if (!fs.existsSync(candidate)) continue;
|
|
69
80
|
const data = _readJsonFile(candidate);
|
|
70
81
|
if (!data || typeof data !== "object") continue;
|
|
@@ -125,6 +136,7 @@ function _resolveCatalogPath(target) {
|
|
|
125
136
|
const candidates = [
|
|
126
137
|
path.join(target, "ai", "registry", "mcp-catalog.json"),
|
|
127
138
|
path.join(target, "templates", "layer", "ai", "registry", "mcp-catalog.json"),
|
|
139
|
+
_packageDataPath("ai", "registry", "mcp-catalog.json"),
|
|
128
140
|
];
|
|
129
141
|
// Also look next to the npm package (when run from a sync'd repo elsewhere).
|
|
130
142
|
const repoRoot = path.resolve(__dirname, "..", "..", "..", "..");
|
|
@@ -203,14 +215,12 @@ function _cmdCatalog(target, args) {
|
|
|
203
215
|
console.log(`\n ${T}available stacks${R}: ${available.join(", ")}\n`);
|
|
204
216
|
}
|
|
205
217
|
|
|
206
|
-
// Flag MCP server entries left behind by
|
|
207
|
-
// connect.
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
//
|
|
211
|
-
//
|
|
212
|
-
// block is dead weight. Detection stays on the unambiguous /sse signal to avoid
|
|
213
|
-
// false positives on a legitimately-wired bearer host-pool config (#57).
|
|
218
|
+
// Flag MCP server entries left behind by older `0dai sync` runs that no longer
|
|
219
|
+
// connect. `mergeMcpConfig` only manages its own servers and never prunes unknown
|
|
220
|
+
// user entries -- even with --reset -- so these persist after upgrade until
|
|
221
|
+
// removed by hand. The current generator connects operator-MCP via the headerless
|
|
222
|
+
// "claude_ai_0dai" OAuth /mcp entry; local stdio entries must point at an
|
|
223
|
+
// existing helper script inside the project or at an absolute helper path.
|
|
214
224
|
function _detectStaleMcpServers(mcpServers) {
|
|
215
225
|
const stale = [];
|
|
216
226
|
for (const [name, cfg] of Object.entries(mcpServers || {})) {
|
|
@@ -224,10 +234,45 @@ function _detectStaleMcpServers(mcpServers) {
|
|
|
224
234
|
return stale;
|
|
225
235
|
}
|
|
226
236
|
|
|
237
|
+
function _mcpServerScriptArg(cfg) {
|
|
238
|
+
const args = Array.isArray(cfg && cfg.args) ? cfg.args : [];
|
|
239
|
+
for (const arg of args) {
|
|
240
|
+
const value = String(arg || "");
|
|
241
|
+
if (/(^|[/\\])mcp_server\.py$/.test(value)) return value;
|
|
242
|
+
}
|
|
243
|
+
const command = typeof (cfg && cfg.command) === "string" ? cfg.command : "";
|
|
244
|
+
return /(^|[/\\])mcp_server\.py$/.test(command) ? command : "";
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function _resolveMcpConfigPath(rawPath, target) {
|
|
248
|
+
const value = String(rawPath || "").trim();
|
|
249
|
+
if (!value) return "";
|
|
250
|
+
return path.resolve(path.isAbsolute(value) ? value : path.join(target, value));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function _detectMissingMcpServerScripts(mcpServers, target) {
|
|
254
|
+
const stale = [];
|
|
255
|
+
for (const [name, cfg] of Object.entries(mcpServers || {})) {
|
|
256
|
+
if (!cfg || typeof cfg !== "object") continue;
|
|
257
|
+
const scriptArg = _mcpServerScriptArg(cfg);
|
|
258
|
+
if (!scriptArg) continue;
|
|
259
|
+
const resolved = _resolveMcpConfigPath(scriptArg, target);
|
|
260
|
+
if (!fs.existsSync(resolved)) {
|
|
261
|
+
stale.push({
|
|
262
|
+
name,
|
|
263
|
+
reason: `missing MCP server script referenced by config (${scriptArg})`,
|
|
264
|
+
path: resolved,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return stale;
|
|
269
|
+
}
|
|
270
|
+
|
|
227
271
|
function _cmdDoctor(target, args) {
|
|
228
272
|
const wantJson = args.includes("--json");
|
|
229
273
|
const report = {
|
|
230
274
|
target,
|
|
275
|
+
local_server_bundled: false,
|
|
231
276
|
server_script: { path: "", ok: false },
|
|
232
277
|
mcp_config: { path: "", ok: false, servers: [], stale: [] },
|
|
233
278
|
tier_manifest: { path: "", ok: false, free_count: 0, pro_count: 0 },
|
|
@@ -236,6 +281,7 @@ function _cmdDoctor(target, args) {
|
|
|
236
281
|
|
|
237
282
|
const script = findRepoScript(target, "mcp_server.py");
|
|
238
283
|
if (script) {
|
|
284
|
+
report.local_server_bundled = true;
|
|
239
285
|
report.server_script.path = script;
|
|
240
286
|
report.server_script.ok = fs.existsSync(script);
|
|
241
287
|
}
|
|
@@ -247,13 +293,15 @@ function _cmdDoctor(target, args) {
|
|
|
247
293
|
if (data && data.mcpServers && typeof data.mcpServers === "object") {
|
|
248
294
|
report.mcp_config.ok = true;
|
|
249
295
|
report.mcp_config.servers = Object.keys(data.mcpServers);
|
|
250
|
-
report.mcp_config.stale =
|
|
296
|
+
report.mcp_config.stale = [
|
|
297
|
+
..._detectStaleMcpServers(data.mcpServers),
|
|
298
|
+
..._detectMissingMcpServerScripts(data.mcpServers, target),
|
|
299
|
+
];
|
|
251
300
|
}
|
|
252
301
|
}
|
|
253
302
|
|
|
254
303
|
const tierCandidates = [
|
|
255
|
-
|
|
256
|
-
path.join(target, "ai", "manifest", "mcp-tool-tiers.json"),
|
|
304
|
+
..._tierManifestCandidates(target),
|
|
257
305
|
];
|
|
258
306
|
for (const candidate of tierCandidates) {
|
|
259
307
|
if (!fs.existsSync(candidate)) continue;
|
|
@@ -278,6 +326,18 @@ function _cmdDoctor(target, args) {
|
|
|
278
326
|
}
|
|
279
327
|
}
|
|
280
328
|
|
|
329
|
+
// The local stdio server script is a repo-checkout artifact that the
|
|
330
|
+
// published npm package intentionally does not bundle: npm users get live MCP
|
|
331
|
+
// calls over the OAuth "claude_ai_0dai" HTTP /mcp entry, not a local server.
|
|
332
|
+
// Read-only list/catalog metadata may be package-bundled, but only treat the
|
|
333
|
+
// server script absence as a failure when we ARE in a repo checkout.
|
|
334
|
+
const allOk = report.local_server_bundled
|
|
335
|
+
? (report.server_script.ok && report.mcp_config.ok
|
|
336
|
+
&& report.mcp_config.stale.length === 0
|
|
337
|
+
&& (report.tier_manifest.ok || report.catalog.ok))
|
|
338
|
+
: report.mcp_config.stale.length === 0;
|
|
339
|
+
if (!allOk) process.exitCode = 1;
|
|
340
|
+
|
|
281
341
|
if (wantJson) {
|
|
282
342
|
console.log(JSON.stringify(report));
|
|
283
343
|
return;
|
|
@@ -285,31 +345,47 @@ function _cmdDoctor(target, args) {
|
|
|
285
345
|
|
|
286
346
|
const ok = (b) => (b ? G + "ok" + R : W + "missing" + R);
|
|
287
347
|
console.log(`\n ${T}0dai mcp doctor${R} target=${target}\n`);
|
|
288
|
-
|
|
348
|
+
|
|
349
|
+
// npm-installed package: the local stdio server is never shipped, so don't
|
|
350
|
+
// dead-end on a bare "missing" line. Explain WHY and point at the actual MCP
|
|
351
|
+
// path npm users have (the OAuth HTTP /mcp entry).
|
|
352
|
+
if (!report.local_server_bundled) {
|
|
353
|
+
console.log(` local stdio MCP server ${D}not bundled (npm install)${R}`);
|
|
354
|
+
console.log(` ${D}The published 0dai package does not ship the local stdio MCP server${R}`);
|
|
355
|
+
console.log(` ${D}(scripts/mcp_server.py). MCP tools are served over the OAuth${R}`);
|
|
356
|
+
console.log(` ${D}"claude_ai_0dai" HTTP /mcp entry instead.${R}`);
|
|
357
|
+
console.log(` ${D}Read-only list/catalog metadata is package-bundled when available.${R}`);
|
|
358
|
+
console.log(` ${G}→${R} run ${T}/mcp${R} in Claude Code and Authenticate the ${T}claude_ai_0dai${R} server`);
|
|
359
|
+
console.log(` ${G}→${R} if that entry is absent, run ${T}0dai sync --target ${target}${R} to write it`);
|
|
360
|
+
console.log("");
|
|
361
|
+
} else {
|
|
362
|
+
console.log(` server script ${ok(report.server_script.ok)} ${D}${report.server_script.path || "(not found)"}${R}`);
|
|
363
|
+
}
|
|
364
|
+
|
|
289
365
|
console.log(` .mcp.json ${ok(report.mcp_config.ok)} ${D}${report.mcp_config.path || "(not found)"}${R}`);
|
|
290
366
|
if (report.mcp_config.ok) {
|
|
291
367
|
console.log(` servers: ${report.mcp_config.servers.join(", ")}`);
|
|
292
368
|
}
|
|
293
369
|
if (report.mcp_config.stale.length) {
|
|
294
|
-
console.log(` ${W}stale${R} (remove from .mcp.json — superseded by the "
|
|
370
|
+
console.log(` ${W}stale${R} (remove from .mcp.json — superseded by the "claude_ai_0dai" OAuth /mcp entry):`);
|
|
295
371
|
for (const s of report.mcp_config.stale) {
|
|
296
|
-
|
|
372
|
+
const detail = s.path ? `${s.reason}: ${s.path}` : s.reason;
|
|
373
|
+
console.log(` ${W}!${R} ${s.name} ${D}— ${detail}${R}`);
|
|
297
374
|
}
|
|
298
375
|
}
|
|
299
|
-
|
|
300
|
-
if (report.
|
|
301
|
-
console.log(`
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
console.log(`
|
|
376
|
+
|
|
377
|
+
if (report.local_server_bundled) {
|
|
378
|
+
console.log(` tier manifest ${ok(report.tier_manifest.ok)} ${D}${report.tier_manifest.path || "(not found)"}${R}`);
|
|
379
|
+
if (report.tier_manifest.ok) {
|
|
380
|
+
console.log(` free=${report.tier_manifest.free_count} pro=${report.tier_manifest.pro_count}`);
|
|
381
|
+
}
|
|
382
|
+
console.log(` catalog ${ok(report.catalog.ok)} ${D}${report.catalog.path || "(not found)"}${R}`);
|
|
383
|
+
if (report.catalog.ok) {
|
|
384
|
+
console.log(` stacks: ${report.catalog.stacks}`);
|
|
385
|
+
}
|
|
306
386
|
}
|
|
307
387
|
console.log("");
|
|
308
388
|
|
|
309
|
-
const allOk = report.server_script.ok && report.mcp_config.ok
|
|
310
|
-
&& report.mcp_config.stale.length === 0
|
|
311
|
-
&& (report.tier_manifest.ok || report.catalog.ok);
|
|
312
|
-
if (!allOk) process.exitCode = 1;
|
|
313
389
|
}
|
|
314
390
|
|
|
315
391
|
function _cmdCall(target, toolName, args) {
|
|
@@ -323,7 +399,9 @@ function _cmdCall(target, toolName, args) {
|
|
|
323
399
|
|
|
324
400
|
const script = findRepoScript(target, "mcp_server.py");
|
|
325
401
|
if (!script) {
|
|
326
|
-
|
|
402
|
+
const message = "mcp call is a repo-checkout helper: scripts/mcp_server.py is not bundled in npm installs. Use the claude_ai_0dai OAuth HTTP /mcp entry for live MCP tool calls.";
|
|
403
|
+
if (wantJsonOut) console.log(JSON.stringify({ error: message, repo_checkout_required: true }));
|
|
404
|
+
else log(message);
|
|
327
405
|
process.exitCode = 1;
|
|
328
406
|
return;
|
|
329
407
|
}
|
package/lib/commands/models.js
CHANGED
|
@@ -1,45 +1,114 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
const shared = require("../shared");
|
|
3
|
-
const { T, R, SUPPORTED_CLIS } = shared;
|
|
3
|
+
const { log, T, R, D, SUPPORTED_CLIS, spawnSync } = shared;
|
|
4
4
|
const {
|
|
5
5
|
probeInstalledCliNames,
|
|
6
6
|
summarizeModelAvailability,
|
|
7
7
|
formatAvailableFooter,
|
|
8
8
|
} = require("../utils/model_ratings");
|
|
9
9
|
|
|
10
|
+
// Scores from benchmark_models.py (3-task: read/count/review) + field-tested ledger.
|
|
11
|
+
// Catalog refreshed 2026-04-19; prior benchmark baseline 2026-04-06.
|
|
12
|
+
// Since then: native claude `--model opus` alias routes to opus-4.7 1M-ctx (CC extended);
|
|
13
|
+
// gpt-5.5 promoted to primary in agent-matrix/model_registry;
|
|
14
|
+
// gpt-5.3-codex empirically verified — does heavy internal reasoning
|
|
15
|
+
// (66k tokens on trivial "hello" prompt), pick deliberately for architecture.
|
|
16
|
+
// Billing: native claude/codex/gemini = subscription (marginal $0/call but tokens tracked
|
|
17
|
+
// for opportunity cost per operator 2026-04-19). RuAPI / opencode-go = pay-per-use.
|
|
18
|
+
const MODELS = [
|
|
19
|
+
{ name: "Claude Opus 4.7 (1M)", tier: "deep", score: 98, cli: "claude", flag: "--model opus", tested: true },
|
|
20
|
+
{ name: "Claude Opus 4.7", tier: "deep", score: 97, cli: "opencode", flag: "-m RuAPI/claude-opus-4.7", tested: true },
|
|
21
|
+
{ name: "GPT-5.5", tier: "deep", score: 96, cli: "codex", flag: "-m gpt-5.5", tested: true },
|
|
22
|
+
{ name: "Claude Opus 4.6", tier: "deep", score: 95, cli: "claude", flag: "--model claude-opus-4-6" },
|
|
23
|
+
{ name: "Opus 4.6 (RuAPI)", tier: "deep", score: 95, cli: "opencode", flag: "-m RuAPI/claude-opus-4.6", tested: true },
|
|
24
|
+
{ name: "GPT-5.4-mini", tier: "fast", score: 93, cli: "codex", flag: "-m gpt-5.4-mini", tested: true },
|
|
25
|
+
{ name: "MiniMax M2.7", tier: "balanced", score: 93, cli: "opencode", flag: "-m opencode-go/minimax-m2.7", tested: true },
|
|
26
|
+
{ name: "Claude Sonnet 4.6", tier: "balanced", score: 90, cli: "claude", flag: "--model sonnet" },
|
|
27
|
+
{ name: "Sonnet 4.6 (RuAPI)",tier: "balanced", score: 90, cli: "opencode", flag: "-m RuAPI/claude-sonnet-4.6", tested: true },
|
|
28
|
+
{ name: "GPT-5.4", tier: "balanced", score: 90, cli: "codex", flag: "-m gpt-5.4", tested: true },
|
|
29
|
+
{ name: "Gemini 3.1 Pro", tier: "deep", score: 89, cli: "opencode", flag: "-m RuAPI/gemini-3.1-pro-preview", tested: true },
|
|
30
|
+
{ name: "Kimi K2.5", tier: "balanced", score: 88, cli: "opencode", flag: "-m opencode-go/kimi-k2.5", tested: true },
|
|
31
|
+
{ name: "Qwen 3.6+ Free", tier: "free", score: 88, cli: "opencode", flag: "-m opencode/qwen3.6-plus-free", tested: true },
|
|
32
|
+
{ name: "GPT-5.2", tier: "balanced", score: 87, cli: "codex", flag: "-m gpt-5.2", tested: true },
|
|
33
|
+
{ name: "Gemini 3.1 Pro", tier: "balanced", score: 85, cli: "gemini", flag: "-m gemini-3.1-pro" },
|
|
34
|
+
{ name: "GPT-5.3 Codex", tier: "deep", score: 83, cli: "codex", flag: "-m gpt-5.3-codex", tested: true },
|
|
35
|
+
{ name: "GPT-5.3 Spark", tier: "fast", score: 82, cli: "codex", flag: "-m gpt-5.3-codex-spark" },
|
|
36
|
+
{ name: "Claude Haiku 4.5", tier: "fast", score: 78, cli: "claude", flag: "--model haiku" },
|
|
37
|
+
{ name: "Gemini 3 Flash", tier: "fast", score: 77, cli: "gemini", flag: "-m gemini-3-flash" },
|
|
38
|
+
{ name: "Mimo v2 Pro", tier: "fast", score: 74, cli: "opencode", flag: "-m opencode-go/mimo-v2-pro", tested: true },
|
|
39
|
+
{ name: "GPT-5.4 (opencode)",tier: "fast", score: 74, cli: "opencode", flag: "-m openai/gpt-5.4", tested: true },
|
|
40
|
+
{ name: "MiniMax M2.5", tier: "slow", score: 57, cli: "opencode", flag: "-m opencode-go/minimax-m2.5", tested: true },
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
function fallbackTierForTask(taskType) {
|
|
44
|
+
const normalized = String(taskType || "").toLowerCase();
|
|
45
|
+
if (["docs", "doc", "test", "tests", "lint"].includes(normalized)) return "fast";
|
|
46
|
+
if (["planning", "plan", "architecture", "design", "review"].includes(normalized)) return "deep";
|
|
47
|
+
return "balanced";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function localRecommendation(taskType, options = {}) {
|
|
51
|
+
const minQuality = Number(options.minQuality || 0);
|
|
52
|
+
const installedCliNames = options.installedCliNames || probeInstalledCliNames(SUPPORTED_CLIS);
|
|
53
|
+
const preferredTier = fallbackTierForTask(taskType);
|
|
54
|
+
const sorted = [...MODELS]
|
|
55
|
+
.filter((model) => !minQuality || model.score >= minQuality)
|
|
56
|
+
.sort((a, b) => b.score - a.score);
|
|
57
|
+
const available = sorted.filter((model) => installedCliNames.has(model.cli));
|
|
58
|
+
const pool = available.length ? available : sorted;
|
|
59
|
+
const tierPool = pool.filter((model) => model.tier === preferredTier);
|
|
60
|
+
const recommendation = tierPool[0] || pool[0] || sorted[0] || null;
|
|
61
|
+
if (!recommendation) return null;
|
|
62
|
+
return {
|
|
63
|
+
...recommendation,
|
|
64
|
+
command: `${recommendation.cli} ${recommendation.flag}`.trim(),
|
|
65
|
+
fallback_reason: available.length ? "local_available_ratings" : "local_static_ratings",
|
|
66
|
+
preferred_tier: preferredTier,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function printLocalRecommendation(rec, reason) {
|
|
71
|
+
console.log(` ${T}${rec.name}${R} (${rec.tier}, score ${rec.score})`);
|
|
72
|
+
console.log(` command: ${rec.command}`);
|
|
73
|
+
console.log(` ${D}${reason}; fallback=${rec.fallback_reason}${R}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function firstOutputLine(result) {
|
|
77
|
+
if (!result) return "";
|
|
78
|
+
const text = `${result.stderr || ""}\n${result.stdout || ""}`.trim();
|
|
79
|
+
return text.split("\n").map((line) => line.trim()).find(Boolean) || "";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function emitLocalRecommendationFallback(taskType, goal, minQuality, asJson, reasonCode, message, routerError = "") {
|
|
83
|
+
const recommendation = localRecommendation(taskType, { minQuality });
|
|
84
|
+
if (asJson) {
|
|
85
|
+
console.log(JSON.stringify({
|
|
86
|
+
status: "fallback",
|
|
87
|
+
reason: reasonCode,
|
|
88
|
+
task: taskType,
|
|
89
|
+
goal,
|
|
90
|
+
...(routerError ? { router_error: routerError } : {}),
|
|
91
|
+
recommendation,
|
|
92
|
+
}, null, 2));
|
|
93
|
+
} else {
|
|
94
|
+
log(message);
|
|
95
|
+
if (routerError) console.log(` ${D}router: ${routerError}${R}`);
|
|
96
|
+
if (recommendation) printLocalRecommendation(recommendation, message);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function runModelRouter(args) {
|
|
101
|
+
const result = spawnSync("python3", args, { encoding: "utf8" });
|
|
102
|
+
if (result.error) return { ok: false, result: { stderr: result.error.message, stdout: "" } };
|
|
103
|
+
if (typeof result.status === "number" && result.status === 0) {
|
|
104
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
105
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
106
|
+
return { ok: true, result };
|
|
107
|
+
}
|
|
108
|
+
return { ok: false, result };
|
|
109
|
+
}
|
|
110
|
+
|
|
10
111
|
function cmdModels(filter) {
|
|
11
|
-
// Scores from benchmark_models.py (3-task: read/count/review) + field-tested ledger.
|
|
12
|
-
// Catalog refreshed 2026-04-19; prior benchmark baseline 2026-04-06.
|
|
13
|
-
// Since then: native claude `--model opus` alias routes to opus-4.7 1M-ctx (CC extended);
|
|
14
|
-
// gpt-5.5 promoted to primary in agent-matrix/model_registry;
|
|
15
|
-
// gpt-5.3-codex empirically verified — does heavy internal reasoning
|
|
16
|
-
// (66k tokens on trivial "hello" prompt), pick deliberately for architecture.
|
|
17
|
-
// Billing: native claude/codex/gemini = subscription (marginal $0/call but tokens tracked
|
|
18
|
-
// for opportunity cost per operator 2026-04-19). RuAPI / opencode-go = pay-per-use.
|
|
19
|
-
const MODELS = [
|
|
20
|
-
{ name: "Claude Opus 4.7 (1M)", tier: "deep", score: 98, cli: "claude", flag: "--model opus", tested: true },
|
|
21
|
-
{ name: "Claude Opus 4.7", tier: "deep", score: 97, cli: "opencode", flag: "-m RuAPI/claude-opus-4.7", tested: true },
|
|
22
|
-
{ name: "GPT-5.5", tier: "deep", score: 96, cli: "codex", flag: "-m gpt-5.5", tested: true },
|
|
23
|
-
{ name: "Claude Opus 4.6", tier: "deep", score: 95, cli: "claude", flag: "--model claude-opus-4-6" },
|
|
24
|
-
{ name: "Opus 4.6 (RuAPI)", tier: "deep", score: 95, cli: "opencode", flag: "-m RuAPI/claude-opus-4.6", tested: true },
|
|
25
|
-
{ name: "GPT-5.4-mini", tier: "fast", score: 93, cli: "codex", flag: "-m gpt-5.4-mini", tested: true },
|
|
26
|
-
{ name: "MiniMax M2.7", tier: "balanced", score: 93, cli: "opencode", flag: "-m opencode-go/minimax-m2.7", tested: true },
|
|
27
|
-
{ name: "Claude Sonnet 4.6", tier: "balanced", score: 90, cli: "claude", flag: "--model sonnet" },
|
|
28
|
-
{ name: "Sonnet 4.6 (RuAPI)",tier: "balanced", score: 90, cli: "opencode", flag: "-m RuAPI/claude-sonnet-4.6", tested: true },
|
|
29
|
-
{ name: "GPT-5.4", tier: "balanced", score: 90, cli: "codex", flag: "-m gpt-5.4", tested: true },
|
|
30
|
-
{ name: "Gemini 3.1 Pro", tier: "deep", score: 89, cli: "opencode", flag: "-m RuAPI/gemini-3.1-pro-preview", tested: true },
|
|
31
|
-
{ name: "Kimi K2.5", tier: "balanced", score: 88, cli: "opencode", flag: "-m opencode-go/kimi-k2.5", tested: true },
|
|
32
|
-
{ name: "Qwen 3.6+ Free", tier: "free", score: 88, cli: "opencode", flag: "-m opencode/qwen3.6-plus-free", tested: true },
|
|
33
|
-
{ name: "GPT-5.2", tier: "balanced", score: 87, cli: "codex", flag: "-m gpt-5.2", tested: true },
|
|
34
|
-
{ name: "Gemini 3.1 Pro", tier: "balanced", score: 85, cli: "gemini", flag: "-m gemini-3.1-pro" },
|
|
35
|
-
{ name: "GPT-5.3 Codex", tier: "deep", score: 83, cli: "codex", flag: "-m gpt-5.3-codex", tested: true },
|
|
36
|
-
{ name: "GPT-5.3 Spark", tier: "fast", score: 82, cli: "codex", flag: "-m gpt-5.3-codex-spark" },
|
|
37
|
-
{ name: "Claude Haiku 4.5", tier: "fast", score: 78, cli: "claude", flag: "--model haiku" },
|
|
38
|
-
{ name: "Gemini 3 Flash", tier: "fast", score: 77, cli: "gemini", flag: "-m gemini-3-flash" },
|
|
39
|
-
{ name: "Mimo v2 Pro", tier: "fast", score: 74, cli: "opencode", flag: "-m opencode-go/mimo-v2-pro", tested: true },
|
|
40
|
-
{ name: "GPT-5.4 (opencode)",tier: "fast", score: 74, cli: "opencode", flag: "-m openai/gpt-5.4", tested: true },
|
|
41
|
-
{ name: "MiniMax M2.5", tier: "slow", score: 57, cli: "opencode", flag: "-m opencode-go/minimax-m2.5", tested: true },
|
|
42
|
-
];
|
|
43
112
|
|
|
44
113
|
const installedCliNames = probeInstalledCliNames(SUPPORTED_CLIS);
|
|
45
114
|
const availability = summarizeModelAvailability(MODELS, SUPPORTED_CLIS, installedCliNames);
|
|
@@ -75,7 +144,7 @@ function cmdModels(filter) {
|
|
|
75
144
|
|
|
76
145
|
async function cmdModelsRecommend(target, args) {
|
|
77
146
|
const shared = require("../shared");
|
|
78
|
-
const {
|
|
147
|
+
const { findRepoScript, requirePlan } = shared;
|
|
79
148
|
|
|
80
149
|
const gate = requirePlan("pro", "Model Recommend", target);
|
|
81
150
|
if (gate) { log(gate.error); log(gate.hint); return; }
|
|
@@ -92,8 +161,18 @@ async function cmdModelsRecommend(target, args) {
|
|
|
92
161
|
return;
|
|
93
162
|
}
|
|
94
163
|
|
|
95
|
-
const recScript =
|
|
96
|
-
if (!recScript) {
|
|
164
|
+
const recScript = shared.resolvePythonScript(target, "model_router.py");
|
|
165
|
+
if (!recScript) {
|
|
166
|
+
emitLocalRecommendationFallback(
|
|
167
|
+
taskType,
|
|
168
|
+
goal,
|
|
169
|
+
minQuality,
|
|
170
|
+
asJson,
|
|
171
|
+
"model_router_unavailable",
|
|
172
|
+
"model router unavailable; using local ratings fallback",
|
|
173
|
+
);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
97
176
|
|
|
98
177
|
// Ledger-recommend: pair per-repo outcome ledger (PR #509) with the static
|
|
99
178
|
// experience-event ranking. Falls back to the legacy `recommend` subcommand
|
|
@@ -103,8 +182,8 @@ async function cmdModelsRecommend(target, args) {
|
|
|
103
182
|
if (taskType) ledgerArgs.push("--task", taskType);
|
|
104
183
|
if (asJson) ledgerArgs.push("--json");
|
|
105
184
|
|
|
106
|
-
const ledgerResult =
|
|
107
|
-
if (
|
|
185
|
+
const ledgerResult = runModelRouter(ledgerArgs);
|
|
186
|
+
if (ledgerResult.ok) return;
|
|
108
187
|
|
|
109
188
|
const fwd = [recScript, "recommend", "--target", target];
|
|
110
189
|
if (taskType) fwd.push("--task", taskType);
|
|
@@ -113,8 +192,26 @@ async function cmdModelsRecommend(target, args) {
|
|
|
113
192
|
if (minQuality > 0) fwd.push("--min-quality", String(minQuality));
|
|
114
193
|
if (asJson) fwd.push("--json");
|
|
115
194
|
|
|
116
|
-
const result =
|
|
117
|
-
if (
|
|
195
|
+
const result = runModelRouter(fwd);
|
|
196
|
+
if (result.ok) return;
|
|
197
|
+
|
|
198
|
+
const routerError = firstOutputLine(result.result) || firstOutputLine(ledgerResult.result);
|
|
199
|
+
emitLocalRecommendationFallback(
|
|
200
|
+
taskType,
|
|
201
|
+
goal,
|
|
202
|
+
minQuality,
|
|
203
|
+
asJson,
|
|
204
|
+
"model_router_failed",
|
|
205
|
+
"model router failed; using local ratings fallback",
|
|
206
|
+
routerError,
|
|
207
|
+
);
|
|
118
208
|
}
|
|
119
209
|
|
|
120
|
-
module.exports = {
|
|
210
|
+
module.exports = {
|
|
211
|
+
cmdModels,
|
|
212
|
+
cmdModelsRecommend,
|
|
213
|
+
localRecommendation,
|
|
214
|
+
fallbackTierForTask,
|
|
215
|
+
MODELS,
|
|
216
|
+
emitLocalRecommendationFallback,
|
|
217
|
+
};
|
package/lib/commands/provider.js
CHANGED
|
@@ -1,84 +1,55 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
const shared = require("../shared");
|
|
4
|
-
const { log, spawnSync,
|
|
4
|
+
const { D, R, log, spawnSync, resolvePythonScript } = shared;
|
|
5
|
+
|
|
6
|
+
function hasTargetArg(args) {
|
|
7
|
+
return args.some((arg, index) => arg === "--target" || arg.startsWith("--target=") || args[index - 1] === "--target");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function providerProfileArgs(target, args) {
|
|
11
|
+
const subcommand = args[0] || "";
|
|
12
|
+
const forwarded = [...args];
|
|
13
|
+
if (target && !hasTargetArg(forwarded) && ["bind", "bindings", "invoke", "status"].includes(subcommand)) {
|
|
14
|
+
forwarded.push("--target", target);
|
|
15
|
+
}
|
|
16
|
+
return forwarded;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function providerRegistryArgs(target, args) {
|
|
20
|
+
const forwarded = [...args];
|
|
21
|
+
if (target && !hasTargetArg(forwarded)) {
|
|
22
|
+
forwarded.push("--target", target);
|
|
23
|
+
}
|
|
24
|
+
return forwarded;
|
|
25
|
+
}
|
|
5
26
|
|
|
6
27
|
function cmdProvider(target, args) {
|
|
7
28
|
// Check for per-repo provider commands first
|
|
8
29
|
const subcommand = args[0] || "";
|
|
9
30
|
|
|
10
31
|
if (subcommand === "list" || subcommand === "switch" || subcommand === "clear") {
|
|
11
|
-
//
|
|
12
|
-
const script =
|
|
32
|
+
// Per-repo provider profile commands: repo-first, bundled fallback.
|
|
33
|
+
const script = resolvePythonScript(target, "provider_registry_cli.py");
|
|
13
34
|
if (!script) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
import sys
|
|
17
|
-
import json
|
|
18
|
-
import pathlib
|
|
19
|
-
sys.path.insert(0, '${target}/scripts')
|
|
20
|
-
import provider_registry as pr
|
|
21
|
-
|
|
22
|
-
target = pathlib.Path('${target}')
|
|
23
|
-
args = ${JSON.stringify(args)}
|
|
24
|
-
subcmd = args[0] if args else 'list'
|
|
25
|
-
|
|
26
|
-
try:
|
|
27
|
-
if subcmd == 'list':
|
|
28
|
-
providers = pr.list_providers_for_repo(target)
|
|
29
|
-
print('Provider Status for', target.name)
|
|
30
|
-
print('=' * 60)
|
|
31
|
-
for p in providers:
|
|
32
|
-
active = '→' if p['active'] else ' '
|
|
33
|
-
source = f"[{p['source']}]" if p['source'] else ''
|
|
34
|
-
creds = '✓' if p['has_credentials'] else '✗'
|
|
35
|
-
print(f" {active} {p['name']:<12} {creds} creds {source:<10} {p['base_url']}")
|
|
36
|
-
print()
|
|
37
|
-
try:
|
|
38
|
-
resolved = pr.resolve_for_repo(target)
|
|
39
|
-
print(f"Active: {resolved['provider_name']} (source={resolved['source']})")
|
|
40
|
-
except pr.ProviderRegistryError as e:
|
|
41
|
-
print(f"No active provider: {e}")
|
|
42
|
-
elif subcmd == 'switch':
|
|
43
|
-
provider_name = ''
|
|
44
|
-
for i, a in enumerate(args):
|
|
45
|
-
if a == '--provider' and i + 1 < len(args):
|
|
46
|
-
provider_name = args[i + 1]
|
|
47
|
-
if not provider_name:
|
|
48
|
-
print('Usage: 0dai provider switch --target . --provider NAME')
|
|
49
|
-
sys.exit(1)
|
|
50
|
-
result = pr.switch_provider(target, provider_name)
|
|
51
|
-
print(f"Switched {target.name} to provider: {result['provider_name']}")
|
|
52
|
-
print(f" base_url: {result['base_url']}")
|
|
53
|
-
print(f" audit log: ~/.0dai/audit/provider-changes.jsonl")
|
|
54
|
-
elif subcmd == 'clear':
|
|
55
|
-
cleared = pr.clear_repo_profile(target)
|
|
56
|
-
if cleared:
|
|
57
|
-
print(f"Cleared per-repo profile for {target.name}")
|
|
58
|
-
print(f"Will fall back to global default or env vars")
|
|
59
|
-
else:
|
|
60
|
-
print(f"No per-repo profile to clear for {target.name}")
|
|
61
|
-
except pr.ProviderRegistryError as e:
|
|
62
|
-
print(f"Error: {e}")
|
|
63
|
-
sys.exit(1)
|
|
64
|
-
`;
|
|
65
|
-
const result = spawnSync("python3", ["-c", pyCode], { stdio: "inherit" });
|
|
66
|
-
if (typeof result.status === "number") process.exit(result.status);
|
|
35
|
+
log("provider registry unavailable in this environment");
|
|
36
|
+
console.log(` ${D}Expected bundled lib/python/provider_registry_cli.py or scripts/provider_registry_cli.py in repo checkout${R}`);
|
|
67
37
|
process.exit(1);
|
|
68
38
|
}
|
|
69
|
-
const forwarded = [script, ...args];
|
|
39
|
+
const forwarded = [script, ...providerRegistryArgs(target, args)];
|
|
70
40
|
const result = spawnSync("python3", forwarded, { stdio: "inherit" });
|
|
71
41
|
if (typeof result.status === "number") process.exit(result.status);
|
|
72
42
|
process.exit(1);
|
|
73
43
|
}
|
|
74
44
|
|
|
75
45
|
// Legacy provider_profiles.py commands
|
|
76
|
-
const script =
|
|
46
|
+
const script = resolvePythonScript(target, "provider_profiles.py");
|
|
77
47
|
if (!script) {
|
|
78
48
|
log("provider profiles unavailable in this environment");
|
|
49
|
+
console.log(` ${D}Expected bundled lib/python/provider_profiles.py or scripts/provider_profiles.py in repo checkout${R}`);
|
|
79
50
|
process.exit(1);
|
|
80
51
|
}
|
|
81
|
-
const forwarded = [script, ...args];
|
|
52
|
+
const forwarded = [script, ...providerProfileArgs(target, args)];
|
|
82
53
|
const result = spawnSync("python3", forwarded, { stdio: "inherit" });
|
|
83
54
|
if (typeof result.status === "number") process.exit(result.status);
|
|
84
55
|
process.exit(1);
|
package/lib/commands/quota.js
CHANGED
|
@@ -22,7 +22,7 @@ function _pad(s, n) { s = String(s); return s + " ".repeat(Math.max(0, n - s.len
|
|
|
22
22
|
function _loadEnvelope(target, opts = {}) {
|
|
23
23
|
const cachePath = path.join(target, "ai", "manifest", "agent-quotas.json");
|
|
24
24
|
const force = !!opts.refresh;
|
|
25
|
-
const script =
|
|
25
|
+
const script = shared.resolvePythonScript(target, "agent_quotas.py");
|
|
26
26
|
if (script) {
|
|
27
27
|
const args = [script, "--target", target, "--json"];
|
|
28
28
|
if (force) args.push("--refresh");
|
package/lib/commands/receipt.js
CHANGED
|
@@ -18,7 +18,7 @@ function _findArg(args, flag) {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
function cmdReceipt(target, args) {
|
|
21
|
-
const script =
|
|
21
|
+
const script = shared.resolvePythonScript(target, "receipt_png.py");
|
|
22
22
|
if (!script) {
|
|
23
23
|
log("receipt generator unavailable — missing scripts/receipt_png.py");
|
|
24
24
|
console.log(` ${D}Run from a 0dai-initialized project root.${R}`);
|
package/lib/commands/run.js
CHANGED
|
@@ -9,12 +9,20 @@ const {
|
|
|
9
9
|
} = require("../utils/run_cost");
|
|
10
10
|
const { recordActivationFirstTask } = require("../utils/activation_telemetry");
|
|
11
11
|
|
|
12
|
+
function printRunHelp() {
|
|
13
|
+
console.log(`Usage: 0dai run <goal> [--dry-run] [--dry-cost] [--max-cost N] [--agent claude|codex|gemini] [--provider deepseek|gemini-direct|codex|claude-opus]`);
|
|
14
|
+
console.log(` Example: 0dai run "add dark mode to settings page"`);
|
|
15
|
+
console.log(` Example: 0dai run "fix login bug" --dry-cost`);
|
|
16
|
+
console.log(` Example: 0dai run "refactor auth" --max-cost 0.50`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isRunHelpRequest(goal, args = []) {
|
|
20
|
+
return goal === "--help" || goal === "-h" || args.includes("--help") || args.includes("-h");
|
|
21
|
+
}
|
|
22
|
+
|
|
12
23
|
async function cmdRun(goal, target, args = []) {
|
|
13
|
-
if (!goal) {
|
|
14
|
-
|
|
15
|
-
console.log(` Example: 0dai run "add dark mode to settings page"`);
|
|
16
|
-
console.log(` Example: 0dai run "fix login bug" --dry-cost`);
|
|
17
|
-
console.log(` Example: 0dai run "refactor auth" --max-cost 0.50`);
|
|
24
|
+
if (!goal || isRunHelpRequest(goal, args)) {
|
|
25
|
+
printRunHelp();
|
|
18
26
|
return;
|
|
19
27
|
}
|
|
20
28
|
|
|
@@ -174,4 +182,4 @@ function providerAgent(provider) {
|
|
|
174
182
|
return null;
|
|
175
183
|
}
|
|
176
184
|
|
|
177
|
-
module.exports = { cmdRun, printCostPreview };
|
|
185
|
+
module.exports = { cmdRun, printCostPreview, printRunHelp };
|