omnirush 0.4.1 → 0.5.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.
@@ -0,0 +1,294 @@
1
+ // Trace artifact format (team spec, 2026-09-24) — converts a pi session
2
+ // transcript into the reference trace shape: ONE single-line JSON object
3
+ // carrying the run header and the FULL conversation (system prompt,
4
+ // every user/assistant message with tool calls + full results — no
5
+ // truncation of message bodies). Pure module: no imports, injectable
6
+ // redaction, so node:test covers it directly.
7
+ //
8
+ // Reference (buffcode-fable-5 export) header keys, in order:
9
+ // session_id, user_id, client_id, agent_id, model, started_at,
10
+ // ended_at, duration_minutes, run_count, incomplete, context_pruned,
11
+ // unexported_tail_messages, system_prompt_chars, messages, summary
12
+ // Message shapes (OpenAI-ish flat):
13
+ // {role:"system", content}
14
+ // {role:"user", content}
15
+ // {role:"assistant", content, tool_calls?:[{id,type,function:{name,
16
+ // arguments}}], reasoning?}
17
+ // {role:"tool", tool_call_id, content, is_error?}
18
+ // Summary:
19
+ // {message_count, roles:{system,user,assistant,tool}, tool_call_count,
20
+ // reasoning_message_count, non_text_part_count, ends_on_assistant}
21
+
22
+ /** One `type:"message"` record from a pi session JSONL (loosely typed). */
23
+ export interface PiRecord {
24
+ type?: string;
25
+ id?: string;
26
+ timestamp?: string;
27
+ message?: {
28
+ role?: string;
29
+ content?: unknown;
30
+ sections?: Record<string, string>;
31
+ toolCallId?: string;
32
+ toolName?: string;
33
+ isError?: boolean;
34
+ provider?: string;
35
+ model?: string;
36
+ stopReason?: string;
37
+ usage?: Record<string, unknown>;
38
+ [key: string]: unknown;
39
+ };
40
+ provider?: string;
41
+ modelId?: string;
42
+ thinkingLevel?: string;
43
+ cwd?: string;
44
+ }
45
+
46
+ export interface ParsedPiSession {
47
+ sessionId: string | null;
48
+ cwd: string | null;
49
+ startedAt: string | null;
50
+ endedAt: string | null;
51
+ /** provider/modelId from the model_change record, when present. */
52
+ model: string | null;
53
+ messageRecords: PiRecord[];
54
+ sawCompaction: boolean;
55
+ }
56
+
57
+ function asRecord(line: string): PiRecord | null {
58
+ try {
59
+ const parsed = JSON.parse(line);
60
+ return parsed && typeof parsed === "object" ? (parsed as PiRecord) : null;
61
+ } catch {
62
+ return null;
63
+ }
64
+ }
65
+
66
+ /** Parse a pi session JSONL (one JSON record per line). */
67
+ export function parsePiSession(text: string): ParsedPiSession {
68
+ const result: ParsedPiSession = {
69
+ sessionId: null,
70
+ cwd: null,
71
+ startedAt: null,
72
+ endedAt: null,
73
+ model: null,
74
+ messageRecords: [],
75
+ sawCompaction: false,
76
+ };
77
+ let firstTimestamp: string | null = null;
78
+ let lastTimestamp: string | null = null;
79
+ for (const line of text.split("\n")) {
80
+ if (!line.trim()) continue;
81
+ const record = asRecord(line);
82
+ if (!record) continue;
83
+ if (record.type === "session") {
84
+ result.sessionId = typeof record.id === "string" ? record.id : result.sessionId;
85
+ result.cwd = typeof record.cwd === "string" ? record.cwd : result.cwd;
86
+ } else if (record.type === "model_change") {
87
+ if (typeof record.provider === "string" && typeof record.modelId === "string") {
88
+ result.model = `${record.provider}/${record.modelId}`;
89
+ }
90
+ } else if (record.type && /compact/i.test(record.type)) {
91
+ result.sawCompaction = true;
92
+ }
93
+ if (typeof record.timestamp === "string") {
94
+ firstTimestamp ??= record.timestamp;
95
+ lastTimestamp = record.timestamp;
96
+ }
97
+ if (record.type === "message" && record.message) {
98
+ result.messageRecords.push(record);
99
+ }
100
+ }
101
+ result.startedAt = firstTimestamp;
102
+ result.endedAt = lastTimestamp ?? firstTimestamp;
103
+ return result;
104
+ }
105
+
106
+ /**
107
+ * Flatten pi message content into display text: string passes through;
108
+ * part arrays join their text parts with a blank line. Non-text parts
109
+ * (images) are COUNTED, not embedded — the reference format counts them
110
+ * in summary.non_text_part_count and carries no base64 payloads.
111
+ */
112
+ export function contentToText(content: unknown): { text: string; nonTextParts: number } {
113
+ if (typeof content === "string") return { text: content, nonTextParts: 0 };
114
+ if (!Array.isArray(content)) {
115
+ return { text: content === null || content === undefined ? "" : String(content), nonTextParts: 0 };
116
+ }
117
+ const texts: string[] = [];
118
+ let nonTextParts = 0;
119
+ for (const part of content as Array<any>) {
120
+ if (part && typeof part === "object" && part.type === "text" && typeof part.text === "string") {
121
+ texts.push(part.text);
122
+ } else {
123
+ nonTextParts += 1;
124
+ }
125
+ }
126
+ return { text: texts.join("\n\n"), nonTextParts };
127
+ }
128
+
129
+ /** Reconstruct the system prompt from a pi system message record. */
130
+ function systemPromptOf(message: PiRecord["message"]): string {
131
+ if (!message) return "";
132
+ if (message.sections && typeof message.sections === "object") {
133
+ return Object.values(message.sections)
134
+ .filter((value) => typeof value === "string")
135
+ .join("\n\n");
136
+ }
137
+ return contentToText(message.content).text;
138
+ }
139
+
140
+ function durationMinutes(startedAt: string | null, endedAt: string | null): number {
141
+ if (!startedAt || !endedAt) return 0;
142
+ const start = Date.parse(startedAt);
143
+ const end = Date.parse(endedAt);
144
+ if (!Number.isFinite(start) || !Number.isFinite(end) || end <= start) return 0;
145
+ return Math.round(((end - start) / 60_000) * 10) / 10;
146
+ }
147
+
148
+ export interface BuildTraceArtifactInput {
149
+ sessionId: string;
150
+ /** Set when this artifact belongs to a subagent session. */
151
+ parentSessionId?: string | null;
152
+ userId?: string | null;
153
+ clientId?: string | null;
154
+ agentId?: string | null;
155
+ parsed: ParsedPiSession;
156
+ /** Scrubber applied to every produced string (existing redaction). */
157
+ redact: (text: string) => string;
158
+ }
159
+
160
+ /**
161
+ * Build the trace artifact object (reference shape; key order mirrored).
162
+ * Returns the plain object — serialize with JSON.stringify for the
163
+ * single-line .jsonl form.
164
+ */
165
+ export function buildTraceArtifact(input: BuildTraceArtifactInput): Record<string, unknown> {
166
+ const { parsed, redact } = input;
167
+ const messages: Array<Record<string, unknown>> = [];
168
+ const roles: Record<string, number> = {};
169
+ let toolCallCount = 0;
170
+ let reasoningMessageCount = 0;
171
+ let nonTextPartCount = 0;
172
+ let systemPrompt = "";
173
+ let model = input.parsed.model ?? null;
174
+
175
+ for (const record of parsed.messageRecords) {
176
+ const message = record.message!;
177
+ const role = typeof message.role === "string" ? message.role : "unknown";
178
+ roles[role] = (roles[role] ?? 0) + 1;
179
+
180
+ if (role === "system") {
181
+ systemPrompt = systemPromptOf(message);
182
+ messages.push({ role: "system", content: redact(systemPrompt) });
183
+ continue;
184
+ }
185
+
186
+ if (role === "assistant") {
187
+ const text = contentToText(message.content);
188
+ const reasoningParts: string[] = [];
189
+ const toolCalls: Array<Record<string, unknown>> = [];
190
+ if (Array.isArray(message.content)) {
191
+ for (const part of message.content as Array<any>) {
192
+ if (part && part.type === "thinking" && typeof part.thinking === "string") {
193
+ reasoningParts.push(part.thinking);
194
+ } else if (part && part.type === "toolCall") {
195
+ toolCallCount += 1;
196
+ toolCalls.push({
197
+ id: typeof part.id === "string" ? part.id : `call_${toolCalls.length}`,
198
+ type: "function",
199
+ function: {
200
+ name: typeof part.name === "string" ? part.name : "unknown",
201
+ arguments: JSON.stringify(part.arguments ?? {}),
202
+ },
203
+ });
204
+ } else if (part && part.type === "text") {
205
+ // already folded into `content` via contentToText
206
+ } else {
207
+ // Images and any other non-text parts: counted in the
208
+ // summary, payload not embedded.
209
+ nonTextPartCount += 1;
210
+ }
211
+ }
212
+ }
213
+ const assistant: Record<string, unknown> = {
214
+ role: "assistant",
215
+ content: redact(text.text),
216
+ };
217
+ if (toolCalls.length > 0) assistant.tool_calls = toolCalls;
218
+ if (reasoningParts.length > 0) assistant.reasoning = redact(reasoningParts.join("\n\n"));
219
+ if (!model && typeof message.provider === "string" && typeof message.model === "string") {
220
+ model = `${message.provider}/${message.model}`;
221
+ }
222
+ messages.push(assistant);
223
+ if (reasoningParts.length > 0) reasoningMessageCount += 1;
224
+ continue;
225
+ }
226
+
227
+ if (role === "toolResult") {
228
+ const text = contentToText(message.content);
229
+ nonTextPartCount += text.nonTextParts;
230
+ const tool: Record<string, unknown> = {
231
+ role: "tool",
232
+ tool_call_id: typeof message.toolCallId === "string" ? message.toolCallId : "",
233
+ content: redact(text.text),
234
+ };
235
+ if (message.isError === true) tool.is_error = true;
236
+ messages.push(tool);
237
+ continue;
238
+ }
239
+
240
+ // user and anything else: full-fidelity text.
241
+ const text = contentToText(message.content);
242
+ nonTextPartCount += text.nonTextParts;
243
+ messages.push({ role, content: redact(text.text) });
244
+ }
245
+
246
+ const endsOnAssistant = messages.length > 0 && messages[messages.length - 1].role === "assistant";
247
+ const runCount = roles.user ?? 0;
248
+ const startedAt = parsed.startedAt;
249
+ const endedAt = parsed.endedAt ?? parsed.startedAt;
250
+
251
+ const artifact: Record<string, unknown> = {
252
+ session_id: input.sessionId,
253
+ user_id: input.userId ?? null,
254
+ client_id: input.clientId ?? null,
255
+ agent_id: input.agentId ?? null,
256
+ model: model ?? "unknown",
257
+ started_at: startedAt,
258
+ ended_at: endedAt,
259
+ duration_minutes: durationMinutes(startedAt, endedAt),
260
+ run_count: runCount,
261
+ incomplete: !endsOnAssistant,
262
+ context_pruned: parsed.sawCompaction,
263
+ unexported_tail_messages: 0,
264
+ system_prompt_chars: systemPrompt.length,
265
+ messages,
266
+ summary: {
267
+ message_count: messages.length,
268
+ roles: {
269
+ system: roles.system ?? 0,
270
+ user: roles.user ?? 0,
271
+ assistant: roles.assistant ?? 0,
272
+ tool: roles.toolResult ?? 0,
273
+ },
274
+ tool_call_count: toolCallCount,
275
+ reasoning_message_count: reasoningMessageCount,
276
+ non_text_part_count: nonTextPartCount,
277
+ ends_on_assistant: endsOnAssistant,
278
+ },
279
+ };
280
+ if (input.parentSessionId && input.parentSessionId !== input.sessionId) {
281
+ artifact.parent_session_id = input.parentSessionId;
282
+ }
283
+ return artifact;
284
+ }
285
+
286
+ /**
287
+ * pi stores sessions under <agentDir>/sessions/<cwd-slug>/<ts>_<id>.jsonl
288
+ * (e.g. /home/dan/proj -> --home-dan-proj--). Advisory only: the
289
+ * collector discovers the right dir by scanning for the session id, so
290
+ * this does not have to be perfect.
291
+ */
292
+ export function sessionDirSlug(cwd: string): string {
293
+ return `${cwd.replace(/\\//g, "-")}-`;
294
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omnirush",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "Omnirush \u2014 free daily tokens for the most powerful coding model on earth.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -0,0 +1,106 @@
1
+ import io, os, shutil, tarfile, urllib.request, zipfile
2
+
3
+ import argparse
4
+ ap = argparse.ArgumentParser()
5
+ ap.add_argument("--repo", default=os.getcwd())
6
+ ap.add_argument("--out", default=os.getcwd())
7
+ ap.add_argument("--only", default=None, help="comma-separated platform subset")
8
+ args = ap.parse_args()
9
+ REPO = os.path.abspath(args.repo)
10
+ WORK = os.path.abspath(args.out)
11
+ ONLY = set(args.only.split(",")) if args.only else None
12
+ BUN_VER = "bun-v1.4.2"
13
+ FD_VER, RG_VER = "v10.5.0", "15.2.0"
14
+ PLATFORMS = {
15
+ "linux-x64": dict(fd=f"fd-{FD_VER}-x86_64-unknown-linux-musl.tar.gz", rg=f"ripgrep-{RG_VER}-x86_64-unknown-linux-musl.tar.gz"),
16
+ "linux-arm64": dict(fd=f"fd-{FD_VER}-aarch64-unknown-linux-musl.tar.gz", rg=f"ripgrep-{RG_VER}-aarch64-unknown-linux-musl.tar.gz"),
17
+ "darwin-aarch64": dict(fd=f"fd-{FD_VER}-aarch64-apple-darwin.tar.gz", rg=f"ripgrep-{RG_VER}-aarch64-apple-darwin.tar.gz"),
18
+ "darwin-x64": dict(fd=f"fd-{FD_VER}-x86_64-apple-darwin.tar.gz", rg=f"ripgrep-{RG_VER}-x86_64-apple-darwin.tar.gz"),
19
+ "windows-x64": dict(fd=f"fd-{FD_VER}-x86_64-pc-windows-msvc.zip", rg=f"ripgrep-{RG_VER}-x86_64-pc-windows-msvc.zip"),
20
+ }
21
+
22
+ def fetch(url):
23
+ import time
24
+ req = urllib.request.Request(url, headers={"User-Agent": "omnirush-release-builder"})
25
+ for attempt in range(3):
26
+ try:
27
+ with urllib.request.urlopen(req, timeout=300) as r:
28
+ return r.read()
29
+ except Exception as e:
30
+ if attempt == 2:
31
+ raise
32
+ print("retry", url.split("/")[-1], "after", e)
33
+ time.sleep(5)
34
+
35
+ def extract_zip(data, dest):
36
+ os.makedirs(dest, exist_ok=True)
37
+ zf = zipfile.ZipFile(io.BytesIO(data))
38
+ zf.extractall(dest)
39
+
40
+ def extract_tgz(data, dest):
41
+ os.makedirs(dest, exist_ok=True)
42
+ tf = tarfile.open(fileobj=io.BytesIO(data), mode="r:gz")
43
+ tf.extractall(dest)
44
+
45
+ def find_bin(root, names):
46
+ for dirpath, _, files in os.walk(root):
47
+ for f in files:
48
+ if f in names:
49
+ return os.path.join(dirpath, f)
50
+ return None
51
+
52
+ for plat, cfg in PLATFORMS.items():
53
+ if ONLY and plat not in ONLY:
54
+ continue
55
+ out_pre = f"omnirush-cli-{plat}.tar.gz" if plat == "linux-x64" else f"omnirush-cli-{plat}.zip"
56
+ if os.path.exists(out_pre) and not ONLY:
57
+ print("skip (built):", plat)
58
+ continue
59
+ stage = os.path.join(WORK, f"omnirush-cli-{plat}")
60
+ shutil.rmtree(stage, ignore_errors=True)
61
+ tmp = os.path.join(WORK, f".tmp-{plat}")
62
+ shutil.rmtree(tmp, ignore_errors=True)
63
+ os.makedirs(os.path.join(stage, ".runtime", "bin"))
64
+ os.makedirs(tmp)
65
+ shutil.copytree(os.path.join(REPO, "src"), os.path.join(stage, "src"))
66
+ shutil.copytree(os.path.join(REPO, "assets"), os.path.join(stage, "assets"))
67
+ shutil.copy2(os.path.join(REPO, "package.json"), os.path.join(stage, "package.json"))
68
+ exe = "bun.exe" if plat == "windows-x64" else "bun"
69
+ bun_plat = plat.replace("arm64", "aarch64")
70
+ bunzip = fetch(f"https://github.com/oven-sh/bun/releases/download/{BUN_VER}/bun-{bun_plat}.zip")
71
+ extract_zip(bunzip, tmp)
72
+ extracted_dir = os.path.join(tmp, f"bun-{bun_plat}")
73
+ shutil.copy2(os.path.join(extracted_dir, exe), os.path.join(stage, ".runtime", "bin", exe))
74
+ if plat != "windows-x64":
75
+ os.chmod(os.path.join(stage, ".runtime", "bin", "bun"), 0o755)
76
+ def fetch_extract(url, dest, cfg_value):
77
+ data = fetch(url)
78
+ if cfg_value.endswith(".zip"):
79
+ extract_zip(data, dest)
80
+ else:
81
+ extract_tgz(data, dest)
82
+ fetch_extract(f"https://github.com/sharkdp/fd/releases/download/{FD_VER}/{cfg['fd']}", os.path.join(tmp, "fd"), cfg["fd"])
83
+ fetch_extract(f"https://github.com/BurntSushi/ripgrep/releases/download/{RG_VER}/{cfg['rg']}", os.path.join(tmp, "rg"), cfg["rg"])
84
+ fb = find_bin(os.path.join(tmp, "fd"), {"fd", "fd.exe"})
85
+ rb = find_bin(os.path.join(tmp, "rg"), {"rg", "rg.exe"})
86
+ assert fb and rb, f"fd/rg not found for {plat}"
87
+ shutil.copy2(fb, os.path.join(stage, ".runtime", "bin", os.path.basename(fb)))
88
+ shutil.copy2(rb, os.path.join(stage, ".runtime", "bin", os.path.basename(rb)))
89
+ if plat == "linux-x64":
90
+ out = f"omnirush-cli-{plat}.tar.gz"
91
+ with tarfile.open(out, "w:gz") as t:
92
+ t.add(stage, arcname=os.path.basename(stage))
93
+ else:
94
+ with tarfile.open(f"omnirush-cli-{plat}.zip", "w") as t: # plain zip via tarfile? no
95
+ pass
96
+ # use zipfile properly
97
+ import zipfile as zfmod
98
+ with zfmod.ZipFile(f"omnirush-cli-{plat}.zip", "w", zfmod.ZIP_DEFLATED) as z:
99
+ for dirpath, _, files in os.walk(stage):
100
+ for f in files:
101
+ full = os.path.join(dirpath, f)
102
+ z.write(full, os.path.relpath(full, WORK))
103
+ print("built", plat)
104
+ shutil.rmtree(stage, ignore_errors=True)
105
+ shutil.rmtree(tmp, ignore_errors=True)
106
+ print("ALL PLATFORMS BUILT")
@@ -145,11 +145,15 @@ function findFile(rootDir, fileName) {
145
145
  return null;
146
146
  }
147
147
 
148
- async function fetchBun(plat) {
149
- const existing = path.join(RUNTIME, exe("bun"));
148
+ export function managedBunPath() {
149
+ return path.join(RUNTIME, exe("bun"));
150
+ }
151
+
152
+ async function fetchBun(plat, log = console.log) {
153
+ const existing = managedBunPath();
150
154
  if (fs.existsSync(existing)) return;
151
155
  const zipUrl = `https://github.com/oven-sh/bun/releases/download/${BUN_VERSION}/bun-${bunAssetPlat(plat)}.zip`;
152
- process.stdout.write(`omnirush: downloading agent runtime (bun for ${plat})...\n`);
156
+ log(`omnirush: downloading agent runtime (bun for ${plat})...`);
153
157
  const buf = await get(zipUrl);
154
158
  fs.mkdirSync(RUNTIME, { recursive: true });
155
159
  extractZip(buf, ROOT);
@@ -157,7 +161,7 @@ async function fetchBun(plat) {
157
161
  fs.copyFileSync(src, existing);
158
162
  if (process.platform !== "win32") fs.chmodSync(existing, 0o755);
159
163
  fs.rmSync(path.join(ROOT, `bun-${bunAssetPlat(plat)}`), { recursive: true, force: true });
160
- console.log("omnirush: runtime ready");
164
+ log("omnirush: runtime ready");
161
165
  }
162
166
 
163
167
  // fd/rg release archives both nest the binary under a versioned directory
@@ -196,9 +200,14 @@ function rgAsset(plat) {
196
200
  return `ripgrep-${RG_VERSION}-${arch}-unknown-linux-musl`;
197
201
  }
198
202
 
199
- async function main() {
203
+ /**
204
+ * Fetch the agent runtime when it is missing (idempotent: existing
205
+ * binaries are kept). Used by postinstall and by `omnirush doctor`
206
+ * self-heal. `log` defaults to console.log.
207
+ */
208
+ export async function ensureRuntime({ log = console.log } = {}) {
200
209
  const plat = platform();
201
- await fetchBun(plat);
210
+ await fetchBun(plat, log);
202
211
  for (const [name, version, repo, archive] of [
203
212
  ["fd", FD_VERSION, "sharkdp/fd", fdAsset(plat)],
204
213
  ["rg", RG_VERSION, "BurntSushi/ripgrep", rgAsset(plat)],
@@ -208,9 +217,17 @@ async function main() {
208
217
  } catch (error) {
209
218
  // Optional tools: warn and continue (the launcher only installs what
210
219
  // exists; the agent degrades to PATH lookup / built-in search).
211
- console.warn(`omnirush: ${name} download failed (search may be degraded):`, error.message);
220
+ log(`omnirush: ${name} download failed (search may be degraded): ${error.message}`);
212
221
  }
213
222
  }
214
223
  }
215
224
 
216
- main().catch((e) => { console.error("omnirush postinstall:", e.message); process.exit(1); });
225
+ async function main() {
226
+ await ensureRuntime();
227
+ }
228
+
229
+ // CLI entry (postinstall). Imported as a module by the launcher — never
230
+ // exit there.
231
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
232
+ main().catch((e) => { console.error("omnirush postinstall:", e.message); process.exit(1); });
233
+ }