@danypops/jittor 0.10.0 → 0.11.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/README.md +8 -5
- package/extension/src/benchmark-tui.ts +4 -0
- package/extension/src/capabilities/codex-recovery.ts +127 -0
- package/extension/src/capabilities/http-headers.ts +5 -0
- package/extension/src/capabilities/local-run-telemetry.ts +104 -0
- package/extension/src/capabilities/provider-response-telemetry.ts +96 -0
- package/extension/src/footer.ts +10 -53
- package/extension/src/index.ts +92 -272
- package/extension/src/session-identity.ts +20 -0
- package/extension/src/tui.ts +20 -11
- package/package.json +1 -1
- package/src/adapters/sqlite-metric-store.ts +11 -2
- package/src/adapters/sqlite-session-identity-store.ts +45 -0
- package/src/cli-commands/benchmarks.ts +140 -0
- package/src/cli-commands/compaction.ts +17 -0
- package/src/cli-commands/context.ts +49 -0
- package/src/cli-commands/metrics.ts +296 -0
- package/src/cli-commands/op.ts +40 -0
- package/src/cli-commands/route-args.ts +15 -0
- package/src/cli-commands/router.ts +207 -0
- package/src/cli-commands/service-daemon.ts +72 -0
- package/src/cli-commands/session.ts +42 -0
- package/src/cli-commands/support.ts +33 -0
- package/src/cli.ts +42 -769
- package/src/constants.ts +7 -0
- package/src/daemon.ts +13 -3
- package/src/db.ts +15 -1
- package/src/operations/benchmark-operations.ts +12 -0
- package/src/operations/context-operations.ts +30 -0
- package/src/operations/metrics-operations.ts +77 -0
- package/src/operations/model-ranking-operations.ts +16 -0
- package/src/operations/router-operations.ts +19 -0
- package/src/operations/session-identity-operations.ts +15 -0
- package/src/operations/session-scope.ts +31 -0
- package/src/operations/types.ts +3 -0
- package/src/ports/metric-store.ts +2 -0
- package/src/ports/router-controller.ts +9 -9
- package/src/ports/session-identity-store.ts +5 -0
- package/src/providers/telemetry-sources.ts +2 -1
- package/src/router.ts +124 -67
- package/src/service.ts +60 -118
- package/src/session-identity-service.ts +55 -0
package/extension/src/tui.ts
CHANGED
|
@@ -1,15 +1,26 @@
|
|
|
1
1
|
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { matchesKey, truncateToWidth } from "@earendil-works/pi-tui";
|
|
3
3
|
import { HUMAN_STATUS_MAX_SOURCES, HUMAN_TEXT_FIELD_MAX_CHARACTERS } from "../../src/constants.ts";
|
|
4
|
-
import type { StoredMetricObservation } from "../../src/domain/metric.ts";
|
|
4
|
+
import type { MetricQuery, StoredMetricObservation } from "../../src/domain/metric.ts";
|
|
5
5
|
import type { PolicyAction, Route } from "../../src/policy.ts";
|
|
6
6
|
import type { RouterStatus } from "../../src/ports/router-controller.ts";
|
|
7
7
|
import type { ProviderBudget } from "./footer.ts";
|
|
8
|
+
import { sessionSecretField } from "./session-identity.ts";
|
|
8
9
|
|
|
9
10
|
export interface JittorPanelClient {
|
|
10
11
|
call(operation: string, input: unknown): Promise<any>;
|
|
11
12
|
}
|
|
12
13
|
|
|
14
|
+
export function providerBudgetMetricQuery(status: RouterStatus): MetricQuery | null {
|
|
15
|
+
switch (status.currentRoute?.provider) {
|
|
16
|
+
case "openai-codex": return { source: "codex-subscription", metric: "used-fraction", order: "desc", limit: 100 };
|
|
17
|
+
case "openrouter": return { source: "openrouter", order: "desc", limit: 20 };
|
|
18
|
+
case "anthropic": return { source: "anthropic", metric: "used-fraction", order: "desc", limit: 20 };
|
|
19
|
+
case "anthropic-vertex": return { source: "anthropic-vertex", metric: "used-fraction", order: "desc", limit: 20 };
|
|
20
|
+
default: return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
13
24
|
type PanelAction = "pause" | "resume" | "refresh" | "override" | "clear-override" | "close";
|
|
14
25
|
|
|
15
26
|
function latest(rows: StoredMetricObservation[], predicate: (row: StoredMetricObservation) => boolean): StoredMetricObservation | undefined {
|
|
@@ -200,12 +211,9 @@ export function buildStatusView(status: RouterStatus, metrics: StoredMetricObser
|
|
|
200
211
|
return lines;
|
|
201
212
|
}
|
|
202
213
|
|
|
203
|
-
async function snapshot(client: JittorPanelClient): Promise<{ status: RouterStatus; metrics: StoredMetricObservation[] }> {
|
|
204
|
-
const status = await client.call("router.status", {}) as RouterStatus;
|
|
205
|
-
const
|
|
206
|
-
const query = provider === "openai-codex"
|
|
207
|
-
? { source: "codex-subscription", metric: "used-fraction", order: "desc", limit: 100 }
|
|
208
|
-
: provider === "openrouter" ? { source: "openrouter", order: "desc", limit: 20 } : null;
|
|
214
|
+
async function snapshot(client: JittorPanelClient, sessionId: string): Promise<{ status: RouterStatus; metrics: StoredMetricObservation[] }> {
|
|
215
|
+
const status = await client.call("router.status", { session_id: sessionId }) as RouterStatus;
|
|
216
|
+
const query = providerBudgetMetricQuery(status);
|
|
209
217
|
const metrics = query ? await client.call("metrics.query", query) as StoredMetricObservation[] : [];
|
|
210
218
|
return { status, metrics };
|
|
211
219
|
}
|
|
@@ -219,8 +227,9 @@ async function chooseOverride(ctx: ExtensionCommandContext, routes: Route[]): Pr
|
|
|
219
227
|
}
|
|
220
228
|
|
|
221
229
|
export async function showJittorPanel(ctx: ExtensionCommandContext, client: JittorPanelClient): Promise<void> {
|
|
230
|
+
const session_id = ctx.sessionManager.getSessionId();
|
|
222
231
|
for (;;) {
|
|
223
|
-
const current = await snapshot(client);
|
|
232
|
+
const current = await snapshot(client, session_id);
|
|
224
233
|
if (ctx.mode !== "tui") {
|
|
225
234
|
ctx.ui.notify(buildStatusView(current.status, current.metrics).join("\n"), "info");
|
|
226
235
|
return;
|
|
@@ -254,17 +263,17 @@ export async function showJittorPanel(ctx: ExtensionCommandContext, client: Jitt
|
|
|
254
263
|
if (action === "refresh") { await client.call("telemetry.poll", {}); continue; }
|
|
255
264
|
if (action === "pause" || action === "resume") {
|
|
256
265
|
if (await ctx.ui.confirm(action === "pause" ? "Emergency-halt provider requests?" : "Release emergency halt?", "This changes provider-request enforcement. Use /jittor off to disable blocking entirely.")) {
|
|
257
|
-
await client.call(action === "pause" ? "router.pause" : "router.resume", {});
|
|
266
|
+
await client.call(action === "pause" ? "router.pause" : "router.resume", { session_id, ...sessionSecretField(session_id) });
|
|
258
267
|
}
|
|
259
268
|
continue;
|
|
260
269
|
}
|
|
261
270
|
if (action === "clear-override") {
|
|
262
|
-
if (await ctx.ui.confirm("Clear route override?", "Policy-controlled routing will resume.")) await client.call("router.clear_override", {});
|
|
271
|
+
if (await ctx.ui.confirm("Clear route override?", "Policy-controlled routing will resume.")) await client.call("router.clear_override", { session_id, ...sessionSecretField(session_id) });
|
|
263
272
|
continue;
|
|
264
273
|
}
|
|
265
274
|
const route = await chooseOverride(ctx, current.status.availableRoutes);
|
|
266
275
|
if (route && await ctx.ui.confirm("Apply route override?", `${routeText(route)} for one hour`)) {
|
|
267
|
-
await client.call("router.override", { route, expiresAt: Date.now() + 60 * 60 * 1_000 });
|
|
276
|
+
await client.call("router.override", { route, expiresAt: Date.now() + 60 * 60 * 1_000, session_id, ...sessionSecretField(session_id) });
|
|
268
277
|
}
|
|
269
278
|
}
|
|
270
279
|
}
|
package/package.json
CHANGED
|
@@ -34,7 +34,17 @@ export class SQLiteMetricStore implements MetricStore {
|
|
|
34
34
|
|
|
35
35
|
record(input: MetricObservation): StoredMetricObservation {
|
|
36
36
|
const observation = validateMetricObservation(input);
|
|
37
|
-
const result = this.
|
|
37
|
+
const result = this.insert(observation);
|
|
38
|
+
return this.get(Number(result.lastInsertRowid));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
recordBatch(inputs: MetricObservation[]): StoredMetricObservation[] {
|
|
42
|
+
const observations = inputs.map((input) => validateMetricObservation(input));
|
|
43
|
+
return this.db.transaction((rows: typeof observations) => rows.map((observation) => this.get(Number(this.insert(observation).lastInsertRowid))))(observations);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private insert(observation: MetricObservation): { lastInsertRowid: number | bigint } {
|
|
47
|
+
return this.db.query(`
|
|
38
48
|
INSERT INTO metric_observations (source, scope, metric, value, unit, observed_at, attributes)
|
|
39
49
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
40
50
|
`).run(
|
|
@@ -46,7 +56,6 @@ export class SQLiteMetricStore implements MetricStore {
|
|
|
46
56
|
observation.observedAt,
|
|
47
57
|
JSON.stringify(observation.attributes ?? {}),
|
|
48
58
|
);
|
|
49
|
-
return this.get(Number(result.lastInsertRowid));
|
|
50
59
|
}
|
|
51
60
|
|
|
52
61
|
query(filter: MetricQuery = {}): StoredMetricObservation[] {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite";
|
|
2
|
+
import { SESSION_IDENTITY_MAX_ROWS } from "../constants.ts";
|
|
3
|
+
import type { SessionIdentityRecord, SessionIdentityStore } from "../ports/session-identity-store.ts";
|
|
4
|
+
|
|
5
|
+
export class SQLiteSessionIdentityStore implements SessionIdentityStore {
|
|
6
|
+
constructor(private readonly db: Database) {}
|
|
7
|
+
|
|
8
|
+
find(sessionId: string): SessionIdentityRecord | undefined {
|
|
9
|
+
const row = this.db.query("SELECT session_id, secret_hash, registered_at, last_seen_at FROM session_identities WHERE session_id = ?").get(sessionId) as
|
|
10
|
+
| { session_id: string; secret_hash: string; registered_at: string; last_seen_at: string }
|
|
11
|
+
| null;
|
|
12
|
+
return row ? { sessionId: row.session_id, secretHash: row.secret_hash, registeredAt: row.registered_at, lastSeenAt: row.last_seen_at } : undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
upsert(record: SessionIdentityRecord): void {
|
|
16
|
+
this.db.transaction(() => {
|
|
17
|
+
this.evictOldestBeyondCap(record.sessionId);
|
|
18
|
+
this.db.query(`
|
|
19
|
+
INSERT INTO session_identities (session_id, secret_hash, registered_at, last_seen_at)
|
|
20
|
+
VALUES (?, ?, ?, ?)
|
|
21
|
+
ON CONFLICT(session_id) DO UPDATE SET secret_hash = excluded.secret_hash, registered_at = excluded.registered_at, last_seen_at = excluded.last_seen_at
|
|
22
|
+
`).run(record.sessionId, record.secretHash, record.registeredAt, record.lastSeenAt);
|
|
23
|
+
})();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
remove(sessionId: string): void {
|
|
27
|
+
this.db.query("DELETE FROM session_identities WHERE session_id = ?").run(sessionId);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
touch(sessionId: string, lastSeenAt: string): void {
|
|
31
|
+
this.db.query("UPDATE session_identities SET last_seen_at = ? WHERE session_id = ?").run(lastSeenAt, sessionId);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
count(): number {
|
|
35
|
+
return (this.db.query("SELECT COUNT(*) AS count FROM session_identities").get() as { count: number }).count;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Bounds distinct registered session identities; evicts the least-recently-seen beyond the cap. */
|
|
39
|
+
private evictOldestBeyondCap(sessionId: string): void {
|
|
40
|
+
const exists = this.db.query("SELECT 1 FROM session_identities WHERE session_id = ?").get(sessionId);
|
|
41
|
+
if (exists) return;
|
|
42
|
+
if (this.count() < SESSION_IDENTITY_MAX_ROWS) return;
|
|
43
|
+
this.db.exec("DELETE FROM session_identities WHERE session_id = (SELECT session_id FROM session_identities ORDER BY last_seen_at ASC LIMIT 1)");
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BENCHMARK_MAX_QUERY_LIMIT,
|
|
3
|
+
MODEL_RANKING_DEFAULT_CONTEXT_WEIGHT,
|
|
4
|
+
MODEL_RANKING_DEFAULT_COST_WEIGHT,
|
|
5
|
+
MODEL_RANKING_DEFAULT_LATENCY_WEIGHT,
|
|
6
|
+
MODEL_RANKING_DEFAULT_QUALITY_WEIGHT,
|
|
7
|
+
MODEL_RANKING_DEFAULT_RELIABILITY_WEIGHT,
|
|
8
|
+
MODEL_RANKING_MAX_SOURCES,
|
|
9
|
+
} from "../constants.ts";
|
|
10
|
+
import type { BenchmarkQuery, BenchmarkQueryResult, BenchmarkRefreshResult } from "../domain/benchmark.ts";
|
|
11
|
+
import type { ModelRecommendationInput } from "../domain/model-ranking-service.ts";
|
|
12
|
+
import type { ModelCandidate, ModelRankingResult, ScopeAuthority, UtilityWeights } from "../domain/model-ranking.ts";
|
|
13
|
+
import { TASK_DOMAINS, TASK_TYPES, type ModelTaskDomain, type ModelTaskType } from "../domain/model-observation.ts";
|
|
14
|
+
import { humanField, type CliDependencies } from "./support.ts";
|
|
15
|
+
import { parseCandidate } from "./route-args.ts";
|
|
16
|
+
|
|
17
|
+
export const BENCHMARKS_USAGE_LINES = [" benchmarks <status|refresh|list|rank> [options] [--json]"];
|
|
18
|
+
|
|
19
|
+
interface BenchmarkArgs {
|
|
20
|
+
action: "status" | "refresh" | "list" | "rank";
|
|
21
|
+
json: boolean;
|
|
22
|
+
force: boolean;
|
|
23
|
+
query?: BenchmarkQuery;
|
|
24
|
+
recommendation?: ModelRecommendationInput & { session_id?: string; session_secret?: string };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function parseBenchmarkArgs(action: string | undefined, args: string[]): BenchmarkArgs | null {
|
|
28
|
+
if (action !== "status" && action !== "refresh" && action !== "list" && action !== "rank") return null;
|
|
29
|
+
let json = false;
|
|
30
|
+
let force = false;
|
|
31
|
+
const query: Partial<BenchmarkQuery> = {};
|
|
32
|
+
const candidates: ModelCandidate[] = [];
|
|
33
|
+
const sourceIds: string[] = [];
|
|
34
|
+
let scopeAuthority: ScopeAuthority = "available-models";
|
|
35
|
+
let domain: ModelTaskDomain = "general";
|
|
36
|
+
let type: ModelTaskType = "general";
|
|
37
|
+
let budgetPressure = 0;
|
|
38
|
+
let sessionId: string | undefined;
|
|
39
|
+
let sessionSecret: string | undefined;
|
|
40
|
+
const weights: UtilityWeights = {
|
|
41
|
+
quality: MODEL_RANKING_DEFAULT_QUALITY_WEIGHT, cost: MODEL_RANKING_DEFAULT_COST_WEIGHT,
|
|
42
|
+
latency: MODEL_RANKING_DEFAULT_LATENCY_WEIGHT, context: MODEL_RANKING_DEFAULT_CONTEXT_WEIGHT,
|
|
43
|
+
reliability: MODEL_RANKING_DEFAULT_RELIABILITY_WEIGHT,
|
|
44
|
+
};
|
|
45
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
46
|
+
const argument = args[index];
|
|
47
|
+
if (argument === "--json") { json = true; continue; }
|
|
48
|
+
if (argument === "--force" && action === "refresh") { force = true; continue; }
|
|
49
|
+
const allowed = action === "list" ? ["--source", "--model", "--dimension", "--limit"]
|
|
50
|
+
: action === "rank" ? ["--candidate", "--source", "--domain", "--type", "--scope", "--budget", "--weight-quality", "--weight-cost", "--weight-latency", "--weight-context", "--weight-reliability", "--session-id", "--session-secret"] : [];
|
|
51
|
+
if (!allowed.includes(argument ?? "")) return null;
|
|
52
|
+
const raw = args[++index];
|
|
53
|
+
if (raw === undefined || raw.length === 0) return null;
|
|
54
|
+
if (action === "list") {
|
|
55
|
+
if (argument === "--limit") {
|
|
56
|
+
const limit = Number(raw);
|
|
57
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > BENCHMARK_MAX_QUERY_LIMIT) return null;
|
|
58
|
+
query.limit = limit;
|
|
59
|
+
} else if (argument === "--source") query.sourceId = raw;
|
|
60
|
+
else if (argument === "--model") query.model = raw;
|
|
61
|
+
else query.dimension = raw;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (argument === "--candidate") {
|
|
65
|
+
const candidate = parseCandidate(raw);
|
|
66
|
+
if (!candidate) return null;
|
|
67
|
+
candidates.push(candidate);
|
|
68
|
+
} else if (argument === "--source") sourceIds.push(raw);
|
|
69
|
+
else if (argument === "--domain") {
|
|
70
|
+
if (!TASK_DOMAINS.includes(raw as ModelTaskDomain)) return null;
|
|
71
|
+
domain = raw as ModelTaskDomain;
|
|
72
|
+
} else if (argument === "--type") {
|
|
73
|
+
if (!TASK_TYPES.includes(raw as ModelTaskType)) return null;
|
|
74
|
+
type = raw as ModelTaskType;
|
|
75
|
+
} else if (argument === "--scope") {
|
|
76
|
+
if (raw !== "exact-session" && raw !== "available-models") return null;
|
|
77
|
+
scopeAuthority = raw;
|
|
78
|
+
} else if (argument === "--budget") budgetPressure = Number(raw);
|
|
79
|
+
else if (argument === "--session-id") sessionId = raw;
|
|
80
|
+
else if (argument === "--session-secret") sessionSecret = raw;
|
|
81
|
+
else {
|
|
82
|
+
const weight = Number(raw);
|
|
83
|
+
if (!Number.isFinite(weight) || weight < 0 || weight > 10) return null;
|
|
84
|
+
weights[argument!.slice("--weight-".length) as keyof UtilityWeights] = weight;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (action === "list" && query.sourceId === undefined) return null;
|
|
88
|
+
if (action === "rank" && (candidates.length === 0 || sourceIds.length > MODEL_RANKING_MAX_SOURCES || !Number.isFinite(budgetPressure) || budgetPressure < 0 || budgetPressure > 2)) return null;
|
|
89
|
+
return {
|
|
90
|
+
action, json, force,
|
|
91
|
+
...(action === "list" ? { query: query as BenchmarkQuery } : {}),
|
|
92
|
+
...(action === "rank" ? { recommendation: { candidates, sourceIds: [...new Set(sourceIds)], scopeAuthority, domain, type, budgetPressure, weights, ...(sessionId ? { session_id: sessionId } : {}), ...(sessionSecret ? { session_secret: sessionSecret } : {}) } } : {}),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function formatBenchmarkStatus(result: BenchmarkRefreshResult): string {
|
|
97
|
+
if (result.sources.length === 0) return "Benchmark sources: none configured";
|
|
98
|
+
return ["Benchmark sources:", ...result.sources.map((source) => {
|
|
99
|
+
const state = source.ok === null ? "not refreshed" : source.ok ? "ready" : "refresh failed";
|
|
100
|
+
return `- ${source.id}: ${state} · ${source.observations.toLocaleString()} observations · ${source.hasEvidence ? "evidence retained" : "no evidence"}`;
|
|
101
|
+
})].join("\n");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function formatBenchmarkQuery(result: BenchmarkQueryResult): string {
|
|
105
|
+
return [
|
|
106
|
+
`Benchmark evidence: ${humanField(result.sourceId)} · ${result.completeness} · ${result.freshness} · ${result.observations.length.toLocaleString()} observations`,
|
|
107
|
+
...result.observations.map((observation) => `- ${humanField(observation.model.canonical)} · ${humanField(observation.dimension)} ${observation.value.toLocaleString()} ${observation.unit} · ${humanField(observation.provenance.publisher)} · confidence ${(observation.provenance.confidence * 100).toFixed(0)}%`),
|
|
108
|
+
].join("\n");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function formatModelRanking(result: ModelRankingResult): string {
|
|
112
|
+
return [
|
|
113
|
+
`Model ranking: ${result.completeness} · scope ${result.scopeAuthority}${result.scopeWarning ? " · advisory only" : ""}`,
|
|
114
|
+
...result.ranked.map((item, index) => `${index + 1}. ${humanField(item.identity)} · utility ${item.utility === null ? "unknown" : item.utility.toFixed(3)} · confidence ${(item.confidence * 100).toFixed(0)}%`),
|
|
115
|
+
...(result.scopeWarning ? [result.scopeWarning] : []),
|
|
116
|
+
].join("\n");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function runBenchmarksCommand(action: string | undefined, rest: string[], deps: CliDependencies, usage: () => number): Promise<number> {
|
|
120
|
+
const parsed = parseBenchmarkArgs(action, rest);
|
|
121
|
+
if (!parsed) return usage();
|
|
122
|
+
try {
|
|
123
|
+
if (parsed.action === "list") {
|
|
124
|
+
const result = await deps.client.call("benchmark.query", parsed.query!);
|
|
125
|
+
deps.stdout(parsed.json ? JSON.stringify(result) : formatBenchmarkQuery(result));
|
|
126
|
+
} else if (parsed.action === "rank") {
|
|
127
|
+
const result = await deps.client.call("models.rank", parsed.recommendation!);
|
|
128
|
+
deps.stdout(parsed.json ? JSON.stringify(result) : formatModelRanking(result));
|
|
129
|
+
} else {
|
|
130
|
+
const result = parsed.action === "refresh"
|
|
131
|
+
? await deps.client.call("benchmark.refresh", { force: parsed.force })
|
|
132
|
+
: await deps.client.call("benchmark.status", {});
|
|
133
|
+
deps.stdout(parsed.json ? JSON.stringify(result) : formatBenchmarkStatus(result));
|
|
134
|
+
}
|
|
135
|
+
return 0;
|
|
136
|
+
} catch (error) {
|
|
137
|
+
deps.stderr(error instanceof Error ? error.message : String(error));
|
|
138
|
+
return 1;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { CompactionDurationEstimate } from "../domain/context-telemetry.ts";
|
|
2
|
+
import { callAndPrint, type CliDependencies } from "./support.ts";
|
|
3
|
+
import { parseJsonOnlyArgs } from "./router.ts";
|
|
4
|
+
|
|
5
|
+
export function formatCompactionEstimate(estimate: CompactionDurationEstimate): string {
|
|
6
|
+
if (estimate.confidence === "cold-start" || estimate.ms === null) {
|
|
7
|
+
return `Compaction duration: cold-start (${estimate.sampleSize.toLocaleString()} sample(s), not enough evidence yet)`;
|
|
8
|
+
}
|
|
9
|
+
return `Compaction duration: ~${estimate.ms.toLocaleString()}ms learned from ${estimate.sampleSize.toLocaleString()} sample(s)`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function runCompactionCommand(action: string | undefined, rest: string[], deps: CliDependencies, usage: () => number): Promise<number> {
|
|
13
|
+
if (action !== "estimate") return usage();
|
|
14
|
+
const parsed = parseJsonOnlyArgs(rest);
|
|
15
|
+
if (!parsed) return usage();
|
|
16
|
+
return callAndPrint(deps, "compaction.estimate", {}, parsed.json, formatCompactionEstimate);
|
|
17
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { ContextAssessment } from "../domain/context-telemetry.ts";
|
|
2
|
+
import type { CliDependencies } from "./support.ts";
|
|
3
|
+
|
|
4
|
+
export const CONTEXT_USAGE_LINES = [" context [--since <ms>] [--until <ms>] [--json]"];
|
|
5
|
+
|
|
6
|
+
function parseContextArgs(args: string[]): { input: { since?: number; until?: number }; json: boolean } | null {
|
|
7
|
+
const input: { since?: number; until?: number } = {};
|
|
8
|
+
let json = false;
|
|
9
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
10
|
+
const argument = args[index];
|
|
11
|
+
if (argument === "--json") { json = true; continue; }
|
|
12
|
+
if (argument !== "--since" && argument !== "--until") return null;
|
|
13
|
+
const raw = args[++index];
|
|
14
|
+
const value = raw === undefined ? Number.NaN : Number(raw);
|
|
15
|
+
if (!Number.isSafeInteger(value) || value < 0) return null;
|
|
16
|
+
if (argument === "--since") input.since = value;
|
|
17
|
+
else input.until = value;
|
|
18
|
+
}
|
|
19
|
+
if (input.since !== undefined && input.until !== undefined && input.until < input.since) return null;
|
|
20
|
+
return { input, json };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function value(value: number | null, suffix = ""): string {
|
|
24
|
+
return value === null ? "unknown" : `${Math.round(value).toLocaleString()}${suffix}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function formatContextAssessment(summary: ContextAssessment): string {
|
|
28
|
+
return [
|
|
29
|
+
`Context assessment: ${summary.completeness}`,
|
|
30
|
+
`Papyrus injection: ${summary.injection.runs} runs · avg ${value(summary.injection.averageCharacters, " chars")} · p95 ${value(summary.injection.p95Characters, " chars")} · max ${value(summary.injection.maxCharacters, " chars")}`,
|
|
31
|
+
`Injection mix: rules ${summary.injection.ruleCharacters.toLocaleString()} chars · tasks ${summary.injection.taskCharacters.toLocaleString()} chars · estimated ${summary.injection.estimatedTokens.toLocaleString()} tokens · unchanged ${summary.injection.unchangedRate === null ? "unknown" : `${(summary.injection.unchangedRate * 100).toFixed(1)}%`}`,
|
|
32
|
+
`Compactions: ${summary.compaction.completed} completed · ${summary.compaction.aborted} aborted · avg ${value(summary.compaction.averageDurationMs, "ms")} · ${summary.compaction.perRun === null ? "unknown" : summary.compaction.perRun.toFixed(3)} per agent run · ${summary.compaction.perTurn === null ? "unknown" : summary.compaction.perTurn.toFixed(3)} per turn`,
|
|
33
|
+
`Between compactions: ${value(summary.compaction.averageTurnsBetween, " turns")} · ${value(summary.compaction.averageProviderTokensBetween, " provider tokens")} · ${value(summary.compaction.averageCacheReadTokensBetween, " cache-read tokens")}`,
|
|
34
|
+
`Reasons: threshold ${summary.compaction.reasons.threshold} · overflow ${summary.compaction.reasons.overflow} · manual ${summary.compaction.reasons.manual}`,
|
|
35
|
+
].join("\n");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function runContextCommand(action: string | undefined, rest: string[], deps: CliDependencies, usage: () => number): Promise<number> {
|
|
39
|
+
const parsed = parseContextArgs([...(action === undefined ? [] : [action]), ...rest]);
|
|
40
|
+
if (!parsed) return usage();
|
|
41
|
+
try {
|
|
42
|
+
const summary = await deps.client.call("context.assess", parsed.input);
|
|
43
|
+
deps.stdout(parsed.json ? JSON.stringify(summary) : formatContextAssessment(summary));
|
|
44
|
+
return 0;
|
|
45
|
+
} catch (error) {
|
|
46
|
+
deps.stderr(error instanceof Error ? error.message : String(error));
|
|
47
|
+
return 1;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { CLI_METRICS_HUMAN_MAX_ROWS, MAX_QUERY_LIMIT, MAX_USAGE_BUCKETS, METRIC_BATCH_MAX_OBSERVATIONS, USAGE_MAX_DISTINCT_SCOPES } from "../constants.ts";
|
|
2
|
+
import { METRIC_UNITS, type MetricObservation, type MetricQuery, type MetricUnit, type StoredMetricObservation } from "../domain/metric.ts";
|
|
3
|
+
import type { TaskCostSummary } from "../domain/task-cost.ts";
|
|
4
|
+
import { callAndPrint, humanField, type CliDependencies } from "./support.ts";
|
|
5
|
+
|
|
6
|
+
export const METRICS_USAGE_LINES = [
|
|
7
|
+
" metrics record --source <s> --scope <s> --metric <s> --value <number|null> --unit <unit> [--observed-at <ms>] [--attributes <json>] [--json]",
|
|
8
|
+
` metrics record-batch --observations <json-array, max ${METRIC_BATCH_MAX_OBSERVATIONS}> [--json]`,
|
|
9
|
+
" metrics query [--source <s>] [--scope <s>] [--metric <s>] [--since <ms>] [--until <ms>] [--limit <n>] [--order asc|desc] [--json]",
|
|
10
|
+
" metrics prune --before <ms> [--force] [--json] (force required if before is newer than 24h ago)",
|
|
11
|
+
` metrics distinct-scopes --source <s> --since <ms> --until <ms> [--limit 1..${USAGE_MAX_DISTINCT_SCOPES}] [--json]`,
|
|
12
|
+
` metrics usage-series --source <s> --since <ms> --until <ms> --bucket-size-ms <ms> --bucket-count 1..${MAX_USAGE_BUCKETS} [--scope-limit 1..${USAGE_MAX_DISTINCT_SCOPES}] [--json]`,
|
|
13
|
+
" metrics cost-by-task --since <ms> --until <ms> [--json]",
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
interface MetricsRecordArgs { input: MetricObservation; json: boolean }
|
|
17
|
+
|
|
18
|
+
function parseMetricsRecordArgs(args: string[]): MetricsRecordArgs | null {
|
|
19
|
+
let json = false;
|
|
20
|
+
let source: string | undefined;
|
|
21
|
+
let scope: string | undefined;
|
|
22
|
+
let metric: string | undefined;
|
|
23
|
+
let value: number | null | undefined;
|
|
24
|
+
let unit: MetricUnit | undefined;
|
|
25
|
+
let observedAt: number | undefined;
|
|
26
|
+
let attributes: Record<string, unknown> | undefined;
|
|
27
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
28
|
+
const argument = args[index];
|
|
29
|
+
if (argument === "--json") { json = true; continue; }
|
|
30
|
+
if (!["--source", "--scope", "--metric", "--value", "--unit", "--observed-at", "--attributes"].includes(argument ?? "")) return null;
|
|
31
|
+
const raw = args[++index];
|
|
32
|
+
if (raw === undefined || raw.length === 0) return null;
|
|
33
|
+
if (argument === "--source") source = raw;
|
|
34
|
+
else if (argument === "--scope") scope = raw;
|
|
35
|
+
else if (argument === "--metric") metric = raw;
|
|
36
|
+
else if (argument === "--value") {
|
|
37
|
+
if (raw.toLowerCase() === "null") value = null;
|
|
38
|
+
else {
|
|
39
|
+
const parsed = Number(raw);
|
|
40
|
+
if (!Number.isFinite(parsed)) return null;
|
|
41
|
+
value = parsed;
|
|
42
|
+
}
|
|
43
|
+
} else if (argument === "--unit") {
|
|
44
|
+
if (!METRIC_UNITS.includes(raw as MetricUnit)) return null;
|
|
45
|
+
unit = raw as MetricUnit;
|
|
46
|
+
} else if (argument === "--observed-at") {
|
|
47
|
+
const parsed = Number(raw);
|
|
48
|
+
if (!Number.isSafeInteger(parsed) || parsed < 0) return null;
|
|
49
|
+
observedAt = parsed;
|
|
50
|
+
} else {
|
|
51
|
+
try {
|
|
52
|
+
const parsed = JSON.parse(raw);
|
|
53
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
|
|
54
|
+
attributes = parsed as Record<string, unknown>;
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (source === undefined || scope === undefined || metric === undefined || value === undefined || unit === undefined) return null;
|
|
61
|
+
return {
|
|
62
|
+
json,
|
|
63
|
+
input: {
|
|
64
|
+
source, scope, metric, value, unit,
|
|
65
|
+
observedAt: observedAt ?? Date.now(),
|
|
66
|
+
...(attributes ? { attributes } : {}),
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface MetricsRecordBatchArgs { input: { observations: MetricObservation[] }; json: boolean }
|
|
72
|
+
|
|
73
|
+
function parseMetricsRecordBatchArgs(args: string[]): MetricsRecordBatchArgs | null {
|
|
74
|
+
let json = false;
|
|
75
|
+
let observations: unknown;
|
|
76
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
77
|
+
const argument = args[index];
|
|
78
|
+
if (argument === "--json") { json = true; continue; }
|
|
79
|
+
if (argument !== "--observations") return null;
|
|
80
|
+
const raw = args[++index];
|
|
81
|
+
if (raw === undefined || raw.length === 0) return null;
|
|
82
|
+
try {
|
|
83
|
+
observations = JSON.parse(raw);
|
|
84
|
+
} catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (!Array.isArray(observations) || observations.length === 0 || observations.length > METRIC_BATCH_MAX_OBSERVATIONS) return null;
|
|
89
|
+
return { input: { observations: observations as MetricObservation[] }, json };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
interface MetricsQueryArgs { input: MetricQuery; json: boolean }
|
|
93
|
+
|
|
94
|
+
function parseMetricsQueryArgs(args: string[]): MetricsQueryArgs | null {
|
|
95
|
+
let json = false;
|
|
96
|
+
const input: MetricQuery = {};
|
|
97
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
98
|
+
const argument = args[index];
|
|
99
|
+
if (argument === "--json") { json = true; continue; }
|
|
100
|
+
if (!["--source", "--scope", "--metric", "--since", "--until", "--limit", "--order"].includes(argument ?? "")) return null;
|
|
101
|
+
const raw = args[++index];
|
|
102
|
+
if (raw === undefined || raw.length === 0) return null;
|
|
103
|
+
if (argument === "--source") input.source = raw;
|
|
104
|
+
else if (argument === "--scope") input.scope = raw;
|
|
105
|
+
else if (argument === "--metric") input.metric = raw;
|
|
106
|
+
else if (argument === "--order") {
|
|
107
|
+
if (raw !== "asc" && raw !== "desc") return null;
|
|
108
|
+
input.order = raw;
|
|
109
|
+
} else {
|
|
110
|
+
const parsed = Number(raw);
|
|
111
|
+
if (!Number.isSafeInteger(parsed) || parsed < 0) return null;
|
|
112
|
+
if (argument === "--since") input.since = parsed;
|
|
113
|
+
else if (argument === "--until") input.until = parsed;
|
|
114
|
+
else {
|
|
115
|
+
if (parsed < 1 || parsed > MAX_QUERY_LIMIT) return null;
|
|
116
|
+
input.limit = parsed;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (input.since !== undefined && input.until !== undefined && input.until < input.since) return null;
|
|
121
|
+
return { input, json };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
interface MetricsDistinctScopesArgs { input: { source: string; since: number; until: number; limit?: number }; json: boolean }
|
|
125
|
+
|
|
126
|
+
function parseMetricsDistinctScopesArgs(args: string[]): MetricsDistinctScopesArgs | null {
|
|
127
|
+
let json = false;
|
|
128
|
+
let source: string | undefined;
|
|
129
|
+
let since: number | undefined;
|
|
130
|
+
let until: number | undefined;
|
|
131
|
+
let limit: number | undefined;
|
|
132
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
133
|
+
const argument = args[index];
|
|
134
|
+
if (argument === "--json") { json = true; continue; }
|
|
135
|
+
if (!["--source", "--since", "--until", "--limit"].includes(argument ?? "")) return null;
|
|
136
|
+
const raw = args[++index];
|
|
137
|
+
if (raw === undefined || raw.length === 0) return null;
|
|
138
|
+
if (argument === "--source") { source = raw; continue; }
|
|
139
|
+
const parsed = Number(raw);
|
|
140
|
+
if (!Number.isSafeInteger(parsed) || parsed < 0) return null;
|
|
141
|
+
if (argument === "--since") since = parsed;
|
|
142
|
+
else if (argument === "--until") until = parsed;
|
|
143
|
+
else {
|
|
144
|
+
if (parsed < 1 || parsed > USAGE_MAX_DISTINCT_SCOPES) return null;
|
|
145
|
+
limit = parsed;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (source === undefined || since === undefined || until === undefined || until < since) return null;
|
|
149
|
+
return { input: { source, since, until, ...(limit === undefined ? {} : { limit }) }, json };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
interface MetricsUsageSeriesArgs { input: { source: string; since: number; until: number; bucketSizeMs: number; bucketCount: number; scopeLimit?: number }; json: boolean }
|
|
153
|
+
|
|
154
|
+
function parseMetricsUsageSeriesArgs(args: string[]): MetricsUsageSeriesArgs | null {
|
|
155
|
+
let json = false;
|
|
156
|
+
let source: string | undefined;
|
|
157
|
+
let since: number | undefined;
|
|
158
|
+
let until: number | undefined;
|
|
159
|
+
let bucketSizeMs: number | undefined;
|
|
160
|
+
let bucketCount: number | undefined;
|
|
161
|
+
let scopeLimit: number | undefined;
|
|
162
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
163
|
+
const argument = args[index];
|
|
164
|
+
if (argument === "--json") { json = true; continue; }
|
|
165
|
+
if (!(["--source", "--since", "--until", "--bucket-size-ms", "--bucket-count", "--scope-limit"].includes(argument ?? ""))) return null;
|
|
166
|
+
const raw = args[++index];
|
|
167
|
+
if (raw === undefined || raw.length === 0) return null;
|
|
168
|
+
if (argument === "--source") { source = raw; continue; }
|
|
169
|
+
const parsed = Number(raw);
|
|
170
|
+
if (!Number.isSafeInteger(parsed) || parsed < 0) return null;
|
|
171
|
+
if (argument === "--since") since = parsed;
|
|
172
|
+
else if (argument === "--until") until = parsed;
|
|
173
|
+
else if (argument === "--bucket-size-ms") { if (parsed < 1) return null; bucketSizeMs = parsed; }
|
|
174
|
+
else if (argument === "--bucket-count") { if (parsed < 1 || parsed > MAX_USAGE_BUCKETS) return null; bucketCount = parsed; }
|
|
175
|
+
else { if (parsed < 1 || parsed > USAGE_MAX_DISTINCT_SCOPES) return null; scopeLimit = parsed; }
|
|
176
|
+
}
|
|
177
|
+
if (source === undefined || since === undefined || until === undefined || until < since || bucketSizeMs === undefined || bucketCount === undefined) return null;
|
|
178
|
+
return { input: { source, since, until, bucketSizeMs, bucketCount, ...(scopeLimit === undefined ? {} : { scopeLimit }) }, json };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
interface CostByTaskArgs { input: { since: number; until: number }; json: boolean }
|
|
182
|
+
|
|
183
|
+
function parseCostByTaskArgs(args: string[]): CostByTaskArgs | null {
|
|
184
|
+
let json = false;
|
|
185
|
+
let since: number | undefined;
|
|
186
|
+
let until: number | undefined;
|
|
187
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
188
|
+
const argument = args[index];
|
|
189
|
+
if (argument === "--json") { json = true; continue; }
|
|
190
|
+
if (!["--since", "--until"].includes(argument ?? "")) return null;
|
|
191
|
+
const raw = args[++index];
|
|
192
|
+
const parsed = Number(raw);
|
|
193
|
+
if (!Number.isSafeInteger(parsed) || parsed < 0) return null;
|
|
194
|
+
if (argument === "--since") since = parsed;
|
|
195
|
+
else until = parsed;
|
|
196
|
+
}
|
|
197
|
+
if (since === undefined || until === undefined || until < since) return null;
|
|
198
|
+
return { input: { since, until }, json };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
interface MetricsPruneArgs { input: { before: number; force?: boolean }; json: boolean }
|
|
202
|
+
|
|
203
|
+
function parseMetricsPruneArgs(args: string[]): MetricsPruneArgs | null {
|
|
204
|
+
let json = false;
|
|
205
|
+
let force = false;
|
|
206
|
+
let before: number | undefined;
|
|
207
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
208
|
+
const argument = args[index];
|
|
209
|
+
if (argument === "--json") { json = true; continue; }
|
|
210
|
+
if (argument === "--force") { force = true; continue; }
|
|
211
|
+
if (argument !== "--before") return null;
|
|
212
|
+
const raw = args[++index];
|
|
213
|
+
const parsed = Number(raw);
|
|
214
|
+
if (!Number.isSafeInteger(parsed) || parsed < 0) return null;
|
|
215
|
+
before = parsed;
|
|
216
|
+
}
|
|
217
|
+
if (before === undefined) return null;
|
|
218
|
+
return { input: { before, ...(force ? { force } : {}) }, json };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function formatMetricsQuery(rows: StoredMetricObservation[]): string {
|
|
222
|
+
if (rows.length === 0) return "Metrics: no observations matched";
|
|
223
|
+
const shown = rows.slice(0, CLI_METRICS_HUMAN_MAX_ROWS);
|
|
224
|
+
const lines = [
|
|
225
|
+
`Metrics: ${rows.length.toLocaleString()} observation(s)${rows.length > shown.length ? ` (showing first ${shown.length})` : ""}`,
|
|
226
|
+
...shown.map((row) => `- ${humanField(row.source)}/${humanField(row.scope)}/${humanField(row.metric)} = ${row.value === null ? "null" : row.value} ${row.unit} @ ${new Date(row.observedAt).toISOString()}`),
|
|
227
|
+
];
|
|
228
|
+
return lines.join("\n");
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function formatMetricsDistinctScopes(scopes: string[]): string {
|
|
232
|
+
if (scopes.length === 0) return "Scopes: none matched";
|
|
233
|
+
return [`Scopes: ${scopes.length.toLocaleString()}`, ...scopes.map((scope) => `- ${humanField(scope)}`)].join("\n");
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function formatMetricsUsageSeries(result: { rows: Array<{ scope: string; metric: string; bucketIndex: number; sum: number }>; truncated: boolean }): string {
|
|
237
|
+
if (result.rows.length === 0) return `Usage series: no data${result.truncated ? " (scope limit reached)" : ""}`;
|
|
238
|
+
const lines = [`Usage series: ${result.rows.length.toLocaleString()} bucket(s)${result.truncated ? " (scope limit reached)" : ""}`];
|
|
239
|
+
for (const row of result.rows) lines.push(`- ${humanField(row.scope)}/${humanField(row.metric)} bucket ${row.bucketIndex}: ${row.sum.toLocaleString()}`);
|
|
240
|
+
return lines.join("\n");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function formatUsdAmount(amount: number): string {
|
|
244
|
+
return `$${amount.toFixed(Math.abs(amount) < 0.01 && amount !== 0 ? 4 : 2)}`;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function formatCostByTask(summary: TaskCostSummary): string {
|
|
248
|
+
const lines = [
|
|
249
|
+
`Cost by task: ${summary.entries.length.toLocaleString()} task(s)${summary.truncated ? " (query limit reached; totals are a lower bound)" : ""}`,
|
|
250
|
+
...summary.entries.flatMap((entry) => [
|
|
251
|
+
`- ${humanField(entry.taskId)}: ${formatUsdAmount(entry.costUsd)} · ↑${entry.inputTokens.toLocaleString()} ↓${entry.outputTokens.toLocaleString()} R${entry.cacheReadTokens.toLocaleString()} W${entry.cacheWriteTokens.toLocaleString()}`,
|
|
252
|
+
...entry.byModel.map((model) => ` · ${humanField(model.provider)}/${humanField(model.model)} (${humanField(model.thinking)}): ${formatUsdAmount(model.costUsd)} · ↑${model.inputTokens.toLocaleString()} ↓${model.outputTokens.toLocaleString()} R${model.cacheReadTokens.toLocaleString()} W${model.cacheWriteTokens.toLocaleString()}`),
|
|
253
|
+
]),
|
|
254
|
+
`Unattributed spend (no task was focused): ${formatUsdAmount(summary.unattributedCostUsd)}`,
|
|
255
|
+
];
|
|
256
|
+
return lines.join("\n");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export async function runMetricsCommand(action: string | undefined, rest: string[], deps: CliDependencies, usage: () => number): Promise<number> {
|
|
260
|
+
if (action === "record") {
|
|
261
|
+
const parsed = parseMetricsRecordArgs(rest);
|
|
262
|
+
if (!parsed) return usage();
|
|
263
|
+
return callAndPrint(deps, "metrics.record", parsed.input, parsed.json, (row) => formatMetricsQuery([row]));
|
|
264
|
+
}
|
|
265
|
+
if (action === "record-batch") {
|
|
266
|
+
const parsed = parseMetricsRecordBatchArgs(rest);
|
|
267
|
+
if (!parsed) return usage();
|
|
268
|
+
return callAndPrint(deps, "metrics.record_batch", parsed.input, parsed.json, formatMetricsQuery);
|
|
269
|
+
}
|
|
270
|
+
if (action === "query") {
|
|
271
|
+
const parsed = parseMetricsQueryArgs(rest);
|
|
272
|
+
if (!parsed) return usage();
|
|
273
|
+
return callAndPrint(deps, "metrics.query", parsed.input, parsed.json, formatMetricsQuery);
|
|
274
|
+
}
|
|
275
|
+
if (action === "prune") {
|
|
276
|
+
const parsed = parseMetricsPruneArgs(rest);
|
|
277
|
+
if (!parsed) return usage();
|
|
278
|
+
return callAndPrint(deps, "metrics.prune", parsed.input, parsed.json, (result) => `Pruned ${result.deleted.toLocaleString()} observation(s)`);
|
|
279
|
+
}
|
|
280
|
+
if (action === "distinct-scopes") {
|
|
281
|
+
const parsed = parseMetricsDistinctScopesArgs(rest);
|
|
282
|
+
if (!parsed) return usage();
|
|
283
|
+
return callAndPrint(deps, "metrics.distinct_scopes", parsed.input, parsed.json, formatMetricsDistinctScopes);
|
|
284
|
+
}
|
|
285
|
+
if (action === "usage-series") {
|
|
286
|
+
const parsed = parseMetricsUsageSeriesArgs(rest);
|
|
287
|
+
if (!parsed) return usage();
|
|
288
|
+
return callAndPrint(deps, "metrics.usage_series", parsed.input, parsed.json, formatMetricsUsageSeries);
|
|
289
|
+
}
|
|
290
|
+
if (action === "cost-by-task") {
|
|
291
|
+
const parsed = parseCostByTaskArgs(rest);
|
|
292
|
+
if (!parsed) return usage();
|
|
293
|
+
return callAndPrint(deps, "metrics.cost_by_task", parsed.input, parsed.json, formatCostByTask);
|
|
294
|
+
}
|
|
295
|
+
return usage();
|
|
296
|
+
}
|