omnirush 0.8.6 → 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 (47) hide show
  1. package/assets/CHANGELOG.md +85 -0
  2. package/assets/extensions/omnirush/agents-lib.ts +134 -13
  3. package/assets/extensions/omnirush/agents.ts +51 -107
  4. package/assets/extensions/omnirush/bgshell-lib.ts +400 -0
  5. package/assets/extensions/omnirush/bgshell.ts +392 -0
  6. package/assets/extensions/omnirush/collector.ts +72 -11
  7. package/assets/extensions/omnirush/commands.ts +2 -0
  8. package/assets/extensions/omnirush/deliveries.ts +145 -0
  9. package/assets/extensions/omnirush/guard/UPSTREAM +2 -0
  10. package/assets/extensions/omnirush/guard/git-command-policy.ts +885 -0
  11. package/assets/extensions/omnirush/guard-lib.ts +230 -0
  12. package/assets/extensions/omnirush/guard.ts +340 -0
  13. package/assets/extensions/omnirush/index.ts +12 -0
  14. package/assets/extensions/omnirush/pi-engine.ts +65 -1
  15. package/assets/extensions/omnirush/sota.ts +52 -4
  16. package/assets/extensions/omnirush/status-lib.ts +3 -0
  17. package/assets/extensions/omnirush/subagents-lib.ts +623 -0
  18. package/assets/extensions/omnirush/subagents.ts +305 -0
  19. package/assets/extensions/omnirush/swarm-lib.ts +142 -0
  20. package/assets/extensions/omnirush/swarm.ts +95 -0
  21. package/assets/extensions/omnirush/voice/capture.ts +502 -0
  22. package/assets/extensions/omnirush/voice/core/UPSTREAM +16 -0
  23. package/assets/extensions/omnirush/voice/core/file-source.ts +70 -0
  24. package/assets/extensions/omnirush/voice/core/index.ts +21 -0
  25. package/assets/extensions/omnirush/voice/core/keyterms.ts +117 -0
  26. package/assets/extensions/omnirush/voice/core/resample.ts +63 -0
  27. package/assets/extensions/omnirush/voice/core/segmenter.ts +231 -0
  28. package/assets/extensions/omnirush/voice/core/session.ts +403 -0
  29. package/assets/extensions/omnirush/voice/core/text.ts +81 -0
  30. package/assets/extensions/omnirush/voice/core/transcriber.ts +135 -0
  31. package/assets/extensions/omnirush/voice/core/types.ts +102 -0
  32. package/assets/extensions/omnirush/voice/core/wav.ts +95 -0
  33. package/assets/extensions/omnirush/voice/keys.ts +435 -0
  34. package/assets/extensions/omnirush/voice/kitty.ts +64 -0
  35. package/assets/extensions/omnirush/voice/pvrecorder-worker.cjs +43 -0
  36. package/assets/extensions/omnirush/voice/settings.ts +67 -0
  37. package/assets/extensions/omnirush/voice.ts +838 -0
  38. package/assets/extensions/omnirush/yolo-lib.ts +80 -0
  39. package/assets/extensions/omnirush/yolo.ts +85 -0
  40. package/package.json +7 -3
  41. package/scripts/brand-engine.js +526 -0
  42. package/scripts/build-all-packages.py +29 -1
  43. package/scripts/smoke-packages.py +32 -1
  44. package/src/bin.js +205 -33
  45. package/src/compat.js +272 -0
  46. package/src/lib.js +64 -0
  47. 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,
@@ -70,14 +76,43 @@ import {
70
76
  const require = createRequire(import.meta.url);
71
77
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
72
78
  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");
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
+ }
81
116
 
82
117
  function managerOrigin() {
83
118
  return (
@@ -111,11 +146,12 @@ function piPackage() {
111
146
  function piEntry() {
112
147
  const found = piPackage();
113
148
  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)");
149
+ throw new Error("could not locate the pinned agent core (broken install). Fix: npm install -g omnirush@latest");
115
150
  }
116
151
 
117
152
  function ensureDirs() {
118
153
  fs.mkdirSync(OMNI_DIR, { recursive: true, mode: 0o700 });
154
+ migrateLegacyAgentData();
119
155
  fs.mkdirSync(PI_AGENT_DIR, { recursive: true });
120
156
  fs.mkdirSync(PI_EXT_DIR, { recursive: true });
121
157
  try {
@@ -233,7 +269,7 @@ function installExtensions() {
233
269
  fs.mkdirSync(to, { recursive: true });
234
270
  for (const entry of fs.readdirSync(from, { withFileTypes: true })) {
235
271
  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")) {
272
+ else if (entry.name.endsWith(".ts") || entry.name.endsWith(".js") || entry.name.endsWith(".cjs")) {
237
273
  fs.copyFileSync(path.join(from, entry.name), path.join(to, entry.name));
238
274
  }
239
275
  }
@@ -729,6 +765,8 @@ function childEnv(auth) {
729
765
  const origin = (process.env.OMNIRUSH_ORIGIN || "").trim().replace(/\/+$/, "") ||
730
766
  gateway.replace(/\/+$/, "").replace(/\/v1$/, ""); // provider base -> manager origin
731
767
  return {
768
+ // OMNIRUSH_* spellings of the core's own settings (PI_* still work).
769
+ ...engineEnvAliases(process.env),
732
770
  // Bootstrap values only: the agent (and every sub-agent it spawns)
733
771
  // re-reads the shared auth file in OMNIRUSH_DIR for each request, so
734
772
  // these never go stale in a way that matters.
@@ -738,15 +776,22 @@ function childEnv(auth) {
738
776
  OMNIRUSH_GATEWAY_URL: gateway,
739
777
  OMNIRUSH_DIR: OMNI_DIR,
740
778
  // /status shows the CLI version; extensions have no package.json of
741
- // their own in ~/.pi, so the launcher hands it over.
779
+ // their own in the agent dir, so the launcher hands it over.
742
780
  OMNIRUSH_VERSION: ownVersion(),
743
781
  // The collector reports it as the environment's engine_version.
744
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, ".."),
745
786
  // The agent resolves its config dir from <APP>_CODING_AGENT_DIR. The
746
787
  // branding patch renames the app to omnirush, so set the omnirush
747
788
  // spelling; the pi spelling stays for unpatched cores.
748
789
  OMNIRUSH_CODING_AGENT_DIR: PI_AGENT_DIR,
749
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()),
750
795
  // The agent core is a fork/rebrand: never phone home to pi.dev.
751
796
  // PI_OFFLINE kills startup update checks (the "Update Available —
752
797
  // pi.dev/changelog" banner), package updates, and install/update
@@ -804,6 +849,32 @@ function spawnAgent(cmd, args, opts) {
804
849
  return child;
805
850
  }
806
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
+
807
878
  async function cmdRun() {
808
879
  ensureDirs();
809
880
  const auth = requireAuth();
@@ -813,11 +884,13 @@ async function cmdRun() {
813
884
  const { cmd, args } = await runtimeFor(entry);
814
885
  // Plain `omnirush` always runs the SOTA model (charter rule 1); user
815
886
  // flags win when present.
816
- 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));
817
890
  spawnAgent(cmd, [...args, ...piArgs], {
818
891
  stdio: "inherit",
819
892
  // The pair may have rotated during launch (model catalog refresh).
820
- env: { ...process.env, ...childEnv(loadAuth(OMNI_DIR) ?? auth) },
893
+ env: { ...process.env, ...childEnv(loadAuth(OMNI_DIR) ?? auth), ...yolo.env },
821
894
  });
822
895
  }
823
896
 
@@ -854,8 +927,18 @@ Options:
854
927
  gpt-5.6-sol, meta-muse-spark, muse-spark-1.1;
855
928
  effort: minimal..max)
856
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)
857
935
  --strict-sota exit non-zero if the served model is not
858
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)
859
942
  --session <file> | --continue session options (forwarded to the agent)
860
943
  -p, --print "<prompt>" print-mode: answer once and exit
861
944
  -h, --help show this help
@@ -865,6 +948,15 @@ Session commands (inside the agent):
865
948
  /usage detailed usage from the manager (counters + devices)
866
949
  /status session status (identity, model, grant, collector)
867
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)
868
960
  /verify model attestation (arrives with the gateway lane)
869
961
 
870
962
  Environment:
@@ -872,11 +964,106 @@ Environment:
872
964
  OMNIRUSH_MODEL default model override (e.g. gpt-6-sol)
873
965
  OMNIRUSH_STATIC_MODELS=1 use the shipped model list (no GET /v1/models)
874
966
  OMNIRUSH_DIR state dir override (default ~/.omnirush)
967
+ OMNIRUSH_AGENT_DIR agent data dir override (default ~/.omnirush/agent)
875
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)
876
977
 
877
978
  Docs: https://omnirush.ai/omnirush — state is stored 0600 in ~/.omnirush`);
878
979
  }
879
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
+
880
1067
  const [, , ...argv] = process.argv;
881
1068
  const cmd = argv[0];
882
1069
  if (cmd === "-h" || cmd === "--help" || cmd === "help") printHelp();
@@ -887,20 +1074,5 @@ else if (cmd === "doctor") await cmdDoctor();
887
1074
  else if (cmd === "--version" || cmd === "-v") console.log(`omnirush ${ownVersion()}`);
888
1075
  else if (cmd === "collect") await cmdCollect(argv.slice(1));
889
1076
  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
- }
1077
+ else if (cmd === "update") await cmdUpdate(argv.slice(1));
1078
+ 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
+ }