omnirush 0.1.1 → 0.2.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.
@@ -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,97 @@ 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
+ /** Ints from the grant header pair, when both are present. */
62
+ export function grantFromUsage(usage: Record<string, string> | null): {
63
+ limitTokens: number;
64
+ remainingTokens: number;
65
+ } | null {
66
+ const limit = Number(usage?.["x-ratelimit-limit-tokens"]);
67
+ const remaining = Number(usage?.["x-ratelimit-remaining-tokens"]);
68
+ if (!Number.isFinite(limit) || !Number.isFinite(remaining)) return null;
69
+ return { limitTokens: limit, remainingTokens: remaining };
70
+ }
71
+
72
+ /** Compact human token count: 1.0B / 997.2M / 12.3k / 972. */
73
+ export function formatTokenCount(value: number): string {
74
+ if (!Number.isFinite(value)) return "?";
75
+ const abs = Math.abs(value);
76
+ if (abs >= 1e9) return `${(value / 1e9).toFixed(1)}B`;
77
+ if (abs >= 1e6) return `${(value / 1e6).toFixed(1)}M`;
78
+ if (abs >= 1e3) return `${(value / 1e3).toFixed(1)}k`;
79
+ return String(Math.round(value));
80
+ }
81
+
82
+ function intOr(value: unknown): number | null {
83
+ const n = Number(value);
84
+ return Number.isFinite(n) ? n : null;
85
+ }
86
+
87
+ /**
88
+ * The lines /grant shows. Everything is best-effort: header data when the
89
+ * agent has made a request, /device/me identity+usage otherwise.
90
+ */
91
+ export function formatGrantLines({
92
+ usage,
93
+ me,
94
+ sessionId,
95
+ }: {
96
+ usage: Record<string, string> | null;
97
+ me?: any;
98
+ sessionId?: string;
99
+ }): string[] {
100
+ const lines: string[] = [];
101
+ const model =
102
+ usage?.["x-omnirush-model"] ?? me?.current_model ?? null;
103
+ const effort = usage?.["x-omnirush-effort"] ?? null;
104
+ lines.push(`model (last turn): ${model ?? "no request yet"}`);
105
+ if (effort) lines.push(`reasoning effort: ${effort}`);
106
+
107
+ const fromHeaders = grantFromUsage(usage);
108
+ const meUsage = me?.usage ?? null;
109
+ const limit =
110
+ fromHeaders?.limitTokens ?? intOr(meUsage?.token_limit) ?? null;
111
+ const remaining =
112
+ fromHeaders?.remainingTokens ?? intOr(meUsage?.remaining_tokens) ?? null;
113
+ const usedFromMe = intOr(meUsage?.used_tokens);
114
+ const used =
115
+ usedFromMe ??
116
+ (limit !== null && remaining !== null ? limit - remaining : null);
117
+
118
+ if (limit !== null && remaining !== null) {
119
+ lines.push(
120
+ `grant: ${formatTokenCount(remaining)} of ${formatTokenCount(limit)} tokens remaining` +
121
+ (used !== null ? ` (used ${formatTokenCount(used)})` : ""),
122
+ );
123
+ } else if (used !== null) {
124
+ lines.push(`grant used: ${formatTokenCount(used)} tokens`);
125
+ }
126
+ if (meUsage?.grant_date) lines.push(`grant date: ${meUsage.grant_date}`);
127
+
128
+ const devices = Array.isArray(me?.devices) ? me.devices.length : null;
129
+ if (devices !== null) {
130
+ lines.push(`devices signed in: ${devices}`);
131
+ }
132
+ if (sessionId) {
133
+ lines.push(`session: ${sessionId.slice(0, 8)}`);
134
+ }
135
+ return lines;
136
+ }
137
+
138
+ /** One-line footer summary for the TUI status area, or null with no data. */
139
+ export function formatGrantStatusLine(
140
+ usage: Record<string, string> | null,
141
+ ): string | null {
142
+ const fromHeaders = grantFromUsage(usage);
143
+ const model = usage?.["x-omnirush-model"];
144
+ if (!fromHeaders && !model) return null;
145
+ const parts: string[] = [];
146
+ if (fromHeaders) {
147
+ parts.push(
148
+ `${formatTokenCount(fromHeaders.remainingTokens)} tok left`,
149
+ );
150
+ }
151
+ if (model) parts.push(model);
152
+ return parts.length > 0 ? parts.join(" · ") : null;
153
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omnirush",
3
- "version": "0.1.1",
3
+ "version": "0.2.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,7 @@
12
12
  },
13
13
  "scripts": {
14
14
  "test": "node --test",
15
- "postinstall": "node scripts/postinstall.js"
15
+ "postinstall": "node scripts/patch-pi-branding.js && node scripts/postinstall.js"
16
16
  },
17
17
  "dependencies": {
18
18
  "@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,189 @@
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
+ }
174
+ const patched = report.packageJsonPatched ? "package.json + " : "";
175
+ console.log(
176
+ `omnirush: branding applied (${patched}${report.applied.length} display strings)` +
177
+ (report.missing.length > 0 ? `; ${report.missing.length} already-applied/absent` : ""),
178
+ );
179
+ }
180
+
181
+ // CLI entry (postinstall). Imported as a module by tests — never exit there.
182
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
183
+ try {
184
+ main();
185
+ } catch (error) {
186
+ // Fail-soft: a cosmetic patch must never break `npm install`.
187
+ console.warn("omnirush: branding patch skipped:", error?.message ?? error);
188
+ }
189
+ }
@@ -1,12 +1,24 @@
1
- // Downloads the platform bun runtime (v1.4.2) on install so `omnirush`
2
- // works immediately on linux / macOS / Windows.
1
+ // Postinstall runtime fetcher:
2
+ // 1. bun (v1.4.2) — required; the agent core runs on it.
3
+ // 2. fd (v10.5.0) — optional; the agent's file search + autocomplete.
4
+ // 3. rg (v15.2.0) — optional; the agent's content search.
5
+ //
6
+ // Binaries land in .runtime/bin/ (per-platform release packages ship the
7
+ // same layout prebuilt). The launcher copies fd/rg into the agent's
8
+ // managed-bin dir on every run, so the agent never attempts its own
9
+ // GitHub download at startup (which fails on restricted networks and
10
+ // repeats on every launch). fd/rg failures are warnings: search degrades
11
+ // gracefully without them. A bun failure stays fatal.
3
12
  import { execFileSync } from "node:child_process";
4
13
  import fs from "node:fs";
5
14
  import https from "node:https";
6
15
  import path from "node:path";
7
16
  import { fileURLToPath } from "node:url";
8
17
 
9
- const VERSION = "bun-v1.4.2";
18
+ const BUN_VERSION = "bun-v1.4.2";
19
+ // fd release tags are v-prefixed; ripgrep tags are bare ("15.2.0").
20
+ const FD_VERSION = "v10.5.0";
21
+ const RG_VERSION = "15.2.0";
10
22
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
11
23
  const ROOT = path.resolve(__dirname, "..");
12
24
  const RUNTIME = path.join(ROOT, ".runtime", "bin");
@@ -18,6 +30,10 @@ function platform() {
18
30
  return `${plat}-${arch}`;
19
31
  }
20
32
 
33
+ function exe(name) {
34
+ return name + (process.platform === "win32" ? ".exe" : "");
35
+ }
36
+
21
37
  function get(url, redirects = 0) {
22
38
  if (redirects > 5) throw new Error("too many redirects");
23
39
  return new Promise((resolve, reject) => {
@@ -57,22 +73,98 @@ function extractZip(buf, dest) {
57
73
  fs.rmSync(tmp, { force: true });
58
74
  }
59
75
 
60
- async function main() {
61
- // Skip when bundled (GitHub release packages ship .runtime prebuilt).
62
- const existing = path.join(RUNTIME, process.platform === "win32" ? "bun.exe" : "bun");
76
+ function extractTarGz(buf, dest, stamp) {
77
+ const tmp = path.join(dest, stamp);
78
+ fs.writeFileSync(tmp, buf);
79
+ // tar handles .tar.gz on linux/macOS; Windows 10+ ships bsdtar in
80
+ // System32 (the agent core relies on the same thing).
81
+ try {
82
+ execFileSync("tar", ["xzf", tmp, "-C", dest]);
83
+ } catch {
84
+ execFileSync("python3", ["-m", "tarfile", "-e", tmp, "-C", dest]);
85
+ }
86
+ fs.rmSync(tmp, { force: true });
87
+ }
88
+
89
+ function findFile(rootDir, fileName) {
90
+ const stack = [rootDir];
91
+ while (stack.length > 0) {
92
+ const dir = stack.pop();
93
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
94
+ const full = path.join(dir, entry.name);
95
+ if (entry.isFile() && entry.name === fileName) return full;
96
+ if (entry.isDirectory()) stack.push(full);
97
+ }
98
+ }
99
+ return null;
100
+ }
101
+
102
+ async function fetchBun(plat) {
103
+ const existing = path.join(RUNTIME, exe("bun"));
63
104
  if (fs.existsSync(existing)) return;
64
- const plat = platform();
65
- const zipUrl = `https://github.com/oven-sh/bun/releases/download/${VERSION}/bun-${plat}.zip`;
105
+ const zipUrl = `https://github.com/oven-sh/bun/releases/download/${BUN_VERSION}/bun-${plat}.zip`;
66
106
  process.stdout.write(`omnirush: downloading agent runtime (bun for ${plat})...\n`);
67
107
  const buf = await get(zipUrl);
68
108
  fs.mkdirSync(RUNTIME, { recursive: true });
69
109
  extractZip(buf, ROOT);
70
- const exe = process.platform === "win32" ? "bun.exe" : "bun";
71
- const src = path.join(ROOT, `bun-${plat}`, exe);
110
+ const src = path.join(ROOT, `bun-${plat}`, exe("bun"));
72
111
  fs.copyFileSync(src, existing);
73
112
  if (process.platform !== "win32") fs.chmodSync(existing, 0o755);
74
113
  fs.rmSync(path.join(ROOT, `bun-${plat}`), { recursive: true, force: true });
75
114
  console.log("omnirush: runtime ready");
76
115
  }
77
116
 
117
+ // fd/rg release archives both nest the binary under a versioned directory
118
+ // (fd-v10.5.0-x86_64-unknown-linux-musl/fd, ripgrep-15.2.0-.../rg).
119
+ async function fetchSearchTool(name, version, repo, archiveName, plat) {
120
+ const existing = path.join(RUNTIME, exe(name));
121
+ if (fs.existsSync(existing)) return;
122
+ const isZip = plat === "windows-x64";
123
+ const asset = isZip ? `${archiveName}.zip` : `${archiveName}.tar.gz`;
124
+ const url = `https://github.com/${repo}/releases/download/${version}/${asset}`;
125
+ process.stdout.write(`omnirush: downloading ${name} (${plat})...\n`);
126
+ const buf = await get(url);
127
+ fs.mkdirSync(RUNTIME, { recursive: true });
128
+ const extractDir = path.join(RUNTIME, `extract-${name}`);
129
+ fs.mkdirSync(extractDir, { recursive: true });
130
+ if (isZip) extractZip(buf, extractDir);
131
+ else extractTarGz(buf, extractDir, asset);
132
+ const found = findFile(extractDir, exe(name));
133
+ if (!found) throw new Error(`${exe(name)} not found in ${asset}`);
134
+ fs.copyFileSync(found, existing);
135
+ if (process.platform !== "win32") fs.chmodSync(existing, 0o755);
136
+ fs.rmSync(extractDir, { recursive: true, force: true });
137
+ }
138
+
139
+ function fdAsset(plat) {
140
+ const arch = process.arch === "arm64" ? "aarch64" : "x86_64";
141
+ if (plat === "windows-x64") return `fd-${FD_VERSION}-${arch}-pc-windows-msvc`;
142
+ if (plat === "darwin-x64" || plat === "darwin-aarch64") return `fd-${FD_VERSION}-${arch}-apple-darwin`;
143
+ return `fd-${FD_VERSION}-${arch}-unknown-linux-musl`;
144
+ }
145
+
146
+ function rgAsset(plat) {
147
+ const arch = process.arch === "arm64" ? "aarch64" : "x86_64";
148
+ if (plat === "windows-x64") return `ripgrep-${RG_VERSION}-${arch}-pc-windows-msvc`;
149
+ if (plat === "darwin-x64" || plat === "darwin-aarch64") return `ripgrep-${RG_VERSION}-${arch}-apple-darwin`;
150
+ return `ripgrep-${RG_VERSION}-${arch}-unknown-linux-musl`;
151
+ }
152
+
153
+ async function main() {
154
+ const plat = platform();
155
+ await fetchBun(plat);
156
+ for (const [name, version, repo, archive] of [
157
+ ["fd", FD_VERSION, "sharkdp/fd", fdAsset(plat)],
158
+ ["rg", RG_VERSION, "BurntSushi/ripgrep", rgAsset(plat)],
159
+ ]) {
160
+ try {
161
+ await fetchSearchTool(name, version, repo, archive, plat);
162
+ } catch (error) {
163
+ // Optional tools: warn and continue (the launcher only installs what
164
+ // exists; the agent degrades to PATH lookup / built-in search).
165
+ console.warn(`omnirush: ${name} download failed (search may be degraded):`, error.message);
166
+ }
167
+ }
168
+ }
169
+
78
170
  main().catch((e) => { console.error("omnirush postinstall:", e.message); process.exit(1); });
package/src/bin.js CHANGED
@@ -18,10 +18,12 @@
18
18
  // OMNIRUSH_GATEWAY_URL provider base URL override ({origin}/v1)
19
19
  // OMNIRUSH_DIR state dir override (default ~/.omnirush)
20
20
  // OMNIRUSH_MODEL model override for plain runs (default
21
- // gpt-6-astra; user --model still wins)
21
+ // gpt-6-astra; gpt-5.6-sol also available; user
22
+ // --model still wins)
22
23
  //
23
- // Agent runs default to `--provider omnirush --model gpt-6-astra` (SOTA
24
- // only — charter rule 1). The sota extension watches every response,
24
+ // Agent runs default to `--provider omnirush --model gpt-6-astra`; the
25
+ // catalog also carries gpt-5.6-sol (`--model gpt-5.6-sol` or
26
+ // OMNIRUSH_MODEL=gpt-5.6-sol). The sota extension watches every response,
25
27
  // refreshes the device token single-flight on 401 (retry once), and
26
28
  // warns on stderr if the gateway serves a different model than
27
29
  // requested; `--strict-sota` makes that violation fatal (exit 3).
@@ -42,6 +44,7 @@ import {
42
44
  withModelDefaults,
43
45
  resolveModelDefaults,
44
46
  } from "./lib.js";
47
+ import { installManagedTools } from "./tools.js";
45
48
  import {
46
49
  clearAuth,
47
50
  deviceAuthorize,
@@ -99,6 +102,11 @@ function ensureDirs() {
99
102
  fs.mkdirSync(OMNI_DIR, { recursive: true, mode: 0o700 });
100
103
  fs.mkdirSync(PI_AGENT_DIR, { recursive: true });
101
104
  fs.mkdirSync(PI_EXT_DIR, { recursive: true });
105
+ try {
106
+ installManagedTools({ agentDir: PI_AGENT_DIR, runtimeRoot: path.resolve(__dirname, "..", ".runtime") });
107
+ } catch {
108
+ /* best effort — pi falls back to PATH, then its own downloader */
109
+ }
102
110
  }
103
111
 
104
112
  // Merge our provider into the user's models.json without clobbering
@@ -355,8 +363,39 @@ async function cmdDoctor() {
355
363
 
356
364
  installModelsJson(gateway);
357
365
  installExtensions();
358
- ok("pi config", true, PI_MODELS_JSON);
366
+ try {
367
+ installManagedTools({ agentDir: PI_AGENT_DIR, runtimeRoot: path.resolve(__dirname, "..", ".runtime") });
368
+ } catch {
369
+ /* best effort */
370
+ }
371
+ ok("agent config", true, PI_MODELS_JSON);
359
372
  ok("extensions", true, PI_EXT_DIR);
373
+ // Search tools: bundled fd/rg (or system ones) keep the agent off its
374
+ // GitHub download path, which fails on restricted networks.
375
+ const toolsRoot = path.join(PI_AGENT_DIR, "bin");
376
+ for (const tool of ["fd", "rg"]) {
377
+ const exe = process.platform === "win32" ? ".exe" : "";
378
+ const managed = path.join(toolsRoot, tool + exe);
379
+ const hasManaged = fs.existsSync(managed);
380
+ let viaPath = false;
381
+ if (!hasManaged) {
382
+ for (const dir of (process.env.PATH || "").split(path.delimiter)) {
383
+ if (!dir) continue;
384
+ try {
385
+ fs.accessSync(path.join(dir, tool + exe), fs.constants.X_OK);
386
+ viaPath = true;
387
+ break;
388
+ } catch {
389
+ /* next */
390
+ }
391
+ }
392
+ }
393
+ ok(
394
+ `search tool ${tool}`,
395
+ hasManaged || viaPath,
396
+ hasManaged ? managed : viaPath ? "found on PATH" : "missing — find/autocomplete degraded",
397
+ );
398
+ }
360
399
  process.exit(healthy ? 0 : 1);
361
400
  }
362
401
 
@@ -385,9 +424,13 @@ function childEnv(auth) {
385
424
  OMNIRUSH_ORIGIN: origin,
386
425
  OMNIRUSH_GATEWAY_URL: gateway,
387
426
  OMNIRUSH_DIR: OMNI_DIR,
388
- // pi resolves its config dir from this env var; keep the child pi on
389
- // exactly the models.json/extensions bin.js just installed.
427
+ // The agent resolves its config dir from <APP>_CODING_AGENT_DIR. The
428
+ // branding patch renames the app to omnirush, so set the omnirush
429
+ // spelling; the pi spelling stays for unpatched cores.
430
+ OMNIRUSH_CODING_AGENT_DIR: PI_AGENT_DIR,
390
431
  PI_CODING_AGENT_DIR: PI_AGENT_DIR,
432
+ // The agent core is a fork/rebrand: never phone home to pi.dev.
433
+ PI_TELEMETRY: "0",
391
434
  };
392
435
  }
393
436
 
@@ -433,7 +476,7 @@ if (cmd === "login") await cmdLogin(argv.slice(1));
433
476
  else if (cmd === "logout") await cmdLogout();
434
477
  else if (cmd === "whoami") await cmdWhoami();
435
478
  else if (cmd === "doctor") await cmdDoctor();
436
- else if (cmd === "--version" || cmd === "-v") console.log("omnirush 0.2.0 (pi core)");
479
+ else if (cmd === "--version" || cmd === "-v") console.log("omnirush 0.2.0");
437
480
  else if (cmd === "collect") await cmdCollect(argv.slice(1));
438
481
  else if (cmd === undefined || cmd === "run") await cmdRun();
439
482
  else {
package/src/lib.js CHANGED
@@ -67,7 +67,8 @@ const VALUE_FLAGS = new Set([
67
67
 
68
68
  /**
69
69
  * Model defaults for plain `omnirush` runs. OMNIRUSH_MODEL overrides the
70
- * default model (e.g. to pin a thinking level like "gpt-6-astra:high").
70
+ * default model — any catalog id below, optionally with a thinking level
71
+ * like "gpt-6-astra:high" or "gpt-5.6-sol:xhigh".
71
72
  */
72
73
  export function resolveModelDefaults(env = process.env) {
73
74
  return {
@@ -76,6 +77,35 @@ export function resolveModelDefaults(env = process.env) {
76
77
  };
77
78
  }
78
79
 
80
+ // The product's shipped catalog: gpt-6-astra (default) and gpt-5.6-sol.
81
+ // Live-verified 2026-09-22 against the production gateway: GET /v1/models
82
+ // lists both ids and POST /v1/responses with model "gpt-5.6-sol" is served
83
+ // as gpt-5.6-sol (x-omnirush-model response header + the model named in
84
+ // every SSE frame), so the client may offer the switch honestly.
85
+ export const MODELS = [
86
+ {
87
+ id: "gpt-6-astra",
88
+ name: "GPT-6 Astra",
89
+ reasoning: true,
90
+ input: ["text", "image"],
91
+ cost: { input: 10, output: 50, cacheRead: 2.5, cacheWrite: 12.5 },
92
+ contextWindow: 400000,
93
+ compat: { supportsMaxOutputTokens: false },
94
+ },
95
+ {
96
+ id: "gpt-5.6-sol",
97
+ name: "GPT-5.6 Sol",
98
+ reasoning: true,
99
+ input: ["text", "image"],
100
+ // Sol list pricing is not published; pi's cost display therefore shows
101
+ // zeros for it (documented default when cost is omitted).
102
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
103
+ // Same codex backend family as astra; same context window.
104
+ contextWindow: 400000,
105
+ compat: { supportsMaxOutputTokens: false },
106
+ },
107
+ ];
108
+
79
109
  /**
80
110
  * Prepend `--provider`/`--model` defaults for agent runs, without
81
111
  * duplicating flags the user already specified. Consumption rules mirror
@@ -145,12 +175,13 @@ export function withModelDefaults(args, defaults) {
145
175
 
146
176
  /**
147
177
  * The provider/models.json payload omnirush installs into pi. This is the
148
- * product's shipped model catalog: SOTA only (charter rule 1). The served
149
- * model is forced server-side by the gateway; the client SOTA guard stays
150
- * on as verification.
178
+ * product's shipped model catalog (MODELS above). The gateway allow-lists
179
+ * the same ids server-side; the client SOTA guard stays on as verification
180
+ * (it compares the model named in the response frames against the one
181
+ * requested).
151
182
  *
152
183
  * Hard-won compat fact: the codex backend rejects
153
- * `max_output_tokens`, so the model sets
184
+ * `max_output_tokens`, so every model sets
154
185
  * `compat.supportsMaxOutputTokens: false` and ships no maxTokens — pi would
155
186
  * otherwise clamp a default and break every request.
156
187
  */
@@ -162,18 +193,22 @@ export function modelsConfig({ gatewayUrl } = {}) {
162
193
  baseUrl: gatewayUrl ?? DEFAULT_GATEWAY_URL,
163
194
  api: "openai-responses",
164
195
  apiKey: "$OMNIRUSH_TOKEN",
165
- models: [
166
- {
167
- id: DEFAULT_MODEL,
168
- name: "GPT-6 Astra",
169
- reasoning: true,
170
- input: ["text", "image"],
171
- cost: { input: 10, output: 50, cacheRead: 2.5, cacheWrite: 12.5 },
172
- contextWindow: 400000,
173
- compat: { supportsMaxOutputTokens: false },
174
- },
175
- ],
196
+ models: MODELS.map((model) => ({ ...model })),
176
197
  },
177
198
  },
178
199
  };
179
200
  }
201
+
202
+ /**
203
+ * Pure mapping of the bundled search-tool binaries for the launcher: pi
204
+ * looks for managed `fd`/`rg` in <agentDir>/bin first and downloads from
205
+ * GitHub when absent. We ship both binaries and the launcher places them
206
+ * there, so the agent never attempts (or repeats) a download.
207
+ */
208
+ export function managedToolTargets({ platform = process.platform, runtimeRoot } = {}) {
209
+ const ext = platform === "win32" ? ".exe" : "";
210
+ return [
211
+ { tool: "fd", source: `${runtimeRoot}/bin/fd${ext}`, dest: `bin/fd${ext}` },
212
+ { tool: "rg", source: `${runtimeRoot}/bin/rg${ext}`, dest: `bin/rg${ext}` },
213
+ ];
214
+ }
package/src/tools.js ADDED
@@ -0,0 +1,40 @@
1
+ // Managed search-tool install (fd, rg) for the agent core.
2
+ //
3
+ // pi checks <agentDir>/bin for managed fd/rg first and, when absent,
4
+ // downloads them from GitHub at every agent startup — on restricted
5
+ // networks that download fails and the attempt repeats on every launch,
6
+ // breaking the find tool and file autocomplete. We bundle the binaries
7
+ // (.runtime/bin, filled by postinstall or the release package) and the
8
+ // launcher places them exactly where pi looks. Best effort: any failure
9
+ // leaves the agent to its own PATH lookup / downloader.
10
+
11
+ import fs from "node:fs";
12
+ import path from "node:path";
13
+
14
+ import { managedToolTargets } from "./lib.js";
15
+
16
+ export function installManagedTools({
17
+ agentDir,
18
+ runtimeRoot,
19
+ platform = process.platform,
20
+ fsImpl = fs,
21
+ } = {}) {
22
+ const installed = [];
23
+ if (!agentDir || !runtimeRoot) return installed;
24
+ for (const target of managedToolTargets({ platform, runtimeRoot })) {
25
+ const dest = path.join(agentDir, target.dest);
26
+ if (fsImpl.existsSync(dest)) continue;
27
+ if (!fsImpl.existsSync(target.source)) continue;
28
+ fsImpl.mkdirSync(path.dirname(dest), { recursive: true });
29
+ fsImpl.copyFileSync(target.source, dest);
30
+ if (platform !== "win32") {
31
+ try {
32
+ fsImpl.chmodSync(dest, 0o755);
33
+ } catch {
34
+ /* best effort */
35
+ }
36
+ }
37
+ installed.push(target.tool);
38
+ }
39
+ return installed;
40
+ }