omnirush 0.4.1 → 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.
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() {
@@ -116,6 +119,29 @@ function ensureDirs() {
116
119
  } catch {
117
120
  /* best effort — pi falls back to PATH, then its own downloader */
118
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
+ }
119
145
  }
120
146
 
121
147
  // Merge our provider into the user's models.json without clobbering
@@ -163,36 +189,44 @@ function isMusl() {
163
189
  }
164
190
 
165
191
  function findBun() {
166
- // Vendored per-platform bun first, then PATH, then the standard
167
- // single-user install location. Linux packages ship both the glibc and
168
- // 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.
169
198
  const runtimeRoot = path.resolve(__dirname, "..", ".runtime");
170
199
  const plat = process.platform;
171
200
  const arch = process.arch === "arm64" ? "aarch64" : "x64";
172
- const candidates = [];
201
+ const managedCandidates = [];
173
202
  if (plat === "win32") {
174
- candidates.push(
175
- path.join(runtimeRoot, "bun-windows-x64", "bin", "bun.exe"),
176
- );
177
- for (const dir of (process.env.PATH || "").split(path.delimiter)) {
178
- if (dir) candidates.push(path.join(dir, "bun.exe"));
179
- }
203
+ managedCandidates.push(managedBunPath());
204
+ managedCandidates.push(path.join(runtimeRoot, "bun-windows-x64", "bin", "bun.exe"));
180
205
  } else {
181
206
  if (plat === "linux") {
182
- if (isMusl()) candidates.push(path.join(runtimeRoot, "bin", "bun-musl"));
183
- else if (arch === "x64") candidates.push(path.join(runtimeRoot, "bin", "bun"));
184
- 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" : ""}`));
185
210
  }
186
- candidates.push(path.join(runtimeRoot, `bun-${plat}-${arch}`, "bin", "bun"));
187
- for (const dir of (process.env.PATH || "").split(path.delimiter)) {
188
- 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 */
189
219
  }
190
- candidates.push(path.join(os.homedir(), ".bun", "bin", "bun"));
191
220
  }
192
- 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) {
193
227
  try {
194
228
  fs.accessSync(c, fs.constants.X_OK);
195
- return c;
229
+ return { path: c, managed: false };
196
230
  } catch {
197
231
  /* next */
198
232
  }
@@ -200,19 +234,57 @@ function findBun() {
200
234
  return null;
201
235
  }
202
236
 
203
- 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) {
204
264
  // pi is Bun-first; its CLI imports node:module#enableCompileCache,
205
- // which needs Node >= 22.8. Prefer bun when present, else enforce the
206
- // Node floor with a clear message.
207
- const bun = findBun();
208
- 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
+ }
209
279
  const [major, minor] = process.versions.node.split(".").map(Number);
210
280
  if (major > 22 || (major === 22 && minor >= 8)) {
211
281
  return { cmd: process.execPath, args: [entry] };
212
282
  }
213
283
  console.error(
214
- `omnirush: Node ${process.versions.node} is too old for the agent core ` +
215
- "(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+.",
216
288
  );
217
289
  process.exit(1);
218
290
  }
@@ -448,6 +520,54 @@ async function cmdDoctor() {
448
520
  );
449
521
  }
450
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
+ }
451
571
  // Search tools: bundled fd/rg (or system ones) keep the agent off its
452
572
  // GitHub download path, which fails on restricted networks.
453
573
  const toolsRoot = path.join(PI_AGENT_DIR, "bin");
@@ -492,6 +612,15 @@ function requireAuth() {
492
612
  process.exit(1);
493
613
  }
494
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
+
495
624
  function ownVersion() {
496
625
  try {
497
626
  return JSON.parse(fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8")).version;
@@ -527,24 +656,68 @@ function childEnv(auth) {
527
656
  };
528
657
  }
529
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
+
530
707
  async function cmdRun() {
531
708
  ensureDirs();
532
709
  const auth = requireAuth();
533
710
  installModelsJson(agentGatewayUrl(auth));
534
711
  installExtensions();
535
712
  const entry = piEntry();
536
- const { cmd, args } = runtimeFor(entry);
713
+ const { cmd, args } = await runtimeFor(entry);
537
714
  // Plain `omnirush` always runs the SOTA model (charter rule 1); user
538
715
  // flags win when present.
539
716
  const piArgs = withModelDefaults([], resolveModelDefaults(process.env));
540
- const child = spawn(cmd, [...args, ...piArgs], {
717
+ spawnAgent(cmd, [...args, ...piArgs], {
541
718
  stdio: "inherit",
542
719
  env: { ...process.env, ...childEnv(auth) },
543
720
  });
544
- child.on("exit", (code, signal) => {
545
- if (signal) process.kill(process.pid, signal);
546
- else process.exit(code ?? 0);
547
- });
548
721
  }
549
722
 
550
723
  async function cmdCollect(argv) {
@@ -552,20 +725,59 @@ async function cmdCollect(argv) {
552
725
  const auth = requireAuth();
553
726
  installExtensions();
554
727
  const entry = path.join(__dirname, "..", "assets", "collect-once.ts");
555
- const { cmd, args } = runtimeFor(entry);
556
- const child = spawn(cmd, [...args, ...argv], {
728
+ const { cmd, args } = await runtimeFor(entry);
729
+ spawnAgent(cmd, [...args, ...argv], {
557
730
  stdio: "inherit",
558
731
  env: { ...process.env, ...childEnv(auth) },
559
732
  });
560
- child.on("exit", (code, signal) => {
561
- if (signal) process.kill(process.pid, signal);
562
- else process.exit(code ?? 0);
563
- });
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`);
564
775
  }
565
776
 
566
777
  const [, , ...argv] = process.argv;
567
778
  const cmd = argv[0];
568
- 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));
569
781
  else if (cmd === "logout") await cmdLogout();
570
782
  else if (cmd === "whoami") await cmdWhoami();
571
783
  else if (cmd === "doctor") await cmdDoctor();
@@ -582,11 +794,10 @@ else {
582
794
  installModelsJson(agentGatewayUrl(auth));
583
795
  installExtensions();
584
796
  const entry = piEntry();
585
- const { cmd: bin, args } = runtimeFor(entry);
797
+ const { cmd: bin, args } = await runtimeFor(entry);
586
798
  const piArgs = withModelDefaults(argv, resolveModelDefaults(process.env));
587
- const child = spawn(bin, [...args, ...piArgs], {
799
+ spawnAgent(bin, [...args, ...piArgs], {
588
800
  stdio: "inherit",
589
801
  env: { ...process.env, ...childEnv(auth) },
590
802
  });
591
- child.on("exit", (code) => process.exit(code ?? 0));
592
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