@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
|
@@ -1,12 +1,43 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
const path = require("path");
|
|
2
4
|
const shared = require("../shared");
|
|
3
5
|
const { log, D, R, spawnSync, findRepoScript } = shared;
|
|
4
6
|
|
|
7
|
+
// npm-installed users have no repo checkout. Fall back to the self-contained
|
|
8
|
+
// python experience closure bundled into the package at lib/python/ by
|
|
9
|
+
// scripts/build-python-bundle.js (#4360).
|
|
10
|
+
function resolveBundledScript(name) {
|
|
11
|
+
const bundled = path.join(__dirname, "..", "python", name);
|
|
12
|
+
return fs.existsSync(bundled) ? bundled : null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function resolvePythonScript(target, name) {
|
|
16
|
+
return findRepoScript(target, name) || resolveBundledScript(name);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function printExperienceHelp() {
|
|
20
|
+
console.log("Usage: 0dai experience [list|stats|record-json|sync|warnings|dismiss]");
|
|
21
|
+
console.log("");
|
|
22
|
+
console.log("Commands:");
|
|
23
|
+
console.log(" list Show recent local experience events");
|
|
24
|
+
console.log(" stats Summarize local experience events");
|
|
25
|
+
console.log(" record-json Record one JSON payload through the repo experience helper");
|
|
26
|
+
console.log(" sync Sync eligible events when the repo helper permits it");
|
|
27
|
+
console.log(" warnings Show anti-pattern warnings");
|
|
28
|
+
console.log(" dismiss Dismiss an anti-pattern warning");
|
|
29
|
+
}
|
|
30
|
+
|
|
5
31
|
function cmdExperience(target, sub, args) {
|
|
6
|
-
|
|
32
|
+
if (sub === "--help" || sub === "-h" || args.includes("--help") || args.includes("-h")) {
|
|
33
|
+
printExperienceHelp();
|
|
34
|
+
process.exit(0);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const experienceScript = resolvePythonScript(target, "experience_pipeline.py");
|
|
7
38
|
if (!experienceScript) {
|
|
8
39
|
log("experience pipeline unavailable in this environment");
|
|
9
|
-
console.log(` ${D}
|
|
40
|
+
console.log(` ${D}Needs scripts/experience_pipeline.py (repo) or the bundled lib/python copy; requires python3${R}`);
|
|
10
41
|
process.exit(1);
|
|
11
42
|
}
|
|
12
43
|
|
|
@@ -34,8 +65,12 @@ function cmdExperience(target, sub, args) {
|
|
|
34
65
|
if (args.includes("--json")) forwarded.push("--json");
|
|
35
66
|
const limitIdx = args.indexOf("--limit");
|
|
36
67
|
if (limitIdx >= 0 && args[limitIdx + 1]) forwarded.push("--limit", args[limitIdx + 1]);
|
|
68
|
+
} else if (command === "record-json") {
|
|
69
|
+
if (args.includes("--json")) forwarded.push("--json");
|
|
70
|
+
const payloadIdx = args.indexOf("--payload");
|
|
71
|
+
if (payloadIdx >= 0 && args[payloadIdx + 1]) forwarded.push("--payload", args[payloadIdx + 1]);
|
|
37
72
|
} else if (command === "warnings") {
|
|
38
|
-
const detectorScript =
|
|
73
|
+
const detectorScript = resolvePythonScript(target, "anti_pattern_detector.py");
|
|
39
74
|
if (!detectorScript) { log("anti-pattern detector unavailable"); process.exit(1); }
|
|
40
75
|
const fwd = [detectorScript, "warnings", "--target", target];
|
|
41
76
|
if (args.includes("--json")) fwd.push("--json");
|
|
@@ -47,7 +82,7 @@ function cmdExperience(target, sub, args) {
|
|
|
47
82
|
if (typeof wr.status === "number") process.exit(wr.status);
|
|
48
83
|
process.exit(1);
|
|
49
84
|
} else if (command === "dismiss") {
|
|
50
|
-
const detectorScript =
|
|
85
|
+
const detectorScript = resolvePythonScript(target, "anti_pattern_detector.py");
|
|
51
86
|
if (!detectorScript) { log("anti-pattern detector unavailable"); process.exit(1); }
|
|
52
87
|
const patternId = args.find(a => a && !a.startsWith("-")) || "";
|
|
53
88
|
if (!patternId) { console.log("Usage: 0dai experience dismiss <pattern_id>"); process.exit(1); }
|
|
@@ -57,7 +92,7 @@ function cmdExperience(target, sub, args) {
|
|
|
57
92
|
if (typeof dr.status === "number") process.exit(dr.status);
|
|
58
93
|
process.exit(1);
|
|
59
94
|
} else {
|
|
60
|
-
|
|
95
|
+
printExperienceHelp();
|
|
61
96
|
process.exit(1);
|
|
62
97
|
}
|
|
63
98
|
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) 2026 0dai.dev
|
|
3
|
+
//
|
|
4
|
+
// F14 G4 Phase 2 — `0dai export --all` CLI command.
|
|
5
|
+
//
|
|
6
|
+
// Bundles user-owned tenant data into a tarball per the contract
|
|
7
|
+
// at docs/governance/data-export-contract.md (F14 G4 Phase 1 /
|
|
8
|
+
// PR #3569). Phase 2 ships personas + path-protect.yaml as real
|
|
9
|
+
// data, plus usage-ledger.jsonl when present; the other 6 surfaces emit placeholder JSON
|
|
10
|
+
// ({"_status": "not-implemented", "since": "F14 G4 Phase 2"}).
|
|
11
|
+
//
|
|
12
|
+
// Cosign signing is deferred to Phase 3 (stubs .sig + .crt as
|
|
13
|
+
// empty files alongside the tarball so the layout contract holds).
|
|
14
|
+
//
|
|
15
|
+
// Usage:
|
|
16
|
+
// 0dai export --all [--output PATH] [--target DIR]
|
|
17
|
+
|
|
18
|
+
"use strict";
|
|
19
|
+
|
|
20
|
+
const shared = require("../shared");
|
|
21
|
+
const { T, R, D, log } = shared;
|
|
22
|
+
const { buildExportTarball } = require("../utils/export-bundler");
|
|
23
|
+
|
|
24
|
+
function parseExportArgs(args) {
|
|
25
|
+
const out = { all: false, output: null, target: process.cwd() };
|
|
26
|
+
for (let i = 0; i < args.length; i++) {
|
|
27
|
+
const a = args[i];
|
|
28
|
+
if (a === "--all") out.all = true;
|
|
29
|
+
else if (a === "--output" && args[i + 1]) { out.output = args[i + 1]; i++; }
|
|
30
|
+
else if (a === "--target" && args[i + 1]) { out.target = args[i + 1]; i++; }
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function defaultOutputName() {
|
|
36
|
+
const now = new Date().toISOString().replace(/[:.]/g, "-").replace(/Z$/, "Z");
|
|
37
|
+
return `0dai-export-${now}.tar.gz`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function cmdExport(target, args) {
|
|
41
|
+
const opts = parseExportArgs(args);
|
|
42
|
+
if (!opts.all) {
|
|
43
|
+
console.error(`${T}[0dai-export]${R} --all is required (partial export is a Phase 2 follow-up)`);
|
|
44
|
+
process.exit(2);
|
|
45
|
+
}
|
|
46
|
+
const sourceRoot = opts.target || target || process.cwd();
|
|
47
|
+
const outputPath = opts.output || defaultOutputName();
|
|
48
|
+
|
|
49
|
+
log(`tenant data source: ${sourceRoot}`);
|
|
50
|
+
log(`output: ${outputPath}`);
|
|
51
|
+
|
|
52
|
+
let result;
|
|
53
|
+
try {
|
|
54
|
+
result = await buildExportTarball({ sourceRoot, outputPath });
|
|
55
|
+
} catch (err) {
|
|
56
|
+
console.error(`${T}[0dai-export]${R} bundle failed: ${err.message}`);
|
|
57
|
+
process.exit(3);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
log(`personas: ${result.counts.personas} bundled`);
|
|
61
|
+
log(`path-protect: ${result.counts.pathProtect} file(s)`);
|
|
62
|
+
log(`usage-ledger: ${result.counts.usageLedger} file(s)`);
|
|
63
|
+
log(`placeholders: ${result.counts.placeholders} surfaces (Phase 2 not-implemented)`);
|
|
64
|
+
log(`tarball: ${result.tarballPath}`);
|
|
65
|
+
log(`sha256: ${result.sha256}`);
|
|
66
|
+
if (result.signed) {
|
|
67
|
+
log(`signature: cosign keyless signed (${result.sigPath})`);
|
|
68
|
+
} else {
|
|
69
|
+
log(`${D}signature: ${result.signSkipReason} — stub .sig + .crt written${R}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = { cmdExport };
|
package/lib/commands/feedback.js
CHANGED
|
@@ -11,7 +11,9 @@ function writeFeedbackOutbox(target, report, result) {
|
|
|
11
11
|
queued_at: new Date().toISOString(),
|
|
12
12
|
project: path.basename(target),
|
|
13
13
|
report,
|
|
14
|
+
error_type: "feedback_endpoint_error",
|
|
14
15
|
error: result && result.error ? result.error : "feedback endpoint did not acknowledge receipt",
|
|
16
|
+
retry_command: "0dai feedback retry",
|
|
15
17
|
response: result || null,
|
|
16
18
|
}, null, 2) + "\n", { mode: 0o600 });
|
|
17
19
|
return outboxPath;
|
|
@@ -33,36 +35,147 @@ function feedbackAccepted(result) {
|
|
|
33
35
|
return Boolean(result && (result.received || result.ok));
|
|
34
36
|
}
|
|
35
37
|
|
|
36
|
-
|
|
38
|
+
function hasHelp(args = []) {
|
|
39
|
+
return args.includes("--help") || args.includes("-h");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function printFeedbackHelp() {
|
|
43
|
+
console.log("Usage: 0dai feedback [push|submit|retry|log|list] [--target PATH] [options]");
|
|
44
|
+
console.log("");
|
|
45
|
+
console.log("Commands:");
|
|
46
|
+
console.log(" push Send local feedback reports and operational log entries");
|
|
47
|
+
console.log(" push Send accepted feedback reports from ai/feedback/");
|
|
48
|
+
console.log(" retry Retry queued feedback from a failed push");
|
|
49
|
+
console.log(" log Append one feedback item to ai/feedback/operational.jsonl");
|
|
50
|
+
console.log(" list List local feedback report files");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function printFeedbackPushHelp() {
|
|
54
|
+
console.log("Usage: 0dai feedback push --target PATH");
|
|
55
|
+
console.log("");
|
|
56
|
+
console.log("Sends accepted feedback reports from ai/feedback/ and operational.jsonl entries.");
|
|
57
|
+
console.log("Accepted reports: ai/feedback/*-report.json and ai/feedback/YYYYMMDD*.json.");
|
|
58
|
+
console.log("Accepted log: ai/feedback/operational.jsonl.");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function printFeedbackSubmitHelp() {
|
|
62
|
+
console.log("Usage: 0dai feedback push --target PATH");
|
|
63
|
+
console.log("");
|
|
64
|
+
console.log("Sends one accepted feedback report file. Use feedback push to send every accepted local report.");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function printFeedbackRetryHelp() {
|
|
68
|
+
console.log("Usage: 0dai feedback retry [--json]");
|
|
69
|
+
console.log("");
|
|
70
|
+
console.log("Retries queued feedback submissions from ~/.0dai/feedback-outbox/.");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function printFeedbackLogHelp() {
|
|
74
|
+
console.log("Usage: 0dai feedback log --type bug|suggestion|friction|positive --detail '...'");
|
|
75
|
+
console.log("");
|
|
76
|
+
console.log("Records one local feedback entry without sending it.");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function printFeedbackListHelp() {
|
|
80
|
+
console.log("Usage: 0dai feedback list");
|
|
81
|
+
console.log("");
|
|
82
|
+
console.log("Lists local *-report.json feedback files.");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function resolveFeedbackSubmitFile(target, fileArg) {
|
|
86
|
+
if (!fileArg) return { error: "missing --file" };
|
|
87
|
+
const fbDir = path.resolve(target, "ai", "feedback");
|
|
88
|
+
const fullPath = path.resolve(target, fileArg);
|
|
89
|
+
if (fullPath !== fbDir && !fullPath.startsWith(`${fbDir}${path.sep}`)) {
|
|
90
|
+
return { error: "file must be under ai/feedback/" };
|
|
91
|
+
}
|
|
92
|
+
if (!fs.existsSync(fullPath)) return { error: `file not found: ${fileArg}` };
|
|
93
|
+
return { fullPath };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function cmdFeedbackPush(target, options = {}) {
|
|
37
97
|
const fbDir = path.join(target, "ai", "feedback");
|
|
98
|
+
const onlyFile = options.onlyFile ? path.resolve(options.onlyFile) : "";
|
|
38
99
|
const items = [];
|
|
100
|
+
const ignored = [];
|
|
101
|
+
let feedbackDirExists = false;
|
|
102
|
+
let jsonlStatus = "missing";
|
|
103
|
+
let includedOperationalLog = false;
|
|
104
|
+
const acceptedFormats = [
|
|
105
|
+
"ai/feedback/*-report.json",
|
|
106
|
+
"ai/feedback/YYYYMMDD*.json",
|
|
107
|
+
"ai/feedback/operational.jsonl",
|
|
108
|
+
];
|
|
39
109
|
|
|
40
110
|
// Collect from report JSON files
|
|
41
111
|
try {
|
|
112
|
+
feedbackDirExists = fs.existsSync(fbDir) && fs.statSync(fbDir).isDirectory();
|
|
42
113
|
for (const f of fs.readdirSync(fbDir)) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
114
|
+
const fullPath = path.join(fbDir, f);
|
|
115
|
+
if (onlyFile && path.resolve(fullPath) !== onlyFile) continue;
|
|
116
|
+
let stat = null;
|
|
117
|
+
try { stat = fs.statSync(fullPath); } catch {}
|
|
118
|
+
if (!stat || !stat.isFile()) {
|
|
119
|
+
ignored.push({ file: f, reason: "not a regular file" });
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (f === "operational.jsonl") continue;
|
|
123
|
+
if (!(f.endsWith("-report.json") || (f.endsWith(".json") && f.match(/^\d{8}/)))) {
|
|
124
|
+
ignored.push({ file: f, reason: "unsupported report name" });
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
const d = JSON.parse(fs.readFileSync(fullPath, "utf8"));
|
|
129
|
+
if (d.project || d.verdict) items.push({ type: "report", data: d, file: f });
|
|
130
|
+
else ignored.push({ file: f, reason: "missing project or verdict" });
|
|
131
|
+
} catch (err) {
|
|
132
|
+
ignored.push({ file: f, reason: `invalid JSON: ${err.message || err}` });
|
|
48
133
|
}
|
|
49
134
|
}
|
|
50
|
-
} catch {
|
|
135
|
+
} catch (err) {
|
|
136
|
+
if (!feedbackDirExists) ignored.push({ file: "ai/feedback/", reason: "directory does not exist" });
|
|
137
|
+
else ignored.push({ file: "ai/feedback/", reason: `could not scan directory: ${err.message || err}` });
|
|
138
|
+
}
|
|
51
139
|
|
|
52
140
|
// Collect from operational.jsonl (feedback log entries)
|
|
53
141
|
const jsonlPath = path.join(fbDir, "operational.jsonl");
|
|
54
142
|
try {
|
|
55
|
-
if (fs.existsSync(jsonlPath)) {
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
143
|
+
if (fs.existsSync(jsonlPath) && (!onlyFile || path.resolve(jsonlPath) === onlyFile)) {
|
|
144
|
+
const raw = fs.readFileSync(jsonlPath, "utf8");
|
|
145
|
+
const lines = raw.trim().split("\n").filter(Boolean);
|
|
146
|
+
jsonlStatus = lines.length ? `${lines.length} line(s)` : "empty";
|
|
147
|
+
if (!lines.length) ignored.push({ file: "operational.jsonl", reason: "empty log" });
|
|
148
|
+
for (const [idx, line] of lines.entries()) {
|
|
149
|
+
try {
|
|
150
|
+
items.push({ type: "log", data: JSON.parse(line) });
|
|
151
|
+
includedOperationalLog = true;
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
ignored.push({ file: `operational.jsonl:${idx + 1}`, reason: `invalid JSON: ${err.message || err}` });
|
|
155
|
+
}
|
|
59
156
|
}
|
|
60
157
|
}
|
|
61
|
-
} catch {
|
|
158
|
+
} catch (err) {
|
|
159
|
+
jsonlStatus = `unreadable: ${err.message || err}`;
|
|
160
|
+
ignored.push({ file: "operational.jsonl", reason: jsonlStatus });
|
|
161
|
+
}
|
|
62
162
|
|
|
63
163
|
if (!items.length) {
|
|
64
164
|
log("no feedback found");
|
|
165
|
+
console.log(` ${D}scanned: ${fbDir}${R}`);
|
|
166
|
+
console.log(` ${D}accepted inputs: ${acceptedFormats.join(", ")}${R}`);
|
|
167
|
+
console.log(` ${D}operational log: ${jsonlStatus}${R}`);
|
|
168
|
+
const shownIgnored = ignored.slice(0, 8);
|
|
169
|
+
for (const item of shownIgnored) {
|
|
170
|
+
console.log(` ${D}ignored ${item.file}: ${item.reason}${R}`);
|
|
171
|
+
}
|
|
172
|
+
if (ignored.length > shownIgnored.length) {
|
|
173
|
+
console.log(` ${D}ignored ${ignored.length - shownIgnored.length} more item(s)${R}`);
|
|
174
|
+
}
|
|
65
175
|
console.log(` ${D}Log feedback first: 0dai feedback log --type suggestion --detail '...'${R}`);
|
|
176
|
+
console.log(` ${D}Report files must include project or verdict before push.${R}`);
|
|
177
|
+
console.log(` ${D}Use 0dai feedback push --target . to send accepted reports from ai/feedback/.${R}`);
|
|
178
|
+
if (onlyFile) process.exitCode = 1;
|
|
66
179
|
return;
|
|
67
180
|
}
|
|
68
181
|
|
|
@@ -81,14 +194,17 @@ async function cmdFeedbackPush(target) {
|
|
|
81
194
|
if (result.warning) console.log(` ${D}${result.warning}${R}`);
|
|
82
195
|
if (result.issue_error) console.log(` ${D}issue warning: ${result.issue_error}${R}`);
|
|
83
196
|
// Archive pushed entries
|
|
84
|
-
if (fs.existsSync(jsonlPath)) {
|
|
197
|
+
if (includedOperationalLog && fs.existsSync(jsonlPath)) {
|
|
85
198
|
const archivePath = path.join(fbDir, `pushed-${Date.now()}.jsonl`);
|
|
86
199
|
fs.renameSync(jsonlPath, archivePath);
|
|
87
200
|
}
|
|
88
201
|
} else {
|
|
89
202
|
const outboxPath = writeFeedbackOutbox(target, report, result);
|
|
90
203
|
log(`error: ${(result && result.error) || "unknown"}`);
|
|
204
|
+
console.log(` ${D}type: feedback_endpoint_error${R}`);
|
|
205
|
+
console.log(` ${D}endpoint: /v1/feedback${R}`);
|
|
91
206
|
console.log(` ${D}queued for retry: ${outboxPath}${R}`);
|
|
207
|
+
console.log(` ${D}retry: 0dai feedback retry${R}`);
|
|
92
208
|
console.log(` ${D}source feedback was left in place${R}`);
|
|
93
209
|
process.exitCode = 1;
|
|
94
210
|
}
|
|
@@ -144,9 +260,35 @@ async function cmdFeedbackRetry(args = []) {
|
|
|
144
260
|
async function cmdFeedback(target, sub, args) {
|
|
145
261
|
const fbDir = path.join(target, "ai", "feedback");
|
|
146
262
|
|
|
263
|
+
if (!sub || sub === "help" || sub === "--help" || sub === "-h") {
|
|
264
|
+
printFeedbackHelp();
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (hasHelp(args)) {
|
|
269
|
+
if (sub === "push") printFeedbackPushHelp();
|
|
270
|
+
else if (sub === "submit") printFeedbackSubmitHelp();
|
|
271
|
+
else if (sub === "retry") printFeedbackRetryHelp();
|
|
272
|
+
else if (sub === "log") printFeedbackLogHelp();
|
|
273
|
+
else if (sub === "list") printFeedbackListHelp();
|
|
274
|
+
else printFeedbackHelp();
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
147
278
|
if (sub === "push") {
|
|
148
279
|
return cmdFeedbackPush(target);
|
|
149
280
|
}
|
|
281
|
+
if (sub === "submit") {
|
|
282
|
+
const fileArg = args.find((_, i) => args[i - 1] === "--file") || "";
|
|
283
|
+
const resolved = resolveFeedbackSubmitFile(target, fileArg);
|
|
284
|
+
if (resolved.error) {
|
|
285
|
+
log(`feedback submit error: ${resolved.error}`);
|
|
286
|
+
printFeedbackSubmitHelp();
|
|
287
|
+
process.exitCode = 1;
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
return cmdFeedbackPush(target, { onlyFile: resolved.fullPath });
|
|
291
|
+
}
|
|
150
292
|
if (sub === "retry") {
|
|
151
293
|
return cmdFeedbackRetry(args);
|
|
152
294
|
}
|
|
@@ -173,7 +315,7 @@ async function cmdFeedback(target, sub, args) {
|
|
|
173
315
|
} catch { log("no feedback directory"); }
|
|
174
316
|
return;
|
|
175
317
|
}
|
|
176
|
-
|
|
318
|
+
printFeedbackHelp();
|
|
177
319
|
}
|
|
178
320
|
|
|
179
|
-
module.exports = { cmdFeedbackPush, cmdFeedbackRetry, cmdFeedback, writeFeedbackOutbox, listFeedbackOutbox, feedbackAccepted };
|
|
321
|
+
module.exports = { cmdFeedbackPush, cmdFeedbackRetry, cmdFeedback, writeFeedbackOutbox, listFeedbackOutbox, feedbackAccepted, resolveFeedbackSubmitFile };
|
package/lib/commands/gh.js
CHANGED
|
@@ -330,12 +330,38 @@ function hasGithubRemote(target) {
|
|
|
330
330
|
return /github\.com[:/]/.test(String(result.stdout || ""));
|
|
331
331
|
}
|
|
332
332
|
|
|
333
|
+
// An existing hook setup that pointing core.hooksPath at .githooks would
|
|
334
|
+
// silently bypass (Husky, or non-sample committed .git/hooks).
|
|
335
|
+
function existingHookSetup(target) {
|
|
336
|
+
try {
|
|
337
|
+
if (fs.existsSync(path.join(target, ".husky"))) return ".husky";
|
|
338
|
+
const hooksDir = path.join(target, ".git", "hooks");
|
|
339
|
+
if (fs.existsSync(hooksDir)) {
|
|
340
|
+
const real = fs.readdirSync(hooksDir).filter((f) => !f.endsWith(".sample"));
|
|
341
|
+
if (real.length) return ".git/hooks";
|
|
342
|
+
}
|
|
343
|
+
} catch {
|
|
344
|
+
/* ignore */
|
|
345
|
+
}
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
|
|
333
349
|
function ensureHooksPath(target) {
|
|
334
350
|
if (!isGitRepo(target)) return false;
|
|
351
|
+
const current = getHooksPath(target);
|
|
352
|
+
if (current === ".githooks") return true; // already ours — nothing to do
|
|
353
|
+
// Warn (don't silently clobber) when the user already has hooks we'd bypass.
|
|
354
|
+
const conflict = current && current !== ".githooks" ? `core.hooksPath=${current}` : existingHookSetup(target);
|
|
335
355
|
const result = spawnSync("git", ["-C", target, "config", "core.hooksPath", ".githooks"], {
|
|
336
356
|
encoding: "utf8",
|
|
337
357
|
stdio: ["ignore", "pipe", "pipe"],
|
|
338
358
|
});
|
|
359
|
+
if (conflict && result.status === 0) {
|
|
360
|
+
log(`warning: redirected git hooks to .githooks — your existing hooks (${conflict}) are now bypassed`);
|
|
361
|
+
console.log(
|
|
362
|
+
` ${D}Keep yours: git config core.hooksPath ${current || "<your-path>"} (or chain them from .githooks). Skip 0dai's policy: ODAI_GIT_POLICY_SKIP=1${R}`,
|
|
363
|
+
);
|
|
364
|
+
}
|
|
339
365
|
return result.status === 0;
|
|
340
366
|
}
|
|
341
367
|
|
package/lib/commands/graph.js
CHANGED
|
@@ -23,8 +23,12 @@ function graphPayloadForPush(localGraph, auth) {
|
|
|
23
23
|
};
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
function missingGraphHint() {
|
|
27
|
+
return "No local graph found. Create ai/manifest/project_graph.json, or from a repo checkout run: python3 scripts/generate_project_graph.py --target .";
|
|
28
|
+
}
|
|
29
|
+
|
|
26
30
|
function readStructuralMemorySummary(target) {
|
|
27
|
-
const script = shared.
|
|
31
|
+
const script = shared.resolvePythonScript(target, "structural_memory.py");
|
|
28
32
|
if (!script) return null;
|
|
29
33
|
const result = shared.spawnSync("python3", [script, "--target", target, "--json"], {
|
|
30
34
|
encoding: "utf8",
|
|
@@ -72,7 +76,7 @@ async function cmdGraph(target, sub, args) {
|
|
|
72
76
|
}
|
|
73
77
|
|
|
74
78
|
if (!fs.existsSync(graphFile)) {
|
|
75
|
-
log(
|
|
79
|
+
log(missingGraphHint());
|
|
76
80
|
return;
|
|
77
81
|
}
|
|
78
82
|
|
|
@@ -264,10 +268,10 @@ async function cmdGraph(target, sub, args) {
|
|
|
264
268
|
|
|
265
269
|
if (sub === "context") {
|
|
266
270
|
if (!fs.existsSync(graphFile)) {
|
|
267
|
-
log(
|
|
271
|
+
log(missingGraphHint());
|
|
268
272
|
return;
|
|
269
273
|
}
|
|
270
|
-
const slicerScript = shared.
|
|
274
|
+
const slicerScript = shared.resolvePythonScript(target, "graph_slicer_cli.py");
|
|
271
275
|
if (!slicerScript) {
|
|
272
276
|
log("graph slicer unavailable in this environment");
|
|
273
277
|
console.log(` ${D}Expected scripts/graph_slicer_cli.py in repo checkout${R}`);
|
|
@@ -333,6 +337,7 @@ module.exports = {
|
|
|
333
337
|
graphAuthToken,
|
|
334
338
|
graphPayloadForPush,
|
|
335
339
|
isPaidGraphPlan,
|
|
340
|
+
missingGraphHint,
|
|
336
341
|
readStructuralMemorySummary,
|
|
337
342
|
formatStructuralMemorySummary,
|
|
338
343
|
};
|
package/lib/commands/heatmap.js
CHANGED
|
@@ -3,7 +3,7 @@ const shared = require("../shared");
|
|
|
3
3
|
const { log, spawnSync, findRepoScript } = shared;
|
|
4
4
|
|
|
5
5
|
function cmdHeatmap(target, args) {
|
|
6
|
-
const script =
|
|
6
|
+
const script = shared.resolvePythonScript(target, "heatmap.py");
|
|
7
7
|
if (!script) {
|
|
8
8
|
log("heatmap script unavailable in this environment");
|
|
9
9
|
return;
|