@osovv/vv-opencode 1.2.10 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/README.md +27 -2
- package/dist/cli.js +4 -2
- package/dist/cli.js.map +1 -1
- package/dist/commands/analytics.d.ts +58 -0
- package/dist/commands/analytics.js +220 -0
- package/dist/commands/analytics.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +6 -5
- package/dist/index.js.map +1 -1
- package/dist/lib/analytics/metrics.d.ts +33 -0
- package/dist/lib/analytics/metrics.js +162 -0
- package/dist/lib/analytics/metrics.js.map +1 -0
- package/dist/lib/analytics/store.d.ts +20 -0
- package/dist/lib/analytics/store.js +81 -0
- package/dist/lib/analytics/store.js.map +1 -0
- package/dist/lib/analytics/types.d.ts +67 -0
- package/dist/lib/analytics/types.js +26 -0
- package/dist/lib/analytics/types.js.map +1 -0
- package/dist/lib/managed-skills.d.ts +1 -1
- package/dist/lib/managed-skills.js +3 -2
- package/dist/lib/managed-skills.js.map +1 -1
- package/dist/lib/opencode.d.ts +2 -0
- package/dist/lib/opencode.js +4 -2
- package/dist/lib/opencode.js.map +1 -1
- package/dist/lib/plugin-toggle-config.d.ts +1 -1
- package/dist/lib/plugin-toggle-config.js +2 -1
- package/dist/lib/plugin-toggle-config.js.map +1 -1
- package/dist/plugins/analytics/index.d.ts +18 -0
- package/dist/plugins/analytics/index.js +151 -0
- package/dist/plugins/analytics/index.js.map +1 -0
- package/dist/plugins/system-context-injection/index.js +10 -1
- package/dist/plugins/system-context-injection/index.js.map +1 -1
- package/dist/tui/analytics/indicator.d.ts +40 -0
- package/dist/tui/analytics/indicator.js +117 -0
- package/dist/tui/analytics/indicator.js.map +1 -0
- package/dist/tui/branding/footer.d.ts +14 -0
- package/dist/tui/branding/footer.js +34 -0
- package/dist/tui/branding/footer.js.map +1 -0
- package/dist/tui.js +25 -7
- package/dist/tui.js.map +1 -1
- package/package.json +6 -2
- package/schemas/vvoc/v3.json +1 -1
- package/templates/agents/vv-controller.md +1 -0
- package/templates/skills/vvoc-usage-analytics/SKILL.md +58 -0
- package/templates/skills/vvoc-usage-analytics/references/opencode-db-queries.md +113 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// FILE: src/lib/analytics/metrics.ts
|
|
2
|
+
// VERSION: 1.0.0
|
|
3
|
+
// START_MODULE_CONTRACT
|
|
4
|
+
// PURPOSE: Pure analytics dedupe, filtering, grouping, metric computation, and CLI formatting helpers with UTC-deterministic time buckets.
|
|
5
|
+
// SCOPE: Last-write-wins dedupe by partID, ISO week bucketing, group key extraction, token-weighted cache hit rate and coverage, inclusive range/project filters, relative and absolute date parsing, and compact display formatting.
|
|
6
|
+
// DEPENDS: [src/lib/analytics/types.ts]
|
|
7
|
+
// LINKS: [M-ANALYTICS-TYPES, M-CLI-ANALYTICS, M-TUI-ANALYTICS-INDICATOR]
|
|
8
|
+
// ROLE: RUNTIME
|
|
9
|
+
// MAP_MODE: EXPORTS
|
|
10
|
+
// END_MODULE_CONTRACT
|
|
11
|
+
//
|
|
12
|
+
// START_MODULE_MAP
|
|
13
|
+
// dedupeUsageRecords - Keeps the last occurrence of each partID in array order.
|
|
14
|
+
// isoWeekKey - ISO week key "YYYY-Wnn" for a UTC timestamp.
|
|
15
|
+
// groupKeyOf - Group key extraction for a usage record.
|
|
16
|
+
// computeGroupedMetrics - Aggregates deduped usage records into per-group metrics.
|
|
17
|
+
// UsageFilter - Inclusive date-range and project-substring filter shape.
|
|
18
|
+
// filterUsageRecords - Applies inclusive date-range and project-substring filters.
|
|
19
|
+
// parseSinceUntil - Parses relative and absolute --since/--until values.
|
|
20
|
+
// formatTokenCount - Compact human token count formatting.
|
|
21
|
+
// formatHitRate - Percentage or "n/a" formatting for nullable hit rates.
|
|
22
|
+
// END_MODULE_MAP
|
|
23
|
+
//
|
|
24
|
+
// START_CHANGE_SUMMARY
|
|
25
|
+
// LAST_CHANGE: [2026-08-19-cache-hit-rate-analytics - Added pure analytics metrics and formatting helpers.]
|
|
26
|
+
// END_CHANGE_SUMMARY
|
|
27
|
+
const DAY_MS = 86_400_000;
|
|
28
|
+
// START_BLOCK_DEDUPE
|
|
29
|
+
/** Keeps only the last occurrence of each partID in array order (last-write-wins). */
|
|
30
|
+
export function dedupeUsageRecords(records) {
|
|
31
|
+
const byPart = new Map();
|
|
32
|
+
for (const record of records)
|
|
33
|
+
byPart.set(record.partID, record);
|
|
34
|
+
return [...byPart.values()];
|
|
35
|
+
}
|
|
36
|
+
// END_BLOCK_DEDUPE
|
|
37
|
+
// START_BLOCK_TIME_BUCKETS
|
|
38
|
+
/** ISO week key "YYYY-Wnn" for a UTC timestamp. */
|
|
39
|
+
export function isoWeekKey(ts) {
|
|
40
|
+
const date = new Date(ts);
|
|
41
|
+
const dayNr = (date.getUTCDay() + 6) % 7;
|
|
42
|
+
date.setUTCDate(date.getUTCDate() - dayNr + 3);
|
|
43
|
+
const year = date.getUTCFullYear();
|
|
44
|
+
const firstThursday = new Date(Date.UTC(year, 0, 4));
|
|
45
|
+
const firstDayNr = (firstThursday.getUTCDay() + 6) % 7;
|
|
46
|
+
firstThursday.setUTCDate(firstThursday.getUTCDate() - firstDayNr + 3);
|
|
47
|
+
const week = 1 + Math.round((date.getTime() - firstThursday.getTime()) / (7 * DAY_MS));
|
|
48
|
+
return `${year}-W${String(week).padStart(2, "0")}`;
|
|
49
|
+
}
|
|
50
|
+
/** Group key extraction; day/week/month buckets are UTC-based. */
|
|
51
|
+
export function groupKeyOf(record, groupBy) {
|
|
52
|
+
switch (groupBy) {
|
|
53
|
+
case "session":
|
|
54
|
+
return record.sessionID;
|
|
55
|
+
case "day":
|
|
56
|
+
return record.ts.slice(0, 10);
|
|
57
|
+
case "week":
|
|
58
|
+
return isoWeekKey(record.ts);
|
|
59
|
+
case "month":
|
|
60
|
+
return record.ts.slice(0, 7);
|
|
61
|
+
case "model":
|
|
62
|
+
return `${record.providerID}/${record.modelID}`;
|
|
63
|
+
case "provider":
|
|
64
|
+
return record.providerID;
|
|
65
|
+
case "project":
|
|
66
|
+
return record.projectDirectory;
|
|
67
|
+
case "vvoc":
|
|
68
|
+
return record.vvocVersion;
|
|
69
|
+
case "opencode":
|
|
70
|
+
return record.opencodeVersion;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// END_BLOCK_TIME_BUCKETS
|
|
74
|
+
// START_BLOCK_GROUPED_METRICS
|
|
75
|
+
/**
|
|
76
|
+
* Aggregates usage records into per-group metrics.
|
|
77
|
+
* hitRate = sum(cacheRead) / sum(cacheRead + cacheWrite + input) over eligible
|
|
78
|
+
* steps (cacheRead + cacheWrite > 0); null when a group has no eligible steps.
|
|
79
|
+
*/
|
|
80
|
+
export function computeGroupedMetrics(usage, groupBy) {
|
|
81
|
+
const groups = new Map();
|
|
82
|
+
for (const record of usage) {
|
|
83
|
+
const key = groupKeyOf(record, groupBy);
|
|
84
|
+
const group = groups.get(key) ?? {
|
|
85
|
+
key,
|
|
86
|
+
steps: 0,
|
|
87
|
+
eligibleSteps: 0,
|
|
88
|
+
coverage: 0,
|
|
89
|
+
hitRate: null,
|
|
90
|
+
cacheRead: 0,
|
|
91
|
+
cacheWrite: 0,
|
|
92
|
+
input: 0,
|
|
93
|
+
output: 0,
|
|
94
|
+
reasoning: 0,
|
|
95
|
+
};
|
|
96
|
+
group.steps += 1;
|
|
97
|
+
group.cacheRead += record.tokens.cacheRead;
|
|
98
|
+
group.cacheWrite += record.tokens.cacheWrite;
|
|
99
|
+
group.input += record.tokens.input;
|
|
100
|
+
group.output += record.tokens.output;
|
|
101
|
+
group.reasoning += record.tokens.reasoning;
|
|
102
|
+
if (record.tokens.cacheRead + record.tokens.cacheWrite > 0)
|
|
103
|
+
group.eligibleSteps += 1;
|
|
104
|
+
groups.set(key, group);
|
|
105
|
+
}
|
|
106
|
+
for (const group of groups.values()) {
|
|
107
|
+
group.coverage = group.steps > 0 ? group.eligibleSteps / group.steps : 0;
|
|
108
|
+
const denominator = group.cacheRead + group.cacheWrite + group.input;
|
|
109
|
+
group.hitRate =
|
|
110
|
+
group.eligibleSteps > 0 && denominator > 0 ? group.cacheRead / denominator : null;
|
|
111
|
+
}
|
|
112
|
+
return [...groups.values()];
|
|
113
|
+
}
|
|
114
|
+
/** Applies inclusive date-range and project-substring filters. */
|
|
115
|
+
export function filterUsageRecords(usage, filter) {
|
|
116
|
+
const untilUpper = filter.until ? `${filter.until}T23:59:59.999Z` : undefined;
|
|
117
|
+
const needle = filter.project?.toLowerCase();
|
|
118
|
+
return usage.filter((record) => {
|
|
119
|
+
if (filter.since && record.ts < `${filter.since}T00:00:00.000Z`)
|
|
120
|
+
return false;
|
|
121
|
+
if (untilUpper && record.ts > untilUpper)
|
|
122
|
+
return false;
|
|
123
|
+
if (needle && !record.projectDirectory.toLowerCase().includes(needle))
|
|
124
|
+
return false;
|
|
125
|
+
return true;
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Parses a --since/--until value. Relative "Nd"/"Nw"/"Nm" resolve against `now`
|
|
130
|
+
* as a UTC date string; absolute "YYYY-MM-DD" passes through. Returns undefined
|
|
131
|
+
* for undefined input and null for unparseable input.
|
|
132
|
+
*/
|
|
133
|
+
export function parseSinceUntil(raw, now) {
|
|
134
|
+
if (raw === undefined)
|
|
135
|
+
return undefined;
|
|
136
|
+
const trimmed = raw.trim().toLowerCase();
|
|
137
|
+
const relative = /^(\d+)([dwm])$/.exec(trimmed);
|
|
138
|
+
if (relative) {
|
|
139
|
+
const amount = Number(relative[1]);
|
|
140
|
+
const unitMs = relative[2] === "d" ? DAY_MS : relative[2] === "w" ? 7 * DAY_MS : 30 * DAY_MS;
|
|
141
|
+
return new Date(now.getTime() - amount * unitMs).toISOString().slice(0, 10);
|
|
142
|
+
}
|
|
143
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(trimmed))
|
|
144
|
+
return trimmed;
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
// END_BLOCK_FILTERS
|
|
148
|
+
// START_BLOCK_FORMATTERS
|
|
149
|
+
/** Compact human token count: 999 -> "999"; 1234 -> "1.2k"; 2100000 -> "2.1M". */
|
|
150
|
+
export function formatTokenCount(value) {
|
|
151
|
+
if (value < 1000)
|
|
152
|
+
return String(value);
|
|
153
|
+
if (value < 1_000_000)
|
|
154
|
+
return `${(value / 1000).toFixed(1)}k`;
|
|
155
|
+
return `${(value / 1_000_000).toFixed(1)}M`;
|
|
156
|
+
}
|
|
157
|
+
/** "n/a" for null; otherwise a percentage with one decimal. */
|
|
158
|
+
export function formatHitRate(rate) {
|
|
159
|
+
return rate === null ? "n/a" : `${(rate * 100).toFixed(1)}%`;
|
|
160
|
+
}
|
|
161
|
+
// END_BLOCK_FORMATTERS
|
|
162
|
+
//# sourceMappingURL=metrics.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metrics.js","sourceRoot":"","sources":["../../../src/lib/analytics/metrics.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { AnalyticsRecord } from "./types.js";
|
|
2
|
+
/** Resolves the analytics store directory inside the global vvoc data root. */
|
|
3
|
+
export declare function getAnalyticsDir(dataHomeOverride?: string): string;
|
|
4
|
+
/** Monthly file name for a record timestamp: usage-YYYY-MM.jsonl. */
|
|
5
|
+
export declare function resolveAnalyticsFileName(ts: string): string;
|
|
6
|
+
/**
|
|
7
|
+
* Appends one record as a JSON line to the monthly file.
|
|
8
|
+
* Creates the analytics directory when missing; never rewrites existing lines.
|
|
9
|
+
*/
|
|
10
|
+
export declare function appendAnalyticsRecord(record: AnalyticsRecord, options?: {
|
|
11
|
+
dataHomeOverride?: string;
|
|
12
|
+
}): Promise<void>;
|
|
13
|
+
/**
|
|
14
|
+
* Reads every usage-*.jsonl file (sorted by file name ascending) and returns
|
|
15
|
+
* parsed records in file-then-line order. Empty and unparsable lines are
|
|
16
|
+
* skipped without failing. A missing directory returns an empty array.
|
|
17
|
+
*/
|
|
18
|
+
export declare function readAnalyticsRecords(options?: {
|
|
19
|
+
dataHomeOverride?: string;
|
|
20
|
+
}): Promise<AnalyticsRecord[]>;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// FILE: src/lib/analytics/store.ts
|
|
2
|
+
// VERSION: 1.0.0
|
|
3
|
+
// START_MODULE_CONTRACT
|
|
4
|
+
// PURPOSE: Append-only monthly JSONL persistence for analytics records under the global vvoc data root.
|
|
5
|
+
// SCOPE: Analytics directory resolution, monthly file naming, record appending with idempotent directory creation, and tolerant chronological reads.
|
|
6
|
+
// DEPENDS: [node:fs/promises, node:path, src/lib/vvoc-paths.ts, src/lib/analytics/types.ts]
|
|
7
|
+
// LINKS: [M-ANALYTICS-TYPES, M-PLUGIN-ANALYTICS, M-CLI-ANALYTICS]
|
|
8
|
+
// ROLE: RUNTIME
|
|
9
|
+
// MAP_MODE: EXPORTS
|
|
10
|
+
// END_MODULE_CONTRACT
|
|
11
|
+
//
|
|
12
|
+
// START_MODULE_MAP
|
|
13
|
+
// getAnalyticsDir - Resolves the analytics store directory inside the global vvoc data root.
|
|
14
|
+
// resolveAnalyticsFileName - Monthly file name for a record timestamp.
|
|
15
|
+
// appendAnalyticsRecord - Appends one record as a JSON line to the monthly file.
|
|
16
|
+
// readAnalyticsRecords - Reads all usage-*.jsonl records in tolerant chronological order.
|
|
17
|
+
// END_MODULE_MAP
|
|
18
|
+
//
|
|
19
|
+
// START_CHANGE_SUMMARY
|
|
20
|
+
// LAST_CHANGE: [2026-08-19-cache-hit-rate-analytics - Added append-only monthly JSONL analytics store.]
|
|
21
|
+
// END_CHANGE_SUMMARY
|
|
22
|
+
import { appendFile, mkdir, readdir, readFile } from "node:fs/promises";
|
|
23
|
+
import { join } from "node:path";
|
|
24
|
+
import { getGlobalVvocDataDir } from "../vvoc-paths.js";
|
|
25
|
+
const ANALYTICS_FILE_PATTERN = /^usage-\d{4}-\d{2}\.jsonl$/;
|
|
26
|
+
// START_BLOCK_ANALYTICS_DIR
|
|
27
|
+
/** Resolves the analytics store directory inside the global vvoc data root. */
|
|
28
|
+
export function getAnalyticsDir(dataHomeOverride) {
|
|
29
|
+
return join(getGlobalVvocDataDir(dataHomeOverride), "analytics");
|
|
30
|
+
}
|
|
31
|
+
/** Monthly file name for a record timestamp: usage-YYYY-MM.jsonl. */
|
|
32
|
+
export function resolveAnalyticsFileName(ts) {
|
|
33
|
+
return `usage-${ts.slice(0, 7)}.jsonl`;
|
|
34
|
+
}
|
|
35
|
+
// END_BLOCK_ANALYTICS_DIR
|
|
36
|
+
// START_BLOCK_ANALYTICS_APPEND
|
|
37
|
+
/**
|
|
38
|
+
* Appends one record as a JSON line to the monthly file.
|
|
39
|
+
* Creates the analytics directory when missing; never rewrites existing lines.
|
|
40
|
+
*/
|
|
41
|
+
export async function appendAnalyticsRecord(record, options = {}) {
|
|
42
|
+
const dir = getAnalyticsDir(options.dataHomeOverride);
|
|
43
|
+
await mkdir(dir, { recursive: true });
|
|
44
|
+
const file = join(dir, resolveAnalyticsFileName(record.ts));
|
|
45
|
+
await appendFile(file, `${JSON.stringify(record)}\n`, "utf8");
|
|
46
|
+
}
|
|
47
|
+
// END_BLOCK_ANALYTICS_APPEND
|
|
48
|
+
// START_BLOCK_ANALYTICS_READ
|
|
49
|
+
/**
|
|
50
|
+
* Reads every usage-*.jsonl file (sorted by file name ascending) and returns
|
|
51
|
+
* parsed records in file-then-line order. Empty and unparsable lines are
|
|
52
|
+
* skipped without failing. A missing directory returns an empty array.
|
|
53
|
+
*/
|
|
54
|
+
export async function readAnalyticsRecords(options = {}) {
|
|
55
|
+
const dir = getAnalyticsDir(options.dataHomeOverride);
|
|
56
|
+
let files;
|
|
57
|
+
try {
|
|
58
|
+
files = (await readdir(dir)).filter((name) => ANALYTICS_FILE_PATTERN.test(name)).sort();
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
const records = [];
|
|
64
|
+
for (const file of files) {
|
|
65
|
+
const text = await readFile(join(dir, file), "utf8");
|
|
66
|
+
for (const line of text.split("\n")) {
|
|
67
|
+
const trimmed = line.trim();
|
|
68
|
+
if (!trimmed)
|
|
69
|
+
continue;
|
|
70
|
+
try {
|
|
71
|
+
records.push(JSON.parse(trimmed));
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// Corrupt or partially written line: skip.
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return records;
|
|
79
|
+
}
|
|
80
|
+
// END_BLOCK_ANALYTICS_READ
|
|
81
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../../../src/lib/analytics/store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/** Token counters for one completed model step. All values are non-negative integers. */
|
|
2
|
+
export type UsageTokens = {
|
|
3
|
+
/** Fresh (uncached) input tokens reported by the provider. */
|
|
4
|
+
input: number;
|
|
5
|
+
/** Output tokens as reported by the provider. */
|
|
6
|
+
output: number;
|
|
7
|
+
/** Reasoning tokens reported separately by the provider. */
|
|
8
|
+
reasoning: number;
|
|
9
|
+
/** Tokens served from the provider prompt cache. */
|
|
10
|
+
cacheRead: number;
|
|
11
|
+
/** Tokens written into the provider prompt cache this step. */
|
|
12
|
+
cacheWrite: number;
|
|
13
|
+
};
|
|
14
|
+
/** One persisted step-finish observation. Readers dedupe by partID last-write-wins. */
|
|
15
|
+
export type UsageRecord = {
|
|
16
|
+
kind: "usage";
|
|
17
|
+
/** ISO timestamp of the observation (event delivery time, UTC). */
|
|
18
|
+
ts: string;
|
|
19
|
+
projectID: string;
|
|
20
|
+
projectDirectory: string;
|
|
21
|
+
sessionID: string;
|
|
22
|
+
messageID: string;
|
|
23
|
+
partID: string;
|
|
24
|
+
providerID: string;
|
|
25
|
+
modelID: string;
|
|
26
|
+
agent: string;
|
|
27
|
+
tokens: UsageTokens;
|
|
28
|
+
/** Cost recorded by OpenCode for the step; 0 when unrecorded. */
|
|
29
|
+
cost: number;
|
|
30
|
+
vvocVersion: string;
|
|
31
|
+
/** Best-effort OpenCode version; "unknown" when unresolved. */
|
|
32
|
+
opencodeVersion: string;
|
|
33
|
+
};
|
|
34
|
+
/** Latest known session metadata. Readers dedupe by sessionID last-write-wins. */
|
|
35
|
+
export type SessionRecord = {
|
|
36
|
+
kind: "session";
|
|
37
|
+
ts: string;
|
|
38
|
+
sessionID: string;
|
|
39
|
+
projectID: string;
|
|
40
|
+
title: string;
|
|
41
|
+
};
|
|
42
|
+
export type AnalyticsRecord = UsageRecord | SessionRecord;
|
|
43
|
+
/** Grouping keys supported by `vvoc analytics cache-hit-rate --group-by`. */
|
|
44
|
+
export type AnalyticsGroupKey = "session" | "day" | "week" | "month" | "model" | "provider" | "project" | "vvoc" | "opencode";
|
|
45
|
+
/** Aggregated metrics for one group. hitRate is null when no eligible steps exist. */
|
|
46
|
+
export type GroupedMetrics = {
|
|
47
|
+
key: string;
|
|
48
|
+
steps: number;
|
|
49
|
+
eligibleSteps: number;
|
|
50
|
+
/** eligibleSteps / steps; 0 when steps === 0. */
|
|
51
|
+
coverage: number;
|
|
52
|
+
/** Token-weighted cache hit rate over eligible steps; null when eligibleSteps === 0. */
|
|
53
|
+
hitRate: number | null;
|
|
54
|
+
cacheRead: number;
|
|
55
|
+
cacheWrite: number;
|
|
56
|
+
input: number;
|
|
57
|
+
output: number;
|
|
58
|
+
reasoning: number;
|
|
59
|
+
};
|
|
60
|
+
/** Rolling per-session sums used by the live TUI indicator. */
|
|
61
|
+
export type IndicatorTokens = {
|
|
62
|
+
steps: number;
|
|
63
|
+
eligibleSteps: number;
|
|
64
|
+
cacheRead: number;
|
|
65
|
+
cacheWrite: number;
|
|
66
|
+
input: number;
|
|
67
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// FILE: src/lib/analytics/types.ts
|
|
2
|
+
// VERSION: 1.0.0
|
|
3
|
+
// START_MODULE_CONTRACT
|
|
4
|
+
// PURPOSE: Define the canonical analytics record and metric types shared by the store, metrics, server plugin, CLI, and TUI indicator.
|
|
5
|
+
// SCOPE: Usage/session persistence record shapes, grouping keys, grouped metric aggregation shape, and indicator token counters.
|
|
6
|
+
// DEPENDS: [none]
|
|
7
|
+
// LINKS: [M-ANALYTICS-STORE, M-ANALYTICS-METRICS, M-PLUGIN-ANALYTICS, M-CLI-ANALYTICS, M-TUI-ANALYTICS-INDICATOR]
|
|
8
|
+
// ROLE: TYPES
|
|
9
|
+
// MAP_MODE: EXPORTS
|
|
10
|
+
// END_MODULE_CONTRACT
|
|
11
|
+
//
|
|
12
|
+
// START_MODULE_MAP
|
|
13
|
+
// UsageTokens - Token counters for one completed model step.
|
|
14
|
+
// UsageRecord - One persisted step-finish observation deduped by partID last-write-wins.
|
|
15
|
+
// SessionRecord - Latest known session metadata deduped by sessionID last-write-wins.
|
|
16
|
+
// AnalyticsRecord - Union of persistable analytics record kinds.
|
|
17
|
+
// AnalyticsGroupKey - Grouping keys supported by the analytics CLI.
|
|
18
|
+
// GroupedMetrics - Aggregated metrics for one group with nullable hit rate.
|
|
19
|
+
// IndicatorTokens - Rolling per-session sums used by the live TUI indicator.
|
|
20
|
+
// END_MODULE_MAP
|
|
21
|
+
//
|
|
22
|
+
// START_CHANGE_SUMMARY
|
|
23
|
+
// LAST_CHANGE: [2026-08-19-cache-hit-rate-analytics - Added canonical analytics record and metric types.]
|
|
24
|
+
// END_CHANGE_SUMMARY
|
|
25
|
+
export {};
|
|
26
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/lib/analytics/types.ts"],"names":[],"mappings":"AAAA,mCAAmC;AACnC,iBAAiB;AACjB,wBAAwB;AACxB,yIAAyI;AACzI,mIAAmI;AACnI,oBAAoB;AACpB,oHAAoH;AACpH,gBAAgB;AAChB,sBAAsB;AACtB,sBAAsB;AACtB,EAAE;AACF,mBAAmB;AACnB,+DAA+D;AAC/D,2FAA2F;AAC3F,wFAAwF;AACxF,mEAAmE;AACnE,sEAAsE;AACtE,8EAA8E;AAC9E,+EAA+E;AAC/E,iBAAiB;AACjB,EAAE;AACF,uBAAuB;AACvB,4GAA4G;AAC5G,qBAAqB"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const MANAGED_SKILL_NAMES: readonly ["vv-spec", "vv-plan", "vv-review", "vv-execute", "vv-reflect", "vv-handoff"];
|
|
1
|
+
export declare const MANAGED_SKILL_NAMES: readonly ["vv-spec", "vv-plan", "vv-review", "vv-execute", "vv-reflect", "vv-handoff", "vvoc-usage-analytics"];
|
|
2
2
|
export type ManagedSkillName = (typeof MANAGED_SKILL_NAMES)[number];
|
|
3
3
|
export declare function getManagedSkillFilePath(skillsDirPath: string, name: ManagedSkillName): string;
|
|
4
4
|
export declare function loadManagedSkillTemplate(name: ManagedSkillName): Promise<string>;
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// END_MODULE_CONTRACT
|
|
11
11
|
//
|
|
12
12
|
// START_MODULE_MAP
|
|
13
|
-
// MANAGED_SKILL_NAMES - Canonical vvoc-managed skill names
|
|
13
|
+
// MANAGED_SKILL_NAMES - Canonical vvoc-managed skill names: vv-* work-protocol skills plus vvoc-* tooling skills.
|
|
14
14
|
// ManagedSkillName - Type for vvoc-managed skill names.
|
|
15
15
|
// getManagedSkillFilePath - Resolves the skill file path inside a vvoc skills directory.
|
|
16
16
|
// loadManagedSkillTemplate - Loads the bundled skill template for a managed skill.
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
// END_MODULE_MAP
|
|
21
21
|
//
|
|
22
22
|
// START_CHANGE_SUMMARY
|
|
23
|
-
// LAST_CHANGE: [
|
|
23
|
+
// LAST_CHANGE: [2026-08-19-usage-analytics-skill - Added the vvoc-usage-analytics tooling skill (first vvoc-* managed skill).]
|
|
24
24
|
// END_CHANGE_SUMMARY
|
|
25
25
|
import { readdir, readFile } from "node:fs/promises";
|
|
26
26
|
import { join } from "node:path";
|
|
@@ -32,6 +32,7 @@ export const MANAGED_SKILL_NAMES = [
|
|
|
32
32
|
"vv-execute",
|
|
33
33
|
"vv-reflect",
|
|
34
34
|
"vv-handoff",
|
|
35
|
+
"vvoc-usage-analytics",
|
|
35
36
|
];
|
|
36
37
|
export function getManagedSkillFilePath(skillsDirPath, name) {
|
|
37
38
|
return join(skillsDirPath, name, "SKILL.md");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"managed-skills.js","sourceRoot":"","sources":["../../src/lib/managed-skills.ts"],"names":[],"mappings":"AAAA,kCAAkC;AAClC,iBAAiB;AACjB,wBAAwB;AACxB,qHAAqH;AACrH,iJAAiJ;AACjJ,4EAA4E;AAC5E,gCAAgC;AAChC,kBAAkB;AAClB,sBAAsB;AACtB,sBAAsB;AACtB,EAAE;AACF,mBAAmB;AACnB,
|
|
1
|
+
{"version":3,"file":"managed-skills.js","sourceRoot":"","sources":["../../src/lib/managed-skills.ts"],"names":[],"mappings":"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"}
|
package/dist/lib/opencode.d.ts
CHANGED
|
@@ -157,6 +157,8 @@ export declare function inspectOpenCodeRuntime(run?: () => Promise<{
|
|
|
157
157
|
stderr: string;
|
|
158
158
|
}>): Promise<OpenCodeRuntimeInspection>;
|
|
159
159
|
export declare function isTuiOpenCodeVersionCompatible(version: string): boolean;
|
|
160
|
+
/** Extracts the first semantic version found in `opencode --version` output. */
|
|
161
|
+
export declare function extractOpenCodeVersion(output: string): string | undefined;
|
|
160
162
|
export declare function inspectInstallation(paths: ResolvedPaths, options?: {
|
|
161
163
|
runtime?: OpenCodeRuntimeInspection;
|
|
162
164
|
}): Promise<InstallationInspection>;
|
package/dist/lib/opencode.js
CHANGED
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
// writeManagedAgentModel - Writes or removes a bundled vvoc-managed OpenCode agent model override in OpenCode config.
|
|
52
52
|
// writeGuardianConfig - Writes the guardian section into the canonical vvoc.json document.
|
|
53
53
|
// inspectOpenCodeRuntime - Reads the installed OpenCode version and evaluates TUI compatibility.
|
|
54
|
+
// extractOpenCodeVersion - Extracts the first semantic version found in `opencode --version` output.
|
|
54
55
|
// isTuiOpenCodeVersionCompatible - Compares an OpenCode version with the managed TUI minimum.
|
|
55
56
|
// inspectInstallation - Reads current OpenCode/vvoc installation state for status and doctor commands.
|
|
56
57
|
// inspectInstallationForScope - Reads installation state using strict/effective layered source resolution.
|
|
@@ -64,7 +65,7 @@
|
|
|
64
65
|
// END_MODULE_MAP
|
|
65
66
|
//
|
|
66
67
|
// START_CHANGE_SUMMARY
|
|
67
|
-
// LAST_CHANGE: [
|
|
68
|
+
// LAST_CHANGE: [2026-08-19-cache-hit-rate-analytics - Exported extractOpenCodeVersion for analytics attribution.]
|
|
68
69
|
// END_CHANGE_SUMMARY
|
|
69
70
|
import { applyEdits, format, modify, parse } from "jsonc-parser";
|
|
70
71
|
import { lstat, mkdir, readFile, readlink, symlink, unlink, writeFile } from "node:fs/promises";
|
|
@@ -689,7 +690,8 @@ async function runOpenCodeVersionCommand() {
|
|
|
689
690
|
]);
|
|
690
691
|
return { exitCode, stdout, stderr };
|
|
691
692
|
}
|
|
692
|
-
|
|
693
|
+
/** Extracts the first semantic version found in `opencode --version` output. */
|
|
694
|
+
export function extractOpenCodeVersion(output) {
|
|
693
695
|
const match = output.match(/(?:^|\s)v?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?)(?=\s|$)/);
|
|
694
696
|
return match?.[1];
|
|
695
697
|
}
|