@niroai/niro 0.16.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.
Files changed (177) hide show
  1. package/CHANGELOG.md +149 -0
  2. package/README.md +123 -302
  3. package/bin/niro.js +12 -1
  4. package/docs/agent-antigravity.md +37 -0
  5. package/docs/agent-claude-code.md +48 -0
  6. package/docs/agent-claude-desktop.md +47 -0
  7. package/docs/agent-cline.md +42 -0
  8. package/docs/agent-codex.md +45 -0
  9. package/docs/agent-copilot-cli.md +42 -0
  10. package/docs/agent-cursor.md +44 -0
  11. package/docs/agent-factory.md +42 -0
  12. package/docs/agent-gemini-cli.md +42 -0
  13. package/docs/agent-goose.md +42 -0
  14. package/docs/agent-kiro.md +37 -0
  15. package/docs/agent-opencode.md +37 -0
  16. package/docs/agent-vscode.md +45 -0
  17. package/docs/agent-windsurf.md +38 -0
  18. package/docs/answer-shapes.md +69 -0
  19. package/docs/builds-and-updates.md +68 -0
  20. package/docs/cloud-README.md +372 -0
  21. package/docs/configuring-a-folder.md +51 -0
  22. package/docs/connect-ci.md +24 -0
  23. package/docs/connect-overview.md +20 -0
  24. package/docs/connect-review-bots.md +17 -0
  25. package/docs/connect-team-server.md +12 -0
  26. package/docs/connect-terminal.md +49 -0
  27. package/docs/connect-web-agents.md +9 -0
  28. package/docs/connect-your-ai-assistant.md +59 -175
  29. package/docs/coverage-audit-and-consent.md +51 -0
  30. package/docs/evaluate-what-niro-did.md +271 -0
  31. package/docs/faq-assistants-and-mcp.md +122 -0
  32. package/docs/faq-feedback-and-receipts.md +33 -0
  33. package/docs/faq-indexing.md +127 -0
  34. package/docs/faq-install-and-setup.md +103 -0
  35. package/docs/faq-licence-and-plans.md +86 -0
  36. package/docs/faq-login-and-account.md +69 -0
  37. package/docs/faq-offline.md +34 -0
  38. package/docs/faq-performance.md +33 -0
  39. package/docs/faq-privacy-and-security.md +113 -0
  40. package/docs/faq-uninstall.md +47 -0
  41. package/docs/faq-updates.md +59 -0
  42. package/docs/faq-worktrees-and-branches.md +59 -0
  43. package/docs/faq-wrong-or-missing-answers.md +275 -0
  44. package/docs/hosted-only.md +33 -0
  45. package/docs/how-the-graph-works.md +47 -0
  46. package/docs/index.md +157 -0
  47. package/docs/licence-and-plans.md +72 -0
  48. package/docs/lifecycle-debug.md +48 -0
  49. package/docs/lifecycle-onboarding.md +32 -0
  50. package/docs/lifecycle-overview.md +77 -0
  51. package/docs/lifecycle-plan-and-design.md +44 -0
  52. package/docs/lifecycle-review.md +30 -0
  53. package/docs/lifecycle-security-and-privacy.md +35 -0
  54. package/docs/lifecycle-test.md +28 -0
  55. package/docs/lifecycle-write-code.md +35 -0
  56. package/docs/llms-full.txt +8015 -0
  57. package/docs/llms.txt +126 -0
  58. package/docs/playbook-add-a-feature.md +108 -0
  59. package/docs/playbook-change-safely.md +93 -0
  60. package/docs/playbook-data-and-events.md +107 -0
  61. package/docs/playbook-debug-after-a-change.md +96 -0
  62. package/docs/playbook-debug-config.md +95 -0
  63. package/docs/playbook-debug-exception.md +124 -0
  64. package/docs/playbook-debug-failing-service-call.md +119 -0
  65. package/docs/playbook-debug-slow-path.md +80 -0
  66. package/docs/playbook-debug-wrong-value.md +137 -0
  67. package/docs/playbook-onboard.md +95 -0
  68. package/docs/playbook-review-a-change.md +72 -0
  69. package/docs/playbook-test-a-change.md +73 -0
  70. package/docs/reference-cli.md +113 -0
  71. package/docs/reference-config-files.md +42 -0
  72. package/docs/reference-engine-commands.md +56 -0
  73. package/docs/reference-environment-variables.md +89 -0
  74. package/docs/reference-exit-codes.md +67 -0
  75. package/docs/reference-limits.md +75 -0
  76. package/docs/reference-mcp-tools.md +326 -0
  77. package/docs/reference-messages.md +180 -0
  78. package/docs/secrets-and-masking.md +48 -0
  79. package/docs/supported-c.md +130 -0
  80. package/docs/supported-cpp.md +157 -0
  81. package/docs/supported-csharp.md +211 -0
  82. package/docs/supported-go.md +258 -0
  83. package/docs/supported-java.md +226 -0
  84. package/docs/supported-javascript.md +223 -0
  85. package/docs/supported-kotlin.md +124 -0
  86. package/docs/supported-overview.md +89 -0
  87. package/docs/supported-php.md +157 -0
  88. package/docs/supported-python.md +224 -0
  89. package/docs/supported-ruby.md +162 -0
  90. package/docs/supported-rust.md +150 -0
  91. package/docs/supported-swift.md +133 -0
  92. package/docs/supported-typescript.md +220 -0
  93. package/docs/tokens-speed-and-accuracy.md +99 -0
  94. package/docs/troubleshooting.md +42 -0
  95. package/docs/uninstall.md +74 -0
  96. package/docs/what-leaves-your-machine.md +71 -0
  97. package/docs/where-things-are-stored.md +59 -0
  98. package/docs/worktrees-and-branches.md +55 -0
  99. package/package.json +5 -3
  100. package/src/commands/add-repo.js +5 -0
  101. package/src/commands/auditSend.js +67 -0
  102. package/src/commands/build.js +6 -0
  103. package/src/commands/docsSend.js +73 -0
  104. package/src/commands/engine.js +120 -0
  105. package/src/commands/engineRoutes.js +105 -0
  106. package/src/commands/guide.js +254 -0
  107. package/src/commands/init.js +16 -7
  108. package/src/commands/initLocal.js +96 -0
  109. package/src/commands/journalSend.js +47 -0
  110. package/src/commands/login.js +89 -12
  111. package/src/commands/mcp.js +22 -6
  112. package/src/commands/plan.js +99 -0
  113. package/src/commands/repoPush.js +33 -0
  114. package/src/commands/requestIntegration.js +38 -0
  115. package/src/commands/share.js +56 -0
  116. package/src/commands/start.js +451 -371
  117. package/src/commands/status.js +57 -0
  118. package/src/commands/takeover.js +6 -0
  119. package/src/commands/uninstall.js +180 -0
  120. package/src/commands/upgrade.js +38 -8
  121. package/src/commands/watch.js +420 -6
  122. package/src/index.js +174 -3
  123. package/src/lib/agentApi.js +32 -1
  124. package/src/lib/api.js +20 -6
  125. package/src/lib/auditUpload.js +293 -0
  126. package/src/lib/branchDrift.js +149 -0
  127. package/src/lib/choose.js +116 -0
  128. package/src/lib/cloudGuard.js +74 -0
  129. package/src/lib/credentials.js +2 -2
  130. package/src/lib/docsMarked.js +55 -0
  131. package/src/lib/docsState.js +213 -0
  132. package/src/lib/docsUpload.js +481 -0
  133. package/src/lib/engine.js +619 -0
  134. package/src/lib/folderSync.js +2 -1
  135. package/src/lib/graphCheck.js +40 -0
  136. package/src/lib/guide.js +293 -0
  137. package/src/lib/guideSearch.js +252 -0
  138. package/src/lib/integrationRequest.js +89 -0
  139. package/src/lib/journalUpload.js +147 -0
  140. package/src/lib/languages.js +129 -0
  141. package/src/lib/lease.js +375 -0
  142. package/src/lib/localBuild.js +285 -0
  143. package/src/lib/localEngine.js +210 -0
  144. package/src/lib/localOnboard.js +124 -0
  145. package/src/lib/localWorktrees.js +243 -0
  146. package/src/lib/machineCheck.js +58 -0
  147. package/src/lib/mcpClients.js +10 -1
  148. package/src/lib/migrationNotice.js +118 -0
  149. package/src/lib/modelFetch.js +153 -0
  150. package/src/lib/nestedOutput.js +23 -9
  151. package/src/lib/onboardingLog.js +34 -0
  152. package/src/lib/planUsage.js +165 -0
  153. package/src/lib/progressBar.js +46 -0
  154. package/src/lib/prompt.js +1 -1
  155. package/src/lib/redact.js +155 -0
  156. package/src/lib/release.js +33 -0
  157. package/src/lib/repoPush.js +495 -0
  158. package/src/lib/secret.js +1 -1
  159. package/src/lib/service.js +372 -63
  160. package/src/lib/shareCode.js +367 -0
  161. package/src/lib/shareExclusions.js +114 -0
  162. package/src/lib/shareFiles.js +129 -0
  163. package/src/lib/spinner.js +9 -5
  164. package/src/lib/watchDaemon.js +33 -10
  165. package/src/lib/watchState.js +12 -1
  166. package/src/lib/windowsPaths.js +67 -0
  167. package/src/mcp/lineCheck.js +382 -0
  168. package/src/mcp/lineName.js +92 -0
  169. package/src/mcp/localBackend.js +751 -0
  170. package/src/mcp/localMode.js +383 -0
  171. package/src/mcp/localSkill.js +189 -0
  172. package/src/mcp/server.js +610 -79
  173. package/src/mcp/setup.js +519 -131
  174. package/src/mcp/shellSearchHook.js +509 -0
  175. package/src/mcp/teardown.js +173 -91
  176. package/src/mcp/usingNiro.js +39 -0
  177. 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 = " ".repeat(require("./nestedOutput").currentIndent()) + 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 };