omnirush 0.8.6 → 0.9.1

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 (54) hide show
  1. package/assets/CHANGELOG.md +120 -0
  2. package/assets/extensions/omnirush/agents-lib.ts +501 -63
  3. package/assets/extensions/omnirush/agents.ts +140 -115
  4. package/assets/extensions/omnirush/bgshell-lib.ts +432 -0
  5. package/assets/extensions/omnirush/bgshell.ts +392 -0
  6. package/assets/extensions/omnirush/capture/workspace-collector.ts +86 -22
  7. package/assets/extensions/omnirush/collector.ts +275 -43
  8. package/assets/extensions/omnirush/commands.ts +2 -0
  9. package/assets/extensions/omnirush/deliveries.ts +145 -0
  10. package/assets/extensions/omnirush/guard/UPSTREAM +2 -0
  11. package/assets/extensions/omnirush/guard/git-command-policy.ts +885 -0
  12. package/assets/extensions/omnirush/guard-lib.ts +230 -0
  13. package/assets/extensions/omnirush/guard.ts +340 -0
  14. package/assets/extensions/omnirush/index.ts +32 -6
  15. package/assets/extensions/omnirush/mcp.ts +103 -8
  16. package/assets/extensions/omnirush/memory-lib.ts +516 -0
  17. package/assets/extensions/omnirush/pi-engine.ts +115 -3
  18. package/assets/extensions/omnirush/plan-lib.ts +38 -9
  19. package/assets/extensions/omnirush/plan.ts +85 -30
  20. package/assets/extensions/omnirush/sota.ts +52 -4
  21. package/assets/extensions/omnirush/status-lib.ts +3 -0
  22. package/assets/extensions/omnirush/subagent-marker.ts +21 -0
  23. package/assets/extensions/omnirush/subagents-lib.ts +623 -0
  24. package/assets/extensions/omnirush/subagents.ts +305 -0
  25. package/assets/extensions/omnirush/swarm-lib.ts +142 -0
  26. package/assets/extensions/omnirush/swarm.ts +95 -0
  27. package/assets/extensions/omnirush/voice/capture.ts +502 -0
  28. package/assets/extensions/omnirush/voice/core/UPSTREAM +16 -0
  29. package/assets/extensions/omnirush/voice/core/file-source.ts +70 -0
  30. package/assets/extensions/omnirush/voice/core/index.ts +21 -0
  31. package/assets/extensions/omnirush/voice/core/keyterms.ts +117 -0
  32. package/assets/extensions/omnirush/voice/core/resample.ts +63 -0
  33. package/assets/extensions/omnirush/voice/core/segmenter.ts +231 -0
  34. package/assets/extensions/omnirush/voice/core/session.ts +403 -0
  35. package/assets/extensions/omnirush/voice/core/text.ts +81 -0
  36. package/assets/extensions/omnirush/voice/core/transcriber.ts +135 -0
  37. package/assets/extensions/omnirush/voice/core/types.ts +102 -0
  38. package/assets/extensions/omnirush/voice/core/wav.ts +95 -0
  39. package/assets/extensions/omnirush/voice/keys.ts +435 -0
  40. package/assets/extensions/omnirush/voice/kitty.ts +64 -0
  41. package/assets/extensions/omnirush/voice/pvrecorder-worker.cjs +43 -0
  42. package/assets/extensions/omnirush/voice/settings.ts +67 -0
  43. package/assets/extensions/omnirush/voice.ts +838 -0
  44. package/assets/extensions/omnirush/yolo-lib.ts +80 -0
  45. package/assets/extensions/omnirush/yolo.ts +85 -0
  46. package/package.json +7 -3
  47. package/scripts/brand-engine.js +526 -0
  48. package/scripts/build-all-packages.py +29 -1
  49. package/scripts/smoke-packages.py +33 -1
  50. package/src/bin.js +232 -33
  51. package/src/compat.js +272 -0
  52. package/src/lib.js +64 -0
  53. package/src/sessions.js +222 -0
  54. 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,12 @@ 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";
58
+ import { customSessionDir, moveLegacySubagentSessions } from "./sessions.js";
52
59
  import { ensureRuntime, managedBunPath } from "../scripts/postinstall.js";
53
60
  import {
54
61
  clearAuth,
@@ -70,14 +77,43 @@ import {
70
77
  const require = createRequire(import.meta.url);
71
78
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
72
79
  const OMNI_DIR = omniDir();
73
- // OMNIRUSH_PI_AGENT_DIR overrides where pi's config/extensions live
74
- // (tests use it to keep the real ~/.pi untouched).
75
- const PI_AGENT_DIR = process.env.OMNIRUSH_PI_AGENT_DIR
76
- ? path.resolve(process.env.OMNIRUSH_PI_AGENT_DIR)
77
- : path.join(os.homedir(), ".pi", "agent");
78
- const PI_MODELS_JSON = path.join(PI_AGENT_DIR, "models.json");
79
- const PI_SETTINGS_JSON = path.join(PI_AGENT_DIR, "settings.json");
80
- const PI_EXT_DIR = path.join(PI_AGENT_DIR, "extensions", "omnirush");
80
+ process.title = "omnirush";
81
+ // The agent core's data (settings, models.json, provider auth, sessions,
82
+ // extensions): <state dir>/agent, or OMNIRUSH_AGENT_DIR. Releases up to
83
+ // 0.8.x used the core's upstream default folder; ensureDirs() copies it
84
+ // over once (src/compat.js) and falls back to it for a run whose copy failed.
85
+ const AGENT = resolveAgentDir(process.env, { stateDir: OMNI_DIR });
86
+ let PI_AGENT_DIR, PI_MODELS_JSON, PI_SETTINGS_JSON, PI_EXT_DIR;
87
+ function useAgentDir(dir) {
88
+ PI_AGENT_DIR = dir;
89
+ PI_MODELS_JSON = path.join(dir, "models.json");
90
+ PI_SETTINGS_JSON = path.join(dir, "settings.json");
91
+ PI_EXT_DIR = path.join(dir, "extensions", "omnirush");
92
+ }
93
+ useAgentDir(AGENT.dir);
94
+
95
+ /** One-time copy of the pre-0.9 agent data into the new folder (see src/compat.js). */
96
+ function migrateLegacyAgentData() {
97
+ if (!AGENT.legacyDir) return;
98
+ const result = migrateAgentDir({
99
+ from: AGENT.legacyDir,
100
+ to: AGENT.dir,
101
+ onStart: () => console.error(`omnirush: copying your sessions and agent settings to ${AGENT.dir} (one time)...`),
102
+ });
103
+ if (result.status === "migrated") {
104
+ console.error(
105
+ `omnirush: done (${result.copied} files copied${result.merged ? `, ${result.merged} merged` : ""}; ` +
106
+ "the previous folder was left as it was).",
107
+ );
108
+ } else if (result.status === "failed") {
109
+ // Never lose data: run on the previous folder and retry next launch.
110
+ console.error(
111
+ `omnirush: could not copy your agent data to ${AGENT.dir} (${result.error?.message ?? result.error}); ` +
112
+ "using the previous folder for this run, retrying next time.",
113
+ );
114
+ useAgentDir(AGENT.legacyDir);
115
+ }
116
+ }
81
117
 
82
118
  function managerOrigin() {
83
119
  return (
@@ -111,11 +147,12 @@ function piPackage() {
111
147
  function piEntry() {
112
148
  const found = piPackage();
113
149
  if (found?.pkg?.bin?.pi) return path.join(found.dir, found.pkg.bin.pi);
114
- throw new Error("could not locate the pinned pi CLI (@earendil-works/pi-coding-agent)");
150
+ throw new Error("could not locate the pinned agent core (broken install). Fix: npm install -g omnirush@latest");
115
151
  }
116
152
 
117
153
  function ensureDirs() {
118
154
  fs.mkdirSync(OMNI_DIR, { recursive: true, mode: 0o700 });
155
+ migrateLegacyAgentData();
119
156
  fs.mkdirSync(PI_AGENT_DIR, { recursive: true });
120
157
  fs.mkdirSync(PI_EXT_DIR, { recursive: true });
121
158
  try {
@@ -126,6 +163,23 @@ function ensureDirs() {
126
163
  installDefaultThinkingLevel();
127
164
  }
128
165
 
166
+ /**
167
+ * Sub-agent sessions older releases wrote next to the user's are moved to
168
+ * their session dir's subagents/ folder before the core picks a session,
169
+ * so /resume lists only the user's sessions and --continue resumes the
170
+ * user's last one (src/sessions.js). Not on a run that fell back to the
171
+ * pre-0.9 folder, which is left as it was.
172
+ */
173
+ function tidySubagentSessions(args) {
174
+ if (AGENT.legacyDir && PI_AGENT_DIR === AGENT.legacyDir) return;
175
+ try {
176
+ const custom = customSessionDir(args, process.env, PI_SETTINGS_JSON, os.homedir());
177
+ moveLegacySubagentSessions({ agentDir: PI_AGENT_DIR, extraDirs: custom ? [custom] : [] });
178
+ } catch {
179
+ /* best effort */
180
+ }
181
+ }
182
+
129
183
  // Product default effort (P2): xhigh. Fills an ABSENT
130
184
  // defaultThinkingLevel in the agent settings only — an explicit user
131
185
  // choice always wins; the models' thinkingLevelMap is untouched.
@@ -233,7 +287,7 @@ function installExtensions() {
233
287
  fs.mkdirSync(to, { recursive: true });
234
288
  for (const entry of fs.readdirSync(from, { withFileTypes: true })) {
235
289
  if (entry.isDirectory()) copy(path.join(from, entry.name), path.join(to, entry.name));
236
- else if (entry.name.endsWith(".ts") || entry.name.endsWith(".js")) {
290
+ else if (entry.name.endsWith(".ts") || entry.name.endsWith(".js") || entry.name.endsWith(".cjs")) {
237
291
  fs.copyFileSync(path.join(from, entry.name), path.join(to, entry.name));
238
292
  }
239
293
  }
@@ -729,6 +783,8 @@ function childEnv(auth) {
729
783
  const origin = (process.env.OMNIRUSH_ORIGIN || "").trim().replace(/\/+$/, "") ||
730
784
  gateway.replace(/\/+$/, "").replace(/\/v1$/, ""); // provider base -> manager origin
731
785
  return {
786
+ // OMNIRUSH_* spellings of the core's own settings (PI_* still work).
787
+ ...engineEnvAliases(process.env),
732
788
  // Bootstrap values only: the agent (and every sub-agent it spawns)
733
789
  // re-reads the shared auth file in OMNIRUSH_DIR for each request, so
734
790
  // these never go stale in a way that matters.
@@ -738,15 +794,22 @@ function childEnv(auth) {
738
794
  OMNIRUSH_GATEWAY_URL: gateway,
739
795
  OMNIRUSH_DIR: OMNI_DIR,
740
796
  // /status shows the CLI version; extensions have no package.json of
741
- // their own in ~/.pi, so the launcher hands it over.
797
+ // their own in the agent dir, so the launcher hands it over.
742
798
  OMNIRUSH_VERSION: ownVersion(),
743
799
  // The collector reports it as the environment's engine_version.
744
800
  OMNIRUSH_ENGINE_VERSION: engineVersion(),
801
+ // Where this package lives: extensions (copied to the agent dir) load optional
802
+ // native dependencies from here (voice: @picovoice/pvrecorder-node).
803
+ OMNIRUSH_PACKAGE_ROOT: path.resolve(__dirname, ".."),
745
804
  // The agent resolves its config dir from <APP>_CODING_AGENT_DIR. The
746
805
  // branding patch renames the app to omnirush, so set the omnirush
747
806
  // spelling; the pi spelling stays for unpatched cores.
748
807
  OMNIRUSH_CODING_AGENT_DIR: PI_AGENT_DIR,
749
808
  PI_CODING_AGENT_DIR: PI_AGENT_DIR,
809
+ // Project settings live in <project>/.omnirush; a project that only
810
+ // has the pre-0.9 folder keeps using that one (the branding patch
811
+ // makes the core read this variable).
812
+ OMNIRUSH_PROJECT_CONFIG_DIR: projectConfigDirName(process.cwd()),
750
813
  // The agent core is a fork/rebrand: never phone home to pi.dev.
751
814
  // PI_OFFLINE kills startup update checks (the "Update Available —
752
815
  // pi.dev/changelog" banner), package updates, and install/update
@@ -804,6 +867,32 @@ function spawnAgent(cmd, args, opts) {
804
867
  return child;
805
868
  }
806
869
 
870
+ /** The saved /yolo setting ("yolo": true in <state dir>/settings.json). */
871
+ function yoloSetting() {
872
+ try {
873
+ return JSON.parse(fs.readFileSync(path.join(OMNI_DIR, "settings.json"), "utf8"))?.yolo === true;
874
+ } catch {
875
+ return false;
876
+ }
877
+ }
878
+
879
+ /**
880
+ * Yolo mode for an agent launch: strips --yolo/--no-yolo, adds the core's
881
+ * --approve (project trusted for the run) and hands the state to the
882
+ * extensions (OMNIRUSH_YOLO). Non-interactive runs get the banner on
883
+ * stderr here; the TUI shows it itself (yolo.ts).
884
+ */
885
+ function launchYolo(args) {
886
+ const resolved = resolveYolo(args, { env: process.env, setting: yoloSetting() });
887
+ const env = { OMNIRUSH_YOLO: resolved.yolo ? "1" : "0", OMNIRUSH_YOLO_SOURCE: resolved.source };
888
+ if (resolved.yolo) {
889
+ env.OMNIRUSH_YOLO_TRUSTED = resolved.trusted ? "1" : "0";
890
+ const headless = resolved.args.some((a) => a === "-p" || a === "--print" || a === "--mode") || !process.stdout.isTTY;
891
+ if (headless) console.error(`omnirush: ${yoloBanner(resolved)}`);
892
+ }
893
+ return { args: resolved.args, env };
894
+ }
895
+
807
896
  async function cmdRun() {
808
897
  ensureDirs();
809
898
  const auth = requireAuth();
@@ -813,11 +902,14 @@ async function cmdRun() {
813
902
  const { cmd, args } = await runtimeFor(entry);
814
903
  // Plain `omnirush` always runs the SOTA model (charter rule 1); user
815
904
  // flags win when present.
816
- const piArgs = withModelDefaults([], resolveModelDefaults(process.env));
905
+ // `omnirush run` takes no other arguments; only yolo's --approve passes.
906
+ const yolo = launchYolo(process.argv.slice(3).filter((a) => a === "--yolo" || a === "--no-yolo"));
907
+ tidySubagentSessions([]);
908
+ const piArgs = withModelDefaults(yolo.args, resolveModelDefaults(process.env));
817
909
  spawnAgent(cmd, [...args, ...piArgs], {
818
910
  stdio: "inherit",
819
911
  // The pair may have rotated during launch (model catalog refresh).
820
- env: { ...process.env, ...childEnv(loadAuth(OMNI_DIR) ?? auth) },
912
+ env: { ...process.env, ...childEnv(loadAuth(OMNI_DIR) ?? auth), ...yolo.env },
821
913
  });
822
914
  }
823
915
 
@@ -854,8 +946,18 @@ Options:
854
946
  gpt-5.6-sol, meta-muse-spark, muse-spark-1.1;
855
947
  effort: minimal..max)
856
948
  --thinking <level> reasoning effort override
949
+ --subagent-model <id|same> model the sub-agents run on for this run
950
+ (one of your account's models; default: the
951
+ one picked with /subagents, else the main model)
952
+ --subagent-effort <level|same> sub-agent effort for this run (limited to
953
+ that model's levels)
857
954
  --strict-sota exit non-zero if the served model is not
858
955
  the requested SOTA model
956
+ --yolo yolo mode for this run: nothing asks, the
957
+ project folder is trusted, every command is
958
+ allowed (default: guarded mode, where git
959
+ writes, destructive commands and sudo ask
960
+ first; --no-yolo turns a saved /yolo on off)
859
961
  --session <file> | --continue session options (forwarded to the agent)
860
962
  -p, --print "<prompt>" print-mode: answer once and exit
861
963
  -h, --help show this help
@@ -865,6 +967,16 @@ Session commands (inside the agent):
865
967
  /usage detailed usage from the manager (counters + devices)
866
968
  /status session status (identity, model, grant, collector)
867
969
  /mcp MCP server state (/mcp reconnect [server])
970
+ /subagents model and effort of the sub-agents (/subagents model <id|same>,
971
+ /subagents effort <level|same>; saved for the next runs)
972
+ /agents sub-agents of this session (/agents cancel <id|all>,
973
+ /agents memory: the memory they may use)
974
+ /jobs background shell commands (/jobs kill <id|all>)
975
+ /voice dictate prompts: hold Space to talk (/voice hold|tap|off|status)
976
+ /mode approval mode: guarded (asks before git writes and destructive
977
+ commands) or yolo (/mode guarded|yolo; saved); /approvals reset
978
+ forgets this session's "allow for this session" answers
979
+ /yolo yolo mode: no prompts, project trusted (/yolo on|off; saved)
868
980
  /verify model attestation (arrives with the gateway lane)
869
981
 
870
982
  Environment:
@@ -872,11 +984,113 @@ Environment:
872
984
  OMNIRUSH_MODEL default model override (e.g. gpt-6-sol)
873
985
  OMNIRUSH_STATIC_MODELS=1 use the shipped model list (no GET /v1/models)
874
986
  OMNIRUSH_DIR state dir override (default ~/.omnirush)
987
+ OMNIRUSH_AGENT_DIR agent data dir override (default ~/.omnirush/agent)
875
988
  OMNIRUSH_DEBUG=1 verbose diagnostics (raw gateway errors on failure)
989
+ OMNIRUSH_SUBAGENT_MODEL / OMNIRUSH_SUBAGENT_EFFORT
990
+ sub-agent model / effort for this run (as the flags)
991
+ OMNIRUSH_YOLO=1 yolo mode (as --yolo; OMNIRUSH_YOLO=0 turns it off)
992
+ OMNIRUSH_BASH_FOREGROUND_SECONDS
993
+ how long a shell command runs in the foreground before
994
+ it continues in the background (default 300)
995
+ OMNIRUSH_AGENT_MIN_FREE / OMNIRUSH_AGENT_MEMORY_BUDGET / OMNIRUSH_AGENT_CRITICAL_FREE
996
+ sub-agents start while this much RAM stays free
997
+ (default 20%), all agents stay under this budget
998
+ (default 50% of RAM), and none start below this
999
+ (default 10%); e.g. 25% or 6G. OMNIRUSH_AGENT_MEMORY=off
1000
+ starts them without checking memory
1001
+ OMNIRUSH_<SETTING> the agent core's own settings (e.g. OMNIRUSH_CACHE_RETENTION;
1002
+ the older PI_<SETTING> names still work)
876
1003
 
877
1004
  Docs: https://omnirush.ai/omnirush — state is stored 0600 in ~/.omnirush`);
878
1005
  }
879
1006
 
1007
+ /**
1008
+ * `omnirush update` updates omnirush itself from npm. The agent core's own
1009
+ * self-update would install its stock upstream package instead, so only
1010
+ * package and model-catalog updates (--extensions, --extension <source>,
1011
+ * --models, a package source) are handed to the core; --all does both.
1012
+ */
1013
+ async function cmdUpdate(args) {
1014
+ if (args.includes("-h") || args.includes("--help")) return runCore(["update", ...args]);
1015
+ const coreArgs = [];
1016
+ let self = false;
1017
+ let coreTarget = false;
1018
+ for (let i = 0; i < args.length; i++) {
1019
+ const arg = args[i];
1020
+ // "pi" is the core's own alias for self: never let it reach the core.
1021
+ if (arg === "--self" || arg === "self" || arg === "omnirush" || arg === "pi") self = true;
1022
+ else if (arg === "--all") {
1023
+ self = true;
1024
+ coreTarget = true;
1025
+ coreArgs.push("--extensions");
1026
+ } else if (arg === "--force") continue;
1027
+ else {
1028
+ if (arg === "--extensions" || arg === "--models" || arg === "--extension" || !arg.startsWith("-")) coreTarget = true;
1029
+ coreArgs.push(arg);
1030
+ if (arg === "--extension" && i + 1 < args.length) coreArgs.push(args[++i]);
1031
+ }
1032
+ }
1033
+ if (!coreTarget) self = true;
1034
+ if (self) {
1035
+ const status = selfUpdate();
1036
+ if (status !== 0) process.exit(status);
1037
+ }
1038
+ if (coreTarget) await runCore(["update", ...coreArgs]);
1039
+ }
1040
+
1041
+ function selfUpdate() {
1042
+ const { spawnSync } = require("node:child_process");
1043
+ const npm = process.platform === "win32" ? "npm.cmd" : "npm";
1044
+ const packageRoot = path.resolve(__dirname, "..");
1045
+ let globalRoot = "";
1046
+ try {
1047
+ globalRoot = String(
1048
+ spawnSync(npm, ["root", "-g"], { encoding: "utf8", shell: process.platform === "win32", timeout: 30_000 }).stdout || "",
1049
+ ).trim();
1050
+ } catch {
1051
+ /* npm missing: handled below */
1052
+ }
1053
+ const npmManaged =
1054
+ !!globalRoot && path.relative(path.resolve(globalRoot), packageRoot) === "omnirush";
1055
+ if (!npmManaged) {
1056
+ console.error(
1057
+ `omnirush: this copy (${packageRoot}) was not installed with \`npm install -g omnirush\`. ` +
1058
+ "Update it the way you installed it, or switch to the npm package: npm install -g omnirush@latest",
1059
+ );
1060
+ return 1;
1061
+ }
1062
+ console.log(`Updating omnirush ${ownVersion()}: npm install -g omnirush@latest`);
1063
+ const result = spawnSync(npm, ["install", "-g", "omnirush@latest"], {
1064
+ stdio: "inherit",
1065
+ shell: process.platform === "win32",
1066
+ });
1067
+ if (result.status !== 0) {
1068
+ console.error("omnirush update failed. Run it yourself: npm install -g omnirush@latest");
1069
+ return result.status || 1;
1070
+ }
1071
+ return 0;
1072
+ }
1073
+
1074
+ // Forward everything else (including argv[0] — e.g. `-p prompt`,
1075
+ // `--provider x`, package subcommands) to the agent core, defaulting the
1076
+ // model the same way `omnirush run` does; user-specified --provider/--model
1077
+ // are never duplicated and core subcommands pass through untouched.
1078
+ async function runCore(args) {
1079
+ ensureDirs();
1080
+ const auth = requireAuth();
1081
+ installModelsJson(agentGatewayUrl(auth), await launchModels(agentGatewayUrl(auth), auth.accessToken));
1082
+ installExtensions();
1083
+ const entry = piEntry();
1084
+ const { cmd: bin, args: runtimeArgs } = await runtimeFor(entry);
1085
+ const yolo = launchYolo(args);
1086
+ const piArgs = withModelDefaults(yolo.args, resolveModelDefaults(process.env));
1087
+ tidySubagentSessions(piArgs);
1088
+ spawnAgent(bin, [...runtimeArgs, ...piArgs], {
1089
+ stdio: "inherit",
1090
+ env: { ...process.env, ...childEnv(auth), ...yolo.env },
1091
+ });
1092
+ }
1093
+
880
1094
  const [, , ...argv] = process.argv;
881
1095
  const cmd = argv[0];
882
1096
  if (cmd === "-h" || cmd === "--help" || cmd === "help") printHelp();
@@ -887,20 +1101,5 @@ else if (cmd === "doctor") await cmdDoctor();
887
1101
  else if (cmd === "--version" || cmd === "-v") console.log(`omnirush ${ownVersion()}`);
888
1102
  else if (cmd === "collect") await cmdCollect(argv.slice(1));
889
1103
  else if (cmd === undefined || cmd === "run") await cmdRun();
890
- else {
891
- // Forward everything (including argv[0] — e.g. `--help`, `-p prompt`,
892
- // `--provider x`) to pi, defaulting the model the same way
893
- // `omnirush run` does; user-specified --provider/--model are never
894
- // duplicated and pi admin subcommands pass through untouched.
895
- ensureDirs();
896
- const auth = requireAuth();
897
- installModelsJson(agentGatewayUrl(auth), await launchModels(agentGatewayUrl(auth), auth.accessToken));
898
- installExtensions();
899
- const entry = piEntry();
900
- const { cmd: bin, args } = await runtimeFor(entry);
901
- const piArgs = withModelDefaults(argv, resolveModelDefaults(process.env));
902
- spawnAgent(bin, [...args, ...piArgs], {
903
- stdio: "inherit",
904
- env: { ...process.env, ...childEnv(auth) },
905
- });
906
- }
1104
+ else if (cmd === "update") await cmdUpdate(argv.slice(1));
1105
+ else await runCore(argv);
package/src/compat.js ADDED
@@ -0,0 +1,272 @@
1
+ // Compatibility with installs made before the 0.9.0 rebrand.
2
+ //
3
+ // Up to 0.8.x the agent core kept its data (settings.json, models.json,
4
+ // provider auth.json, sessions/, installed packages) in its upstream
5
+ // default folder, ~/.pi/agent, and read project settings from <project>/.pi.
6
+ // From 0.9.0 everything the user sees is named omnirush:
7
+ //
8
+ // agent data ~/.omnirush/agent (inside the state dir, so
9
+ // OMNIRUSH_DIR moves it too)
10
+ // project config <project>/.omnirush
11
+ // env variables OMNIRUSH_* (the core's PI_* names still work)
12
+ //
13
+ // This module holds the pure pieces the launcher uses to get there
14
+ // without losing anything:
15
+ //
16
+ // resolveAgentDir where the agent data lives this run (and the
17
+ // legacy folder to migrate from)
18
+ // migrateAgentDir one-time COPY of the legacy folder into the new
19
+ // one (the old folder is left untouched, so an
20
+ // older omnirush keeps working); never overwrites,
21
+ // merges the three JSON config files key by key,
22
+ // writes a marker so it runs once, resumes after a
23
+ // crash, and reports failure so the launcher can
24
+ // fall back to the legacy folder for that run
25
+ // projectConfigDirName the project config folder name for a cwd: a
26
+ // project that only has a legacy .pi folder keeps
27
+ // using it
28
+ // engineEnvAliases OMNIRUSH_* spellings of the core's PI_* settings
29
+
30
+ import fs from "node:fs";
31
+ import os from "node:os";
32
+ import path from "node:path";
33
+
34
+ /** Marker written into the new agent dir once the legacy data was copied. */
35
+ export const MIGRATION_MARKER = ".migrated.json";
36
+
37
+ /** Project config folder (the core's configDir after the branding patch). */
38
+ export const PROJECT_CONFIG_DIR = ".omnirush";
39
+ const LEGACY_PROJECT_CONFIG_DIR = ".pi";
40
+ /** Entries that make a project config folder live (the core's trust-gated resources). */
41
+ const PROJECT_RESOURCES = ["settings.json", "extensions", "skills", "prompts", "themes", "SYSTEM.md", "APPEND_SYSTEM.md"];
42
+
43
+ /** Top-level JSON files merged key by key when both folders have them (the new folder's keys win). */
44
+ const MERGED_JSON = new Set(["settings.json", "models.json", "auth.json"]);
45
+
46
+ /**
47
+ * The core's own PI_* settings a user may set, accepted as OMNIRUSH_<NAME>
48
+ * (the launcher hands them to the core under the name it reads). The
49
+ * original PI_<NAME> keeps working unchanged. PI_OFFLINE / PI_TELEMETRY
50
+ * are always forced by the launcher and are not listed.
51
+ */
52
+ export const ENGINE_ENV_NAMES = [
53
+ "CACHE_RETENTION",
54
+ "CLEAR_ON_SHRINK",
55
+ "EXPERIMENTAL",
56
+ "HARDWARE_CURSOR",
57
+ "HYPERLINKS",
58
+ "IMAGE_PROTOCOL",
59
+ "OAUTH_CALLBACK_HOST",
60
+ "PACKAGE_DIR",
61
+ "RADIUS_GATEWAY",
62
+ "SHARE_VIEWER_URL",
63
+ "SKIP_VERSION_CHECK",
64
+ "STARTUP_BENCHMARK",
65
+ "TIMING",
66
+ "TRUE_COLOR",
67
+ "TUI_DEBUG",
68
+ "TUI_DEBUG_REDRAW",
69
+ "TUI_ESC_TIMEOUT",
70
+ "TUI_WRITE_LOG",
71
+ ];
72
+
73
+ /**
74
+ * Where the agent data lives: OMNIRUSH_AGENT_DIR (or its pre-0.9 spelling
75
+ * OMNIRUSH_PI_AGENT_DIR, or the core's own OMNIRUSH_CODING_AGENT_DIR, which
76
+ * nested runs inherit) when set — then nothing is migrated — else
77
+ * <stateDir>/agent, with ~/.pi/agent as the legacy folder to migrate from.
78
+ */
79
+ export function resolveAgentDir(env = process.env, { stateDir, home = os.homedir() } = {}) {
80
+ const override = String(env.OMNIRUSH_AGENT_DIR || env.OMNIRUSH_PI_AGENT_DIR || env.OMNIRUSH_CODING_AGENT_DIR || "").trim();
81
+ if (override) return { dir: path.resolve(override), legacyDir: null };
82
+ const base = stateDir || path.join(home, ".omnirush");
83
+ return { dir: path.join(base, "agent"), legacyDir: path.join(home, ".pi", "agent") };
84
+ }
85
+
86
+ function isInside(parent, child, pathImpl) {
87
+ const relative = pathImpl.relative(parent, child);
88
+ return relative === "" || (!!relative && !relative.startsWith("..") && !pathImpl.isAbsolute(relative));
89
+ }
90
+
91
+ function writeAtomic(fsImpl, dest, data, mode) {
92
+ const tmp = `${dest}.${process.pid}.migrating`;
93
+ fsImpl.writeFileSync(tmp, data, mode === undefined ? undefined : { mode });
94
+ fsImpl.renameSync(tmp, dest);
95
+ }
96
+
97
+ function readJson(fsImpl, file) {
98
+ try {
99
+ const value = JSON.parse(fsImpl.readFileSync(file, "utf8"));
100
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
101
+ } catch {
102
+ return null;
103
+ }
104
+ }
105
+
106
+ /** Old keys fill the gaps of the new file; models.json merges its providers map the same way. */
107
+ function mergeJson(name, older, newer) {
108
+ const merged = { ...older, ...newer };
109
+ if (name === "models.json" && older.providers && typeof older.providers === "object") {
110
+ merged.providers = { ...older.providers, ...(newer.providers || {}) };
111
+ }
112
+ return merged;
113
+ }
114
+
115
+ /**
116
+ * A session file's header names its parent session by absolute path; a
117
+ * copied session points at the copied parent instead.
118
+ */
119
+ export function rebaseSessionHeader(content, from, to, pathImpl = path) {
120
+ const newline = content.indexOf("\n");
121
+ const first = newline === -1 ? content : content.slice(0, newline);
122
+ let header;
123
+ try {
124
+ header = JSON.parse(first);
125
+ } catch {
126
+ return content;
127
+ }
128
+ if (!header || header.type !== "session" || typeof header.parentSession !== "string") return content;
129
+ if (!isInside(from, header.parentSession, pathImpl)) return content;
130
+ header.parentSession = pathImpl.join(to, pathImpl.relative(from, header.parentSession));
131
+ return JSON.stringify(header) + (newline === -1 ? "" : content.slice(newline));
132
+ }
133
+
134
+ /**
135
+ * Copy the legacy agent folder into the new one, once. Never overwrites a
136
+ * file the new folder already has (the three JSON config files are merged
137
+ * instead), keeps modes and timestamps (the core orders sessions by them),
138
+ * skips lock files and our own extension folder (reinstalled at every
139
+ * launch), and writes MIGRATION_MARKER last: an interrupted copy resumes
140
+ * on the next start, a finished one never runs again.
141
+ *
142
+ * Returns { status: "migrated" | "already" | "none" | "failed", copied,
143
+ * merged, skipped, error? }.
144
+ */
145
+ export function migrateAgentDir({ from, to, fsImpl = fs, pathImpl = path, now = () => new Date(), onStart } = {}) {
146
+ const result = { status: "none", copied: 0, merged: 0, skipped: 0 };
147
+ const marker = pathImpl.join(to, MIGRATION_MARKER);
148
+ if (fsImpl.existsSync(marker)) return { ...result, status: "already" };
149
+ try {
150
+ fsImpl.mkdirSync(to, { recursive: true });
151
+ let legacy = null;
152
+ try {
153
+ legacy = from && pathImpl.resolve(from) !== pathImpl.resolve(to) ? fsImpl.statSync(from) : null;
154
+ } catch {
155
+ legacy = null;
156
+ }
157
+ if (legacy?.isDirectory()) {
158
+ onStart?.();
159
+ copyTree(from, to, "");
160
+ result.status = "migrated";
161
+ }
162
+ const record = { version: 1, from: legacy?.isDirectory() ? from : null, at: now().toISOString(), copied: result.copied, merged: result.merged, skipped: result.skipped };
163
+ writeAtomic(fsImpl, marker, JSON.stringify(record, null, 2) + "\n");
164
+ return result;
165
+ } catch (error) {
166
+ return { ...result, status: "failed", error };
167
+ }
168
+
169
+ function copyTree(srcDir, destDir, rel) {
170
+ for (const entry of fsImpl.readdirSync(srcDir, { withFileTypes: true })) {
171
+ const name = entry.name;
172
+ const relPath = rel ? `${rel}/${name}` : name;
173
+ if (name === MIGRATION_MARKER || name.endsWith(".lock") || name.endsWith(".migrating")) continue;
174
+ if (relPath === "extensions/omnirush") continue;
175
+ const src = pathImpl.join(srcDir, name);
176
+ const dest = pathImpl.join(destDir, name);
177
+ const stat = fsImpl.lstatSync(src);
178
+ if (stat.isDirectory()) {
179
+ fsImpl.mkdirSync(dest, { recursive: true });
180
+ copyTree(src, dest, relPath);
181
+ continue;
182
+ }
183
+ const exists = (() => {
184
+ try {
185
+ fsImpl.lstatSync(dest);
186
+ return true;
187
+ } catch {
188
+ return false;
189
+ }
190
+ })();
191
+ if (stat.isSymbolicLink()) {
192
+ if (exists) {
193
+ result.skipped++;
194
+ continue;
195
+ }
196
+ try {
197
+ fsImpl.symlinkSync(fsImpl.readlinkSync(src), dest);
198
+ result.copied++;
199
+ } catch {
200
+ result.skipped++; // e.g. Windows without symlink rights
201
+ }
202
+ continue;
203
+ }
204
+ if (!stat.isFile()) continue;
205
+ if (exists) {
206
+ if (!rel && MERGED_JSON.has(name)) {
207
+ const older = readJson(fsImpl, src);
208
+ const newer = readJson(fsImpl, dest);
209
+ if (older && newer) {
210
+ const merged = mergeJson(name, older, newer);
211
+ if (JSON.stringify(merged) !== JSON.stringify(newer)) {
212
+ writeAtomic(fsImpl, dest, JSON.stringify(merged, null, 2) + "\n", fsImpl.statSync(dest).mode & 0o777);
213
+ result.merged++;
214
+ continue;
215
+ }
216
+ }
217
+ }
218
+ result.skipped++;
219
+ continue;
220
+ }
221
+ const tmp = `${dest}.${process.pid}.migrating`;
222
+ if (relPath.startsWith("sessions/") && name.endsWith(".jsonl")) {
223
+ const content = fsImpl.readFileSync(src, "utf8");
224
+ fsImpl.writeFileSync(tmp, rebaseSessionHeader(content, from, to, pathImpl));
225
+ } else {
226
+ fsImpl.copyFileSync(src, tmp);
227
+ }
228
+ try {
229
+ fsImpl.chmodSync(tmp, stat.mode & 0o777);
230
+ } catch {
231
+ /* modes are advisory on Windows */
232
+ }
233
+ fsImpl.utimesSync(tmp, stat.atime, stat.mtime);
234
+ fsImpl.renameSync(tmp, dest);
235
+ result.copied++;
236
+ }
237
+ }
238
+ }
239
+
240
+ /**
241
+ * The project config folder the core should read in `cwd`: .omnirush, or
242
+ * the legacy .pi when the project has one with settings or resources and
243
+ * no .omnirush yet (the launcher passes it as OMNIRUSH_PROJECT_CONFIG_DIR).
244
+ */
245
+ export function projectConfigDirName(cwd, fsImpl = fs) {
246
+ try {
247
+ if (fsImpl.existsSync(path.join(cwd, PROJECT_CONFIG_DIR))) return PROJECT_CONFIG_DIR;
248
+ const legacy = path.join(cwd, LEGACY_PROJECT_CONFIG_DIR);
249
+ if (PROJECT_RESOURCES.some((entry) => fsImpl.existsSync(path.join(legacy, entry)))) return LEGACY_PROJECT_CONFIG_DIR;
250
+ } catch {
251
+ /* unreadable cwd: the default */
252
+ }
253
+ return PROJECT_CONFIG_DIR;
254
+ }
255
+
256
+ /**
257
+ * The core-facing variables for the OMNIRUSH_* spellings the user set:
258
+ * OMNIRUSH_<NAME> -> PI_<NAME> (the OMNIRUSH_ value wins over a PI_ one),
259
+ * and the pre-0.9 PI_CODING_AGENT_SESSION_DIR -> the OMNIRUSH_ name the
260
+ * rebranded core reads.
261
+ */
262
+ export function engineEnvAliases(env = process.env) {
263
+ const out = {};
264
+ for (const name of ENGINE_ENV_NAMES) {
265
+ const value = env[`OMNIRUSH_${name}`];
266
+ if (value !== undefined && value !== "") out[`PI_${name}`] = value;
267
+ }
268
+ if (env.PI_CODING_AGENT_SESSION_DIR && !env.OMNIRUSH_CODING_AGENT_SESSION_DIR) {
269
+ out.OMNIRUSH_CODING_AGENT_SESSION_DIR = env.PI_CODING_AGENT_SESSION_DIR;
270
+ }
271
+ return out;
272
+ }