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
@@ -18,10 +18,21 @@
18
18
  // The MCP SDK is imported from the committed sibling bundle
19
19
  // (mcp-client.bundle.js): pi copies this directory into
20
20
  // ~/.pi/agent/extensions/omnirush/ and jiti there resolves only sibling
21
- // files, never npm packages.
21
+ // files, never npm packages. It is loaded on first connect, so a process
22
+ // without MCP servers never pays for it.
23
+ //
24
+ // Sub-agents (spawn_agents children) do not start every server up front:
25
+ // each child used to spawn its own copy of every configured stdio server
26
+ // at session start (20 children x 3 servers = 60 extra processes). A child
27
+ // registers the tools from the list its top agent saved
28
+ // (<omnirush dir>/mcp-tools.json, per server config) and connects a server
29
+ // only when it first calls one of its tools; without a saved list it
30
+ // connects at start as before.
22
31
 
32
+ import { createHash, randomUUID } from "node:crypto";
33
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
23
34
  import path from "node:path";
24
- import { Client, McpError, StdioClientTransport, StreamableHTTPClientTransport } from "./mcp-client.bundle.js";
35
+ import type { Client } from "./mcp-client.bundle.js";
25
36
  import { omniDir } from "./auth";
26
37
  import {
27
38
  dedupeToolNames,
@@ -53,7 +64,62 @@ const SHUTDOWN_BUDGET_MS = 3_000;
53
64
  // Stderr lines kept per stdio server for failure diagnostics.
54
65
  const STDERR_TAIL_LINES = 5;
55
66
 
56
- type ServerStatus = "connecting" | "connected" | "failed";
67
+ /** "deferred": tools registered from the saved list, connects on first use (sub-agents). */
68
+ type ServerStatus = "connecting" | "connected" | "failed" | "deferred";
69
+
70
+ type McpSdk = typeof import("./mcp-client.bundle.js");
71
+ let sdkPromise: Promise<McpSdk> | null = null;
72
+ let loadedSdk: McpSdk | null = null;
73
+ /** The MCP SDK bundle, imported on first use. */
74
+ function loadSdk(): Promise<McpSdk> {
75
+ sdkPromise ??= import("./mcp-client.bundle.js").then((sdk) => {
76
+ loadedSdk = sdk;
77
+ return sdk;
78
+ });
79
+ return sdkPromise;
80
+ }
81
+
82
+ /** Saved tool lists (by the process that connected), keyed by server name. */
83
+ export const MCP_TOOLS_CACHE_FILE = "mcp-tools.json";
84
+ /** A saved list older than this is not trusted (the server may have changed). */
85
+ const TOOLS_CACHE_MAX_AGE_MS = 7 * 24 * 3600_000;
86
+
87
+ /** Identity of a server config: a saved tool list only counts for the same config. */
88
+ export function serverConfigKey(config: McpServerConfig): string {
89
+ return createHash("sha256").update(JSON.stringify(config)).digest("hex").slice(0, 32);
90
+ }
91
+
92
+ function readToolsCache(dir: string): Record<string, { key: string; at: number; tools: McpToolInfo[] }> {
93
+ try {
94
+ const parsed = JSON.parse(readFileSync(path.join(dir, MCP_TOOLS_CACHE_FILE), "utf8"));
95
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
96
+ } catch {
97
+ return {};
98
+ }
99
+ }
100
+
101
+ /** Save one server's tool list (atomic; other servers' entries kept). */
102
+ function saveToolsCache(dir: string, state: ServerState): void {
103
+ try {
104
+ const cache = readToolsCache(dir);
105
+ cache[state.name] = { key: serverConfigKey(state.config), at: Date.now(), tools: state.tools };
106
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
107
+ const file = path.join(dir, MCP_TOOLS_CACHE_FILE);
108
+ const tmp = `${file}.${randomUUID()}.tmp`;
109
+ writeFileSync(tmp, JSON.stringify(cache), { mode: 0o600 });
110
+ renameSync(tmp, file);
111
+ } catch {
112
+ /* only an optimization for sub-agents */
113
+ }
114
+ }
115
+
116
+ /** A saved tool list for this server config, or null. */
117
+ function cachedTools(dir: string, state: ServerState, now = Date.now()): McpToolInfo[] | null {
118
+ const entry = readToolsCache(dir)[state.name];
119
+ if (!entry || entry.key !== serverConfigKey(state.config) || !Array.isArray(entry.tools)) return null;
120
+ if (!(now - Number(entry.at) < TOOLS_CACHE_MAX_AGE_MS)) return null;
121
+ return entry.tools.filter((tool) => tool && typeof tool.name === "string");
122
+ }
57
123
 
58
124
  /** OMNIRUSH_DEBUG=1 diagnostics (same convention as the sota guard). */
59
125
  function debug(line: string): void {
@@ -79,6 +145,8 @@ interface ServerState {
79
145
  tools: McpToolInfo[];
80
146
  client?: Client;
81
147
  stderrTail: string[];
148
+ /** A deferred server's connect in flight (single-flight for parallel tool calls). */
149
+ connecting?: Promise<void>;
82
150
  }
83
151
 
84
152
  function withTimeout<T>(promise: Promise<T>, ms: number, label: string): Promise<T> {
@@ -138,6 +206,7 @@ function contentText(content: Array<Record<string, any>>): string {
138
206
 
139
207
  /** Connect one server (bounded). Throws on failure — caller isolates. */
140
208
  async function connectServer(state: ServerState): Promise<void> {
209
+ const { Client, StdioClientTransport, StreamableHTTPClientTransport } = await loadSdk();
141
210
  const client = new Client({
142
211
  name: "omnirush",
143
212
  version: (process.env.OMNIRUSH_VERSION || "").trim() || "0.0.0",
@@ -194,6 +263,11 @@ async function connectServer(state: ServerState): Promise<void> {
194
263
  }
195
264
  }
196
265
 
266
+ /** A spawn_agents child process. */
267
+ function isSubagent(): boolean {
268
+ return Boolean(String(process.env.OMNIRUSH_PARENT_SESSION ?? "").trim());
269
+ }
270
+
197
271
  function describeServer(state: ServerState): string {
198
272
  if (state.config.url) return `${state.name} (${state.config.url})`;
199
273
  const args = (state.config.args ?? []).join(" ");
@@ -231,6 +305,11 @@ export default function (pi: any) {
231
305
  void toolCallId;
232
306
  void onUpdate;
233
307
  const current = servers.get(state.name);
308
+ if (current?.status === "deferred") {
309
+ // A sub-agent's first call to this server: connect now.
310
+ current.connecting ??= connectServer(current).finally(() => { current.connecting = undefined; });
311
+ await current.connecting.catch(() => undefined);
312
+ }
234
313
  const client = current?.client;
235
314
  if (current?.status !== "connected" || !client) {
236
315
  throw new Error(
@@ -248,7 +327,7 @@ export default function (pi: any) {
248
327
  { timeout: CALL_TIMEOUT_MS, signal },
249
328
  );
250
329
  } catch (error: any) {
251
- const detail = error instanceof McpError ? error.message : error?.message ?? String(error);
330
+ const detail = loadedSdk && error instanceof loadedSdk.McpError ? error.message : error?.message ?? String(error);
252
331
  throw new Error(`MCP call failed on "${state.name}": ${detail}`);
253
332
  }
254
333
  const content = mcpContentToPiContent(result?.content);
@@ -284,6 +363,7 @@ export default function (pi: any) {
284
363
  try {
285
364
  await connectServer(state);
286
365
  registerServerTools(state);
366
+ saveToolsCache(omniDir(), state);
287
367
  } catch {
288
368
  /* connectServer recorded the failure; isolation keeps going */
289
369
  }
@@ -316,7 +396,9 @@ export default function (pi: any) {
316
396
  const stateLabel =
317
397
  state.status === "connected"
318
398
  ? `connected, ${state.tools.length} tool${state.tools.length === 1 ? "" : "s"}`
319
- : state.status === "connecting"
399
+ : state.status === "deferred"
400
+ ? `connects on first use, ${state.tools.length} tool${state.tools.length === 1 ? "" : "s"}`
401
+ : state.status === "connecting"
320
402
  ? "connecting…"
321
403
  : `failed: ${state.error ?? "unknown error"}`;
322
404
  lines.push(` ${describeServer(state)} — ${stateLabel}`);
@@ -367,10 +449,23 @@ export default function (pi: any) {
367
449
  for (const error of errors) {
368
450
  ctx?.ui?.notify?.(`mcp config: ${error}`, "warning");
369
451
  }
370
- if (servers.size > 0) {
371
- // Isolated + bounded per server; failures surface via /mcp only.
372
- await connectAll();
452
+ if (servers.size === 0) return;
453
+ if (isSubagent()) {
454
+ // Tools from the saved list; the server starts on first use.
455
+ const dir = omniDir();
456
+ for (const state of servers.values()) {
457
+ const tools = cachedTools(dir, state);
458
+ if (!tools) continue;
459
+ state.tools = tools;
460
+ state.status = "deferred";
461
+ registerServerTools(state);
462
+ }
463
+ const rest = [...servers.values()].filter((state) => state.status !== "deferred");
464
+ await Promise.allSettled(rest.map((state) => connectAll(state.name)));
465
+ return;
373
466
  }
467
+ // Isolated + bounded per server; failures surface via /mcp only.
468
+ await connectAll();
374
469
  });
375
470
 
376
471
  pi.on("session_shutdown", async () => {