@osovv/vv-opencode 1.2.11 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +27 -2
  3. package/dist/cli.js +4 -2
  4. package/dist/cli.js.map +1 -1
  5. package/dist/commands/analytics.d.ts +58 -0
  6. package/dist/commands/analytics.js +220 -0
  7. package/dist/commands/analytics.js.map +1 -0
  8. package/dist/index.d.ts +1 -0
  9. package/dist/index.js +6 -5
  10. package/dist/index.js.map +1 -1
  11. package/dist/lib/analytics/metrics.d.ts +33 -0
  12. package/dist/lib/analytics/metrics.js +162 -0
  13. package/dist/lib/analytics/metrics.js.map +1 -0
  14. package/dist/lib/analytics/store.d.ts +20 -0
  15. package/dist/lib/analytics/store.js +81 -0
  16. package/dist/lib/analytics/store.js.map +1 -0
  17. package/dist/lib/analytics/types.d.ts +67 -0
  18. package/dist/lib/analytics/types.js +26 -0
  19. package/dist/lib/analytics/types.js.map +1 -0
  20. package/dist/lib/managed-skills.d.ts +1 -1
  21. package/dist/lib/managed-skills.js +3 -2
  22. package/dist/lib/managed-skills.js.map +1 -1
  23. package/dist/lib/opencode.d.ts +2 -0
  24. package/dist/lib/opencode.js +4 -2
  25. package/dist/lib/opencode.js.map +1 -1
  26. package/dist/lib/plugin-toggle-config.d.ts +1 -1
  27. package/dist/lib/plugin-toggle-config.js +2 -1
  28. package/dist/lib/plugin-toggle-config.js.map +1 -1
  29. package/dist/plugins/analytics/index.d.ts +18 -0
  30. package/dist/plugins/analytics/index.js +151 -0
  31. package/dist/plugins/analytics/index.js.map +1 -0
  32. package/dist/tui/analytics/indicator.d.ts +41 -0
  33. package/dist/tui/analytics/indicator.js +118 -0
  34. package/dist/tui/analytics/indicator.js.map +1 -0
  35. package/dist/tui/branding/footer.d.ts +14 -0
  36. package/dist/tui/branding/footer.js +35 -0
  37. package/dist/tui/branding/footer.js.map +1 -0
  38. package/dist/tui/color.d.ts +13 -0
  39. package/dist/tui/color.js +30 -0
  40. package/dist/tui/color.js.map +1 -0
  41. package/dist/tui.js +25 -7
  42. package/dist/tui.js.map +1 -1
  43. package/package.json +6 -2
  44. package/schemas/vvoc/v3.json +1 -1
  45. package/templates/skills/vvoc-usage-analytics/SKILL.md +58 -0
  46. 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, including vv-reflect and vv-handoff.
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: [v0.5.3 - Added vv-handoff to the canonical managed skill set.]
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":"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"}
1
+ {"version":3,"file":"managed-skills.js","sourceRoot":"","sources":["../../src/lib/managed-skills.ts"],"names":[],"mappings":"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"}
@@ -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>;
@@ -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: [v1.4.0 - Hardened ensureManagedSkillSymlink: a non-symlink at the managed path is treated as user-owned and skipped instead of unlinked, and an already-correct vvoc link is kept rather than recreated.]
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
- function extractOpenCodeVersion(output) {
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
  }