sigmap 7.0.1 → 7.2.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 +33 -0
- package/README.md +20 -12
- package/gen-context.js +647 -16
- package/llms-full.txt +9 -2
- package/llms.txt +7 -7
- package/package.json +5 -3
- package/packages/cli/package.json +2 -2
- package/packages/core/README.md +2 -2
- package/packages/core/package.json +2 -2
- package/src/format/gain-terminal.js +206 -0
- package/src/mcp/server.js +1 -1
- package/src/tracking/aggregate.js +195 -0
- package/src/tracking/logger.js +83 -1
- package/src/tracking/pricing.js +43 -0
package/llms-full.txt
CHANGED
|
@@ -9,7 +9,7 @@ the files relevant to the task — cutting tokens ~97% while keeping answers
|
|
|
9
9
|
grounded. Deterministic, offline, no embeddings or vector database. Works with
|
|
10
10
|
Claude, Cursor, GitHub Copilot, Aider, Windsurf, local LLMs, and MCP.
|
|
11
11
|
|
|
12
|
-
# Version: 7.0
|
|
12
|
+
# Version: 7.2.0 | Benchmark: sigmap-v7.0-main (2026-06-14)
|
|
13
13
|
# Source: auto-generated from package.json, version.json, src/mcp/tools.js, src/config/defaults.js
|
|
14
14
|
# Regenerate: npm run generate:llms | Validate: npm run validate:llms
|
|
15
15
|
|
|
@@ -67,6 +67,13 @@ sigmap --suggest-tool "<task>" Recommend model tier for a task descrip
|
|
|
67
67
|
sigmap --suggest-tool "<task>" --json Machine-readable tier recommendation
|
|
68
68
|
sigmap --health Print composite health score
|
|
69
69
|
sigmap --health --json Machine-readable health score
|
|
70
|
+
sigmap gain Token-savings dashboard (totals + by-operation)
|
|
71
|
+
sigmap gain --all Add daily / weekly / monthly trend tables
|
|
72
|
+
sigmap gain --json Aggregate savings as JSON
|
|
73
|
+
sigmap gain --since 7d Window filter (7d, 30d, 12h, or ISO date)
|
|
74
|
+
sigmap gain --top <n> | --model <name> Limit rows / set $ pricing model
|
|
75
|
+
sigmap gain --reset Clear the local savings log (.context/gain.ndjson)
|
|
76
|
+
sigmap ... --no-track Disable gain savings capture for this run
|
|
70
77
|
sigmap --diff Generate context for git-changed files only
|
|
71
78
|
sigmap --diff <base-ref> Generate context + structural diff vs base ref (e.g. main)
|
|
72
79
|
sigmap --diff --staged Generate context for staged files only
|
|
@@ -273,7 +280,7 @@ the relevant regulation before publication; SigMap makes no legal claims.
|
|
|
273
280
|
- Author: Manoj Mallick
|
|
274
281
|
- License: MIT
|
|
275
282
|
- Repository: https://github.com/manojmallick/sigmap
|
|
276
|
-
- Documentation: https://
|
|
283
|
+
- Documentation: https://sigmap.io/
|
|
277
284
|
- npm: https://www.npmjs.com/package/sigmap
|
|
278
285
|
- Benchmark dataset: https://doi.org/10.5281/zenodo.19898842
|
|
279
286
|
- Issues: https://github.com/manojmallick/sigmap/issues
|
package/llms.txt
CHANGED
|
@@ -9,7 +9,7 @@ the files relevant to the task — cutting tokens ~97% while keeping answers
|
|
|
9
9
|
grounded. Deterministic, offline, no embeddings or vector database. Works with
|
|
10
10
|
Claude, Cursor, GitHub Copilot, Aider, Windsurf, local LLMs, and MCP.
|
|
11
11
|
|
|
12
|
-
# Version: 7.0
|
|
12
|
+
# Version: 7.2.0 | Benchmark: sigmap-v7.0-main (2026-06-14)
|
|
13
13
|
# Source: auto-generated from package.json, version.json, src/mcp/tools.js, src/config/defaults.js
|
|
14
14
|
# Regenerate: npm run generate:llms | Validate: npm run validate:llms
|
|
15
15
|
|
|
@@ -39,18 +39,18 @@ npx sigmap --mcp # start the MCP server over stdio
|
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
## Docs
|
|
42
|
-
- [Full documentation](https://
|
|
43
|
-
- [CLI reference](https://
|
|
44
|
-
- [MCP tools](https://
|
|
45
|
-
- [Benchmark methodology](https://
|
|
46
|
-
- [Configuration guide](https://
|
|
42
|
+
- [Full documentation](https://sigmap.io/)
|
|
43
|
+
- [CLI reference](https://sigmap.io/guide/cli)
|
|
44
|
+
- [MCP tools](https://sigmap.io/guide/mcp)
|
|
45
|
+
- [Benchmark methodology](https://sigmap.io/guide/benchmark)
|
|
46
|
+
- [Configuration guide](https://sigmap.io/guide/config)
|
|
47
47
|
- [Changelog](https://github.com/manojmallick/sigmap/blob/main/CHANGELOG.md)
|
|
48
48
|
|
|
49
49
|
## Optional
|
|
50
50
|
- [GitHub repository](https://github.com/manojmallick/sigmap)
|
|
51
51
|
- [npm package](https://www.npmjs.com/package/sigmap)
|
|
52
52
|
- [Benchmark dataset (Zenodo)](https://doi.org/10.5281/zenodo.19898842)
|
|
53
|
-
- [Full LLM reference](https://
|
|
53
|
+
- [Full LLM reference](https://sigmap.io/llms-full.txt)
|
|
54
54
|
|
|
55
55
|
SigMap — grounded AI coding context. The lightweight, deterministic alternative
|
|
56
56
|
to embeddings for feeding the right code to AI assistants.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sigmap",
|
|
3
|
-
"version": "7.0
|
|
3
|
+
"version": "7.2.0",
|
|
4
4
|
"description": "97% token reduction for AI coding. Extracts function & class signatures with TF-IDF ranking to feed only the right files to Claude, Cursor, Copilot, Aider, Windsurf, local LLMs & MCP. Zero dependencies, runs offline via npx.",
|
|
5
5
|
"main": "packages/core/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -31,12 +31,14 @@
|
|
|
31
31
|
"health": "node gen-context.js --health",
|
|
32
32
|
"map": "node gen-project-map.js",
|
|
33
33
|
"mcp": "node gen-context.js --mcp",
|
|
34
|
+
"check:bundle": "node scripts/check-bundle.mjs",
|
|
35
|
+
"check:version-meta": "node scripts/check-version-meta.mjs",
|
|
34
36
|
"build:binary": "node scripts/build-binary.mjs",
|
|
35
37
|
"verify:binary": "node scripts/verify-binary.mjs",
|
|
36
38
|
"version:sync": "node scripts/sync-versions.mjs",
|
|
37
39
|
"generate:llms": "node scripts/generate-llms.mjs",
|
|
38
40
|
"validate:llms": "node scripts/validate-llms.mjs",
|
|
39
|
-
"prepublishOnly": "node scripts/generate-llms.mjs"
|
|
41
|
+
"prepublishOnly": "node scripts/check-bundle.mjs && node scripts/check-version-meta.mjs && node scripts/generate-llms.mjs"
|
|
40
42
|
},
|
|
41
43
|
"files": [
|
|
42
44
|
"gen-context.js",
|
|
@@ -82,7 +84,7 @@
|
|
|
82
84
|
"type": "git",
|
|
83
85
|
"url": "https://github.com/manojmallick/sigmap.git"
|
|
84
86
|
},
|
|
85
|
-
"homepage": "https://
|
|
87
|
+
"homepage": "https://sigmap.io/",
|
|
86
88
|
"bugs": {
|
|
87
89
|
"url": "https://github.com/manojmallick/sigmap/issues"
|
|
88
90
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sigmap-cli",
|
|
3
|
-
"version": "7.0
|
|
3
|
+
"version": "7.2.0",
|
|
4
4
|
"description": "SigMap CLI wrapper — thin adapter for programmatic CLI invocation",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"keywords": [
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"url": "https://github.com/manojmallick/sigmap.git",
|
|
19
19
|
"directory": "packages/cli"
|
|
20
20
|
},
|
|
21
|
-
"homepage": "https://
|
|
21
|
+
"homepage": "https://sigmap.io/",
|
|
22
22
|
"license": "MIT",
|
|
23
23
|
"engines": {
|
|
24
24
|
"node": ">=18.0.0"
|
package/packages/core/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# sigmap-core
|
|
2
2
|
|
|
3
|
-
Programmatic API for [SigMap](https://
|
|
3
|
+
Programmatic API for [SigMap](https://sigmap.io/) — zero-dependency code signature extraction, ranked retrieval, secret scanning, project health scoring, and multi-adapter output formatting (v3.0+).
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -145,7 +145,7 @@ const geminiInstruction = adapt(context, 'gemini');
|
|
|
145
145
|
// All 6 adapters: copilot | claude | cursor | windsurf | openai | gemini
|
|
146
146
|
```
|
|
147
147
|
|
|
148
|
-
See the full [roadmap](https://
|
|
148
|
+
See the full [roadmap](https://sigmap.io/roadmap.html).
|
|
149
149
|
|
|
150
150
|
## Zero dependencies
|
|
151
151
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sigmap-core",
|
|
3
|
-
"version": "7.0
|
|
3
|
+
"version": "7.2.0",
|
|
4
4
|
"description": "SigMap core library — zero-dependency code signature extraction, retrieval, and security scanning",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"keywords": [
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"url": "https://github.com/manojmallick/sigmap.git",
|
|
21
21
|
"directory": "packages/core"
|
|
22
22
|
},
|
|
23
|
-
"homepage": "https://
|
|
23
|
+
"homepage": "https://sigmap.io/",
|
|
24
24
|
"license": "MIT",
|
|
25
25
|
"engines": {
|
|
26
26
|
"node": ">=18.0.0"
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SigMap `gain` — terminal renderer.
|
|
5
|
+
*
|
|
6
|
+
* Zero-dependency ANSI dashboard for token savings. Honors NO_COLOR and
|
|
7
|
+
* non-TTY output (plain text for pipes / --json consumers elsewhere).
|
|
8
|
+
*
|
|
9
|
+
* "saved" is a counterfactual estimate (whole-file baseline − actual context),
|
|
10
|
+
* never a measured delta — the footer says so on every view.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const USE_COLOR = !process.env.NO_COLOR && process.stdout.isTTY;
|
|
14
|
+
|
|
15
|
+
const C = {
|
|
16
|
+
reset: USE_COLOR ? '\x1b[0m' : '',
|
|
17
|
+
dim: USE_COLOR ? '\x1b[2m' : '',
|
|
18
|
+
bold: USE_COLOR ? '\x1b[1m' : '',
|
|
19
|
+
green: USE_COLOR ? '\x1b[32m' : '',
|
|
20
|
+
yellow: USE_COLOR ? '\x1b[33m' : '',
|
|
21
|
+
red: USE_COLOR ? '\x1b[31m' : '',
|
|
22
|
+
cyan: USE_COLOR ? '\x1b[36m' : '',
|
|
23
|
+
gray: USE_COLOR ? '\x1b[90m' : '',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// ── formatters ───────────────────────────────────────────────────────────
|
|
27
|
+
function humanTokens(n) {
|
|
28
|
+
n = Number(n) || 0;
|
|
29
|
+
if (n >= 1e9) return (n / 1e9).toFixed(1) + 'B';
|
|
30
|
+
if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M';
|
|
31
|
+
if (n >= 1e3) return (n / 1e3).toFixed(1) + 'K';
|
|
32
|
+
return String(Math.round(n));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function fmtInt(n) {
|
|
36
|
+
return (Number(n) || 0).toLocaleString('en-US');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function fmtUSD(n) {
|
|
40
|
+
n = Number(n) || 0;
|
|
41
|
+
if (n >= 1000) return '$' + n.toLocaleString('en-US', { maximumFractionDigits: 0 });
|
|
42
|
+
return '$' + n.toFixed(2);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function fmtDuration(ms) {
|
|
46
|
+
ms = Number(ms) || 0;
|
|
47
|
+
if (ms >= 3.6e6) return (ms / 3.6e6).toFixed(1) + 'h';
|
|
48
|
+
if (ms >= 60000) return (ms / 60000).toFixed(1) + 'm';
|
|
49
|
+
if (ms >= 1000) return (ms / 1000).toFixed(1) + 's';
|
|
50
|
+
return Math.round(ms) + 'ms';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function fmtPct(p) {
|
|
54
|
+
return (Number(p) || 0).toFixed(1) + '%';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function colorPct(p, text) {
|
|
58
|
+
if (!USE_COLOR) return text;
|
|
59
|
+
const c = p >= 85 ? C.green : p >= 60 ? C.yellow : C.red;
|
|
60
|
+
return c + text + C.reset;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function pad(s, w, align) {
|
|
64
|
+
s = String(s);
|
|
65
|
+
const visible = s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
66
|
+
const gap = Math.max(0, w - visible.length);
|
|
67
|
+
return align === 'right' ? ' '.repeat(gap) + s : s + ' '.repeat(gap);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Solid horizontal efficiency bar with a dotted remainder. */
|
|
71
|
+
function bar(pct, width) {
|
|
72
|
+
const filled = Math.round((clamp(pct, 0, 100) / 100) * width);
|
|
73
|
+
const solid = '█'.repeat(filled);
|
|
74
|
+
const rest = '░'.repeat(Math.max(0, width - filled));
|
|
75
|
+
return (USE_COLOR ? C.green : '') + solid + (USE_COLOR ? C.gray : '') + rest + C.reset;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Proportional impact bar (share of total saved). */
|
|
79
|
+
function impactBar(sharePct, width) {
|
|
80
|
+
const n = Math.max(1, Math.round((clamp(sharePct, 0, 100) / 100) * width));
|
|
81
|
+
return (USE_COLOR ? C.cyan : '') + '█'.repeat(n) + C.reset;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const clamp = (n, lo, hi) => Math.max(lo, Math.min(hi, n));
|
|
85
|
+
const rule = (w) => C.gray + '─'.repeat(w) + C.reset;
|
|
86
|
+
|
|
87
|
+
// ── views ──────────────────────────────────────────────────────────────
|
|
88
|
+
/**
|
|
89
|
+
* Render the global summary + by-operation table.
|
|
90
|
+
* @param {object} agg output of aggregate()
|
|
91
|
+
* @param {object} [opts] { version, scope }
|
|
92
|
+
* @returns {string}
|
|
93
|
+
*/
|
|
94
|
+
function renderSummary(agg, opts = {}) {
|
|
95
|
+
const W = 74;
|
|
96
|
+
const t = agg.totals;
|
|
97
|
+
const L = [];
|
|
98
|
+
const scope = opts.scope || 'this repo';
|
|
99
|
+
const ver = opts.version ? `v${opts.version}` : '';
|
|
100
|
+
|
|
101
|
+
L.push('');
|
|
102
|
+
const heading = `⚡ SigMap — Token Savings (${scope})`;
|
|
103
|
+
L.push(' ' + C.bold + C.cyan + heading + C.reset +
|
|
104
|
+
pad(`${C.green}✓${C.reset} ${C.dim}${ver}${C.reset}`, W - heading.length, 'right'));
|
|
105
|
+
L.push(' ' + rule(W));
|
|
106
|
+
L.push('');
|
|
107
|
+
|
|
108
|
+
if (t.count === 0) {
|
|
109
|
+
L.push(` ${C.yellow}No usage recorded yet.${C.reset}`);
|
|
110
|
+
L.push(` ${C.dim}Run a few queries (sigmap ask "...") or seed a demo:${C.reset}`);
|
|
111
|
+
L.push(` ${C.dim} node scripts/gain.mjs --demo${C.reset}`);
|
|
112
|
+
L.push('');
|
|
113
|
+
return L.join('\n');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const label = (k) => C.dim + pad(k, 21) + C.reset;
|
|
117
|
+
L.push(' ' + label('Total operations') + ': ' + C.bold + fmtInt(t.count) + C.reset);
|
|
118
|
+
L.push(' ' + label('Whole-file baseline') + ': ' + humanTokens(t.baseline) + ' tok' +
|
|
119
|
+
` ${C.dim}← est. cost of feeding full files${C.reset}`);
|
|
120
|
+
L.push(' ' + label('SigMap context') + ': ' + humanTokens(t.actual) + ' tok');
|
|
121
|
+
L.push(' ' + label('Tokens saved') + ': ' + C.bold + humanTokens(t.saved) + C.reset +
|
|
122
|
+
' (' + colorPct(t.savedPct, fmtPct(t.savedPct)) + ')');
|
|
123
|
+
L.push(' ' + label('Est. money saved') + ': ' + C.bold + C.green + fmtUSD(t.usdSaved) + C.reset +
|
|
124
|
+
` ${C.dim}(${agg.price.model} input @ $${agg.price.perMtok}/M · --model to change)${C.reset}`);
|
|
125
|
+
L.push(' ' + label('Avg latency') + ': ' + fmtDuration(t.avgMs) + ' / op' +
|
|
126
|
+
` ${C.dim}(local, no API round-trip)${C.reset}`);
|
|
127
|
+
L.push('');
|
|
128
|
+
L.push(' ' + label('Efficiency') + ': ▕' + bar(t.savedPct, 30) + '▏ ' +
|
|
129
|
+
colorPct(t.savedPct, C.bold + fmtPct(t.savedPct) + C.reset));
|
|
130
|
+
L.push('');
|
|
131
|
+
|
|
132
|
+
// By operation table
|
|
133
|
+
L.push(` ${C.bold}By operation${C.reset}`);
|
|
134
|
+
L.push(' ' + rule(W));
|
|
135
|
+
L.push(' ' + C.dim +
|
|
136
|
+
pad('#', 3) + pad('Operation', 24) + pad('Count', 8, 'right') +
|
|
137
|
+
pad('Saved', 9, 'right') + pad('Avg%', 8, 'right') + pad('Time', 8, 'right') +
|
|
138
|
+
' Impact' + C.reset);
|
|
139
|
+
agg.byOp.forEach((o, i) => {
|
|
140
|
+
L.push(' ' +
|
|
141
|
+
pad(`${i + 1}.`, 3) +
|
|
142
|
+
pad(o.op, 24) +
|
|
143
|
+
pad(fmtInt(o.count), 8, 'right') +
|
|
144
|
+
pad(humanTokens(o.saved), 9, 'right') +
|
|
145
|
+
pad(colorPct(o.avgPct, fmtPct(o.avgPct)), 8, 'right') +
|
|
146
|
+
pad(fmtDuration(o.avgMs), 8, 'right') +
|
|
147
|
+
' ' + impactBar(o.sharePct, 12));
|
|
148
|
+
});
|
|
149
|
+
L.push(' ' + rule(W));
|
|
150
|
+
L.push(` ${C.dim}saved = baseline − actual · estimated vs whole-file reads · local-only${C.reset}`);
|
|
151
|
+
L.push('');
|
|
152
|
+
return L.join('\n');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Render daily / weekly / monthly trend tables.
|
|
157
|
+
* @param {object} agg
|
|
158
|
+
* @returns {string}
|
|
159
|
+
*/
|
|
160
|
+
function renderBreakdown(agg) {
|
|
161
|
+
const L = [];
|
|
162
|
+
if (agg.totals.count === 0) return renderSummary(agg);
|
|
163
|
+
|
|
164
|
+
const section = (icon, title, rows, keyHeader) => {
|
|
165
|
+
L.push('');
|
|
166
|
+
L.push(` ${C.bold}${icon} ${title}${C.reset}`);
|
|
167
|
+
L.push(' ' + C.dim +
|
|
168
|
+
pad(keyHeader, 14) + pad('Ops', 7, 'right') + pad('Baseline', 11, 'right') +
|
|
169
|
+
pad('Actual', 10, 'right') + pad('Saved', 10, 'right') + pad('Save%', 8, 'right') +
|
|
170
|
+
pad('Time', 8, 'right') + C.reset);
|
|
171
|
+
let TB = 0, TA = 0, TS = 0, TC = 0, TM = 0;
|
|
172
|
+
for (const r of rows) {
|
|
173
|
+
TB += r.baseline; TA += r.actual; TS += r.saved; TC += r.count; TM += r.ms;
|
|
174
|
+
L.push(' ' +
|
|
175
|
+
pad(r.key, 14) + pad(fmtInt(r.count), 7, 'right') +
|
|
176
|
+
pad(humanTokens(r.baseline), 11, 'right') + pad(humanTokens(r.actual), 10, 'right') +
|
|
177
|
+
pad(humanTokens(r.saved), 10, 'right') +
|
|
178
|
+
pad(colorPct(r.savedPct, fmtPct(r.savedPct)), 8, 'right') +
|
|
179
|
+
pad(fmtDuration(r.ms), 8, 'right'));
|
|
180
|
+
}
|
|
181
|
+
const tp = TB > 0 ? (TS / TB) * 100 : 0;
|
|
182
|
+
L.push(' ' + C.bold +
|
|
183
|
+
pad('TOTAL', 14) + pad(fmtInt(TC), 7, 'right') +
|
|
184
|
+
pad(humanTokens(TB), 11, 'right') + pad(humanTokens(TA), 10, 'right') +
|
|
185
|
+
pad(humanTokens(TS), 10, 'right') + pad(fmtPct(tp), 8, 'right') +
|
|
186
|
+
pad(fmtDuration(TM), 8, 'right') + C.reset);
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const daily = agg.buckets.daily.slice(-30);
|
|
190
|
+
section('📅', `Daily (last ${daily.length})`, daily, 'Date');
|
|
191
|
+
const weekly = agg.buckets.weekly.slice(-6);
|
|
192
|
+
section('📆', `Weekly (last ${weekly.length})`, weekly, 'Week of');
|
|
193
|
+
const monthly = agg.buckets.monthly.slice(-3);
|
|
194
|
+
section('🗓️', `Monthly (last ${monthly.length})`, monthly, 'Month');
|
|
195
|
+
L.push('');
|
|
196
|
+
L.push(` ${C.dim}saved = baseline − actual · estimated vs whole-file reads · local-only${C.reset}`);
|
|
197
|
+
L.push('');
|
|
198
|
+
return L.join('\n');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
module.exports = {
|
|
202
|
+
renderSummary,
|
|
203
|
+
renderBreakdown,
|
|
204
|
+
// exported for tests
|
|
205
|
+
humanTokens, fmtUSD, fmtDuration, fmtPct,
|
|
206
|
+
};
|
package/src/mcp/server.js
CHANGED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SigMap usage aggregation for the `gain` dashboard.
|
|
5
|
+
*
|
|
6
|
+
* Pure, zero-dependency functions that turn raw NDJSON usage records into the
|
|
7
|
+
* totals / by-operation / time-bucket shapes the terminal renderer consumes.
|
|
8
|
+
*
|
|
9
|
+
* Tolerant of BOTH schemas:
|
|
10
|
+
* - new: { op, baselineTokens, actualTokens, savedTokens, savedPct, durationMs, model }
|
|
11
|
+
* - legacy: { rawTokens, finalTokens, reductionPct } (from logger.js v0.9)
|
|
12
|
+
*
|
|
13
|
+
* "saved" is a counterfactual estimate (baseline − actual), never a measured
|
|
14
|
+
* delta. Callers are responsible for labeling it as such in the UI.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const { resolvePrice } = require('./pricing');
|
|
18
|
+
|
|
19
|
+
const clamp = (n, lo, hi) => Math.max(lo, Math.min(hi, n));
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Normalize one raw record into a canonical shape.
|
|
23
|
+
* @param {object} rec
|
|
24
|
+
*/
|
|
25
|
+
function normalize(rec) {
|
|
26
|
+
const baseline = num(rec.baselineTokens != null ? rec.baselineTokens : rec.rawTokens);
|
|
27
|
+
const actual = num(rec.actualTokens != null ? rec.actualTokens : rec.finalTokens);
|
|
28
|
+
const saved = rec.savedTokens != null ? num(rec.savedTokens) : Math.max(0, baseline - actual);
|
|
29
|
+
const savedPct = rec.savedPct != null
|
|
30
|
+
? num(rec.savedPct)
|
|
31
|
+
: rec.reductionPct != null
|
|
32
|
+
? num(rec.reductionPct)
|
|
33
|
+
: baseline > 0 ? (saved / baseline) * 100 : 0;
|
|
34
|
+
return {
|
|
35
|
+
ts: rec.ts || null,
|
|
36
|
+
op: normalizeOp(rec.op),
|
|
37
|
+
baseline,
|
|
38
|
+
actual,
|
|
39
|
+
saved,
|
|
40
|
+
savedPct: clamp(savedPct, 0, 100),
|
|
41
|
+
durationMs: num(rec.durationMs),
|
|
42
|
+
model: rec.model || null,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function normalizeOp(op) {
|
|
47
|
+
if (!op) return 'generate';
|
|
48
|
+
return String(op);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function num(v) {
|
|
52
|
+
const n = Number(v);
|
|
53
|
+
return Number.isFinite(n) ? n : 0;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Parse a --since value into a cutoff Date (or null for "all time").
|
|
58
|
+
* Accepts: "7d", "30d", "12h", or an ISO date "2026-06-01".
|
|
59
|
+
* @param {string} since
|
|
60
|
+
* @param {number} [nowMs] - injectable clock for tests
|
|
61
|
+
* @returns {Date|null}
|
|
62
|
+
*/
|
|
63
|
+
function parseSince(since, nowMs) {
|
|
64
|
+
if (!since) return null;
|
|
65
|
+
const now = nowMs != null ? nowMs : Date.now();
|
|
66
|
+
const rel = /^(\d+)([dhw])$/.exec(String(since).trim());
|
|
67
|
+
if (rel) {
|
|
68
|
+
const n = parseInt(rel[1], 10);
|
|
69
|
+
const unit = rel[2];
|
|
70
|
+
const ms = unit === 'h' ? 3.6e6 : unit === 'w' ? 6.048e8 : 8.64e7;
|
|
71
|
+
return new Date(now - n * ms);
|
|
72
|
+
}
|
|
73
|
+
const d = new Date(since);
|
|
74
|
+
return Number.isNaN(d.getTime()) ? null : d;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Bucket records by calendar granularity.
|
|
79
|
+
* @param {object[]} records - normalized records
|
|
80
|
+
* @param {'day'|'week'|'month'} granularity
|
|
81
|
+
* @returns {Array<{key,count,baseline,actual,saved,savedPct,ms}>} ascending by key
|
|
82
|
+
*/
|
|
83
|
+
function bucketBy(records, granularity) {
|
|
84
|
+
const map = new Map();
|
|
85
|
+
for (const r of records) {
|
|
86
|
+
if (!r.ts) continue;
|
|
87
|
+
const key = bucketKey(r.ts, granularity);
|
|
88
|
+
if (!key) continue;
|
|
89
|
+
let b = map.get(key);
|
|
90
|
+
if (!b) { b = { key, count: 0, baseline: 0, actual: 0, saved: 0, ms: 0 }; map.set(key, b); }
|
|
91
|
+
b.count += 1;
|
|
92
|
+
b.baseline += r.baseline;
|
|
93
|
+
b.actual += r.actual;
|
|
94
|
+
b.saved += r.saved;
|
|
95
|
+
b.ms += r.durationMs;
|
|
96
|
+
}
|
|
97
|
+
return [...map.values()]
|
|
98
|
+
.map((b) => ({ ...b, savedPct: b.baseline > 0 ? clamp((b.saved / b.baseline) * 100, 0, 100) : 0 }))
|
|
99
|
+
.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function bucketKey(ts, granularity) {
|
|
103
|
+
const d = new Date(ts);
|
|
104
|
+
if (Number.isNaN(d.getTime())) return null;
|
|
105
|
+
const y = d.getUTCFullYear();
|
|
106
|
+
const m = String(d.getUTCMonth() + 1).padStart(2, '0');
|
|
107
|
+
const day = String(d.getUTCDate()).padStart(2, '0');
|
|
108
|
+
if (granularity === 'month') return `${y}-${m}`;
|
|
109
|
+
if (granularity === 'week') {
|
|
110
|
+
// ISO-ish: key by the Monday (UTC) of that week.
|
|
111
|
+
const tmp = new Date(Date.UTC(y, d.getUTCMonth(), d.getUTCDate()));
|
|
112
|
+
const dow = (tmp.getUTCDay() + 6) % 7; // 0 = Monday
|
|
113
|
+
tmp.setUTCDate(tmp.getUTCDate() - dow);
|
|
114
|
+
return `${tmp.getUTCFullYear()}-${String(tmp.getUTCMonth() + 1).padStart(2, '0')}-${String(tmp.getUTCDate()).padStart(2, '0')}`;
|
|
115
|
+
}
|
|
116
|
+
return `${y}-${m}-${day}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Full aggregation for the `gain` dashboard.
|
|
121
|
+
* @param {object[]} rawRecords
|
|
122
|
+
* @param {object} [opts]
|
|
123
|
+
* @param {string} [opts.model] pricing model
|
|
124
|
+
* @param {string} [opts.since] window filter
|
|
125
|
+
* @param {number} [opts.top] limit byOp rows (0 = all)
|
|
126
|
+
* @param {number} [opts.nowMs] injectable clock
|
|
127
|
+
* @returns {object}
|
|
128
|
+
*/
|
|
129
|
+
function aggregate(rawRecords, opts = {}) {
|
|
130
|
+
const price = resolvePrice(opts.model);
|
|
131
|
+
const cutoff = parseSince(opts.since, opts.nowMs);
|
|
132
|
+
|
|
133
|
+
let records = (rawRecords || []).map(normalize);
|
|
134
|
+
if (cutoff) records = records.filter((r) => r.ts && new Date(r.ts) >= cutoff);
|
|
135
|
+
|
|
136
|
+
const totals = {
|
|
137
|
+
count: records.length,
|
|
138
|
+
baseline: 0,
|
|
139
|
+
actual: 0,
|
|
140
|
+
saved: 0,
|
|
141
|
+
totalMs: 0,
|
|
142
|
+
savedPct: 0,
|
|
143
|
+
avgMs: 0,
|
|
144
|
+
usdSaved: 0,
|
|
145
|
+
firstTs: null,
|
|
146
|
+
lastTs: null,
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const opMap = new Map();
|
|
150
|
+
for (const r of records) {
|
|
151
|
+
totals.baseline += r.baseline;
|
|
152
|
+
totals.actual += r.actual;
|
|
153
|
+
totals.saved += r.saved;
|
|
154
|
+
totals.totalMs += r.durationMs;
|
|
155
|
+
if (r.ts) {
|
|
156
|
+
if (!totals.firstTs || r.ts < totals.firstTs) totals.firstTs = r.ts;
|
|
157
|
+
if (!totals.lastTs || r.ts > totals.lastTs) totals.lastTs = r.ts;
|
|
158
|
+
}
|
|
159
|
+
let o = opMap.get(r.op);
|
|
160
|
+
if (!o) { o = { op: r.op, count: 0, baseline: 0, saved: 0, ms: 0 }; opMap.set(r.op, o); }
|
|
161
|
+
o.count += 1;
|
|
162
|
+
o.baseline += r.baseline;
|
|
163
|
+
o.saved += r.saved;
|
|
164
|
+
o.ms += r.durationMs;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
totals.savedPct = totals.baseline > 0 ? clamp((totals.saved / totals.baseline) * 100, 0, 100) : 0;
|
|
168
|
+
totals.avgMs = totals.count > 0 ? Math.round(totals.totalMs / totals.count) : 0;
|
|
169
|
+
totals.usdSaved = totals.saved * price.perToken;
|
|
170
|
+
|
|
171
|
+
let byOp = [...opMap.values()].map((o) => ({
|
|
172
|
+
op: o.op,
|
|
173
|
+
count: o.count,
|
|
174
|
+
saved: o.saved,
|
|
175
|
+
avgPct: o.baseline > 0 ? clamp((o.saved / o.baseline) * 100, 0, 100) : 0,
|
|
176
|
+
avgMs: o.count > 0 ? Math.round(o.ms / o.count) : 0,
|
|
177
|
+
usdSaved: o.saved * price.perToken,
|
|
178
|
+
sharePct: totals.saved > 0 ? (o.saved / totals.saved) * 100 : 0,
|
|
179
|
+
})).sort((a, b) => b.saved - a.saved);
|
|
180
|
+
|
|
181
|
+
if (opts.top && opts.top > 0) byOp = byOp.slice(0, opts.top);
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
price,
|
|
185
|
+
totals,
|
|
186
|
+
byOp,
|
|
187
|
+
buckets: {
|
|
188
|
+
daily: bucketBy(records, 'day'),
|
|
189
|
+
weekly: bucketBy(records, 'week'),
|
|
190
|
+
monthly: bucketBy(records, 'month'),
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
module.exports = { aggregate, bucketBy, parseSince, normalize };
|
package/src/tracking/logger.js
CHANGED
|
@@ -18,6 +18,9 @@ const fs = require('fs');
|
|
|
18
18
|
const path = require('path');
|
|
19
19
|
|
|
20
20
|
const LOG_FILE = path.join('.context', 'usage.ndjson');
|
|
21
|
+
// Dedicated log for the `gain` dashboard (extended schema). Kept separate from
|
|
22
|
+
// usage.ndjson so the legacy health/nudge history never collides with it.
|
|
23
|
+
const GAIN_FILE = path.join('.context', 'gain.ndjson');
|
|
21
24
|
|
|
22
25
|
/**
|
|
23
26
|
* Append one run entry to the usage log.
|
|
@@ -73,6 +76,25 @@ function readLog(cwd) {
|
|
|
73
76
|
}
|
|
74
77
|
}
|
|
75
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Read and parse all `gain` dashboard records (oldest first).
|
|
81
|
+
* @param {string} cwd
|
|
82
|
+
* @returns {object[]}
|
|
83
|
+
*/
|
|
84
|
+
function readGainLog(cwd) {
|
|
85
|
+
try {
|
|
86
|
+
const logPath = path.join(cwd, GAIN_FILE);
|
|
87
|
+
if (!fs.existsSync(logPath)) return [];
|
|
88
|
+
return fs.readFileSync(logPath, 'utf8')
|
|
89
|
+
.split('\n')
|
|
90
|
+
.filter(Boolean)
|
|
91
|
+
.map((line) => { try { return JSON.parse(line); } catch (_) { return null; } })
|
|
92
|
+
.filter(Boolean);
|
|
93
|
+
} catch (_) {
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
76
98
|
/**
|
|
77
99
|
* Compute summary statistics from an array of log records.
|
|
78
100
|
* @param {object[]} entries
|
|
@@ -112,4 +134,64 @@ function summarize(entries) {
|
|
|
112
134
|
};
|
|
113
135
|
}
|
|
114
136
|
|
|
115
|
-
|
|
137
|
+
/**
|
|
138
|
+
* Whether `gain` savings capture is enabled. Default: ON (privacy-safe,
|
|
139
|
+
* local-only, counts only — no paths, source, or query text). This is
|
|
140
|
+
* intentionally decoupled from the legacy `config.tracking` flag (which gates
|
|
141
|
+
* the usage.ndjson health log and defaults OFF). Opt out of gain capture via:
|
|
142
|
+
* config.gainTracking === false · --no-track · SIGMAP_NO_TRACK=1
|
|
143
|
+
* @param {object} [config]
|
|
144
|
+
* @param {string[]} [argv]
|
|
145
|
+
* @returns {boolean}
|
|
146
|
+
*/
|
|
147
|
+
function isTrackingEnabled(config, argv) {
|
|
148
|
+
const a = argv || (typeof process !== 'undefined' ? process.argv : []);
|
|
149
|
+
if (process.env && process.env.SIGMAP_NO_TRACK) return false;
|
|
150
|
+
if (a && a.includes('--no-track')) return false;
|
|
151
|
+
if (config && config.gainTracking === false) return false;
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Append one operation to the usage log using the extended `gain` schema.
|
|
157
|
+
* Reuses the same NDJSON file as logRun and is tolerant of partial input.
|
|
158
|
+
* Never throws — tracking must never break the main process.
|
|
159
|
+
*
|
|
160
|
+
* @param {object} entry
|
|
161
|
+
* @param {string} entry.op e.g. 'ask' | 'generate' | 'query' | 'mcp:get_map'
|
|
162
|
+
* @param {number} entry.baselineTokens whole-file / candidate baseline (counterfactual)
|
|
163
|
+
* @param {number} entry.actualTokens tokens SigMap actually emitted
|
|
164
|
+
* @param {number} [entry.durationMs]
|
|
165
|
+
* @param {string} [entry.model]
|
|
166
|
+
* @param {string} [entry.version]
|
|
167
|
+
* @param {string} cwd
|
|
168
|
+
*/
|
|
169
|
+
function recordUsage(entry, cwd) {
|
|
170
|
+
try {
|
|
171
|
+
const logPath = path.join(cwd, GAIN_FILE);
|
|
172
|
+
const dir = path.dirname(logPath);
|
|
173
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
174
|
+
|
|
175
|
+
const baseline = Math.max(0, Number(entry.baselineTokens) || 0);
|
|
176
|
+
const actual = Math.max(0, Number(entry.actualTokens) || 0);
|
|
177
|
+
const saved = Math.max(0, baseline - actual);
|
|
178
|
+
const record = {
|
|
179
|
+
ts: new Date().toISOString(),
|
|
180
|
+
v: entry.version || '0.9.0',
|
|
181
|
+
op: entry.op || 'generate',
|
|
182
|
+
baselineTokens: baseline,
|
|
183
|
+
actualTokens: actual,
|
|
184
|
+
savedTokens: saved,
|
|
185
|
+
savedPct: baseline > 0 ? parseFloat(((saved / baseline) * 100).toFixed(1)) : 0,
|
|
186
|
+
durationMs: Math.max(0, Math.round(Number(entry.durationMs) || 0)),
|
|
187
|
+
model: entry.model || null,
|
|
188
|
+
ok: entry.ok !== false,
|
|
189
|
+
};
|
|
190
|
+
fs.appendFileSync(logPath, JSON.stringify(record) + '\n', 'utf8');
|
|
191
|
+
} catch (err) {
|
|
192
|
+
// Never crash the main process — tracking is optional.
|
|
193
|
+
if (process.stderr) process.stderr.write(`[sigmap] tracking: could not write log: ${err.message}\n`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
module.exports = { logRun, recordUsage, readLog, readGainLog, summarize, isTrackingEnabled, GAIN_FILE };
|