omnirush 0.8.6 → 0.9.1

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 (54) hide show
  1. package/assets/CHANGELOG.md +120 -0
  2. package/assets/extensions/omnirush/agents-lib.ts +501 -63
  3. package/assets/extensions/omnirush/agents.ts +140 -115
  4. package/assets/extensions/omnirush/bgshell-lib.ts +432 -0
  5. package/assets/extensions/omnirush/bgshell.ts +392 -0
  6. package/assets/extensions/omnirush/capture/workspace-collector.ts +86 -22
  7. package/assets/extensions/omnirush/collector.ts +275 -43
  8. package/assets/extensions/omnirush/commands.ts +2 -0
  9. package/assets/extensions/omnirush/deliveries.ts +145 -0
  10. package/assets/extensions/omnirush/guard/UPSTREAM +2 -0
  11. package/assets/extensions/omnirush/guard/git-command-policy.ts +885 -0
  12. package/assets/extensions/omnirush/guard-lib.ts +230 -0
  13. package/assets/extensions/omnirush/guard.ts +340 -0
  14. package/assets/extensions/omnirush/index.ts +32 -6
  15. package/assets/extensions/omnirush/mcp.ts +103 -8
  16. package/assets/extensions/omnirush/memory-lib.ts +516 -0
  17. package/assets/extensions/omnirush/pi-engine.ts +115 -3
  18. package/assets/extensions/omnirush/plan-lib.ts +38 -9
  19. package/assets/extensions/omnirush/plan.ts +85 -30
  20. package/assets/extensions/omnirush/sota.ts +52 -4
  21. package/assets/extensions/omnirush/status-lib.ts +3 -0
  22. package/assets/extensions/omnirush/subagent-marker.ts +21 -0
  23. package/assets/extensions/omnirush/subagents-lib.ts +623 -0
  24. package/assets/extensions/omnirush/subagents.ts +305 -0
  25. package/assets/extensions/omnirush/swarm-lib.ts +142 -0
  26. package/assets/extensions/omnirush/swarm.ts +95 -0
  27. package/assets/extensions/omnirush/voice/capture.ts +502 -0
  28. package/assets/extensions/omnirush/voice/core/UPSTREAM +16 -0
  29. package/assets/extensions/omnirush/voice/core/file-source.ts +70 -0
  30. package/assets/extensions/omnirush/voice/core/index.ts +21 -0
  31. package/assets/extensions/omnirush/voice/core/keyterms.ts +117 -0
  32. package/assets/extensions/omnirush/voice/core/resample.ts +63 -0
  33. package/assets/extensions/omnirush/voice/core/segmenter.ts +231 -0
  34. package/assets/extensions/omnirush/voice/core/session.ts +403 -0
  35. package/assets/extensions/omnirush/voice/core/text.ts +81 -0
  36. package/assets/extensions/omnirush/voice/core/transcriber.ts +135 -0
  37. package/assets/extensions/omnirush/voice/core/types.ts +102 -0
  38. package/assets/extensions/omnirush/voice/core/wav.ts +95 -0
  39. package/assets/extensions/omnirush/voice/keys.ts +435 -0
  40. package/assets/extensions/omnirush/voice/kitty.ts +64 -0
  41. package/assets/extensions/omnirush/voice/pvrecorder-worker.cjs +43 -0
  42. package/assets/extensions/omnirush/voice/settings.ts +67 -0
  43. package/assets/extensions/omnirush/voice.ts +838 -0
  44. package/assets/extensions/omnirush/yolo-lib.ts +80 -0
  45. package/assets/extensions/omnirush/yolo.ts +85 -0
  46. package/package.json +7 -3
  47. package/scripts/brand-engine.js +526 -0
  48. package/scripts/build-all-packages.py +29 -1
  49. package/scripts/smoke-packages.py +33 -1
  50. package/src/bin.js +232 -33
  51. package/src/compat.js +272 -0
  52. package/src/lib.js +64 -0
  53. package/src/sessions.js +222 -0
  54. package/scripts/patch-pi-branding.js +0 -251
@@ -0,0 +1,526 @@
1
+ #!/usr/bin/env node
2
+ // Rebrand the vendored agent core (@earendil-works/pi-coding-agent) so
3
+ // users see "omnirush", never the core's upstream name. Runs on
4
+ // postinstall (before the bun download) and at package time
5
+ // (scripts/build-all-packages.py); safe to re-run (idempotent).
6
+ //
7
+ // Mechanisms, in order of preference:
8
+ // 1. package.json `piConfig` — the core's supported rebrand hook.
9
+ // `name: "omnirush"` drives APP_NAME/APP_TITLE (banner, terminal and
10
+ // process title, --help usage lines, auth hints, debug-log name, the
11
+ // OMNIRUSH_CODING_AGENT_DIR / _SESSION_DIR env vars) and disables the
12
+ // core's official-distribution first-run wizard. `configDir:
13
+ // ".omnirush"` names the project config folder (<project>/.omnirush)
14
+ // and every path the core prints (~/.omnirush/agent/...). The launcher
15
+ // always passes the agent dir explicitly (src/compat.js migrates the
16
+ // pre-0.9 folder).
17
+ // 2. The core's CHANGELOG.md (shown by /changelog and the "what's new"
18
+ // block after an update) is replaced by ours (assets/CHANGELOG.md).
19
+ // 3. Literal string replacements across the installed dist/**/*.js (the
20
+ // standalone files and the runtime bundle the launcher runs) for the
21
+ // display strings that do not go through APP_NAME, plus a few code
22
+ // shapes: the OMNIRUSH_PROJECT_CONFIG_DIR hook (legacy project
23
+ // folders), OMNIRUSH_* copies of the env the core exports to shell
24
+ // commands, AI_AGENT, the HTTP User-Agent and temp-file names.
25
+ //
26
+ // Not renamed (internal, never shown): package ids and imports, code
27
+ // identifiers, session entry/customType ids, telemetry attribute keys,
28
+ // protocol values third-party providers expect, and the PI_* env names the
29
+ // core reads (the launcher maps OMNIRUSH_* spellings onto them).
30
+ //
31
+ // Maintenance: after bumping the core, run `node scripts/brand-engine.js`
32
+ // and `node --test test/branding.test.js`; a target that moved is reported
33
+ // as missing (fail-soft here, a failing test there) — re-derive its `from`
34
+ // from the new dist. test/branding.test.js also scans the dist for
35
+ // user-visible upstream-name strings.
36
+
37
+ import fs from "node:fs";
38
+ import path from "node:path";
39
+ import { fileURLToPath } from "node:url";
40
+
41
+ const PKG = "@earendil-works/pi-coding-agent";
42
+ const BRAND = "omnirush";
43
+ export const BRAND_CONFIG_DIR = ".omnirush";
44
+ const OUR_CHANGELOG = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "assets", "CHANGELOG.md");
45
+
46
+ // `from` must match the installed dist exactly; applied with replaceAll to
47
+ // every dist/**/*.js. A `to` must never contain its own `from` (idempotency).
48
+ export const REPLACEMENTS = [
49
+ // --- project config folder: legacy <project>/.pi fallback -------------
50
+ {
51
+ label: "project config dir hook (bundle)",
52
+ from: 'CONFIG_DIR_NAME=pkg.piConfig?.configDir||".pi"',
53
+ to: 'CONFIG_DIR_NAME=process.env.OMNIRUSH_PROJECT_CONFIG_DIR||pkg.piConfig?.configDir||".pi"',
54
+ },
55
+ {
56
+ label: "project config dir hook (dist)",
57
+ from: 'CONFIG_DIR_NAME = pkg.piConfig?.configDir || ".pi"',
58
+ to: 'CONFIG_DIR_NAME = process.env.OMNIRUSH_PROJECT_CONFIG_DIR || pkg.piConfig?.configDir || ".pi"',
59
+ },
60
+
61
+ // --- nothing goes to the core's upstream developers ---------------------
62
+ // /bug (report upload) and /share (session upload) send user data to the
63
+ // core's upstream service: hidden from the command list/autocomplete and
64
+ // no-ops when typed. Crash notes no longer point at /bug. Install
65
+ // telemetry, analytics ids, the upstream version check, the upstream
66
+ // model-catalog overlay and the upstream Radius gateway provider are off
67
+ // regardless of PI_OFFLINE / PI_TELEMETRY (the launcher also sets those).
68
+ {
69
+ label: "hide /share and /bug from the command list (bundle)",
70
+ from:
71
+ '{name:"share",description:"Share session as a secret GitHub gist"},{name:"bug",description:"Report a bug to the Pi developers",argumentHint:"<description>"},',
72
+ to: "",
73
+ },
74
+ {
75
+ label: "hide /share and /bug from the command list (dist)",
76
+ from:
77
+ ' { name: "share", description: "Share session as a secret GitHub gist" },\n { name: "bug", description: "Report a bug to the Pi developers", argumentHint: "<description>" },\n',
78
+ to: "",
79
+ },
80
+ {
81
+ label: "/share is a no-op",
82
+ from: "async handleShareCommand(){",
83
+ to: 'async handleShareCommand(_off){return this.showWarning("/share is not available in omnirush");',
84
+ },
85
+ {
86
+ label: "/bug is a no-op",
87
+ from: "async handleBugCommand(hint){",
88
+ to: 'async handleBugCommand(_off){return this.showWarning("/bug is not available in omnirush");',
89
+ },
90
+ {
91
+ label: "no /bug hint after errors",
92
+ from: "suggestBugReport(){this.bugReportHintShown||",
93
+ to: "suggestBugReport(){return;this.bugReportHintShown||",
94
+ },
95
+ {
96
+ label: "startup crash note without /bug",
97
+ from: " Run /bug to report it; the crash details are attached automatically.",
98
+ to: "",
99
+ },
100
+ {
101
+ label: "crash exit note without /bug",
102
+ from: "crashReportInstructions(){return`To report this crash:",
103
+ to: 'crashReportInstructions(){return"The crash details were saved in the agent data folder (crashes.json).";`To report this crash:',
104
+ },
105
+ {
106
+ label: "install telemetry off",
107
+ from: "function isInstallTelemetryEnabled(settingsManager,telemetryEnv=process.env.PI_TELEMETRY){",
108
+ to: "function isInstallTelemetryEnabled(_s,_e){return!1;",
109
+ },
110
+ {
111
+ label: "install report off",
112
+ from: "reportInstallTelemetry(version2){",
113
+ to: "reportInstallTelemetry(_v){return;",
114
+ },
115
+ {
116
+ label: "analytics off",
117
+ from: "getEnableAnalytics(){return this.settings.enableAnalytics??!1}getTrackingId(){return this.settings.trackingId}",
118
+ to: "getEnableAnalytics(){return!1}getTrackingId(){return}",
119
+ },
120
+ {
121
+ label: "upstream version check off",
122
+ from: "async function getLatestPiRelease(currentVersion,options={}){if(process.env.PI_OFFLINE)return;",
123
+ to: "async function getLatestPiRelease(currentVersion,options={}){return;",
124
+ },
125
+ {
126
+ label: "upstream model-catalog overlay off",
127
+ from: "if(!await context.publish({update:()=>{dynamicModels=restored}})||!context.allowNetwork",
128
+ to: "if(!await context.publish({update:()=>{dynamicModels=restored}})||!0||!context.allowNetwork",
129
+ },
130
+ {
131
+ label: "upstream Radius gateway provider off",
132
+ from: 'providers=builtinProviders().map(provider=>provider.id==="radius"?provider:',
133
+ to: 'providers=builtinProviders().filter(provider=>provider.id!=="radius").map(provider=>provider.id==="radius"?provider:',
134
+ },
135
+
136
+ // --- TUI -----------------------------------------------------------------
137
+ {
138
+ label: "TUI banner onboarding line",
139
+ from: "Pi can explain its own features and look up its docs. Ask it how to use or extend Pi.",
140
+ to: "Omnirush can explain its own features and look up its docs. Ask it how to use or extend Omnirush.",
141
+ },
142
+ {
143
+ label: "untrusted-project warning",
144
+ from: "then restart pi.",
145
+ to: "then restart omnirush.",
146
+ },
147
+ {
148
+ label: "crash hint",
149
+ from: '"start pi and"',
150
+ to: '"start omnirush and"',
151
+ },
152
+ {
153
+ label: "tmux keys hint",
154
+ from: "Pi works best with csi-u.",
155
+ to: "Omnirush works best with csi-u.",
156
+ },
157
+ {
158
+ label: "/bug disclaimer",
159
+ from: "This report goes to the Pi developers (Earendil) and is not shared publicly. It includes your pi version,",
160
+ to: "This report goes to the agent core's upstream developers (Earendil) and is not shared publicly. It includes your omnirush version,",
161
+ },
162
+ {
163
+ label: "announcement easter egg",
164
+ from: '"pi has joined Earendil"',
165
+ to: '"The agent core has joined Earendil"',
166
+ },
167
+ {
168
+ label: "external editor note",
169
+ from: "Pi will resume when the editor exits.",
170
+ to: "Omnirush will resume when the editor exits.",
171
+ },
172
+ {
173
+ label: "first-run analytics text",
174
+ from: "and bugs within Pi.",
175
+ to: "and bugs within omnirush.",
176
+ },
177
+ {
178
+ label: "/share artifact title",
179
+ from: '"Pi session"',
180
+ to: '"Omnirush session"',
181
+ },
182
+
183
+ // --- system prompt (also what traces record) ---------------------------
184
+ {
185
+ label: "system prompt persona",
186
+ from: "operating inside pi, a coding agent harness",
187
+ to: "operating inside omnirush, a coding agent harness",
188
+ },
189
+ {
190
+ label: "system prompt docs section header",
191
+ from:
192
+ "Pi documentation (read only when the user asks about pi itself, its SDK, extensions, themes, skills, or TUI):",
193
+ to: "Omnirush documentation (read only when the user asks about the agent itself, its SDK, extensions, themes, skills, or TUI):",
194
+ },
195
+ {
196
+ // Inside a template literal in the core: no backticks in `to`.
197
+ label: "system prompt docs reading rule",
198
+ from: "When reading pi docs or examples",
199
+ to:
200
+ "The docs use the agent core's upstream name: when answering from them, call the CLI omnirush, its data folder ~/.omnirush/agent, the project folder .omnirush/ and use the OMNIRUSH_ environment prefix\n- When reading the bundled docs or examples",
201
+ },
202
+ {
203
+ label: "system prompt packages doc reference",
204
+ from: "pi packages (docs/packages.md)",
205
+ to: "agent packages (docs/packages.md)",
206
+ },
207
+ {
208
+ label: "system prompt topics rule",
209
+ from: "When working on pi topics, read the docs and examples",
210
+ to: "When working on agent topics, read the docs and examples",
211
+ },
212
+ {
213
+ label: "system prompt read-fully rule",
214
+ from: "Always read pi .md files completely",
215
+ to: "Always read the agent .md files completely",
216
+ },
217
+ {
218
+ label: "bug report persona",
219
+ from: "about pi, the coding agent they are talking to",
220
+ to: "about omnirush, the coding agent they are talking to",
221
+ },
222
+ {
223
+ label: "bug report recipient",
224
+ from: "Write a report for the pi developers",
225
+ to: "Write a report for the omnirush developers",
226
+ },
227
+
228
+ // --- CLI help and subcommand output ------------------------------------
229
+ {
230
+ label: "--help update line",
231
+ from: "Update pi, extensions, or model catalogs",
232
+ to: "Update omnirush, extensions, or model catalogs",
233
+ },
234
+ {
235
+ label: "--help update alias token",
236
+ from: "[source|self|pi]",
237
+ to: "[source|self]",
238
+ },
239
+ {
240
+ label: "update help: summary",
241
+ from: "Update pi, installed packages, or model catalogs.",
242
+ to: "Update omnirush, installed packages, or model catalogs.",
243
+ },
244
+ {
245
+ label: "update help: --self",
246
+ from: "Update pi only (default when no target is given)",
247
+ to: "Update omnirush only (default when no target is given)",
248
+ },
249
+ {
250
+ label: "update help: --all",
251
+ from: "Update pi and installed packages",
252
+ to: "Update omnirush and installed packages",
253
+ },
254
+ {
255
+ label: "update help: --force",
256
+ from: "Reinstall pi even if the current version is latest",
257
+ to: "Reinstall omnirush even if the current version is latest",
258
+ },
259
+ {
260
+ label: "update help: short form",
261
+ from: "update Update pi only",
262
+ to: "update Update omnirush only",
263
+ },
264
+ {
265
+ label: "update help: short form --all",
266
+ from: "Update pi and all extensions",
267
+ to: "Update omnirush and all extensions",
268
+ },
269
+ {
270
+ label: "update help: self alias",
271
+ from: "update pi Update pi only (self works as alias to pi)",
272
+ to: "update self Update omnirush only (same as --self)",
273
+ },
274
+ {
275
+ // What the 0.8.x patcher left behind (dev checkouts that were patched before).
276
+ label: "update help: self alias (0.8 patch)",
277
+ from: "update pi Update the agent only (self works as an alias)",
278
+ to: "update self Update omnirush only (same as --self)",
279
+ },
280
+ {
281
+ label: "update: managed install errors",
282
+ from: "managed Pi ",
283
+ to: "managed omnirush ",
284
+ },
285
+ {
286
+ label: "update: managed install smoke test",
287
+ from: "Managed Pi smoke test",
288
+ to: "Managed omnirush smoke test",
289
+ },
290
+ {
291
+ label: "update: binary download hint",
292
+ from: "Download from: https://github.com/earendil-works/pi/releases/latest",
293
+ to: "Update with: npm install -g omnirush@latest",
294
+ },
295
+ {
296
+ label: "auth subcommand usage",
297
+ from: "\n pi auth ",
298
+ to: "\n omnirush auth ",
299
+ },
300
+ {
301
+ label: "--help env: package dir",
302
+ from: " PI_PACKAGE_DIR - ",
303
+ to: " OMNIRUSH_PACKAGE_DIR - ",
304
+ },
305
+ {
306
+ label: "--help env: offline",
307
+ from: " PI_OFFLINE - ",
308
+ to: " OMNIRUSH_OFFLINE - ",
309
+ },
310
+ {
311
+ label: "--help env: telemetry",
312
+ from: " PI_TELEMETRY - ",
313
+ to: " OMNIRUSH_TELEMETRY - ",
314
+ },
315
+ {
316
+ label: "--help env: share viewer",
317
+ from: " PI_SHARE_VIEWER_URL - Base URL for /share command (default: https://pi.dev/session/)",
318
+ to: " OMNIRUSH_SHARE_VIEWER_URL - Base URL for /share command",
319
+ },
320
+ {
321
+ label: "--help --offline flag",
322
+ from: "(same as PI_OFFLINE=1)",
323
+ to: "(same as OMNIRUSH_OFFLINE=1)",
324
+ },
325
+
326
+ // --- environment the core sets for itself and for shell commands ------
327
+ {
328
+ label: "AI_AGENT (bundle)",
329
+ from: 'process.env.AI_AGENT="pi"',
330
+ to: 'process.env.AI_AGENT="omnirush"',
331
+ },
332
+ {
333
+ label: "AI_AGENT (dist)",
334
+ from: 'process.env.AI_AGENT = "pi"',
335
+ to: 'process.env.AI_AGENT = "omnirush"',
336
+ },
337
+ {
338
+ label: "OMNIRUSH_CODING_AGENT marker",
339
+ from: 'process.env.PI_CODING_AGENT="true"',
340
+ to: 'process.env.PI_CODING_AGENT=process.env.OMNIRUSH_CODING_AGENT="true"',
341
+ },
342
+ {
343
+ // Shell commands the agent runs see PI_SESSION_ID & co.; mirror each as
344
+ // OMNIRUSH_<NAME> (and drop a stale inherited one when the PI_ one is unset).
345
+ label: "OMNIRUSH_* session env for shell commands",
346
+ from: "ctx.thinkingLevel&&(env2.PI_REASONING_LEVEL=ctx.thinkingLevel)}let baseContext={command,cwd,env:env2}",
347
+ to:
348
+ 'ctx.thinkingLevel&&(env2.PI_REASONING_LEVEL=ctx.thinkingLevel)}for(let k of["SESSION_ID","SESSION_FILE","PROVIDER","MODEL","REASONING_LEVEL"])env2["PI_"+k]===void 0?delete env2["OMNIRUSH_"+k]:env2["OMNIRUSH_"+k]=env2["PI_"+k];let baseContext={command,cwd,env:env2}',
349
+ },
350
+
351
+ // --- HTTP User-Agent (the omnirush gateway sees it) --------------------
352
+ {
353
+ label: "provider User-Agent",
354
+ from: "`pi (${nodeOs.platform()}",
355
+ to: "`omnirush (${nodeOs.platform()}",
356
+ },
357
+ {
358
+ label: "provider User-Agent (browser)",
359
+ from: '"pi (browser)"',
360
+ to: '"omnirush (browser)"',
361
+ },
362
+ {
363
+ label: "core User-Agent",
364
+ from: "`pi/${version",
365
+ to: "`omnirush/${version",
366
+ },
367
+
368
+ // --- file names users see (tool output notes, editor, zip, crash log) --
369
+ { label: "bash output temp file", from: '"pi-bash"', to: '"omnirush-bash"' },
370
+ { label: "bash executor temp file", from: "`pi-bash-${id}.log`", to: "`omnirush-bash-${id}.log`" },
371
+ { label: "tool output temp file", from: '"pi-output"', to: '"omnirush-output"' },
372
+ { label: "powershell temp file", from: '"pi-powershell"', to: '"omnirush-powershell"' },
373
+ { label: "external editor temp dir", from: '"pi-editor-"', to: '"omnirush-editor-"' },
374
+ { label: "clipboard image file", from: "`pi-clipboard-${", to: "`omnirush-clipboard-${" },
375
+ { label: "WSL clipboard file", from: "`pi-wsl-clip-${", to: "`omnirush-wsl-clip-${" },
376
+ { label: "share temp dir", from: '"pi-share-"', to: '"omnirush-share-"' },
377
+ { label: "bug report zip", from: "`pi-bug-report-${id}.zip`", to: "`omnirush-bug-report-${id}.zip`" },
378
+ { label: "TUI crash log", from: '"pi-tui-crash.log"', to: '"omnirush-tui-crash.log"' },
379
+ { label: "TUI debug log", from: '"pi-tui-debug.log"', to: '"omnirush-tui-debug.log"' },
380
+
381
+ // --- update banner (belt-and-braces; the launcher sets PI_OFFLINE) -----
382
+ // showNewVersionNotification returns before its body (the parameter is
383
+ // renamed so the replacement never re-matches); the standalone dist and
384
+ // the minified bundle spell the parameter differently.
385
+ {
386
+ label: "TUI update banner (standalone dist)",
387
+ from: "showNewVersionNotification(release) {",
388
+ to: "showNewVersionNotification(_release) { return;",
389
+ },
390
+ {
391
+ label: "TUI update banner (runtime bundle chunk)",
392
+ from: "showNewVersionNotification(release2){",
393
+ to: "showNewVersionNotification(_r){return;",
394
+ },
395
+ {
396
+ label: "upstream changelog URL",
397
+ from: '"https://pi.dev/changelog"',
398
+ to: '""',
399
+ },
400
+ ];
401
+
402
+ function findPiRoot(startDir) {
403
+ let dir = startDir;
404
+ for (let i = 0; i < 8; i++) {
405
+ const candidate = path.join(dir, "node_modules", PKG);
406
+ if (fs.existsSync(path.join(candidate, "package.json"))) return candidate;
407
+ const parent = path.dirname(dir);
408
+ if (parent === dir) return null;
409
+ dir = parent;
410
+ }
411
+ return null;
412
+ }
413
+
414
+ function listDistFiles(root) {
415
+ const out = [];
416
+ const stack = [root];
417
+ while (stack.length > 0) {
418
+ const dir = stack.pop();
419
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
420
+ const full = path.join(dir, entry.name);
421
+ if (entry.isDirectory()) stack.push(full);
422
+ else if (entry.isFile() && entry.name.endsWith(".js")) out.push(full);
423
+ }
424
+ }
425
+ return out;
426
+ }
427
+
428
+ /**
429
+ * Apply the rebrand to the installed core. Returns a report:
430
+ * { piRoot, packageJsonPatched, changelogReplaced, applied: [{label, files}], missing: [{label, reason}] }
431
+ */
432
+ export function applyBrandingPatches(startDir, { version } = {}) {
433
+ const piRoot = findPiRoot(startDir);
434
+ if (!piRoot) {
435
+ return { piRoot: null, packageJsonPatched: false, applied: [], missing: [{ label: "core", reason: `${PKG} not installed` }] };
436
+ }
437
+
438
+ // 1. The supported rebrand hook (+ the product version: the banner and
439
+ // --version of the core read their own package.json).
440
+ let packageJsonPatched = false;
441
+ const pkgPath = path.join(piRoot, "package.json");
442
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
443
+ if (pkg.piConfig?.name !== BRAND || pkg.piConfig?.configDir !== BRAND_CONFIG_DIR || (version && pkg.version !== version)) {
444
+ pkg.piConfig = { ...(pkg.piConfig || {}), name: BRAND, configDir: BRAND_CONFIG_DIR };
445
+ if (version) pkg.version = version;
446
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
447
+ packageJsonPatched = true;
448
+ }
449
+
450
+ // 2. Our release notes in place of the core's.
451
+ let changelogReplaced = false;
452
+ try {
453
+ const ours = fs.readFileSync(OUR_CHANGELOG, "utf8");
454
+ const target = path.join(piRoot, "CHANGELOG.md");
455
+ let current = null;
456
+ try {
457
+ current = fs.readFileSync(target, "utf8");
458
+ } catch {
459
+ /* absent */
460
+ }
461
+ if (current !== ours) {
462
+ fs.writeFileSync(target, ours);
463
+ changelogReplaced = true;
464
+ }
465
+ } catch {
466
+ /* no changelog shipped: the core's stays */
467
+ }
468
+
469
+ // 3. Literal display strings, everywhere under dist/.
470
+ const applied = [];
471
+ const missing = [];
472
+ const files = listDistFiles(path.join(piRoot, "dist"));
473
+ const contents = new Map();
474
+ for (const file of files) {
475
+ try {
476
+ contents.set(file, fs.readFileSync(file, "utf8"));
477
+ } catch {
478
+ /* unreadable: skipped */
479
+ }
480
+ }
481
+ const dirty = new Set();
482
+ for (const replacement of REPLACEMENTS) {
483
+ let hits = 0;
484
+ for (const [file, content] of contents) {
485
+ if (!content.includes(replacement.from)) continue;
486
+ contents.set(file, content.replaceAll(replacement.from, replacement.to));
487
+ dirty.add(file);
488
+ hits++;
489
+ }
490
+ if (hits > 0) applied.push({ label: replacement.label, files: hits });
491
+ else missing.push({ label: replacement.label, reason: "target string not found (already applied or core updated)" });
492
+ }
493
+ for (const file of dirty) fs.writeFileSync(file, contents.get(file));
494
+ return { piRoot, packageJsonPatched, changelogReplaced, applied, missing };
495
+ }
496
+
497
+ function main() {
498
+ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
499
+ let version;
500
+ try {
501
+ version = JSON.parse(fs.readFileSync(path.join(repoRoot, "package.json"), "utf8")).version;
502
+ } catch {
503
+ /* cosmetic: keep the core's own version */
504
+ }
505
+ const report = applyBrandingPatches(repoRoot, { version });
506
+ if (!report.piRoot) {
507
+ console.log("omnirush: agent core not installed; skipped branding patch");
508
+ return;
509
+ }
510
+ const patched = report.packageJsonPatched ? "package.json + " : "";
511
+ const notes = report.changelogReplaced ? " + release notes" : "";
512
+ console.log(
513
+ `omnirush: branding applied (${patched}${report.applied.length} display strings${notes})` +
514
+ (report.missing.length > 0 ? `; ${report.missing.length} already-applied/absent` : ""),
515
+ );
516
+ }
517
+
518
+ // CLI entry (postinstall). Imported as a module by tests — never exit there.
519
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
520
+ try {
521
+ main();
522
+ } catch (error) {
523
+ // Fail-soft: a cosmetic patch must never break `npm install`.
524
+ console.warn("omnirush: branding patch skipped:", error?.message ?? error);
525
+ }
526
+ }
@@ -51,6 +51,33 @@ def find_bin(root, names):
51
51
 
52
52
  RELEASE_NAME = {"darwin-aarch64": "macos-arm64", "darwin-x64": "macos-x64"}
53
53
 
54
+ # Voice mode's native recorder (@picovoice/pvrecorder-node, an optional
55
+ # dependency: Apache-2.0, no access key). The npm package carries every
56
+ # platform's addon; each archive keeps only its own. linux-arm64 gets the
57
+ # Raspberry Pi aarch64 builds (the only arm64 Linux ones upstream ships);
58
+ # elsewhere voice falls back to the system recorders.
59
+ PVRECORDER = os.path.join("node_modules", "@picovoice", "pvrecorder-node")
60
+ PVRECORDER_LIBS = {
61
+ "linux-x64": ["linux/x86_64"],
62
+ "linux-arm64": ["raspberry-pi/cortex-a53-aarch64", "raspberry-pi/cortex-a72-aarch64", "raspberry-pi/cortex-a76-aarch64"],
63
+ "darwin-aarch64": ["mac/arm64"],
64
+ "darwin-x64": ["mac/x86_64"],
65
+ "windows-x64": ["windows/amd64"],
66
+ }
67
+
68
+
69
+ def vendor_pvrecorder(plat, stage):
70
+ src = os.path.join(REPO, PVRECORDER)
71
+ if not os.path.isdir(src):
72
+ raise SystemExit(f"missing voice recorder: {src} (run npm install first)")
73
+ dest = os.path.join(stage, PVRECORDER)
74
+ shutil.copytree(src, dest, ignore=shutil.ignore_patterns("lib", "src", "test", "*.map"))
75
+ for sub in PVRECORDER_LIBS[plat]:
76
+ lib = os.path.join(src, "lib", sub)
77
+ if not os.path.isdir(lib):
78
+ raise SystemExit(f"voice recorder has no {sub} build")
79
+ shutil.copytree(lib, os.path.join(dest, "lib", sub))
80
+
54
81
  for plat, cfg in PLATFORMS.items():
55
82
  if ONLY and plat not in ONLY:
56
83
  continue
@@ -80,11 +107,12 @@ for plat, cfg in PLATFORMS.items():
80
107
  if not os.path.isdir(nm_src):
81
108
  raise SystemExit(f"missing vendored agent runtime: {nm_src} (run npm install first)")
82
109
  shutil.copytree(nm_src, os.path.join(stage, "node_modules", "@earendil-works"), symlinks=True)
110
+ vendor_pvrecorder(plat, stage)
83
111
  shutil.copytree(os.path.join(REPO, "scripts"), os.path.join(stage, "scripts"))
84
112
  import subprocess
85
113
 
86
114
  subprocess.run(
87
- ["node", os.path.join(stage, "scripts", "patch-pi-branding.js")],
115
+ ["node", os.path.join(stage, "scripts", "brand-engine.js")],
88
116
  cwd=stage, check=True,
89
117
  )
90
118
  exe = "bun.exe" if plat == "windows-x64" else "bun"
@@ -27,14 +27,38 @@ REQUIRED_PATHS = [
27
27
  "package.json",
28
28
  "src/bin.js",
29
29
  "src/lib.js",
30
+ "src/compat.js",
31
+ "src/sessions.js",
30
32
  "scripts/postinstall.js",
31
- "scripts/patch-pi-branding.js",
33
+ "scripts/brand-engine.js",
32
34
  "assets/extensions/omnirush/index.ts",
35
+ "assets/CHANGELOG.md",
33
36
  "node_modules/@earendil-works/pi-coding-agent/package.json",
34
37
  "node_modules/@earendil-works/pi-coding-agent/dist",
35
38
  ".runtime/bin/bun",
36
39
  ]
37
40
 
41
+ # Voice mode's native recorder, one addon per archive (build-all-packages.py).
42
+ PVRECORDER_ADDON = {
43
+ "omnirush-cli-linux-x64.tar.gz": "linux/x86_64",
44
+ "omnirush-cli-linux-arm64.tar.gz": "raspberry-pi/cortex-a72-aarch64",
45
+ "omnirush-cli-macos-arm64.zip": "mac/arm64",
46
+ "omnirush-cli-macos-x64.zip": "mac/x86_64",
47
+ "omnirush-cli-windows-x64.zip": "windows/amd64",
48
+ }
49
+
50
+
51
+ def check_pvrecorder(name, names, root):
52
+ base = "node_modules/@picovoice/pvrecorder-node"
53
+ for required in (f"{base}/package.json", f"{base}/dist/index.js", f"{base}/lib/{PVRECORDER_ADDON[name]}/pv_recorder.node",
54
+ "assets/extensions/omnirush/voice.ts", "assets/extensions/omnirush/voice/pvrecorder-worker.cjs"):
55
+ if not has_entry(names, root, required):
56
+ fail(f"{name} lacks {required}")
57
+ others = {n for n in names if f"{base}/lib/" in n and n.endswith(".node") and PVRECORDER_ADDON[name] not in n
58
+ and not (name.endswith("linux-arm64.tar.gz") and "aarch64" in n)}
59
+ if others:
60
+ fail(f"{name} carries other platforms' recorder addons: {sorted(others)[:3]}")
61
+
38
62
 
39
63
  def fail(message):
40
64
  print(f"SMOKE FAIL: {message}")
@@ -86,6 +110,7 @@ def main():
86
110
  for required in REQUIRED_PATHS:
87
111
  if not has_entry(names, root, required):
88
112
  fail(f"linux-x64 archive lacks {required}")
113
+ check_pvrecorder("omnirush-cli-linux-x64.tar.gz", names, root)
89
114
  with tarfile.open(tgz, "r:gz") as t:
90
115
  t.extractall(work)
91
116
  extracted = os.path.join(work, root)
@@ -106,6 +131,12 @@ def main():
106
131
  if repo_version and repo_version not in out:
107
132
  fail(f"--version {out!r} does not carry the repo version {repo_version}")
108
133
  print(f"ok (run): linux-x64 bin.js --version -> {out}")
134
+ core_pkg = json.load(open(os.path.join(
135
+ extracted, "node_modules", "@earendil-works", "pi-coding-agent", "package.json")))
136
+ if core_pkg.get("piConfig", {}).get("name") != "omnirush" or \
137
+ core_pkg.get("piConfig", {}).get("configDir") != ".omnirush":
138
+ fail(f"vendored agent core is not branded: piConfig={core_pkg.get('piConfig')!r}")
139
+ print("ok (branding): vendored agent core carries the omnirush name and folders")
109
140
 
110
141
  # Remaining archives: structural smoke only (cross-packaged OSes).
111
142
  for name in sorted(EXPECTED):
@@ -117,6 +148,7 @@ def main():
117
148
  for required in REQUIRED_PATHS[:len(REQUIRED_PATHS) - 1] + [bun_name]:
118
149
  if not has_entry(names, root, required):
119
150
  fail(f"{name} lacks {required}")
151
+ check_pvrecorder(name, names, root)
120
152
  print(f"ok (structure): {name}")
121
153
  finally:
122
154
  shutil.rmtree(work, ignore_errors=True)