@niroai/niro 0.17.0 → 1.0.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
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The plan and its line usage, in one line, for `niro start` and `niro plan`.
|
|
3
|
+
*
|
|
4
|
+
* Usage is the engine's own total, `lines_counted` in `niro-local projects --json`: the number its
|
|
5
|
+
* line limit compares, counted by its own rule (each repository once, by its largest checkout,
|
|
6
|
+
* whichever projects hold it; a folder inside another indexed folder is not counted again). Nothing
|
|
7
|
+
* is added up here (Niraj, 2026-11-06, Q2: "niro-cli no longer adds lines up per project"). Each
|
|
8
|
+
* folder's own `lines` is what that folder's build read, for the list, never for a sum. The limit is
|
|
9
|
+
* the lease's `max_lines`; absent means none. Free has no limit while the account shares redacted
|
|
10
|
+
* code, so that case says why.
|
|
11
|
+
*/
|
|
12
|
+
const localEngine = require("./localEngine");
|
|
13
|
+
|
|
14
|
+
/** Lines this machine holds, as the engine's limit counts them; null when the engine cannot say. */
|
|
15
|
+
async function linesUsed({ run = localEngine.run } = {}) {
|
|
16
|
+
try {
|
|
17
|
+
const total = JSON.parse(await run(["projects", "--json"])).lines_counted;
|
|
18
|
+
return Number.isInteger(total) && total >= 0 ? total : null;
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Is this folder already counted against the limit, so the engine builds it even at or over the limit?
|
|
26
|
+
* The engine decides (`niro-local projects --json --path`, `already_counted`): a folder indexed before,
|
|
27
|
+
* a temporary copy of an indexed repository, a service inside an indexed monorepo. Only a folder that
|
|
28
|
+
* adds new lines is refused (Niraj, 2026-11-06). False when the engine cannot say, which refuses as
|
|
29
|
+
* before rather than letting a new repository past.
|
|
30
|
+
*/
|
|
31
|
+
async function alreadyCounted(dir, { run = localEngine.run } = {}) {
|
|
32
|
+
try {
|
|
33
|
+
return JSON.parse(await run(["projects", "--json", "--path", dir])).already_counted === true;
|
|
34
|
+
} catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Every indexed folder with its line count, largest first, as the engine reports them; [] when it cannot say. */
|
|
40
|
+
async function folders({ run = localEngine.run } = {}) {
|
|
41
|
+
try {
|
|
42
|
+
const { projects = [] } = JSON.parse(await run(["projects", "--json"]));
|
|
43
|
+
return projects
|
|
44
|
+
.flatMap((p) => (p.worktrees || []).map((w) => ({ path: w.path, project: p.name, lines: Number(w.lines) || 0 })))
|
|
45
|
+
.sort((a, b) => b.lines - a.lines);
|
|
46
|
+
} catch {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const n = (x) => x.toLocaleString("en-US");
|
|
52
|
+
|
|
53
|
+
/** "Free, 212,400 of 500,000 lines used", or null with no claims to read. */
|
|
54
|
+
function describe(claims, used) {
|
|
55
|
+
if (!claims) return null;
|
|
56
|
+
const name = claims.plan_name || "Your plan";
|
|
57
|
+
if (claims.max_lines === undefined || claims.max_lines === null) {
|
|
58
|
+
return claims.share_code && claims.plan_id === "plan-free" ? `${name}, Unlimited Lines of Code while you share` : `${name}, Unlimited Lines of Code`;
|
|
59
|
+
}
|
|
60
|
+
if (used === null) return `${name}, up to ${n(claims.max_lines)} lines`;
|
|
61
|
+
// The build that takes a machine over is allowed to finish, so "6,153 of 5,000 used" is a
|
|
62
|
+
// real state; said as "over" rather than as a fraction above one.
|
|
63
|
+
if (used > claims.max_lines) return `${name}, ${n(used)} lines indexed, ${n(used - claims.max_lines)} over the ${n(claims.max_lines)} limit`;
|
|
64
|
+
return `${name}, ${n(used)} of ${n(claims.max_lines)} lines used`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* What the next build will do, when it will not run: at or over the limit the engine starts no build
|
|
69
|
+
* of a folder that adds new lines (niro_local/licence/limit.py `room` and `already_counted`), while
|
|
70
|
+
* folders already indexed still rebuild. Null under the limit.
|
|
71
|
+
*/
|
|
72
|
+
function noRoomNote(claims, used) {
|
|
73
|
+
if (!claims || claims.max_lines === undefined || claims.max_lines === null || used === null) return null;
|
|
74
|
+
if (used < claims.max_lines) return null;
|
|
75
|
+
return "No room left under the limit, so a new folder will not be indexed. Folders already indexed still rebuild.";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Would the engine refuse to build this folder for the line limit? Only at or over the limit
|
|
80
|
+
* (`noRoomNote`), and only for a folder the engine does not already count (`alreadyCounted`), which
|
|
81
|
+
* is asked only then. The same rule as the engine's `_no_room`, so niro-cli refuses exactly what the
|
|
82
|
+
* engine would.
|
|
83
|
+
*/
|
|
84
|
+
async function refusesBuild(claims, used, dir, { counted = (d) => module.exports.alreadyCounted(d) } = {}) {
|
|
85
|
+
if (!module.exports.noRoomNote(claims, used)) return false;
|
|
86
|
+
return !(await counted(dir));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The console this machine signed in through (saved by `niro login`), else production. */
|
|
90
|
+
function consoleUrl({ load = require("./config").load } = {}) {
|
|
91
|
+
try { return (load().appUrl || "https://console.niroai.dev").replace(/\/+$/, ""); } catch { return "https://console.niroai.dev"; }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** The console's /limit page for these numbers. */
|
|
95
|
+
function limitUrl(used, max, opts) {
|
|
96
|
+
return `${consoleUrl(opts)}/limit?used=${used}&limit=${max}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* What leaves this machine on every plan, whatever the sharing switch says: for each line the coverage
|
|
101
|
+
* audit flags, the method that holds it and one line for each name it uses, with secrets masked
|
|
102
|
+
* (auditUpload.js; niro-local `audit/snippet.py`, since 2026-11-09, when the clause changed from "files the
|
|
103
|
+
* audit flags" as the audit snippet plan asked), and the record of which Niro tools the
|
|
104
|
+
* assistant used (journalUpload.js), which carries tool names, timings, a short task summary and
|
|
105
|
+
* the names, paths and search text asked about. Niraj approved this wording on 2026-11-06 ("yes
|
|
106
|
+
* your wording suggestion is good"). README.md, the niro-local skill (src/mcp/localSkill.js) and
|
|
107
|
+
* docs/uninstall.md repeat it word for word, and test/planUsage.test.js fails when they drift.
|
|
108
|
+
*
|
|
109
|
+
* The last sentence (Niraj, 2026-11-06: "important to say also that niro tools don't share any
|
|
110
|
+
* code") holds for this release: every tool call is answered by the engine on this machine
|
|
111
|
+
* (src/mcp/server.js main, `!release.cloud`), the engine makes no network calls, LLM documentation
|
|
112
|
+
* is off (release.js `llmDocs: false`), and the tool record carries names, paths and search text
|
|
113
|
+
* but no file contents. It says "the contents of your files", not "code", because a search pattern
|
|
114
|
+
* in the tool record can be a short piece of code the assistant typed. Turning `cloud` or
|
|
115
|
+
* `llmDocs` on makes it false: revisit it then.
|
|
116
|
+
*/
|
|
117
|
+
const AUDIT_CLAUSE = "Your code stays on this machine, except, for each line the audit flags, the whole method that holds it and one line for each name it uses, sent with secrets masked and author and company names removed";
|
|
118
|
+
const JOURNAL_SENTENCE = "A record of which Niro tools your assistant used is also sent, to improve Niro and to show you its value.";
|
|
119
|
+
const TOOLS_SENTENCE = "Niro's tools answer your assistant from the graph on this machine and never send the contents of your files.";
|
|
120
|
+
const WHAT_LEAVES = `${AUDIT_CLAUSE}. ${JOURNAL_SENTENCE} ${TOOLS_SENTENCE}`;
|
|
121
|
+
// What "Help improve Niro" on adds, in the console's words (spec-builder-react shared/constants/codeSharing.ts).
|
|
122
|
+
const SHARING_ON_SENTENCE = "Niro also receives your code, redacted on this machine (secrets, author and company names removed), to improve graph accuracy. It is deleted right after processing and never used to train generative AI.";
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The "Sharing" line of `niro status` and `niro plan`: the switch, then the same sentence either
|
|
126
|
+
* way. Since 2026-11-11, sharing on means the watcher sends, by itself, the files Niro indexes in
|
|
127
|
+
* every folder not excluded with `niro share exclude`: the whole set the first time, then changed
|
|
128
|
+
* files, redacted on this machine (shareCode.js; Niraj: "we need the pm2 to share the code as
|
|
129
|
+
* well"). That is what "Niro also receives your code" says. `niro audit-push-repo` sends the same
|
|
130
|
+
* files of one folder when typed, with sharing on or off, and asks before sending an excluded folder
|
|
131
|
+
* (repoPush.js).
|
|
132
|
+
*
|
|
133
|
+
* `null` is a licence that could not be read: the audit and `niro audit-push-repo` send nothing
|
|
134
|
+
* without one, but the journal needs only the login token and still goes, and a search pattern in
|
|
135
|
+
* it can be a short piece of code; so "no source files", not "nothing" or "no code".
|
|
136
|
+
*/
|
|
137
|
+
function sharingText(shareCode) {
|
|
138
|
+
if (shareCode === null || shareCode === undefined) return "unknown, so no source files are sent";
|
|
139
|
+
if (!shareCode) return `off. ${WHAT_LEAVES}`;
|
|
140
|
+
return `on. ${SHARING_ON_SENTENCE} Apart from that, ${WHAT_LEAVES.charAt(0).toLowerCase()}${WHAT_LEAVES.slice(1)}`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* The "Help improve Niro" block of `niro plan`: "on" or "off", then three lines, each ✓ (happens)
|
|
145
|
+
* or ✗ (does not). The first two hold either way: the audit's flagged methods and the tool record
|
|
146
|
+
* are sent on every plan (auditUpload.js, journalUpload.js). The third is the background code
|
|
147
|
+
* sharing, which runs only while sharing is on (shareCode.js). Wording Niraj's, 2026-11-11. The
|
|
148
|
+
* same facts as WHAT_LEAVES, which the README, the skill and the uninstall guide keep as one
|
|
149
|
+
* sentence. `pad` indents the three lines under the value column.
|
|
150
|
+
*/
|
|
151
|
+
function sharingBlock(shareCode, pad) {
|
|
152
|
+
const color = require("./color");
|
|
153
|
+
if (shareCode === null || shareCode === undefined) return [color.dim(sharingText(null))];
|
|
154
|
+
const row = (yes, text) => `${" ".repeat(pad)}${yes ? color.green("✓") : color.red("✗")} ${text}`;
|
|
155
|
+
return [
|
|
156
|
+
shareCode ? "on" : "off",
|
|
157
|
+
row(true, "Your code is indexed and answered on this machine"),
|
|
158
|
+
row(true, "Sent in the background: a record of which Niro tools ran, and the methods the audit flags with the lines they reference (secrets, author and company names removed)"),
|
|
159
|
+
shareCode
|
|
160
|
+
? row(true, "Code shared in the background: the files Niro indexes, redacted here, deleted after processing, never used to train AI")
|
|
161
|
+
: row(false, "Code sharing is off"),
|
|
162
|
+
];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
module.exports = { linesUsed, alreadyCounted, folders, describe, noRoomNote, refusesBuild, consoleUrl, limitUrl, sharingText, sharingBlock, WHAT_LEAVES };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The build bar in `niro start` step 4:
|
|
3
|
+
* ████████████░░░░░░░░ 62% 790 of 1,284 files about 6 s left
|
|
4
|
+
*
|
|
5
|
+
* `render` is pure. `create` draws it in place on a terminal and, where nothing can be
|
|
6
|
+
* redrawn (piped output), prints it only at each quarter so a log is not flooded.
|
|
7
|
+
*/
|
|
8
|
+
const WIDTH = 20;
|
|
9
|
+
|
|
10
|
+
function render(done, total, elapsedMs) {
|
|
11
|
+
const fraction = total > 0 ? Math.min(1, done / total) : 0;
|
|
12
|
+
const filled = Math.floor(fraction * WIDTH);
|
|
13
|
+
// Rounded, but never 100% until the last file is in.
|
|
14
|
+
const pct = done < total ? Math.min(99, Math.round(fraction * 100)) : 100;
|
|
15
|
+
let text = `${"█".repeat(filled)}${"░".repeat(WIDTH - filled)} ${pct}% ` +
|
|
16
|
+
`${done.toLocaleString("en-US")} of ${total.toLocaleString("en-US")} files`;
|
|
17
|
+
if (done > 0 && done < total && elapsedMs > 0) {
|
|
18
|
+
const left = Math.round(((total - done) * elapsedMs) / done / 1000);
|
|
19
|
+
text += ` about ${left} s left`;
|
|
20
|
+
}
|
|
21
|
+
return text;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function create({ stream = process.stdout, prefix = "" } = {}) {
|
|
25
|
+
const startedAt = Date.now();
|
|
26
|
+
const tty = Boolean(stream.isTTY) && process.env.TERM !== "dumb";
|
|
27
|
+
let lastQuarter = -1;
|
|
28
|
+
return {
|
|
29
|
+
update(done, total) {
|
|
30
|
+
const line = prefix + render(done, total, Date.now() - startedAt);
|
|
31
|
+
if (tty) {
|
|
32
|
+
stream.write(`\r\x1b[2K${line}`);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const quarter = total > 0 ? Math.floor((done / total) * 4) : 0;
|
|
36
|
+
if (quarter === lastQuarter) return;
|
|
37
|
+
lastQuarter = quarter;
|
|
38
|
+
stream.write(`${line}\n`);
|
|
39
|
+
},
|
|
40
|
+
stop() {
|
|
41
|
+
if (tty) stream.write("\r\x1b[2K");
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = { render, create };
|
package/src/lib/prompt.js
CHANGED
|
@@ -20,7 +20,7 @@ function rl() {
|
|
|
20
20
|
function formatPrompt(question, defaultValue) {
|
|
21
21
|
// Questions asked from inside a nested step sit at that step's depth. readline writes
|
|
22
22
|
// straight to stdout, so withIndent's console wrappers never see them.
|
|
23
|
-
question =
|
|
23
|
+
question = require("./nestedOutput").currentPad() + question;
|
|
24
24
|
return defaultValue != null && defaultValue !== ""
|
|
25
25
|
? `${question} ${color.dim(`(default: ${defaultValue})`)}: `
|
|
26
26
|
: `${question}: `;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What is removed from a flagged file before it leaves the machine (design §6.6, contract C4):
|
|
3
|
+
* the names of people and companies. Secrets are not handled here any more.
|
|
4
|
+
*
|
|
5
|
+
* authors → <author> @author, Author:/Authors:, Created by, email addresses
|
|
6
|
+
* companies → <company> copyright holders, the org in the user's own email domain, the
|
|
7
|
+
* git remote owner, and every later mention of any of them
|
|
8
|
+
*
|
|
9
|
+
* **Secrets are masked by the engine, not here (decision D4, Niraj, 2026-11-06).** The engine
|
|
10
|
+
* masks every upload with llm-gateway's own rules before niro-cli sees it
|
|
11
|
+
* (niro-local `docs/usecases/on-machine-redaction.md`), and every line it hands over carries
|
|
12
|
+
* `redacted`; auditUpload.js sends no line without it. This file used to mask secrets as `****`,
|
|
13
|
+
* and its whole-line rule rewrote ordinary code (`self.password = password` became
|
|
14
|
+
* `self.password = ****`), so that half was removed rather than kept beside the engine's.
|
|
15
|
+
*
|
|
16
|
+
* Authors and companies stay here because they need what only the CLI knows: the signed-in email
|
|
17
|
+
* and the git remote owner. Marked names only: a person's name in a plain comment has nothing to
|
|
18
|
+
* find it by, and the plan screen promises exactly the kinds above.
|
|
19
|
+
*/
|
|
20
|
+
const zlib = require("zlib");
|
|
21
|
+
|
|
22
|
+
const AUTHOR = "<author>";
|
|
23
|
+
const COMPANY = "<company>";
|
|
24
|
+
|
|
25
|
+
const EMAIL = /[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*\.[A-Za-z]{2,}/g;
|
|
26
|
+
const AT_AUTHOR = /(@author\s+)[^\n*]+/g;
|
|
27
|
+
const AUTHOR_LINE = /\b((?:Authors?|AUTHORS?)\s*:\s*)[^\n]+/g;
|
|
28
|
+
const CREATED_BY = /(Created by\s+)(.+?)(?=\s+on\s|\s*$)/gm;
|
|
29
|
+
// Only at the start of a line or a comment, so `"Copyright " + name` in code is not harvested.
|
|
30
|
+
// The end of the match never takes a line break: until 2026-11-09 it ended `(\s*(?:\*\/)?)\s*$`, and that
|
|
31
|
+
// last `\s*` ate the line break after a ` */` (or after the holder) that the replacement never put back, so
|
|
32
|
+
// `/*\n * Copyright (c) 2019 Foo Corp.\n */\n\npackage x;` lost a line and every flag below it pointed one
|
|
33
|
+
// line too low on the server. Only spaces and tabs now.
|
|
34
|
+
const COPYRIGHT = /^([ \t]*(?:\/\/+|\/\*+|\*|#+|--|;+|<!--)?[ \t]*(?:Copyright|SPDX-FileCopyrightText:)\s*(?:\([cC]\)|©)?\s*(?:\d{4}(?:\s*[-,]\s*\d{4})*,?\s+)?)([^\n*]+?)([ \t]*(?:\*\/)?)[ \t]*$/gm;
|
|
35
|
+
|
|
36
|
+
const PUBLIC_MAIL = new Set([
|
|
37
|
+
"gmail", "googlemail", "outlook", "hotmail", "live", "msn", "yahoo", "icloud", "me", "mac",
|
|
38
|
+
"proton", "protonmail", "pm", "aol", "gmx", "mail", "yandex", "zoho", "fastmail", "users",
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
function escape(s) {
|
|
42
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The org names the context gives: the email domain's first label, and the remote owner. */
|
|
46
|
+
function contextCompanies({ email, remoteOwner } = {}) {
|
|
47
|
+
const names = [];
|
|
48
|
+
const domain = email && String(email).split("@")[1];
|
|
49
|
+
const org = domain && domain.split(".")[0].toLowerCase();
|
|
50
|
+
if (org && !PUBLIC_MAIL.has(org)) names.push(org);
|
|
51
|
+
if (remoteOwner) names.push(String(remoteOwner));
|
|
52
|
+
return names;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function redactSource(text, ctx = {}) {
|
|
56
|
+
if (typeof text !== "string" || !text) return text;
|
|
57
|
+
let out = text;
|
|
58
|
+
|
|
59
|
+
// Authors.
|
|
60
|
+
out = out.replace(EMAIL, AUTHOR);
|
|
61
|
+
out = out.replace(AT_AUTHOR, `$1${AUTHOR}`);
|
|
62
|
+
out = out.replace(AUTHOR_LINE, `$1${AUTHOR}`);
|
|
63
|
+
out = out.replace(CREATED_BY, `$1${AUTHOR}`);
|
|
64
|
+
|
|
65
|
+
// Companies: copyright holders first, then every mention of them and of the context's orgs.
|
|
66
|
+
const holders = [];
|
|
67
|
+
out = out.replace(COPYRIGHT, (whole, lead, holder, tail) => {
|
|
68
|
+
const name = holder.replace(/[.,]?\s*All rights reserved\.?$/i, "").replace(/[.,;]+$/, "").trim();
|
|
69
|
+
if (!name || name.startsWith("<")) return whole;
|
|
70
|
+
holders.push(name);
|
|
71
|
+
return `${lead}${COMPANY}${tail}`;
|
|
72
|
+
});
|
|
73
|
+
const names = [...new Set([...holders, ...contextCompanies(ctx)])]
|
|
74
|
+
.filter((n) => n.length >= 3)
|
|
75
|
+
.sort((a, b) => b.length - a.length); // "acme-labs" before "acme"
|
|
76
|
+
for (const name of names) {
|
|
77
|
+
out = out.replace(new RegExp(`(?<![\\w-])${escape(name)}(?![\\w-])`, "gi"), COMPANY);
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** The text's lines, split on "\n" only; a final "\n" ends the last line (as the engine and niro-audit count). */
|
|
83
|
+
function linesOf(text) {
|
|
84
|
+
if (!text) return [];
|
|
85
|
+
return (text.endsWith("\n") ? text.slice(0, -1) : text).split("\n");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Why a snippet's text must not be sent, or null. Since 2026-11-09 the engine sends a snippet, not the
|
|
90
|
+
* whole flagged file (`source_form: "snippet"`, niro-local `audit/snippet.py`): the file with every line
|
|
91
|
+
* outside `snippet_lines` empty. Here, before it leaves: it has `source_line_count` lines, and every line
|
|
92
|
+
* outside the ranges is empty. niro-audit refuses such an item too (`SnippetCheck`).
|
|
93
|
+
*/
|
|
94
|
+
function snippetProblem(text, payload) {
|
|
95
|
+
const lines = linesOf(text);
|
|
96
|
+
if (lines.length !== payload.source_line_count) {
|
|
97
|
+
return `the snippet has ${lines.length} lines where the file has ${payload.source_line_count}`;
|
|
98
|
+
}
|
|
99
|
+
const ranges = Array.isArray(payload.snippet_lines) ? payload.snippet_lines : [];
|
|
100
|
+
const kept = (n) => ranges.some((r) => Array.isArray(r) && r[0] <= n && n <= r[1]);
|
|
101
|
+
for (let i = 0; i < lines.length; i++) {
|
|
102
|
+
if (lines[i] !== "" && !kept(i + 1)) return `line ${i + 1} is outside the snippet and not empty`;
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* A queued audit payload with author and company names removed from its source, explanation and
|
|
109
|
+
* every flag's matched_text. Its secrets were masked by the engine.
|
|
110
|
+
* `sha` stays the original file's hash: it is the server's dedup key. A payload with nothing
|
|
111
|
+
* to remove comes back byte for byte. Source that cannot be read throws, so the caller keeps
|
|
112
|
+
* the item queued instead of sending it unredacted.
|
|
113
|
+
*
|
|
114
|
+
* Throws too, keeping the item queued, when redaction would change the number of lines (every flag's
|
|
115
|
+
* line number would then point at the wrong text on the server), or when a snippet holds any line
|
|
116
|
+
* outside its ranges, before redaction or after.
|
|
117
|
+
*/
|
|
118
|
+
function redactItem(payload, ctx = {}) {
|
|
119
|
+
if (!payload || typeof payload !== "object") return payload;
|
|
120
|
+
const out = { ...payload };
|
|
121
|
+
const snippet = payload.source_form === "snippet";
|
|
122
|
+
if (typeof payload.compressed_source === "string") {
|
|
123
|
+
let original;
|
|
124
|
+
try {
|
|
125
|
+
original = zlib.gunzipSync(Buffer.from(payload.compressed_source, "base64")).toString("utf8");
|
|
126
|
+
} catch {
|
|
127
|
+
throw new Error("the flagged file's source could not be read, so it was not sent");
|
|
128
|
+
}
|
|
129
|
+
if (snippet && snippetProblem(original, payload)) {
|
|
130
|
+
throw new Error(`the snippet was not sent: ${snippetProblem(original, payload)}`);
|
|
131
|
+
}
|
|
132
|
+
const cleaned = redactSource(original, ctx);
|
|
133
|
+
if (linesOf(cleaned).length !== linesOf(original).length) {
|
|
134
|
+
throw new Error("removing names would have moved the source's lines, so it was not sent");
|
|
135
|
+
}
|
|
136
|
+
if (snippet && snippetProblem(cleaned, payload)) {
|
|
137
|
+
throw new Error(`the snippet was not sent: ${snippetProblem(cleaned, payload)}`);
|
|
138
|
+
}
|
|
139
|
+
if (cleaned !== original) {
|
|
140
|
+
out.compressed_source = zlib.gzipSync(Buffer.from(cleaned, "utf8")).toString("base64");
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (typeof payload.explanation === "string") out.explanation = redactSource(payload.explanation, ctx);
|
|
144
|
+
for (const [key, value] of Object.entries(payload)) {
|
|
145
|
+
if (!key.endsWith("_flags") || !Array.isArray(value)) continue;
|
|
146
|
+
out[key] = value.map((flag) =>
|
|
147
|
+
flag && typeof flag.matched_text === "string"
|
|
148
|
+
? { ...flag, matched_text: redactSource(flag.matched_text, ctx) }
|
|
149
|
+
: flag
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
return out;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
module.exports = { redactSource, redactItem, contextCompanies, linesOf, snippetProblem };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* What this release ships.
|
|
5
|
+
*
|
|
6
|
+
* `cloud` is the hosted product: the hosted MCP server (aiassistant.niroai.dev) behind
|
|
7
|
+
* `niro mcp serve`, and the cloud project commands (init, add, build, projects, ...). Off in the
|
|
8
|
+
* local-engine release (Divya, 2026-11-05: "mcp and cloud commands both should be a switch off in
|
|
9
|
+
* this release"): the bridge answers everything from the engine on this machine and the cloud
|
|
10
|
+
* commands are not registered. Sign-in, the plan's licence and code sharing still use Niro's
|
|
11
|
+
* services; they are the account, not the hosted graph.
|
|
12
|
+
*
|
|
13
|
+
* A constant, not an environment variable, so a published package cannot be switched back on by
|
|
14
|
+
* whoever runs it. Turning the cloud back on is an edit here and a release.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* `llmDocs` is LLM documentation: `niro docs-send`, the watcher's documentation round, the
|
|
18
|
+
* embedding model download, and every text telling a person or an assistant about
|
|
19
|
+
* `niro-local build --llm`, find_reusable_code or find_known_issues. Off in this release
|
|
20
|
+
* (Niraj, 2026-11-06: "turn off llm flag for now (do not expose it), we will add this flag and
|
|
21
|
+
* that's when we will handle it"); the engine has the same switch (niro-local
|
|
22
|
+
* `reuse.IN_THIS_RELEASE`). The code is kept: setting this to true brings it all back.
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* How a person or an assistant is told to rebuild a folder by hand. Without the cloud, the engine is the released one,
|
|
26
|
+
* which builds only with the licence niro-cli passes it, so a typed `niro-local build` is refused and `niro build`
|
|
27
|
+
* (niro-cli) is the command that works. A function, so it reads `cloud` when it is said.
|
|
28
|
+
*/
|
|
29
|
+
function rebuildCommand() {
|
|
30
|
+
return module.exports.cloud ? "niro-local build" : "niro build";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = { cloud: false, llmDocs: false, rebuildCommand };
|