@niroai/niro 0.17.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +136 -0
- package/README.md +123 -302
- package/bin/niro.js +12 -1
- package/docs/agent-antigravity.md +37 -0
- package/docs/agent-claude-code.md +48 -0
- package/docs/agent-claude-desktop.md +47 -0
- package/docs/agent-cline.md +42 -0
- package/docs/agent-codex.md +45 -0
- package/docs/agent-copilot-cli.md +42 -0
- package/docs/agent-cursor.md +44 -0
- package/docs/agent-factory.md +42 -0
- package/docs/agent-gemini-cli.md +42 -0
- package/docs/agent-goose.md +42 -0
- package/docs/agent-kiro.md +37 -0
- package/docs/agent-opencode.md +37 -0
- package/docs/agent-vscode.md +45 -0
- package/docs/agent-windsurf.md +38 -0
- package/docs/answer-shapes.md +69 -0
- package/docs/builds-and-updates.md +68 -0
- package/docs/cloud-README.md +372 -0
- package/docs/configuring-a-folder.md +51 -0
- package/docs/connect-ci.md +24 -0
- package/docs/connect-overview.md +20 -0
- package/docs/connect-review-bots.md +17 -0
- package/docs/connect-team-server.md +12 -0
- package/docs/connect-terminal.md +49 -0
- package/docs/connect-web-agents.md +9 -0
- package/docs/connect-your-ai-assistant.md +59 -175
- package/docs/coverage-audit-and-consent.md +51 -0
- package/docs/evaluate-what-niro-did.md +271 -0
- package/docs/faq-assistants-and-mcp.md +122 -0
- package/docs/faq-feedback-and-receipts.md +33 -0
- package/docs/faq-indexing.md +127 -0
- package/docs/faq-install-and-setup.md +103 -0
- package/docs/faq-licence-and-plans.md +86 -0
- package/docs/faq-login-and-account.md +69 -0
- package/docs/faq-offline.md +34 -0
- package/docs/faq-performance.md +33 -0
- package/docs/faq-privacy-and-security.md +113 -0
- package/docs/faq-uninstall.md +47 -0
- package/docs/faq-updates.md +59 -0
- package/docs/faq-worktrees-and-branches.md +59 -0
- package/docs/faq-wrong-or-missing-answers.md +275 -0
- package/docs/hosted-only.md +33 -0
- package/docs/how-the-graph-works.md +47 -0
- package/docs/index.md +157 -0
- package/docs/licence-and-plans.md +72 -0
- package/docs/lifecycle-debug.md +48 -0
- package/docs/lifecycle-onboarding.md +32 -0
- package/docs/lifecycle-overview.md +77 -0
- package/docs/lifecycle-plan-and-design.md +44 -0
- package/docs/lifecycle-review.md +30 -0
- package/docs/lifecycle-security-and-privacy.md +35 -0
- package/docs/lifecycle-test.md +28 -0
- package/docs/lifecycle-write-code.md +35 -0
- package/docs/llms-full.txt +8015 -0
- package/docs/llms.txt +126 -0
- package/docs/playbook-add-a-feature.md +108 -0
- package/docs/playbook-change-safely.md +93 -0
- package/docs/playbook-data-and-events.md +107 -0
- package/docs/playbook-debug-after-a-change.md +96 -0
- package/docs/playbook-debug-config.md +95 -0
- package/docs/playbook-debug-exception.md +124 -0
- package/docs/playbook-debug-failing-service-call.md +119 -0
- package/docs/playbook-debug-slow-path.md +80 -0
- package/docs/playbook-debug-wrong-value.md +137 -0
- package/docs/playbook-onboard.md +95 -0
- package/docs/playbook-review-a-change.md +72 -0
- package/docs/playbook-test-a-change.md +73 -0
- package/docs/reference-cli.md +113 -0
- package/docs/reference-config-files.md +42 -0
- package/docs/reference-engine-commands.md +56 -0
- package/docs/reference-environment-variables.md +89 -0
- package/docs/reference-exit-codes.md +67 -0
- package/docs/reference-limits.md +75 -0
- package/docs/reference-mcp-tools.md +326 -0
- package/docs/reference-messages.md +180 -0
- package/docs/secrets-and-masking.md +48 -0
- package/docs/supported-c.md +130 -0
- package/docs/supported-cpp.md +157 -0
- package/docs/supported-csharp.md +211 -0
- package/docs/supported-go.md +258 -0
- package/docs/supported-java.md +226 -0
- package/docs/supported-javascript.md +223 -0
- package/docs/supported-kotlin.md +124 -0
- package/docs/supported-overview.md +89 -0
- package/docs/supported-php.md +157 -0
- package/docs/supported-python.md +224 -0
- package/docs/supported-ruby.md +162 -0
- package/docs/supported-rust.md +150 -0
- package/docs/supported-swift.md +133 -0
- package/docs/supported-typescript.md +220 -0
- package/docs/tokens-speed-and-accuracy.md +99 -0
- package/docs/troubleshooting.md +42 -0
- package/docs/uninstall.md +74 -0
- package/docs/what-leaves-your-machine.md +71 -0
- package/docs/where-things-are-stored.md +59 -0
- package/docs/worktrees-and-branches.md +55 -0
- package/package.json +5 -3
- package/src/commands/add-repo.js +5 -0
- package/src/commands/auditSend.js +67 -0
- package/src/commands/build.js +6 -0
- package/src/commands/docsSend.js +73 -0
- package/src/commands/engine.js +120 -0
- package/src/commands/engineRoutes.js +105 -0
- package/src/commands/guide.js +254 -0
- package/src/commands/init.js +6 -2
- package/src/commands/initLocal.js +96 -0
- package/src/commands/journalSend.js +47 -0
- package/src/commands/login.js +89 -12
- package/src/commands/mcp.js +22 -6
- package/src/commands/plan.js +99 -0
- package/src/commands/repoPush.js +33 -0
- package/src/commands/requestIntegration.js +38 -0
- package/src/commands/share.js +56 -0
- package/src/commands/start.js +451 -371
- package/src/commands/status.js +57 -0
- package/src/commands/takeover.js +6 -0
- package/src/commands/uninstall.js +180 -0
- package/src/commands/upgrade.js +38 -8
- package/src/commands/watch.js +420 -6
- package/src/index.js +174 -3
- package/src/lib/agentApi.js +32 -1
- package/src/lib/api.js +20 -6
- package/src/lib/auditUpload.js +293 -0
- package/src/lib/branchDrift.js +149 -0
- package/src/lib/choose.js +116 -0
- package/src/lib/cloudGuard.js +74 -0
- package/src/lib/credentials.js +2 -2
- package/src/lib/docsMarked.js +55 -0
- package/src/lib/docsState.js +213 -0
- package/src/lib/docsUpload.js +481 -0
- package/src/lib/engine.js +619 -0
- package/src/lib/folderSync.js +2 -1
- package/src/lib/graphCheck.js +40 -0
- package/src/lib/guide.js +293 -0
- package/src/lib/guideSearch.js +252 -0
- package/src/lib/integrationRequest.js +89 -0
- package/src/lib/journalUpload.js +147 -0
- package/src/lib/languages.js +129 -0
- package/src/lib/lease.js +375 -0
- package/src/lib/localBuild.js +285 -0
- package/src/lib/localEngine.js +210 -0
- package/src/lib/localOnboard.js +124 -0
- package/src/lib/localWorktrees.js +243 -0
- package/src/lib/machineCheck.js +58 -0
- package/src/lib/mcpClients.js +10 -1
- package/src/lib/migrationNotice.js +118 -0
- package/src/lib/modelFetch.js +153 -0
- package/src/lib/nestedOutput.js +23 -9
- package/src/lib/onboardingLog.js +34 -0
- package/src/lib/planUsage.js +165 -0
- package/src/lib/progressBar.js +46 -0
- package/src/lib/prompt.js +1 -1
- package/src/lib/redact.js +155 -0
- package/src/lib/release.js +33 -0
- package/src/lib/repoPush.js +495 -0
- package/src/lib/secret.js +1 -1
- package/src/lib/service.js +372 -63
- package/src/lib/shareCode.js +367 -0
- package/src/lib/shareExclusions.js +114 -0
- package/src/lib/shareFiles.js +129 -0
- package/src/lib/spinner.js +9 -5
- package/src/lib/watchDaemon.js +33 -10
- package/src/lib/watchState.js +12 -1
- package/src/lib/windowsPaths.js +67 -0
- package/src/mcp/lineCheck.js +382 -0
- package/src/mcp/lineName.js +92 -0
- package/src/mcp/localBackend.js +751 -0
- package/src/mcp/localMode.js +383 -0
- package/src/mcp/localSkill.js +189 -0
- package/src/mcp/server.js +610 -79
- package/src/mcp/setup.js +519 -131
- package/src/mcp/shellSearchHook.js +509 -0
- package/src/mcp/teardown.js +173 -91
- package/src/mcp/usingNiro.js +39 -0
- package/docs/working-on-branches.md +0 -162
package/src/index.js
CHANGED
|
@@ -4,11 +4,14 @@ const { Command } = require("commander");
|
|
|
4
4
|
const pkg = require("../package.json");
|
|
5
5
|
|
|
6
6
|
function main() {
|
|
7
|
+
// Windows: programs started through cmd.exe are not looked up in the current folder first.
|
|
8
|
+
require("./lib/windowsPaths").harden();
|
|
7
9
|
const program = new Command();
|
|
8
10
|
program
|
|
9
11
|
.name("niro")
|
|
10
12
|
.description("Niro CLI — onboard repos, build indexes, manage projects from the terminal.")
|
|
11
13
|
.version(pkg.version)
|
|
14
|
+
.addHelpText("after", `\nLanguages: ${require("./lib/languages").supportedList()}.`)
|
|
12
15
|
.option("--api-url <url>", "backend to log in to — `niro login` only (default: $NIRO_API_URL or https://aiorchestrator.niroai.dev)");
|
|
13
16
|
|
|
14
17
|
// --api-url is accepted by `niro login` and REFUSED everywhere else.
|
|
@@ -40,7 +43,7 @@ function main() {
|
|
|
40
43
|
|
|
41
44
|
program
|
|
42
45
|
.command("start [path]")
|
|
43
|
-
.description("First-time setup, end to end: sign in,
|
|
46
|
+
.description("First-time setup, end to end: check this machine, sign in, install the local engine, build this folder's graph, connect your AI assistants.")
|
|
44
47
|
.action((pathArg) => run(async () => {
|
|
45
48
|
const startCmd = require("./commands/start");
|
|
46
49
|
await startCmd.start(pathArg, {});
|
|
@@ -57,6 +60,15 @@ function main() {
|
|
|
57
60
|
await loginCmd.login({ ...opts, apiUrl: program.opts().apiUrl }); // login: the one command that may set it
|
|
58
61
|
}));
|
|
59
62
|
|
|
63
|
+
program
|
|
64
|
+
.command("uninstall")
|
|
65
|
+
.description("Remove Niro from this machine: assistant entries, the watch daemon, ~/.niro and ~/.niro-local.")
|
|
66
|
+
.option("-y, --yes", "do not ask for confirmation")
|
|
67
|
+
.action((opts) => run(async () => {
|
|
68
|
+
const uninstallCmd = require("./commands/uninstall");
|
|
69
|
+
await uninstallCmd.uninstall(opts);
|
|
70
|
+
}));
|
|
71
|
+
|
|
60
72
|
program
|
|
61
73
|
.command("logout")
|
|
62
74
|
.description("Remove locally stored credentials.")
|
|
@@ -65,6 +77,38 @@ function main() {
|
|
|
65
77
|
await logoutCmd.logout();
|
|
66
78
|
}));
|
|
67
79
|
|
|
80
|
+
program
|
|
81
|
+
.command("plan [action] [folder]")
|
|
82
|
+
.description("Show your plan, the folders this machine has indexed against its limit, and your options. `niro plan remove <folder>` stops indexing a folder.")
|
|
83
|
+
.option("--json", "emit machine-readable JSON (niro plan only)")
|
|
84
|
+
.action((action, folder, opts) => run(async () => {
|
|
85
|
+
const planCmd = require("./commands/plan");
|
|
86
|
+
if (action === undefined) return planCmd.plan({ json: !!opts.json });
|
|
87
|
+
if (action === "remove") return planCmd.remove(folder);
|
|
88
|
+
throw new Error(`Unknown plan action: ${action}. Use "niro plan" or "niro plan remove <folder>".`);
|
|
89
|
+
}));
|
|
90
|
+
|
|
91
|
+
program
|
|
92
|
+
.command("share [action] [folder]")
|
|
93
|
+
.description("Keep folders out of code sharing while Help improve Niro is on. `niro share` lists them, `niro share exclude <folder>` keeps one out, `niro share include <folder>` shares it again.")
|
|
94
|
+
.action((action, folder) => run(async () => {
|
|
95
|
+
const shareCmd = require("./commands/share");
|
|
96
|
+
if (action === undefined) return shareCmd.show();
|
|
97
|
+
if (action === "exclude") return shareCmd.exclude(folder);
|
|
98
|
+
if (action === "include") return shareCmd.include(folder);
|
|
99
|
+
throw new Error(`Unknown share action: ${action}. Use "niro share", "niro share exclude <folder>" or "niro share include <folder>".`);
|
|
100
|
+
}));
|
|
101
|
+
|
|
102
|
+
program
|
|
103
|
+
.command("request-integration <name>")
|
|
104
|
+
.description("Ask Niro's team to support a language, framework or tool Niro does not read yet. Asks before sending; the request is kept with your account.")
|
|
105
|
+
.option("--kind <kind>", "language, framework or tool", "language")
|
|
106
|
+
.option("--reason <text>", "what you would use it for")
|
|
107
|
+
.option("-y, --yes", "do not ask for confirmation")
|
|
108
|
+
.action((name, opts) => run(async () => {
|
|
109
|
+
await require("./commands/requestIntegration").requestIntegration(name, opts);
|
|
110
|
+
}));
|
|
111
|
+
|
|
68
112
|
program
|
|
69
113
|
.command("whoami")
|
|
70
114
|
.description("Print the account currently logged in.")
|
|
@@ -74,6 +118,61 @@ function main() {
|
|
|
74
118
|
await whoami.whoami(opts);
|
|
75
119
|
}));
|
|
76
120
|
|
|
121
|
+
program
|
|
122
|
+
.command("audit-send")
|
|
123
|
+
.description("Send the local engine's coverage-audit findings (what the graph missed) to Niro.")
|
|
124
|
+
.option("--limit <n>", "how many waiting items to send in this run (default 50)")
|
|
125
|
+
.action((opts) => run(async () => {
|
|
126
|
+
const auditSend = require("./commands/auditSend");
|
|
127
|
+
await auditSend.auditSend(opts);
|
|
128
|
+
}));
|
|
129
|
+
|
|
130
|
+
program
|
|
131
|
+
.command("engine [action]")
|
|
132
|
+
.description("Put the local engine on this machine (install), or say where it is (status).")
|
|
133
|
+
.option("--force", "download it again even if it is already here")
|
|
134
|
+
.action((action, opts) => run(async () => {
|
|
135
|
+
const engineCmd = require("./commands/engine");
|
|
136
|
+
if (action === undefined || action === "status") return engineCmd.status();
|
|
137
|
+
if (action === "install") return engineCmd.install(opts);
|
|
138
|
+
throw new Error(`Unknown engine action: ${action}. Use "install" or "status".`);
|
|
139
|
+
}));
|
|
140
|
+
|
|
141
|
+
program
|
|
142
|
+
.command("audit-push-repo [path]")
|
|
143
|
+
.description("Send this folder's indexed files to Niro, masked, so Niro can look at a problem with you. Works with code sharing on or off; asks first.")
|
|
144
|
+
.option("--force", "send it again even if this folder was sent before")
|
|
145
|
+
.option("-y, --yes", "skip the confirmation (required when there is no terminal)")
|
|
146
|
+
.action((pathArg, opts) => run(async () => {
|
|
147
|
+
const cmd = require("./commands/repoPush");
|
|
148
|
+
await cmd.pushRepo(pathArg, { ...opts });
|
|
149
|
+
}));
|
|
150
|
+
|
|
151
|
+
program
|
|
152
|
+
.command("journal-send")
|
|
153
|
+
.description("Send the local engine's record of which MCP tools were asked for to Niro.")
|
|
154
|
+
.option("--limit <n>", "how many sessions to send in this run (default 20)")
|
|
155
|
+
.action((opts) => run(async () => {
|
|
156
|
+
const journalSend = require("./commands/journalSend");
|
|
157
|
+
await journalSend.journalSend(opts);
|
|
158
|
+
}));
|
|
159
|
+
|
|
160
|
+
// LLM documentation is off in this release (src/lib/release.js `llmDocs`): the command is not listed, and typed
|
|
161
|
+
// anyway it says so in one line, the same sentence the engine gives for `niro-local docs`.
|
|
162
|
+
program
|
|
163
|
+
.command("docs-send", { hidden: !require("./lib/release").llmDocs })
|
|
164
|
+
.description("Send the local engine's documentation requests (folders built with --llm) through Niro, once.")
|
|
165
|
+
.option("--limit <n>", "how many request batches to send in this run (default 20)")
|
|
166
|
+
.action((opts) => run(async () => {
|
|
167
|
+
if (!require("./lib/release").llmDocs) {
|
|
168
|
+
console.error(require("./lib/docsUpload").NOT_IN_THIS_RELEASE);
|
|
169
|
+
process.exitCode = 2;
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const docsSend = require("./commands/docsSend");
|
|
173
|
+
await docsSend.docsSend(opts);
|
|
174
|
+
}));
|
|
175
|
+
|
|
77
176
|
program
|
|
78
177
|
.command("init [path]")
|
|
79
178
|
.description("Onboard a local repo end-to-end: create project, register repo, scan env vars, build index.")
|
|
@@ -337,8 +436,10 @@ function main() {
|
|
|
337
436
|
.description("Stop niro-watch and remove it from system startup.")
|
|
338
437
|
.action(() => run(async () => {
|
|
339
438
|
const service = require("./lib/service");
|
|
340
|
-
service.uninstall();
|
|
341
|
-
console.log("
|
|
439
|
+
const r = service.uninstall();
|
|
440
|
+
if (r === false) return console.log("PM2 is not installed, so there is no niro-watch to remove.");
|
|
441
|
+
console.log(`${require("./lib/color").green("✓")} niro-watch stopped and removed.`);
|
|
442
|
+
if (r.hook === "manual") console.log(` PM2 still starts on login (with nothing of Niro's). To remove that too: ${r.command}`);
|
|
342
443
|
}));
|
|
343
444
|
svc
|
|
344
445
|
.command("status")
|
|
@@ -377,6 +478,12 @@ function main() {
|
|
|
377
478
|
.option("--on", "Turn the end-of-task line back on.")
|
|
378
479
|
.option("--off", "Stop showing the end-of-task line.")
|
|
379
480
|
.action((opts) => { require("./commands/mcp").receipt(opts); });
|
|
481
|
+
// Listed so `niro mcp --help` names it. bin/niro.js answers it before this file is loaded,
|
|
482
|
+
// because it runs at the end of every turn in Claude Code (src/mcp/lineCheck.js).
|
|
483
|
+
mcp
|
|
484
|
+
.command("line-check")
|
|
485
|
+
.description("The Claude Code Stop hook: asks the assistant, once, for the end-of-task line it forgot.")
|
|
486
|
+
.action(() => {});
|
|
380
487
|
|
|
381
488
|
// Bare `niro` used to print an 18-command help listing, which is the worst possible
|
|
382
489
|
// first screen for someone who does not yet know what any of them do. With no stored
|
|
@@ -386,6 +493,69 @@ function main() {
|
|
|
386
493
|
process.argv.push("start");
|
|
387
494
|
}
|
|
388
495
|
|
|
496
|
+
// The cloud project commands, left out of a release without the cloud (src/lib/release.js).
|
|
497
|
+
// Registered above and filtered here, so switching the cloud back on is one constant.
|
|
498
|
+
if (!require("./lib/release").cloud) {
|
|
499
|
+
const CLOUD_COMMANDS = new Set([
|
|
500
|
+
"init", "add", "info", "edit", "remove", "new-temp-project", "discard-temp-project",
|
|
501
|
+
"doctor", "projects", "build", "delete",
|
|
502
|
+
// Not cloud project commands, but only the cloud serves them so far: `ab` switches the hosted
|
|
503
|
+
// MCP's A/B window, and `status <project_id>` names a cloud project. Back when local has them.
|
|
504
|
+
"ab", "status",
|
|
505
|
+
]);
|
|
506
|
+
program.commands = program.commands.filter((c) => !CLOUD_COMMANDS.has(c.name()));
|
|
507
|
+
// `niro watch` stays: it is what the niro-watch daemon runs (`service.install`), and without it
|
|
508
|
+
// the daemon crash-looped on "unknown command 'watch'" (2026-11-05). Hidden from --help, since
|
|
509
|
+
// nobody types it; with no cloud it watches only the local engine's folders (watch.js).
|
|
510
|
+
const watchCmd = program.commands.find((c) => c.name() === "watch");
|
|
511
|
+
if (watchCmd) watchCmd._hidden = true;
|
|
512
|
+
// `niro init` is the local one in this release: add a folder to the engine and build it, with every option of
|
|
513
|
+
// the engine's own `init`, meaning the same (Niraj, 2026-11-06: "support all option of niro-local init to niro init").
|
|
514
|
+
program
|
|
515
|
+
.command("init [path]")
|
|
516
|
+
.description("Add a folder to the local engine and build its graph, on a machine `niro start` has set up.")
|
|
517
|
+
.option("--project <name>", "project to join or create (defaults to the folder's own name; a new branch of a repository already indexed becomes that project's temporary copy)")
|
|
518
|
+
.option("--branch <name>", "the branch of a folder that is not in git; for a folder in git the branch is read from git and this is ignored")
|
|
519
|
+
.option("--path <folder>", "folder to add (defaults to [path], or the current folder)")
|
|
520
|
+
.option("--base-url <url>", "the address this folder's service answers on (worked out from the source when left out; an empty value leaves it unset)")
|
|
521
|
+
.option("--force", "add a folder that holds nothing resembling a project (never a home folder, the filesystem root or a system folder)")
|
|
522
|
+
.action((pathArg, opts) => run(async () => {
|
|
523
|
+
await require("./commands/initLocal").initLocal(pathArg, {
|
|
524
|
+
initOptions: { project: opts.project, branch: opts.branch, path: opts.path, baseUrl: opts.baseUrl, force: Boolean(opts.force) },
|
|
525
|
+
});
|
|
526
|
+
}));
|
|
527
|
+
program
|
|
528
|
+
.command("status")
|
|
529
|
+
.description("This folder's graph: indexed or not, last build, plan, connected assistants; then the local engine's own status for the folder.")
|
|
530
|
+
.option("--json", "emit machine-readable JSON (niro's part only)")
|
|
531
|
+
.action((opts) => run(async () => {
|
|
532
|
+
await require("./commands/status").status(undefined, { ...opts });
|
|
533
|
+
// Then the engine's own answer for this folder (which project holds it, or which it can
|
|
534
|
+
// join). Not with --json, whose reader expects one JSON object; niro's exit code stands.
|
|
535
|
+
if (opts.json) return;
|
|
536
|
+
const own = process.exitCode;
|
|
537
|
+
console.log("");
|
|
538
|
+
await require("./commands/engineRoutes").routeToEngine("status", [], { licensed: false });
|
|
539
|
+
process.exitCode = own;
|
|
540
|
+
}));
|
|
541
|
+
program
|
|
542
|
+
.command("build [path]")
|
|
543
|
+
.description("Rebuild the graph of a folder already added with `niro init`.")
|
|
544
|
+
.option("--full", "parse every file again, rather than only what changed")
|
|
545
|
+
.option("--project <name>", "for a folder several projects hold: build only this project's copy")
|
|
546
|
+
.action((pathArg, opts) => run(async () => {
|
|
547
|
+
const buildArgs = [];
|
|
548
|
+
if (opts.full) buildArgs.push("--full");
|
|
549
|
+
if (opts.project) buildArgs.push("--project", opts.project);
|
|
550
|
+
await require("./commands/initLocal").initLocal(pathArg, { rebuildOnly: true, buildArgs });
|
|
551
|
+
}));
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// The local engine's other commands, as niro commands (src/commands/engineRoutes.js). After the
|
|
555
|
+
// filter above, because `remove` and `projects` are routed only while the cloud is off.
|
|
556
|
+
require("./commands/engineRoutes").register(program, require("./lib/release"));
|
|
557
|
+
require("./commands/guide").register(program, run); // `niro guide`, the docs in the package (both releases)
|
|
558
|
+
|
|
389
559
|
program.parseAsync(process.argv).catch((err) => {
|
|
390
560
|
console.error(err && err.message ? err.message : err);
|
|
391
561
|
process.exit(1);
|
|
@@ -400,6 +570,7 @@ function run(fn) {
|
|
|
400
570
|
return Promise.resolve()
|
|
401
571
|
.then(fn)
|
|
402
572
|
.catch((err) => {
|
|
573
|
+
if (err && err.alreadySaid) process.exit(err.exitCode || 1);
|
|
403
574
|
if (err && (err.code === "ENOAUTH" || err.code === "ENOPROJECT" || err.code === "EAMBIGUOUS")) {
|
|
404
575
|
console.error(err.message);
|
|
405
576
|
process.exit(1);
|
package/src/lib/agentApi.js
CHANGED
|
@@ -60,6 +60,37 @@ async function registerRepo({ displayName, repositoryUrl, branch, moduleExcludeP
|
|
|
60
60
|
return data.git_id;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Register a workspace for `niro audit-push-repo`, marked as shared to help improve Niro.
|
|
65
|
+
*
|
|
66
|
+
* The mark is what lets the server delete this code when the account switches sharing off
|
|
67
|
+
* (ai-orchestrator keeps one `shared_code_repos` record per marked workspace and deletes the
|
|
68
|
+
* code 7 days after it first sees sharing off). A workspace from `niro init` or `niro add repo`
|
|
69
|
+
* goes through `registerRepo` above and is never marked, so it is never deleted that way.
|
|
70
|
+
*
|
|
71
|
+
* Returns `{ gitId, sharedForImprovement }`. `sharedForImprovement` is true only when the
|
|
72
|
+
* server echoed the mark back as a real boolean: an older server ignores the unknown field
|
|
73
|
+
* without saying so, and the caller must then refuse to upload.
|
|
74
|
+
*/
|
|
75
|
+
async function registerSharedRepo({ displayName }) {
|
|
76
|
+
const data = await api.post("/api/agent/repos", {
|
|
77
|
+
display_name: displayName,
|
|
78
|
+
shared_for_improvement: true,
|
|
79
|
+
});
|
|
80
|
+
return {
|
|
81
|
+
gitId: data && data.git_id,
|
|
82
|
+
sharedForImprovement: Boolean(data) && data.shared_for_improvement === true,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Delete a workspace record this CLI registered. Used only to take back a shared workspace the
|
|
88
|
+
* server did not mark (see `registerSharedRepo`), so nothing unmarked is left behind.
|
|
89
|
+
*/
|
|
90
|
+
async function deleteRepo(gitId) {
|
|
91
|
+
return api.del(`/api/git/${encodeURIComponent(gitId)}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
63
94
|
/**
|
|
64
95
|
* Fetch backend-suggested module exclude patterns for a repo.
|
|
65
96
|
* Returns an array of { pattern, label, module_count, sample_modules }.
|
|
@@ -273,4 +304,4 @@ async function getFormDataImpl() {
|
|
|
273
304
|
throw new Error("FormData/Blob not available. Node 18+ is required.");
|
|
274
305
|
}
|
|
275
306
|
|
|
276
|
-
module.exports = { registerRepo, filterManifest, startSync, uploadFileBatch, completeSync, upsertFile, deleteFile, heartbeat, getStatus, repoExists, isRepoNotFound, getModuleExcludeSuggestions, updateModuleExcludePatterns };
|
|
307
|
+
module.exports = { registerRepo, registerSharedRepo, deleteRepo, filterManifest, startSync, uploadFileBatch, completeSync, upsertFile, deleteFile, heartbeat, getStatus, repoExists, isRepoNotFound, getModuleExcludeSuggestions, updateModuleExcludePatterns };
|
package/src/lib/api.js
CHANGED
|
@@ -65,7 +65,7 @@ let inFlightRefresh = null;
|
|
|
65
65
|
* return the new token.
|
|
66
66
|
* Never logs request/response bodies (they carry the token/api_key).
|
|
67
67
|
*/
|
|
68
|
-
async function ensureFreshToken(
|
|
68
|
+
async function ensureFreshToken() {
|
|
69
69
|
const creds = credentials.load();
|
|
70
70
|
const durable = creds && (creds.cliToken || creds.niroApiKey);
|
|
71
71
|
if (!creds || (!creds.token && !durable)) {
|
|
@@ -88,7 +88,7 @@ async function ensureFreshToken(opts = {}) {
|
|
|
88
88
|
if (inFlightRefresh) {
|
|
89
89
|
return inFlightRefresh;
|
|
90
90
|
}
|
|
91
|
-
inFlightRefresh = doRefresh(creds,
|
|
91
|
+
inFlightRefresh = doRefresh(creds, baseUrl(creds)).finally(() => {
|
|
92
92
|
inFlightRefresh = null;
|
|
93
93
|
});
|
|
94
94
|
return inFlightRefresh;
|
|
@@ -126,7 +126,7 @@ async function doRefresh(creds, base) {
|
|
|
126
126
|
return newToken;
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
async function request(method, path, { body, query, apiUrl, token, expectStatus, skipRefresh, noAuth, isRetry } = {}) {
|
|
129
|
+
async function request(method, path, { body, query, apiUrl, token, expectStatus, skipRefresh, noAuth, isRetry, signal } = {}) {
|
|
130
130
|
const creds = credentials.load();
|
|
131
131
|
const base = apiUrl || baseUrl(creds);
|
|
132
132
|
const url = new URL(path.startsWith("/") ? path : `/${path}`, base);
|
|
@@ -143,6 +143,10 @@ async function request(method, path, { body, query, apiUrl, token, expectStatus,
|
|
|
143
143
|
|
|
144
144
|
const init = { method, headers };
|
|
145
145
|
if (body !== undefined) init.body = typeof body === "string" ? body : JSON.stringify(body);
|
|
146
|
+
// Caller-supplied cancellation, used by anything that must not wait indefinitely (the
|
|
147
|
+
// licence lease). Nothing changes for a caller that does not pass one: fetch has no timeout
|
|
148
|
+
// of its own either way.
|
|
149
|
+
if (signal) init.signal = signal;
|
|
146
150
|
|
|
147
151
|
let res;
|
|
148
152
|
try {
|
|
@@ -159,12 +163,22 @@ async function request(method, path, { body, query, apiUrl, token, expectStatus,
|
|
|
159
163
|
if (res.status === 401 && !isRetry && !skipRefresh && !token && !noAuth) {
|
|
160
164
|
let fresh;
|
|
161
165
|
try {
|
|
162
|
-
|
|
163
|
-
|
|
166
|
+
// At the sign-in service, not at `base`: billing, niro-audit and ai-assistant check a token
|
|
167
|
+
// but never issue one, so refreshing against them failed and every call to them stayed
|
|
168
|
+
// refused once the token had expired (2026-11-05: the licence, `niro plan` and the engine
|
|
169
|
+
// download all broke an hour into a session).
|
|
170
|
+
fresh = await ensureFreshToken();
|
|
171
|
+
} catch (err) {
|
|
172
|
+
// The sign-in service refusing the renewal with 403 is the account itself being refused
|
|
173
|
+
// (account_blocked, account_waiting: OnPremiseAuthController.cliTokenLogin), not this machine
|
|
174
|
+
// being signed out. Passed on as that 403, so the licence channel tells the engine REFUSED
|
|
175
|
+
// instead of reading it as "billing unreachable" and letting an open session run on until
|
|
176
|
+
// the lease's offline allowance ends (2026-11-11). Any other failure stays the original 401.
|
|
177
|
+
if (err && err.status === 403) throw err;
|
|
164
178
|
fresh = null;
|
|
165
179
|
}
|
|
166
180
|
if (fresh) {
|
|
167
|
-
return request(method, path, { body, query, apiUrl, token: fresh, expectStatus, isRetry: true });
|
|
181
|
+
return request(method, path, { body, query, apiUrl, token: fresh, expectStatus, isRetry: true, signal });
|
|
168
182
|
}
|
|
169
183
|
}
|
|
170
184
|
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sends the local engine's coverage-audit findings to niro-audit.
|
|
3
|
+
*
|
|
4
|
+
* Why this lives here and not in the engine: the engine holds no account and no credential,
|
|
5
|
+
* by design. It indexes code and writes what it found to its own queue. Anything that
|
|
6
|
+
* leaves the machine leaves through the CLI, which is the part that signed in.
|
|
7
|
+
*
|
|
8
|
+
* So the engine is asked for finished payloads and told which ones arrived, and this file
|
|
9
|
+
* never builds a payload itself:
|
|
10
|
+
*
|
|
11
|
+
* niro-local audit --emit JSON Lines, one per waiting item
|
|
12
|
+
* POST <audit>/api/audit/items each item's `payload`, verbatim
|
|
13
|
+
* POST <audit>/api/audit/items/resolve for a file that came back clean
|
|
14
|
+
* niro-local audit --mark-sent tab-separated rows of what arrived
|
|
15
|
+
* POST <audit>/api/audit/catalog a folder's catalogue signals (kind "catalog", since 2026-11-09)
|
|
16
|
+
* niro-local audit --mark-catalog-sent tab-separated (folder id, sha) of the catalogues that arrived
|
|
17
|
+
*
|
|
18
|
+
* Keeping the field names on the engine's side of that line means the wire format has one
|
|
19
|
+
* home. If the server adds a field, the engine changes and this file does not.
|
|
20
|
+
*
|
|
21
|
+
* A 409 is success, not failure: the server's unique dedup index refused a file it already
|
|
22
|
+
* holds for that content hash, which is exactly what we want it to do. The row is marked
|
|
23
|
+
* sent so the engine stops offering it.
|
|
24
|
+
*
|
|
25
|
+
* Nothing here logs a payload. They carry the customer's source.
|
|
26
|
+
*
|
|
27
|
+
* **Two gates before anything leaves.** The lease must be readable (a machine that is not signed
|
|
28
|
+
* in, or cannot reach its plan, sends nothing at all, not even a request); sharing is NOT asked,
|
|
29
|
+
* because the coverage audit is sent on every plan (2026-11-05; until then it needed
|
|
30
|
+
* `share_code: true`, design §6.6). Then every payload is redacted. The engine has already masked
|
|
31
|
+
* its secrets with llm-gateway's rules (niro-local `docs/usecases/on-machine-redaction.md`) and
|
|
32
|
+
* says so with `redacted` on each line; a line without it comes from an older engine that masked
|
|
33
|
+
* nothing, so it is left in the queue, not sent, until the engine is updated. Here redact.js then
|
|
34
|
+
* removes author and company names, with `sha` kept as the dedup key. The engine's payload is
|
|
35
|
+
* otherwise posted as it came; a clean file goes byte for byte.
|
|
36
|
+
*/
|
|
37
|
+
const path = require("path");
|
|
38
|
+
|
|
39
|
+
const localEngine = require("./localEngine");
|
|
40
|
+
const api = require("./api");
|
|
41
|
+
const lease = require("./lease");
|
|
42
|
+
const credentials = require("./credentials");
|
|
43
|
+
const git = require("./git");
|
|
44
|
+
const { redactItem } = require("./redact");
|
|
45
|
+
|
|
46
|
+
/** Production niro-audit (ECS niro-aws-audit-service behind niro-aws-lb). NIRO_AUDIT_URL overrides it. */
|
|
47
|
+
const DEFAULT_AUDIT_URL = "https://audit.cloud.niroai.dev";
|
|
48
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
49
|
+
const DEFAULT_BATCH = 50;
|
|
50
|
+
|
|
51
|
+
/** Which audit service to post to. */
|
|
52
|
+
/** Env var, then `auditUrl` in ~/.niro/config.json (as journalUpload reads mcpServerUrl), then the service. */
|
|
53
|
+
function auditUrl() {
|
|
54
|
+
let configured = null;
|
|
55
|
+
try { configured = require("./config").load().auditUrl; } catch { /* the default */ }
|
|
56
|
+
return (process.env.NIRO_AUDIT_URL || configured || DEFAULT_AUDIT_URL).replace(/\/+$/, "");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** An API key, only if one was deliberately put in the environment. */
|
|
60
|
+
function apiKey() {
|
|
61
|
+
return process.env.NIRO_AUDIT_API_KEY || process.env.NIRO_API_KEY || "";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* What this machine authenticates with, resolved once per run.
|
|
66
|
+
*
|
|
67
|
+
* Normally it is the login token: `niro login` leaves a short-lived JWT behind, and
|
|
68
|
+
* niro-audit now verifies it on the two routes this file uses. Niraj, 2026-10-15: "we don't
|
|
69
|
+
* work with any api tokens any more", and "niro-audit should also accept a cli token from a
|
|
70
|
+
* developer machine". So a developer needs nothing beyond being logged in, and no API key is
|
|
71
|
+
* ever created for, or stored on, their machine.
|
|
72
|
+
*
|
|
73
|
+
* `api.ensureFreshToken` is reused rather than reimplemented: it returns the stored token when
|
|
74
|
+
* it is still good and otherwise exchanges the durable CLI token for a new one, sharing a
|
|
75
|
+
* single refresh between concurrent callers. A second copy of that logic here would be a
|
|
76
|
+
* second thing to get wrong, and this runs from a watcher that may be up for hours.
|
|
77
|
+
*
|
|
78
|
+
* An API key in the environment wins when one is set, for the upload's own header only. It is
|
|
79
|
+
* not a way round `niro login`: the plan is read through the sign-in first (uploadPending), and
|
|
80
|
+
* the watcher is never given the key. Letting it win keeps a deployment working whose niro-audit
|
|
81
|
+
* has no JWT secret configured yet. Nothing creates it; it is only read.
|
|
82
|
+
*/
|
|
83
|
+
async function authHeaders(freshToken) {
|
|
84
|
+
const key = apiKey();
|
|
85
|
+
if (key) {
|
|
86
|
+
return { "Content-Type": "application/json", "X-API-Key": key };
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
return {
|
|
90
|
+
"Content-Type": "application/json",
|
|
91
|
+
Authorization: `Bearer ${await freshToken()}`,
|
|
92
|
+
};
|
|
93
|
+
} catch (err) {
|
|
94
|
+
if (err && err.code === "ENOAUTH") {
|
|
95
|
+
// The key is not a way round the sign-in: the plan is read through the `niro login` sign-in
|
|
96
|
+
// before anything is sent (uploadPending, lease.currentClaims), and the watcher is never given
|
|
97
|
+
// the key (service.js WATCHER_ENV_EXCLUDED). Saying "set the key instead" sent people to a
|
|
98
|
+
// dead end (docs/connect-ci.md).
|
|
99
|
+
throw new Error(
|
|
100
|
+
`${err.message} The audit is not sent without that sign-in, because it reads your plan through it first. ` +
|
|
101
|
+
"NIRO_AUDIT_API_KEY only changes the key the upload itself is signed with, so it does not help on its own."
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
throw err;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function postJson(path, body, headers) {
|
|
109
|
+
const controller = new AbortController();
|
|
110
|
+
const timer = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);
|
|
111
|
+
try {
|
|
112
|
+
return await fetch(`${auditUrl()}${path}`, {
|
|
113
|
+
method: "POST",
|
|
114
|
+
headers,
|
|
115
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
116
|
+
signal: controller.signal,
|
|
117
|
+
});
|
|
118
|
+
} finally {
|
|
119
|
+
clearTimeout(timer);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Send everything the engine is holding.
|
|
125
|
+
*
|
|
126
|
+
* Returns counts rather than printing, so a caller can report it its own way: the command
|
|
127
|
+
* prints, and the watcher will log one line.
|
|
128
|
+
*/
|
|
129
|
+
/** The owner in a git remote: `acme` from git@github.com:acme/x.git or https://host/acme/x. */
|
|
130
|
+
function remoteOwner(originUrl) {
|
|
131
|
+
const m = /[:/]([^/:]+)\/[^/]+?(?:\.git)?\/?$/.exec(originUrl || "");
|
|
132
|
+
return m ? m[1] : null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** worktree id → its git remote owner, from the engine's project list. Empty when unknown. */
|
|
136
|
+
async function ownersByWorktree() {
|
|
137
|
+
const owners = {};
|
|
138
|
+
try {
|
|
139
|
+
const parsed = JSON.parse(await localEngine.run(["projects", "--json"]));
|
|
140
|
+
for (const project of parsed.projects || []) {
|
|
141
|
+
for (const w of project.worktrees || []) {
|
|
142
|
+
owners[w.id] = remoteOwner(git.inspect(path.resolve(String(w.path))).originUrl);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
} catch {
|
|
146
|
+
// Without an owner the email domain and copyright holders are still removed.
|
|
147
|
+
}
|
|
148
|
+
return owners;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* A niro-audit older than the catalogue route (2026-11-09) refuses it: a login token on a route that is not one
|
|
153
|
+
* of its sending routes is answered 403 `admin_required`, and an admin's or a key's request finds no route (404 or
|
|
154
|
+
* 405). The catalogue then waits in the engine's queue for a server that takes it; it is not a failure of the audit.
|
|
155
|
+
*/
|
|
156
|
+
async function serverTakesNoCatalogue(response) {
|
|
157
|
+
if (response.status === 404 || response.status === 405) return true;
|
|
158
|
+
if (response.status !== 403) return false;
|
|
159
|
+
const text = await response.text().catch(() => "");
|
|
160
|
+
return text.includes("admin_required");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function uploadPending({ limit = DEFAULT_BATCH, freshToken = api.ensureFreshToken } = {}) {
|
|
164
|
+
const result = {
|
|
165
|
+
submitted: 0, duplicate: 0, resolved: 0, closedNothing: 0, catalogs: 0, catalogsWaiting: 0, failed: 0, failures: [],
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// First of all: is there a `niro login` sign-in? Without one the plan read below fails too, and the
|
|
169
|
+
// user saw only "the plan could not be read", which hid the cause. The same rule as every command
|
|
170
|
+
// that needs a sign-in (credentials.requireCreds); an API key in the environment does not count.
|
|
171
|
+
try {
|
|
172
|
+
credentials.requireCreds(credentials.load());
|
|
173
|
+
} catch (err) {
|
|
174
|
+
if (!err || err.code !== "ENOAUTH") throw err;
|
|
175
|
+
result.skipped = "not signed in. The audit reads your plan through your `niro login` sign-in, so run `niro login` first";
|
|
176
|
+
return result;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Then, before a token or the queue: is this machine signed in to a readable plan?
|
|
180
|
+
// Sharing is not asked: the coverage audit (redacted flagged files) is sent on every plan,
|
|
181
|
+
// like the tool-usage record. Only a whole-repository push needs sharing on (repoPush.js).
|
|
182
|
+
// Divya, 2026-11-05: "only full repo audit should not come when sharing is off".
|
|
183
|
+
// Since 2026-11-11 that holds for the background code sharing (shareCode.js) only: a typed
|
|
184
|
+
// `niro audit-push-repo` sends with sharing off, because typing it and answering yes is the consent.
|
|
185
|
+
const claims = await lease.currentClaims().catch(() => null);
|
|
186
|
+
if (!claims) {
|
|
187
|
+
result.skipped = "the plan could not be read, so nothing was sent";
|
|
188
|
+
return result;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Before the engine is asked for anything, so a machine that cannot authenticate does not
|
|
192
|
+
// pull payloads out of the queue only to fail to send them. Resolved once for the whole run:
|
|
193
|
+
// the token outlives a batch, and refreshing per item would be a request per file.
|
|
194
|
+
const headers = await authHeaders(freshToken);
|
|
195
|
+
|
|
196
|
+
const emitted = await localEngine.run(["audit", "--emit", "--limit", String(limit)]);
|
|
197
|
+
const items = emitted
|
|
198
|
+
.split("\n")
|
|
199
|
+
.map((line) => line.trim())
|
|
200
|
+
.filter(Boolean)
|
|
201
|
+
.map((line) => JSON.parse(line));
|
|
202
|
+
|
|
203
|
+
const email = (credentials.load() || {}).email || null;
|
|
204
|
+
const owners = items.some((i) => i.kind === "submission") ? await ownersByWorktree() : {};
|
|
205
|
+
|
|
206
|
+
const sent = [];
|
|
207
|
+
const catalogsSent = [];
|
|
208
|
+
for (const item of items) {
|
|
209
|
+
if (item.kind === "not_sent") {
|
|
210
|
+
// Flagged files the engine dropped: not UTF-8 text (it cannot mask them), or, since 2026-11-09, a
|
|
211
|
+
// whole file an older engine queued that could not be cut to a snippet, or a snippet that failed its
|
|
212
|
+
// check. Counted, never named. A line with no reason is from an engine that only had the first.
|
|
213
|
+
const count = Number(item.count) || 0;
|
|
214
|
+
if (!item.reason || item.reason === "not UTF-8 text") result.notText = (result.notText || 0) + count;
|
|
215
|
+
else result.notSnippet = (result.notSnippet || 0) + count;
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (!item.redacted) {
|
|
219
|
+
// An older engine: its payload was never masked. Kept in its queue, not marked, not sent.
|
|
220
|
+
result.unmasked = (result.unmasked || 0) + 1;
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
try {
|
|
224
|
+
if (item.kind === "submission") {
|
|
225
|
+
// Throws on source it cannot read, which lands in the catch below: kept, not sent.
|
|
226
|
+
const payload = redactItem(item.payload, { email, remoteOwner: owners[item.worktree_id] || null });
|
|
227
|
+
const response = await postJson("/api/audit/items", payload, headers);
|
|
228
|
+
if (response.status === 201 || response.ok) {
|
|
229
|
+
result.submitted += 1;
|
|
230
|
+
} else if (response.status === 409) {
|
|
231
|
+
// Already held for this content hash. The server's dedup index did its job.
|
|
232
|
+
result.duplicate += 1;
|
|
233
|
+
} else {
|
|
234
|
+
result.failed += 1;
|
|
235
|
+
result.failures.push(`${item.source_id}: HTTP ${response.status}`);
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
sent.push([item.worktree_id, item.source_id, item.sha].join("\t"));
|
|
239
|
+
} else if (item.kind === "catalog") {
|
|
240
|
+
// What one build of one folder tells the catalogue: extensions read in no language and outside library
|
|
241
|
+
// names, with counts. The engine leaves out every file name, path, import line and own-code name
|
|
242
|
+
// (niro-local audit/catalog.py), so there is no source to redact; it is posted as the engine wrote it.
|
|
243
|
+
const response = await postJson("/api/audit/catalog", item.payload, headers);
|
|
244
|
+
if (response.status === 201 || response.ok) {
|
|
245
|
+
result.catalogs += 1;
|
|
246
|
+
catalogsSent.push([item.worktree_id, item.sha].join("\t"));
|
|
247
|
+
} else if (await serverTakesNoCatalogue(response)) {
|
|
248
|
+
result.catalogsWaiting += 1;
|
|
249
|
+
} else {
|
|
250
|
+
result.failed += 1;
|
|
251
|
+
result.failures.push(`catalogue: HTTP ${response.status}`);
|
|
252
|
+
}
|
|
253
|
+
} else if (item.kind === "resolution") {
|
|
254
|
+
// repoId matters: the server's resolve matches on (project, repo, file), so a
|
|
255
|
+
// request without it matches nothing and still answers 200. Reading the count it
|
|
256
|
+
// returns is what tells a real close from a no-op.
|
|
257
|
+
const query = new URLSearchParams({
|
|
258
|
+
projectId: item.project_id,
|
|
259
|
+
repoId: item.repo_id || "",
|
|
260
|
+
// The path as the server holds it: masked by the engine exactly as the submission was.
|
|
261
|
+
sourceId: item.masked_source_id || item.source_id,
|
|
262
|
+
});
|
|
263
|
+
const response = await postJson(`/api/audit/items/resolve?${query}`, undefined, headers);
|
|
264
|
+
if (!response.ok) {
|
|
265
|
+
result.failed += 1;
|
|
266
|
+
result.failures.push(`${item.source_id} (resolve): HTTP ${response.status}`);
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
const closed = await response
|
|
270
|
+
.json()
|
|
271
|
+
.then((body) => (body && body.data && body.data.resolved) || 0)
|
|
272
|
+
.catch(() => 0);
|
|
273
|
+
result.resolved += closed;
|
|
274
|
+
result.closedNothing += closed === 0 ? 1 : 0;
|
|
275
|
+
sent.push([item.worktree_id, item.source_id].join("\t"));
|
|
276
|
+
}
|
|
277
|
+
} catch (problem) {
|
|
278
|
+
// One item failing must not lose the rest: it stays queued and is retried next time.
|
|
279
|
+
result.failed += 1;
|
|
280
|
+
result.failures.push(`${item.source_id || item.kind}: ${problem.message}`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Only what actually arrived is marked, so a failure is retried rather than forgotten.
|
|
285
|
+
if (sent.length > 0) await localEngine.run(["audit", "--mark-sent"], sent.join("\n") + "\n");
|
|
286
|
+
if (catalogsSent.length > 0) {
|
|
287
|
+
await localEngine.run(["audit", "--mark-catalog-sent"], catalogsSent.join("\n") + "\n");
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return result;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
module.exports = { uploadPending, auditUrl, remoteOwner, DEFAULT_AUDIT_URL };
|