omnirush 0.1.1 → 0.3.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.
@@ -14,9 +14,12 @@ import path from "node:path";
14
14
  import { gatewayUrlForOrigin, omniDir, resolveOrigin } from "./auth";
15
15
  import { WorkspaceCollector } from "./collector-lib";
16
16
  import { sharedRefresher } from "./refresh";
17
- import { recordGatewayUsage } from "./usage";
17
+ import { recordGatewayUsage, storedUsageRecord } from "./usage";
18
18
 
19
- const SHUTDOWN_UPLOAD_BUDGET_MS = 15_000;
19
+ // End snapshots are full-tree (reproducible environment) and chunked, so
20
+ // shutdown can legitimately take minutes on big workspaces. Bounded so a
21
+ // wedged upload still can't wedge exit.
22
+ const SHUTDOWN_UPLOAD_BUDGET_MS = 120_000;
20
23
 
21
24
  /** pi session ids are UUIDs; still, never let a foreign format stall us. */
22
25
  function sanitizeSessionId(raw: string): string {
@@ -81,7 +84,8 @@ export default function (pi: any) {
81
84
  const usage = recordGatewayUsage(event?.headers);
82
85
  collector.recordTrace(sessionId, "provider.response", {
83
86
  status: event?.status,
84
- ...(usage ? { omnirush: usage } : {}),
87
+ // Stored records stay brand-neutral (the traces are the product).
88
+ ...(usage ? { agent: storedUsageRecord(usage) } : {}),
85
89
  });
86
90
  });
87
91
 
@@ -1,22 +1,38 @@
1
1
  /**
2
- * Omnirush commands: /grant (latest gateway grant/usage headers) and
3
- * /verify (prove the served model is genuine — full implementation lands
4
- * with the gateway's model-attestation endpoint).
2
+ * Omnirush commands: /grant (today's token grant + usage) and /verify
3
+ * (prove the served model is genuine — full implementation lands with the
4
+ * gateway's model-attestation endpoint), plus the footer status line that
5
+ * keeps grant/model info visible while the agent runs.
5
6
  */
6
7
 
7
8
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
8
- import { gatewayUsage } from "./usage";
9
+ import { deviceMe, resolveOrigin } from "./auth";
10
+ import { formatGrantLines, formatGrantStatusLine, gatewayUsage } from "./usage";
11
+
12
+ /** Fetch the signed-in identity + server-side usage, best effort. */
13
+ async function fetchIdentity(): Promise<any> {
14
+ const token = (process.env.OMNIRUSH_TOKEN || "").trim();
15
+ if (!token) return null;
16
+ try {
17
+ return await deviceMe(resolveOrigin(process.env), {
18
+ accessToken: token,
19
+ fetchImpl: globalThis.fetch,
20
+ });
21
+ } catch {
22
+ return null;
23
+ }
24
+ }
9
25
 
10
26
  export default function (pi: ExtensionAPI) {
11
27
  pi.registerCommand("grant", {
12
28
  description: "Show today's Omnirush token grant and usage",
13
29
  handler: async (ctx: any) => {
14
30
  const usage = gatewayUsage();
31
+ const me = await fetchIdentity();
32
+ const sessionId = ctx?.sessionManager?.getSessionId?.() ?? undefined;
33
+ const lines = formatGrantLines({ usage, me, sessionId });
15
34
  ctx.ui?.notify?.(
16
- usage
17
- ? `Omnirush gateway usage (latest x-omnirush-* headers): ${JSON.stringify(usage)}`
18
- : `Omnirush: no gateway usage seen yet. Provider: ${process.env.PI_PROVIDER ?? "omnirush"}, ` +
19
- `model: ${process.env.PI_MODEL ?? "gpt-6-astra"}.`,
35
+ ["Omnirush usage:", ...lines.map((line) => ` ${line}`)].join("\n"),
20
36
  );
21
37
  },
22
38
  });
@@ -31,4 +47,14 @@ export default function (pi: ExtensionAPI) {
31
47
  );
32
48
  },
33
49
  });
50
+
51
+ // Footer status: grant remaining + served model, refreshed on every
52
+ // provider response (the sota tap records the headers into the shared
53
+ // usage store before this event fires) and on model switches.
54
+ const refreshStatus = (ctx: any) => {
55
+ const line = formatGrantStatusLine(gatewayUsage());
56
+ if (line) ctx?.ui?.setStatus?.("omnirush-grant", line);
57
+ };
58
+ pi.on("after_provider_response", (_event: any, ctx: any) => refreshStatus(ctx));
59
+ pi.on("model_select", (_event: any, ctx: any) => refreshStatus(ctx));
34
60
  }
@@ -24,25 +24,39 @@ import {
24
24
  formatSotaWarning,
25
25
  servedModelFromSseData,
26
26
  } from "./sota-lib";
27
+ import { recordGatewayUsage } from "./usage";
27
28
 
28
29
  const PROVIDER_ID = "omnirush";
29
30
  const GATEWAY_DEFAULT = "https://omnirush.ai/omnirush/v1";
30
31
  const STRICT_EXIT_CODE = 3;
31
32
 
32
33
  // Must mirror the models.json payload bin.js installs (src/lib.js
33
- // modelsConfig) so provider composition keeps serving the same catalog.
34
- // The `api` field is load-bearing: provider composition routes streams to
35
- // this base only when the model's api matches one of base.getModels().
36
- const CATALOG_MODEL = {
37
- id: "gpt-6-astra",
38
- name: "GPT-6 Astra",
39
- api: "openai-responses",
40
- reasoning: true,
41
- input: ["text", "image"],
42
- cost: { input: 10, output: 50, cacheRead: 2.5, cacheWrite: 12.5 },
43
- contextWindow: 400000,
44
- compat: { supportsMaxOutputTokens: false },
45
- };
34
+ // modelsConfig / MODELS) so provider composition keeps serving the same
35
+ // catalog. The `api` field is load-bearing: provider composition routes
36
+ // streams to this base only when the model's api matches one of
37
+ // base.getModels().
38
+ const CATALOG_MODELS = [
39
+ {
40
+ id: "gpt-6-astra",
41
+ name: "GPT-6 Astra",
42
+ api: "openai-responses",
43
+ reasoning: true,
44
+ input: ["text", "image"],
45
+ cost: { input: 10, output: 50, cacheRead: 2.5, cacheWrite: 12.5 },
46
+ contextWindow: 400000,
47
+ compat: { supportsMaxOutputTokens: false },
48
+ },
49
+ {
50
+ id: "gpt-5.6-sol",
51
+ name: "GPT-5.6 Sol",
52
+ api: "openai-responses",
53
+ reasoning: true,
54
+ input: ["text", "image"],
55
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
56
+ contextWindow: 400000,
57
+ compat: { supportsMaxOutputTokens: false },
58
+ },
59
+ ];
46
60
 
47
61
  function warnStderr(line: string): void {
48
62
  try {
@@ -123,6 +137,14 @@ function tapFetch(
123
137
  return async (input: any, init: any) => {
124
138
  let response = await baseFetch(input, init);
125
139
  debug(`provider fetch ${typeof input === "string" ? input : input?.url} -> ${response?.status}`);
140
+ // Capture the gateway's grant/usage headers (x-omnirush-* plus the
141
+ // x-ratelimit-*-tokens grant pair) straight from the raw response —
142
+ // guaranteed availability here, independent of pi's event plumbing.
143
+ try {
144
+ recordGatewayUsage(response?.headers);
145
+ } catch {
146
+ /* usage capture must never break the request */
147
+ }
126
148
  if (response?.status === 401) {
127
149
  const tokenUsed = bearerFromInit(init) || bearerFromInit(input);
128
150
  debug(`401 seen; bearer present: ${Boolean(tokenUsed)}`);
@@ -214,7 +236,7 @@ export default function (pi: any) {
214
236
  },
215
237
  },
216
238
  },
217
- getModels: () => [CATALOG_MODEL],
239
+ getModels: () => CATALOG_MODELS.map((model) => ({ ...model })),
218
240
  stream: streamWith(false),
219
241
  streamSimple: streamWith(true),
220
242
  });
@@ -1,16 +1,54 @@
1
- // Latest x-omnirush-* gateway headers (grant/usage info the manager
2
- // attaches to /v1 responses). Kept for /grant and trace events; never
3
- // contains credentials.
1
+ // Latest gateway grant/usage state, captured from response headers the
2
+ // manager attaches to /v1 responses. Kept for /grant and the footer status
3
+ // line; never contains credentials.
4
+ //
5
+ // Live header contract (verified 2026-09-22 against the production
6
+ // gateway, omnirush/manager gateway.py):
7
+ // x-omnirush-model - resolved request model
8
+ // x-omnirush-effort - requested reasoning effort (set when present)
9
+ // x-ratelimit-limit-tokens - daily grant size
10
+ // x-ratelimit-remaining-tokens - grant remaining after settlement lag
4
11
 
5
- let latest: Record<string, string> | null = null;
12
+ export const GRANT_HEADER_NAMES = [
13
+ "x-ratelimit-limit-tokens",
14
+ "x-ratelimit-remaining-tokens",
15
+ ];
6
16
 
7
- export function recordGatewayUsage(headers: unknown): Record<string, string> | null {
8
- if (!headers || typeof headers !== "object") return null;
17
+ function* headerPairs(headers: unknown): Generator<[string, string]> {
18
+ if (!headers) return;
19
+ if (typeof (headers as any).forEach === "function") {
20
+ // Fetch Headers instance (the sota tap sees the raw Response).
21
+ const pairs: [string, string][] = [];
22
+ (headers as Headers).forEach((value, name) => pairs.push([name, value]));
23
+ for (const pair of pairs) yield pair;
24
+ return;
25
+ }
26
+ if (typeof headers === "object") {
27
+ for (const [name, value] of Object.entries(headers as Record<string, unknown>)) {
28
+ if (value !== undefined && value !== null) yield [name, String(value)];
29
+ }
30
+ }
31
+ }
32
+
33
+ /** Extract the gateway usage headers from a Headers object or plain record. */
34
+ export function pickGatewayUsage(headers: unknown): Record<string, string> {
9
35
  const found: Record<string, string> = {};
10
- for (const [name, value] of Object.entries(headers as Record<string, unknown>)) {
11
- if (!name.toLowerCase().startsWith("x-omnirush-")) continue;
12
- found[name] = String(value);
36
+ for (const [name, value] of headerPairs(headers)) {
37
+ const lower = name.toLowerCase();
38
+ if (
39
+ lower.startsWith("x-omnirush-") ||
40
+ GRANT_HEADER_NAMES.includes(lower)
41
+ ) {
42
+ found[lower] = value;
43
+ }
13
44
  }
45
+ return found;
46
+ }
47
+
48
+ let latest: Record<string, string> | null = null;
49
+
50
+ export function recordGatewayUsage(headers: unknown): Record<string, string> | null {
51
+ const found = pickGatewayUsage(headers);
14
52
  const hasAny = Object.keys(found).length > 0;
15
53
  latest = hasAny ? found : latest;
16
54
  return hasAny ? found : null;
@@ -19,3 +57,113 @@ export function recordGatewayUsage(headers: unknown): Record<string, string> | n
19
57
  export function gatewayUsage(): Record<string, string> | null {
20
58
  return latest;
21
59
  }
60
+
61
+ /**
62
+ * Usage record as embedded into STORED trace events: gateway header
63
+ * names are normalized so the stored artifacts carry no brand strings —
64
+ * "x-omnirush-model" -> "model", "x-omnirush-effort" -> "effort";
65
+ * generic headers (x-ratelimit-*) keep their names.
66
+ */
67
+ export function storedUsageRecord(usage: Record<string, string>): Record<string, string> {
68
+ const stored: Record<string, string> = {};
69
+ for (const [name, value] of Object.entries(usage)) {
70
+ const lower = name.toLowerCase();
71
+ const key = lower.startsWith("x-omnirush-") ? lower.slice("x-omnirush-".length) : lower;
72
+ stored[key] = value;
73
+ }
74
+ return stored;
75
+ }
76
+
77
+ /** Ints from the grant header pair, when both are present. */
78
+ export function grantFromUsage(usage: Record<string, string> | null): {
79
+ limitTokens: number;
80
+ remainingTokens: number;
81
+ } | null {
82
+ const limit = Number(usage?.["x-ratelimit-limit-tokens"]);
83
+ const remaining = Number(usage?.["x-ratelimit-remaining-tokens"]);
84
+ if (!Number.isFinite(limit) || !Number.isFinite(remaining)) return null;
85
+ return { limitTokens: limit, remainingTokens: remaining };
86
+ }
87
+
88
+ /** Compact human token count: 1.0B / 997.2M / 12.3k / 972. */
89
+ export function formatTokenCount(value: number): string {
90
+ if (!Number.isFinite(value)) return "?";
91
+ const abs = Math.abs(value);
92
+ if (abs >= 1e9) return `${(value / 1e9).toFixed(1)}B`;
93
+ if (abs >= 1e6) return `${(value / 1e6).toFixed(1)}M`;
94
+ if (abs >= 1e3) return `${(value / 1e3).toFixed(1)}k`;
95
+ return String(Math.round(value));
96
+ }
97
+
98
+ function intOr(value: unknown): number | null {
99
+ const n = Number(value);
100
+ return Number.isFinite(n) ? n : null;
101
+ }
102
+
103
+ /**
104
+ * The lines /grant shows. Everything is best-effort: header data when the
105
+ * agent has made a request, /device/me identity+usage otherwise.
106
+ */
107
+ export function formatGrantLines({
108
+ usage,
109
+ me,
110
+ sessionId,
111
+ }: {
112
+ usage: Record<string, string> | null;
113
+ me?: any;
114
+ sessionId?: string;
115
+ }): string[] {
116
+ const lines: string[] = [];
117
+ const model =
118
+ usage?.["x-omnirush-model"] ?? me?.current_model ?? null;
119
+ const effort = usage?.["x-omnirush-effort"] ?? null;
120
+ lines.push(`model (last turn): ${model ?? "no request yet"}`);
121
+ if (effort) lines.push(`reasoning effort: ${effort}`);
122
+
123
+ const fromHeaders = grantFromUsage(usage);
124
+ const meUsage = me?.usage ?? null;
125
+ const limit =
126
+ fromHeaders?.limitTokens ?? intOr(meUsage?.token_limit) ?? null;
127
+ const remaining =
128
+ fromHeaders?.remainingTokens ?? intOr(meUsage?.remaining_tokens) ?? null;
129
+ const usedFromMe = intOr(meUsage?.used_tokens);
130
+ const used =
131
+ usedFromMe ??
132
+ (limit !== null && remaining !== null ? limit - remaining : null);
133
+
134
+ if (limit !== null && remaining !== null) {
135
+ lines.push(
136
+ `grant: ${formatTokenCount(remaining)} of ${formatTokenCount(limit)} tokens remaining` +
137
+ (used !== null ? ` (used ${formatTokenCount(used)})` : ""),
138
+ );
139
+ } else if (used !== null) {
140
+ lines.push(`grant used: ${formatTokenCount(used)} tokens`);
141
+ }
142
+ if (meUsage?.grant_date) lines.push(`grant date: ${meUsage.grant_date}`);
143
+
144
+ const devices = Array.isArray(me?.devices) ? me.devices.length : null;
145
+ if (devices !== null) {
146
+ lines.push(`devices signed in: ${devices}`);
147
+ }
148
+ if (sessionId) {
149
+ lines.push(`session: ${sessionId.slice(0, 8)}`);
150
+ }
151
+ return lines;
152
+ }
153
+
154
+ /** One-line footer summary for the TUI status area, or null with no data. */
155
+ export function formatGrantStatusLine(
156
+ usage: Record<string, string> | null,
157
+ ): string | null {
158
+ const fromHeaders = grantFromUsage(usage);
159
+ const model = usage?.["x-omnirush-model"];
160
+ if (!fromHeaders && !model) return null;
161
+ const parts: string[] = [];
162
+ if (fromHeaders) {
163
+ parts.push(
164
+ `${formatTokenCount(fromHeaders.remainingTokens)} tok left`,
165
+ );
166
+ }
167
+ if (model) parts.push(model);
168
+ return parts.length > 0 ? parts.join(" · ") : null;
169
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omnirush",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "description": "Omnirush \u2014 free daily tokens for the most powerful coding model on earth.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -12,7 +12,8 @@
12
12
  },
13
13
  "scripts": {
14
14
  "test": "node --test",
15
- "postinstall": "node scripts/postinstall.js"
15
+ "test:bun": "if [ -x .runtime/bin/bun ]; then .runtime/bin/bun test test/; else bun test test/; fi",
16
+ "postinstall": "node scripts/patch-pi-branding.js && node scripts/postinstall.js"
16
17
  },
17
18
  "dependencies": {
18
19
  "@earendil-works/pi-coding-agent": "0.86.1"
@@ -14,6 +14,17 @@ elif [ "$PLATFORM" = "windows-x64" ]; then
14
14
  else
15
15
  cp ".runtime/bun-$PLATFORM/bin/bun" "$STAGE/.runtime/bin/bun"
16
16
  fi
17
+ # Search tools (fd, rg): bundled so the agent never attempts its own
18
+ # GitHub download at startup.
19
+ TOOL_EXT=""
20
+ [ "$PLATFORM" = "windows-x64" ] && TOOL_EXT=".exe"
21
+ for tool in fd rg; do
22
+ if [ -f ".runtime/bin/$tool$TOOL_EXT" ]; then
23
+ cp ".runtime/bin/$tool$TOOL_EXT" "$STAGE/.runtime/bin/$tool$TOOL_EXT"
24
+ else
25
+ echo "warning: .runtime/bin/$tool$TOOL_EXT missing — run scripts/postinstall.js first" >&2
26
+ fi
27
+ done
17
28
  if [[ "$PLATFORM" == windows* ]]; then
18
29
  zip -q -r "$STAGE.zip" "$STAGE"
19
30
  else
@@ -0,0 +1,224 @@
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
+ ];
97
+
98
+ function findPiRoot(startDir) {
99
+ let dir = startDir;
100
+ for (let i = 0; i < 8; i++) {
101
+ const candidate = path.join(dir, "node_modules", PKG);
102
+ if (fs.existsSync(path.join(candidate, "package.json"))) return candidate;
103
+ const parent = path.dirname(dir);
104
+ if (parent === dir) return null;
105
+ dir = parent;
106
+ }
107
+ return null;
108
+ }
109
+
110
+ function listDistFiles(root) {
111
+ const out = [];
112
+ const stack = [root];
113
+ while (stack.length > 0) {
114
+ const dir = stack.pop();
115
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
116
+ const full = path.join(dir, entry.name);
117
+ if (entry.isDirectory()) stack.push(full);
118
+ else if (entry.isFile() && entry.name.endsWith(".js")) out.push(full);
119
+ }
120
+ }
121
+ return out;
122
+ }
123
+
124
+ /**
125
+ * Apply the rebrand to the installed core. Returns a report:
126
+ * { piRoot, packageJsonPatched, applied: [{label, files}], missing: [{label, reason}] }
127
+ */
128
+ export function applyBrandingPatches(startDir) {
129
+ const piRoot = findPiRoot(startDir);
130
+ if (!piRoot) {
131
+ return { piRoot: null, packageJsonPatched: false, applied: [], missing: [{ label: "core", reason: `${PKG} not installed` }] };
132
+ }
133
+
134
+ // 1. The supported rebrand hook.
135
+ let packageJsonPatched = false;
136
+ const pkgPath = path.join(piRoot, "package.json");
137
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
138
+ if (pkg.piConfig?.name !== BRAND) {
139
+ pkg.piConfig = { ...(pkg.piConfig || {}), name: BRAND };
140
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
141
+ packageJsonPatched = true;
142
+ }
143
+
144
+ // 2. Literal display strings, everywhere under dist/.
145
+ const applied = [];
146
+ const missing = [];
147
+ const files = listDistFiles(path.join(piRoot, "dist"));
148
+ for (const replacement of REPLACEMENTS) {
149
+ let hits = 0;
150
+ for (const file of files) {
151
+ let content;
152
+ try {
153
+ content = fs.readFileSync(file, "utf8");
154
+ } catch {
155
+ continue;
156
+ }
157
+ if (!content.includes(replacement.from)) continue;
158
+ fs.writeFileSync(file, content.replaceAll(replacement.from, replacement.to));
159
+ hits++;
160
+ }
161
+ if (hits > 0) applied.push({ label: replacement.label, files: hits });
162
+ else missing.push({ label: replacement.label, reason: "target string not found (already applied or core updated)" });
163
+ }
164
+ return { piRoot, packageJsonPatched, applied, missing };
165
+ }
166
+
167
+ function main() {
168
+ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
169
+ const report = applyBrandingPatches(repoRoot);
170
+ if (!report.piRoot) {
171
+ console.log("omnirush: agent core not installed; skipped branding patch");
172
+ return;
173
+ // Version alignment: pi's self-reported version (banner/help) reads its
174
+ // own package.json — rewrite it to the omnirush release version so the
175
+ // product version, not upstream's, is what users and support see.
176
+ try {
177
+ const piPkgPath = path.join(report.piRoot, "package.json");
178
+ const piPkg = JSON.parse(fs.readFileSync(piPkgPath, "utf8"));
179
+ const ourVersion = JSON.parse(
180
+ fs.readFileSync(path.join(repoRoot, "package.json"), "utf8"),
181
+ ).version;
182
+ if (piPkg.version !== ourVersion) {
183
+ piPkg.version = ourVersion;
184
+ fs.writeFileSync(piPkgPath, JSON.stringify(piPkg, null, 2) + "\n");
185
+ report.versionAligned = ourVersion;
186
+ }
187
+ } catch {
188
+ /* cosmetic — never fail the patch over version alignment */
189
+ }
190
+ }
191
+ // Version alignment: pi's self-reported version (banner/help) reads its
192
+ // own package.json — rewrite it to the omnirush release version so the
193
+ // product version, not upstream's, is what users and support see.
194
+ try {
195
+ const piPkgPath = path.join(report.piRoot, "package.json");
196
+ const piPkg = JSON.parse(fs.readFileSync(piPkgPath, "utf8"));
197
+ const ourVersion = JSON.parse(
198
+ fs.readFileSync(path.join(repoRoot, "package.json"), "utf8"),
199
+ ).version;
200
+ if (piPkg.version !== ourVersion) {
201
+ piPkg.version = ourVersion;
202
+ fs.writeFileSync(piPkgPath, JSON.stringify(piPkg, null, 2) + "\n");
203
+ report.versionAligned = ourVersion;
204
+ }
205
+ } catch {
206
+ /* cosmetic — never fail the patch over version alignment */
207
+ }
208
+ const patched = report.packageJsonPatched ? "package.json + " : "";
209
+ const ver = report.versionAligned ? `; version aligned to ${report.versionAligned}` : "";
210
+ console.log(
211
+ `omnirush: branding applied (${patched}${report.applied.length} display strings)${ver}` +
212
+ (report.missing.length > 0 ? `; ${report.missing.length} already-applied/absent` : ""),
213
+ );
214
+ }
215
+
216
+ // CLI entry (postinstall). Imported as a module by tests — never exit there.
217
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
218
+ try {
219
+ main();
220
+ } catch (error) {
221
+ // Fail-soft: a cosmetic patch must never break `npm install`.
222
+ console.warn("omnirush: branding patch skipped:", error?.message ?? error);
223
+ }
224
+ }