omnirush 0.8.5 → 0.9.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 (49) hide show
  1. package/assets/CHANGELOG.md +85 -0
  2. package/assets/collect-once.ts +6 -5
  3. package/assets/extensions/omnirush/agents-lib.ts +196 -12
  4. package/assets/extensions/omnirush/agents.ts +97 -105
  5. package/assets/extensions/omnirush/auth.js +399 -115
  6. package/assets/extensions/omnirush/bgshell-lib.ts +400 -0
  7. package/assets/extensions/omnirush/bgshell.ts +392 -0
  8. package/assets/extensions/omnirush/collector.ts +103 -16
  9. package/assets/extensions/omnirush/commands.ts +14 -15
  10. package/assets/extensions/omnirush/deliveries.ts +145 -0
  11. package/assets/extensions/omnirush/guard/UPSTREAM +2 -0
  12. package/assets/extensions/omnirush/guard/git-command-policy.ts +885 -0
  13. package/assets/extensions/omnirush/guard-lib.ts +230 -0
  14. package/assets/extensions/omnirush/guard.ts +340 -0
  15. package/assets/extensions/omnirush/index.ts +12 -0
  16. package/assets/extensions/omnirush/pi-engine.ts +65 -1
  17. package/assets/extensions/omnirush/sota.ts +104 -47
  18. package/assets/extensions/omnirush/status-lib.ts +3 -0
  19. package/assets/extensions/omnirush/subagents-lib.ts +623 -0
  20. package/assets/extensions/omnirush/subagents.ts +305 -0
  21. package/assets/extensions/omnirush/swarm-lib.ts +142 -0
  22. package/assets/extensions/omnirush/swarm.ts +95 -0
  23. package/assets/extensions/omnirush/voice/capture.ts +502 -0
  24. package/assets/extensions/omnirush/voice/core/UPSTREAM +16 -0
  25. package/assets/extensions/omnirush/voice/core/file-source.ts +70 -0
  26. package/assets/extensions/omnirush/voice/core/index.ts +21 -0
  27. package/assets/extensions/omnirush/voice/core/keyterms.ts +117 -0
  28. package/assets/extensions/omnirush/voice/core/resample.ts +63 -0
  29. package/assets/extensions/omnirush/voice/core/segmenter.ts +231 -0
  30. package/assets/extensions/omnirush/voice/core/session.ts +403 -0
  31. package/assets/extensions/omnirush/voice/core/text.ts +81 -0
  32. package/assets/extensions/omnirush/voice/core/transcriber.ts +135 -0
  33. package/assets/extensions/omnirush/voice/core/types.ts +102 -0
  34. package/assets/extensions/omnirush/voice/core/wav.ts +95 -0
  35. package/assets/extensions/omnirush/voice/keys.ts +435 -0
  36. package/assets/extensions/omnirush/voice/kitty.ts +64 -0
  37. package/assets/extensions/omnirush/voice/pvrecorder-worker.cjs +43 -0
  38. package/assets/extensions/omnirush/voice/settings.ts +67 -0
  39. package/assets/extensions/omnirush/voice.ts +838 -0
  40. package/assets/extensions/omnirush/yolo-lib.ts +80 -0
  41. package/assets/extensions/omnirush/yolo.ts +85 -0
  42. package/package.json +6 -2
  43. package/scripts/brand-engine.js +526 -0
  44. package/scripts/build-all-packages.py +29 -1
  45. package/scripts/smoke-packages.py +32 -1
  46. package/src/bin.js +252 -53
  47. package/src/compat.js +272 -0
  48. package/src/lib.js +64 -0
  49. package/scripts/patch-pi-branding.js +0 -251
package/src/compat.js ADDED
@@ -0,0 +1,272 @@
1
+ // Compatibility with installs made before the 0.9.0 rebrand.
2
+ //
3
+ // Up to 0.8.x the agent core kept its data (settings.json, models.json,
4
+ // provider auth.json, sessions/, installed packages) in its upstream
5
+ // default folder, ~/.pi/agent, and read project settings from <project>/.pi.
6
+ // From 0.9.0 everything the user sees is named omnirush:
7
+ //
8
+ // agent data ~/.omnirush/agent (inside the state dir, so
9
+ // OMNIRUSH_DIR moves it too)
10
+ // project config <project>/.omnirush
11
+ // env variables OMNIRUSH_* (the core's PI_* names still work)
12
+ //
13
+ // This module holds the pure pieces the launcher uses to get there
14
+ // without losing anything:
15
+ //
16
+ // resolveAgentDir where the agent data lives this run (and the
17
+ // legacy folder to migrate from)
18
+ // migrateAgentDir one-time COPY of the legacy folder into the new
19
+ // one (the old folder is left untouched, so an
20
+ // older omnirush keeps working); never overwrites,
21
+ // merges the three JSON config files key by key,
22
+ // writes a marker so it runs once, resumes after a
23
+ // crash, and reports failure so the launcher can
24
+ // fall back to the legacy folder for that run
25
+ // projectConfigDirName the project config folder name for a cwd: a
26
+ // project that only has a legacy .pi folder keeps
27
+ // using it
28
+ // engineEnvAliases OMNIRUSH_* spellings of the core's PI_* settings
29
+
30
+ import fs from "node:fs";
31
+ import os from "node:os";
32
+ import path from "node:path";
33
+
34
+ /** Marker written into the new agent dir once the legacy data was copied. */
35
+ export const MIGRATION_MARKER = ".migrated.json";
36
+
37
+ /** Project config folder (the core's configDir after the branding patch). */
38
+ export const PROJECT_CONFIG_DIR = ".omnirush";
39
+ const LEGACY_PROJECT_CONFIG_DIR = ".pi";
40
+ /** Entries that make a project config folder live (the core's trust-gated resources). */
41
+ const PROJECT_RESOURCES = ["settings.json", "extensions", "skills", "prompts", "themes", "SYSTEM.md", "APPEND_SYSTEM.md"];
42
+
43
+ /** Top-level JSON files merged key by key when both folders have them (the new folder's keys win). */
44
+ const MERGED_JSON = new Set(["settings.json", "models.json", "auth.json"]);
45
+
46
+ /**
47
+ * The core's own PI_* settings a user may set, accepted as OMNIRUSH_<NAME>
48
+ * (the launcher hands them to the core under the name it reads). The
49
+ * original PI_<NAME> keeps working unchanged. PI_OFFLINE / PI_TELEMETRY
50
+ * are always forced by the launcher and are not listed.
51
+ */
52
+ export const ENGINE_ENV_NAMES = [
53
+ "CACHE_RETENTION",
54
+ "CLEAR_ON_SHRINK",
55
+ "EXPERIMENTAL",
56
+ "HARDWARE_CURSOR",
57
+ "HYPERLINKS",
58
+ "IMAGE_PROTOCOL",
59
+ "OAUTH_CALLBACK_HOST",
60
+ "PACKAGE_DIR",
61
+ "RADIUS_GATEWAY",
62
+ "SHARE_VIEWER_URL",
63
+ "SKIP_VERSION_CHECK",
64
+ "STARTUP_BENCHMARK",
65
+ "TIMING",
66
+ "TRUE_COLOR",
67
+ "TUI_DEBUG",
68
+ "TUI_DEBUG_REDRAW",
69
+ "TUI_ESC_TIMEOUT",
70
+ "TUI_WRITE_LOG",
71
+ ];
72
+
73
+ /**
74
+ * Where the agent data lives: OMNIRUSH_AGENT_DIR (or its pre-0.9 spelling
75
+ * OMNIRUSH_PI_AGENT_DIR, or the core's own OMNIRUSH_CODING_AGENT_DIR, which
76
+ * nested runs inherit) when set — then nothing is migrated — else
77
+ * <stateDir>/agent, with ~/.pi/agent as the legacy folder to migrate from.
78
+ */
79
+ export function resolveAgentDir(env = process.env, { stateDir, home = os.homedir() } = {}) {
80
+ const override = String(env.OMNIRUSH_AGENT_DIR || env.OMNIRUSH_PI_AGENT_DIR || env.OMNIRUSH_CODING_AGENT_DIR || "").trim();
81
+ if (override) return { dir: path.resolve(override), legacyDir: null };
82
+ const base = stateDir || path.join(home, ".omnirush");
83
+ return { dir: path.join(base, "agent"), legacyDir: path.join(home, ".pi", "agent") };
84
+ }
85
+
86
+ function isInside(parent, child, pathImpl) {
87
+ const relative = pathImpl.relative(parent, child);
88
+ return relative === "" || (!!relative && !relative.startsWith("..") && !pathImpl.isAbsolute(relative));
89
+ }
90
+
91
+ function writeAtomic(fsImpl, dest, data, mode) {
92
+ const tmp = `${dest}.${process.pid}.migrating`;
93
+ fsImpl.writeFileSync(tmp, data, mode === undefined ? undefined : { mode });
94
+ fsImpl.renameSync(tmp, dest);
95
+ }
96
+
97
+ function readJson(fsImpl, file) {
98
+ try {
99
+ const value = JSON.parse(fsImpl.readFileSync(file, "utf8"));
100
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
101
+ } catch {
102
+ return null;
103
+ }
104
+ }
105
+
106
+ /** Old keys fill the gaps of the new file; models.json merges its providers map the same way. */
107
+ function mergeJson(name, older, newer) {
108
+ const merged = { ...older, ...newer };
109
+ if (name === "models.json" && older.providers && typeof older.providers === "object") {
110
+ merged.providers = { ...older.providers, ...(newer.providers || {}) };
111
+ }
112
+ return merged;
113
+ }
114
+
115
+ /**
116
+ * A session file's header names its parent session by absolute path; a
117
+ * copied session points at the copied parent instead.
118
+ */
119
+ export function rebaseSessionHeader(content, from, to, pathImpl = path) {
120
+ const newline = content.indexOf("\n");
121
+ const first = newline === -1 ? content : content.slice(0, newline);
122
+ let header;
123
+ try {
124
+ header = JSON.parse(first);
125
+ } catch {
126
+ return content;
127
+ }
128
+ if (!header || header.type !== "session" || typeof header.parentSession !== "string") return content;
129
+ if (!isInside(from, header.parentSession, pathImpl)) return content;
130
+ header.parentSession = pathImpl.join(to, pathImpl.relative(from, header.parentSession));
131
+ return JSON.stringify(header) + (newline === -1 ? "" : content.slice(newline));
132
+ }
133
+
134
+ /**
135
+ * Copy the legacy agent folder into the new one, once. Never overwrites a
136
+ * file the new folder already has (the three JSON config files are merged
137
+ * instead), keeps modes and timestamps (the core orders sessions by them),
138
+ * skips lock files and our own extension folder (reinstalled at every
139
+ * launch), and writes MIGRATION_MARKER last: an interrupted copy resumes
140
+ * on the next start, a finished one never runs again.
141
+ *
142
+ * Returns { status: "migrated" | "already" | "none" | "failed", copied,
143
+ * merged, skipped, error? }.
144
+ */
145
+ export function migrateAgentDir({ from, to, fsImpl = fs, pathImpl = path, now = () => new Date(), onStart } = {}) {
146
+ const result = { status: "none", copied: 0, merged: 0, skipped: 0 };
147
+ const marker = pathImpl.join(to, MIGRATION_MARKER);
148
+ if (fsImpl.existsSync(marker)) return { ...result, status: "already" };
149
+ try {
150
+ fsImpl.mkdirSync(to, { recursive: true });
151
+ let legacy = null;
152
+ try {
153
+ legacy = from && pathImpl.resolve(from) !== pathImpl.resolve(to) ? fsImpl.statSync(from) : null;
154
+ } catch {
155
+ legacy = null;
156
+ }
157
+ if (legacy?.isDirectory()) {
158
+ onStart?.();
159
+ copyTree(from, to, "");
160
+ result.status = "migrated";
161
+ }
162
+ const record = { version: 1, from: legacy?.isDirectory() ? from : null, at: now().toISOString(), copied: result.copied, merged: result.merged, skipped: result.skipped };
163
+ writeAtomic(fsImpl, marker, JSON.stringify(record, null, 2) + "\n");
164
+ return result;
165
+ } catch (error) {
166
+ return { ...result, status: "failed", error };
167
+ }
168
+
169
+ function copyTree(srcDir, destDir, rel) {
170
+ for (const entry of fsImpl.readdirSync(srcDir, { withFileTypes: true })) {
171
+ const name = entry.name;
172
+ const relPath = rel ? `${rel}/${name}` : name;
173
+ if (name === MIGRATION_MARKER || name.endsWith(".lock") || name.endsWith(".migrating")) continue;
174
+ if (relPath === "extensions/omnirush") continue;
175
+ const src = pathImpl.join(srcDir, name);
176
+ const dest = pathImpl.join(destDir, name);
177
+ const stat = fsImpl.lstatSync(src);
178
+ if (stat.isDirectory()) {
179
+ fsImpl.mkdirSync(dest, { recursive: true });
180
+ copyTree(src, dest, relPath);
181
+ continue;
182
+ }
183
+ const exists = (() => {
184
+ try {
185
+ fsImpl.lstatSync(dest);
186
+ return true;
187
+ } catch {
188
+ return false;
189
+ }
190
+ })();
191
+ if (stat.isSymbolicLink()) {
192
+ if (exists) {
193
+ result.skipped++;
194
+ continue;
195
+ }
196
+ try {
197
+ fsImpl.symlinkSync(fsImpl.readlinkSync(src), dest);
198
+ result.copied++;
199
+ } catch {
200
+ result.skipped++; // e.g. Windows without symlink rights
201
+ }
202
+ continue;
203
+ }
204
+ if (!stat.isFile()) continue;
205
+ if (exists) {
206
+ if (!rel && MERGED_JSON.has(name)) {
207
+ const older = readJson(fsImpl, src);
208
+ const newer = readJson(fsImpl, dest);
209
+ if (older && newer) {
210
+ const merged = mergeJson(name, older, newer);
211
+ if (JSON.stringify(merged) !== JSON.stringify(newer)) {
212
+ writeAtomic(fsImpl, dest, JSON.stringify(merged, null, 2) + "\n", fsImpl.statSync(dest).mode & 0o777);
213
+ result.merged++;
214
+ continue;
215
+ }
216
+ }
217
+ }
218
+ result.skipped++;
219
+ continue;
220
+ }
221
+ const tmp = `${dest}.${process.pid}.migrating`;
222
+ if (relPath.startsWith("sessions/") && name.endsWith(".jsonl")) {
223
+ const content = fsImpl.readFileSync(src, "utf8");
224
+ fsImpl.writeFileSync(tmp, rebaseSessionHeader(content, from, to, pathImpl));
225
+ } else {
226
+ fsImpl.copyFileSync(src, tmp);
227
+ }
228
+ try {
229
+ fsImpl.chmodSync(tmp, stat.mode & 0o777);
230
+ } catch {
231
+ /* modes are advisory on Windows */
232
+ }
233
+ fsImpl.utimesSync(tmp, stat.atime, stat.mtime);
234
+ fsImpl.renameSync(tmp, dest);
235
+ result.copied++;
236
+ }
237
+ }
238
+ }
239
+
240
+ /**
241
+ * The project config folder the core should read in `cwd`: .omnirush, or
242
+ * the legacy .pi when the project has one with settings or resources and
243
+ * no .omnirush yet (the launcher passes it as OMNIRUSH_PROJECT_CONFIG_DIR).
244
+ */
245
+ export function projectConfigDirName(cwd, fsImpl = fs) {
246
+ try {
247
+ if (fsImpl.existsSync(path.join(cwd, PROJECT_CONFIG_DIR))) return PROJECT_CONFIG_DIR;
248
+ const legacy = path.join(cwd, LEGACY_PROJECT_CONFIG_DIR);
249
+ if (PROJECT_RESOURCES.some((entry) => fsImpl.existsSync(path.join(legacy, entry)))) return LEGACY_PROJECT_CONFIG_DIR;
250
+ } catch {
251
+ /* unreadable cwd: the default */
252
+ }
253
+ return PROJECT_CONFIG_DIR;
254
+ }
255
+
256
+ /**
257
+ * The core-facing variables for the OMNIRUSH_* spellings the user set:
258
+ * OMNIRUSH_<NAME> -> PI_<NAME> (the OMNIRUSH_ value wins over a PI_ one),
259
+ * and the pre-0.9 PI_CODING_AGENT_SESSION_DIR -> the OMNIRUSH_ name the
260
+ * rebranded core reads.
261
+ */
262
+ export function engineEnvAliases(env = process.env) {
263
+ const out = {};
264
+ for (const name of ENGINE_ENV_NAMES) {
265
+ const value = env[`OMNIRUSH_${name}`];
266
+ if (value !== undefined && value !== "") out[`PI_${name}`] = value;
267
+ }
268
+ if (env.PI_CODING_AGENT_SESSION_DIR && !env.OMNIRUSH_CODING_AGENT_SESSION_DIR) {
269
+ out.OMNIRUSH_CODING_AGENT_SESSION_DIR = env.PI_CODING_AGENT_SESSION_DIR;
270
+ }
271
+ return out;
272
+ }
package/src/lib.js CHANGED
@@ -403,3 +403,67 @@ export function managedToolTargets({ platform = process.platform, runtimeRoot }
403
403
  { tool: "rg", source: `${runtimeRoot}/bin/rg${ext}`, dest: `bin/rg${ext}` },
404
404
  ];
405
405
  }
406
+
407
+ // --- yolo mode ---------------------------------------------------------------
408
+ //
409
+ // `--yolo`, `/yolo on` (saved as "yolo": true in <state dir>/settings.json) or
410
+ // OMNIRUSH_YOLO=1: the project folder is trusted for the run (the core's
411
+ // --approve) and nothing asks for confirmation. Precedence: --yolo/--no-yolo,
412
+ // then OMNIRUSH_YOLO, then the saved setting. Default: off.
413
+
414
+ export const YOLO_BANNER = "yolo mode: no prompts, project trusted";
415
+
416
+ const TRUE_WORDS = new Set(["1", "true", "on", "yes"]);
417
+ const FALSE_WORDS = new Set(["0", "false", "off", "no"]);
418
+
419
+ /** OMNIRUSH_YOLO as true/false, or undefined when unset or unrecognised. */
420
+ export function parseYoloEnv(value) {
421
+ const v = String(value ?? "").trim().toLowerCase();
422
+ if (TRUE_WORDS.has(v)) return true;
423
+ if (FALSE_WORDS.has(v)) return false;
424
+ return undefined;
425
+ }
426
+
427
+ /**
428
+ * Resolve yolo mode for one launch. Strips --yolo/--no-yolo from `args`
429
+ * (never past `--`) and, when yolo is on for an agent run, adds the core's
430
+ * --approve unless the user already chose -a/--approve/-na/--no-approve.
431
+ * Returns { yolo, source, trusted, args }: `trusted` is false only when an
432
+ * explicit --no-approve wins over yolo's project trust.
433
+ */
434
+ export function resolveYolo(args, { env = process.env, setting } = {}) {
435
+ let flag;
436
+ const rest = [];
437
+ let passthrough = false;
438
+ for (const arg of args) {
439
+ if (!passthrough && arg === "--") passthrough = true;
440
+ if (!passthrough && arg === "--yolo") flag = true;
441
+ else if (!passthrough && arg === "--no-yolo") flag = false;
442
+ else rest.push(arg);
443
+ }
444
+ let yolo = false;
445
+ let source = "default";
446
+ const fromEnv = parseYoloEnv(env.OMNIRUSH_YOLO);
447
+ if (flag !== undefined) {
448
+ yolo = flag;
449
+ source = "flag";
450
+ } else if (fromEnv !== undefined) {
451
+ yolo = fromEnv;
452
+ source = "env";
453
+ } else if (setting === true) {
454
+ yolo = true;
455
+ source = "setting";
456
+ }
457
+ const subcommand = rest.length > 0 && PI_SUBCOMMANDS.has(rest[0]);
458
+ if (!yolo || subcommand) return { yolo: yolo && !subcommand, source, trusted: !subcommand && yolo, args: rest };
459
+ const end = rest.indexOf("--");
460
+ const scan = end < 0 ? rest : rest.slice(0, end);
461
+ if (scan.some((a) => a === "--no-approve" || a === "-na")) return { yolo, source, trusted: false, args: rest };
462
+ if (scan.some((a) => a === "--approve" || a === "-a")) return { yolo, source, trusted: true, args: rest };
463
+ return { yolo, source, trusted: true, args: ["--approve", ...rest] };
464
+ }
465
+
466
+ /** The one-line banner for a resolved yolo launch. */
467
+ export function yoloBanner({ trusted }) {
468
+ return trusted ? YOLO_BANNER : "yolo mode: no prompts (project not trusted: --no-approve)";
469
+ }
@@ -1,251 +0,0 @@
1
- #!/usr/bin/env node
2
- // Rebrand the vendored agent core (@earendil-works/pi-coding-agent) so
3
- // users see "omnirush", never "pi"/"Pi". Runs on postinstall (before the
4
- // bun download) and is safe to re-run (idempotent).
5
- //
6
- // Two mechanisms:
7
- // 1. package.json `piConfig.name = "omnirush"` — pi's supported rebrand
8
- // hook. APP_NAME/APP_TITLE (banner logo "omnirush v0.86.1", terminal
9
- // title, --help usage lines, auth hints, debug-log name, the
10
- // <APP>_CODING_AGENT_DIR env var) all derive from it, in dist and in
11
- // the runtime bundle. It also disables pi's official-distribution
12
- // first-time-setup wizard (upstream treats forks/rebrands that way).
13
- // 2. Literal string replacements across the installed dist/*.js (the
14
- // standalone files and the runtime bundle chunks) for the handful of
15
- // user-visible strings that do not go through APP_NAME.
16
- //
17
- // Never renamed: file paths, config/session directory names, function or
18
- // variable identifiers, env-var names (PI_OFFLINE, PI_TELEMETRY, ... are
19
- // the core's real env contract), or the `pi` admin-subcommand aliases —
20
- // only display strings.
21
- //
22
- // Fail-soft: a core update that moved a string logs a warning; it never
23
- // breaks `npm install`.
24
-
25
- import fs from "node:fs";
26
- import path from "node:path";
27
- import { fileURLToPath } from "node:url";
28
-
29
- const PKG = "@earendil-works/pi-coding-agent";
30
- const BRAND = "omnirush";
31
-
32
- // Display strings that do not flow through APP_NAME. `from` must match the
33
- // installed dist exactly; applied with replaceAll to every dist/**/*.js.
34
- export const REPLACEMENTS = [
35
- {
36
- label: "TUI banner onboarding line",
37
- from: "Pi can explain its own features and look up its docs. Ask it how to use or extend Pi.",
38
- to: "Omnirush can explain its own features and look up its docs. Ask it how to use or extend Omnirush.",
39
- },
40
- {
41
- label: "system prompt persona",
42
- from: "operating inside pi, a coding agent harness",
43
- to: "operating inside omnirush, a coding agent harness",
44
- },
45
- {
46
- label: "system prompt docs section header",
47
- from:
48
- "Pi documentation (read only when the user asks about pi itself, its SDK, extensions, themes, skills, or TUI):",
49
- to: "Omnirush documentation (read only when the user asks about the agent itself, its SDK, extensions, themes, skills, or TUI):",
50
- },
51
- {
52
- label: "system prompt docs reading rule",
53
- from: "When reading pi docs or examples",
54
- to: "When reading the bundled docs or examples",
55
- },
56
- {
57
- label: "system prompt packages doc reference",
58
- from: "pi packages (docs/packages.md)",
59
- to: "agent packages (docs/packages.md)",
60
- },
61
- {
62
- label: "system prompt topics rule",
63
- from: "When working on pi topics, read the docs and examples",
64
- to: "When working on agent topics, read the docs and examples",
65
- },
66
- {
67
- label: "system prompt read-fully rule",
68
- from: "Always read pi .md files completely",
69
- to: "Always read the agent .md files completely",
70
- },
71
- {
72
- label: "bug report persona",
73
- from: "about pi, the coding agent they are talking to",
74
- to: "about omnirush, the coding agent they are talking to",
75
- },
76
- {
77
- label: "bug report recipient",
78
- from: "Write a report for the pi developers",
79
- to: "Write a report for the omnirush developers",
80
- },
81
- {
82
- label: "--help update line",
83
- from: "Update pi, extensions, or model catalogs",
84
- to: "Update the agent, extensions, or model catalogs",
85
- },
86
- {
87
- label: "--help update alias token",
88
- from: "[source|self|pi]",
89
- to: "[source|self]",
90
- },
91
- {
92
- label: "update subcommand description",
93
- from: "Update pi only (self works as alias to pi)",
94
- to: "Update the agent only (self works as an alias)",
95
- },
96
- // Belt-and-braces against the pi.dev update banner (Foxtran's 0.73.1
97
- // report: a stale core showed "Update Available — pi.dev/changelog"
98
- // even though PI_OFFLINE should suppress the check). PI_OFFLINE=1 in
99
- // the spawn env stops the check; these rewrites make the banner
100
- // UNRENDERABLE even where PI_OFFLINE is absent (direct pi spawns,
101
- // older cores): showNewVersionNotification returns before its body
102
- // (the parameter is renamed so the replacement stays idempotent and
103
- // never re-matches), and the pi.dev changelog URL is blanked. The
104
- // parameter spelling differs between the standalone dist (readable)
105
- // and the minified runtime bundle chunk, hence the two variants; a
106
- // future core that renames the parameter just reports "missing"
107
- // (fail-soft) and we would re-derive the target.
108
- {
109
- label: "TUI update banner (standalone dist)",
110
- from: "showNewVersionNotification(release) {",
111
- to: "showNewVersionNotification(_release) { return;",
112
- },
113
- {
114
- label: "TUI update banner (runtime bundle chunk)",
115
- from: "showNewVersionNotification(release2){",
116
- to: "showNewVersionNotification(_r){return;",
117
- },
118
- {
119
- label: "pi.dev changelog URL",
120
- from: '"https://pi.dev/changelog"',
121
- to: '""',
122
- },
123
- ];
124
-
125
- function findPiRoot(startDir) {
126
- let dir = startDir;
127
- for (let i = 0; i < 8; i++) {
128
- const candidate = path.join(dir, "node_modules", PKG);
129
- if (fs.existsSync(path.join(candidate, "package.json"))) return candidate;
130
- const parent = path.dirname(dir);
131
- if (parent === dir) return null;
132
- dir = parent;
133
- }
134
- return null;
135
- }
136
-
137
- function listDistFiles(root) {
138
- const out = [];
139
- const stack = [root];
140
- while (stack.length > 0) {
141
- const dir = stack.pop();
142
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
143
- const full = path.join(dir, entry.name);
144
- if (entry.isDirectory()) stack.push(full);
145
- else if (entry.isFile() && entry.name.endsWith(".js")) out.push(full);
146
- }
147
- }
148
- return out;
149
- }
150
-
151
- /**
152
- * Apply the rebrand to the installed core. Returns a report:
153
- * { piRoot, packageJsonPatched, applied: [{label, files}], missing: [{label, reason}] }
154
- */
155
- export function applyBrandingPatches(startDir) {
156
- const piRoot = findPiRoot(startDir);
157
- if (!piRoot) {
158
- return { piRoot: null, packageJsonPatched: false, applied: [], missing: [{ label: "core", reason: `${PKG} not installed` }] };
159
- }
160
-
161
- // 1. The supported rebrand hook.
162
- let packageJsonPatched = false;
163
- const pkgPath = path.join(piRoot, "package.json");
164
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
165
- if (pkg.piConfig?.name !== BRAND) {
166
- pkg.piConfig = { ...(pkg.piConfig || {}), name: BRAND };
167
- fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
168
- packageJsonPatched = true;
169
- }
170
-
171
- // 2. Literal display strings, everywhere under dist/.
172
- const applied = [];
173
- const missing = [];
174
- const files = listDistFiles(path.join(piRoot, "dist"));
175
- for (const replacement of REPLACEMENTS) {
176
- let hits = 0;
177
- for (const file of files) {
178
- let content;
179
- try {
180
- content = fs.readFileSync(file, "utf8");
181
- } catch {
182
- continue;
183
- }
184
- if (!content.includes(replacement.from)) continue;
185
- fs.writeFileSync(file, content.replaceAll(replacement.from, replacement.to));
186
- hits++;
187
- }
188
- if (hits > 0) applied.push({ label: replacement.label, files: hits });
189
- else missing.push({ label: replacement.label, reason: "target string not found (already applied or core updated)" });
190
- }
191
- return { piRoot, packageJsonPatched, applied, missing };
192
- }
193
-
194
- function main() {
195
- const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
196
- const report = applyBrandingPatches(repoRoot);
197
- if (!report.piRoot) {
198
- console.log("omnirush: agent core not installed; skipped branding patch");
199
- return;
200
- // Version alignment: pi's self-reported version (banner/help) reads its
201
- // own package.json — rewrite it to the omnirush release version so the
202
- // product version, not upstream's, is what users and support see.
203
- try {
204
- const piPkgPath = path.join(report.piRoot, "package.json");
205
- const piPkg = JSON.parse(fs.readFileSync(piPkgPath, "utf8"));
206
- const ourVersion = JSON.parse(
207
- fs.readFileSync(path.join(repoRoot, "package.json"), "utf8"),
208
- ).version;
209
- if (piPkg.version !== ourVersion) {
210
- piPkg.version = ourVersion;
211
- fs.writeFileSync(piPkgPath, JSON.stringify(piPkg, null, 2) + "\n");
212
- report.versionAligned = ourVersion;
213
- }
214
- } catch {
215
- /* cosmetic — never fail the patch over version alignment */
216
- }
217
- }
218
- // Version alignment: pi's self-reported version (banner/help) reads its
219
- // own package.json — rewrite it to the omnirush release version so the
220
- // product version, not upstream's, is what users and support see.
221
- try {
222
- const piPkgPath = path.join(report.piRoot, "package.json");
223
- const piPkg = JSON.parse(fs.readFileSync(piPkgPath, "utf8"));
224
- const ourVersion = JSON.parse(
225
- fs.readFileSync(path.join(repoRoot, "package.json"), "utf8"),
226
- ).version;
227
- if (piPkg.version !== ourVersion) {
228
- piPkg.version = ourVersion;
229
- fs.writeFileSync(piPkgPath, JSON.stringify(piPkg, null, 2) + "\n");
230
- report.versionAligned = ourVersion;
231
- }
232
- } catch {
233
- /* cosmetic — never fail the patch over version alignment */
234
- }
235
- const patched = report.packageJsonPatched ? "package.json + " : "";
236
- const ver = report.versionAligned ? `; version aligned to ${report.versionAligned}` : "";
237
- console.log(
238
- `omnirush: branding applied (${patched}${report.applied.length} display strings)${ver}` +
239
- (report.missing.length > 0 ? `; ${report.missing.length} already-applied/absent` : ""),
240
- );
241
- }
242
-
243
- // CLI entry (postinstall). Imported as a module by tests — never exit there.
244
- if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
245
- try {
246
- main();
247
- } catch (error) {
248
- // Fail-soft: a cosmetic patch must never break `npm install`.
249
- console.warn("omnirush: branding patch skipped:", error?.message ?? error);
250
- }
251
- }