copilot-metrics 0.1.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/src/cli.js ADDED
@@ -0,0 +1,288 @@
1
+ 'use strict';
2
+
3
+ const { resolvePaths } = require('./paths');
4
+ const {
5
+ ensureDataDirs,
6
+ vscodeSettings,
7
+ copilotCliEnvironment,
8
+ shellExports,
9
+ hookConfig,
10
+ installHook,
11
+ setupSnapshot,
12
+ } = require('./setup');
13
+ const { appendHookEvent, readJsonFromStream } = require('./hook-logger');
14
+ const { initStore } = require('./sqlite-store');
15
+ const { ingestFile } = require('./ingest');
16
+ const { MODEL_PRICES, PRICING_VERSION } = require('./pricing');
17
+ const {
18
+ labelOverview,
19
+ labelSummary,
20
+ labelDetails,
21
+ modelReport,
22
+ repoReport,
23
+ unattributedReport,
24
+ formatLabels,
25
+ formatLabelSummary,
26
+ formatModels,
27
+ formatRepos,
28
+ formatUnattributed,
29
+ } = require('./reports');
30
+ const { loadConfiguredExtractors } = require('./label-extractors');
31
+
32
+ function parseFlags(args) {
33
+ const flags = {};
34
+ const rest = [];
35
+ for (let i = 0; i < args.length; i += 1) {
36
+ const arg = args[i];
37
+ if (!arg.startsWith('--')) {
38
+ rest.push(arg);
39
+ continue;
40
+ }
41
+ const [rawKey, inlineValue] = arg.slice(2).split('=', 2);
42
+ const key = rawKey.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
43
+ if (inlineValue !== undefined) {
44
+ flags[key] = inlineValue;
45
+ } else if (args[i + 1] && !args[i + 1].startsWith('--')) {
46
+ flags[key] = args[i + 1];
47
+ i += 1;
48
+ } else {
49
+ flags[key] = true;
50
+ }
51
+ }
52
+ return { flags, rest };
53
+ }
54
+
55
+ function writeOutput(stdout, value, asJson = false) {
56
+ if (asJson) {
57
+ stdout.write(`${JSON.stringify(value, null, 2)}\n`);
58
+ return;
59
+ }
60
+ stdout.write(`${value}\n`);
61
+ }
62
+
63
+ function helpText() {
64
+ return `copilot-metrics
65
+
66
+ Usage:
67
+ copilot-metrics init [--json]
68
+ copilot-metrics paths [--json]
69
+ copilot-metrics setup vscode [--json]
70
+ copilot-metrics setup copilot-cli [--json]
71
+ copilot-metrics hooks preview [--scope local|global] [--surface both|vscode|copilot-cli] [--json]
72
+ copilot-metrics hooks install [--scope local|global] [--surface both|vscode|copilot-cli] [--json]
73
+ copilot-metrics hook-log --event <name>
74
+ copilot-metrics store init [--json]
75
+ copilot-metrics import --source vscode|copilot-cli|hooks --file <path> [--json]
76
+ copilot-metrics report labels [--json]
77
+ copilot-metrics report label <id> [--detail] [--json]
78
+ copilot-metrics report models [--json]
79
+ copilot-metrics report repos [--json]
80
+ copilot-metrics report unattributed [--json]
81
+ copilot-metrics pricing list [--json]
82
+
83
+ Environment:
84
+ COPILOT_METRICS_HOME Override the central data directory.
85
+ `;
86
+ }
87
+
88
+ function formatPaths(paths) {
89
+ return [
90
+ `home: ${paths.home}`,
91
+ `telemetry: ${paths.telemetryDir}`,
92
+ `hooks: ${paths.hooksDir}`,
93
+ `store: ${paths.storeDir}`,
94
+ `VS Code OTel JSONL: ${paths.vscodeOtelJsonl}`,
95
+ `Copilot CLI OTel JSONL: ${paths.copilotCliOtelJsonl}`,
96
+ `Hook events JSONL: ${paths.hookEventsJsonl}`,
97
+ `SQLite store: ${paths.usageDb}`,
98
+ ].join('\n');
99
+ }
100
+
101
+ function formatVscode(settings) {
102
+ return [
103
+ 'Add these settings to VS Code Insiders settings.json:',
104
+ JSON.stringify(settings, null, 2),
105
+ '',
106
+ 'Content capture is disabled by default.',
107
+ ].join('\n');
108
+ }
109
+
110
+ function formatCopilotCli(env) {
111
+ return [
112
+ 'Export these variables before running Copilot CLI:',
113
+ shellExports(env),
114
+ '',
115
+ 'Content capture is disabled by default.',
116
+ ].join('\n');
117
+ }
118
+
119
+ async function main(args, io) {
120
+ const { flags, rest } = parseFlags(args);
121
+ const json = flags.json === true;
122
+ const paths = resolvePaths({ env: io.env, cwd: io.cwd, home: flags.home });
123
+ const [command, subcommand] = rest;
124
+
125
+ if (!command || command === 'help' || command === '--help' || command === '-h') {
126
+ writeOutput(io.stdout, helpText(), false);
127
+ return;
128
+ }
129
+
130
+ if (command === 'init') {
131
+ ensureDataDirs(paths);
132
+ writeOutput(io.stdout, json ? paths : `Initialized Copilot Metrics data directory:\n${formatPaths(paths)}`, json);
133
+ return;
134
+ }
135
+
136
+ if (command === 'paths') {
137
+ writeOutput(io.stdout, json ? paths : formatPaths(paths), json);
138
+ return;
139
+ }
140
+
141
+ if (command === 'setup') {
142
+ if (subcommand === 'vscode') {
143
+ const settings = vscodeSettings(paths);
144
+ writeOutput(io.stdout, json ? settings : formatVscode(settings), json);
145
+ return;
146
+ }
147
+ if (subcommand === 'copilot-cli') {
148
+ const env = copilotCliEnvironment(paths);
149
+ writeOutput(io.stdout, json ? env : formatCopilotCli(env), json);
150
+ return;
151
+ }
152
+ if (!subcommand || subcommand === 'all') {
153
+ const snapshot = setupSnapshot({ env: io.env, cwd: io.cwd, home: flags.home, command: io.commandPath });
154
+ writeOutput(io.stdout, json ? snapshot : [
155
+ formatPaths(snapshot.paths),
156
+ '',
157
+ formatVscode(snapshot.vscode),
158
+ '',
159
+ formatCopilotCli(snapshot.copilotCli),
160
+ ].join('\n'), json);
161
+ return;
162
+ }
163
+ throw new Error(`Unknown setup target "${subcommand}". Use vscode or copilot-cli.`);
164
+ }
165
+
166
+ if (command === 'hooks') {
167
+ const scope = flags.scope || 'local';
168
+ const surface = flags.surface || 'both';
169
+ if (subcommand === 'preview') {
170
+ const config = hookConfig(paths, { cwd: io.cwd, scope, surface, command: io.commandPath });
171
+ writeOutput(io.stdout, json ? config : JSON.stringify(config, null, 2), json);
172
+ return;
173
+ }
174
+ if (subcommand === 'install') {
175
+ ensureDataDirs(paths);
176
+ const result = installHook(paths, { cwd: io.cwd, scope, surface, command: io.commandPath });
177
+ writeOutput(io.stdout, json ? result : `Installed ${scope} ${surface} hook config: ${result.target}`, json);
178
+ return;
179
+ }
180
+ throw new Error(`Unknown hooks action "${subcommand}". Use preview or install.`);
181
+ }
182
+
183
+ if (command === 'hook-log') {
184
+ const payload = await readJsonFromStream(io.stdin);
185
+ const result = appendHookEvent(payload, {
186
+ env: io.env,
187
+ cwd: io.cwd,
188
+ home: flags.home,
189
+ event: flags.event,
190
+ includePromptPreview: flags.includePromptPreview === true,
191
+ });
192
+ if (flags.quiet === true) return;
193
+ writeOutput(io.stdout, json ? result : `Logged hook event: ${result.path}`, json);
194
+ return;
195
+ }
196
+
197
+ if (command === 'store') {
198
+ if (subcommand !== 'init') {
199
+ throw new Error(`Unknown store action "${subcommand}". Use init.`);
200
+ }
201
+ ensureDataDirs(paths);
202
+ await initStore(paths.usageDb);
203
+ writeOutput(io.stdout, json ? { dbPath: paths.usageDb } : `Initialized SQLite store: ${paths.usageDb}`, json);
204
+ return;
205
+ }
206
+
207
+ if (command === 'import') {
208
+ const source = flags.source;
209
+ const file = flags.file || (source === 'vscode'
210
+ ? paths.vscodeOtelJsonl
211
+ : source === 'copilot-cli'
212
+ ? paths.copilotCliOtelJsonl
213
+ : source === 'hooks'
214
+ ? paths.hookEventsJsonl
215
+ : null);
216
+ if (!['vscode', 'copilot-cli', 'hooks'].includes(source)) {
217
+ throw new Error('import requires --source vscode|copilot-cli|hooks');
218
+ }
219
+ if (!file) throw new Error('import requires --file <path>');
220
+ ensureDataDirs(paths);
221
+ const result = await ingestFile({
222
+ dbPath: paths.usageDb,
223
+ file,
224
+ source,
225
+ extractors: loadConfiguredExtractors(paths.configJson, io.cwd),
226
+ });
227
+ writeOutput(io.stdout, json ? result : [
228
+ `Imported ${result.raw_records} raw ${source} records into ${result.dbPath}`,
229
+ `Normalized usage records: ${result.usage_records}`,
230
+ `Hook events: ${result.hook_events}`,
231
+ `Label evidence: ${result.label_evidence}`,
232
+ `Warnings: ${result.warnings.length}`,
233
+ `Costs are ${result.estimate_label}`,
234
+ ].join('\n'), json);
235
+ return;
236
+ }
237
+
238
+ if (command === 'report') {
239
+ if (subcommand === 'labels') {
240
+ const rows = await labelOverview(paths.usageDb);
241
+ writeOutput(io.stdout, json ? { labels: rows } : formatLabels(rows), json);
242
+ return;
243
+ }
244
+ if (subcommand === 'label') {
245
+ const label = rest[2];
246
+ if (!label) throw new Error('report label requires <id>');
247
+ const summary = await labelSummary(paths.usageDb, label);
248
+ if (flags.detail === true) {
249
+ const details = await labelDetails(paths.usageDb, label);
250
+ writeOutput(io.stdout, json ? { label: summary, details } : formatLabelSummary(summary, details), json);
251
+ return;
252
+ }
253
+ writeOutput(io.stdout, json ? { label: summary } : formatLabelSummary(summary), json);
254
+ return;
255
+ }
256
+ if (subcommand === 'models') {
257
+ const rows = await modelReport(paths.usageDb);
258
+ writeOutput(io.stdout, json ? { models: rows } : formatModels(rows), json);
259
+ return;
260
+ }
261
+ if (subcommand === 'repos') {
262
+ const rows = await repoReport(paths.usageDb);
263
+ writeOutput(io.stdout, json ? { repos: rows } : formatRepos(rows), json);
264
+ return;
265
+ }
266
+ if (subcommand === 'unattributed') {
267
+ const rows = await unattributedReport(paths.usageDb);
268
+ writeOutput(io.stdout, json ? { unattributed: rows } : formatUnattributed(rows), json);
269
+ return;
270
+ }
271
+ throw new Error(`Unknown report "${subcommand}". Use labels, label, models, repos, or unattributed.`);
272
+ }
273
+
274
+ if (command === 'pricing') {
275
+ if (subcommand !== 'list') throw new Error(`Unknown pricing action "${subcommand}". Use list.`);
276
+ const payload = { version: PRICING_VERSION, unit: 'USD per 1M tokens', models: MODEL_PRICES };
277
+ writeOutput(io.stdout, json ? payload : JSON.stringify(payload, null, 2), json);
278
+ return;
279
+ }
280
+
281
+ throw new Error(`Unknown command "${command}". Run copilot-metrics --help.`);
282
+ }
283
+
284
+ module.exports = {
285
+ main,
286
+ parseFlags,
287
+ helpText,
288
+ };
@@ -0,0 +1,79 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const { resolvePaths } = require('./paths');
6
+
7
+ const LABEL_RE = /\b[A-Z][A-Z0-9]+-\d+\b/g;
8
+
9
+ function extractLabelsFromValue(value, labels = new Set()) {
10
+ if (typeof value === 'string') {
11
+ for (const match of value.matchAll(LABEL_RE)) labels.add(match[0]);
12
+ return labels;
13
+ }
14
+ if (Array.isArray(value)) {
15
+ for (const item of value) extractLabelsFromValue(item, labels);
16
+ return labels;
17
+ }
18
+ if (value && typeof value === 'object') {
19
+ for (const item of Object.values(value)) extractLabelsFromValue(item, labels);
20
+ }
21
+ return labels;
22
+ }
23
+
24
+ function firstString(payload, keys) {
25
+ for (const key of keys) {
26
+ const value = payload && payload[key];
27
+ if (typeof value === 'string' && value.trim()) return value;
28
+ }
29
+ return null;
30
+ }
31
+
32
+ function redactHookPayload(payload, options = {}) {
33
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
34
+ payload = {};
35
+ }
36
+ const includePromptPreview = options.includePromptPreview === true;
37
+ const labels = Array.from(extractLabelsFromValue(payload)).sort();
38
+ const prompt = firstString(payload, ['prompt', 'userPrompt', 'message', 'input']);
39
+
40
+ return {
41
+ captured_at: new Date().toISOString(),
42
+ event: options.event || firstString(payload, ['event', 'hookEventName', 'hook_event_name']) || null,
43
+ session_id: firstString(payload, ['session_id', 'sessionId', 'conversationId']),
44
+ cwd: firstString(payload, ['cwd', 'workingDirectory']),
45
+ repo: firstString(payload, ['repo', 'repository']),
46
+ branch: firstString(payload, ['branch', 'gitBranch']),
47
+ transcript_path: firstString(payload, ['transcript_path', 'transcriptPath']),
48
+ task_hint: firstString(payload, ['task_hint', 'taskHint', 'title']),
49
+ tool_name: firstString(payload, ['tool_name', 'toolName', 'name']),
50
+ surface: firstString(payload, ['surface', 'agentSurface']),
51
+ labels,
52
+ prompt_preview: includePromptPreview && prompt ? prompt.slice(0, 160) : undefined,
53
+ raw_prompt_stored: false,
54
+ };
55
+ }
56
+
57
+ function appendHookEvent(payload, options = {}) {
58
+ const paths = resolvePaths(options);
59
+ const record = redactHookPayload(payload, options);
60
+ fs.mkdirSync(path.dirname(paths.hookEventsJsonl), { recursive: true, mode: 0o700 });
61
+ fs.appendFileSync(paths.hookEventsJsonl, `${JSON.stringify(record)}\n`, { mode: 0o600 });
62
+ return { path: paths.hookEventsJsonl, record };
63
+ }
64
+
65
+ async function readJsonFromStream(stream) {
66
+ const chunks = [];
67
+ for await (const chunk of stream) chunks.push(Buffer.from(chunk));
68
+ const text = Buffer.concat(chunks).toString('utf8').trim();
69
+ if (!text) return {};
70
+ return JSON.parse(text);
71
+ }
72
+
73
+ module.exports = {
74
+ LABEL_RE,
75
+ extractLabelsFromValue,
76
+ redactHookPayload,
77
+ appendHookEvent,
78
+ readJsonFromStream,
79
+ };
package/src/ingest.js ADDED
@@ -0,0 +1,71 @@
1
+ 'use strict';
2
+
3
+ const { readJsonl } = require('./jsonl');
4
+ const { normalizePayload, normalizeHookEvent } = require('./otel');
5
+ const { estimateCost, PRICING_VERSION } = require('./pricing');
6
+ const { insertImport } = require('./sqlite-store');
7
+ const { attachUsageLabelEvidence, attachHookLabelEvidence } = require('./labels');
8
+
9
+ function enrichCosts(records) {
10
+ return records.map((record) => {
11
+ const estimate = estimateCost(record);
12
+ const warnings = [...record.warnings];
13
+ if (estimate.warning) warnings.push(estimate.warning);
14
+ return {
15
+ ...record,
16
+ estimated_usd: estimate.estimated_usd,
17
+ estimated_ai_credits: estimate.estimated_ai_credits,
18
+ estimate_label: `estimate:${PRICING_VERSION}`,
19
+ warnings,
20
+ };
21
+ });
22
+ }
23
+
24
+ async function ingestFile(options) {
25
+ const { dbPath, file, source } = options;
26
+ const parsed = readJsonl(file);
27
+ const warnings = [...parsed.warnings];
28
+ const usageRecords = [];
29
+ const hookEvents = [];
30
+
31
+ for (const record of parsed.records) {
32
+ if (source === 'hooks') {
33
+ const event = normalizeHookEvent(record.value, source, record.line);
34
+ if (event) hookEvents.push(event);
35
+ continue;
36
+ }
37
+ usageRecords.push(...normalizePayload(record.value, source, record.line));
38
+ }
39
+
40
+ const extractorOptions = { extractors: options.extractors || [] };
41
+ const enrichedUsage = attachUsageLabelEvidence(enrichCosts(usageRecords), extractorOptions);
42
+ const enrichedHooks = attachHookLabelEvidence(hookEvents, extractorOptions);
43
+ for (const usage of enrichedUsage) {
44
+ for (const warning of usage.warnings) {
45
+ warnings.push({
46
+ code: warning.split(':')[0],
47
+ line: usage.raw_line,
48
+ message: warning,
49
+ });
50
+ }
51
+ }
52
+
53
+ await insertImport(dbPath, source, parsed.records, enrichedUsage, enrichedHooks, warnings);
54
+
55
+ return {
56
+ source,
57
+ file,
58
+ dbPath,
59
+ raw_records: parsed.records.length,
60
+ usage_records: enrichedUsage.length,
61
+ hook_events: enrichedHooks.length,
62
+ label_evidence: enrichedUsage.reduce((sum, usage) => sum + (usage.label_evidence || []).length, 0)
63
+ + enrichedHooks.reduce((sum, event) => sum + (event.label_evidence || []).length, 0),
64
+ warnings,
65
+ estimate_label: `estimate:${PRICING_VERSION}`,
66
+ };
67
+ }
68
+
69
+ module.exports = {
70
+ ingestFile,
71
+ };
package/src/jsonl.js ADDED
@@ -0,0 +1,28 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+
5
+ function readJsonl(file) {
6
+ const text = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '';
7
+ const records = [];
8
+ const warnings = [];
9
+
10
+ text.split(/\r?\n/).forEach((line, index) => {
11
+ if (!line.trim()) return;
12
+ try {
13
+ records.push({ line: index + 1, value: JSON.parse(line) });
14
+ } catch (error) {
15
+ warnings.push({
16
+ code: 'malformed_jsonl',
17
+ line: index + 1,
18
+ message: error.message,
19
+ });
20
+ }
21
+ });
22
+
23
+ return { records, warnings };
24
+ }
25
+
26
+ module.exports = {
27
+ readJsonl,
28
+ };
@@ -0,0 +1,120 @@
1
+ 'use strict';
2
+
3
+ const path = require('node:path');
4
+
5
+ const JIRA_LABEL_RE = /\b([A-Z][A-Z0-9]+-\d+)\b/gi;
6
+
7
+ function canonicalLabel(label) {
8
+ return String(label || '').trim().toUpperCase();
9
+ }
10
+
11
+ function fieldConfidence(field) {
12
+ if (field === 'labels') return 1;
13
+ if (field === 'branch') return 0.85;
14
+ if (field === 'cwd') return 0.75;
15
+ if (field === 'task_hint') return 0.7;
16
+ if (field === 'repo') return 0.35;
17
+ return 0.5;
18
+ }
19
+
20
+ function sourceValue(field, value, label) {
21
+ if (['prompt', 'message', 'input', 'prompt_preview', 'task_hint'].includes(field)) return label;
22
+ if (typeof value === 'string') return value.slice(0, 240);
23
+ if (Array.isArray(value)) return value.map((item) => String(item).slice(0, 120)).join(',');
24
+ return label;
25
+ }
26
+
27
+ function extractJiraLabels(sourceType, sourceData = {}) {
28
+ const evidence = [];
29
+ const fields = {
30
+ labels: sourceData.labels,
31
+ branch: sourceData.branch,
32
+ cwd: sourceData.cwd,
33
+ repo: sourceData.repo,
34
+ task_hint: sourceData.task_hint,
35
+ prompt: sourceData.prompt,
36
+ prompt_preview: sourceData.prompt_preview,
37
+ message: sourceData.message,
38
+ input: sourceData.input,
39
+ };
40
+
41
+ for (const [field, value] of Object.entries(fields)) {
42
+ const values = Array.isArray(value) ? value : [value];
43
+ for (const item of values) {
44
+ if (item === undefined || item === null) continue;
45
+ const text = String(item);
46
+ for (const match of text.matchAll(JIRA_LABEL_RE)) {
47
+ const label = canonicalLabel(match[1]);
48
+ evidence.push({
49
+ label,
50
+ source_type: sourceType,
51
+ source_field: field,
52
+ source_value: sourceValue(field, item, label),
53
+ confidence: fieldConfidence(field),
54
+ });
55
+ }
56
+ }
57
+ }
58
+
59
+ return evidence;
60
+ }
61
+
62
+ function normalizeExtractorResult(result, sourceType) {
63
+ const items = Array.isArray(result) ? result : [];
64
+ return items
65
+ .map((item) => (typeof item === 'string' ? { label: item } : item))
66
+ .filter((item) => item && item.label)
67
+ .map((item) => ({
68
+ label: canonicalLabel(item.label),
69
+ source_type: item.source_type || sourceType,
70
+ source_field: item.source_field || item.field || 'custom',
71
+ source_value: item.source_value || item.value || canonicalLabel(item.label),
72
+ confidence: Number.isFinite(Number(item.confidence)) ? Number(item.confidence) : 0.5,
73
+ }));
74
+ }
75
+
76
+ function runLabelExtractors(sourceType, sourceData, customExtractors = []) {
77
+ const extractors = [extractJiraLabels, ...customExtractors];
78
+ const seen = new Set();
79
+ const evidence = [];
80
+
81
+ for (const extractor of extractors) {
82
+ const results = normalizeExtractorResult(extractor(sourceType, sourceData), sourceType);
83
+ for (const item of results) {
84
+ const key = `${item.label}\0${item.source_type}\0${item.source_field}\0${item.source_value}`;
85
+ if (seen.has(key)) continue;
86
+ seen.add(key);
87
+ evidence.push(item);
88
+ }
89
+ }
90
+
91
+ return evidence;
92
+ }
93
+
94
+ function loadConfiguredExtractors(configPath, cwd = process.cwd()) {
95
+ let config;
96
+ try {
97
+ config = require(configPath);
98
+ } catch {
99
+ return [];
100
+ }
101
+
102
+ const configured = Array.isArray(config.labelExtractors) ? config.labelExtractors : [];
103
+ return configured.map((entry) => {
104
+ const modulePath = typeof entry === 'string' ? entry : entry && entry.path;
105
+ if (!modulePath) return null;
106
+ const resolved = path.isAbsolute(modulePath) ? modulePath : path.resolve(cwd, modulePath);
107
+ const mod = require(resolved);
108
+ if (typeof mod === 'function') return mod;
109
+ if (typeof mod.extractLabels === 'function') return mod.extractLabels;
110
+ throw new Error(`Configured label extractor does not export a function: ${modulePath}`);
111
+ }).filter(Boolean);
112
+ }
113
+
114
+ module.exports = {
115
+ JIRA_LABEL_RE,
116
+ canonicalLabel,
117
+ extractJiraLabels,
118
+ loadConfiguredExtractors,
119
+ runLabelExtractors,
120
+ };
package/src/labels.js ADDED
@@ -0,0 +1,56 @@
1
+ 'use strict';
2
+
3
+ const { runLabelExtractors } = require('./label-extractors');
4
+
5
+ function usageSourceData(usage) {
6
+ return {
7
+ labels: usage.labels || [],
8
+ repo: usage.repo,
9
+ branch: usage.branch,
10
+ cwd: usage.cwd,
11
+ session_id: usage.session_id,
12
+ conversation_id: usage.conversation_id,
13
+ task_hint: usage.task_hint,
14
+ prompt: usage.prompt,
15
+ prompt_preview: usage.prompt_preview,
16
+ message: usage.message,
17
+ input: usage.input,
18
+ };
19
+ }
20
+
21
+ function hookSourceData(event) {
22
+ const payload = event.payload || {};
23
+ return {
24
+ labels: event.labels || payload.labels || [],
25
+ repo: event.repo || payload.repo,
26
+ branch: event.branch || payload.branch,
27
+ cwd: event.cwd || payload.cwd,
28
+ session_id: event.session_id || payload.session_id,
29
+ task_hint: event.task_hint || payload.task_hint || payload.taskHint,
30
+ prompt: payload.prompt,
31
+ prompt_preview: payload.prompt_preview,
32
+ message: payload.message,
33
+ input: payload.input,
34
+ };
35
+ }
36
+
37
+ function attachUsageLabelEvidence(usageRecords, options = {}) {
38
+ return usageRecords.map((usage) => ({
39
+ ...usage,
40
+ label_evidence: runLabelExtractors('usage', usageSourceData(usage), options.extractors),
41
+ }));
42
+ }
43
+
44
+ function attachHookLabelEvidence(hookEvents, options = {}) {
45
+ return hookEvents.map((event) => ({
46
+ ...event,
47
+ label_evidence: runLabelExtractors('hook', hookSourceData(event), options.extractors),
48
+ }));
49
+ }
50
+
51
+ module.exports = {
52
+ usageSourceData,
53
+ hookSourceData,
54
+ attachUsageLabelEvidence,
55
+ attachHookLabelEvidence,
56
+ };