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
|
@@ -14,9 +14,12 @@ import path from "node:path";
|
|
|
14
14
|
import { gatewayUrlForOrigin, omniDir, resolveOrigin } from "./auth";
|
|
15
15
|
import { WorkspaceCollector } from "./collector-lib";
|
|
16
16
|
import { sharedRefresher } from "./refresh";
|
|
17
|
-
import { recordGatewayUsage } from "./usage";
|
|
17
|
+
import { recordGatewayUsage, storedUsageRecord } from "./usage";
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
// End snapshots are full-tree (reproducible environment) and chunked, so
|
|
20
|
+
// shutdown can legitimately take minutes on big workspaces. Bounded so a
|
|
21
|
+
// wedged upload still can't wedge exit.
|
|
22
|
+
const SHUTDOWN_UPLOAD_BUDGET_MS = 120_000;
|
|
20
23
|
|
|
21
24
|
/** pi session ids are UUIDs; still, never let a foreign format stall us. */
|
|
22
25
|
function sanitizeSessionId(raw: string): string {
|
|
@@ -81,7 +84,8 @@ export default function (pi: any) {
|
|
|
81
84
|
const usage = recordGatewayUsage(event?.headers);
|
|
82
85
|
collector.recordTrace(sessionId, "provider.response", {
|
|
83
86
|
status: event?.status,
|
|
84
|
-
|
|
87
|
+
// Stored records stay brand-neutral (the traces are the product).
|
|
88
|
+
...(usage ? { agent: storedUsageRecord(usage) } : {}),
|
|
85
89
|
});
|
|
86
90
|
});
|
|
87
91
|
|
|
@@ -1,22 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Omnirush commands: /grant (
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Omnirush commands: /grant (today's token grant + usage) and /verify
|
|
3
|
+
* (prove the served model is genuine — full implementation lands with the
|
|
4
|
+
* gateway's model-attestation endpoint), plus the footer status line that
|
|
5
|
+
* keeps grant/model info visible while the agent runs.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
8
|
-
import {
|
|
9
|
+
import { deviceMe, resolveOrigin } from "./auth";
|
|
10
|
+
import { formatGrantLines, formatGrantStatusLine, gatewayUsage } from "./usage";
|
|
11
|
+
|
|
12
|
+
/** Fetch the signed-in identity + server-side usage, best effort. */
|
|
13
|
+
async function fetchIdentity(): Promise<any> {
|
|
14
|
+
const token = (process.env.OMNIRUSH_TOKEN || "").trim();
|
|
15
|
+
if (!token) return null;
|
|
16
|
+
try {
|
|
17
|
+
return await deviceMe(resolveOrigin(process.env), {
|
|
18
|
+
accessToken: token,
|
|
19
|
+
fetchImpl: globalThis.fetch,
|
|
20
|
+
});
|
|
21
|
+
} catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
9
25
|
|
|
10
26
|
export default function (pi: ExtensionAPI) {
|
|
11
27
|
pi.registerCommand("grant", {
|
|
12
28
|
description: "Show today's Omnirush token grant and usage",
|
|
13
29
|
handler: async (ctx: any) => {
|
|
14
30
|
const usage = gatewayUsage();
|
|
31
|
+
const me = await fetchIdentity();
|
|
32
|
+
const sessionId = ctx?.sessionManager?.getSessionId?.() ?? undefined;
|
|
33
|
+
const lines = formatGrantLines({ usage, me, sessionId });
|
|
15
34
|
ctx.ui?.notify?.(
|
|
16
|
-
usage
|
|
17
|
-
? `Omnirush gateway usage (latest x-omnirush-* headers): ${JSON.stringify(usage)}`
|
|
18
|
-
: `Omnirush: no gateway usage seen yet. Provider: ${process.env.PI_PROVIDER ?? "omnirush"}, ` +
|
|
19
|
-
`model: ${process.env.PI_MODEL ?? "gpt-6-astra"}.`,
|
|
35
|
+
["Omnirush usage:", ...lines.map((line) => ` ${line}`)].join("\n"),
|
|
20
36
|
);
|
|
21
37
|
},
|
|
22
38
|
});
|
|
@@ -31,4 +47,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
31
47
|
);
|
|
32
48
|
},
|
|
33
49
|
});
|
|
50
|
+
|
|
51
|
+
// Footer status: grant remaining + served model, refreshed on every
|
|
52
|
+
// provider response (the sota tap records the headers into the shared
|
|
53
|
+
// usage store before this event fires) and on model switches.
|
|
54
|
+
const refreshStatus = (ctx: any) => {
|
|
55
|
+
const line = formatGrantStatusLine(gatewayUsage());
|
|
56
|
+
if (line) ctx?.ui?.setStatus?.("omnirush-grant", line);
|
|
57
|
+
};
|
|
58
|
+
pi.on("after_provider_response", (_event: any, ctx: any) => refreshStatus(ctx));
|
|
59
|
+
pi.on("model_select", (_event: any, ctx: any) => refreshStatus(ctx));
|
|
34
60
|
}
|
|
@@ -24,25 +24,39 @@ import {
|
|
|
24
24
|
formatSotaWarning,
|
|
25
25
|
servedModelFromSseData,
|
|
26
26
|
} from "./sota-lib";
|
|
27
|
+
import { recordGatewayUsage } from "./usage";
|
|
27
28
|
|
|
28
29
|
const PROVIDER_ID = "omnirush";
|
|
29
30
|
const GATEWAY_DEFAULT = "https://omnirush.ai/omnirush/v1";
|
|
30
31
|
const STRICT_EXIT_CODE = 3;
|
|
31
32
|
|
|
32
33
|
// Must mirror the models.json payload bin.js installs (src/lib.js
|
|
33
|
-
// modelsConfig) so provider composition keeps serving the same
|
|
34
|
-
// The `api` field is load-bearing: provider composition routes
|
|
35
|
-
// this base only when the model's api matches one of
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
34
|
+
// modelsConfig / MODELS) so provider composition keeps serving the same
|
|
35
|
+
// catalog. The `api` field is load-bearing: provider composition routes
|
|
36
|
+
// streams to this base only when the model's api matches one of
|
|
37
|
+
// base.getModels().
|
|
38
|
+
const CATALOG_MODELS = [
|
|
39
|
+
{
|
|
40
|
+
id: "gpt-6-astra",
|
|
41
|
+
name: "GPT-6 Astra",
|
|
42
|
+
api: "openai-responses",
|
|
43
|
+
reasoning: true,
|
|
44
|
+
input: ["text", "image"],
|
|
45
|
+
cost: { input: 10, output: 50, cacheRead: 2.5, cacheWrite: 12.5 },
|
|
46
|
+
contextWindow: 400000,
|
|
47
|
+
compat: { supportsMaxOutputTokens: false },
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: "gpt-5.6-sol",
|
|
51
|
+
name: "GPT-5.6 Sol",
|
|
52
|
+
api: "openai-responses",
|
|
53
|
+
reasoning: true,
|
|
54
|
+
input: ["text", "image"],
|
|
55
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
56
|
+
contextWindow: 400000,
|
|
57
|
+
compat: { supportsMaxOutputTokens: false },
|
|
58
|
+
},
|
|
59
|
+
];
|
|
46
60
|
|
|
47
61
|
function warnStderr(line: string): void {
|
|
48
62
|
try {
|
|
@@ -123,6 +137,14 @@ function tapFetch(
|
|
|
123
137
|
return async (input: any, init: any) => {
|
|
124
138
|
let response = await baseFetch(input, init);
|
|
125
139
|
debug(`provider fetch ${typeof input === "string" ? input : input?.url} -> ${response?.status}`);
|
|
140
|
+
// Capture the gateway's grant/usage headers (x-omnirush-* plus the
|
|
141
|
+
// x-ratelimit-*-tokens grant pair) straight from the raw response —
|
|
142
|
+
// guaranteed availability here, independent of pi's event plumbing.
|
|
143
|
+
try {
|
|
144
|
+
recordGatewayUsage(response?.headers);
|
|
145
|
+
} catch {
|
|
146
|
+
/* usage capture must never break the request */
|
|
147
|
+
}
|
|
126
148
|
if (response?.status === 401) {
|
|
127
149
|
const tokenUsed = bearerFromInit(init) || bearerFromInit(input);
|
|
128
150
|
debug(`401 seen; bearer present: ${Boolean(tokenUsed)}`);
|
|
@@ -214,7 +236,7 @@ export default function (pi: any) {
|
|
|
214
236
|
},
|
|
215
237
|
},
|
|
216
238
|
},
|
|
217
|
-
getModels: () =>
|
|
239
|
+
getModels: () => CATALOG_MODELS.map((model) => ({ ...model })),
|
|
218
240
|
stream: streamWith(false),
|
|
219
241
|
streamSimple: streamWith(true),
|
|
220
242
|
});
|
|
@@ -1,16 +1,54 @@
|
|
|
1
|
-
// Latest
|
|
2
|
-
// attaches to /v1 responses
|
|
3
|
-
// contains credentials.
|
|
1
|
+
// Latest gateway grant/usage state, captured from response headers the
|
|
2
|
+
// manager attaches to /v1 responses. Kept for /grant and the footer status
|
|
3
|
+
// line; never contains credentials.
|
|
4
|
+
//
|
|
5
|
+
// Live header contract (verified 2026-09-22 against the production
|
|
6
|
+
// gateway, omnirush/manager gateway.py):
|
|
7
|
+
// x-omnirush-model - resolved request model
|
|
8
|
+
// x-omnirush-effort - requested reasoning effort (set when present)
|
|
9
|
+
// x-ratelimit-limit-tokens - daily grant size
|
|
10
|
+
// x-ratelimit-remaining-tokens - grant remaining after settlement lag
|
|
4
11
|
|
|
5
|
-
|
|
12
|
+
export const GRANT_HEADER_NAMES = [
|
|
13
|
+
"x-ratelimit-limit-tokens",
|
|
14
|
+
"x-ratelimit-remaining-tokens",
|
|
15
|
+
];
|
|
6
16
|
|
|
7
|
-
|
|
8
|
-
if (!headers
|
|
17
|
+
function* headerPairs(headers: unknown): Generator<[string, string]> {
|
|
18
|
+
if (!headers) return;
|
|
19
|
+
if (typeof (headers as any).forEach === "function") {
|
|
20
|
+
// Fetch Headers instance (the sota tap sees the raw Response).
|
|
21
|
+
const pairs: [string, string][] = [];
|
|
22
|
+
(headers as Headers).forEach((value, name) => pairs.push([name, value]));
|
|
23
|
+
for (const pair of pairs) yield pair;
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (typeof headers === "object") {
|
|
27
|
+
for (const [name, value] of Object.entries(headers as Record<string, unknown>)) {
|
|
28
|
+
if (value !== undefined && value !== null) yield [name, String(value)];
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Extract the gateway usage headers from a Headers object or plain record. */
|
|
34
|
+
export function pickGatewayUsage(headers: unknown): Record<string, string> {
|
|
9
35
|
const found: Record<string, string> = {};
|
|
10
|
-
for (const [name, value] of
|
|
11
|
-
|
|
12
|
-
|
|
36
|
+
for (const [name, value] of headerPairs(headers)) {
|
|
37
|
+
const lower = name.toLowerCase();
|
|
38
|
+
if (
|
|
39
|
+
lower.startsWith("x-omnirush-") ||
|
|
40
|
+
GRANT_HEADER_NAMES.includes(lower)
|
|
41
|
+
) {
|
|
42
|
+
found[lower] = value;
|
|
43
|
+
}
|
|
13
44
|
}
|
|
45
|
+
return found;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let latest: Record<string, string> | null = null;
|
|
49
|
+
|
|
50
|
+
export function recordGatewayUsage(headers: unknown): Record<string, string> | null {
|
|
51
|
+
const found = pickGatewayUsage(headers);
|
|
14
52
|
const hasAny = Object.keys(found).length > 0;
|
|
15
53
|
latest = hasAny ? found : latest;
|
|
16
54
|
return hasAny ? found : null;
|
|
@@ -19,3 +57,113 @@ export function recordGatewayUsage(headers: unknown): Record<string, string> | n
|
|
|
19
57
|
export function gatewayUsage(): Record<string, string> | null {
|
|
20
58
|
return latest;
|
|
21
59
|
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Usage record as embedded into STORED trace events: gateway header
|
|
63
|
+
* names are normalized so the stored artifacts carry no brand strings —
|
|
64
|
+
* "x-omnirush-model" -> "model", "x-omnirush-effort" -> "effort";
|
|
65
|
+
* generic headers (x-ratelimit-*) keep their names.
|
|
66
|
+
*/
|
|
67
|
+
export function storedUsageRecord(usage: Record<string, string>): Record<string, string> {
|
|
68
|
+
const stored: Record<string, string> = {};
|
|
69
|
+
for (const [name, value] of Object.entries(usage)) {
|
|
70
|
+
const lower = name.toLowerCase();
|
|
71
|
+
const key = lower.startsWith("x-omnirush-") ? lower.slice("x-omnirush-".length) : lower;
|
|
72
|
+
stored[key] = value;
|
|
73
|
+
}
|
|
74
|
+
return stored;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Ints from the grant header pair, when both are present. */
|
|
78
|
+
export function grantFromUsage(usage: Record<string, string> | null): {
|
|
79
|
+
limitTokens: number;
|
|
80
|
+
remainingTokens: number;
|
|
81
|
+
} | null {
|
|
82
|
+
const limit = Number(usage?.["x-ratelimit-limit-tokens"]);
|
|
83
|
+
const remaining = Number(usage?.["x-ratelimit-remaining-tokens"]);
|
|
84
|
+
if (!Number.isFinite(limit) || !Number.isFinite(remaining)) return null;
|
|
85
|
+
return { limitTokens: limit, remainingTokens: remaining };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Compact human token count: 1.0B / 997.2M / 12.3k / 972. */
|
|
89
|
+
export function formatTokenCount(value: number): string {
|
|
90
|
+
if (!Number.isFinite(value)) return "?";
|
|
91
|
+
const abs = Math.abs(value);
|
|
92
|
+
if (abs >= 1e9) return `${(value / 1e9).toFixed(1)}B`;
|
|
93
|
+
if (abs >= 1e6) return `${(value / 1e6).toFixed(1)}M`;
|
|
94
|
+
if (abs >= 1e3) return `${(value / 1e3).toFixed(1)}k`;
|
|
95
|
+
return String(Math.round(value));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function intOr(value: unknown): number | null {
|
|
99
|
+
const n = Number(value);
|
|
100
|
+
return Number.isFinite(n) ? n : null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The lines /grant shows. Everything is best-effort: header data when the
|
|
105
|
+
* agent has made a request, /device/me identity+usage otherwise.
|
|
106
|
+
*/
|
|
107
|
+
export function formatGrantLines({
|
|
108
|
+
usage,
|
|
109
|
+
me,
|
|
110
|
+
sessionId,
|
|
111
|
+
}: {
|
|
112
|
+
usage: Record<string, string> | null;
|
|
113
|
+
me?: any;
|
|
114
|
+
sessionId?: string;
|
|
115
|
+
}): string[] {
|
|
116
|
+
const lines: string[] = [];
|
|
117
|
+
const model =
|
|
118
|
+
usage?.["x-omnirush-model"] ?? me?.current_model ?? null;
|
|
119
|
+
const effort = usage?.["x-omnirush-effort"] ?? null;
|
|
120
|
+
lines.push(`model (last turn): ${model ?? "no request yet"}`);
|
|
121
|
+
if (effort) lines.push(`reasoning effort: ${effort}`);
|
|
122
|
+
|
|
123
|
+
const fromHeaders = grantFromUsage(usage);
|
|
124
|
+
const meUsage = me?.usage ?? null;
|
|
125
|
+
const limit =
|
|
126
|
+
fromHeaders?.limitTokens ?? intOr(meUsage?.token_limit) ?? null;
|
|
127
|
+
const remaining =
|
|
128
|
+
fromHeaders?.remainingTokens ?? intOr(meUsage?.remaining_tokens) ?? null;
|
|
129
|
+
const usedFromMe = intOr(meUsage?.used_tokens);
|
|
130
|
+
const used =
|
|
131
|
+
usedFromMe ??
|
|
132
|
+
(limit !== null && remaining !== null ? limit - remaining : null);
|
|
133
|
+
|
|
134
|
+
if (limit !== null && remaining !== null) {
|
|
135
|
+
lines.push(
|
|
136
|
+
`grant: ${formatTokenCount(remaining)} of ${formatTokenCount(limit)} tokens remaining` +
|
|
137
|
+
(used !== null ? ` (used ${formatTokenCount(used)})` : ""),
|
|
138
|
+
);
|
|
139
|
+
} else if (used !== null) {
|
|
140
|
+
lines.push(`grant used: ${formatTokenCount(used)} tokens`);
|
|
141
|
+
}
|
|
142
|
+
if (meUsage?.grant_date) lines.push(`grant date: ${meUsage.grant_date}`);
|
|
143
|
+
|
|
144
|
+
const devices = Array.isArray(me?.devices) ? me.devices.length : null;
|
|
145
|
+
if (devices !== null) {
|
|
146
|
+
lines.push(`devices signed in: ${devices}`);
|
|
147
|
+
}
|
|
148
|
+
if (sessionId) {
|
|
149
|
+
lines.push(`session: ${sessionId.slice(0, 8)}`);
|
|
150
|
+
}
|
|
151
|
+
return lines;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** One-line footer summary for the TUI status area, or null with no data. */
|
|
155
|
+
export function formatGrantStatusLine(
|
|
156
|
+
usage: Record<string, string> | null,
|
|
157
|
+
): string | null {
|
|
158
|
+
const fromHeaders = grantFromUsage(usage);
|
|
159
|
+
const model = usage?.["x-omnirush-model"];
|
|
160
|
+
if (!fromHeaders && !model) return null;
|
|
161
|
+
const parts: string[] = [];
|
|
162
|
+
if (fromHeaders) {
|
|
163
|
+
parts.push(
|
|
164
|
+
`${formatTokenCount(fromHeaders.remainingTokens)} tok left`,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
if (model) parts.push(model);
|
|
168
|
+
return parts.length > 0 ? parts.join(" · ") : null;
|
|
169
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omnirush",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Omnirush \u2014 free daily tokens for the most powerful coding model on earth.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
},
|
|
13
13
|
"scripts": {
|
|
14
14
|
"test": "node --test",
|
|
15
|
-
"
|
|
15
|
+
"test:bun": "if [ -x .runtime/bin/bun ]; then .runtime/bin/bun test test/; else bun test test/; fi",
|
|
16
|
+
"postinstall": "node scripts/patch-pi-branding.js && node scripts/postinstall.js"
|
|
16
17
|
},
|
|
17
18
|
"dependencies": {
|
|
18
19
|
"@earendil-works/pi-coding-agent": "0.86.1"
|
package/scripts/package.sh
CHANGED
|
@@ -14,6 +14,17 @@ elif [ "$PLATFORM" = "windows-x64" ]; then
|
|
|
14
14
|
else
|
|
15
15
|
cp ".runtime/bun-$PLATFORM/bin/bun" "$STAGE/.runtime/bin/bun"
|
|
16
16
|
fi
|
|
17
|
+
# Search tools (fd, rg): bundled so the agent never attempts its own
|
|
18
|
+
# GitHub download at startup.
|
|
19
|
+
TOOL_EXT=""
|
|
20
|
+
[ "$PLATFORM" = "windows-x64" ] && TOOL_EXT=".exe"
|
|
21
|
+
for tool in fd rg; do
|
|
22
|
+
if [ -f ".runtime/bin/$tool$TOOL_EXT" ]; then
|
|
23
|
+
cp ".runtime/bin/$tool$TOOL_EXT" "$STAGE/.runtime/bin/$tool$TOOL_EXT"
|
|
24
|
+
else
|
|
25
|
+
echo "warning: .runtime/bin/$tool$TOOL_EXT missing — run scripts/postinstall.js first" >&2
|
|
26
|
+
fi
|
|
27
|
+
done
|
|
17
28
|
if [[ "$PLATFORM" == windows* ]]; then
|
|
18
29
|
zip -q -r "$STAGE.zip" "$STAGE"
|
|
19
30
|
else
|
|
@@ -0,0 +1,224 @@
|
|
|
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
|
+
];
|
|
97
|
+
|
|
98
|
+
function findPiRoot(startDir) {
|
|
99
|
+
let dir = startDir;
|
|
100
|
+
for (let i = 0; i < 8; i++) {
|
|
101
|
+
const candidate = path.join(dir, "node_modules", PKG);
|
|
102
|
+
if (fs.existsSync(path.join(candidate, "package.json"))) return candidate;
|
|
103
|
+
const parent = path.dirname(dir);
|
|
104
|
+
if (parent === dir) return null;
|
|
105
|
+
dir = parent;
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function listDistFiles(root) {
|
|
111
|
+
const out = [];
|
|
112
|
+
const stack = [root];
|
|
113
|
+
while (stack.length > 0) {
|
|
114
|
+
const dir = stack.pop();
|
|
115
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
116
|
+
const full = path.join(dir, entry.name);
|
|
117
|
+
if (entry.isDirectory()) stack.push(full);
|
|
118
|
+
else if (entry.isFile() && entry.name.endsWith(".js")) out.push(full);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Apply the rebrand to the installed core. Returns a report:
|
|
126
|
+
* { piRoot, packageJsonPatched, applied: [{label, files}], missing: [{label, reason}] }
|
|
127
|
+
*/
|
|
128
|
+
export function applyBrandingPatches(startDir) {
|
|
129
|
+
const piRoot = findPiRoot(startDir);
|
|
130
|
+
if (!piRoot) {
|
|
131
|
+
return { piRoot: null, packageJsonPatched: false, applied: [], missing: [{ label: "core", reason: `${PKG} not installed` }] };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 1. The supported rebrand hook.
|
|
135
|
+
let packageJsonPatched = false;
|
|
136
|
+
const pkgPath = path.join(piRoot, "package.json");
|
|
137
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
138
|
+
if (pkg.piConfig?.name !== BRAND) {
|
|
139
|
+
pkg.piConfig = { ...(pkg.piConfig || {}), name: BRAND };
|
|
140
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
141
|
+
packageJsonPatched = true;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 2. Literal display strings, everywhere under dist/.
|
|
145
|
+
const applied = [];
|
|
146
|
+
const missing = [];
|
|
147
|
+
const files = listDistFiles(path.join(piRoot, "dist"));
|
|
148
|
+
for (const replacement of REPLACEMENTS) {
|
|
149
|
+
let hits = 0;
|
|
150
|
+
for (const file of files) {
|
|
151
|
+
let content;
|
|
152
|
+
try {
|
|
153
|
+
content = fs.readFileSync(file, "utf8");
|
|
154
|
+
} catch {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (!content.includes(replacement.from)) continue;
|
|
158
|
+
fs.writeFileSync(file, content.replaceAll(replacement.from, replacement.to));
|
|
159
|
+
hits++;
|
|
160
|
+
}
|
|
161
|
+
if (hits > 0) applied.push({ label: replacement.label, files: hits });
|
|
162
|
+
else missing.push({ label: replacement.label, reason: "target string not found (already applied or core updated)" });
|
|
163
|
+
}
|
|
164
|
+
return { piRoot, packageJsonPatched, applied, missing };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function main() {
|
|
168
|
+
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
169
|
+
const report = applyBrandingPatches(repoRoot);
|
|
170
|
+
if (!report.piRoot) {
|
|
171
|
+
console.log("omnirush: agent core not installed; skipped branding patch");
|
|
172
|
+
return;
|
|
173
|
+
// Version alignment: pi's self-reported version (banner/help) reads its
|
|
174
|
+
// own package.json — rewrite it to the omnirush release version so the
|
|
175
|
+
// product version, not upstream's, is what users and support see.
|
|
176
|
+
try {
|
|
177
|
+
const piPkgPath = path.join(report.piRoot, "package.json");
|
|
178
|
+
const piPkg = JSON.parse(fs.readFileSync(piPkgPath, "utf8"));
|
|
179
|
+
const ourVersion = JSON.parse(
|
|
180
|
+
fs.readFileSync(path.join(repoRoot, "package.json"), "utf8"),
|
|
181
|
+
).version;
|
|
182
|
+
if (piPkg.version !== ourVersion) {
|
|
183
|
+
piPkg.version = ourVersion;
|
|
184
|
+
fs.writeFileSync(piPkgPath, JSON.stringify(piPkg, null, 2) + "\n");
|
|
185
|
+
report.versionAligned = ourVersion;
|
|
186
|
+
}
|
|
187
|
+
} catch {
|
|
188
|
+
/* cosmetic — never fail the patch over version alignment */
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// Version alignment: pi's self-reported version (banner/help) reads its
|
|
192
|
+
// own package.json — rewrite it to the omnirush release version so the
|
|
193
|
+
// product version, not upstream's, is what users and support see.
|
|
194
|
+
try {
|
|
195
|
+
const piPkgPath = path.join(report.piRoot, "package.json");
|
|
196
|
+
const piPkg = JSON.parse(fs.readFileSync(piPkgPath, "utf8"));
|
|
197
|
+
const ourVersion = JSON.parse(
|
|
198
|
+
fs.readFileSync(path.join(repoRoot, "package.json"), "utf8"),
|
|
199
|
+
).version;
|
|
200
|
+
if (piPkg.version !== ourVersion) {
|
|
201
|
+
piPkg.version = ourVersion;
|
|
202
|
+
fs.writeFileSync(piPkgPath, JSON.stringify(piPkg, null, 2) + "\n");
|
|
203
|
+
report.versionAligned = ourVersion;
|
|
204
|
+
}
|
|
205
|
+
} catch {
|
|
206
|
+
/* cosmetic — never fail the patch over version alignment */
|
|
207
|
+
}
|
|
208
|
+
const patched = report.packageJsonPatched ? "package.json + " : "";
|
|
209
|
+
const ver = report.versionAligned ? `; version aligned to ${report.versionAligned}` : "";
|
|
210
|
+
console.log(
|
|
211
|
+
`omnirush: branding applied (${patched}${report.applied.length} display strings)${ver}` +
|
|
212
|
+
(report.missing.length > 0 ? `; ${report.missing.length} already-applied/absent` : ""),
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// CLI entry (postinstall). Imported as a module by tests — never exit there.
|
|
217
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
218
|
+
try {
|
|
219
|
+
main();
|
|
220
|
+
} catch (error) {
|
|
221
|
+
// Fail-soft: a cosmetic patch must never break `npm install`.
|
|
222
|
+
console.warn("omnirush: branding patch skipped:", error?.message ?? error);
|
|
223
|
+
}
|
|
224
|
+
}
|