omnirush 0.8.5 → 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.
- package/assets/CHANGELOG.md +85 -0
- package/assets/collect-once.ts +6 -5
- package/assets/extensions/omnirush/agents-lib.ts +196 -12
- package/assets/extensions/omnirush/agents.ts +97 -105
- package/assets/extensions/omnirush/auth.js +399 -115
- package/assets/extensions/omnirush/bgshell-lib.ts +400 -0
- package/assets/extensions/omnirush/bgshell.ts +392 -0
- package/assets/extensions/omnirush/collector.ts +103 -16
- package/assets/extensions/omnirush/commands.ts +14 -15
- package/assets/extensions/omnirush/deliveries.ts +145 -0
- package/assets/extensions/omnirush/guard/UPSTREAM +2 -0
- package/assets/extensions/omnirush/guard/git-command-policy.ts +885 -0
- package/assets/extensions/omnirush/guard-lib.ts +230 -0
- package/assets/extensions/omnirush/guard.ts +340 -0
- package/assets/extensions/omnirush/index.ts +12 -0
- package/assets/extensions/omnirush/pi-engine.ts +65 -1
- package/assets/extensions/omnirush/sota.ts +104 -47
- package/assets/extensions/omnirush/status-lib.ts +3 -0
- package/assets/extensions/omnirush/subagents-lib.ts +623 -0
- package/assets/extensions/omnirush/subagents.ts +305 -0
- package/assets/extensions/omnirush/swarm-lib.ts +142 -0
- package/assets/extensions/omnirush/swarm.ts +95 -0
- package/assets/extensions/omnirush/voice/capture.ts +502 -0
- package/assets/extensions/omnirush/voice/core/UPSTREAM +16 -0
- package/assets/extensions/omnirush/voice/core/file-source.ts +70 -0
- package/assets/extensions/omnirush/voice/core/index.ts +21 -0
- package/assets/extensions/omnirush/voice/core/keyterms.ts +117 -0
- package/assets/extensions/omnirush/voice/core/resample.ts +63 -0
- package/assets/extensions/omnirush/voice/core/segmenter.ts +231 -0
- package/assets/extensions/omnirush/voice/core/session.ts +403 -0
- package/assets/extensions/omnirush/voice/core/text.ts +81 -0
- package/assets/extensions/omnirush/voice/core/transcriber.ts +135 -0
- package/assets/extensions/omnirush/voice/core/types.ts +102 -0
- package/assets/extensions/omnirush/voice/core/wav.ts +95 -0
- package/assets/extensions/omnirush/voice/keys.ts +435 -0
- package/assets/extensions/omnirush/voice/kitty.ts +64 -0
- package/assets/extensions/omnirush/voice/pvrecorder-worker.cjs +43 -0
- package/assets/extensions/omnirush/voice/settings.ts +67 -0
- package/assets/extensions/omnirush/voice.ts +838 -0
- package/assets/extensions/omnirush/yolo-lib.ts +80 -0
- package/assets/extensions/omnirush/yolo.ts +85 -0
- package/package.json +6 -2
- package/scripts/brand-engine.js +526 -0
- package/scripts/build-all-packages.py +29 -1
- package/scripts/smoke-packages.py +32 -1
- package/src/bin.js +252 -53
- package/src/compat.js +272 -0
- package/src/lib.js +64 -0
- package/scripts/patch-pi-branding.js +0 -251
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
|
+
}
|
package/src/lib.js
CHANGED
|
@@ -403,3 +403,67 @@ export function managedToolTargets({ platform = process.platform, runtimeRoot }
|
|
|
403
403
|
{ tool: "rg", source: `${runtimeRoot}/bin/rg${ext}`, dest: `bin/rg${ext}` },
|
|
404
404
|
];
|
|
405
405
|
}
|
|
406
|
+
|
|
407
|
+
// --- yolo mode ---------------------------------------------------------------
|
|
408
|
+
//
|
|
409
|
+
// `--yolo`, `/yolo on` (saved as "yolo": true in <state dir>/settings.json) or
|
|
410
|
+
// OMNIRUSH_YOLO=1: the project folder is trusted for the run (the core's
|
|
411
|
+
// --approve) and nothing asks for confirmation. Precedence: --yolo/--no-yolo,
|
|
412
|
+
// then OMNIRUSH_YOLO, then the saved setting. Default: off.
|
|
413
|
+
|
|
414
|
+
export const YOLO_BANNER = "yolo mode: no prompts, project trusted";
|
|
415
|
+
|
|
416
|
+
const TRUE_WORDS = new Set(["1", "true", "on", "yes"]);
|
|
417
|
+
const FALSE_WORDS = new Set(["0", "false", "off", "no"]);
|
|
418
|
+
|
|
419
|
+
/** OMNIRUSH_YOLO as true/false, or undefined when unset or unrecognised. */
|
|
420
|
+
export function parseYoloEnv(value) {
|
|
421
|
+
const v = String(value ?? "").trim().toLowerCase();
|
|
422
|
+
if (TRUE_WORDS.has(v)) return true;
|
|
423
|
+
if (FALSE_WORDS.has(v)) return false;
|
|
424
|
+
return undefined;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Resolve yolo mode for one launch. Strips --yolo/--no-yolo from `args`
|
|
429
|
+
* (never past `--`) and, when yolo is on for an agent run, adds the core's
|
|
430
|
+
* --approve unless the user already chose -a/--approve/-na/--no-approve.
|
|
431
|
+
* Returns { yolo, source, trusted, args }: `trusted` is false only when an
|
|
432
|
+
* explicit --no-approve wins over yolo's project trust.
|
|
433
|
+
*/
|
|
434
|
+
export function resolveYolo(args, { env = process.env, setting } = {}) {
|
|
435
|
+
let flag;
|
|
436
|
+
const rest = [];
|
|
437
|
+
let passthrough = false;
|
|
438
|
+
for (const arg of args) {
|
|
439
|
+
if (!passthrough && arg === "--") passthrough = true;
|
|
440
|
+
if (!passthrough && arg === "--yolo") flag = true;
|
|
441
|
+
else if (!passthrough && arg === "--no-yolo") flag = false;
|
|
442
|
+
else rest.push(arg);
|
|
443
|
+
}
|
|
444
|
+
let yolo = false;
|
|
445
|
+
let source = "default";
|
|
446
|
+
const fromEnv = parseYoloEnv(env.OMNIRUSH_YOLO);
|
|
447
|
+
if (flag !== undefined) {
|
|
448
|
+
yolo = flag;
|
|
449
|
+
source = "flag";
|
|
450
|
+
} else if (fromEnv !== undefined) {
|
|
451
|
+
yolo = fromEnv;
|
|
452
|
+
source = "env";
|
|
453
|
+
} else if (setting === true) {
|
|
454
|
+
yolo = true;
|
|
455
|
+
source = "setting";
|
|
456
|
+
}
|
|
457
|
+
const subcommand = rest.length > 0 && PI_SUBCOMMANDS.has(rest[0]);
|
|
458
|
+
if (!yolo || subcommand) return { yolo: yolo && !subcommand, source, trusted: !subcommand && yolo, args: rest };
|
|
459
|
+
const end = rest.indexOf("--");
|
|
460
|
+
const scan = end < 0 ? rest : rest.slice(0, end);
|
|
461
|
+
if (scan.some((a) => a === "--no-approve" || a === "-na")) return { yolo, source, trusted: false, args: rest };
|
|
462
|
+
if (scan.some((a) => a === "--approve" || a === "-a")) return { yolo, source, trusted: true, args: rest };
|
|
463
|
+
return { yolo, source, trusted: true, args: ["--approve", ...rest] };
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/** The one-line banner for a resolved yolo launch. */
|
|
467
|
+
export function yoloBanner({ trusted }) {
|
|
468
|
+
return trusted ? YOLO_BANNER : "yolo mode: no prompts (project not trusted: --no-approve)";
|
|
469
|
+
}
|
|
@@ -1,251 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Rebrand the vendored agent core (@earendil-works/pi-coding-agent) so
|
|
3
|
-
// users see "omnirush", never "pi"/"Pi". Runs on postinstall (before the
|
|
4
|
-
// bun download) and is safe to re-run (idempotent).
|
|
5
|
-
//
|
|
6
|
-
// Two mechanisms:
|
|
7
|
-
// 1. package.json `piConfig.name = "omnirush"` — pi's supported rebrand
|
|
8
|
-
// hook. APP_NAME/APP_TITLE (banner logo "omnirush v0.86.1", terminal
|
|
9
|
-
// title, --help usage lines, auth hints, debug-log name, the
|
|
10
|
-
// <APP>_CODING_AGENT_DIR env var) all derive from it, in dist and in
|
|
11
|
-
// the runtime bundle. It also disables pi's official-distribution
|
|
12
|
-
// first-time-setup wizard (upstream treats forks/rebrands that way).
|
|
13
|
-
// 2. Literal string replacements across the installed dist/*.js (the
|
|
14
|
-
// standalone files and the runtime bundle chunks) for the handful of
|
|
15
|
-
// user-visible strings that do not go through APP_NAME.
|
|
16
|
-
//
|
|
17
|
-
// Never renamed: file paths, config/session directory names, function or
|
|
18
|
-
// variable identifiers, env-var names (PI_OFFLINE, PI_TELEMETRY, ... are
|
|
19
|
-
// the core's real env contract), or the `pi` admin-subcommand aliases —
|
|
20
|
-
// only display strings.
|
|
21
|
-
//
|
|
22
|
-
// Fail-soft: a core update that moved a string logs a warning; it never
|
|
23
|
-
// breaks `npm install`.
|
|
24
|
-
|
|
25
|
-
import fs from "node:fs";
|
|
26
|
-
import path from "node:path";
|
|
27
|
-
import { fileURLToPath } from "node:url";
|
|
28
|
-
|
|
29
|
-
const PKG = "@earendil-works/pi-coding-agent";
|
|
30
|
-
const BRAND = "omnirush";
|
|
31
|
-
|
|
32
|
-
// Display strings that do not flow through APP_NAME. `from` must match the
|
|
33
|
-
// installed dist exactly; applied with replaceAll to every dist/**/*.js.
|
|
34
|
-
export const REPLACEMENTS = [
|
|
35
|
-
{
|
|
36
|
-
label: "TUI banner onboarding line",
|
|
37
|
-
from: "Pi can explain its own features and look up its docs. Ask it how to use or extend Pi.",
|
|
38
|
-
to: "Omnirush can explain its own features and look up its docs. Ask it how to use or extend Omnirush.",
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
label: "system prompt persona",
|
|
42
|
-
from: "operating inside pi, a coding agent harness",
|
|
43
|
-
to: "operating inside omnirush, a coding agent harness",
|
|
44
|
-
},
|
|
45
|
-
{
|
|
46
|
-
label: "system prompt docs section header",
|
|
47
|
-
from:
|
|
48
|
-
"Pi documentation (read only when the user asks about pi itself, its SDK, extensions, themes, skills, or TUI):",
|
|
49
|
-
to: "Omnirush documentation (read only when the user asks about the agent itself, its SDK, extensions, themes, skills, or TUI):",
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
label: "system prompt docs reading rule",
|
|
53
|
-
from: "When reading pi docs or examples",
|
|
54
|
-
to: "When reading the bundled docs or examples",
|
|
55
|
-
},
|
|
56
|
-
{
|
|
57
|
-
label: "system prompt packages doc reference",
|
|
58
|
-
from: "pi packages (docs/packages.md)",
|
|
59
|
-
to: "agent packages (docs/packages.md)",
|
|
60
|
-
},
|
|
61
|
-
{
|
|
62
|
-
label: "system prompt topics rule",
|
|
63
|
-
from: "When working on pi topics, read the docs and examples",
|
|
64
|
-
to: "When working on agent topics, read the docs and examples",
|
|
65
|
-
},
|
|
66
|
-
{
|
|
67
|
-
label: "system prompt read-fully rule",
|
|
68
|
-
from: "Always read pi .md files completely",
|
|
69
|
-
to: "Always read the agent .md files completely",
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
label: "bug report persona",
|
|
73
|
-
from: "about pi, the coding agent they are talking to",
|
|
74
|
-
to: "about omnirush, the coding agent they are talking to",
|
|
75
|
-
},
|
|
76
|
-
{
|
|
77
|
-
label: "bug report recipient",
|
|
78
|
-
from: "Write a report for the pi developers",
|
|
79
|
-
to: "Write a report for the omnirush developers",
|
|
80
|
-
},
|
|
81
|
-
{
|
|
82
|
-
label: "--help update line",
|
|
83
|
-
from: "Update pi, extensions, or model catalogs",
|
|
84
|
-
to: "Update the agent, extensions, or model catalogs",
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
label: "--help update alias token",
|
|
88
|
-
from: "[source|self|pi]",
|
|
89
|
-
to: "[source|self]",
|
|
90
|
-
},
|
|
91
|
-
{
|
|
92
|
-
label: "update subcommand description",
|
|
93
|
-
from: "Update pi only (self works as alias to pi)",
|
|
94
|
-
to: "Update the agent only (self works as an alias)",
|
|
95
|
-
},
|
|
96
|
-
// Belt-and-braces against the pi.dev update banner (Foxtran's 0.73.1
|
|
97
|
-
// report: a stale core showed "Update Available — pi.dev/changelog"
|
|
98
|
-
// even though PI_OFFLINE should suppress the check). PI_OFFLINE=1 in
|
|
99
|
-
// the spawn env stops the check; these rewrites make the banner
|
|
100
|
-
// UNRENDERABLE even where PI_OFFLINE is absent (direct pi spawns,
|
|
101
|
-
// older cores): showNewVersionNotification returns before its body
|
|
102
|
-
// (the parameter is renamed so the replacement stays idempotent and
|
|
103
|
-
// never re-matches), and the pi.dev changelog URL is blanked. The
|
|
104
|
-
// parameter spelling differs between the standalone dist (readable)
|
|
105
|
-
// and the minified runtime bundle chunk, hence the two variants; a
|
|
106
|
-
// future core that renames the parameter just reports "missing"
|
|
107
|
-
// (fail-soft) and we would re-derive the target.
|
|
108
|
-
{
|
|
109
|
-
label: "TUI update banner (standalone dist)",
|
|
110
|
-
from: "showNewVersionNotification(release) {",
|
|
111
|
-
to: "showNewVersionNotification(_release) { return;",
|
|
112
|
-
},
|
|
113
|
-
{
|
|
114
|
-
label: "TUI update banner (runtime bundle chunk)",
|
|
115
|
-
from: "showNewVersionNotification(release2){",
|
|
116
|
-
to: "showNewVersionNotification(_r){return;",
|
|
117
|
-
},
|
|
118
|
-
{
|
|
119
|
-
label: "pi.dev changelog URL",
|
|
120
|
-
from: '"https://pi.dev/changelog"',
|
|
121
|
-
to: '""',
|
|
122
|
-
},
|
|
123
|
-
];
|
|
124
|
-
|
|
125
|
-
function findPiRoot(startDir) {
|
|
126
|
-
let dir = startDir;
|
|
127
|
-
for (let i = 0; i < 8; i++) {
|
|
128
|
-
const candidate = path.join(dir, "node_modules", PKG);
|
|
129
|
-
if (fs.existsSync(path.join(candidate, "package.json"))) return candidate;
|
|
130
|
-
const parent = path.dirname(dir);
|
|
131
|
-
if (parent === dir) return null;
|
|
132
|
-
dir = parent;
|
|
133
|
-
}
|
|
134
|
-
return null;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function listDistFiles(root) {
|
|
138
|
-
const out = [];
|
|
139
|
-
const stack = [root];
|
|
140
|
-
while (stack.length > 0) {
|
|
141
|
-
const dir = stack.pop();
|
|
142
|
-
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
143
|
-
const full = path.join(dir, entry.name);
|
|
144
|
-
if (entry.isDirectory()) stack.push(full);
|
|
145
|
-
else if (entry.isFile() && entry.name.endsWith(".js")) out.push(full);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
return out;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Apply the rebrand to the installed core. Returns a report:
|
|
153
|
-
* { piRoot, packageJsonPatched, applied: [{label, files}], missing: [{label, reason}] }
|
|
154
|
-
*/
|
|
155
|
-
export function applyBrandingPatches(startDir) {
|
|
156
|
-
const piRoot = findPiRoot(startDir);
|
|
157
|
-
if (!piRoot) {
|
|
158
|
-
return { piRoot: null, packageJsonPatched: false, applied: [], missing: [{ label: "core", reason: `${PKG} not installed` }] };
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// 1. The supported rebrand hook.
|
|
162
|
-
let packageJsonPatched = false;
|
|
163
|
-
const pkgPath = path.join(piRoot, "package.json");
|
|
164
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
165
|
-
if (pkg.piConfig?.name !== BRAND) {
|
|
166
|
-
pkg.piConfig = { ...(pkg.piConfig || {}), name: BRAND };
|
|
167
|
-
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
168
|
-
packageJsonPatched = true;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// 2. Literal display strings, everywhere under dist/.
|
|
172
|
-
const applied = [];
|
|
173
|
-
const missing = [];
|
|
174
|
-
const files = listDistFiles(path.join(piRoot, "dist"));
|
|
175
|
-
for (const replacement of REPLACEMENTS) {
|
|
176
|
-
let hits = 0;
|
|
177
|
-
for (const file of files) {
|
|
178
|
-
let content;
|
|
179
|
-
try {
|
|
180
|
-
content = fs.readFileSync(file, "utf8");
|
|
181
|
-
} catch {
|
|
182
|
-
continue;
|
|
183
|
-
}
|
|
184
|
-
if (!content.includes(replacement.from)) continue;
|
|
185
|
-
fs.writeFileSync(file, content.replaceAll(replacement.from, replacement.to));
|
|
186
|
-
hits++;
|
|
187
|
-
}
|
|
188
|
-
if (hits > 0) applied.push({ label: replacement.label, files: hits });
|
|
189
|
-
else missing.push({ label: replacement.label, reason: "target string not found (already applied or core updated)" });
|
|
190
|
-
}
|
|
191
|
-
return { piRoot, packageJsonPatched, applied, missing };
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function main() {
|
|
195
|
-
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
196
|
-
const report = applyBrandingPatches(repoRoot);
|
|
197
|
-
if (!report.piRoot) {
|
|
198
|
-
console.log("omnirush: agent core not installed; skipped branding patch");
|
|
199
|
-
return;
|
|
200
|
-
// Version alignment: pi's self-reported version (banner/help) reads its
|
|
201
|
-
// own package.json — rewrite it to the omnirush release version so the
|
|
202
|
-
// product version, not upstream's, is what users and support see.
|
|
203
|
-
try {
|
|
204
|
-
const piPkgPath = path.join(report.piRoot, "package.json");
|
|
205
|
-
const piPkg = JSON.parse(fs.readFileSync(piPkgPath, "utf8"));
|
|
206
|
-
const ourVersion = JSON.parse(
|
|
207
|
-
fs.readFileSync(path.join(repoRoot, "package.json"), "utf8"),
|
|
208
|
-
).version;
|
|
209
|
-
if (piPkg.version !== ourVersion) {
|
|
210
|
-
piPkg.version = ourVersion;
|
|
211
|
-
fs.writeFileSync(piPkgPath, JSON.stringify(piPkg, null, 2) + "\n");
|
|
212
|
-
report.versionAligned = ourVersion;
|
|
213
|
-
}
|
|
214
|
-
} catch {
|
|
215
|
-
/* cosmetic — never fail the patch over version alignment */
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
// Version alignment: pi's self-reported version (banner/help) reads its
|
|
219
|
-
// own package.json — rewrite it to the omnirush release version so the
|
|
220
|
-
// product version, not upstream's, is what users and support see.
|
|
221
|
-
try {
|
|
222
|
-
const piPkgPath = path.join(report.piRoot, "package.json");
|
|
223
|
-
const piPkg = JSON.parse(fs.readFileSync(piPkgPath, "utf8"));
|
|
224
|
-
const ourVersion = JSON.parse(
|
|
225
|
-
fs.readFileSync(path.join(repoRoot, "package.json"), "utf8"),
|
|
226
|
-
).version;
|
|
227
|
-
if (piPkg.version !== ourVersion) {
|
|
228
|
-
piPkg.version = ourVersion;
|
|
229
|
-
fs.writeFileSync(piPkgPath, JSON.stringify(piPkg, null, 2) + "\n");
|
|
230
|
-
report.versionAligned = ourVersion;
|
|
231
|
-
}
|
|
232
|
-
} catch {
|
|
233
|
-
/* cosmetic — never fail the patch over version alignment */
|
|
234
|
-
}
|
|
235
|
-
const patched = report.packageJsonPatched ? "package.json + " : "";
|
|
236
|
-
const ver = report.versionAligned ? `; version aligned to ${report.versionAligned}` : "";
|
|
237
|
-
console.log(
|
|
238
|
-
`omnirush: branding applied (${patched}${report.applied.length} display strings)${ver}` +
|
|
239
|
-
(report.missing.length > 0 ? `; ${report.missing.length} already-applied/absent` : ""),
|
|
240
|
-
);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// CLI entry (postinstall). Imported as a module by tests — never exit there.
|
|
244
|
-
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
245
|
-
try {
|
|
246
|
-
main();
|
|
247
|
-
} catch (error) {
|
|
248
|
-
// Fail-soft: a cosmetic patch must never break `npm install`.
|
|
249
|
-
console.warn("omnirush: branding patch skipped:", error?.message ?? error);
|
|
250
|
-
}
|
|
251
|
-
}
|