@0dai-dev/cli 4.3.5 → 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 +214 -40
- 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 +55 -1
- package/lib/commands/compliance.js +1 -1
- package/lib/commands/detect.js +10 -4
- package/lib/commands/doctor.js +545 -26
- package/lib/commands/experience.js +40 -5
- package/lib/commands/export.js +73 -0
- 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 +222 -30
- package/lib/commands/mcp.js +129 -21
- 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 +18 -7
- package/lib/commands/runner.js +31 -1
- package/lib/commands/status.js +44 -11
- package/lib/commands/swarm.js +130 -12
- package/lib/commands/trust.js +286 -0
- 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 +46 -9
- 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 +97 -14
- package/lib/tui/index.mjs +35174 -0
- package/lib/utils/activation_telemetry.js +230 -11
- package/lib/utils/constants.js +7 -1
- package/lib/utils/export-bundler.js +285 -0
- package/lib/utils/identity.js +198 -1
- 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;
|
|
@@ -72,7 +83,7 @@ function _toolsFromTierManifest(target, plan) {
|
|
|
72
83
|
const section = exposure[plan] || exposure.free;
|
|
73
84
|
if (!section) continue;
|
|
74
85
|
const tools = Array.isArray(section.tools) ? section.tools.slice() : [];
|
|
75
|
-
return { source: candidate, plan, tools, count: tools.length };
|
|
86
|
+
return { source: candidate, plan, tools, count: tools.length, exposure };
|
|
76
87
|
}
|
|
77
88
|
return null;
|
|
78
89
|
}
|
|
@@ -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,18 +215,73 @@ function _cmdCatalog(target, args) {
|
|
|
203
215
|
console.log(`\n ${T}available stacks${R}: ${available.join(", ")}\n`);
|
|
204
216
|
}
|
|
205
217
|
|
|
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.
|
|
224
|
+
function _detectStaleMcpServers(mcpServers) {
|
|
225
|
+
const stale = [];
|
|
226
|
+
for (const [name, cfg] of Object.entries(mcpServers || {})) {
|
|
227
|
+
if (!cfg || typeof cfg !== "object") continue;
|
|
228
|
+
const url = typeof cfg.url === "string" ? cfg.url : "";
|
|
229
|
+
const transport = typeof cfg.transport === "string" ? cfg.transport : "";
|
|
230
|
+
if (/\/sse\/?(\?|$)/.test(url) || transport === "sse") {
|
|
231
|
+
stale.push({ name, reason: "deprecated SSE endpoint/transport (HTTP 404)" });
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return stale;
|
|
235
|
+
}
|
|
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
|
+
|
|
206
271
|
function _cmdDoctor(target, args) {
|
|
207
272
|
const wantJson = args.includes("--json");
|
|
208
273
|
const report = {
|
|
209
274
|
target,
|
|
275
|
+
local_server_bundled: false,
|
|
210
276
|
server_script: { path: "", ok: false },
|
|
211
|
-
mcp_config: { path: "", ok: false, servers: [] },
|
|
277
|
+
mcp_config: { path: "", ok: false, servers: [], stale: [] },
|
|
212
278
|
tier_manifest: { path: "", ok: false, free_count: 0, pro_count: 0 },
|
|
213
279
|
catalog: { path: "", ok: false, stacks: 0 },
|
|
214
280
|
};
|
|
215
281
|
|
|
216
282
|
const script = findRepoScript(target, "mcp_server.py");
|
|
217
283
|
if (script) {
|
|
284
|
+
report.local_server_bundled = true;
|
|
218
285
|
report.server_script.path = script;
|
|
219
286
|
report.server_script.ok = fs.existsSync(script);
|
|
220
287
|
}
|
|
@@ -226,12 +293,15 @@ function _cmdDoctor(target, args) {
|
|
|
226
293
|
if (data && data.mcpServers && typeof data.mcpServers === "object") {
|
|
227
294
|
report.mcp_config.ok = true;
|
|
228
295
|
report.mcp_config.servers = Object.keys(data.mcpServers);
|
|
296
|
+
report.mcp_config.stale = [
|
|
297
|
+
..._detectStaleMcpServers(data.mcpServers),
|
|
298
|
+
..._detectMissingMcpServerScripts(data.mcpServers, target),
|
|
299
|
+
];
|
|
229
300
|
}
|
|
230
301
|
}
|
|
231
302
|
|
|
232
303
|
const tierCandidates = [
|
|
233
|
-
|
|
234
|
-
path.join(target, "ai", "manifest", "mcp-tool-tiers.json"),
|
|
304
|
+
..._tierManifestCandidates(target),
|
|
235
305
|
];
|
|
236
306
|
for (const candidate of tierCandidates) {
|
|
237
307
|
if (!fs.existsSync(candidate)) continue;
|
|
@@ -256,6 +326,18 @@ function _cmdDoctor(target, args) {
|
|
|
256
326
|
}
|
|
257
327
|
}
|
|
258
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
|
+
|
|
259
341
|
if (wantJson) {
|
|
260
342
|
console.log(JSON.stringify(report));
|
|
261
343
|
return;
|
|
@@ -263,23 +345,47 @@ function _cmdDoctor(target, args) {
|
|
|
263
345
|
|
|
264
346
|
const ok = (b) => (b ? G + "ok" + R : W + "missing" + R);
|
|
265
347
|
console.log(`\n ${T}0dai mcp doctor${R} target=${target}\n`);
|
|
266
|
-
|
|
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
|
+
|
|
267
365
|
console.log(` .mcp.json ${ok(report.mcp_config.ok)} ${D}${report.mcp_config.path || "(not found)"}${R}`);
|
|
268
366
|
if (report.mcp_config.ok) {
|
|
269
367
|
console.log(` servers: ${report.mcp_config.servers.join(", ")}`);
|
|
270
368
|
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
369
|
+
if (report.mcp_config.stale.length) {
|
|
370
|
+
console.log(` ${W}stale${R} (remove from .mcp.json — superseded by the "claude_ai_0dai" OAuth /mcp entry):`);
|
|
371
|
+
for (const s of report.mcp_config.stale) {
|
|
372
|
+
const detail = s.path ? `${s.reason}: ${s.path}` : s.reason;
|
|
373
|
+
console.log(` ${W}!${R} ${s.name} ${D}— ${detail}${R}`);
|
|
374
|
+
}
|
|
274
375
|
}
|
|
275
|
-
|
|
276
|
-
if (report.
|
|
277
|
-
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
|
+
}
|
|
278
386
|
}
|
|
279
387
|
console.log("");
|
|
280
388
|
|
|
281
|
-
const allOk = report.server_script.ok && report.mcp_config.ok && (report.tier_manifest.ok || report.catalog.ok);
|
|
282
|
-
if (!allOk) process.exitCode = 1;
|
|
283
389
|
}
|
|
284
390
|
|
|
285
391
|
function _cmdCall(target, toolName, args) {
|
|
@@ -293,7 +399,9 @@ function _cmdCall(target, toolName, args) {
|
|
|
293
399
|
|
|
294
400
|
const script = findRepoScript(target, "mcp_server.py");
|
|
295
401
|
if (!script) {
|
|
296
|
-
|
|
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);
|
|
297
405
|
process.exitCode = 1;
|
|
298
406
|
return;
|
|
299
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
|
|
|
@@ -125,7 +133,10 @@ async function cmdRun(goal, target, args = []) {
|
|
|
125
133
|
|
|
126
134
|
if (created.length > 0) {
|
|
127
135
|
const identity = shared.buildProjectIdentity(target, shared.collectMetadata(target));
|
|
128
|
-
recordActivationFirstTask(target, identity.project_id
|
|
136
|
+
recordActivationFirstTask(target, identity.project_id, {
|
|
137
|
+
outcome: "task_queued",
|
|
138
|
+
task_count: created.length,
|
|
139
|
+
});
|
|
129
140
|
}
|
|
130
141
|
}
|
|
131
142
|
|
|
@@ -171,4 +182,4 @@ function providerAgent(provider) {
|
|
|
171
182
|
return null;
|
|
172
183
|
}
|
|
173
184
|
|
|
174
|
-
module.exports = { cmdRun, printCostPreview };
|
|
185
|
+
module.exports = { cmdRun, printCostPreview, printRunHelp };
|