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.
- package/assets/collect-once.ts +77 -39
- package/assets/extensions/omnirush/auth.js +56 -17
- package/assets/extensions/omnirush/collector-lib.ts +606 -179
- package/assets/extensions/omnirush/collector.ts +45 -1
- package/assets/extensions/omnirush/retry.js +144 -0
- package/assets/extensions/omnirush/sota-lib.ts +64 -0
- package/assets/extensions/omnirush/sota.ts +159 -28
- package/assets/extensions/omnirush/trace-format.ts +294 -0
- package/package.json +1 -1
- package/scripts/build-all-packages.py +106 -0
- package/scripts/postinstall.js +25 -8
- package/src/bin.js +253 -42
- package/src/lib.js +13 -0
|
@@ -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
|
@@ -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")
|
package/scripts/postinstall.js
CHANGED
|
@@ -145,11 +145,15 @@ function findFile(rootDir, fileName) {
|
|
|
145
145
|
return null;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
220
|
+
log(`omnirush: ${name} download failed (search may be degraded): ${error.message}`);
|
|
212
221
|
}
|
|
213
222
|
}
|
|
214
223
|
}
|
|
215
224
|
|
|
216
|
-
main()
|
|
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
|
+
}
|