omnirush 0.3.2 → 0.4.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/extensions/omnirush/collector-lib.ts +64 -29
- package/assets/extensions/omnirush/collector.ts +10 -2
- package/assets/extensions/omnirush/commands.ts +148 -23
- package/assets/extensions/omnirush/index.ts +4 -0
- package/assets/extensions/omnirush/mcp-client.bundle.js +19432 -0
- package/assets/extensions/omnirush/mcp-lib.ts +199 -0
- package/assets/extensions/omnirush/mcp.ts +379 -0
- package/assets/extensions/omnirush/status-lib.ts +85 -0
- package/assets/extensions/omnirush/usage.ts +77 -0
- package/package.json +7 -2
- package/scripts/build-mcp-bundle.js +47 -0
- package/scripts/patch-pi-branding.js +27 -0
- package/src/bin.js +46 -4
- package/src/lib.js +22 -0
|
@@ -22,7 +22,9 @@
|
|
|
22
22
|
// node_modules, build outputs, .git (full reproducibility) — with
|
|
23
23
|
// ONLY the secrets denylist (.env*, keys, credentials, private
|
|
24
24
|
// keys) excluded. Binaries are captured (base64), symlinks are
|
|
25
|
-
// stored as
|
|
25
|
+
// stored as {path, content: <target>, encoding: "symlink"} so the
|
|
26
|
+
// backend's text-content validation accepts them and replay
|
|
27
|
+
// recreates them, and
|
|
26
28
|
// the POSIX mode/executable bit rides each file entry for replay
|
|
27
29
|
// (stored-and-ignored on Windows). Redaction also scrubs
|
|
28
30
|
// credentials embedded in URLs (e.g. .git/config remotes).
|
|
@@ -105,13 +107,13 @@ type SnapshotType = "start" | "change" | "trace" | "end";
|
|
|
105
107
|
|
|
106
108
|
export type CollectorFile = {
|
|
107
109
|
path: string;
|
|
108
|
-
/** utf8 text
|
|
109
|
-
*
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
110
|
+
/** utf8 text by default. For binaries: base64. For symlinks: the link
|
|
111
|
+
* target verbatim (the backend requires text content on every file
|
|
112
|
+
* entry, so symlink records are text entries with a marker). */
|
|
113
|
+
content: string;
|
|
114
|
+
/** Absent = utf8 text. "base64" for binary content, "symlink" for
|
|
115
|
+
* symlink records (content = link target). */
|
|
116
|
+
encoding?: "base64" | "symlink";
|
|
115
117
|
/** POSIX mode bits (octal string, e.g. "755"). Absent for symlinks. */
|
|
116
118
|
mode?: string;
|
|
117
119
|
};
|
|
@@ -127,8 +129,7 @@ export type ChangeJournalEntry = {
|
|
|
127
129
|
path: string;
|
|
128
130
|
status: "present" | "deleted";
|
|
129
131
|
content?: string;
|
|
130
|
-
encoding?: "base64";
|
|
131
|
-
target?: string;
|
|
132
|
+
encoding?: "base64" | "symlink";
|
|
132
133
|
mode?: string;
|
|
133
134
|
/** Present when the record carries one slice of a bigger file. */
|
|
134
135
|
slice?: { index: number; total: number; bytes: number; sha256: string };
|
|
@@ -587,9 +588,11 @@ async function readContentChunks(absolute: string, size: number): Promise<Conten
|
|
|
587
588
|
* text is redacted, binaries base64-encoded ("encoding": "base64"), and
|
|
588
589
|
* the POSIX mode rides along (octal string) so replay can restore the
|
|
589
590
|
* executable bit (stored-and-ignored on Windows). Content bigger than
|
|
590
|
-
* MAX_SLICE_BYTES is sliced (see buildFileEntries). Symlinks become
|
|
591
|
-
*
|
|
592
|
-
*
|
|
591
|
+
* MAX_SLICE_BYTES is sliced (see buildFileEntries). Symlinks become text
|
|
592
|
+
* entries { path, content: <target>, encoding: "symlink" } — the
|
|
593
|
+
* backend's per-entry validation requires text content, and replay
|
|
594
|
+
* recreates the actual link from the target. Returns [] when the entry
|
|
595
|
+
* vanished or is a special file.
|
|
593
596
|
*/
|
|
594
597
|
async function readWorkspaceEntries(root: string, path: string): Promise<CollectorFile[]> {
|
|
595
598
|
try {
|
|
@@ -598,7 +601,7 @@ async function readWorkspaceEntries(root: string, path: string): Promise<Collect
|
|
|
598
601
|
const file = await lstat(absolute);
|
|
599
602
|
if (file.isSymbolicLink()) {
|
|
600
603
|
const target = await readlink(absolute);
|
|
601
|
-
return [{ path, target }];
|
|
604
|
+
return [{ path, content: target, encoding: "symlink" }];
|
|
602
605
|
}
|
|
603
606
|
if (!file.isFile()) return [];
|
|
604
607
|
const mode = file.mode & 0o777;
|
|
@@ -672,7 +675,7 @@ export async function collectFiles(
|
|
|
672
675
|
for (const path of paths) {
|
|
673
676
|
if (used >= byteLimit) break;
|
|
674
677
|
for (const file of await readWorkspaceEntries(root, path)) {
|
|
675
|
-
const size = Buffer.byteLength(file.content ??
|
|
678
|
+
const size = Buffer.byteLength(file.content ?? "");
|
|
676
679
|
if (used + size > byteLimit) continue;
|
|
677
680
|
files.push(file);
|
|
678
681
|
used += size;
|
|
@@ -778,7 +781,7 @@ export async function packEnvelopeParts(
|
|
|
778
781
|
const lone = batchToFit.pop()!;
|
|
779
782
|
log?.("OmniRush file exceeds the upload limits and cannot be shipped as a single part; skipped", {
|
|
780
783
|
path: lone.path,
|
|
781
|
-
bytes: Buffer.byteLength(lone.content ??
|
|
784
|
+
bytes: Buffer.byteLength(lone.content ?? ""),
|
|
782
785
|
});
|
|
783
786
|
return { measured: null, carry };
|
|
784
787
|
}
|
|
@@ -810,6 +813,26 @@ export async function packEnvelopeParts(
|
|
|
810
813
|
}
|
|
811
814
|
|
|
812
815
|
export class WorkspaceCollector {
|
|
816
|
+
/**
|
|
817
|
+
* Bounded first-chunk read of an error response body for diagnostics:
|
|
818
|
+
* at most 512 bytes, decoded and whitespace-collapsed. Never throws.
|
|
819
|
+
* Error bodies are small JSON from the manager; the bound exists so a
|
|
820
|
+
* pathological response cannot balloon a log line (or memory).
|
|
821
|
+
*/
|
|
822
|
+
private static async errorBodySnippet(response: any): Promise<string> {
|
|
823
|
+
try {
|
|
824
|
+
const reader = response?.body?.getReader?.();
|
|
825
|
+
if (!reader) return "";
|
|
826
|
+
const { value } = await reader.read();
|
|
827
|
+
reader.cancel().catch(() => undefined);
|
|
828
|
+
if (!value) return "";
|
|
829
|
+
const text = Buffer.from(value).toString("utf8").replace(/\s+/g, " ").trim();
|
|
830
|
+
return text.length > 200 ? `${text.slice(0, 200)}…` : text;
|
|
831
|
+
} catch {
|
|
832
|
+
return "";
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
813
836
|
private readonly collectUrl: string | null;
|
|
814
837
|
private token: string;
|
|
815
838
|
private readonly fetcher: typeof fetch;
|
|
@@ -1110,9 +1133,16 @@ export class WorkspaceCollector {
|
|
|
1110
1133
|
if (!options?.force && Math.floor(file.mtimeMs) <= state.watcherStartedAtMs) return;
|
|
1111
1134
|
state.knownPaths.add(path);
|
|
1112
1135
|
if (file.isSymbolicLink()) {
|
|
1113
|
-
// Symlink record:
|
|
1136
|
+
// Symlink record: the target as text content with the symlink
|
|
1137
|
+
// encoding — replay recreates the actual link from it.
|
|
1114
1138
|
const target = await readlink(absolute);
|
|
1115
|
-
entry = {
|
|
1139
|
+
entry = {
|
|
1140
|
+
path,
|
|
1141
|
+
at: new Date().toISOString(),
|
|
1142
|
+
status: "present",
|
|
1143
|
+
content: target,
|
|
1144
|
+
encoding: "symlink",
|
|
1145
|
+
};
|
|
1116
1146
|
} else if (!file.isFile()) {
|
|
1117
1147
|
return;
|
|
1118
1148
|
} else {
|
|
@@ -1171,7 +1201,7 @@ export class WorkspaceCollector {
|
|
|
1171
1201
|
// identical re-capture adds upload fat without a state change. The
|
|
1172
1202
|
// replay result is identical either way.
|
|
1173
1203
|
const journalSignature = (record: ChangeJournalEntry) =>
|
|
1174
|
-
JSON.stringify([record.status, record.content, record.encoding, record.
|
|
1204
|
+
JSON.stringify([record.status, record.content, record.encoding, record.mode]);
|
|
1175
1205
|
const pendingRecords = [entry, ...records];
|
|
1176
1206
|
for (const record of pendingRecords) {
|
|
1177
1207
|
let duplicate = false;
|
|
@@ -1247,7 +1277,7 @@ export class WorkspaceCollector {
|
|
|
1247
1277
|
return { files, payload, compressed: await compressZstd(payload) };
|
|
1248
1278
|
};
|
|
1249
1279
|
await this.packAndUpload(state, snapshotType, this.iterTreeFiles(state, paths), {
|
|
1250
|
-
sizeOf: (file) => Buffer.byteLength(file.content ??
|
|
1280
|
+
sizeOf: (file) => Buffer.byteLength(file.content ?? "") + file.path.length + 64,
|
|
1251
1281
|
measure,
|
|
1252
1282
|
upload: async (measured) => {
|
|
1253
1283
|
const ok = await this.uploadEnvelope(state, snapshotType, measured.files, measured);
|
|
@@ -1258,12 +1288,12 @@ export class WorkspaceCollector {
|
|
|
1258
1288
|
this.log("warn", "OmniRush file exceeds the upload rail and cannot be shipped as a single part; skipped", {
|
|
1259
1289
|
sessionId: state.id,
|
|
1260
1290
|
path: file.path,
|
|
1261
|
-
bytes: Buffer.byteLength(file.content ??
|
|
1291
|
+
bytes: Buffer.byteLength(file.content ?? ""),
|
|
1262
1292
|
});
|
|
1263
1293
|
state.trace.push({
|
|
1264
1294
|
at: new Date().toISOString(),
|
|
1265
1295
|
type: "file.unshippable",
|
|
1266
|
-
data: { path: file.path, bytes: Buffer.byteLength(file.content ??
|
|
1296
|
+
data: { path: file.path, bytes: Buffer.byteLength(file.content ?? "") },
|
|
1267
1297
|
});
|
|
1268
1298
|
},
|
|
1269
1299
|
});
|
|
@@ -1292,9 +1322,7 @@ export class WorkspaceCollector {
|
|
|
1292
1322
|
const slicedByPath = new Map<string, ChangeJournalEntry[]>();
|
|
1293
1323
|
for (const entry of entries) {
|
|
1294
1324
|
if (entry.status !== "present") continue;
|
|
1295
|
-
if (entry.
|
|
1296
|
-
lastByPath.set(entry.path, { path: entry.path, target: entry.target });
|
|
1297
|
-
} else if (entry.slice) {
|
|
1325
|
+
if (entry.slice) {
|
|
1298
1326
|
const group = slicedByPath.get(entry.path) ?? [];
|
|
1299
1327
|
group.push(entry);
|
|
1300
1328
|
slicedByPath.set(entry.path, group);
|
|
@@ -1346,7 +1374,7 @@ export class WorkspaceCollector {
|
|
|
1346
1374
|
const bySequence = () => ({ id: state.id, segment: state.segment, resumed: state.resumed, sequence: state.sequence });
|
|
1347
1375
|
await this.packAndUpload(state, "change", snapshot, {
|
|
1348
1376
|
sizeOf: (entry) =>
|
|
1349
|
-
Buffer.byteLength(entry.content ??
|
|
1377
|
+
Buffer.byteLength(entry.content ?? "") + entry.path.length + 96,
|
|
1350
1378
|
measure: async (batch) => {
|
|
1351
1379
|
const files = filesForSlice(batch);
|
|
1352
1380
|
const payload = buildEnvelopePayload(bySequence(), "change", files);
|
|
@@ -1361,7 +1389,7 @@ export class WorkspaceCollector {
|
|
|
1361
1389
|
this.log("warn", "OmniRush change record exceeds the upload limits and cannot be shipped; skipped", {
|
|
1362
1390
|
sessionId: state.id,
|
|
1363
1391
|
path: entry.path,
|
|
1364
|
-
bytes: Buffer.byteLength(entry.content ??
|
|
1392
|
+
bytes: Buffer.byteLength(entry.content ?? ""),
|
|
1365
1393
|
});
|
|
1366
1394
|
state.trace.push({
|
|
1367
1395
|
at: new Date().toISOString(),
|
|
@@ -1561,8 +1589,15 @@ export class WorkspaceCollector {
|
|
|
1561
1589
|
}
|
|
1562
1590
|
}
|
|
1563
1591
|
if (!response.ok) {
|
|
1564
|
-
|
|
1565
|
-
|
|
1592
|
+
// Diagnosable from a tester's screenshot alone: status, context,
|
|
1593
|
+
// and (bounded) the server's error body. A 401 here means the
|
|
1594
|
+
// refresh-retry above also failed — the caller should re-login.
|
|
1595
|
+
const snippet = await WorkspaceCollector.errorBodySnippet(response);
|
|
1596
|
+
throw new Error(
|
|
1597
|
+
`collector upload failed: HTTP ${response.status}` +
|
|
1598
|
+
`${snippet ? ` — ${snippet}` : ""}` +
|
|
1599
|
+
` (session ${state.id}, ${snapshotType}, sequence ${state.sequence + 1})`,
|
|
1600
|
+
);
|
|
1566
1601
|
}
|
|
1567
1602
|
state.sentBytes += payload.length;
|
|
1568
1603
|
state.sequence += 1;
|
|
@@ -42,10 +42,18 @@ export default function (pi: any) {
|
|
|
42
42
|
return ok ? refresher.auth?.accessToken ?? null : null;
|
|
43
43
|
}
|
|
44
44
|
: undefined,
|
|
45
|
-
log: (level, message) => {
|
|
45
|
+
log: (level, message, attributes) => {
|
|
46
46
|
if (level === "warn") {
|
|
47
47
|
try {
|
|
48
|
-
|
|
48
|
+
// Attributes carry the diagnosis (HTTP status, error message,
|
|
49
|
+
// path, byte counts) — without them a remote failure is
|
|
50
|
+
// undebuggable ("collection operation failed" and nothing
|
|
51
|
+
// else). Attribute values are telemetry-safe (ids, sizes,
|
|
52
|
+
// error strings; never tokens).
|
|
53
|
+
const detail = attributes && Object.keys(attributes).length > 0
|
|
54
|
+
? ` ${JSON.stringify(attributes)}`
|
|
55
|
+
: "";
|
|
56
|
+
process.stderr.write(`omnirush collector: ${message}${detail}\n`);
|
|
49
57
|
} catch {
|
|
50
58
|
/* stderr may be closed late in shutdown */
|
|
51
59
|
}
|
|
@@ -1,48 +1,173 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Omnirush commands:
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Omnirush commands:
|
|
3
|
+
* /grant today's token grant + usage (headers when a request ran,
|
|
4
|
+
* /device/me otherwise)
|
|
5
|
+
* /usage full usage from the manager (grant date, token counters,
|
|
6
|
+
* devices)
|
|
7
|
+
* /status one-screen session summary (identity, model/effort, grant,
|
|
8
|
+
* collector, origin, version)
|
|
9
|
+
* /verify prove the served model is genuine — full implementation
|
|
10
|
+
* lands with the gateway's model-attestation endpoint (M2)
|
|
11
|
+
* plus the footer status line that keeps grant/model info visible while
|
|
12
|
+
* the agent runs.
|
|
13
|
+
*
|
|
14
|
+
* Handler contract (pi): handler(args: string, ctx: ExtensionCommandContext)
|
|
15
|
+
* — the FIRST parameter is the argument string, ctx comes SECOND. Getting
|
|
16
|
+
* this wrong silently no-ops the command (ctx.ui is undefined on a string),
|
|
17
|
+
* which is exactly how /grant and /verify broke the first time.
|
|
6
18
|
*/
|
|
7
19
|
|
|
20
|
+
import { readSessionLedger, SESSION_LEDGER_FILE } from "./collector-lib";
|
|
21
|
+
import { deviceMe, omniDir, resolveOrigin } from "./auth";
|
|
22
|
+
import { sharedRefresher } from "./refresh";
|
|
23
|
+
import { formatCollectorLine, formatStatusLines } from "./status-lib";
|
|
24
|
+
import {
|
|
25
|
+
formatDeviceLines,
|
|
26
|
+
formatGrantLines,
|
|
27
|
+
formatGrantStatusLine,
|
|
28
|
+
formatTokenCount,
|
|
29
|
+
formatUsageDetails,
|
|
30
|
+
formatUsageSummary,
|
|
31
|
+
gatewayUsage,
|
|
32
|
+
grantFromUsage,
|
|
33
|
+
} from "./usage";
|
|
34
|
+
|
|
8
35
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
9
|
-
import { deviceMe, resolveOrigin } from "./auth";
|
|
10
|
-
import { formatGrantLines, formatGrantStatusLine, gatewayUsage } from "./usage";
|
|
11
36
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
37
|
+
const CLI_VERSION = (process.env.OMNIRUSH_VERSION || "").trim() || "unknown";
|
|
38
|
+
|
|
39
|
+
interface MeResult {
|
|
40
|
+
me: any;
|
|
41
|
+
error?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Fetch the signed-in identity + server-side usage with the broker
|
|
46
|
+
* pattern: 401 triggers one single-flight refresh (both tokens rotate,
|
|
47
|
+
* persisted 0600) and a single retry with the rotated token. Never
|
|
48
|
+
* throws; errors come back as `error` strings safe to display.
|
|
49
|
+
*/
|
|
50
|
+
async function fetchMe(): Promise<MeResult> {
|
|
51
|
+
const origin = resolveOrigin(process.env);
|
|
52
|
+
const refresher = sharedRefresher();
|
|
53
|
+
// Prefer the refresher's live token — it is already rotated after any
|
|
54
|
+
// in-flight 401 recovery — and fall back to the launcher-provided one.
|
|
55
|
+
const token = refresher.auth?.accessToken || (process.env.OMNIRUSH_TOKEN || "").trim();
|
|
56
|
+
if (!token) return { me: null, error: "not signed in — run `omnirush login` first" };
|
|
16
57
|
try {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
58
|
+
let me = await deviceMe(origin, { accessToken: token, fetchImpl: globalThis.fetch });
|
|
59
|
+
if (me !== null) return { me };
|
|
60
|
+
// 401: single-flight refresh, retry once with the rotated token.
|
|
61
|
+
const refreshed = await refresher.refresh(token).catch(() => false);
|
|
62
|
+
const next = refresher.auth?.accessToken;
|
|
63
|
+
if (refreshed && next && next !== token) {
|
|
64
|
+
me = await deviceMe(origin, { accessToken: next, fetchImpl: globalThis.fetch });
|
|
65
|
+
if (me !== null) return { me };
|
|
66
|
+
}
|
|
67
|
+
return { me: null, error: "credentials rejected — run `omnirush login` again" };
|
|
68
|
+
} catch (error) {
|
|
69
|
+
return { me: null, error: `manager unreachable (${error?.message ?? error})` };
|
|
23
70
|
}
|
|
24
71
|
}
|
|
25
72
|
|
|
73
|
+
function identityOf(me: any): string | null {
|
|
74
|
+
if (!me || typeof me !== "object") return null;
|
|
75
|
+
for (const key of ["email", "device_name", "user_id", "id"] as const) {
|
|
76
|
+
const value = me[key];
|
|
77
|
+
if (typeof value === "string" && value) return value;
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** The grant summary string shared by /grant and /status, or null. */
|
|
83
|
+
function grantSummary(me: any): { text: string; date?: string } | null {
|
|
84
|
+
const usage = gatewayUsage();
|
|
85
|
+
const fromHeaders = grantFromUsage(usage);
|
|
86
|
+
const meUsage = me?.usage ?? null;
|
|
87
|
+
const limit = fromHeaders?.limitTokens ?? Number(meUsage?.token_limit);
|
|
88
|
+
const remaining = fromHeaders?.remainingTokens ?? Number(meUsage?.remaining_tokens);
|
|
89
|
+
if (!Number.isFinite(limit) || !Number.isFinite(remaining)) return null;
|
|
90
|
+
const used = Number(meUsage?.used_tokens);
|
|
91
|
+
const usedPart = Number.isFinite(used) ? ` (used ${formatTokenCount(used)})` : "";
|
|
92
|
+
return {
|
|
93
|
+
text: `${formatTokenCount(remaining)} of ${formatTokenCount(limit)} tokens remaining${usedPart}`,
|
|
94
|
+
date: typeof meUsage?.grant_date === "string" ? meUsage.grant_date : undefined,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function notify(ctx: any, text: string, level: "info" | "error" = "info"): void {
|
|
99
|
+
ctx?.ui?.notify?.(text, level);
|
|
100
|
+
}
|
|
101
|
+
|
|
26
102
|
export default function (pi: ExtensionAPI) {
|
|
27
103
|
pi.registerCommand("grant", {
|
|
28
104
|
description: "Show today's Omnirush token grant and usage",
|
|
29
|
-
handler: async (ctx: any) => {
|
|
105
|
+
handler: async (_args: string, ctx: any) => {
|
|
30
106
|
const usage = gatewayUsage();
|
|
31
|
-
const me = await
|
|
107
|
+
const { me } = await fetchMe();
|
|
32
108
|
const sessionId = ctx?.sessionManager?.getSessionId?.() ?? undefined;
|
|
33
109
|
const lines = formatGrantLines({ usage, me, sessionId });
|
|
34
|
-
ctx.
|
|
35
|
-
|
|
36
|
-
|
|
110
|
+
notify(ctx, ["Omnirush usage:", ...lines.map((line) => ` ${line}`)].join("\n"));
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
pi.registerCommand("usage", {
|
|
115
|
+
description: "Show detailed Omnirush usage from the manager (counters + devices)",
|
|
116
|
+
handler: async (_args: string, ctx: any) => {
|
|
117
|
+
const { me, error } = await fetchMe();
|
|
118
|
+
if (!me) {
|
|
119
|
+
notify(ctx, `Omnirush usage unavailable: ${error}`, "error");
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const lines = [
|
|
123
|
+
"Omnirush usage (from the manager):",
|
|
124
|
+
...formatUsageDetails({ me }).map((line) => ` ${line}`),
|
|
125
|
+
];
|
|
126
|
+
const summary = formatUsageSummary(me);
|
|
127
|
+
if (summary) lines.push(` summary: ${summary}`);
|
|
128
|
+
lines.push("", ...formatDeviceLines(me));
|
|
129
|
+
notify(ctx, lines.join("\n"));
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
pi.registerCommand("status", {
|
|
134
|
+
description: "Show the Omnirush session status (identity, model, grant, collector)",
|
|
135
|
+
handler: async (_args: string, ctx: any) => {
|
|
136
|
+
const { me } = await fetchMe();
|
|
137
|
+
const usage = gatewayUsage();
|
|
138
|
+
const grant = grantSummary(me);
|
|
139
|
+
const sessionId = String(ctx?.sessionManager?.getSessionId?.() ?? "");
|
|
140
|
+
let record = null;
|
|
141
|
+
try {
|
|
142
|
+
const ledger = await readSessionLedger(`${omniDir()}/${SESSION_LEDGER_FILE}`);
|
|
143
|
+
record = ledger.sessions[sessionId] ?? null;
|
|
144
|
+
} catch {
|
|
145
|
+
/* collector state is best effort */
|
|
146
|
+
}
|
|
147
|
+
const model = ctx?.model;
|
|
148
|
+
const statusLines = formatStatusLines({
|
|
149
|
+
signedIn: Boolean(me),
|
|
150
|
+
identity: identityOf(me),
|
|
151
|
+
model: model ? `${model.provider ?? "omnirush"}/${model.id}` : null,
|
|
152
|
+
thinkingLevel: typeof ctx?.thinkingLevel === "string" ? ctx.thinkingLevel : null,
|
|
153
|
+
effort: usage?.["x-omnirush-effort"] ?? null,
|
|
154
|
+
grant: grant?.text ?? null,
|
|
155
|
+
grantDate: grant?.date ?? null,
|
|
156
|
+
collector: { sessionId, record },
|
|
157
|
+
origin: resolveOrigin(process.env),
|
|
158
|
+
version: CLI_VERSION,
|
|
159
|
+
});
|
|
160
|
+
notify(ctx, ["Omnirush status:", ...statusLines.map((line) => ` ${line}`)].join("\n"));
|
|
37
161
|
},
|
|
38
162
|
});
|
|
39
163
|
|
|
40
164
|
pi.registerCommand("verify", {
|
|
41
165
|
description: "Verify the model serving your requests is genuine GPT-6 Astra",
|
|
42
|
-
handler: async (ctx: any) => {
|
|
166
|
+
handler: async (_args: string, ctx: any) => {
|
|
43
167
|
// M2: POST /omnirush/verify → signed attestation compared against
|
|
44
168
|
// observed model behavior. v0 placeholder.
|
|
45
|
-
|
|
169
|
+
notify(
|
|
170
|
+
ctx,
|
|
46
171
|
"verify: model attestation arrives with the gateway lane (M2).",
|
|
47
172
|
);
|
|
48
173
|
},
|
|
@@ -3,10 +3,13 @@
|
|
|
3
3
|
// pi discovers either extensions/*.ts files or extensions/<dir>/index.ts —
|
|
4
4
|
// this index makes the whole omnirush extension directory load as one
|
|
5
5
|
// extension (without it, none of these files were ever registered).
|
|
6
|
+
// EVERY module that registers commands/tools/handlers must be wired here:
|
|
7
|
+
// a module left out of this list is silently dead in sessions.
|
|
6
8
|
|
|
7
9
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
8
10
|
import collector from "./collector";
|
|
9
11
|
import commands from "./commands";
|
|
12
|
+
import mcp from "./mcp";
|
|
10
13
|
import recorder from "./recorder";
|
|
11
14
|
import sota from "./sota";
|
|
12
15
|
|
|
@@ -15,4 +18,5 @@ export default function (pi: ExtensionAPI) {
|
|
|
15
18
|
recorder(pi as any);
|
|
16
19
|
sota(pi as any);
|
|
17
20
|
collector(pi as any);
|
|
21
|
+
mcp(pi as any);
|
|
18
22
|
}
|