@cesarus777/opencode-cgp 0.3.0 → 0.4.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/CHANGELOG.md +14 -0
- package/README.md +36 -7
- package/dist/cli.js +822 -35
- package/dist/core.js +49 -4
- package/dist/index.js +570 -9
- package/dist/rpc.js +129 -2
- package/dist/tui.js +128 -5
- package/dist/types/adapter.d.ts +2 -0
- package/dist/types/clihelp.d.ts +2 -0
- package/dist/types/core.d.ts +1 -0
- package/dist/types/dashboard/command.d.ts +1 -1
- package/dist/types/dashboard/connection.d.ts +2 -0
- package/dist/types/dashboard/controller.d.ts +3 -0
- package/dist/types/dashboard/models.d.ts +42 -0
- package/dist/types/dashboard/modelview.d.ts +30 -0
- package/dist/types/dashboard/port.d.ts +6 -0
- package/dist/types/dashboard/terminal.d.ts +5 -0
- package/dist/types/dashboard/view.d.ts +3 -1
- package/dist/types/history.d.ts +1 -0
- package/dist/types/modelanalytics.d.ts +8 -0
- package/dist/types/modelanalyticsserver.d.ts +16 -0
- package/dist/types/modelseries.d.ts +43 -0
- package/dist/types/rpc.d.ts +28 -0
- package/dist/types/tui/chartformat.d.ts +1 -1
- package/package.json +2 -6
package/dist/cli.js
CHANGED
|
@@ -44,6 +44,12 @@ function isHistoryRead(value) {
|
|
|
44
44
|
function isHistoryResult(value) {
|
|
45
45
|
return valid(value, historyResultSchema);
|
|
46
46
|
}
|
|
47
|
+
function isAnalyticsRead(value) {
|
|
48
|
+
return valid(value, analyticsReadSchema) && record(value) && typeof value.from === "number" && typeof value.to === "number" && value.from < value.to && value.to - value.from <= 604800000 && !(value.model !== undefined && value.unknownModel === true) && (!(value.modelCursor || value.requestCursor) || typeof value.snapshotId === "string");
|
|
49
|
+
}
|
|
50
|
+
function isAnalyticsResult(value) {
|
|
51
|
+
return valid(value, analyticsResultSchema);
|
|
52
|
+
}
|
|
47
53
|
function record(x) {
|
|
48
54
|
return typeof x === "object" && x !== null && !Array.isArray(x);
|
|
49
55
|
}
|
|
@@ -101,7 +107,7 @@ var count, text, enumeration = (...values) => ({ type: "string", enum: values })
|
|
|
101
107
|
type: "array",
|
|
102
108
|
items,
|
|
103
109
|
maxItems
|
|
104
|
-
}), measurement, row, batchFields, readSchema, batchSchema, resultSchema, monitorRpc, nonnegative, performanceSchema, performanceRow, performanceBatchSchema, performanceResultSchema, performanceRpc, chartPointSchema, chartRequestSchema, historyReadSchema, historyResultSchema, historyRpc;
|
|
110
|
+
}), measurement, row, batchFields, readSchema, batchSchema, resultSchema, monitorRpc, nonnegative, performanceSchema, performanceRow, performanceBatchSchema, performanceResultSchema, performanceRpc, chartPointSchema, chartRequestSchema, historyReadSchema, historyResultSchema, historyRpc, modelSummarySchema, analyticsReadSchema, analyticsResultSchema, analyticsRpc;
|
|
105
111
|
var init_rpc = __esm(() => {
|
|
106
112
|
count = {
|
|
107
113
|
type: "integer",
|
|
@@ -337,6 +343,123 @@ var init_rpc = __esm(() => {
|
|
|
337
343
|
methods: { read: { input: historyReadSchema, output: historyResultSchema } },
|
|
338
344
|
events: {}
|
|
339
345
|
});
|
|
346
|
+
modelSummarySchema = object({
|
|
347
|
+
model: text,
|
|
348
|
+
sessions: count,
|
|
349
|
+
completedRequests: count,
|
|
350
|
+
partialRequests: count,
|
|
351
|
+
legacyAttributionRequests: count,
|
|
352
|
+
tpsRequests: count,
|
|
353
|
+
ttftRequests: count,
|
|
354
|
+
excludedTpsRequests: count,
|
|
355
|
+
excludedTtftRequests: count,
|
|
356
|
+
outputTokens: nonnegative,
|
|
357
|
+
durationMs: nonnegative,
|
|
358
|
+
averageTps: nonnegative,
|
|
359
|
+
meanTtftMs: nonnegative,
|
|
360
|
+
medianTtftMs: nonnegative,
|
|
361
|
+
p95TtftMs: nonnegative
|
|
362
|
+
}, [
|
|
363
|
+
"sessions",
|
|
364
|
+
"completedRequests",
|
|
365
|
+
"partialRequests",
|
|
366
|
+
"legacyAttributionRequests",
|
|
367
|
+
"tpsRequests",
|
|
368
|
+
"ttftRequests",
|
|
369
|
+
"excludedTpsRequests",
|
|
370
|
+
"excludedTtftRequests",
|
|
371
|
+
"outputTokens",
|
|
372
|
+
"durationMs"
|
|
373
|
+
]);
|
|
374
|
+
analyticsReadSchema = object({
|
|
375
|
+
schemaVersion: { const: 1 },
|
|
376
|
+
from: count,
|
|
377
|
+
to: count,
|
|
378
|
+
buckets: { type: "integer", minimum: 1, maximum: 240 },
|
|
379
|
+
agent: text,
|
|
380
|
+
model: text,
|
|
381
|
+
unknownModel: { type: "boolean" },
|
|
382
|
+
sort: enumeration("requests", "tps", "ttft", "identity"),
|
|
383
|
+
snapshotId: text,
|
|
384
|
+
modelCursor: text,
|
|
385
|
+
requestCursor: text
|
|
386
|
+
}, ["schemaVersion", "from", "to", "buckets"]);
|
|
387
|
+
analyticsResultSchema = object({
|
|
388
|
+
schemaVersion: { const: 1 },
|
|
389
|
+
generatedAt: count,
|
|
390
|
+
expiresAt: count,
|
|
391
|
+
snapshotId: text,
|
|
392
|
+
enabled: { type: "boolean" },
|
|
393
|
+
from: count,
|
|
394
|
+
to: count,
|
|
395
|
+
agent: text,
|
|
396
|
+
models: array(modelSummarySchema, 50),
|
|
397
|
+
totalModels: count,
|
|
398
|
+
nextModelsCursor: text,
|
|
399
|
+
selected: modelSummarySchema,
|
|
400
|
+
buckets: array(object({ from: nonnegative, to: nonnegative, summary: modelSummarySchema }, ["from", "to"]), 240),
|
|
401
|
+
requests: array(object({
|
|
402
|
+
location: text,
|
|
403
|
+
sessionId: text,
|
|
404
|
+
requestId: text,
|
|
405
|
+
startedAt: count,
|
|
406
|
+
completedAt: count,
|
|
407
|
+
model: text,
|
|
408
|
+
agent: text,
|
|
409
|
+
attribution: enumeration("request", "legacy-record", "unknown"),
|
|
410
|
+
partial: { type: "boolean" },
|
|
411
|
+
outputTokens: nonnegative,
|
|
412
|
+
ttftMs: nonnegative
|
|
413
|
+
}, [
|
|
414
|
+
"location",
|
|
415
|
+
"sessionId",
|
|
416
|
+
"requestId",
|
|
417
|
+
"startedAt",
|
|
418
|
+
"completedAt",
|
|
419
|
+
"attribution",
|
|
420
|
+
"partial"
|
|
421
|
+
]), 50),
|
|
422
|
+
nextRequestsCursor: text,
|
|
423
|
+
coverage: object({
|
|
424
|
+
source: enumeration("retained-request-records"),
|
|
425
|
+
scannedRecords: count,
|
|
426
|
+
unscannedRecords: count,
|
|
427
|
+
duplicateRecords: count,
|
|
428
|
+
conflictingRequests: count,
|
|
429
|
+
missingIdentityRecords: count,
|
|
430
|
+
incompleteRequests: count,
|
|
431
|
+
invalidTimingRequests: count,
|
|
432
|
+
gapRecords: count,
|
|
433
|
+
maxRequests: count,
|
|
434
|
+
partial: { type: "boolean" }
|
|
435
|
+
}),
|
|
436
|
+
metadata: historyResultSchema.properties.metadata ?? {},
|
|
437
|
+
gaps: array(count, 128),
|
|
438
|
+
omittedGaps: count
|
|
439
|
+
}, [
|
|
440
|
+
"schemaVersion",
|
|
441
|
+
"generatedAt",
|
|
442
|
+
"expiresAt",
|
|
443
|
+
"snapshotId",
|
|
444
|
+
"enabled",
|
|
445
|
+
"from",
|
|
446
|
+
"to",
|
|
447
|
+
"models",
|
|
448
|
+
"totalModels",
|
|
449
|
+
"buckets",
|
|
450
|
+
"requests",
|
|
451
|
+
"coverage",
|
|
452
|
+
"metadata",
|
|
453
|
+
"gaps",
|
|
454
|
+
"omittedGaps"
|
|
455
|
+
]);
|
|
456
|
+
analyticsRpc = Rpc.define({
|
|
457
|
+
id: "context-guard.analytics.v1",
|
|
458
|
+
methods: {
|
|
459
|
+
read: { input: analyticsReadSchema, output: analyticsResultSchema }
|
|
460
|
+
},
|
|
461
|
+
events: {}
|
|
462
|
+
});
|
|
340
463
|
});
|
|
341
464
|
|
|
342
465
|
// src/history.ts
|
|
@@ -529,6 +652,7 @@ function scalarPerformance(p) {
|
|
|
529
652
|
async function readPerformanceHistory(directory, filter = {}) {
|
|
530
653
|
const records = [];
|
|
531
654
|
const files = await segments(directory);
|
|
655
|
+
filter.signal?.throwIfAborted();
|
|
532
656
|
const maxBytes = filter.maxBytes ?? 64 * 1024 * 1024;
|
|
533
657
|
let bytes = 0, skippedBytes = 0, malformedRecords = 0;
|
|
534
658
|
const selected = [];
|
|
@@ -541,6 +665,7 @@ async function readPerformanceHistory(directory, filter = {}) {
|
|
|
541
665
|
selected.unshift(file);
|
|
542
666
|
}
|
|
543
667
|
for (const file of selected) {
|
|
668
|
+
filter.signal?.throwIfAborted();
|
|
544
669
|
const handle = await open(join(directory, file.name), "r");
|
|
545
670
|
let source;
|
|
546
671
|
try {
|
|
@@ -550,7 +675,12 @@ async function readPerformanceHistory(directory, filter = {}) {
|
|
|
550
675
|
} finally {
|
|
551
676
|
await handle.close();
|
|
552
677
|
}
|
|
678
|
+
let scannedLines = 0;
|
|
553
679
|
for (const line of source.split("\n")) {
|
|
680
|
+
if (filter.signal && ++scannedLines % 256 === 0) {
|
|
681
|
+
await new Promise((done) => setTimeout(done, 0));
|
|
682
|
+
filter.signal.throwIfAborted();
|
|
683
|
+
}
|
|
554
684
|
if (!line)
|
|
555
685
|
continue;
|
|
556
686
|
try {
|
|
@@ -663,19 +793,43 @@ async function loadTerminalRenderer() {
|
|
|
663
793
|
try {
|
|
664
794
|
return await import("@opentui/core");
|
|
665
795
|
} catch {
|
|
666
|
-
throw new Error("Terminal renderer unavailable;
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
var dashboardHelp = `Usage: context-guard dashboard tui [
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
796
|
+
throw new Error("Terminal renderer unavailable; reinstall @cesarus777/opencode-cgp with optional dependencies enabled for its platform renderer, using Bun >=1.3.0");
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
var dashboardHelp = `Usage: context-guard dashboard tui [options]
|
|
800
|
+
Open a read-only live terminal dashboard for an existing OpenCode server.
|
|
801
|
+
|
|
802
|
+
Start here
|
|
803
|
+
1. Keep an OpenCode server running with Context Guard registered.
|
|
804
|
+
2. Open a separate terminal tab, window or pane for the dashboard.
|
|
805
|
+
3. In the same project directory, run: context-guard dashboard tui
|
|
806
|
+
Keep working in OpenCode; the dashboard reads live updates from its server.
|
|
807
|
+
|
|
808
|
+
Options
|
|
809
|
+
--directory PATH Owning project directory (local default: current directory)
|
|
810
|
+
--server URL Connect to an explicit server; requires --directory with
|
|
811
|
+
an absolute server-side project path. Use HTTPS except
|
|
812
|
+
on loopback. Credentials in URLs are rejected.
|
|
813
|
+
-h, --help Show this help
|
|
814
|
+
|
|
815
|
+
Examples
|
|
816
|
+
context-guard dashboard tui
|
|
817
|
+
context-guard dashboard tui --directory /absolute/project
|
|
818
|
+
context-guard dashboard tui --server https://server.example --directory /server/project
|
|
819
|
+
|
|
820
|
+
Requirements
|
|
821
|
+
Interactive terminal and Bun >=1.3.0. The OpenTUI renderer is installed
|
|
822
|
+
automatically with CGP. This command never starts a server.
|
|
823
|
+
|
|
824
|
+
Authentication (explicit servers)
|
|
825
|
+
CONTEXT_GUARD_PASSWORD Server password
|
|
826
|
+
CONTEXT_GUARD_USERNAME Server username (default: opencode)
|
|
827
|
+
|
|
828
|
+
Keys
|
|
829
|
+
1/2/3/4 overview/detail/history/models; arrows or j/k move; Enter detail/chart;
|
|
830
|
+
Models: Left/Right metric, [/] bucket, d requests, n/p pages, s sort;
|
|
831
|
+
/ session search; f filters (Models: agent); l location; w time window;
|
|
832
|
+
r refresh; ? help; q or Ctrl+C quit.`;
|
|
679
833
|
var init_command = () => {
|
|
680
834
|
};
|
|
681
835
|
|
|
@@ -1143,6 +1297,249 @@ var init_inventory = __esm(() => {
|
|
|
1143
1297
|
});
|
|
1144
1298
|
});
|
|
1145
1299
|
|
|
1300
|
+
// src/dashboard/port.ts
|
|
1301
|
+
var AnalyticsUnavailable;
|
|
1302
|
+
var init_port = __esm(() => {
|
|
1303
|
+
AnalyticsUnavailable = class AnalyticsUnavailable extends Error {
|
|
1304
|
+
reason;
|
|
1305
|
+
constructor(reason) {
|
|
1306
|
+
super(`Model analytics ${reason}`);
|
|
1307
|
+
this.reason = reason;
|
|
1308
|
+
}
|
|
1309
|
+
};
|
|
1310
|
+
});
|
|
1311
|
+
|
|
1312
|
+
// src/dashboard/models.ts
|
|
1313
|
+
class ModelsController {
|
|
1314
|
+
changed;
|
|
1315
|
+
port;
|
|
1316
|
+
active = false;
|
|
1317
|
+
key = "";
|
|
1318
|
+
agent = "";
|
|
1319
|
+
seedModel = "";
|
|
1320
|
+
selection;
|
|
1321
|
+
resolution = 120;
|
|
1322
|
+
from = 0;
|
|
1323
|
+
to = 1;
|
|
1324
|
+
generation = 0;
|
|
1325
|
+
abort;
|
|
1326
|
+
pending;
|
|
1327
|
+
modelPages = [undefined];
|
|
1328
|
+
requestPages = [undefined];
|
|
1329
|
+
state = {
|
|
1330
|
+
status: "idle",
|
|
1331
|
+
stale: false,
|
|
1332
|
+
sort: "requests",
|
|
1333
|
+
windowMs: 300000,
|
|
1334
|
+
modelPage: 0,
|
|
1335
|
+
requestPage: 0
|
|
1336
|
+
};
|
|
1337
|
+
constructor(changed) {
|
|
1338
|
+
this.changed = changed;
|
|
1339
|
+
}
|
|
1340
|
+
getView() {
|
|
1341
|
+
return this.state;
|
|
1342
|
+
}
|
|
1343
|
+
configure(port, active, windowMs, agent, seedModel, now) {
|
|
1344
|
+
const key = JSON.stringify([
|
|
1345
|
+
port.location.directory,
|
|
1346
|
+
windowMs,
|
|
1347
|
+
agent,
|
|
1348
|
+
seedModel
|
|
1349
|
+
]);
|
|
1350
|
+
const different = key !== this.key;
|
|
1351
|
+
const preserveSelection = this.port?.location.directory === port.location.directory && this.agent === agent && this.seedModel === seedModel;
|
|
1352
|
+
const entering = active && !this.active;
|
|
1353
|
+
if (different || !active)
|
|
1354
|
+
this.cancel();
|
|
1355
|
+
this.port = port;
|
|
1356
|
+
this.active = active;
|
|
1357
|
+
if (different) {
|
|
1358
|
+
this.key = key;
|
|
1359
|
+
this.agent = agent;
|
|
1360
|
+
this.seedModel = seedModel;
|
|
1361
|
+
if (!preserveSelection)
|
|
1362
|
+
this.selection = seedModel || undefined;
|
|
1363
|
+
this.modelPages = [undefined];
|
|
1364
|
+
this.requestPages = [undefined];
|
|
1365
|
+
this.state = {
|
|
1366
|
+
status: "idle",
|
|
1367
|
+
stale: false,
|
|
1368
|
+
sort: this.state.sort,
|
|
1369
|
+
windowMs,
|
|
1370
|
+
modelPage: 0,
|
|
1371
|
+
requestPage: 0
|
|
1372
|
+
};
|
|
1373
|
+
}
|
|
1374
|
+
if (active && (different || entering))
|
|
1375
|
+
this.refresh(now);
|
|
1376
|
+
}
|
|
1377
|
+
cancel() {
|
|
1378
|
+
this.generation++;
|
|
1379
|
+
this.abort?.abort();
|
|
1380
|
+
this.abort = undefined;
|
|
1381
|
+
this.pending = undefined;
|
|
1382
|
+
}
|
|
1383
|
+
stop() {
|
|
1384
|
+
this.cancel();
|
|
1385
|
+
this.active = false;
|
|
1386
|
+
this.state = { ...this.state, stale: Boolean(this.state.result) };
|
|
1387
|
+
}
|
|
1388
|
+
async refresh(now) {
|
|
1389
|
+
if (!this.active || !this.port)
|
|
1390
|
+
return;
|
|
1391
|
+
if (this.pending)
|
|
1392
|
+
return this.pending;
|
|
1393
|
+
this.to = Math.max(1, Math.floor(now));
|
|
1394
|
+
this.from = Math.max(0, this.to - this.state.windowMs);
|
|
1395
|
+
this.modelPages = [undefined];
|
|
1396
|
+
this.requestPages = [undefined];
|
|
1397
|
+
this.state = { ...this.state, modelPage: 0, requestPage: 0 };
|
|
1398
|
+
return this.query(false);
|
|
1399
|
+
}
|
|
1400
|
+
setWidth(width) {
|
|
1401
|
+
const next = Math.max(1, Math.min(120, Math.floor(width) - 10));
|
|
1402
|
+
if (next === this.resolution)
|
|
1403
|
+
return;
|
|
1404
|
+
this.resolution = next;
|
|
1405
|
+
if (this.active && this.state.result)
|
|
1406
|
+
this.query(true);
|
|
1407
|
+
}
|
|
1408
|
+
select(model) {
|
|
1409
|
+
if (!this.active || model === this.selection)
|
|
1410
|
+
return;
|
|
1411
|
+
this.selection = model;
|
|
1412
|
+
this.requestPages = [undefined];
|
|
1413
|
+
this.state = { ...this.state, requestPage: 0 };
|
|
1414
|
+
this.query(true);
|
|
1415
|
+
}
|
|
1416
|
+
sort() {
|
|
1417
|
+
const sorts = ["requests", "tps", "ttft", "identity"];
|
|
1418
|
+
this.modelPages = [undefined];
|
|
1419
|
+
this.state = {
|
|
1420
|
+
...this.state,
|
|
1421
|
+
sort: sorts[(sorts.indexOf(this.state.sort) + 1) % sorts.length] ?? "requests",
|
|
1422
|
+
modelPage: 0
|
|
1423
|
+
};
|
|
1424
|
+
this.query(true);
|
|
1425
|
+
}
|
|
1426
|
+
page(kind, direction) {
|
|
1427
|
+
if (this.state.status !== "ready")
|
|
1428
|
+
return;
|
|
1429
|
+
const pages = kind === "models" ? this.modelPages : this.requestPages;
|
|
1430
|
+
const index = kind === "models" ? this.state.modelPage : this.state.requestPage;
|
|
1431
|
+
const next = kind === "models" ? this.state.result?.nextModelsCursor : this.state.result?.nextRequestsCursor;
|
|
1432
|
+
if (direction < 0 && index === 0 || direction > 0 && !next)
|
|
1433
|
+
return;
|
|
1434
|
+
if (direction > 0)
|
|
1435
|
+
pages[index + 1] = next;
|
|
1436
|
+
this.state = {
|
|
1437
|
+
...this.state,
|
|
1438
|
+
...kind === "models" ? { modelPage: index + direction, requestPage: 0 } : { requestPage: index + direction }
|
|
1439
|
+
};
|
|
1440
|
+
if (kind === "models") {
|
|
1441
|
+
this.selection = undefined;
|
|
1442
|
+
this.requestPages = [undefined];
|
|
1443
|
+
}
|
|
1444
|
+
this.query(true);
|
|
1445
|
+
}
|
|
1446
|
+
query(reuse) {
|
|
1447
|
+
this.cancel();
|
|
1448
|
+
const port = this.port;
|
|
1449
|
+
if (!this.active || !port)
|
|
1450
|
+
return Promise.resolve();
|
|
1451
|
+
if (!port.analytics) {
|
|
1452
|
+
this.state = { ...this.state, status: "unsupported", stale: false };
|
|
1453
|
+
this.changed();
|
|
1454
|
+
return Promise.resolve();
|
|
1455
|
+
}
|
|
1456
|
+
const read = port.analytics.bind(port);
|
|
1457
|
+
const controller = new AbortController;
|
|
1458
|
+
this.abort = controller;
|
|
1459
|
+
const generation = this.generation;
|
|
1460
|
+
const signal = AbortSignal.any([
|
|
1461
|
+
controller.signal,
|
|
1462
|
+
AbortSignal.timeout(15000)
|
|
1463
|
+
]);
|
|
1464
|
+
const modelCursor = this.modelPages[this.state.modelPage];
|
|
1465
|
+
const requestCursor = this.requestPages[this.state.requestPage];
|
|
1466
|
+
const input = {
|
|
1467
|
+
schemaVersion: 1,
|
|
1468
|
+
from: this.from,
|
|
1469
|
+
to: this.to,
|
|
1470
|
+
buckets: this.resolution,
|
|
1471
|
+
sort: this.state.sort,
|
|
1472
|
+
...this.agent ? { agent: this.agent } : {},
|
|
1473
|
+
...this.selection === "" ? { unknownModel: true } : this.selection ? { model: this.selection } : {},
|
|
1474
|
+
...reuse && this.state.result ? { snapshotId: this.state.result.snapshotId } : {},
|
|
1475
|
+
...reuse && modelCursor ? { modelCursor } : {},
|
|
1476
|
+
...reuse && requestCursor ? { requestCursor } : {}
|
|
1477
|
+
};
|
|
1478
|
+
this.state = {
|
|
1479
|
+
...this.state,
|
|
1480
|
+
status: "loading",
|
|
1481
|
+
stale: Boolean(this.state.result)
|
|
1482
|
+
};
|
|
1483
|
+
this.changed();
|
|
1484
|
+
const task = Promise.resolve().then(async () => {
|
|
1485
|
+
try {
|
|
1486
|
+
let result;
|
|
1487
|
+
try {
|
|
1488
|
+
result = await read(input, signal);
|
|
1489
|
+
} catch (error) {
|
|
1490
|
+
if (!input.snapshotId || signal.aborted || error instanceof AnalyticsUnavailable && error.reason === "unsupported")
|
|
1491
|
+
throw error;
|
|
1492
|
+
const {
|
|
1493
|
+
snapshotId: _id,
|
|
1494
|
+
modelCursor: _m,
|
|
1495
|
+
requestCursor: _r,
|
|
1496
|
+
...fresh
|
|
1497
|
+
} = input;
|
|
1498
|
+
result = await read(fresh, signal);
|
|
1499
|
+
if (generation !== this.generation)
|
|
1500
|
+
return;
|
|
1501
|
+
signal.throwIfAborted();
|
|
1502
|
+
this.modelPages = [undefined];
|
|
1503
|
+
this.requestPages = [undefined];
|
|
1504
|
+
this.state = { ...this.state, modelPage: 0, requestPage: 0 };
|
|
1505
|
+
}
|
|
1506
|
+
if (generation !== this.generation)
|
|
1507
|
+
return;
|
|
1508
|
+
signal.throwIfAborted();
|
|
1509
|
+
if (!isAnalyticsResult(result) || result.from !== input.from || result.to !== input.to || result.agent !== input.agent || result.requests.some((r) => r.location !== port.location.directory))
|
|
1510
|
+
throw new Error("Invalid analytics response");
|
|
1511
|
+
if (result.selected)
|
|
1512
|
+
this.selection = result.selected.model ?? "";
|
|
1513
|
+
this.state = {
|
|
1514
|
+
...this.state,
|
|
1515
|
+
result: structuredClone(result),
|
|
1516
|
+
status: result.enabled ? "ready" : "disabled",
|
|
1517
|
+
stale: false
|
|
1518
|
+
};
|
|
1519
|
+
} catch (error) {
|
|
1520
|
+
if (generation !== this.generation || controller.signal.aborted)
|
|
1521
|
+
return;
|
|
1522
|
+
this.state = {
|
|
1523
|
+
...this.state,
|
|
1524
|
+
status: error instanceof AnalyticsUnavailable && error.reason === "unsupported" && !this.state.result ? "unsupported" : "unavailable",
|
|
1525
|
+
stale: Boolean(this.state.result)
|
|
1526
|
+
};
|
|
1527
|
+
} finally {
|
|
1528
|
+
if (generation === this.generation) {
|
|
1529
|
+
this.pending = undefined;
|
|
1530
|
+
this.changed();
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
});
|
|
1534
|
+
this.pending = task;
|
|
1535
|
+
return task;
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
var init_models = __esm(() => {
|
|
1539
|
+
init_rpc();
|
|
1540
|
+
init_port();
|
|
1541
|
+
});
|
|
1542
|
+
|
|
1146
1543
|
// src/dashboard/view.ts
|
|
1147
1544
|
function immutableView(value) {
|
|
1148
1545
|
const copy = structuredClone(value);
|
|
@@ -1235,6 +1632,7 @@ class DashboardController {
|
|
|
1235
1632
|
refreshTimer;
|
|
1236
1633
|
ageTimer;
|
|
1237
1634
|
view;
|
|
1635
|
+
models;
|
|
1238
1636
|
constructor(port, options = {}) {
|
|
1239
1637
|
this.limits = { ...options.limits ?? inventoryLimits };
|
|
1240
1638
|
validateInventoryLimits(this.limits);
|
|
@@ -1245,6 +1643,7 @@ class DashboardController {
|
|
|
1245
1643
|
throw new Error("Invalid dashboard refresh interval");
|
|
1246
1644
|
this.state = this.createLocation(port);
|
|
1247
1645
|
this.locations.set(port.location.directory, this.state);
|
|
1646
|
+
this.models = new ModelsController(() => this.publish());
|
|
1248
1647
|
this.view = this.buildView();
|
|
1249
1648
|
}
|
|
1250
1649
|
createLocation(port) {
|
|
@@ -1294,6 +1693,7 @@ class DashboardController {
|
|
|
1294
1693
|
await this.refresh();
|
|
1295
1694
|
}
|
|
1296
1695
|
stop() {
|
|
1696
|
+
this.models.stop();
|
|
1297
1697
|
this.running = false;
|
|
1298
1698
|
this.generation++;
|
|
1299
1699
|
this.scope?.abort();
|
|
@@ -1328,6 +1728,7 @@ class DashboardController {
|
|
|
1328
1728
|
this.locations.delete(directory);
|
|
1329
1729
|
this.locations.set(directory, next);
|
|
1330
1730
|
this.state = next;
|
|
1731
|
+
this.configureModels();
|
|
1331
1732
|
if (this.locations.size > 4) {
|
|
1332
1733
|
const oldest = this.locations.keys().next().value;
|
|
1333
1734
|
if (oldest !== undefined)
|
|
@@ -1349,11 +1750,12 @@ class DashboardController {
|
|
|
1349
1750
|
this.publish();
|
|
1350
1751
|
}
|
|
1351
1752
|
setScreen(screen) {
|
|
1352
|
-
if (!["overview", "detail", "history"].includes(screen))
|
|
1753
|
+
if (!["overview", "detail", "history", "models"].includes(screen))
|
|
1353
1754
|
throw new Error("Invalid dashboard screen");
|
|
1354
1755
|
if (screen === this.screen)
|
|
1355
1756
|
return;
|
|
1356
1757
|
this.screen = screen;
|
|
1758
|
+
this.configureModels();
|
|
1357
1759
|
this.queueHistory();
|
|
1358
1760
|
this.publish();
|
|
1359
1761
|
}
|
|
@@ -1363,6 +1765,7 @@ class DashboardController {
|
|
|
1363
1765
|
if (windowMs === this.windowMs)
|
|
1364
1766
|
return;
|
|
1365
1767
|
this.windowMs = windowMs;
|
|
1768
|
+
this.configureModels();
|
|
1366
1769
|
this.queueHistory();
|
|
1367
1770
|
this.publish();
|
|
1368
1771
|
}
|
|
@@ -1375,11 +1778,18 @@ class DashboardController {
|
|
|
1375
1778
|
agent: filters.agent ?? this.filters.agent,
|
|
1376
1779
|
model: filters.model ?? this.filters.model
|
|
1377
1780
|
};
|
|
1781
|
+
this.configureModels();
|
|
1378
1782
|
this.publish();
|
|
1379
1783
|
}
|
|
1784
|
+
configureModels() {
|
|
1785
|
+
this.models.configure(this.state.port, this.running && this.screen === "models", this.windowMs, this.filters.agent, this.filters.model, this.serverNow());
|
|
1786
|
+
}
|
|
1380
1787
|
async refresh() {
|
|
1381
1788
|
if (!this.running || !this.scope)
|
|
1382
1789
|
return;
|
|
1790
|
+
this.configureModels();
|
|
1791
|
+
if (this.screen === "models")
|
|
1792
|
+
this.models.refresh(this.serverNow());
|
|
1383
1793
|
for (const cached of this.cache.values())
|
|
1384
1794
|
if (cached.directory === this.state.port.location.directory)
|
|
1385
1795
|
cached.valid = false;
|
|
@@ -1680,6 +2090,7 @@ class DashboardController {
|
|
|
1680
2090
|
"maximum-500-points-and-requests"
|
|
1681
2091
|
]
|
|
1682
2092
|
},
|
|
2093
|
+
models: this.models.getView(),
|
|
1683
2094
|
notices: [
|
|
1684
2095
|
...!state.complete ? ["partial-inventory"] : [],
|
|
1685
2096
|
...state.connection !== "live" ? ["live-data-stale"] : [],
|
|
@@ -1692,6 +2103,7 @@ var init_controller = __esm(() => {
|
|
|
1692
2103
|
init_rpc();
|
|
1693
2104
|
init_transport();
|
|
1694
2105
|
init_inventory();
|
|
2106
|
+
init_models();
|
|
1695
2107
|
init_view();
|
|
1696
2108
|
});
|
|
1697
2109
|
|
|
@@ -1844,6 +2256,19 @@ async function connectDashboard(options, discovery = Service) {
|
|
|
1844
2256
|
}
|
|
1845
2257
|
};
|
|
1846
2258
|
},
|
|
2259
|
+
async analytics(input, signal) {
|
|
2260
|
+
if (!isAnalyticsRead(input))
|
|
2261
|
+
throw new Error("Invalid analytics request");
|
|
2262
|
+
try {
|
|
2263
|
+
const result = await client.rpc(analyticsRpc).read(input, { location, signal: deadline(signal) });
|
|
2264
|
+
if (!isAnalyticsResult(result))
|
|
2265
|
+
throw new Error("Invalid analytics response");
|
|
2266
|
+
return result;
|
|
2267
|
+
} catch (error) {
|
|
2268
|
+
const missing = typeof error === "object" && error !== null && "type" in error && (error.type === "rpc.unavailable" || error.type === "rpc.method_not_found");
|
|
2269
|
+
throw new AnalyticsUnavailable(missing ? "unsupported" : "unavailable");
|
|
2270
|
+
}
|
|
2271
|
+
},
|
|
1847
2272
|
async history(input, signal) {
|
|
1848
2273
|
if (!isHistoryRead(input))
|
|
1849
2274
|
throw new Error("History requires one session, at most seven days and 16\u2013500 points");
|
|
@@ -1871,6 +2296,7 @@ async function connectDashboard(options, discovery = Service) {
|
|
|
1871
2296
|
}
|
|
1872
2297
|
var init_connection = __esm(() => {
|
|
1873
2298
|
init_rpc();
|
|
2299
|
+
init_port();
|
|
1874
2300
|
});
|
|
1875
2301
|
|
|
1876
2302
|
// src/tui/format.ts
|
|
@@ -1991,9 +2417,9 @@ var init_format = __esm(() => {
|
|
|
1991
2417
|
});
|
|
1992
2418
|
|
|
1993
2419
|
// src/tui/chartformat.ts
|
|
1994
|
-
function columns(points, from, to, width, gaps = []) {
|
|
2420
|
+
function columns(points, from, to, width, gaps = [], halfOpen = false) {
|
|
1995
2421
|
const size = Math.max(1, Math.min(120, Math.floor(width))), values = Array.from({ length: size });
|
|
1996
|
-
const column = (time) => Math.max(0, Math.min(size - 1, Math.floor((time - from) / Math.max(1, to - from) * (size - 1))));
|
|
2422
|
+
const column = (time) => Math.max(0, Math.min(size - 1, Math.floor((time - from) / Math.max(1, to - from) * (halfOpen ? size : size - 1))));
|
|
1997
2423
|
let min = Infinity, max = -Infinity;
|
|
1998
2424
|
for (const point of points) {
|
|
1999
2425
|
if (point.time < from || point.time > to || point.value === undefined || !Number.isFinite(point.value) || point.value < 0)
|
|
@@ -2017,12 +2443,12 @@ function sparkline(points, from, to, width, gaps = []) {
|
|
|
2017
2443
|
chars[column(time)] = "\u2506";
|
|
2018
2444
|
return { line: chars.join(""), ...min === Infinity ? {} : { min, max } };
|
|
2019
2445
|
}
|
|
2020
|
-
function metricPlot(points, from, to, width, gaps = []) {
|
|
2446
|
+
function metricPlot(points, from, to, width, gaps = [], halfOpen = false) {
|
|
2021
2447
|
const label = (value) => value >= 1e6 ? `${(value / 1e6).toFixed(1)}M` : value >= 1000 ? `${(value / 1000).toFixed(1)}k` : value.toFixed(1);
|
|
2022
2448
|
const peak = Math.max(0, ...points.filter((p) => p.time >= from && p.time <= to && Number.isFinite(p.value)).map((p) => p.value ?? 0));
|
|
2023
2449
|
const scale = Math.max(1, peak);
|
|
2024
2450
|
const gutter = width >= 20 ? Math.max(label(scale).length, 3) : 0;
|
|
2025
|
-
const { values, column } = columns(points, from, to, Math.max(1, width - gutter - 2), gaps);
|
|
2451
|
+
const { values, column } = columns(points, from, to, Math.max(1, halfOpen ? Math.min(points.length, width - gutter - 2) : width - gutter - 2), gaps, halfOpen);
|
|
2026
2452
|
const gapColumns = new Set(gaps.filter((time) => time >= from && time <= to).map(column));
|
|
2027
2453
|
const height = 6;
|
|
2028
2454
|
const blocks = " \u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
|
|
@@ -2099,6 +2525,108 @@ var init_chartformat = __esm(() => {
|
|
|
2099
2525
|
];
|
|
2100
2526
|
});
|
|
2101
2527
|
|
|
2528
|
+
// src/dashboard/modelview.ts
|
|
2529
|
+
function metricValue(summary, metric2) {
|
|
2530
|
+
if (metric2.key === "outputTokens" && !summary?.tpsRequests)
|
|
2531
|
+
return;
|
|
2532
|
+
const value = summary?.[metric2.key];
|
|
2533
|
+
return value === undefined ? undefined : value / metric2.divisor;
|
|
2534
|
+
}
|
|
2535
|
+
function terminalModelRow(model, focused) {
|
|
2536
|
+
return [
|
|
2537
|
+
`${focused ? "> " : " "}${model.model ?? "unknown model attribution"}`,
|
|
2538
|
+
` ${stats(model)}`,
|
|
2539
|
+
` TPS eligible ${model.tpsRequests}; TTFT eligible ${model.ttftRequests}; partial ${model.partialRequests}; legacy attribution ${model.legacyAttributionRequests}`
|
|
2540
|
+
];
|
|
2541
|
+
}
|
|
2542
|
+
function terminalModels(view, width, detail, metricIndex, sample, focus, requests) {
|
|
2543
|
+
const state = view.models;
|
|
2544
|
+
const result = state.result;
|
|
2545
|
+
const lines = [
|
|
2546
|
+
`Models \xB7 ${state.status}${state.stale ? " \xB7 STALE cached results" : ""} \xB7 ${state.windowMs / 60000} minutes \xB7 retained history`,
|
|
2547
|
+
`Agent: ${(result?.agent ?? view.filters.agent) || "all"} \xB7 sort ${state.sort} \xB7 location ${view.location}`,
|
|
2548
|
+
"4 Models \xB7 Enter chart \xB7 Esc model list/overview \xB7 w window \xB7 f agent \xB7 r refresh"
|
|
2549
|
+
];
|
|
2550
|
+
if (state.status === "unsupported")
|
|
2551
|
+
return [
|
|
2552
|
+
...lines,
|
|
2553
|
+
"Models requires a server with context-guard.analytics.v1; update the server plugin. Overview and session history remain available."
|
|
2554
|
+
];
|
|
2555
|
+
if (state.status === "disabled")
|
|
2556
|
+
return [
|
|
2557
|
+
...lines,
|
|
2558
|
+
"Retained performance recording is disabled on this server."
|
|
2559
|
+
];
|
|
2560
|
+
if (!result)
|
|
2561
|
+
return [
|
|
2562
|
+
...lines,
|
|
2563
|
+
state.status === "loading" ? "Loading model aggregates\u2026" : "Model analytics unavailable; press r to retry."
|
|
2564
|
+
];
|
|
2565
|
+
lines.push(`${new Date(result.from).toISOString()} \u2192 ${new Date(result.to).toISOString()} (UTC; completion time)`);
|
|
2566
|
+
const partial = result.coverage.partial || result.metadata.skippedBytes > 0 || result.metadata.malformedRecords > 0 || result.gaps.length > 0 || result.omittedGaps > 0;
|
|
2567
|
+
lines.push(`Coverage: ${partial ? "PARTIAL" : "bounded observations"}; 64 MiB scan limit; ${result.coverage.maxRequests} request identities maximum; no pre-observation reconstruction.`);
|
|
2568
|
+
lines.push(`Archive retained since ${result.metadata.retainedFrom === undefined ? "unknown" : new Date(result.metadata.retainedFrom).toISOString()}; snapshot observed at ${new Date(result.generatedAt).toISOString()}.`);
|
|
2569
|
+
lines.push(`Scan: ${result.coverage.scannedRecords} records; ${result.metadata.skippedBytes} bytes skipped; ${result.metadata.malformedRecords} malformed; ${result.coverage.conflictingRequests} conflicting; ${result.coverage.incompleteRequests} incomplete; ${result.coverage.invalidTimingRequests} invalid timing; gaps ${result.gaps.length}${result.omittedGaps ? ` (+${result.omittedGaps} omitted)` : ""}.`);
|
|
2570
|
+
if (!detail) {
|
|
2571
|
+
lines.push(`Model page ${state.modelPage + 1} \xB7 ${result.totalModels} models \xB7 Up/Down scroll/select \xB7 n/p pages \xB7 s sort`);
|
|
2572
|
+
if (!result.models.length)
|
|
2573
|
+
lines.push("No completed requests match this range and agent filter.");
|
|
2574
|
+
result.models.forEach((model, index) => {
|
|
2575
|
+
lines.push(...terminalModelRow(model, index === focus));
|
|
2576
|
+
});
|
|
2577
|
+
return lines;
|
|
2578
|
+
}
|
|
2579
|
+
const selected = result.selected;
|
|
2580
|
+
if (!selected)
|
|
2581
|
+
return [
|
|
2582
|
+
...lines,
|
|
2583
|
+
"No completed requests for this model in the selected range."
|
|
2584
|
+
];
|
|
2585
|
+
lines.push(`Model: ${selected.model ?? "unknown model attribution"}`, stats(selected));
|
|
2586
|
+
lines.push(`Reported output ${number(selected.tpsRequests ? selected.outputTokens : undefined)} tokens; matched duration ${number(selected.tpsRequests ? selected.durationMs / 1000 : undefined)}s; TPS eligible/excluded ${selected.tpsRequests}/${selected.excludedTpsRequests}; TTFT eligible/excluded ${selected.ttftRequests}/${selected.excludedTtftRequests}; partial ${selected.partialRequests}; legacy attribution ${selected.legacyAttributionRequests}`);
|
|
2587
|
+
if (requests) {
|
|
2588
|
+
lines.push(`Request page ${state.requestPage + 1} \xB7 n/p pages \xB7 d chart \xB7 completed stream \u2260 task success`);
|
|
2589
|
+
result.requests.forEach((r) => lines.push(`Request ${r.requestId} \xB7 session ${r.sessionId}`, `Completed ${new Date(r.completedAt).toISOString()} \xB7 ${r.agent ?? "unknown agent"} \xB7 output ${number(r.outputTokens)} \xB7 duration ${number((r.completedAt - r.startedAt) / 1000)}s \xB7 TTFT ${number(r.ttftMs === undefined ? undefined : r.ttftMs / 1000)}s ~ \xB7 ${r.attribution}${r.partial ? " \xB7 partial" : ""}`));
|
|
2590
|
+
return lines;
|
|
2591
|
+
}
|
|
2592
|
+
const metric2 = modelMetrics[metricIndex] ?? modelMetrics[0];
|
|
2593
|
+
lines.push(`\u2039 ${metric2.label} (${metric2.unit}) \u203A \xB7 Left/Right metric \xB7 [/] bucket \xB7 d requests`);
|
|
2594
|
+
lines.push(...metricPlot(result.buckets.map((bucket2) => {
|
|
2595
|
+
const value = metricValue(bucket2.summary, metric2);
|
|
2596
|
+
return {
|
|
2597
|
+
time: (bucket2.from + bucket2.to) / 2,
|
|
2598
|
+
...value === undefined ? {} : { value }
|
|
2599
|
+
};
|
|
2600
|
+
}), result.from, result.to, width, result.gaps, true));
|
|
2601
|
+
lines.push("UTC buckets \xB7 blank = no eligible observations \xB7 \u2506 = retained gap; partial coverage may hide other gaps.");
|
|
2602
|
+
lines.push("TPS = reported output / summed matched request durations (includes TTFT); concurrent durations add. In-flight requests enter after stream completion.");
|
|
2603
|
+
const bucket = result.buckets[Math.min(sample, Math.max(0, result.buckets.length - 1))];
|
|
2604
|
+
if (bucket) {
|
|
2605
|
+
lines.push(`Bucket ${Math.min(sample + 1, result.buckets.length)}/${result.buckets.length}: ${new Date(bucket.from).toISOString()} \u2192 ${new Date(bucket.to).toISOString()}`);
|
|
2606
|
+
lines.push(bucket.summary ? stats(bucket.summary) : "No eligible observations; measurements unknown.");
|
|
2607
|
+
lines.push(`Selected ${metric2.label}: ${number(metricValue(bucket.summary, metric2))} ${metric2.unit}`);
|
|
2608
|
+
if (bucket.summary)
|
|
2609
|
+
lines.push(`Bucket eligibility: TPS ${bucket.summary.tpsRequests}/${bucket.summary.completedRequests}; TTFT ${bucket.summary.ttftRequests}/${bucket.summary.completedRequests}; partial ${bucket.summary.partialRequests}`);
|
|
2610
|
+
}
|
|
2611
|
+
return lines;
|
|
2612
|
+
}
|
|
2613
|
+
var modelMetrics, number = (n) => n === undefined ? "?" : Number.isInteger(n) ? String(n) : n.toFixed(2), stats = (m) => `${m.sessions} sessions \xB7 ${m.completedRequests} completed \xB7 weighted TPS ${number(m.averageTps)} \xB7 TTFT median/p95 ${number(m.medianTtftMs === undefined ? undefined : m.medianTtftMs / 1000)}/${number(m.p95TtftMs === undefined ? undefined : m.p95TtftMs / 1000)}s ~`;
|
|
2614
|
+
var init_modelview = __esm(() => {
|
|
2615
|
+
init_chartformat();
|
|
2616
|
+
modelMetrics = [
|
|
2617
|
+
{ key: "averageTps", label: "Weighted request TPS", unit: "t/s", divisor: 1 },
|
|
2618
|
+
{ key: "medianTtftMs", label: "TTFT median ~", unit: "s", divisor: 1000 },
|
|
2619
|
+
{ key: "p95TtftMs", label: "TTFT p95 ~", unit: "s", divisor: 1000 },
|
|
2620
|
+
{
|
|
2621
|
+
key: "completedRequests",
|
|
2622
|
+
label: "Completed requests",
|
|
2623
|
+
unit: "requests",
|
|
2624
|
+
divisor: 1
|
|
2625
|
+
},
|
|
2626
|
+
{ key: "outputTokens", label: "Reported output", unit: "tokens", divisor: 1 }
|
|
2627
|
+
];
|
|
2628
|
+
});
|
|
2629
|
+
|
|
2102
2630
|
// src/dashboard/terminalview.ts
|
|
2103
2631
|
function overviewRow(row2) {
|
|
2104
2632
|
const s = row2.snapshot;
|
|
@@ -2181,7 +2709,11 @@ function terminalHistory(view, width, metricIndex, sample) {
|
|
|
2181
2709
|
];
|
|
2182
2710
|
}
|
|
2183
2711
|
function wrapped(lines, width) {
|
|
2184
|
-
|
|
2712
|
+
const available = Math.max(1, width);
|
|
2713
|
+
return lines.flatMap((line) => {
|
|
2714
|
+
const clean = clip(line, Math.max(available, cells(line)));
|
|
2715
|
+
return cells(clean) <= available ? [clean] : wrap(clean, available);
|
|
2716
|
+
});
|
|
2185
2717
|
}
|
|
2186
2718
|
function headerLines(view, width) {
|
|
2187
2719
|
return [
|
|
@@ -2261,7 +2793,10 @@ var init_terminalview = __esm(() => {
|
|
|
2261
2793
|
];
|
|
2262
2794
|
terminalHelp = [
|
|
2263
2795
|
"Context Guard standalone dashboard \u2014 keyboard help",
|
|
2264
|
-
"1 overview \xB7 2 detail \xB7 3 history \xB7 Enter detail",
|
|
2796
|
+
"1 overview \xB7 2 detail \xB7 3 history \xB7 4 models \xB7 Enter detail/chart",
|
|
2797
|
+
"Models: Up/Down scroll tall rows, then select \xB7 Enter aggregate chart \xB7 Esc model list \xB7 s sort",
|
|
2798
|
+
"Models charts: Left/Right metric \xB7 w window \xB7 [/] bucket \xB7 d request details",
|
|
2799
|
+
"Models list/request details: n/p pages \xB7 f exact agent filter \xB7 r refresh",
|
|
2265
2800
|
"Up/Down or j/k: select session in overview; scroll other screens",
|
|
2266
2801
|
"PageUp/PageDown: move by a page \xB7 Home/End: first/last",
|
|
2267
2802
|
"Left/Right: history metric (six metrics) \xB7 w: cycle 5m/1h/24h/7d",
|
|
@@ -2291,6 +2826,11 @@ class TerminalShell {
|
|
|
2291
2826
|
scroll = 0;
|
|
2292
2827
|
metric = 0;
|
|
2293
2828
|
sample = 0;
|
|
2829
|
+
modelDetail = false;
|
|
2830
|
+
modelRequests = false;
|
|
2831
|
+
modelFocus = 0;
|
|
2832
|
+
modelFocusId;
|
|
2833
|
+
modelViewport;
|
|
2294
2834
|
help = false;
|
|
2295
2835
|
editor;
|
|
2296
2836
|
input = "";
|
|
@@ -2336,7 +2876,7 @@ class TerminalShell {
|
|
|
2336
2876
|
this.task(this.controller.selectLocation(this.input));
|
|
2337
2877
|
} else
|
|
2338
2878
|
this.controller.setFilters({ [editor]: this.input });
|
|
2339
|
-
this.editor = editor === "agent" ? "model" : undefined;
|
|
2879
|
+
this.editor = editor === "agent" && view.screen !== "models" ? "model" : undefined;
|
|
2340
2880
|
this.input = this.editor === "model" ? view.filters.model : "";
|
|
2341
2881
|
this.scroll = 0;
|
|
2342
2882
|
this.notice = "";
|
|
@@ -2356,6 +2896,11 @@ class TerminalShell {
|
|
|
2356
2896
|
if (name === "escape") {
|
|
2357
2897
|
this.help = false;
|
|
2358
2898
|
this.scroll = 0;
|
|
2899
|
+
if (view.screen === "models" && this.modelDetail) {
|
|
2900
|
+
this.modelDetail = false;
|
|
2901
|
+
this.modelRequests = false;
|
|
2902
|
+
return;
|
|
2903
|
+
}
|
|
2359
2904
|
this.controller.setScreen("overview");
|
|
2360
2905
|
return;
|
|
2361
2906
|
}
|
|
@@ -2378,6 +2923,81 @@ class TerminalShell {
|
|
|
2378
2923
|
this.task(this.controller.refresh());
|
|
2379
2924
|
return;
|
|
2380
2925
|
}
|
|
2926
|
+
if (name === "4") {
|
|
2927
|
+
this.help = false;
|
|
2928
|
+
this.scroll = 0;
|
|
2929
|
+
this.modelDetail = false;
|
|
2930
|
+
this.modelRequests = false;
|
|
2931
|
+
this.controller.setScreen("models");
|
|
2932
|
+
return;
|
|
2933
|
+
}
|
|
2934
|
+
if (view.screen === "models" && !this.help) {
|
|
2935
|
+
if (name === "enter" || name === "return") {
|
|
2936
|
+
const model = view.models.result?.models[this.modelFocus];
|
|
2937
|
+
if (model) {
|
|
2938
|
+
this.controller.models.select(model.model ?? "");
|
|
2939
|
+
this.modelDetail = true;
|
|
2940
|
+
this.modelRequests = false;
|
|
2941
|
+
this.sample = 0;
|
|
2942
|
+
this.scroll = 0;
|
|
2943
|
+
}
|
|
2944
|
+
return;
|
|
2945
|
+
}
|
|
2946
|
+
if (name === "w") {
|
|
2947
|
+
this.controller.setWindow(historyWindows[(historyWindows.indexOf(view.models.windowMs) + 1) % historyWindows.length] ?? historyWindows[0]);
|
|
2948
|
+
this.sample = 0;
|
|
2949
|
+
this.scroll = 0;
|
|
2950
|
+
return;
|
|
2951
|
+
}
|
|
2952
|
+
if (name === "s" && !this.modelDetail) {
|
|
2953
|
+
this.controller.models.sort();
|
|
2954
|
+
this.scroll = 0;
|
|
2955
|
+
return;
|
|
2956
|
+
}
|
|
2957
|
+
if (name === "n" || name === "p") {
|
|
2958
|
+
if (!this.modelDetail || this.modelRequests) {
|
|
2959
|
+
this.controller.models.page(this.modelDetail ? "requests" : "models", name === "n" ? 1 : -1);
|
|
2960
|
+
if (!this.modelDetail) {
|
|
2961
|
+
this.modelFocus = 0;
|
|
2962
|
+
this.modelFocusId = undefined;
|
|
2963
|
+
}
|
|
2964
|
+
this.scroll = 0;
|
|
2965
|
+
}
|
|
2966
|
+
return;
|
|
2967
|
+
}
|
|
2968
|
+
if (this.modelDetail) {
|
|
2969
|
+
if (name === "d") {
|
|
2970
|
+
this.modelRequests = !this.modelRequests;
|
|
2971
|
+
this.scroll = 0;
|
|
2972
|
+
return;
|
|
2973
|
+
}
|
|
2974
|
+
if (name === "left" || name === "right") {
|
|
2975
|
+
this.metric = (this.metric + modelMetrics.length + (name === "left" ? -1 : 1)) % modelMetrics.length;
|
|
2976
|
+
this.sample = 0;
|
|
2977
|
+
this.scroll = 0;
|
|
2978
|
+
return;
|
|
2979
|
+
}
|
|
2980
|
+
if (name === "[" || name === "]") {
|
|
2981
|
+
this.sample = Math.max(0, Math.min((view.models.result?.buckets.length ?? 1) - 1, this.sample + (name === "[" ? -1 : 1)));
|
|
2982
|
+
return;
|
|
2983
|
+
}
|
|
2984
|
+
} else if (["up", "down", "j", "k", "home", "end", "pageup", "pagedown"].includes(name)) {
|
|
2985
|
+
const models = view.models.result?.models ?? [];
|
|
2986
|
+
const delta2 = name === "down" || name === "j" ? 1 : name === "up" || name === "k" ? -1 : name === "pagedown" ? page : -page;
|
|
2987
|
+
const viewport = this.modelViewport;
|
|
2988
|
+
if (viewport?.focus === this.modelFocus && viewport.end - viewport.start > viewport.height && name !== "home" && name !== "end" && (delta2 > 0 ? this.scroll + viewport.height < viewport.end : this.scroll > viewport.start)) {
|
|
2989
|
+
this.scroll = Math.max(viewport.start, Math.min(viewport.end - viewport.height, this.scroll + delta2));
|
|
2990
|
+
return;
|
|
2991
|
+
}
|
|
2992
|
+
if (name === "home")
|
|
2993
|
+
this.scroll = 0;
|
|
2994
|
+
if (name === "end")
|
|
2995
|
+
this.scroll = Number.MAX_SAFE_INTEGER;
|
|
2996
|
+
this.modelFocus = Math.max(0, Math.min(models.length - 1, name === "home" ? 0 : name === "end" ? models.length - 1 : this.modelFocus + delta2));
|
|
2997
|
+
this.modelFocusId = models[this.modelFocus]?.model ?? "";
|
|
2998
|
+
return;
|
|
2999
|
+
}
|
|
3000
|
+
}
|
|
2381
3001
|
if (["1", "2", "3", "return", "enter"].includes(name)) {
|
|
2382
3002
|
this.help = false;
|
|
2383
3003
|
this.scroll = 0;
|
|
@@ -2414,14 +3034,28 @@ class TerminalShell {
|
|
|
2414
3034
|
frame(width, height) {
|
|
2415
3035
|
width = Math.max(1, width);
|
|
2416
3036
|
height = Math.max(1, height);
|
|
3037
|
+
this.controller.models.setWidth(width);
|
|
2417
3038
|
const view = this.controller.getView();
|
|
2418
|
-
|
|
3039
|
+
if (view.screen === "models") {
|
|
3040
|
+
const models = view.models.result?.models ?? [];
|
|
3041
|
+
const identity2 = this.modelFocusId ?? view.models.result?.selected?.model ?? "";
|
|
3042
|
+
const focused = models.findIndex((m) => (m.model ?? "") === identity2);
|
|
3043
|
+
this.modelFocus = Math.max(0, focused < 0 ? Math.min(this.modelFocus, models.length - 1) : focused);
|
|
3044
|
+
if (view.models.status === "ready" && models.length)
|
|
3045
|
+
this.modelFocusId = models[this.modelFocus]?.model ?? "";
|
|
3046
|
+
}
|
|
3047
|
+
const identity = view.screen === "models" ? JSON.stringify([
|
|
3048
|
+
view.location,
|
|
3049
|
+
view.models.windowMs,
|
|
3050
|
+
view.filters.agent,
|
|
3051
|
+
view.models.result?.selected?.model
|
|
3052
|
+
]) : `${view.location}\0${view.selectedSessionId}\0${view.history.windowMs}`;
|
|
2419
3053
|
if (identity !== this.historyIdentity) {
|
|
2420
3054
|
this.sample = 0;
|
|
2421
3055
|
this.scroll = 0;
|
|
2422
3056
|
this.historyIdentity = identity;
|
|
2423
3057
|
}
|
|
2424
|
-
const footer = this.editor ? `${this.editor}> ${this.input} [Enter apply; Esc cancel${this.editor === "location" ? "; Tab remembered" : ""}]` : "? help q quit \xB7 1 overview 2 detail 3 history \xB7 / search f filters l location r refresh";
|
|
3058
|
+
const footer = this.editor ? `${this.editor}> ${this.input} [Enter apply; Esc cancel${this.editor === "location" ? "; Tab remembered" : ""}]` : "? help q quit \xB7 1 overview 2 detail 3 history 4 models \xB7 / search f filters l location r refresh";
|
|
2425
3059
|
if (height < 5)
|
|
2426
3060
|
return [
|
|
2427
3061
|
clip("Context Guard \xB7 ? help \xB7 q quit", width),
|
|
@@ -2434,7 +3068,7 @@ class TerminalShell {
|
|
|
2434
3068
|
...view.inventory.notices,
|
|
2435
3069
|
...view.health?.reasons ?? [],
|
|
2436
3070
|
...this.notice ? [this.notice] : [],
|
|
2437
|
-
...Object.values(view.filters).some(Boolean) ? [
|
|
3071
|
+
...view.screen !== "models" && Object.values(view.filters).some(Boolean) ? [
|
|
2438
3072
|
`Filters: search=${view.filters.search} agent=${view.filters.agent} model=${view.filters.model}`
|
|
2439
3073
|
] : []
|
|
2440
3074
|
];
|
|
@@ -2469,8 +3103,28 @@ class TerminalShell {
|
|
|
2469
3103
|
} else
|
|
2470
3104
|
body = wrapped([
|
|
2471
3105
|
...notices,
|
|
2472
|
-
...view.screen === "detail" ? terminalDetail(view.selected) : terminalHistory(view, width, this.metric, this.sample)
|
|
3106
|
+
...view.screen === "detail" ? terminalDetail(view.selected) : view.screen === "models" ? terminalModels(view, width, this.modelDetail, this.metric, this.sample, this.modelFocus, this.modelRequests) : terminalHistory(view, width, this.metric, this.sample)
|
|
2473
3107
|
], width);
|
|
3108
|
+
this.modelViewport = undefined;
|
|
3109
|
+
if (view.screen === "models" && !this.modelDetail && !this.help && view.models.result?.models.length) {
|
|
3110
|
+
const heights = (view.models.result?.models ?? []).map((model, index) => wrapped(terminalModelRow(model, index === this.modelFocus), width).length);
|
|
3111
|
+
const start = body.length - heights.slice(this.modelFocus).reduce((a, b) => a + b, 0);
|
|
3112
|
+
const end = start + (heights[this.modelFocus] ?? 0);
|
|
3113
|
+
this.modelViewport = {
|
|
3114
|
+
focus: this.modelFocus,
|
|
3115
|
+
start,
|
|
3116
|
+
end,
|
|
3117
|
+
height: available
|
|
3118
|
+
};
|
|
3119
|
+
if (end - start > available) {
|
|
3120
|
+
this.scroll = Math.max(start, Math.min(this.scroll, end - available));
|
|
3121
|
+
} else {
|
|
3122
|
+
if (start < this.scroll)
|
|
3123
|
+
this.scroll = start;
|
|
3124
|
+
if (end > this.scroll + available)
|
|
3125
|
+
this.scroll = end - available;
|
|
3126
|
+
}
|
|
3127
|
+
}
|
|
2474
3128
|
this.scroll = Math.max(0, Math.min(this.scroll, Math.max(0, body.length - available)));
|
|
2475
3129
|
return [
|
|
2476
3130
|
...header,
|
|
@@ -2600,6 +3254,7 @@ var init_terminal = __esm(() => {
|
|
|
2600
3254
|
init_connection();
|
|
2601
3255
|
init_command();
|
|
2602
3256
|
init_view();
|
|
3257
|
+
init_modelview();
|
|
2603
3258
|
init_format();
|
|
2604
3259
|
init_terminalview();
|
|
2605
3260
|
});
|
|
@@ -2647,7 +3302,7 @@ function reconcile(a, b) {
|
|
|
2647
3302
|
partial: a.partial || b.partial
|
|
2648
3303
|
};
|
|
2649
3304
|
}
|
|
2650
|
-
function
|
|
3305
|
+
function analyzeModelRequests(records, options) {
|
|
2651
3306
|
const maxRequests = options.maxRequests ?? 1e5;
|
|
2652
3307
|
if (!Number.isSafeInteger(maxRequests) || maxRequests < 1 || maxRequests > 1e6)
|
|
2653
3308
|
throw new Error("maxRequests must be an integer from 1 to 1000000");
|
|
@@ -2708,7 +3363,7 @@ function aggregateModels(records, options) {
|
|
|
2708
3363
|
requests.set(key, { fact, conflict: false });
|
|
2709
3364
|
}
|
|
2710
3365
|
}
|
|
2711
|
-
const
|
|
3366
|
+
const accepted = [];
|
|
2712
3367
|
for (const { fact: r, conflict } of requests.values()) {
|
|
2713
3368
|
if (conflict) {
|
|
2714
3369
|
coverage.conflictingRequests++;
|
|
@@ -2725,6 +3380,22 @@ function aggregateModels(records, options) {
|
|
|
2725
3380
|
}
|
|
2726
3381
|
if (options.from !== undefined && r.completedAt < options.from || options.to !== undefined && r.completedAt >= options.to || options.model !== undefined && r.model !== options.model || options.agent !== undefined && r.agent !== options.agent)
|
|
2727
3382
|
continue;
|
|
3383
|
+
accepted.push({ ...r, completedAt: r.completedAt });
|
|
3384
|
+
}
|
|
3385
|
+
const models = summarizeModelRequests(accepted);
|
|
3386
|
+
coverage.partial = coverage.unscannedRecords > 0 || coverage.conflictingRequests > 0 || coverage.missingIdentityRecords > 0 || coverage.incompleteRequests > 0 || coverage.invalidTimingRequests > 0 || coverage.gapRecords > 0 || models.some((s) => s.partialRequests > 0 || s.legacyAttributionRequests > 0 || s.excludedTpsRequests > 0 || s.excludedTtftRequests > 0 || !s.model);
|
|
3387
|
+
return {
|
|
3388
|
+
schemaVersion: 1,
|
|
3389
|
+
location: options.location,
|
|
3390
|
+
models,
|
|
3391
|
+
coverage,
|
|
3392
|
+
requests: accepted
|
|
3393
|
+
};
|
|
3394
|
+
}
|
|
3395
|
+
function summarizeModelRequests(requests) {
|
|
3396
|
+
const groups = new Map;
|
|
3397
|
+
for (const r of requests) {
|
|
3398
|
+
const duration = r.completedAt - r.startedAt;
|
|
2728
3399
|
let group = groups.get(r.model);
|
|
2729
3400
|
if (!group) {
|
|
2730
3401
|
group = {
|
|
@@ -2779,8 +3450,11 @@ function aggregateModels(records, options) {
|
|
|
2779
3450
|
}
|
|
2780
3451
|
return s;
|
|
2781
3452
|
}).sort((a, b) => (a.model ?? "").localeCompare(b.model ?? ""));
|
|
2782
|
-
|
|
2783
|
-
|
|
3453
|
+
return models;
|
|
3454
|
+
}
|
|
3455
|
+
function aggregateModels(records, options) {
|
|
3456
|
+
const { requests: _requests, ...result } = analyzeModelRequests(records, options);
|
|
3457
|
+
return result;
|
|
2784
3458
|
}
|
|
2785
3459
|
var nonnegative2 = (n) => n !== undefined && Number.isFinite(n) && n >= 0;
|
|
2786
3460
|
|
|
@@ -2793,7 +3467,7 @@ import { isAbsolute as isAbsolute4, resolve as resolve2 } from "path";
|
|
|
2793
3467
|
async function inspectModels(args) {
|
|
2794
3468
|
const directory = args[0];
|
|
2795
3469
|
if (!directory || !isAbsolute4(directory))
|
|
2796
|
-
throw new Error("
|
|
3470
|
+
throw new Error("Use context-guard models --help for usage and examples; the history directory must be an absolute path");
|
|
2797
3471
|
const values = new Map;
|
|
2798
3472
|
let csv = false, json = false;
|
|
2799
3473
|
for (let i = 1;i < args.length; i++) {
|
|
@@ -3592,9 +4266,124 @@ async function readJournal(path, maxBytes = 16 * 1024 * 1024) {
|
|
|
3592
4266
|
return records;
|
|
3593
4267
|
}
|
|
3594
4268
|
|
|
4269
|
+
// src/clihelp.ts
|
|
4270
|
+
var mainHelp = `Context Guard \u2014 context monitoring for OpenCode V2
|
|
4271
|
+
|
|
4272
|
+
Start here
|
|
4273
|
+
In your project, run: opencode
|
|
4274
|
+
Inside OpenCode, enter: /context-guard
|
|
4275
|
+
The plugin loads with OpenCode; observe mode is the default.
|
|
4276
|
+
|
|
4277
|
+
Usage: context-guard <command> [options]
|
|
4278
|
+
context-guard <journal.jsonl> [--history | --json]
|
|
4279
|
+
|
|
4280
|
+
Commands
|
|
4281
|
+
dashboard tui Open the standalone live terminal dashboard
|
|
4282
|
+
models Compare models across retained history
|
|
4283
|
+
metrics Export retained performance records as JSON or CSV
|
|
4284
|
+
charts Generate an interactive HTML report
|
|
4285
|
+
<journal.jsonl> Inspect diagnostic session records
|
|
4286
|
+
|
|
4287
|
+
Examples
|
|
4288
|
+
context-guard dashboard tui
|
|
4289
|
+
context-guard models /absolute/history-directory
|
|
4290
|
+
context-guard charts /absolute/history-directory --output report.html
|
|
4291
|
+
context-guard /absolute/events.jsonl --history
|
|
4292
|
+
|
|
4293
|
+
Help
|
|
4294
|
+
context-guard <command> --help Show options and examples
|
|
4295
|
+
context-guard help journal Explain diagnostic inspection
|
|
4296
|
+
-h, --help Show help
|
|
4297
|
+
|
|
4298
|
+
Requires Bun >=1.1.34; the standalone dashboard requires Bun >=1.3.0.
|
|
4299
|
+
From a source checkout, replace context-guard with bun run inspect.`;
|
|
4300
|
+
var filterOptions = ` --agent NAME Filter by exact agent name
|
|
4301
|
+
--model PROVIDER/MODEL
|
|
4302
|
+
Filter by exact model ID
|
|
4303
|
+
--from ISO-or-ms Start time: ISO timestamp or Unix milliseconds
|
|
4304
|
+
--to ISO-or-ms End time: ISO timestamp or Unix milliseconds`;
|
|
4305
|
+
var historyOptions = ` --session ID Filter by session
|
|
4306
|
+
${filterOptions}`;
|
|
4307
|
+
var historyPath = `Use the owning location's history.path directory, not its diagnostic JSONL
|
|
4308
|
+
file. By default it is a location-hashed directory under
|
|
4309
|
+
\$XDG_STATE_HOME/opencode-context-guard (or ~/.local/state/opencode-context-guard).
|
|
4310
|
+
A custom telemetry.path does not change the history directory.`;
|
|
4311
|
+
var commandHelp = {
|
|
4312
|
+
models: `Usage: context-guard models <history-directory> [options]
|
|
4313
|
+
Compare model throughput and time to first token across retained sessions.
|
|
4314
|
+
The history directory must be an absolute path. Output defaults to JSON.
|
|
4315
|
+
|
|
4316
|
+
Options
|
|
4317
|
+
${filterOptions}
|
|
4318
|
+
--csv Export CSV; coverage metadata goes to stderr
|
|
4319
|
+
--json Export JSON (default); cannot combine with --csv
|
|
4320
|
+
-h, --help Show this help
|
|
4321
|
+
|
|
4322
|
+
Example
|
|
4323
|
+
context-guard models /absolute/history-directory --agent build --csv
|
|
4324
|
+
|
|
4325
|
+
${historyPath}`,
|
|
4326
|
+
metrics: `Usage: context-guard metrics <history-directory> [options]
|
|
4327
|
+
Read retained performance records. Output defaults to JSON.
|
|
4328
|
+
|
|
4329
|
+
Options
|
|
4330
|
+
${historyOptions}
|
|
4331
|
+
--csv Export CSV; coverage metadata goes to stderr
|
|
4332
|
+
--chart FILE.html Generate an HTML report instead (see charts --help)
|
|
4333
|
+
-h, --help Show this help
|
|
4334
|
+
|
|
4335
|
+
Example
|
|
4336
|
+
context-guard metrics /absolute/history-directory --csv > metrics.csv
|
|
4337
|
+
|
|
4338
|
+
${historyPath}`,
|
|
4339
|
+
charts: `Usage: context-guard charts <history-directory> [options]
|
|
4340
|
+
Generate a self-contained interactive HTML report, then open it in a browser.
|
|
4341
|
+
|
|
4342
|
+
Options
|
|
4343
|
+
${historyOptions}
|
|
4344
|
+
--output FILE.html Write the report (default: context-guard-charts.html)
|
|
4345
|
+
--max-points N Points per series: 16\u20135000 (default: 1000)
|
|
4346
|
+
--max-sessions N Session instances: 1\u20131024 (default: 128)
|
|
4347
|
+
--force Overwrite an existing report
|
|
4348
|
+
-h, --help Show this help
|
|
4349
|
+
|
|
4350
|
+
Example
|
|
4351
|
+
context-guard charts /absolute/history-directory --output report.html
|
|
4352
|
+
|
|
4353
|
+
${historyPath}`,
|
|
4354
|
+
journal: `Usage: context-guard <journal.jsonl> [options]
|
|
4355
|
+
Inspect the diagnostic journal reported in OpenCode's Context Guard startup log.
|
|
4356
|
+
By default, print the latest retained record for each session, one JSON per line.
|
|
4357
|
+
|
|
4358
|
+
Options
|
|
4359
|
+
--json Print latest session records as a JSON array
|
|
4360
|
+
--history Print all retained journal records as a JSON array
|
|
4361
|
+
-h, --help Show this help
|
|
4362
|
+
|
|
4363
|
+
Example
|
|
4364
|
+
context-guard /absolute/path/from/startup.jsonl --history
|
|
4365
|
+
|
|
4366
|
+
This reads a diagnostic file, not a performance history directory.`
|
|
4367
|
+
};
|
|
4368
|
+
|
|
3595
4369
|
// src/cli.ts
|
|
3596
4370
|
var args = process.argv.slice(2);
|
|
3597
|
-
|
|
4371
|
+
var helpRequested = args.some((arg) => arg === "--help" || arg === "-h");
|
|
4372
|
+
var helpTopic = args[0] === "help" ? args[1] : args[0];
|
|
4373
|
+
if (!args[0] || args[0] === "help" || helpRequested || args.length === 1 && (args[0] === "dashboard" || Object.hasOwn(commandHelp, args[0]))) {
|
|
4374
|
+
if (helpTopic === "dashboard") {
|
|
4375
|
+
const { dashboardHelp: dashboardHelp2 } = await Promise.resolve().then(() => (init_command(), exports_command));
|
|
4376
|
+
console.log(dashboardHelp2);
|
|
4377
|
+
} else if (helpTopic && Object.hasOwn(commandHelp, helpTopic)) {
|
|
4378
|
+
console.log(commandHelp[helpTopic]);
|
|
4379
|
+
} else if (args[0] === "help" && helpTopic) {
|
|
4380
|
+
console.error(`Unknown help topic. Use context-guard --help for commands.`);
|
|
4381
|
+
process.exitCode = 1;
|
|
4382
|
+
} else if (helpRequested && helpTopic && !helpTopic.startsWith("-")) {
|
|
4383
|
+
console.log(commandHelp.journal);
|
|
4384
|
+
} else
|
|
4385
|
+
console.log(mainHelp);
|
|
4386
|
+
} else if (args[0] === "dashboard") {
|
|
3598
4387
|
const { parseDashboardArgs: parseDashboardArgs2, dashboardHelp: dashboardHelp2, checkTerminalRuntime: checkTerminalRuntime2 } = await Promise.resolve().then(() => (init_command(), exports_command));
|
|
3599
4388
|
try {
|
|
3600
4389
|
const options = parseDashboardArgs2(args.slice(1), process.cwd(), process.env);
|
|
@@ -3611,15 +4400,13 @@ if (args[0] === "dashboard") {
|
|
|
3611
4400
|
console.error(safe ? message : "Dashboard startup failed; check server, owning directory, authentication and renderer installation");
|
|
3612
4401
|
process.exitCode = 1;
|
|
3613
4402
|
}
|
|
3614
|
-
} else if (!args[0] || args.includes("--help")) {
|
|
3615
|
-
console.log("Usage: bun run inspect /absolute/path/to/journal.jsonl [--history] [--json]\nThe ready log reports the location-specific journal path.\nMetrics: inspect metrics /absolute/history-directory [--session ID] [--from ISO-or-ms] [--to ISO-or-ms] [--csv]\nModels: inspect models /absolute/history-directory [--model provider/model] [--agent NAME] [--from ISO-or-ms] [--to ISO-or-ms] [--csv | --json]\nCharts: inspect charts /absolute/history-directory --output report.html [--session ID] [--agent NAME] [--model provider/model] [--from ISO-or-ms] [--to ISO-or-ms] [--max-points 1000] [--force]");
|
|
3616
4403
|
} else if (args[0] === "models") {
|
|
3617
4404
|
const { inspectModels: inspectModels2 } = await Promise.resolve().then(() => (init_modelcli(), exports_modelcli));
|
|
3618
4405
|
await inspectModels2(args.slice(1));
|
|
3619
4406
|
} else if (args[0] === "metrics" || args[0] === "charts") {
|
|
3620
4407
|
const directory = args[1];
|
|
3621
4408
|
if (!directory || directory.startsWith("--"))
|
|
3622
|
-
throw new Error(
|
|
4409
|
+
throw new Error(`Use context-guard ${args[0]} --help for usage and examples`);
|
|
3623
4410
|
const option = (name) => {
|
|
3624
4411
|
const index = args.indexOf(name);
|
|
3625
4412
|
if (index < 0)
|