omnirush 0.3.3 → 0.4.2

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.
@@ -93,6 +93,33 @@ export const REPLACEMENTS = [
93
93
  from: "Update pi only (self works as alias to pi)",
94
94
  to: "Update the agent only (self works as an alias)",
95
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
+ },
96
123
  ];
97
124
 
98
125
  function findPiRoot(startDir) {
@@ -145,11 +145,15 @@ function findFile(rootDir, fileName) {
145
145
  return null;
146
146
  }
147
147
 
148
- async function fetchBun(plat) {
149
- const existing = path.join(RUNTIME, exe("bun"));
148
+ export function managedBunPath() {
149
+ return path.join(RUNTIME, exe("bun"));
150
+ }
151
+
152
+ async function fetchBun(plat, log = console.log) {
153
+ const existing = managedBunPath();
150
154
  if (fs.existsSync(existing)) return;
151
155
  const zipUrl = `https://github.com/oven-sh/bun/releases/download/${BUN_VERSION}/bun-${bunAssetPlat(plat)}.zip`;
152
- process.stdout.write(`omnirush: downloading agent runtime (bun for ${plat})...\n`);
156
+ log(`omnirush: downloading agent runtime (bun for ${plat})...`);
153
157
  const buf = await get(zipUrl);
154
158
  fs.mkdirSync(RUNTIME, { recursive: true });
155
159
  extractZip(buf, ROOT);
@@ -157,7 +161,7 @@ async function fetchBun(plat) {
157
161
  fs.copyFileSync(src, existing);
158
162
  if (process.platform !== "win32") fs.chmodSync(existing, 0o755);
159
163
  fs.rmSync(path.join(ROOT, `bun-${bunAssetPlat(plat)}`), { recursive: true, force: true });
160
- console.log("omnirush: runtime ready");
164
+ log("omnirush: runtime ready");
161
165
  }
162
166
 
163
167
  // fd/rg release archives both nest the binary under a versioned directory
@@ -196,9 +200,14 @@ function rgAsset(plat) {
196
200
  return `ripgrep-${RG_VERSION}-${arch}-unknown-linux-musl`;
197
201
  }
198
202
 
199
- async function main() {
203
+ /**
204
+ * Fetch the agent runtime when it is missing (idempotent: existing
205
+ * binaries are kept). Used by postinstall and by `omnirush doctor`
206
+ * self-heal. `log` defaults to console.log.
207
+ */
208
+ export async function ensureRuntime({ log = console.log } = {}) {
200
209
  const plat = platform();
201
- await fetchBun(plat);
210
+ await fetchBun(plat, log);
202
211
  for (const [name, version, repo, archive] of [
203
212
  ["fd", FD_VERSION, "sharkdp/fd", fdAsset(plat)],
204
213
  ["rg", RG_VERSION, "BurntSushi/ripgrep", rgAsset(plat)],
@@ -208,9 +217,17 @@ async function main() {
208
217
  } catch (error) {
209
218
  // Optional tools: warn and continue (the launcher only installs what
210
219
  // exists; the agent degrades to PATH lookup / built-in search).
211
- console.warn(`omnirush: ${name} download failed (search may be degraded):`, error.message);
220
+ log(`omnirush: ${name} download failed (search may be degraded): ${error.message}`);
212
221
  }
213
222
  }
214
223
  }
215
224
 
216
- main().catch((e) => { console.error("omnirush postinstall:", e.message); process.exit(1); });
225
+ async function main() {
226
+ await ensureRuntime();
227
+ }
228
+
229
+ // CLI entry (postinstall). Imported as a module by the launcher — never
230
+ // exit there.
231
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
232
+ main().catch((e) => { console.error("omnirush postinstall:", e.message); process.exit(1); });
233
+ }
package/src/bin.js CHANGED
@@ -42,9 +42,11 @@ import {
42
42
  modelsConfig,
43
43
  resolveGatewayUrl,
44
44
  withModelDefaults,
45
+ withDefaultThinkingLevel,
45
46
  resolveModelDefaults,
46
47
  } from "./lib.js";
47
48
  import { installManagedTools } from "./tools.js";
49
+ import { ensureRuntime, managedBunPath } from "../scripts/postinstall.js";
48
50
  import {
49
51
  clearAuth,
50
52
  deviceAuthorize,
@@ -70,6 +72,7 @@ const PI_AGENT_DIR = process.env.OMNIRUSH_PI_AGENT_DIR
70
72
  ? path.resolve(process.env.OMNIRUSH_PI_AGENT_DIR)
71
73
  : path.join(os.homedir(), ".pi", "agent");
72
74
  const PI_MODELS_JSON = path.join(PI_AGENT_DIR, "models.json");
75
+ const PI_SETTINGS_JSON = path.join(PI_AGENT_DIR, "settings.json");
73
76
  const PI_EXT_DIR = path.join(PI_AGENT_DIR, "extensions", "omnirush");
74
77
 
75
78
  function managerOrigin() {
@@ -79,7 +82,7 @@ function managerOrigin() {
79
82
  );
80
83
  }
81
84
 
82
- function piEntry() {
85
+ function piPackage() {
83
86
  // The pi package is Bun-first and its "exports" map blocks plain Node
84
87
  // resolution, so locate it by path: walk up from our own package root
85
88
  // to the nearest node_modules containing it.
@@ -88,13 +91,22 @@ function piEntry() {
88
91
  for (let i = 0; i < 6; i++) {
89
92
  const pkgPath = path.join(dir, "node_modules", PKG, "package.json");
90
93
  if (fs.existsSync(pkgPath)) {
91
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
92
- if (pkg.bin?.pi) return path.join(dir, "node_modules", PKG, pkg.bin.pi);
94
+ try {
95
+ return { dir: path.dirname(pkgPath), pkg: JSON.parse(fs.readFileSync(pkgPath, "utf8")) };
96
+ } catch {
97
+ return null;
98
+ }
93
99
  }
94
100
  const parent = path.dirname(dir);
95
101
  if (parent === dir) break;
96
102
  dir = parent;
97
103
  }
104
+ return null;
105
+ }
106
+
107
+ function piEntry() {
108
+ const found = piPackage();
109
+ if (found?.pkg?.bin?.pi) return path.join(found.dir, found.pkg.bin.pi);
98
110
  throw new Error("could not locate the pinned pi CLI (@earendil-works/pi-coding-agent)");
99
111
  }
100
112
 
@@ -107,6 +119,29 @@ function ensureDirs() {
107
119
  } catch {
108
120
  /* best effort — pi falls back to PATH, then its own downloader */
109
121
  }
122
+ installDefaultThinkingLevel();
123
+ }
124
+
125
+ // Product default effort (P2): xhigh. Fills an ABSENT
126
+ // defaultThinkingLevel in the agent settings only — an explicit user
127
+ // choice always wins; the models' thinkingLevelMap is untouched.
128
+ function installDefaultThinkingLevel() {
129
+ let settings = {};
130
+ try {
131
+ settings = JSON.parse(fs.readFileSync(PI_SETTINGS_JSON, "utf8"));
132
+ } catch {
133
+ /* fresh install or unparsable — start clean rather than clobber */
134
+ settings = {};
135
+ }
136
+ let next;
137
+ try {
138
+ next = withDefaultThinkingLevel(settings);
139
+ } catch {
140
+ return;
141
+ }
142
+ if (next !== settings) {
143
+ fs.writeFileSync(PI_SETTINGS_JSON, JSON.stringify(next, null, 2) + "\n");
144
+ }
110
145
  }
111
146
 
112
147
  // Merge our provider into the user's models.json without clobbering
@@ -154,36 +189,44 @@ function isMusl() {
154
189
  }
155
190
 
156
191
  function findBun() {
157
- // Vendored per-platform bun first, then PATH, then the standard
158
- // single-user install location. Linux packages ship both the glibc and
159
- // the musl binary; the launcher picks by the running libc.
192
+ // MANAGED runtime first — always (issue #7: a system bun on PATH
193
+ // shadowed ours when .runtime was missing from a dev checkout, and a
194
+ // mismatched bun breaks the fork's own extensions). Vendored paths are
195
+ // canonical; PATH and ~/.bun are last-resort fallbacks that only run
196
+ // after a self-heal download was attempted. Linux packages ship both
197
+ // the glibc and the musl binary; the launcher picks by the running libc.
160
198
  const runtimeRoot = path.resolve(__dirname, "..", ".runtime");
161
199
  const plat = process.platform;
162
200
  const arch = process.arch === "arm64" ? "aarch64" : "x64";
163
- const candidates = [];
201
+ const managedCandidates = [];
164
202
  if (plat === "win32") {
165
- candidates.push(
166
- path.join(runtimeRoot, "bun-windows-x64", "bin", "bun.exe"),
167
- );
168
- for (const dir of (process.env.PATH || "").split(path.delimiter)) {
169
- if (dir) candidates.push(path.join(dir, "bun.exe"));
170
- }
203
+ managedCandidates.push(managedBunPath());
204
+ managedCandidates.push(path.join(runtimeRoot, "bun-windows-x64", "bin", "bun.exe"));
171
205
  } else {
172
206
  if (plat === "linux") {
173
- if (isMusl()) candidates.push(path.join(runtimeRoot, "bin", "bun-musl"));
174
- else if (arch === "x64") candidates.push(path.join(runtimeRoot, "bin", "bun"));
175
- candidates.push(path.join(runtimeRoot, "bin", `bun-${arch}${isMusl() ? "-musl" : ""}`));
207
+ if (isMusl()) managedCandidates.push(path.join(runtimeRoot, "bin", "bun-musl"));
208
+ else if (arch === "x64") managedCandidates.push(path.join(runtimeRoot, "bin", "bun"));
209
+ managedCandidates.push(path.join(runtimeRoot, "bin", `bun-${arch}${isMusl() ? "-musl" : ""}`));
176
210
  }
177
- candidates.push(path.join(runtimeRoot, `bun-${plat}-${arch}`, "bin", "bun"));
178
- for (const dir of (process.env.PATH || "").split(path.delimiter)) {
179
- if (dir) candidates.push(path.join(dir, "bun"));
211
+ managedCandidates.push(path.join(runtimeRoot, `bun-${plat}-${arch}`, "bin", "bun"));
212
+ }
213
+ for (const c of managedCandidates) {
214
+ try {
215
+ fs.accessSync(c, fs.constants.X_OK);
216
+ return { path: c, managed: true };
217
+ } catch {
218
+ /* next */
180
219
  }
181
- candidates.push(path.join(os.homedir(), ".bun", "bin", "bun"));
182
220
  }
183
- for (const c of candidates) {
221
+ const fallbackCandidates = [];
222
+ for (const dir of (process.env.PATH || "").split(path.delimiter)) {
223
+ if (dir) fallbackCandidates.push(path.join(dir, plat === "win32" ? "bun.exe" : "bun"));
224
+ }
225
+ fallbackCandidates.push(path.join(os.homedir(), ".bun", "bin", plat === "win32" ? "bun.exe" : "bun"));
226
+ for (const c of fallbackCandidates) {
184
227
  try {
185
228
  fs.accessSync(c, fs.constants.X_OK);
186
- return c;
229
+ return { path: c, managed: false };
187
230
  } catch {
188
231
  /* next */
189
232
  }
@@ -191,19 +234,57 @@ function findBun() {
191
234
  return null;
192
235
  }
193
236
 
194
- function runtimeFor(entry) {
237
+ /**
238
+ * Self-healing managed-runtime resolution: if no MANAGED bun is
239
+ * installed, run the same fetch the postinstall runs, then re-check. A
240
+ * PATH bun is only used when the managed download is impossible — and
241
+ * the caller reports it.
242
+ */
243
+ async function resolveBun() {
244
+ let found = findBun();
245
+ if (found?.managed) return found;
246
+ // Self-heal: fetch the managed runtime (downloads only what is
247
+ // missing; a few MB-100MB depending on platform).
248
+ try {
249
+ await ensureRuntime();
250
+ } catch (error) {
251
+ // fall through: maybe a usable fallback still exists
252
+ try {
253
+ fs.accessSync(managedBunPath(), fs.constants.X_OK);
254
+ found = { path: managedBunPath(), managed: true };
255
+ } catch {
256
+ console.error(`omnirush: managed runtime download failed (${error?.message ?? error})`);
257
+ }
258
+ }
259
+ if (!found?.managed) found = findBun();
260
+ return found;
261
+ }
262
+
263
+ async function runtimeFor(entry) {
195
264
  // pi is Bun-first; its CLI imports node:module#enableCompileCache,
196
- // which needs Node >= 22.8. Prefer bun when present, else enforce the
197
- // Node floor with a clear message.
198
- const bun = findBun();
199
- if (bun) return { cmd: bun, args: [entry] };
265
+ // which needs Node >= 22.8. The MANAGED bun wins unconditionally
266
+ // (self-heal download first); a PATH bun is a last resort. Plain node
267
+ // 22.8+ runs the agent without any bun.
268
+ const found = await resolveBun();
269
+ if (found) {
270
+ if (!found.managed) {
271
+ console.error(
272
+ "omnirush: using a non-managed bun from PATH " +
273
+ `(${found.path}) — the managed runtime is missing. Fix: run ` +
274
+ "`npm install -g omnirush` again (retries the runtime download).",
275
+ );
276
+ }
277
+ return { cmd: found.path, args: [entry] };
278
+ }
200
279
  const [major, minor] = process.versions.node.split(".").map(Number);
201
280
  if (major > 22 || (major === 22 && minor >= 8)) {
202
281
  return { cmd: process.execPath, args: [entry] };
203
282
  }
204
283
  console.error(
205
- `omnirush: Node ${process.versions.node} is too old for the agent core ` +
206
- "(need >= 22.8) and bun was not found. Install Node 22+ or bun.",
284
+ `omnirush couldn't set up its agent runtime (Node ${process.versions.node} is too ` +
285
+ "old for the agent core, which needs Node >= 22.8, and the managed runtime " +
286
+ "download did not succeed — network or install issue). Fix: run " +
287
+ "`npm install -g omnirush` again (retries the runtime download), or install Node 22.8+.",
207
288
  );
208
289
  process.exit(1);
209
290
  }
@@ -421,6 +502,72 @@ async function cmdDoctor() {
421
502
  }
422
503
  ok("agent config", true, PI_MODELS_JSON);
423
504
  ok("extensions", true, PI_EXT_DIR);
505
+ // Agent core version: a stale core predates the omnirush provider and
506
+ // branding and shows pi.dev "Update Available" banners (Foxtran's
507
+ // 0.73.1 report). Advisory WARN with the fix; exit code unaffected.
508
+ {
509
+ const found = piPackage();
510
+ const coreVersion = String(found?.pkg?.version ?? "");
511
+ const own = ownVersion();
512
+ if (!coreVersion) {
513
+ console.log(`WARN agent core — pinned agent package not found (broken install). Fix: omnirush update`);
514
+ } else if (coreVersion === own) {
515
+ ok("agent core", true, `v${coreVersion}`);
516
+ } else {
517
+ console.log(
518
+ `WARN agent core — installed core is v${coreVersion}, this omnirush expects v${own} ` +
519
+ "(stale install: pi.dev update banners, missing omnirush provider). Fix: omnirush update",
520
+ );
521
+ }
522
+ }
523
+ // Agent runtime: the managed bun must exist and always wins (issue #7:
524
+ // a PATH bun shadowed the managed runtime for a dev). Self-heal by
525
+ // downloading it right here when missing; report WHICH runtime is in
526
+ // use so support can see it.
527
+ {
528
+ let found = findBun();
529
+ let healError = null;
530
+ if (!found?.managed) {
531
+ try {
532
+ await ensureRuntime();
533
+ } catch (error) {
534
+ healError = error;
535
+ }
536
+ found = findBun();
537
+ }
538
+ if (found?.managed) {
539
+ ok(
540
+ "runtime",
541
+ true,
542
+ `bundled bun ${bunVersionOf(found.path)} (${found.path})`,
543
+ );
544
+ } else if (healError) {
545
+ ok(
546
+ "runtime",
547
+ false,
548
+ `managed runtime missing and the download failed (${healError?.message ?? healError}) — ` +
549
+ "Fix: run `npm install -g omnirush` again (retries the runtime download), or install Node 22.8+",
550
+ );
551
+ } else if (found) {
552
+ console.log(
553
+ `WARN runtime — using a NON-managed bun ${bunVersionOf(found.path)} from PATH (${found.path}); ` +
554
+ "the managed runtime is missing. Fix: run `npm install -g omnirush` again (retries the runtime download)",
555
+ );
556
+ } else {
557
+ const [major, minor] = process.versions.node.split(".").map(Number);
558
+ if (major > 22 || (major === 22 && minor >= 8)) {
559
+ ok("runtime", true, "no bun needed — Node " + process.versions.node + " runs the agent");
560
+ } else {
561
+ ok(
562
+ "runtime",
563
+ false,
564
+ `Node ${process.versions.node} is too old for the agent core (need >= 22.8) and the managed ` +
565
+ "runtime is missing — Fix: run `npm install -g omnirush` again (retries the runtime download), " +
566
+ "or install Node 22.8+",
567
+ );
568
+ }
569
+ }
570
+ }
424
571
  // Search tools: bundled fd/rg (or system ones) keep the agent off its
425
572
  // GitHub download path, which fails on restricted networks.
426
573
  const toolsRoot = path.join(PI_AGENT_DIR, "bin");
@@ -465,6 +612,23 @@ function requireAuth() {
465
612
  process.exit(1);
466
613
  }
467
614
 
615
+ function bunVersionOf(bunPath) {
616
+ try {
617
+ const { execFileSync } = require("node:child_process");
618
+ return String(execFileSync(bunPath, ["--version"], { timeout: 5000 })).trim() || "unknown";
619
+ } catch {
620
+ return "unknown";
621
+ }
622
+ }
623
+
624
+ function ownVersion() {
625
+ try {
626
+ return JSON.parse(fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8")).version;
627
+ } catch {
628
+ return "unknown";
629
+ }
630
+ }
631
+
468
632
  function childEnv(auth) {
469
633
  const gateway = agentGatewayUrl(auth);
470
634
  const origin = (process.env.OMNIRUSH_ORIGIN || "").trim().replace(/\/+$/, "") ||
@@ -475,34 +639,85 @@ function childEnv(auth) {
475
639
  OMNIRUSH_ORIGIN: origin,
476
640
  OMNIRUSH_GATEWAY_URL: gateway,
477
641
  OMNIRUSH_DIR: OMNI_DIR,
642
+ // /status shows the CLI version; extensions have no package.json of
643
+ // their own in ~/.pi, so the launcher hands it over.
644
+ OMNIRUSH_VERSION: ownVersion(),
478
645
  // The agent resolves its config dir from <APP>_CODING_AGENT_DIR. The
479
646
  // branding patch renames the app to omnirush, so set the omnirush
480
647
  // spelling; the pi spelling stays for unpatched cores.
481
648
  OMNIRUSH_CODING_AGENT_DIR: PI_AGENT_DIR,
482
649
  PI_CODING_AGENT_DIR: PI_AGENT_DIR,
483
650
  // The agent core is a fork/rebrand: never phone home to pi.dev.
651
+ // PI_OFFLINE kills startup update checks (the "Update Available —
652
+ // pi.dev/changelog" banner), package updates, and install/update
653
+ // telemetry; PI_TELEMETRY stays for cores that predate PI_OFFLINE.
654
+ PI_OFFLINE: "1",
484
655
  PI_TELEMETRY: "0",
485
656
  };
486
657
  }
487
658
 
659
+ /**
660
+ * Spawn the agent with launcher signal hygiene (issue #9). The terminal's
661
+ * Ctrl+C already reaches the whole foreground process group — the child
662
+ * (bun -> pi) handles it itself (first ^C interrupts the turn, second
663
+ * exits pi). The launcher therefore IGNORES SIGINT (it used to die
664
+ * immediately, orphaning the TUI and forcing a third ^C), forwards
665
+ * process-directed signals, and exits only when the child does, with the
666
+ * child's exit code or signal.
667
+ */
668
+ function spawnAgent(cmd, args, opts) {
669
+ const child = spawn(cmd, args, opts);
670
+ let childAlive = true;
671
+ let lastExit = { code: 0, signal: null };
672
+ child.on("exit", (code, signal) => {
673
+ childAlive = false;
674
+ lastExit = { code, signal };
675
+ });
676
+ const onInterrupt = () => {
677
+ if (!childAlive) {
678
+ process.exit(lastExit.code ?? 0);
679
+ }
680
+ // Otherwise let pi handle the interrupt — never double-send, never
681
+ // exit first.
682
+ };
683
+ const forward = (signal) => () => {
684
+ if (childAlive) {
685
+ try {
686
+ child.kill(signal);
687
+ } catch {
688
+ /* already gone */
689
+ }
690
+ } else {
691
+ process.exit(lastExit.code ?? 0);
692
+ }
693
+ };
694
+ process.on("SIGINT", onInterrupt);
695
+ process.on("SIGTERM", forward("SIGTERM"));
696
+ if (process.platform !== "win32") {
697
+ process.on("SIGHUP", forward("SIGHUP"));
698
+ process.on("SIGQUIT", forward("SIGQUIT"));
699
+ }
700
+ child.on("exit", (code, signal) => {
701
+ if (signal) process.kill(process.pid, signal);
702
+ else process.exit(code ?? 0);
703
+ });
704
+ return child;
705
+ }
706
+
488
707
  async function cmdRun() {
489
708
  ensureDirs();
490
709
  const auth = requireAuth();
491
710
  installModelsJson(agentGatewayUrl(auth));
492
711
  installExtensions();
493
712
  const entry = piEntry();
494
- const { cmd, args } = runtimeFor(entry);
713
+ const { cmd, args } = await runtimeFor(entry);
495
714
  // Plain `omnirush` always runs the SOTA model (charter rule 1); user
496
715
  // flags win when present.
497
716
  const piArgs = withModelDefaults([], resolveModelDefaults(process.env));
498
- const child = spawn(cmd, [...args, ...piArgs], {
717
+ spawnAgent(cmd, [...args, ...piArgs], {
499
718
  stdio: "inherit",
500
719
  env: { ...process.env, ...childEnv(auth) },
501
720
  });
502
- child.on("exit", (code, signal) => {
503
- if (signal) process.kill(process.pid, signal);
504
- else process.exit(code ?? 0);
505
- });
506
721
  }
507
722
 
508
723
  async function cmdCollect(argv) {
@@ -510,24 +725,63 @@ async function cmdCollect(argv) {
510
725
  const auth = requireAuth();
511
726
  installExtensions();
512
727
  const entry = path.join(__dirname, "..", "assets", "collect-once.ts");
513
- const { cmd, args } = runtimeFor(entry);
514
- const child = spawn(cmd, [...args, ...argv], {
728
+ const { cmd, args } = await runtimeFor(entry);
729
+ spawnAgent(cmd, [...args, ...argv], {
515
730
  stdio: "inherit",
516
731
  env: { ...process.env, ...childEnv(auth) },
517
732
  });
518
- child.on("exit", (code, signal) => {
519
- if (signal) process.kill(process.pid, signal);
520
- else process.exit(code ?? 0);
521
- });
733
+ }
734
+
735
+ // Native omnirush help (issue #8): --help/-h must render OUR commands
736
+ // and branding — never forward to the agent core's help.
737
+ function printHelp() {
738
+ console.log(`omnirush — free daily tokens for the most powerful coding model on earth
739
+
740
+ Usage:
741
+ omnirush start the agent (default: gpt-6-astra)
742
+ omnirush -p "<prompt>" run a one-shot prompt
743
+ omnirush login sign in (device flow via your browser)
744
+ omnirush logout forget the stored device tokens
745
+ omnirush whoami show the signed-in identity
746
+ omnirush doctor check auth, manager, runtime, agent config
747
+ omnirush collect [--dir PATH] one-shot workspace snapshot upload
748
+ omnirush update update omnirush (npm install -g omnirush@latest)
749
+ omnirush --version show the omnirush version
750
+
751
+ Options:
752
+ --model <model[:effort]> model override (gpt-6-astra, gpt-5.6-sol;
753
+ effort: minimal..max)
754
+ --thinking <level> reasoning effort override
755
+ --strict-sota exit non-zero if the served model is not
756
+ the requested SOTA model
757
+ --session <file> | --continue session options (forwarded to the agent)
758
+ -p, --print "<prompt>" print-mode: answer once and exit
759
+ -h, --help show this help
760
+
761
+ Session commands (inside the agent):
762
+ /grant today's token grant and usage
763
+ /usage detailed usage from the manager (counters + devices)
764
+ /status session status (identity, model, grant, collector)
765
+ /mcp MCP server state (/mcp reconnect [server])
766
+ /verify model attestation (arrives with the gateway lane)
767
+
768
+ Environment:
769
+ OMNIRUSH_ORIGIN manager origin override
770
+ OMNIRUSH_MODEL default model override (e.g. gpt-5.6-sol)
771
+ OMNIRUSH_DIR state dir override (default ~/.omnirush)
772
+ OMNIRUSH_DEBUG=1 verbose diagnostics (raw gateway errors on failure)
773
+
774
+ Docs: https://omnirush.ai/omnirush — state is stored 0600 in ~/.omnirush`);
522
775
  }
523
776
 
524
777
  const [, , ...argv] = process.argv;
525
778
  const cmd = argv[0];
526
- if (cmd === "login") await cmdLogin(argv.slice(1));
779
+ if (cmd === "-h" || cmd === "--help" || cmd === "help") printHelp();
780
+ else if (cmd === "login") await cmdLogin(argv.slice(1));
527
781
  else if (cmd === "logout") await cmdLogout();
528
782
  else if (cmd === "whoami") await cmdWhoami();
529
783
  else if (cmd === "doctor") await cmdDoctor();
530
- else if (cmd === "--version" || cmd === "-v") console.log("omnirush 0.3.3");
784
+ else if (cmd === "--version" || cmd === "-v") console.log(`omnirush ${ownVersion()}`);
531
785
  else if (cmd === "collect") await cmdCollect(argv.slice(1));
532
786
  else if (cmd === undefined || cmd === "run") await cmdRun();
533
787
  else {
@@ -540,11 +794,10 @@ else {
540
794
  installModelsJson(agentGatewayUrl(auth));
541
795
  installExtensions();
542
796
  const entry = piEntry();
543
- const { cmd: bin, args } = runtimeFor(entry);
797
+ const { cmd: bin, args } = await runtimeFor(entry);
544
798
  const piArgs = withModelDefaults(argv, resolveModelDefaults(process.env));
545
- const child = spawn(bin, [...args, ...piArgs], {
799
+ spawnAgent(bin, [...args, ...piArgs], {
546
800
  stdio: "inherit",
547
801
  env: { ...process.env, ...childEnv(auth) },
548
802
  });
549
- child.on("exit", (code) => process.exit(code ?? 0));
550
803
  }
package/src/lib.js CHANGED
@@ -221,6 +221,19 @@ export function modelsConfig({ gatewayUrl } = {}) {
221
221
  };
222
222
  }
223
223
 
224
+ /**
225
+ * Product default effort (Dan's call, 2026-09-24): xhigh, not pi's
226
+ * medium. The `thinkingLevelMap` on each model is unchanged — this only
227
+ * fills an ABSENT `defaultThinkingLevel` in the agent's settings.json,
228
+ * so an explicit user choice (saved via Ctrl+S in /thinking, or edited
229
+ * manually) always wins. Returns the settings object to persist; pure.
230
+ */
231
+ export function withDefaultThinkingLevel(settings, level = "xhigh") {
232
+ const base = settings && typeof settings === "object" && !Array.isArray(settings) ? settings : {};
233
+ if (base.defaultThinkingLevel !== undefined) return base;
234
+ return { ...base, defaultThinkingLevel: level };
235
+ }
236
+
224
237
  /**
225
238
  * Pure mapping of the bundled search-tool binaries for the launcher: pi
226
239
  * looks for managed `fd`/`rg` in <agentDir>/bin first and downloads from