omnirush 0.8.6 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/CHANGELOG.md +120 -0
- package/assets/extensions/omnirush/agents-lib.ts +501 -63
- package/assets/extensions/omnirush/agents.ts +140 -115
- package/assets/extensions/omnirush/bgshell-lib.ts +432 -0
- package/assets/extensions/omnirush/bgshell.ts +392 -0
- package/assets/extensions/omnirush/capture/workspace-collector.ts +86 -22
- package/assets/extensions/omnirush/collector.ts +275 -43
- package/assets/extensions/omnirush/commands.ts +2 -0
- 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 +32 -6
- package/assets/extensions/omnirush/mcp.ts +103 -8
- package/assets/extensions/omnirush/memory-lib.ts +516 -0
- package/assets/extensions/omnirush/pi-engine.ts +115 -3
- package/assets/extensions/omnirush/plan-lib.ts +38 -9
- package/assets/extensions/omnirush/plan.ts +85 -30
- package/assets/extensions/omnirush/sota.ts +52 -4
- package/assets/extensions/omnirush/status-lib.ts +3 -0
- package/assets/extensions/omnirush/subagent-marker.ts +21 -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 +7 -3
- package/scripts/brand-engine.js +526 -0
- package/scripts/build-all-packages.py +29 -1
- package/scripts/smoke-packages.py +33 -1
- package/src/bin.js +232 -33
- package/src/compat.js +272 -0
- package/src/lib.js +64 -0
- package/src/sessions.js +222 -0
- package/scripts/patch-pi-branding.js +0 -251
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
|
+
}
|
package/src/sessions.js
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
// Sub-agent sessions out of the user's resume list.
|
|
2
|
+
//
|
|
3
|
+
// spawn_agents children are headless agent runs with session files of
|
|
4
|
+
// their own. Up to 0.9.0 they were written next to the user's sessions
|
|
5
|
+
// (<agentDir>/sessions/<cwd-slug>/), so the core's session picker
|
|
6
|
+
// (/resume) listed every sub-agent run and --continue picked whichever
|
|
7
|
+
// session file changed last — often a background sub-agent's rather than
|
|
8
|
+
// the user's conversation. Children now go to <session dir>/subagents/
|
|
9
|
+
// (agents-lib.ts subagentSessionDir), which the core never lists.
|
|
10
|
+
//
|
|
11
|
+
// moveLegacySubagentSessions puts the children older releases left in a
|
|
12
|
+
// session dir into that folder too (moved, never deleted: the collector
|
|
13
|
+
// still finds them there when a resumed session records its sub-agents).
|
|
14
|
+
// It runs at every agent launch before the core picks a session, and
|
|
15
|
+
// stays cheap on large folders: a marker per session dir records when it
|
|
16
|
+
// was last sorted, and only files changed since then are opened (their
|
|
17
|
+
// first lines only). Files changed in the last minute are left for the
|
|
18
|
+
// next launch, in case an older release is still writing them.
|
|
19
|
+
//
|
|
20
|
+
// A child is recognised from the top of its session file, by the same
|
|
21
|
+
// signals the collector uses (pi-engine.ts subagentTask): the marker entry
|
|
22
|
+
// a sub-agent writes into its own session before its task
|
|
23
|
+
// (SUBAGENT_ENTRY_TYPE), or — sessions of releases that wrote no marker —
|
|
24
|
+
// a first user message "Task: <task>" (SUBAGENT_TASK_PREFIX), confirmed by
|
|
25
|
+
// the role prompt every sub-agent has run with since 0.5.4 (appended to
|
|
26
|
+
// the system prompt: "You are a <role> subagent ..."), which the file
|
|
27
|
+
// itself carries, so no other session has to be read.
|
|
28
|
+
|
|
29
|
+
import fs from "node:fs";
|
|
30
|
+
import path from "node:path";
|
|
31
|
+
|
|
32
|
+
/** Same name as agents-lib.ts SUBAGENT_SESSIONS_DIR. */
|
|
33
|
+
export const SUBAGENT_SESSIONS_DIR = "subagents";
|
|
34
|
+
/** Same values as pi-engine.ts SUBAGENT_ENTRY_TYPE / SUBAGENT_TASK_PREFIX. */
|
|
35
|
+
export const SUBAGENT_ENTRY_TYPE = "omnirush-subagent";
|
|
36
|
+
export const SUBAGENT_TASK_PREFIX = "Task: ";
|
|
37
|
+
/** Per session dir: when it was last sorted (a .json file: the core only lists *.jsonl). */
|
|
38
|
+
export const SORT_MARKER = ".subagents-sorted.json";
|
|
39
|
+
/** Files changed more recently than this are left for the next launch. */
|
|
40
|
+
export const SETTLE_MS = 60_000;
|
|
41
|
+
/** How much of a session file is read looking for its first user message. */
|
|
42
|
+
const HEAD_LIMIT = 1024 * 1024;
|
|
43
|
+
const CHUNK = 64 * 1024;
|
|
44
|
+
|
|
45
|
+
const ROLE_PROMPT = /\bYou are an? [\w-]+ subagent\b/;
|
|
46
|
+
|
|
47
|
+
function textOf(content) {
|
|
48
|
+
if (typeof content === "string") return content;
|
|
49
|
+
if (!Array.isArray(content)) return "";
|
|
50
|
+
return content.map((part) => (part && typeof part.text === "string" ? part.text : "")).join("");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Whether a session file is a sub-agent run: a sub-agent marker entry
|
|
55
|
+
* before its first user message, or a first user message "Task: ..."
|
|
56
|
+
* after a system prompt with a sub-agent role. Reads the file only up to
|
|
57
|
+
* that first user message.
|
|
58
|
+
*/
|
|
59
|
+
export function isSubagentSession(file, fsImpl = fs) {
|
|
60
|
+
let fd;
|
|
61
|
+
try {
|
|
62
|
+
fd = fsImpl.openSync(file, "r");
|
|
63
|
+
} catch {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
const buffer = Buffer.alloc(CHUNK);
|
|
68
|
+
let pending = "";
|
|
69
|
+
let read = 0;
|
|
70
|
+
let header = null;
|
|
71
|
+
let rolePrompt = false;
|
|
72
|
+
while (read < HEAD_LIMIT) {
|
|
73
|
+
const bytes = fsImpl.readSync(fd, buffer, 0, CHUNK, read);
|
|
74
|
+
if (bytes <= 0) break;
|
|
75
|
+
read += bytes;
|
|
76
|
+
pending += buffer.toString("utf8", 0, bytes);
|
|
77
|
+
let newline;
|
|
78
|
+
while ((newline = pending.indexOf("\n")) !== -1) {
|
|
79
|
+
const line = pending.slice(0, newline);
|
|
80
|
+
pending = pending.slice(newline + 1);
|
|
81
|
+
if (!line.trim()) continue;
|
|
82
|
+
let entry;
|
|
83
|
+
try {
|
|
84
|
+
entry = JSON.parse(line);
|
|
85
|
+
} catch {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
if (!header) {
|
|
89
|
+
// A fork (parentSession) is the user's own branch of a session.
|
|
90
|
+
if (entry?.type !== "session" || entry.parentSession) return false;
|
|
91
|
+
header = entry;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (entry?.type === "custom" && entry.customType === SUBAGENT_ENTRY_TYPE) return true;
|
|
95
|
+
if (entry?.type !== "message") continue;
|
|
96
|
+
const message = entry.message;
|
|
97
|
+
if (message?.role === "system") {
|
|
98
|
+
const addendum = message.sections?.addendum;
|
|
99
|
+
if (typeof addendum === "string" && ROLE_PROMPT.test(addendum)) rolePrompt = true;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (message?.role === "user") return rolePrompt && textOf(message.content).startsWith(SUBAGENT_TASK_PREFIX);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return false;
|
|
106
|
+
} catch {
|
|
107
|
+
return false;
|
|
108
|
+
} finally {
|
|
109
|
+
try {
|
|
110
|
+
fsImpl.closeSync(fd);
|
|
111
|
+
} catch {
|
|
112
|
+
/* closed */
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function readMarker(dir, fsImpl) {
|
|
118
|
+
try {
|
|
119
|
+
const value = JSON.parse(fsImpl.readFileSync(path.join(dir, SORT_MARKER), "utf8"));
|
|
120
|
+
return Number.isFinite(value?.sortedUpTo) ? value.sortedUpTo : 0;
|
|
121
|
+
} catch {
|
|
122
|
+
return 0;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Move the sub-agent sessions older releases left in `dir` into
|
|
128
|
+
* dir/subagents/. Returns how many were moved. Never throws.
|
|
129
|
+
*/
|
|
130
|
+
export function sortSessionDir(dir, { fsImpl = fs, now = Date.now } = {}) {
|
|
131
|
+
let moved = 0;
|
|
132
|
+
try {
|
|
133
|
+
const since = readMarker(dir, fsImpl);
|
|
134
|
+
const cutoff = now() - SETTLE_MS;
|
|
135
|
+
let names;
|
|
136
|
+
try {
|
|
137
|
+
names = fsImpl.readdirSync(dir);
|
|
138
|
+
} catch {
|
|
139
|
+
return 0;
|
|
140
|
+
}
|
|
141
|
+
let sawFiles = false;
|
|
142
|
+
for (const name of names) {
|
|
143
|
+
if (!name.endsWith(".jsonl")) continue;
|
|
144
|
+
sawFiles = true;
|
|
145
|
+
const file = path.join(dir, name);
|
|
146
|
+
let stats;
|
|
147
|
+
try {
|
|
148
|
+
stats = fsImpl.statSync(file);
|
|
149
|
+
} catch {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (!stats.isFile()) continue;
|
|
153
|
+
// Sorted already (a user session): only changes since are looked at.
|
|
154
|
+
if (stats.mtimeMs < since) continue;
|
|
155
|
+
// Maybe still being written by an older release: next launch.
|
|
156
|
+
if (stats.mtimeMs >= cutoff) continue;
|
|
157
|
+
if (!isSubagentSession(file, fsImpl)) continue;
|
|
158
|
+
const target = path.join(dir, SUBAGENT_SESSIONS_DIR);
|
|
159
|
+
try {
|
|
160
|
+
fsImpl.mkdirSync(target, { recursive: true });
|
|
161
|
+
const dest = path.join(target, name);
|
|
162
|
+
if (fsImpl.existsSync(dest)) continue;
|
|
163
|
+
fsImpl.renameSync(file, dest);
|
|
164
|
+
moved++;
|
|
165
|
+
} catch {
|
|
166
|
+
/* left where it is; tried again next launch */
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (sawFiles) {
|
|
170
|
+
const tmp = path.join(dir, `${SORT_MARKER}.${process.pid}.tmp`);
|
|
171
|
+
fsImpl.writeFileSync(tmp, JSON.stringify({ version: 1, sortedUpTo: cutoff }) + "\n");
|
|
172
|
+
fsImpl.renameSync(tmp, path.join(dir, SORT_MARKER));
|
|
173
|
+
}
|
|
174
|
+
} catch {
|
|
175
|
+
/* best effort: never in the way of a launch */
|
|
176
|
+
}
|
|
177
|
+
return moved;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Sort every project's session dir under <agentDir>/sessions (the picker's
|
|
182
|
+
* "all" scope lists them all) plus any custom session dir in use.
|
|
183
|
+
*/
|
|
184
|
+
export function moveLegacySubagentSessions({ agentDir, extraDirs = [], fsImpl = fs, now = Date.now } = {}) {
|
|
185
|
+
const dirs = [];
|
|
186
|
+
const root = path.join(agentDir, "sessions");
|
|
187
|
+
try {
|
|
188
|
+
for (const entry of fsImpl.readdirSync(root, { withFileTypes: true })) {
|
|
189
|
+
if (entry.isDirectory()) dirs.push(path.join(root, entry.name));
|
|
190
|
+
}
|
|
191
|
+
} catch {
|
|
192
|
+
/* no sessions yet */
|
|
193
|
+
}
|
|
194
|
+
for (const dir of extraDirs) {
|
|
195
|
+
if (typeof dir === "string" && dir && !dirs.includes(path.resolve(dir))) dirs.push(path.resolve(dir));
|
|
196
|
+
}
|
|
197
|
+
let moved = 0;
|
|
198
|
+
for (const dir of dirs) moved += sortSessionDir(dir, { fsImpl, now });
|
|
199
|
+
return moved;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* The custom session dir an agent launch uses, if any: --session-dir,
|
|
204
|
+
* else OMNIRUSH_/PI_CODING_AGENT_SESSION_DIR, else the "sessionDir"
|
|
205
|
+
* setting (as the core resolves it).
|
|
206
|
+
*/
|
|
207
|
+
export function customSessionDir(args, env, settingsFile, home, fsImpl = fs) {
|
|
208
|
+
const expand = (value) => (value === "~" ? home : value.startsWith("~/") ? path.join(home, value.slice(2)) : value);
|
|
209
|
+
for (let i = 0; i < args.length; i++) {
|
|
210
|
+
if (args[i] === "--session-dir" && args[i + 1]) return expand(args[i + 1]);
|
|
211
|
+
if (args[i].startsWith("--session-dir=")) return expand(args[i].slice("--session-dir=".length));
|
|
212
|
+
}
|
|
213
|
+
const fromEnv = env.OMNIRUSH_CODING_AGENT_SESSION_DIR || env.PI_CODING_AGENT_SESSION_DIR;
|
|
214
|
+
if (fromEnv) return expand(fromEnv);
|
|
215
|
+
try {
|
|
216
|
+
const value = JSON.parse(fsImpl.readFileSync(settingsFile, "utf8"))?.sessionDir;
|
|
217
|
+
if (typeof value === "string" && value.trim()) return expand(value.trim());
|
|
218
|
+
} catch {
|
|
219
|
+
/* no settings */
|
|
220
|
+
}
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
@@ -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
|
-
}
|