omnirush 0.1.1 → 0.3.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 +46 -32
- package/assets/extensions/omnirush/collector-lib.ts +937 -226
- package/assets/extensions/omnirush/collector.ts +7 -3
- package/assets/extensions/omnirush/commands.ts +34 -8
- package/assets/extensions/omnirush/sota.ts +36 -14
- package/assets/extensions/omnirush/usage.ts +157 -9
- package/package.json +3 -2
- package/scripts/package.sh +11 -0
- package/scripts/patch-pi-branding.js +224 -0
- package/scripts/postinstall.js +102 -10
- package/src/bin.js +87 -7
- package/src/lib.js +51 -16
- package/src/tools.js +40 -0
package/scripts/postinstall.js
CHANGED
|
@@ -1,12 +1,24 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
1
|
+
// Postinstall runtime fetcher:
|
|
2
|
+
// 1. bun (v1.4.2) — required; the agent core runs on it.
|
|
3
|
+
// 2. fd (v10.5.0) — optional; the agent's file search + autocomplete.
|
|
4
|
+
// 3. rg (v15.2.0) — optional; the agent's content search.
|
|
5
|
+
//
|
|
6
|
+
// Binaries land in .runtime/bin/ (per-platform release packages ship the
|
|
7
|
+
// same layout prebuilt). The launcher copies fd/rg into the agent's
|
|
8
|
+
// managed-bin dir on every run, so the agent never attempts its own
|
|
9
|
+
// GitHub download at startup (which fails on restricted networks and
|
|
10
|
+
// repeats on every launch). fd/rg failures are warnings: search degrades
|
|
11
|
+
// gracefully without them. A bun failure stays fatal.
|
|
3
12
|
import { execFileSync } from "node:child_process";
|
|
4
13
|
import fs from "node:fs";
|
|
5
14
|
import https from "node:https";
|
|
6
15
|
import path from "node:path";
|
|
7
16
|
import { fileURLToPath } from "node:url";
|
|
8
17
|
|
|
9
|
-
const
|
|
18
|
+
const BUN_VERSION = "bun-v1.4.2";
|
|
19
|
+
// fd release tags are v-prefixed; ripgrep tags are bare ("15.2.0").
|
|
20
|
+
const FD_VERSION = "v10.5.0";
|
|
21
|
+
const RG_VERSION = "15.2.0";
|
|
10
22
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
11
23
|
const ROOT = path.resolve(__dirname, "..");
|
|
12
24
|
const RUNTIME = path.join(ROOT, ".runtime", "bin");
|
|
@@ -18,6 +30,10 @@ function platform() {
|
|
|
18
30
|
return `${plat}-${arch}`;
|
|
19
31
|
}
|
|
20
32
|
|
|
33
|
+
function exe(name) {
|
|
34
|
+
return name + (process.platform === "win32" ? ".exe" : "");
|
|
35
|
+
}
|
|
36
|
+
|
|
21
37
|
function get(url, redirects = 0) {
|
|
22
38
|
if (redirects > 5) throw new Error("too many redirects");
|
|
23
39
|
return new Promise((resolve, reject) => {
|
|
@@ -57,22 +73,98 @@ function extractZip(buf, dest) {
|
|
|
57
73
|
fs.rmSync(tmp, { force: true });
|
|
58
74
|
}
|
|
59
75
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
76
|
+
function extractTarGz(buf, dest, stamp) {
|
|
77
|
+
const tmp = path.join(dest, stamp);
|
|
78
|
+
fs.writeFileSync(tmp, buf);
|
|
79
|
+
// tar handles .tar.gz on linux/macOS; Windows 10+ ships bsdtar in
|
|
80
|
+
// System32 (the agent core relies on the same thing).
|
|
81
|
+
try {
|
|
82
|
+
execFileSync("tar", ["xzf", tmp, "-C", dest]);
|
|
83
|
+
} catch {
|
|
84
|
+
execFileSync("python3", ["-m", "tarfile", "-e", tmp, "-C", dest]);
|
|
85
|
+
}
|
|
86
|
+
fs.rmSync(tmp, { force: true });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function findFile(rootDir, fileName) {
|
|
90
|
+
const stack = [rootDir];
|
|
91
|
+
while (stack.length > 0) {
|
|
92
|
+
const dir = stack.pop();
|
|
93
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
94
|
+
const full = path.join(dir, entry.name);
|
|
95
|
+
if (entry.isFile() && entry.name === fileName) return full;
|
|
96
|
+
if (entry.isDirectory()) stack.push(full);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function fetchBun(plat) {
|
|
103
|
+
const existing = path.join(RUNTIME, exe("bun"));
|
|
63
104
|
if (fs.existsSync(existing)) return;
|
|
64
|
-
const
|
|
65
|
-
const zipUrl = `https://github.com/oven-sh/bun/releases/download/${VERSION}/bun-${plat}.zip`;
|
|
105
|
+
const zipUrl = `https://github.com/oven-sh/bun/releases/download/${BUN_VERSION}/bun-${plat}.zip`;
|
|
66
106
|
process.stdout.write(`omnirush: downloading agent runtime (bun for ${plat})...\n`);
|
|
67
107
|
const buf = await get(zipUrl);
|
|
68
108
|
fs.mkdirSync(RUNTIME, { recursive: true });
|
|
69
109
|
extractZip(buf, ROOT);
|
|
70
|
-
const
|
|
71
|
-
const src = path.join(ROOT, `bun-${plat}`, exe);
|
|
110
|
+
const src = path.join(ROOT, `bun-${plat}`, exe("bun"));
|
|
72
111
|
fs.copyFileSync(src, existing);
|
|
73
112
|
if (process.platform !== "win32") fs.chmodSync(existing, 0o755);
|
|
74
113
|
fs.rmSync(path.join(ROOT, `bun-${plat}`), { recursive: true, force: true });
|
|
75
114
|
console.log("omnirush: runtime ready");
|
|
76
115
|
}
|
|
77
116
|
|
|
117
|
+
// fd/rg release archives both nest the binary under a versioned directory
|
|
118
|
+
// (fd-v10.5.0-x86_64-unknown-linux-musl/fd, ripgrep-15.2.0-.../rg).
|
|
119
|
+
async function fetchSearchTool(name, version, repo, archiveName, plat) {
|
|
120
|
+
const existing = path.join(RUNTIME, exe(name));
|
|
121
|
+
if (fs.existsSync(existing)) return;
|
|
122
|
+
const isZip = plat === "windows-x64";
|
|
123
|
+
const asset = isZip ? `${archiveName}.zip` : `${archiveName}.tar.gz`;
|
|
124
|
+
const url = `https://github.com/${repo}/releases/download/${version}/${asset}`;
|
|
125
|
+
process.stdout.write(`omnirush: downloading ${name} (${plat})...\n`);
|
|
126
|
+
const buf = await get(url);
|
|
127
|
+
fs.mkdirSync(RUNTIME, { recursive: true });
|
|
128
|
+
const extractDir = path.join(RUNTIME, `extract-${name}`);
|
|
129
|
+
fs.mkdirSync(extractDir, { recursive: true });
|
|
130
|
+
if (isZip) extractZip(buf, extractDir);
|
|
131
|
+
else extractTarGz(buf, extractDir, asset);
|
|
132
|
+
const found = findFile(extractDir, exe(name));
|
|
133
|
+
if (!found) throw new Error(`${exe(name)} not found in ${asset}`);
|
|
134
|
+
fs.copyFileSync(found, existing);
|
|
135
|
+
if (process.platform !== "win32") fs.chmodSync(existing, 0o755);
|
|
136
|
+
fs.rmSync(extractDir, { recursive: true, force: true });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function fdAsset(plat) {
|
|
140
|
+
const arch = process.arch === "arm64" ? "aarch64" : "x86_64";
|
|
141
|
+
if (plat === "windows-x64") return `fd-${FD_VERSION}-${arch}-pc-windows-msvc`;
|
|
142
|
+
if (plat === "darwin-x64" || plat === "darwin-aarch64") return `fd-${FD_VERSION}-${arch}-apple-darwin`;
|
|
143
|
+
return `fd-${FD_VERSION}-${arch}-unknown-linux-musl`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function rgAsset(plat) {
|
|
147
|
+
const arch = process.arch === "arm64" ? "aarch64" : "x86_64";
|
|
148
|
+
if (plat === "windows-x64") return `ripgrep-${RG_VERSION}-${arch}-pc-windows-msvc`;
|
|
149
|
+
if (plat === "darwin-x64" || plat === "darwin-aarch64") return `ripgrep-${RG_VERSION}-${arch}-apple-darwin`;
|
|
150
|
+
return `ripgrep-${RG_VERSION}-${arch}-unknown-linux-musl`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function main() {
|
|
154
|
+
const plat = platform();
|
|
155
|
+
await fetchBun(plat);
|
|
156
|
+
for (const [name, version, repo, archive] of [
|
|
157
|
+
["fd", FD_VERSION, "sharkdp/fd", fdAsset(plat)],
|
|
158
|
+
["rg", RG_VERSION, "BurntSushi/ripgrep", rgAsset(plat)],
|
|
159
|
+
]) {
|
|
160
|
+
try {
|
|
161
|
+
await fetchSearchTool(name, version, repo, archive, plat);
|
|
162
|
+
} catch (error) {
|
|
163
|
+
// Optional tools: warn and continue (the launcher only installs what
|
|
164
|
+
// exists; the agent degrades to PATH lookup / built-in search).
|
|
165
|
+
console.warn(`omnirush: ${name} download failed (search may be degraded):`, error.message);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
78
170
|
main().catch((e) => { console.error("omnirush postinstall:", e.message); process.exit(1); });
|
package/src/bin.js
CHANGED
|
@@ -18,10 +18,12 @@
|
|
|
18
18
|
// OMNIRUSH_GATEWAY_URL provider base URL override ({origin}/v1)
|
|
19
19
|
// OMNIRUSH_DIR state dir override (default ~/.omnirush)
|
|
20
20
|
// OMNIRUSH_MODEL model override for plain runs (default
|
|
21
|
-
// gpt-6-astra;
|
|
21
|
+
// gpt-6-astra; gpt-5.6-sol also available; user
|
|
22
|
+
// --model still wins)
|
|
22
23
|
//
|
|
23
|
-
// Agent runs default to `--provider omnirush --model gpt-6-astra
|
|
24
|
-
//
|
|
24
|
+
// Agent runs default to `--provider omnirush --model gpt-6-astra`; the
|
|
25
|
+
// catalog also carries gpt-5.6-sol (`--model gpt-5.6-sol` or
|
|
26
|
+
// OMNIRUSH_MODEL=gpt-5.6-sol). The sota extension watches every response,
|
|
25
27
|
// refreshes the device token single-flight on 401 (retry once), and
|
|
26
28
|
// warns on stderr if the gateway serves a different model than
|
|
27
29
|
// requested; `--strict-sota` makes that violation fatal (exit 3).
|
|
@@ -42,6 +44,7 @@ import {
|
|
|
42
44
|
withModelDefaults,
|
|
43
45
|
resolveModelDefaults,
|
|
44
46
|
} from "./lib.js";
|
|
47
|
+
import { installManagedTools } from "./tools.js";
|
|
45
48
|
import {
|
|
46
49
|
clearAuth,
|
|
47
50
|
deviceAuthorize,
|
|
@@ -99,6 +102,11 @@ function ensureDirs() {
|
|
|
99
102
|
fs.mkdirSync(OMNI_DIR, { recursive: true, mode: 0o700 });
|
|
100
103
|
fs.mkdirSync(PI_AGENT_DIR, { recursive: true });
|
|
101
104
|
fs.mkdirSync(PI_EXT_DIR, { recursive: true });
|
|
105
|
+
try {
|
|
106
|
+
installManagedTools({ agentDir: PI_AGENT_DIR, runtimeRoot: path.resolve(__dirname, "..", ".runtime") });
|
|
107
|
+
} catch {
|
|
108
|
+
/* best effort — pi falls back to PATH, then its own downloader */
|
|
109
|
+
}
|
|
102
110
|
}
|
|
103
111
|
|
|
104
112
|
// Merge our provider into the user's models.json without clobbering
|
|
@@ -353,10 +361,78 @@ async function cmdDoctor() {
|
|
|
353
361
|
}
|
|
354
362
|
}
|
|
355
363
|
|
|
364
|
+
// Update check: npm latest vs this install (cached, once/day).
|
|
365
|
+
try {
|
|
366
|
+
const own = JSON.parse(fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8")).version;
|
|
367
|
+
const cacheFile = path.join(OMNI_DIR, "update-check.json");
|
|
368
|
+
let latest = null;
|
|
369
|
+
let cache = {};
|
|
370
|
+
try { cache = JSON.parse(fs.readFileSync(cacheFile, "utf8")); } catch {}
|
|
371
|
+
if (cache.latest && Date.now() - cache.checked_at < 24 * 60 * 60 * 1000) {
|
|
372
|
+
latest = cache.latest;
|
|
373
|
+
} else {
|
|
374
|
+
const r = await fetchJson("https://registry.npmjs.org/omnirush/latest", { timeout: 5000 });
|
|
375
|
+
if (r.status === 200) {
|
|
376
|
+
latest = JSON.parse(r.body).version;
|
|
377
|
+
fs.writeFileSync(cacheFile, JSON.stringify({ latest, checked_at: Date.now() }));
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
if (latest && latest !== own) {
|
|
381
|
+
const cmp = (a, b) => {
|
|
382
|
+
const pa = a.split(".").map(Number), pb = b.split(".").map(Number);
|
|
383
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
384
|
+
const d = (pa[i] || 0) - (pb[i] || 0);
|
|
385
|
+
if (d) return d;
|
|
386
|
+
}
|
|
387
|
+
return 0;
|
|
388
|
+
};
|
|
389
|
+
if (cmp(latest, own) > 0) {
|
|
390
|
+
ok("update", true, `update available: ${latest} (installed ${own}) — run omnirush update`);
|
|
391
|
+
} else {
|
|
392
|
+
ok("update", true, `installed ${own} is newer than published ${latest} (pre-release)`);
|
|
393
|
+
}
|
|
394
|
+
} else if (latest) {
|
|
395
|
+
ok("update", true, `up to date (${own})`);
|
|
396
|
+
}
|
|
397
|
+
} catch {
|
|
398
|
+
/* update check is advisory only */
|
|
399
|
+
}
|
|
400
|
+
|
|
356
401
|
installModelsJson(gateway);
|
|
357
402
|
installExtensions();
|
|
358
|
-
|
|
403
|
+
try {
|
|
404
|
+
installManagedTools({ agentDir: PI_AGENT_DIR, runtimeRoot: path.resolve(__dirname, "..", ".runtime") });
|
|
405
|
+
} catch {
|
|
406
|
+
/* best effort */
|
|
407
|
+
}
|
|
408
|
+
ok("agent config", true, PI_MODELS_JSON);
|
|
359
409
|
ok("extensions", true, PI_EXT_DIR);
|
|
410
|
+
// Search tools: bundled fd/rg (or system ones) keep the agent off its
|
|
411
|
+
// GitHub download path, which fails on restricted networks.
|
|
412
|
+
const toolsRoot = path.join(PI_AGENT_DIR, "bin");
|
|
413
|
+
for (const tool of ["fd", "rg"]) {
|
|
414
|
+
const exe = process.platform === "win32" ? ".exe" : "";
|
|
415
|
+
const managed = path.join(toolsRoot, tool + exe);
|
|
416
|
+
const hasManaged = fs.existsSync(managed);
|
|
417
|
+
let viaPath = false;
|
|
418
|
+
if (!hasManaged) {
|
|
419
|
+
for (const dir of (process.env.PATH || "").split(path.delimiter)) {
|
|
420
|
+
if (!dir) continue;
|
|
421
|
+
try {
|
|
422
|
+
fs.accessSync(path.join(dir, tool + exe), fs.constants.X_OK);
|
|
423
|
+
viaPath = true;
|
|
424
|
+
break;
|
|
425
|
+
} catch {
|
|
426
|
+
/* next */
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
ok(
|
|
431
|
+
`search tool ${tool}`,
|
|
432
|
+
hasManaged || viaPath,
|
|
433
|
+
hasManaged ? managed : viaPath ? "found on PATH" : "missing — find/autocomplete degraded",
|
|
434
|
+
);
|
|
435
|
+
}
|
|
360
436
|
process.exit(healthy ? 0 : 1);
|
|
361
437
|
}
|
|
362
438
|
|
|
@@ -385,9 +461,13 @@ function childEnv(auth) {
|
|
|
385
461
|
OMNIRUSH_ORIGIN: origin,
|
|
386
462
|
OMNIRUSH_GATEWAY_URL: gateway,
|
|
387
463
|
OMNIRUSH_DIR: OMNI_DIR,
|
|
388
|
-
//
|
|
389
|
-
//
|
|
464
|
+
// The agent resolves its config dir from <APP>_CODING_AGENT_DIR. The
|
|
465
|
+
// branding patch renames the app to omnirush, so set the omnirush
|
|
466
|
+
// spelling; the pi spelling stays for unpatched cores.
|
|
467
|
+
OMNIRUSH_CODING_AGENT_DIR: PI_AGENT_DIR,
|
|
390
468
|
PI_CODING_AGENT_DIR: PI_AGENT_DIR,
|
|
469
|
+
// The agent core is a fork/rebrand: never phone home to pi.dev.
|
|
470
|
+
PI_TELEMETRY: "0",
|
|
391
471
|
};
|
|
392
472
|
}
|
|
393
473
|
|
|
@@ -433,7 +513,7 @@ if (cmd === "login") await cmdLogin(argv.slice(1));
|
|
|
433
513
|
else if (cmd === "logout") await cmdLogout();
|
|
434
514
|
else if (cmd === "whoami") await cmdWhoami();
|
|
435
515
|
else if (cmd === "doctor") await cmdDoctor();
|
|
436
|
-
else if (cmd === "--version" || cmd === "-v") console.log("omnirush 0.
|
|
516
|
+
else if (cmd === "--version" || cmd === "-v") console.log("omnirush 0.3.0");
|
|
437
517
|
else if (cmd === "collect") await cmdCollect(argv.slice(1));
|
|
438
518
|
else if (cmd === undefined || cmd === "run") await cmdRun();
|
|
439
519
|
else {
|
package/src/lib.js
CHANGED
|
@@ -67,7 +67,8 @@ const VALUE_FLAGS = new Set([
|
|
|
67
67
|
|
|
68
68
|
/**
|
|
69
69
|
* Model defaults for plain `omnirush` runs. OMNIRUSH_MODEL overrides the
|
|
70
|
-
* default model
|
|
70
|
+
* default model — any catalog id below, optionally with a thinking level
|
|
71
|
+
* like "gpt-6-astra:high" or "gpt-5.6-sol:xhigh".
|
|
71
72
|
*/
|
|
72
73
|
export function resolveModelDefaults(env = process.env) {
|
|
73
74
|
return {
|
|
@@ -76,6 +77,35 @@ export function resolveModelDefaults(env = process.env) {
|
|
|
76
77
|
};
|
|
77
78
|
}
|
|
78
79
|
|
|
80
|
+
// The product's shipped catalog: gpt-6-astra (default) and gpt-5.6-sol.
|
|
81
|
+
// Live-verified 2026-09-22 against the production gateway: GET /v1/models
|
|
82
|
+
// lists both ids and POST /v1/responses with model "gpt-5.6-sol" is served
|
|
83
|
+
// as gpt-5.6-sol (x-omnirush-model response header + the model named in
|
|
84
|
+
// every SSE frame), so the client may offer the switch honestly.
|
|
85
|
+
export const MODELS = [
|
|
86
|
+
{
|
|
87
|
+
id: "gpt-6-astra",
|
|
88
|
+
name: "GPT-6 Astra",
|
|
89
|
+
reasoning: true,
|
|
90
|
+
input: ["text", "image"],
|
|
91
|
+
cost: { input: 10, output: 50, cacheRead: 2.5, cacheWrite: 12.5 },
|
|
92
|
+
contextWindow: 400000,
|
|
93
|
+
compat: { supportsMaxOutputTokens: false },
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
id: "gpt-5.6-sol",
|
|
97
|
+
name: "GPT-5.6 Sol",
|
|
98
|
+
reasoning: true,
|
|
99
|
+
input: ["text", "image"],
|
|
100
|
+
// Sol list pricing is not published; pi's cost display therefore shows
|
|
101
|
+
// zeros for it (documented default when cost is omitted).
|
|
102
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
103
|
+
// Same codex backend family as astra; same context window.
|
|
104
|
+
contextWindow: 400000,
|
|
105
|
+
compat: { supportsMaxOutputTokens: false },
|
|
106
|
+
},
|
|
107
|
+
];
|
|
108
|
+
|
|
79
109
|
/**
|
|
80
110
|
* Prepend `--provider`/`--model` defaults for agent runs, without
|
|
81
111
|
* duplicating flags the user already specified. Consumption rules mirror
|
|
@@ -145,12 +175,13 @@ export function withModelDefaults(args, defaults) {
|
|
|
145
175
|
|
|
146
176
|
/**
|
|
147
177
|
* The provider/models.json payload omnirush installs into pi. This is the
|
|
148
|
-
* product's shipped model catalog
|
|
149
|
-
*
|
|
150
|
-
*
|
|
178
|
+
* product's shipped model catalog (MODELS above). The gateway allow-lists
|
|
179
|
+
* the same ids server-side; the client SOTA guard stays on as verification
|
|
180
|
+
* (it compares the model named in the response frames against the one
|
|
181
|
+
* requested).
|
|
151
182
|
*
|
|
152
183
|
* Hard-won compat fact: the codex backend rejects
|
|
153
|
-
* `max_output_tokens`, so
|
|
184
|
+
* `max_output_tokens`, so every model sets
|
|
154
185
|
* `compat.supportsMaxOutputTokens: false` and ships no maxTokens — pi would
|
|
155
186
|
* otherwise clamp a default and break every request.
|
|
156
187
|
*/
|
|
@@ -162,18 +193,22 @@ export function modelsConfig({ gatewayUrl } = {}) {
|
|
|
162
193
|
baseUrl: gatewayUrl ?? DEFAULT_GATEWAY_URL,
|
|
163
194
|
api: "openai-responses",
|
|
164
195
|
apiKey: "$OMNIRUSH_TOKEN",
|
|
165
|
-
models:
|
|
166
|
-
{
|
|
167
|
-
id: DEFAULT_MODEL,
|
|
168
|
-
name: "GPT-6 Astra",
|
|
169
|
-
reasoning: true,
|
|
170
|
-
input: ["text", "image"],
|
|
171
|
-
cost: { input: 10, output: 50, cacheRead: 2.5, cacheWrite: 12.5 },
|
|
172
|
-
contextWindow: 400000,
|
|
173
|
-
compat: { supportsMaxOutputTokens: false },
|
|
174
|
-
},
|
|
175
|
-
],
|
|
196
|
+
models: MODELS.map((model) => ({ ...model })),
|
|
176
197
|
},
|
|
177
198
|
},
|
|
178
199
|
};
|
|
179
200
|
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Pure mapping of the bundled search-tool binaries for the launcher: pi
|
|
204
|
+
* looks for managed `fd`/`rg` in <agentDir>/bin first and downloads from
|
|
205
|
+
* GitHub when absent. We ship both binaries and the launcher places them
|
|
206
|
+
* there, so the agent never attempts (or repeats) a download.
|
|
207
|
+
*/
|
|
208
|
+
export function managedToolTargets({ platform = process.platform, runtimeRoot } = {}) {
|
|
209
|
+
const ext = platform === "win32" ? ".exe" : "";
|
|
210
|
+
return [
|
|
211
|
+
{ tool: "fd", source: `${runtimeRoot}/bin/fd${ext}`, dest: `bin/fd${ext}` },
|
|
212
|
+
{ tool: "rg", source: `${runtimeRoot}/bin/rg${ext}`, dest: `bin/rg${ext}` },
|
|
213
|
+
];
|
|
214
|
+
}
|
package/src/tools.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Managed search-tool install (fd, rg) for the agent core.
|
|
2
|
+
//
|
|
3
|
+
// pi checks <agentDir>/bin for managed fd/rg first and, when absent,
|
|
4
|
+
// downloads them from GitHub at every agent startup — on restricted
|
|
5
|
+
// networks that download fails and the attempt repeats on every launch,
|
|
6
|
+
// breaking the find tool and file autocomplete. We bundle the binaries
|
|
7
|
+
// (.runtime/bin, filled by postinstall or the release package) and the
|
|
8
|
+
// launcher places them exactly where pi looks. Best effort: any failure
|
|
9
|
+
// leaves the agent to its own PATH lookup / downloader.
|
|
10
|
+
|
|
11
|
+
import fs from "node:fs";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
|
|
14
|
+
import { managedToolTargets } from "./lib.js";
|
|
15
|
+
|
|
16
|
+
export function installManagedTools({
|
|
17
|
+
agentDir,
|
|
18
|
+
runtimeRoot,
|
|
19
|
+
platform = process.platform,
|
|
20
|
+
fsImpl = fs,
|
|
21
|
+
} = {}) {
|
|
22
|
+
const installed = [];
|
|
23
|
+
if (!agentDir || !runtimeRoot) return installed;
|
|
24
|
+
for (const target of managedToolTargets({ platform, runtimeRoot })) {
|
|
25
|
+
const dest = path.join(agentDir, target.dest);
|
|
26
|
+
if (fsImpl.existsSync(dest)) continue;
|
|
27
|
+
if (!fsImpl.existsSync(target.source)) continue;
|
|
28
|
+
fsImpl.mkdirSync(path.dirname(dest), { recursive: true });
|
|
29
|
+
fsImpl.copyFileSync(target.source, dest);
|
|
30
|
+
if (platform !== "win32") {
|
|
31
|
+
try {
|
|
32
|
+
fsImpl.chmodSync(dest, 0o755);
|
|
33
|
+
} catch {
|
|
34
|
+
/* best effort */
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
installed.push(target.tool);
|
|
38
|
+
}
|
|
39
|
+
return installed;
|
|
40
|
+
}
|