omnirush 0.8.5 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/assets/CHANGELOG.md +85 -0
  2. package/assets/collect-once.ts +6 -5
  3. package/assets/extensions/omnirush/agents-lib.ts +196 -12
  4. package/assets/extensions/omnirush/agents.ts +97 -105
  5. package/assets/extensions/omnirush/auth.js +399 -115
  6. package/assets/extensions/omnirush/bgshell-lib.ts +400 -0
  7. package/assets/extensions/omnirush/bgshell.ts +392 -0
  8. package/assets/extensions/omnirush/collector.ts +103 -16
  9. package/assets/extensions/omnirush/commands.ts +14 -15
  10. package/assets/extensions/omnirush/deliveries.ts +145 -0
  11. package/assets/extensions/omnirush/guard/UPSTREAM +2 -0
  12. package/assets/extensions/omnirush/guard/git-command-policy.ts +885 -0
  13. package/assets/extensions/omnirush/guard-lib.ts +230 -0
  14. package/assets/extensions/omnirush/guard.ts +340 -0
  15. package/assets/extensions/omnirush/index.ts +12 -0
  16. package/assets/extensions/omnirush/pi-engine.ts +65 -1
  17. package/assets/extensions/omnirush/sota.ts +104 -47
  18. package/assets/extensions/omnirush/status-lib.ts +3 -0
  19. package/assets/extensions/omnirush/subagents-lib.ts +623 -0
  20. package/assets/extensions/omnirush/subagents.ts +305 -0
  21. package/assets/extensions/omnirush/swarm-lib.ts +142 -0
  22. package/assets/extensions/omnirush/swarm.ts +95 -0
  23. package/assets/extensions/omnirush/voice/capture.ts +502 -0
  24. package/assets/extensions/omnirush/voice/core/UPSTREAM +16 -0
  25. package/assets/extensions/omnirush/voice/core/file-source.ts +70 -0
  26. package/assets/extensions/omnirush/voice/core/index.ts +21 -0
  27. package/assets/extensions/omnirush/voice/core/keyterms.ts +117 -0
  28. package/assets/extensions/omnirush/voice/core/resample.ts +63 -0
  29. package/assets/extensions/omnirush/voice/core/segmenter.ts +231 -0
  30. package/assets/extensions/omnirush/voice/core/session.ts +403 -0
  31. package/assets/extensions/omnirush/voice/core/text.ts +81 -0
  32. package/assets/extensions/omnirush/voice/core/transcriber.ts +135 -0
  33. package/assets/extensions/omnirush/voice/core/types.ts +102 -0
  34. package/assets/extensions/omnirush/voice/core/wav.ts +95 -0
  35. package/assets/extensions/omnirush/voice/keys.ts +435 -0
  36. package/assets/extensions/omnirush/voice/kitty.ts +64 -0
  37. package/assets/extensions/omnirush/voice/pvrecorder-worker.cjs +43 -0
  38. package/assets/extensions/omnirush/voice/settings.ts +67 -0
  39. package/assets/extensions/omnirush/voice.ts +838 -0
  40. package/assets/extensions/omnirush/yolo-lib.ts +80 -0
  41. package/assets/extensions/omnirush/yolo.ts +85 -0
  42. package/package.json +6 -2
  43. package/scripts/brand-engine.js +526 -0
  44. package/scripts/build-all-packages.py +29 -1
  45. package/scripts/smoke-packages.py +32 -1
  46. package/src/bin.js +252 -53
  47. package/src/compat.js +272 -0
  48. package/src/lib.js +64 -0
  49. package/scripts/patch-pi-branding.js +0 -251
package/src/bin.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // omnirush — launcher for the Omnirush coding agent (pi core).
2
+ // omnirush — launcher for the Omnirush coding agent.
3
3
  //
4
4
  // The CLI is a thin client of the Omnirush manager (a single public
5
5
  // HTTPS origin, real cert — no helper proxy, no custom CA):
@@ -8,15 +8,18 @@
8
8
  // stored 0600 in ~/.omnirush/auth.json)
9
9
  // omnirush logout forget the stored tokens
10
10
  // omnirush whoami show the signed-in device identity
11
- // omnirush doctor check auth (or pending login), manager, pi config
11
+ // omnirush doctor check auth (or pending login), manager, agent config
12
12
  // omnirush collect one-shot workspace snapshot upload
13
- // omnirush run the agent (default; forwards to pi)
13
+ // omnirush update update omnirush from npm
14
+ // omnirush run the agent (default; forwards to the agent core)
14
15
  //
15
16
  // Environment:
16
17
  // OMNIRUSH_ORIGIN manager origin override (default
17
18
  // https://omnirush.ai/omnirush)
18
19
  // OMNIRUSH_GATEWAY_URL provider base URL override ({origin}/v1)
19
20
  // OMNIRUSH_DIR state dir override (default ~/.omnirush)
21
+ // OMNIRUSH_AGENT_DIR agent data dir override (default <state dir>/agent;
22
+ // OMNIRUSH_PI_AGENT_DIR is the older spelling)
20
23
  // OMNIRUSH_MODEL model override for plain runs (default
21
24
  // gpt-6-astra; gpt-6-sol, gpt-5.6-sol and the Muse
22
25
  // models also available; user
@@ -47,8 +50,11 @@ import {
47
50
  withModelDefaults,
48
51
  withDefaultThinkingLevel,
49
52
  resolveModelDefaults,
53
+ resolveYolo,
54
+ yoloBanner,
50
55
  } from "./lib.js";
51
56
  import { installManagedTools } from "./tools.js";
57
+ import { engineEnvAliases, migrateAgentDir, projectConfigDirName, resolveAgentDir } from "./compat.js";
52
58
  import { ensureRuntime, managedBunPath } from "../scripts/postinstall.js";
53
59
  import {
54
60
  clearAuth,
@@ -60,23 +66,53 @@ import {
60
66
  openBrowser,
61
67
  pollForToken,
62
68
  redactForDisplay,
63
- refreshTokens,
64
69
  saveAuth,
65
70
  savePendingFlow,
66
71
  clearPendingFlow,
72
+ createRefresher,
73
+ sessionEndedMessage,
67
74
  } from "../assets/extensions/omnirush/auth.js";
68
75
 
69
76
  const require = createRequire(import.meta.url);
70
77
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
71
78
  const OMNI_DIR = omniDir();
72
- // OMNIRUSH_PI_AGENT_DIR overrides where pi's config/extensions live
73
- // (tests use it to keep the real ~/.pi untouched).
74
- const PI_AGENT_DIR = process.env.OMNIRUSH_PI_AGENT_DIR
75
- ? path.resolve(process.env.OMNIRUSH_PI_AGENT_DIR)
76
- : path.join(os.homedir(), ".pi", "agent");
77
- const PI_MODELS_JSON = path.join(PI_AGENT_DIR, "models.json");
78
- const PI_SETTINGS_JSON = path.join(PI_AGENT_DIR, "settings.json");
79
- const PI_EXT_DIR = path.join(PI_AGENT_DIR, "extensions", "omnirush");
79
+ process.title = "omnirush";
80
+ // The agent core's data (settings, models.json, provider auth, sessions,
81
+ // extensions): <state dir>/agent, or OMNIRUSH_AGENT_DIR. Releases up to
82
+ // 0.8.x used the core's upstream default folder; ensureDirs() copies it
83
+ // over once (src/compat.js) and falls back to it for a run whose copy failed.
84
+ const AGENT = resolveAgentDir(process.env, { stateDir: OMNI_DIR });
85
+ let PI_AGENT_DIR, PI_MODELS_JSON, PI_SETTINGS_JSON, PI_EXT_DIR;
86
+ function useAgentDir(dir) {
87
+ PI_AGENT_DIR = dir;
88
+ PI_MODELS_JSON = path.join(dir, "models.json");
89
+ PI_SETTINGS_JSON = path.join(dir, "settings.json");
90
+ PI_EXT_DIR = path.join(dir, "extensions", "omnirush");
91
+ }
92
+ useAgentDir(AGENT.dir);
93
+
94
+ /** One-time copy of the pre-0.9 agent data into the new folder (see src/compat.js). */
95
+ function migrateLegacyAgentData() {
96
+ if (!AGENT.legacyDir) return;
97
+ const result = migrateAgentDir({
98
+ from: AGENT.legacyDir,
99
+ to: AGENT.dir,
100
+ onStart: () => console.error(`omnirush: copying your sessions and agent settings to ${AGENT.dir} (one time)...`),
101
+ });
102
+ if (result.status === "migrated") {
103
+ console.error(
104
+ `omnirush: done (${result.copied} files copied${result.merged ? `, ${result.merged} merged` : ""}; ` +
105
+ "the previous folder was left as it was).",
106
+ );
107
+ } else if (result.status === "failed") {
108
+ // Never lose data: run on the previous folder and retry next launch.
109
+ console.error(
110
+ `omnirush: could not copy your agent data to ${AGENT.dir} (${result.error?.message ?? result.error}); ` +
111
+ "using the previous folder for this run, retrying next time.",
112
+ );
113
+ useAgentDir(AGENT.legacyDir);
114
+ }
115
+ }
80
116
 
81
117
  function managerOrigin() {
82
118
  return (
@@ -110,11 +146,12 @@ function piPackage() {
110
146
  function piEntry() {
111
147
  const found = piPackage();
112
148
  if (found?.pkg?.bin?.pi) return path.join(found.dir, found.pkg.bin.pi);
113
- throw new Error("could not locate the pinned pi CLI (@earendil-works/pi-coding-agent)");
149
+ throw new Error("could not locate the pinned agent core (broken install). Fix: npm install -g omnirush@latest");
114
150
  }
115
151
 
116
152
  function ensureDirs() {
117
153
  fs.mkdirSync(OMNI_DIR, { recursive: true, mode: 0o700 });
154
+ migrateLegacyAgentData();
118
155
  fs.mkdirSync(PI_AGENT_DIR, { recursive: true });
119
156
  fs.mkdirSync(PI_EXT_DIR, { recursive: true });
120
157
  try {
@@ -162,10 +199,21 @@ async function launchModels(gatewayUrl, accessToken) {
162
199
  const base = String(gatewayUrl).replace(/\/+$/, "");
163
200
  if (process.env.OMNIRUSH_STATIC_MODELS !== "1" && accessToken) {
164
201
  try {
165
- const response = await fetch(`${base}/models`, {
166
- headers: { Authorization: `Bearer ${accessToken}`, Accept: "application/json" },
202
+ const get = (token) => fetch(`${base}/models`, {
203
+ headers: { Authorization: `Bearer ${token}`, Accept: "application/json" },
167
204
  signal: AbortSignal.timeout(2500),
168
205
  });
206
+ let response = await get(accessToken);
207
+ if (response.status === 401) {
208
+ // The stored access token expired (or another process rotated):
209
+ // refresh now, under the shared lock, so the agent starts with a
210
+ // working pair on disk.
211
+ await response.body?.cancel().catch(() => undefined);
212
+ const outcome = await createRefresher({ dir: OMNI_DIR, origin: managerOrigin() })
213
+ .recover(accessToken)
214
+ .catch(() => null);
215
+ if (outcome?.status === "ok") response = await get(outcome.accessToken);
216
+ }
169
217
  if (response.ok) {
170
218
  const payload = await response.json();
171
219
  const models = catalogFromGateway(payload);
@@ -221,7 +269,7 @@ function installExtensions() {
221
269
  fs.mkdirSync(to, { recursive: true });
222
270
  for (const entry of fs.readdirSync(from, { withFileTypes: true })) {
223
271
  if (entry.isDirectory()) copy(path.join(from, entry.name), path.join(to, entry.name));
224
- else if (entry.name.endsWith(".ts") || entry.name.endsWith(".js")) {
272
+ else if (entry.name.endsWith(".ts") || entry.name.endsWith(".js") || entry.name.endsWith(".cjs")) {
225
273
  fs.copyFileSync(path.join(from, entry.name), path.join(to, entry.name));
226
274
  }
227
275
  }
@@ -444,15 +492,24 @@ async function cmdLogout() {
444
492
  console.log(`Signed out. Credentials removed from ${OMNI_DIR}`);
445
493
  }
446
494
 
447
- async function fetchMeWithRefresh(auth) {
495
+ /**
496
+ * GET /device/me with the current on-disk token; a 401 adopts a pair
497
+ * another process already rotated, or refreshes under the shared refresh
498
+ * lock (never a bare refresh that could race a running agent). Resolves
499
+ * {me} or {me: null, outcome} with the recover() verdict.
500
+ */
501
+ async function fetchMeWithRefresh() {
448
502
  const origin = managerOrigin();
449
- let me = await deviceMe(origin, { accessToken: auth.accessToken, fetchImpl: globalThis.fetch });
450
- if (me !== null) return { me, auth };
451
- // 401: single-flight refresh (rotate both), retry once.
452
- const next = await refreshTokens(origin, { refreshToken: auth.refreshToken, fetchImpl: globalThis.fetch });
453
- saveAuth(OMNI_DIR, next);
454
- me = await deviceMe(origin, { accessToken: next.accessToken, fetchImpl: globalThis.fetch });
455
- return { me, auth: next };
503
+ const refresher = createRefresher({ dir: OMNI_DIR, origin });
504
+ let token = refresher.accessToken();
505
+ for (let round = 0; round < 3; round++) {
506
+ const me = await deviceMe(origin, { accessToken: token, fetchImpl: globalThis.fetch });
507
+ if (me !== null) return { me };
508
+ const outcome = await refresher.recover(token);
509
+ if (outcome.status !== "ok") return { me: null, outcome };
510
+ token = outcome.accessToken;
511
+ }
512
+ return { me: null, outcome: { status: "dead", reason: "session", detail: "access token refused after refresh" } };
456
513
  }
457
514
 
458
515
  async function cmdWhoami() {
@@ -461,9 +518,13 @@ async function cmdWhoami() {
461
518
  console.error("Omnirush: not signed in. Run `omnirush login` first.");
462
519
  process.exit(1);
463
520
  }
464
- const { me } = await fetchMeWithRefresh(auth);
521
+ const { me, outcome } = await fetchMeWithRefresh();
465
522
  if (!me) {
466
- console.error("Omnirush: credentials rejected — run `omnirush login` again.");
523
+ console.error(
524
+ outcome?.status === "transient"
525
+ ? `Omnirush: cannot reach the manager to verify the session (${outcome.error}) — try again shortly.`
526
+ : sessionEndedMessage(outcome),
527
+ );
467
528
  process.exit(1);
468
529
  }
469
530
  console.log(JSON.stringify(redactForDisplay(me), null, 2));
@@ -491,21 +552,19 @@ async function cmdDoctor() {
491
552
  const auth = loadAuth(OMNI_DIR);
492
553
  if (auth) {
493
554
  try {
494
- const { me } = await fetchMeWithRefresh(auth);
555
+ const { me, outcome } = await fetchMeWithRefresh();
495
556
  if (me) {
496
557
  const label =
497
558
  (me && typeof me === "object" && (me.device_name || me.email || me.user_id || me.id)) ||
498
559
  "device recognized";
499
560
  ok("auth", true, `signed in (${label})`);
561
+ } else if (outcome?.status === "transient") {
562
+ ok("auth", false, `cannot verify session (${outcome.error})`);
500
563
  } else {
501
- ok("auth", false, "credentials rejected — run `omnirush login` again");
564
+ ok("auth", false, sessionEndedMessage(outcome).replace(/^Omnirush: /, ""));
502
565
  }
503
566
  } catch (e) {
504
- if (e?.name === "RefreshRejectedError") {
505
- ok("auth", false, "session expired — run `omnirush login` again");
506
- } else {
507
- ok("auth", false, `cannot verify session (${e.message})`);
508
- }
567
+ ok("auth", false, `cannot verify session (${e.message})`);
509
568
  }
510
569
  } else {
511
570
  const pending = loadPendingFlow(OMNI_DIR);
@@ -706,21 +765,33 @@ function childEnv(auth) {
706
765
  const origin = (process.env.OMNIRUSH_ORIGIN || "").trim().replace(/\/+$/, "") ||
707
766
  gateway.replace(/\/+$/, "").replace(/\/v1$/, ""); // provider base -> manager origin
708
767
  return {
768
+ // OMNIRUSH_* spellings of the core's own settings (PI_* still work).
769
+ ...engineEnvAliases(process.env),
770
+ // Bootstrap values only: the agent (and every sub-agent it spawns)
771
+ // re-reads the shared auth file in OMNIRUSH_DIR for each request, so
772
+ // these never go stale in a way that matters.
709
773
  OMNIRUSH_TOKEN: auth.accessToken,
710
774
  OMNIRUSH_ACCESS_TOKEN: auth.accessToken,
711
775
  OMNIRUSH_ORIGIN: origin,
712
776
  OMNIRUSH_GATEWAY_URL: gateway,
713
777
  OMNIRUSH_DIR: OMNI_DIR,
714
778
  // /status shows the CLI version; extensions have no package.json of
715
- // their own in ~/.pi, so the launcher hands it over.
779
+ // their own in the agent dir, so the launcher hands it over.
716
780
  OMNIRUSH_VERSION: ownVersion(),
717
781
  // The collector reports it as the environment's engine_version.
718
782
  OMNIRUSH_ENGINE_VERSION: engineVersion(),
783
+ // Where this package lives: extensions (copied to the agent dir) load optional
784
+ // native dependencies from here (voice: @picovoice/pvrecorder-node).
785
+ OMNIRUSH_PACKAGE_ROOT: path.resolve(__dirname, ".."),
719
786
  // The agent resolves its config dir from <APP>_CODING_AGENT_DIR. The
720
787
  // branding patch renames the app to omnirush, so set the omnirush
721
788
  // spelling; the pi spelling stays for unpatched cores.
722
789
  OMNIRUSH_CODING_AGENT_DIR: PI_AGENT_DIR,
723
790
  PI_CODING_AGENT_DIR: PI_AGENT_DIR,
791
+ // Project settings live in <project>/.omnirush; a project that only
792
+ // has the pre-0.9 folder keeps using that one (the branding patch
793
+ // makes the core read this variable).
794
+ OMNIRUSH_PROJECT_CONFIG_DIR: projectConfigDirName(process.cwd()),
724
795
  // The agent core is a fork/rebrand: never phone home to pi.dev.
725
796
  // PI_OFFLINE kills startup update checks (the "Update Available —
726
797
  // pi.dev/changelog" banner), package updates, and install/update
@@ -778,6 +849,32 @@ function spawnAgent(cmd, args, opts) {
778
849
  return child;
779
850
  }
780
851
 
852
+ /** The saved /yolo setting ("yolo": true in <state dir>/settings.json). */
853
+ function yoloSetting() {
854
+ try {
855
+ return JSON.parse(fs.readFileSync(path.join(OMNI_DIR, "settings.json"), "utf8"))?.yolo === true;
856
+ } catch {
857
+ return false;
858
+ }
859
+ }
860
+
861
+ /**
862
+ * Yolo mode for an agent launch: strips --yolo/--no-yolo, adds the core's
863
+ * --approve (project trusted for the run) and hands the state to the
864
+ * extensions (OMNIRUSH_YOLO). Non-interactive runs get the banner on
865
+ * stderr here; the TUI shows it itself (yolo.ts).
866
+ */
867
+ function launchYolo(args) {
868
+ const resolved = resolveYolo(args, { env: process.env, setting: yoloSetting() });
869
+ const env = { OMNIRUSH_YOLO: resolved.yolo ? "1" : "0", OMNIRUSH_YOLO_SOURCE: resolved.source };
870
+ if (resolved.yolo) {
871
+ env.OMNIRUSH_YOLO_TRUSTED = resolved.trusted ? "1" : "0";
872
+ const headless = resolved.args.some((a) => a === "-p" || a === "--print" || a === "--mode") || !process.stdout.isTTY;
873
+ if (headless) console.error(`omnirush: ${yoloBanner(resolved)}`);
874
+ }
875
+ return { args: resolved.args, env };
876
+ }
877
+
781
878
  async function cmdRun() {
782
879
  ensureDirs();
783
880
  const auth = requireAuth();
@@ -787,10 +884,13 @@ async function cmdRun() {
787
884
  const { cmd, args } = await runtimeFor(entry);
788
885
  // Plain `omnirush` always runs the SOTA model (charter rule 1); user
789
886
  // flags win when present.
790
- const piArgs = withModelDefaults([], resolveModelDefaults(process.env));
887
+ // `omnirush run` takes no other arguments; only yolo's --approve passes.
888
+ const yolo = launchYolo(process.argv.slice(3).filter((a) => a === "--yolo" || a === "--no-yolo"));
889
+ const piArgs = withModelDefaults(yolo.args, resolveModelDefaults(process.env));
791
890
  spawnAgent(cmd, [...args, ...piArgs], {
792
891
  stdio: "inherit",
793
- env: { ...process.env, ...childEnv(auth) },
892
+ // The pair may have rotated during launch (model catalog refresh).
893
+ env: { ...process.env, ...childEnv(loadAuth(OMNI_DIR) ?? auth), ...yolo.env },
794
894
  });
795
895
  }
796
896
 
@@ -827,8 +927,18 @@ Options:
827
927
  gpt-5.6-sol, meta-muse-spark, muse-spark-1.1;
828
928
  effort: minimal..max)
829
929
  --thinking <level> reasoning effort override
930
+ --subagent-model <id|same> model the sub-agents run on for this run
931
+ (one of your account's models; default: the
932
+ one picked with /subagents, else the main model)
933
+ --subagent-effort <level|same> sub-agent effort for this run (limited to
934
+ that model's levels)
830
935
  --strict-sota exit non-zero if the served model is not
831
936
  the requested SOTA model
937
+ --yolo yolo mode for this run: nothing asks, the
938
+ project folder is trusted, every command is
939
+ allowed (default: guarded mode, where git
940
+ writes, destructive commands and sudo ask
941
+ first; --no-yolo turns a saved /yolo on off)
832
942
  --session <file> | --continue session options (forwarded to the agent)
833
943
  -p, --print "<prompt>" print-mode: answer once and exit
834
944
  -h, --help show this help
@@ -838,6 +948,15 @@ Session commands (inside the agent):
838
948
  /usage detailed usage from the manager (counters + devices)
839
949
  /status session status (identity, model, grant, collector)
840
950
  /mcp MCP server state (/mcp reconnect [server])
951
+ /subagents model and effort of the sub-agents (/subagents model <id|same>,
952
+ /subagents effort <level|same>; saved for the next runs)
953
+ /agents sub-agents of this session (/agents cancel <id|all>)
954
+ /jobs background shell commands (/jobs kill <id|all>)
955
+ /voice dictate prompts: hold Space to talk (/voice hold|tap|off|status)
956
+ /mode approval mode: guarded (asks before git writes and destructive
957
+ commands) or yolo (/mode guarded|yolo; saved); /approvals reset
958
+ forgets this session's "allow for this session" answers
959
+ /yolo yolo mode: no prompts, project trusted (/yolo on|off; saved)
841
960
  /verify model attestation (arrives with the gateway lane)
842
961
 
843
962
  Environment:
@@ -845,11 +964,106 @@ Environment:
845
964
  OMNIRUSH_MODEL default model override (e.g. gpt-6-sol)
846
965
  OMNIRUSH_STATIC_MODELS=1 use the shipped model list (no GET /v1/models)
847
966
  OMNIRUSH_DIR state dir override (default ~/.omnirush)
967
+ OMNIRUSH_AGENT_DIR agent data dir override (default ~/.omnirush/agent)
848
968
  OMNIRUSH_DEBUG=1 verbose diagnostics (raw gateway errors on failure)
969
+ OMNIRUSH_SUBAGENT_MODEL / OMNIRUSH_SUBAGENT_EFFORT
970
+ sub-agent model / effort for this run (as the flags)
971
+ OMNIRUSH_YOLO=1 yolo mode (as --yolo; OMNIRUSH_YOLO=0 turns it off)
972
+ OMNIRUSH_BASH_FOREGROUND_SECONDS
973
+ how long a shell command runs in the foreground before
974
+ it continues in the background (default 300)
975
+ OMNIRUSH_<SETTING> the agent core's own settings (e.g. OMNIRUSH_CACHE_RETENTION;
976
+ the older PI_<SETTING> names still work)
849
977
 
850
978
  Docs: https://omnirush.ai/omnirush — state is stored 0600 in ~/.omnirush`);
851
979
  }
852
980
 
981
+ /**
982
+ * `omnirush update` updates omnirush itself from npm. The agent core's own
983
+ * self-update would install its stock upstream package instead, so only
984
+ * package and model-catalog updates (--extensions, --extension <source>,
985
+ * --models, a package source) are handed to the core; --all does both.
986
+ */
987
+ async function cmdUpdate(args) {
988
+ if (args.includes("-h") || args.includes("--help")) return runCore(["update", ...args]);
989
+ const coreArgs = [];
990
+ let self = false;
991
+ let coreTarget = false;
992
+ for (let i = 0; i < args.length; i++) {
993
+ const arg = args[i];
994
+ // "pi" is the core's own alias for self: never let it reach the core.
995
+ if (arg === "--self" || arg === "self" || arg === "omnirush" || arg === "pi") self = true;
996
+ else if (arg === "--all") {
997
+ self = true;
998
+ coreTarget = true;
999
+ coreArgs.push("--extensions");
1000
+ } else if (arg === "--force") continue;
1001
+ else {
1002
+ if (arg === "--extensions" || arg === "--models" || arg === "--extension" || !arg.startsWith("-")) coreTarget = true;
1003
+ coreArgs.push(arg);
1004
+ if (arg === "--extension" && i + 1 < args.length) coreArgs.push(args[++i]);
1005
+ }
1006
+ }
1007
+ if (!coreTarget) self = true;
1008
+ if (self) {
1009
+ const status = selfUpdate();
1010
+ if (status !== 0) process.exit(status);
1011
+ }
1012
+ if (coreTarget) await runCore(["update", ...coreArgs]);
1013
+ }
1014
+
1015
+ function selfUpdate() {
1016
+ const { spawnSync } = require("node:child_process");
1017
+ const npm = process.platform === "win32" ? "npm.cmd" : "npm";
1018
+ const packageRoot = path.resolve(__dirname, "..");
1019
+ let globalRoot = "";
1020
+ try {
1021
+ globalRoot = String(
1022
+ spawnSync(npm, ["root", "-g"], { encoding: "utf8", shell: process.platform === "win32", timeout: 30_000 }).stdout || "",
1023
+ ).trim();
1024
+ } catch {
1025
+ /* npm missing: handled below */
1026
+ }
1027
+ const npmManaged =
1028
+ !!globalRoot && path.relative(path.resolve(globalRoot), packageRoot) === "omnirush";
1029
+ if (!npmManaged) {
1030
+ console.error(
1031
+ `omnirush: this copy (${packageRoot}) was not installed with \`npm install -g omnirush\`. ` +
1032
+ "Update it the way you installed it, or switch to the npm package: npm install -g omnirush@latest",
1033
+ );
1034
+ return 1;
1035
+ }
1036
+ console.log(`Updating omnirush ${ownVersion()}: npm install -g omnirush@latest`);
1037
+ const result = spawnSync(npm, ["install", "-g", "omnirush@latest"], {
1038
+ stdio: "inherit",
1039
+ shell: process.platform === "win32",
1040
+ });
1041
+ if (result.status !== 0) {
1042
+ console.error("omnirush update failed. Run it yourself: npm install -g omnirush@latest");
1043
+ return result.status || 1;
1044
+ }
1045
+ return 0;
1046
+ }
1047
+
1048
+ // Forward everything else (including argv[0] — e.g. `-p prompt`,
1049
+ // `--provider x`, package subcommands) to the agent core, defaulting the
1050
+ // model the same way `omnirush run` does; user-specified --provider/--model
1051
+ // are never duplicated and core subcommands pass through untouched.
1052
+ async function runCore(args) {
1053
+ ensureDirs();
1054
+ const auth = requireAuth();
1055
+ installModelsJson(agentGatewayUrl(auth), await launchModels(agentGatewayUrl(auth), auth.accessToken));
1056
+ installExtensions();
1057
+ const entry = piEntry();
1058
+ const { cmd: bin, args: runtimeArgs } = await runtimeFor(entry);
1059
+ const yolo = launchYolo(args);
1060
+ const piArgs = withModelDefaults(yolo.args, resolveModelDefaults(process.env));
1061
+ spawnAgent(bin, [...runtimeArgs, ...piArgs], {
1062
+ stdio: "inherit",
1063
+ env: { ...process.env, ...childEnv(auth), ...yolo.env },
1064
+ });
1065
+ }
1066
+
853
1067
  const [, , ...argv] = process.argv;
854
1068
  const cmd = argv[0];
855
1069
  if (cmd === "-h" || cmd === "--help" || cmd === "help") printHelp();
@@ -860,20 +1074,5 @@ else if (cmd === "doctor") await cmdDoctor();
860
1074
  else if (cmd === "--version" || cmd === "-v") console.log(`omnirush ${ownVersion()}`);
861
1075
  else if (cmd === "collect") await cmdCollect(argv.slice(1));
862
1076
  else if (cmd === undefined || cmd === "run") await cmdRun();
863
- else {
864
- // Forward everything (including argv[0] — e.g. `--help`, `-p prompt`,
865
- // `--provider x`) to pi, defaulting the model the same way
866
- // `omnirush run` does; user-specified --provider/--model are never
867
- // duplicated and pi admin subcommands pass through untouched.
868
- ensureDirs();
869
- const auth = requireAuth();
870
- installModelsJson(agentGatewayUrl(auth), await launchModels(agentGatewayUrl(auth), auth.accessToken));
871
- installExtensions();
872
- const entry = piEntry();
873
- const { cmd: bin, args } = await runtimeFor(entry);
874
- const piArgs = withModelDefaults(argv, resolveModelDefaults(process.env));
875
- spawnAgent(bin, [...args, ...piArgs], {
876
- stdio: "inherit",
877
- env: { ...process.env, ...childEnv(auth) },
878
- });
879
- }
1077
+ else if (cmd === "update") await cmdUpdate(argv.slice(1));
1078
+ else await runCore(argv);