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/CHANGELOG.md ADDED
@@ -0,0 +1,22 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 - 2026-05-30
4
+
5
+ First local release candidate for `copilot-metrics`.
6
+
7
+ ### Added
8
+
9
+ - CLI setup helpers for central local data directories, VS Code Copilot OTel settings, Copilot CLI OTel environment exports, and local/global hook config.
10
+ - Redacted hook logger that captures safe attribution metadata with content capture disabled by default.
11
+ - Local SQLite-backed import for VS Code Copilot OTel JSONL, Copilot CLI OTel JSONL, and Copilot CLI hook JSONL.
12
+ - LLM span normalization, root-agent double-count prevention, token extraction, model pricing estimates, AI Credit estimates, malformed-row warnings, and unknown-model warnings.
13
+ - Jira-style label extraction with evidence-preserving attribution by source, field, session, repo, branch, cwd, and confidence.
14
+ - Configurable custom label extractors loaded from local config without modifying package source.
15
+ - CLI reports for label overview, single-label summary/detail, models, repos/directories, and unattributed usage, with human and JSON output.
16
+ - Release smoke checks, package verification, GitHub Actions npm publishing workflow, MIT license, and release checklist.
17
+
18
+ ### Notes
19
+
20
+ - Cost and AI Credit values are estimates only. GitHub billing remains the source of truth.
21
+ - Prompt/content capture is disabled by default.
22
+ - Official usage reconciliation, collector mode, richer privacy controls, and dashboard views are deferred.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 copilot-metrics contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,174 @@
1
+ # Copilot Metrics
2
+
3
+ `copilot-metrics` is a local-first CLI for estimating GitHub Copilot usage from local OpenTelemetry and hook metadata. It helps answer which Jira-style labels, repos, models, and Copilot surfaces are driving estimated AI Credit usage.
4
+
5
+ Costs are estimates, not official billing records. GitHub billing remains the source of truth.
6
+
7
+ ## Install
8
+
9
+ From npm:
10
+
11
+ ```bash
12
+ npx copilot-metrics@0.1.0 --help
13
+ npx copilot-metrics@0.1.0 init
14
+ ```
15
+
16
+ From this checkout:
17
+
18
+ ```bash
19
+ npm ci
20
+ npm test
21
+ npm run cli -- --help
22
+ ```
23
+
24
+ ## Data Directory
25
+
26
+ By default, all metadata is stored in a user-level local folder:
27
+
28
+ - Linux: `$XDG_DATA_HOME/copilot-metrics` or `~/.local/share/copilot-metrics`
29
+ - macOS: `~/Library/Application Support/copilot-metrics`
30
+ - Windows: `%LOCALAPPDATA%\\copilot-metrics`
31
+
32
+ Override it with:
33
+
34
+ ```bash
35
+ export COPILOT_METRICS_HOME=/path/to/copilot-metrics-data
36
+ ```
37
+
38
+ Useful commands:
39
+
40
+ ```bash
41
+ npx copilot-metrics@0.1.0 init
42
+ npx copilot-metrics@0.1.0 paths --json
43
+ ```
44
+
45
+ ## Configure Telemetry
46
+
47
+ Print VS Code Insiders Copilot Chat OpenTelemetry settings:
48
+
49
+ ```bash
50
+ npx copilot-metrics@0.1.0 setup vscode
51
+ ```
52
+
53
+ Print Copilot CLI OpenTelemetry environment exports:
54
+
55
+ ```bash
56
+ npx copilot-metrics@0.1.0 setup copilot-cli
57
+ ```
58
+
59
+ Content capture is disabled by default. Do not enable richer prompt capture unless you explicitly accept the privacy tradeoff.
60
+
61
+ ## Configure Hooks
62
+
63
+ Preview repo-local hook config. The default `--surface both` emits the Copilot CLI lower camel case hook format:
64
+
65
+ ```bash
66
+ npx copilot-metrics@0.1.0 hooks preview --scope local --surface both
67
+ ```
68
+
69
+ Install repo-local or user-global hook config:
70
+
71
+ ```bash
72
+ npx copilot-metrics@0.1.0 hooks install --scope local --surface both
73
+ npx copilot-metrics@0.1.0 hooks install --scope global --surface both
74
+ ```
75
+
76
+ Local install writes `.github/hooks/copilot-metrics.json`. Global install updates `~/.copilot/settings.json` idempotently, replacing prior `copilot-metrics` hook entries while preserving other settings and hooks. Use `--surface vscode` for VS Code-only PascalCase events or `--surface copilot-cli` for CLI-native lower camel case events. The hook logger writes redacted JSONL metadata to the central data directory. It extracts Jira-style labels such as `DEMO-12345` from safe metadata and does not store full prompt text by default.
77
+
78
+ ## Import Telemetry
79
+
80
+ Initialize the local SQLite store and import JSONL files:
81
+
82
+ ```bash
83
+ npx copilot-metrics@0.1.0 store init
84
+ npx copilot-metrics@0.1.0 import --source vscode --file ~/.local/share/copilot-metrics/telemetry/vscode-copilot-otel.jsonl
85
+ npx copilot-metrics@0.1.0 import --source copilot-cli --file ~/.local/share/copilot-metrics/telemetry/copilot-cli-otel.jsonl
86
+ npx copilot-metrics@0.1.0 import --source hooks --file ~/.local/share/copilot-metrics/hooks/copilot-hooks.jsonl
87
+ ```
88
+
89
+ Imports persist raw records, normalized LLM usage records, hook events, label evidence, and import warnings.
90
+
91
+ ## Reports
92
+
93
+ Run local reports from the SQLite store:
94
+
95
+ ```bash
96
+ npx copilot-metrics@0.1.0 report labels
97
+ npx copilot-metrics@0.1.0 report label DEMO-12345
98
+ npx copilot-metrics@0.1.0 report label DEMO-12345 --detail
99
+ npx copilot-metrics@0.1.0 report models
100
+ npx copilot-metrics@0.1.0 report repos
101
+ npx copilot-metrics@0.1.0 report unattributed
102
+ ```
103
+
104
+ Every report supports `--json`:
105
+
106
+ ```bash
107
+ npx copilot-metrics@0.1.0 report labels --json
108
+ ```
109
+
110
+ ## Attribution Model
111
+
112
+ The default extractor finds Jira-style labels such as `DEMO-12345` from safe metadata including hook labels, branch names, cwd/path values, repo metadata, and task hints.
113
+
114
+ Attribution is stored as evidence with source, field, session, repo, branch, cwd, confidence, and related usage or hook record IDs. This makes the data useful for later analysis, such as deciding whether a label was the main task or a sidetrack.
115
+
116
+ Full prompt content is not stored by default. Prompt-like fields are only used to extract labels and the stored source value is reduced to the matched label.
117
+
118
+ ## Custom Label Extractors
119
+
120
+ Custom extractors are configured in the local `config.json`; you do not modify package source.
121
+
122
+ After `copilot-metrics init`, add a module path:
123
+
124
+ ```json
125
+ {
126
+ "labelExtractors": ["/absolute/path/to/my-extractor.cjs"]
127
+ }
128
+ ```
129
+
130
+ Relative paths are resolved from the current working directory when the CLI runs.
131
+
132
+ The module should export a function, or an object with `extractLabels`. Each extractor receives:
133
+
134
+ - `sourceType`: for example `usage` or `hook`
135
+ - `sourceData`: safe metadata for that source
136
+
137
+ It returns zero or more labels, either as strings or evidence objects:
138
+
139
+ ```js
140
+ const extractor = (sourceType, sourceData) => {
141
+ if (sourceData.branch === 'main') return [];
142
+ return [{ label: 'TEAM-123', source_field: 'branch', source_type: sourceType, confidence: 0.5 }];
143
+ };
144
+
145
+ module.exports = extractor;
146
+ ```
147
+
148
+ ## Release Verification
149
+
150
+ For a release candidate checkout:
151
+
152
+ ```bash
153
+ npm test
154
+ npm run check
155
+ npm run smoke
156
+ npm run verify:package
157
+ ```
158
+
159
+ Manual Copilot CLI validation is local-only and not run in CI:
160
+
161
+ ```bash
162
+ node scripts/manual-copilot-cli-flow.js --setup-only
163
+ node scripts/manual-copilot-cli-flow.js --run-prompt --model gpt-5-mini
164
+ ```
165
+
166
+ The manual prompt performs one harmless tool call so Copilot CLI hook execution can be validated; answer quality is not part of the check. During the prompt run, the helper temporarily adds generated hooks to `~/.copilot/settings.json` and restores the original settings afterward.
167
+
168
+ ## Current Limits
169
+
170
+ - Costs are estimates, not official billing records.
171
+ - Official GitHub usage report reconciliation is not included in `0.1.0`.
172
+ - Local OTLP collector mode is not included in `0.1.0`.
173
+ - Richer prompt/content capture and redaction controls are not included in `0.1.0`.
174
+ - Dashboard views are deferred until the CLI/query model proves useful.
package/RELEASE.md ADDED
@@ -0,0 +1,74 @@
1
+ # Release Checklist
2
+
3
+ This project publishes `copilot-metrics` to npm through GitHub Actions. The human gate is creating the GitHub release or tag that triggers the workflow; the workflow runs verification and then `npm publish`.
4
+
5
+ ## Prerequisites
6
+
7
+ - GitHub repository exists at `nnexai/copilot-metrics`.
8
+ - npm Trusted Publishing is configured for package `copilot-metrics`:
9
+ - Publisher: GitHub Actions
10
+ - Repository: `nnexai/copilot-metrics`
11
+ - Workflow filename: `npm-publish.yml`
12
+ - Allowed action: `npm publish`
13
+ - npm package name is available or owned by the publishing account.
14
+ - Local working tree is clean before tagging.
15
+
16
+ ## Local Verification
17
+
18
+ Run:
19
+
20
+ ```bash
21
+ npm ci
22
+ npm test
23
+ npm run check
24
+ npm run smoke
25
+ npm run verify:package
26
+ npm pack --dry-run --json
27
+ ```
28
+
29
+ The package must not include `.planning/`, `.codex/`, `test/`, fixture data, local telemetry, or generated SQLite stores.
30
+
31
+ ## Manual Copilot CLI Validation
32
+
33
+ This is not a CI step because it depends on a local authenticated Copilot CLI and can call external services.
34
+
35
+ Run setup/dry-run first:
36
+
37
+ ```bash
38
+ node scripts/manual-copilot-cli-flow.js --setup-only
39
+ ```
40
+
41
+ Run the full local validation from this checkout:
42
+
43
+ ```bash
44
+ node scripts/manual-copilot-cli-flow.js --run-prompt --model gpt-5-mini
45
+ ```
46
+
47
+ The script creates an example workspace, configures `copilot-metrics`, installs repo-local hook config, temporarily applies user-level Copilot CLI hook settings for the prompt run, imports collected telemetry/hook JSONL, prints report output, and restores the original Copilot settings.
48
+
49
+ ## GitHub Actions Publish
50
+
51
+ 1. Confirm `package.json` version is `0.1.0`.
52
+ 2. Commit all release changes.
53
+ 3. Push `main`.
54
+ 4. Create a GitHub release for `v0.1.0`.
55
+ 5. Confirm the `Node.js Package` workflow passes and publishes to npm through Trusted Publishing.
56
+
57
+ ## Post-Publish Verification
58
+
59
+ After the workflow publishes:
60
+
61
+ ```bash
62
+ npm view copilot-metrics@0.1.0 version
63
+ npm view copilot-metrics@0.1.0 dist.tarball
64
+ npx copilot-metrics@0.1.0 --help
65
+ npx copilot-metrics@0.1.0 paths --json
66
+ ```
67
+
68
+ ## Do Not Publish
69
+
70
+ - Local telemetry JSONL files
71
+ - Generated SQLite stores
72
+ - `.planning/` artifacts
73
+ - `.codex/` runtime files
74
+ - Prompt content or hook logs
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { main } = require('../src/cli');
5
+
6
+ main(process.argv.slice(2), {
7
+ stdin: process.stdin,
8
+ stdout: process.stdout,
9
+ stderr: process.stderr,
10
+ env: process.env,
11
+ cwd: process.cwd(),
12
+ commandPath: process.argv[1],
13
+ }).catch((error) => {
14
+ process.stderr.write(`copilot-metrics: ${error.message}\n`);
15
+ process.exitCode = 1;
16
+ });
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "copilot-metrics",
3
+ "version": "0.1.0",
4
+ "description": "Local-first Copilot usage telemetry setup and reporting tools.",
5
+ "type": "commonjs",
6
+ "homepage": "https://github.com/nnexai/copilot-metrics#readme",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+ssh://git@github.com/nnexai/copilot-metrics.git"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/nnexai/copilot-metrics/issues"
13
+ },
14
+ "bin": {
15
+ "copilot-metrics": "bin/copilot-metrics.js"
16
+ },
17
+ "files": [
18
+ "bin/",
19
+ "src/",
20
+ "skills/",
21
+ "scripts/manual-copilot-cli-flow.js",
22
+ "README.md",
23
+ "CHANGELOG.md",
24
+ "RELEASE.md",
25
+ "LICENSE"
26
+ ],
27
+ "scripts": {
28
+ "cli": "node bin/copilot-metrics.js",
29
+ "check": "node --check bin/copilot-metrics.js && node --check src/*.js",
30
+ "smoke": "node scripts/smoke.js",
31
+ "verify:package": "node scripts/verify-package.js",
32
+ "test": "node --test"
33
+ },
34
+ "keywords": [
35
+ "copilot",
36
+ "telemetry",
37
+ "opentelemetry",
38
+ "cli",
39
+ "ai-credits"
40
+ ],
41
+ "engines": {
42
+ "node": ">=20"
43
+ },
44
+ "license": "MIT",
45
+ "dependencies": {
46
+ "sql.js": "^1.14.1"
47
+ }
48
+ }
@@ -0,0 +1,134 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { execFileSync, spawnSync } = require('node:child_process');
5
+ const fs = require('node:fs');
6
+ const os = require('node:os');
7
+ const path = require('node:path');
8
+ const { mergeGlobalSettingsHooks } = require('../src/setup');
9
+
10
+ const root = path.join(__dirname, '..');
11
+ const cli = path.join(root, 'bin', 'copilot-metrics.js');
12
+
13
+ function hasFlag(name) {
14
+ return process.argv.includes(name);
15
+ }
16
+
17
+ function option(name, fallback) {
18
+ const index = process.argv.indexOf(name);
19
+ return index >= 0 && process.argv[index + 1] ? process.argv[index + 1] : fallback;
20
+ }
21
+
22
+ function run(command, args, options = {}) {
23
+ return execFileSync(command, args, {
24
+ encoding: 'utf8',
25
+ stdio: options.stdio || 'pipe',
26
+ cwd: options.cwd,
27
+ env: options.env,
28
+ });
29
+ }
30
+
31
+ function runCli(args, cwd, home) {
32
+ return run(process.execPath, [cli, ...args, '--home', home], { cwd });
33
+ }
34
+
35
+ const workspace = path.resolve(option('--workspace', fs.mkdtempSync(path.join(os.tmpdir(), 'copilot-metrics-example-'))));
36
+ const metricsHome = path.resolve(option('--home', path.join(workspace, '.copilot-metrics-data')));
37
+ const model = option('--model', 'gpt-5-mini');
38
+ const prompt = option(
39
+ '--prompt',
40
+ 'Run pwd once, then reply with exactly: copilot-metrics validation ok',
41
+ );
42
+ const runPrompt = hasFlag('--run-prompt');
43
+ const setupOnly = hasFlag('--setup-only') || !runPrompt;
44
+
45
+ fs.mkdirSync(workspace, { recursive: true });
46
+ fs.writeFileSync(path.join(workspace, 'README.md'), '# copilot-metrics validation workspace\n', { flag: 'a' });
47
+ if (!fs.existsSync(path.join(workspace, '.git'))) {
48
+ run('git', ['init'], { cwd: workspace });
49
+ }
50
+ try {
51
+ run('git', ['remote', 'get-url', 'origin'], { cwd: workspace });
52
+ } catch {
53
+ run('git', ['remote', 'add', 'origin', 'https://github.com/nnexai/copilot-metrics.git'], { cwd: workspace });
54
+ }
55
+
56
+ runCli(['init', '--json'], workspace, metricsHome);
57
+ const hookInstall = JSON.parse(runCli(['hooks', 'install', '--scope', 'local', '--surface', 'both', '--json'], workspace, metricsHome));
58
+ const envConfig = JSON.parse(runCli(['setup', 'copilot-cli', '--json'], workspace, metricsHome));
59
+ const paths = JSON.parse(runCli(['paths', '--json'], workspace, metricsHome));
60
+
61
+ const envFile = path.join(workspace, '.copilot-metrics.env');
62
+ fs.writeFileSync(envFile, `${Object.entries(envConfig).map(([key, value]) => `export ${key}=${JSON.stringify(value)}`).join('\n')}\n`);
63
+
64
+ const result = {
65
+ workspace,
66
+ metricsHome,
67
+ envFile,
68
+ hookConfig: hookInstall.target,
69
+ temporaryGlobalSettings: path.join(process.env.COPILOT_HOME || path.join(os.homedir(), '.copilot'), 'settings.json'),
70
+ copilotCliOtelJsonl: paths.copilotCliOtelJsonl,
71
+ hookEventsJsonl: paths.hookEventsJsonl,
72
+ ranPrompt: false,
73
+ telemetryExists: false,
74
+ hooksExist: false,
75
+ };
76
+
77
+ if (setupOnly) {
78
+ process.stdout.write(`${JSON.stringify({ ...result, next: 'Re-run with --run-prompt to call Copilot CLI.' }, null, 2)}\n`);
79
+ process.exit(0);
80
+ }
81
+
82
+ const settingsExisted = fs.existsSync(result.temporaryGlobalSettings);
83
+ const originalSettings = settingsExisted ? fs.readFileSync(result.temporaryGlobalSettings, 'utf8') : null;
84
+ let copilot;
85
+ try {
86
+ const settings = settingsExisted ? JSON.parse(originalSettings) : {};
87
+ fs.mkdirSync(path.dirname(result.temporaryGlobalSettings), { recursive: true });
88
+ fs.writeFileSync(
89
+ result.temporaryGlobalSettings,
90
+ `${JSON.stringify(mergeGlobalSettingsHooks(settings, hookInstall.config.hooks), null, 2)}\n`,
91
+ );
92
+
93
+ copilot = spawnSync('copilot', ['-p', prompt, '--yolo', '--model', model, '--no-auto-update', '--silent'], {
94
+ cwd: workspace,
95
+ env: {
96
+ ...process.env,
97
+ ...envConfig,
98
+ COPILOT_METRICS_HOME: metricsHome,
99
+ },
100
+ encoding: 'utf8',
101
+ maxBuffer: 1024 * 1024 * 8,
102
+ });
103
+ } finally {
104
+ if (settingsExisted) fs.writeFileSync(result.temporaryGlobalSettings, originalSettings);
105
+ else if (fs.existsSync(result.temporaryGlobalSettings)) fs.rmSync(result.temporaryGlobalSettings);
106
+ }
107
+
108
+ result.ranPrompt = true;
109
+ result.exitCode = copilot.status;
110
+ result.stdout = (copilot.stdout || '').slice(0, 2000);
111
+ result.stderr = (copilot.stderr || '').slice(0, 2000);
112
+
113
+ if (copilot.status !== 0) {
114
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
115
+ process.exit(copilot.status || 1);
116
+ }
117
+
118
+ result.telemetryExists = fs.existsSync(paths.copilotCliOtelJsonl) && fs.statSync(paths.copilotCliOtelJsonl).size > 0;
119
+ result.hooksExist = fs.existsSync(paths.hookEventsJsonl) && fs.statSync(paths.hookEventsJsonl).size > 0;
120
+
121
+ if (result.telemetryExists) {
122
+ result.import = JSON.parse(runCli(['import', '--source', 'copilot-cli', '--file', paths.copilotCliOtelJsonl, '--json'], workspace, metricsHome));
123
+ result.models = JSON.parse(runCli(['report', 'models', '--json'], workspace, metricsHome));
124
+ }
125
+
126
+ if (result.hooksExist) {
127
+ result.hooksImport = JSON.parse(runCli(['import', '--source', 'hooks', '--file', paths.hookEventsJsonl, '--json'], workspace, metricsHome));
128
+ }
129
+
130
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
131
+
132
+ if (!result.telemetryExists || !result.hooksExist) {
133
+ process.exitCode = 2;
134
+ }
@@ -0,0 +1,36 @@
1
+ ---
2
+ name: copilot-metrics
3
+ description: Query local Copilot Metrics data through the CLI without reading sensitive raw prompt content by default.
4
+ ---
5
+
6
+ # Copilot Metrics Skill
7
+
8
+ Use this skill when the user asks which Jira labels, repos, models, or Copilot surfaces are driving estimated local Copilot usage.
9
+
10
+ ## Rules
11
+
12
+ - Use the `copilot-metrics` CLI instead of reading raw telemetry files directly.
13
+ - Start with `copilot-metrics paths --json` to find the local data directory.
14
+ - If the data directory is missing, suggest `copilot-metrics init`.
15
+ - Treat all costs as estimates, not official GitHub billing records.
16
+ - Do not read or display full prompts unless the user explicitly says content capture is enabled and asks for that content.
17
+ - Prefer machine-readable output when summarizing for another tool: use `--json` where available.
18
+
19
+ ## Useful Commands
20
+
21
+ ```bash
22
+ copilot-metrics paths --json
23
+ copilot-metrics setup vscode --json
24
+ copilot-metrics setup copilot-cli --json
25
+ copilot-metrics hooks preview --scope local --json
26
+ ```
27
+
28
+ Future report commands should be preferred when present, for example:
29
+
30
+ ```bash
31
+ copilot-metrics report labels --json
32
+ copilot-metrics report label DEMO-12345 --json
33
+ copilot-metrics report unattributed --json
34
+ ```
35
+
36
+ If a future report command is unavailable, explain that ingestion and reporting have not been implemented in this checkout yet.