sigmap 7.0.1 → 7.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 +15 -0
- package/README.md +20 -12
- package/gen-context.js +96 -4
- package/llms-full.txt +9 -2
- package/llms.txt +7 -7
- package/package.json +2 -2
- 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/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,21 @@ Format: [Semantic Versioning](https://semver.org/)
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
+
## [7.1.0] — 2026-06-16
|
|
14
|
+
|
|
15
|
+
Minor release — a token-savings dashboard in the terminal, plus domain and sponsorship docs.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- **Token-savings dashboard — `sigmap gain` (#260):** surfaces cumulative savings right in the terminal — total tokens saved, % efficiency, estimated $ saved, latency, and a by-operation breakdown, plus `gain --all` for daily / weekly / monthly trends. Savings are captured per operation (`ask`, `generate`) into a dedicated local log `.context/gain.ndjson` (counts only — no paths, source, or query text). Capture is **default-on** and privacy-safe; opt out via `--no-track`, `SIGMAP_NO_TRACK=1`, or `config.gainTracking:false`. The legacy `usage.ndjson` / `--track` health log is unchanged. New `src/tracking/{aggregate,pricing}.js` (zero-dep aggregation + model→$/Mtok pricing) and `src/format/gain-terminal.js` (ANSI renderer, `NO_COLOR`/non-TTY safe). Flags: `gain --all | --json | --since <7d|ISO> | --top <n> | --model <name> | --reset`. "Saved" is labeled everywhere as an estimate vs the whole-file baseline.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- **Docs served at the sigmap.io root (#258):** the docs site now builds with base `/` (dropped the `/sigmap/` path prefix) and the project domain points to sigmap.io.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
- **Sponsorship transparency (#257):** README gained a Sponsor section with a tier ladder and funding goal; detail moved into `SPONSOR.md`, with an "About the maintainer" note and a low-barrier welcome.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
13
28
|
## [7.0.1] — 2026-06-14
|
|
14
29
|
|
|
15
30
|
Patch release — supply-chain hardening and package hygiene, plus a wider star nudge.
|
package/README.md
CHANGED
|
@@ -100,7 +100,7 @@ Repos tested : 21 (JavaScript, Python, Go, Rust, Java, R, C++, C#, Dart, Swift
|
|
|
100
100
|
Measured on 90 coding tasks across 18 real public repos. No LLM API — fully reproducible.
|
|
101
101
|
|
|
102
102
|
**Resources:**
|
|
103
|
-
- [Full methodology →](https://
|
|
103
|
+
- [Full methodology →](https://sigmap.io/guide/benchmark.html)
|
|
104
104
|
- [Benchmark suite (GitHub)](https://github.com/manojmallick/sigmap-benchmark-suite) — scripts, tasks, and raw data
|
|
105
105
|
- [Benchmark data (Zenodo)](https://zenodo.org/records/19898842) — archived results for reproducibility
|
|
106
106
|
|
|
@@ -172,8 +172,8 @@ sigmap --adapter claude # works with Claude Code
|
|
|
172
172
|
**Open-source agents & local LLMs:**
|
|
173
173
|
|
|
174
174
|
Use SigMap with open-source tools and fully self-hosted setups:
|
|
175
|
-
- **[Open-source agents guide →](https://
|
|
176
|
-
- **[Local LLMs guide →](https://
|
|
175
|
+
- **[Open-source agents guide →](https://sigmap.io/guide/agents)** — OpenCode, Aider, OpenHands, Cline
|
|
176
|
+
- **[Local LLMs guide →](https://sigmap.io/guide/local-llms)** — Ollama, llama.cpp, vLLM (no API keys, full privacy)
|
|
177
177
|
|
|
178
178
|
**IDE extensions:**
|
|
179
179
|
|
|
@@ -219,21 +219,21 @@ sigmap --health
|
|
|
219
219
|
| 👶 **New** | [Quick start guide](docs/readmes/GETTING_STARTED.md) — setup in 60 seconds |
|
|
220
220
|
| ⚡ **Daily** | `sigmap ask` / `sigmap validate` / `sigmap judge` |
|
|
221
221
|
| 🧠 **Advanced** | [Context strategies](docs/readmes/CONTEXT_STRATEGIES.md) · [MCP setup](docs/readmes/MCP_SETUP.md) |
|
|
222
|
-
| 🏢 **Teams** | [Config reference](https://
|
|
222
|
+
| 🏢 **Teams** | [Config reference](https://sigmap.io/guide/config.html) · [CI setup](docs/readmes/ENTERPRISE_SETUP.md) |
|
|
223
223
|
|
|
224
224
|
---
|
|
225
225
|
|
|
226
226
|
## Docs
|
|
227
227
|
|
|
228
|
-
**[
|
|
228
|
+
**[sigmap.io](https://sigmap.io)**
|
|
229
229
|
|
|
230
230
|
| Section | Link |
|
|
231
231
|
|---|---|
|
|
232
|
-
| CLI reference (32 commands) | [cli.html](https://
|
|
233
|
-
| Benchmark methodology | [benchmark.html](https://
|
|
234
|
-
| Config reference | [config.html](https://
|
|
235
|
-
| Roadmap | [roadmap.html](https://
|
|
236
|
-
| 31 languages | [generalization.html](https://
|
|
232
|
+
| CLI reference (32 commands) | [cli.html](https://sigmap.io/guide/cli.html) |
|
|
233
|
+
| Benchmark methodology | [benchmark.html](https://sigmap.io/guide/benchmark.html) |
|
|
234
|
+
| Config reference | [config.html](https://sigmap.io/guide/config.html) |
|
|
235
|
+
| Roadmap | [roadmap.html](https://sigmap.io/guide/roadmap.html) |
|
|
236
|
+
| 31 languages | [generalization.html](https://sigmap.io/guide/generalization.html) |
|
|
237
237
|
|
|
238
238
|
---
|
|
239
239
|
|
|
@@ -245,6 +245,14 @@ If SigMap saves you context or API spend, a ⭐ on [GitHub](https://github.com/m
|
|
|
245
245
|
|
|
246
246
|
---
|
|
247
247
|
|
|
248
|
+
## Sponsor
|
|
249
|
+
|
|
250
|
+
SigMap is built and maintained by one developer, kept **zero-dependency**, offline, and free. If it saves your team context or API spend, sponsoring keeps it that way — and funds the benchmark CI, the `sigmap.io` domain, and ongoing supply-chain hardening.
|
|
251
|
+
|
|
252
|
+
💜 **[Become a sponsor →](https://github.com/sponsors/manojmallick)** · see **[SPONSOR.md](SPONSOR.md)** for tiers and exactly where your support goes. Any amount helps — even $1/mo — and a ⭐ or a share counts too.
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
248
256
|
## Contributing
|
|
249
257
|
|
|
250
258
|
SigMap welcomes contributions!
|
|
@@ -284,7 +292,7 @@ TypeScript · JavaScript · Python · Java · Kotlin · Go · Rust · C# · C/C+
|
|
|
284
292
|
|
|
285
293
|
All implemented with zero external dependencies.
|
|
286
294
|
|
|
287
|
-
[Full language table →](https://
|
|
295
|
+
[Full language table →](https://sigmap.io/guide/generalization.html)
|
|
288
296
|
|
|
289
297
|
---
|
|
290
298
|
|
|
@@ -296,7 +304,7 @@ MIT © 2026 [Manoj Mallick](https://github.com/manojmallick) · Made in Amsterda
|
|
|
296
304
|
|
|
297
305
|
<div align="center">
|
|
298
306
|
|
|
299
|
-
**[Docs](https://
|
|
307
|
+
**[Docs](https://sigmap.io) · [Changelog](CHANGELOG.md) · [Roadmap](https://sigmap.io/roadmap.html) · [npm](https://www.npmjs.com/package/sigmap)**
|
|
300
308
|
|
|
301
309
|
⭐ [Star on GitHub](https://github.com/manojmallick/sigmap) if SigMap saves you tokens.
|
|
302
310
|
|
package/gen-context.js
CHANGED
|
@@ -6254,7 +6254,7 @@ const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, exp
|
|
|
6254
6254
|
|
|
6255
6255
|
const SERVER_INFO = {
|
|
6256
6256
|
name: 'sigmap',
|
|
6257
|
-
version: '7.0
|
|
6257
|
+
version: '7.1.0',
|
|
6258
6258
|
description: 'SigMap MCP server — code signatures on demand',
|
|
6259
6259
|
};
|
|
6260
6260
|
|
|
@@ -8928,6 +8928,20 @@ __factories["./src/discovery/source-root-registry"] = function(module, exports)
|
|
|
8928
8928
|
module.exports = { REGISTRY };
|
|
8929
8929
|
};
|
|
8930
8930
|
|
|
8931
|
+
// ── ./src/util/git ──
|
|
8932
|
+
__factories["./src/util/git"] = function(module, exports) {
|
|
8933
|
+
'use strict';
|
|
8934
|
+
const { execFileSync } = require('child_process');
|
|
8935
|
+
function git(args, opts = {}) {
|
|
8936
|
+
return execFileSync('git', args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], ...opts });
|
|
8937
|
+
}
|
|
8938
|
+
function tryGit(args, opts = {}) {
|
|
8939
|
+
try { return git(args, opts).toString().trim(); }
|
|
8940
|
+
catch (_) { return ''; }
|
|
8941
|
+
}
|
|
8942
|
+
module.exports = { git, tryGit };
|
|
8943
|
+
};
|
|
8944
|
+
|
|
8931
8945
|
// ── ./src/discovery/sigmapignore ──
|
|
8932
8946
|
__factories["./src/discovery/sigmapignore"] = function(module, exports) {
|
|
8933
8947
|
'use strict';
|
|
@@ -10947,7 +10961,7 @@ function __tryGit(args, opts = {}) {
|
|
|
10947
10961
|
catch (_) { return ''; }
|
|
10948
10962
|
}
|
|
10949
10963
|
|
|
10950
|
-
const VERSION = '7.0
|
|
10964
|
+
const VERSION = '7.1.0';
|
|
10951
10965
|
const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
|
|
10952
10966
|
|
|
10953
10967
|
function requireSourceOrBundled(key) {
|
|
@@ -12180,6 +12194,7 @@ function runDiff(cwd, config, stagedOnly, baseRef) {
|
|
|
12180
12194
|
// Core generate pipeline
|
|
12181
12195
|
// ---------------------------------------------------------------------------
|
|
12182
12196
|
function runGenerate(cwd, config, reportMode, reportJson = false) {
|
|
12197
|
+
const __genT0 = Date.now();
|
|
12183
12198
|
const ignorePatterns = loadIgnorePatterns(cwd);
|
|
12184
12199
|
let allFiles = buildFileList(cwd, config);
|
|
12185
12200
|
|
|
@@ -12379,7 +12394,7 @@ function runGenerate(cwd, config, reportMode, reportJson = false) {
|
|
|
12379
12394
|
printReport(result.inputTokenTotal, result.finalTokens, result.fileCount, result.droppedCount, reportJson, effectiveMaxTokens, result.coverageResult, config.autoMaxTokens !== false && effectiveMaxTokens !== config.maxTokens, config.maxTokens);
|
|
12380
12395
|
}
|
|
12381
12396
|
|
|
12382
|
-
// Usage tracking (v0.9) —
|
|
12397
|
+
// Usage tracking (v0.9) — legacy health log, opt-in via config.tracking / --track.
|
|
12383
12398
|
const trackingEnabled = !!(config.tracking || process.argv.includes('--track'));
|
|
12384
12399
|
if (trackingEnabled && !reportMode) {
|
|
12385
12400
|
try {
|
|
@@ -12399,6 +12414,22 @@ function runGenerate(cwd, config, reportMode, reportJson = false) {
|
|
|
12399
12414
|
}
|
|
12400
12415
|
}
|
|
12401
12416
|
|
|
12417
|
+
// gain: capture generate savings (separate gain.ndjson; default ON, opt-out).
|
|
12418
|
+
if (!reportMode) {
|
|
12419
|
+
try {
|
|
12420
|
+
const { recordUsage, isTrackingEnabled } = requireSourceOrBundled('./src/tracking/logger');
|
|
12421
|
+
if (isTrackingEnabled(config, process.argv)) {
|
|
12422
|
+
recordUsage({
|
|
12423
|
+
version: VERSION,
|
|
12424
|
+
op: 'generate',
|
|
12425
|
+
baselineTokens: result.inputTokenTotal,
|
|
12426
|
+
actualTokens: result.finalTokens,
|
|
12427
|
+
durationMs: Date.now() - __genT0,
|
|
12428
|
+
}, cwd);
|
|
12429
|
+
}
|
|
12430
|
+
} catch (_) { /* gain capture is best-effort */ }
|
|
12431
|
+
}
|
|
12432
|
+
|
|
12402
12433
|
// Feature 8: post-run summary — 6-line stdout after every normal run
|
|
12403
12434
|
if (!reportMode
|
|
12404
12435
|
&& !process.argv.includes('--json')
|
|
@@ -12695,6 +12726,13 @@ Usage:
|
|
|
12695
12726
|
${cmd} --suggest-tool "<task>" --json Machine-readable tier recommendation
|
|
12696
12727
|
${cmd} --health Print composite health score
|
|
12697
12728
|
${cmd} --health --json Machine-readable health score
|
|
12729
|
+
${cmd} gain Token-savings dashboard (totals + by-operation)
|
|
12730
|
+
${cmd} gain --all Add daily / weekly / monthly trend tables
|
|
12731
|
+
${cmd} gain --json Aggregate savings as JSON
|
|
12732
|
+
${cmd} gain --since 7d Window filter (7d, 30d, 12h, or ISO date)
|
|
12733
|
+
${cmd} gain --top <n> | --model <name> Limit rows / set $ pricing model
|
|
12734
|
+
${cmd} gain --reset Clear the local savings log (.context/gain.ndjson)
|
|
12735
|
+
${cmd} ... --no-track Disable gain savings capture for this run
|
|
12698
12736
|
${cmd} --diff Generate context for git-changed files only
|
|
12699
12737
|
${cmd} --diff <base-ref> Generate context + structural diff vs base ref (e.g. main)
|
|
12700
12738
|
${cmd} --diff --staged Generate context for staged files only
|
|
@@ -13094,6 +13132,7 @@ function main() {
|
|
|
13094
13132
|
|
|
13095
13133
|
// v4.2: `sigmap ask "<query>"` — unified pipeline
|
|
13096
13134
|
if (args[0] === 'ask') {
|
|
13135
|
+
const __askT0 = Date.now();
|
|
13097
13136
|
// v6.8: Handle --followup flag which may appear before or after query
|
|
13098
13137
|
let query = args[1];
|
|
13099
13138
|
if (query === '--followup' && args[2]) {
|
|
@@ -13261,6 +13300,21 @@ function main() {
|
|
|
13261
13300
|
bar,
|
|
13262
13301
|
].join('\n'));
|
|
13263
13302
|
}
|
|
13303
|
+
// gain: capture this query's savings for the `sigmap gain` dashboard.
|
|
13304
|
+
try {
|
|
13305
|
+
const { recordUsage, isTrackingEnabled } = requireSourceOrBundled('./src/tracking/logger');
|
|
13306
|
+
if (isTrackingEnabled(config, args)) {
|
|
13307
|
+
recordUsage({
|
|
13308
|
+
version: VERSION,
|
|
13309
|
+
op: 'ask',
|
|
13310
|
+
baselineTokens: rawTok,
|
|
13311
|
+
actualTokens: ctxTok,
|
|
13312
|
+
durationMs: Date.now() - __askT0,
|
|
13313
|
+
model,
|
|
13314
|
+
}, cwd);
|
|
13315
|
+
}
|
|
13316
|
+
} catch (_) { /* gain capture is best-effort */ }
|
|
13317
|
+
|
|
13264
13318
|
// v7.0.0: record the run and show the one-time star nudge (interactive only).
|
|
13265
13319
|
try {
|
|
13266
13320
|
const { checkStarNudge } = requireSourceOrBundled('./src/nudge');
|
|
@@ -13273,6 +13327,44 @@ function main() {
|
|
|
13273
13327
|
process.exit(0);
|
|
13274
13328
|
}
|
|
13275
13329
|
|
|
13330
|
+
// `sigmap gain` — token-savings dashboard (totals, by-operation, trends).
|
|
13331
|
+
if (args[0] === 'gain') {
|
|
13332
|
+
const valOf = (f, d) => { const i = args.indexOf(f); return i >= 0 && args[i + 1] ? args[i + 1] : d; };
|
|
13333
|
+
if (args.includes('--reset')) {
|
|
13334
|
+
try {
|
|
13335
|
+
const { GAIN_FILE } = requireSourceOrBundled('./src/tracking/logger');
|
|
13336
|
+
const p = path.join(cwd, GAIN_FILE);
|
|
13337
|
+
if (fs.existsSync(p)) fs.unlinkSync(p);
|
|
13338
|
+
console.log('[sigmap] gain: usage log cleared.');
|
|
13339
|
+
} catch (e) { console.error(`[sigmap] gain: ${e.message}`); }
|
|
13340
|
+
process.exit(0);
|
|
13341
|
+
}
|
|
13342
|
+
try {
|
|
13343
|
+
const { readGainLog } = requireSourceOrBundled('./src/tracking/logger');
|
|
13344
|
+
const { aggregate } = requireSourceOrBundled('./src/tracking/aggregate');
|
|
13345
|
+
const { renderSummary, renderBreakdown } = requireSourceOrBundled('./src/format/gain-terminal');
|
|
13346
|
+
const records = readGainLog(cwd);
|
|
13347
|
+
const agg = aggregate(records, {
|
|
13348
|
+
model: valOf('--model', 'claude-sonnet'),
|
|
13349
|
+
since: valOf('--since', null),
|
|
13350
|
+
top: parseInt(valOf('--top', args.includes('--all') ? '0' : '10'), 10),
|
|
13351
|
+
});
|
|
13352
|
+
if (args.includes('--json')) {
|
|
13353
|
+
process.stdout.write(JSON.stringify(agg, null, 2) + '\n');
|
|
13354
|
+
} else {
|
|
13355
|
+
const out = args.includes('--all')
|
|
13356
|
+
? renderSummary(agg, { version: VERSION }) + '\n' + renderBreakdown(agg)
|
|
13357
|
+
: renderSummary(agg, { version: VERSION });
|
|
13358
|
+
process.stdout.write(out + '\n');
|
|
13359
|
+
}
|
|
13360
|
+
} catch (e) {
|
|
13361
|
+
console.error(`[sigmap] gain: ${e.message}`);
|
|
13362
|
+
console.error(' (gain modules require src/ — for single-file installs, regenerate the bundle)');
|
|
13363
|
+
process.exit(1);
|
|
13364
|
+
}
|
|
13365
|
+
process.exit(0);
|
|
13366
|
+
}
|
|
13367
|
+
|
|
13276
13368
|
// v4.2: `sigmap suggest-profile` — auto-detect task type from git state
|
|
13277
13369
|
if (args[0] === 'suggest-profile') {
|
|
13278
13370
|
const short = args.includes('--short');
|
|
@@ -13359,7 +13451,7 @@ function main() {
|
|
|
13359
13451
|
const shareText = [
|
|
13360
13452
|
'Generated with SigMap — zero-dependency AI context engine',
|
|
13361
13453
|
`${reduction}% fewer tokens · ${hitAt5}% retrieval accuracy · 6× better results`,
|
|
13362
|
-
'https://sigmap.
|
|
13454
|
+
'https://sigmap.io',
|
|
13363
13455
|
].join('\n');
|
|
13364
13456
|
|
|
13365
13457
|
console.log(shareText);
|
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.1.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.1.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.1.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": {
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
"type": "git",
|
|
83
83
|
"url": "https://github.com/manojmallick/sigmap.git"
|
|
84
84
|
},
|
|
85
|
-
"homepage": "https://
|
|
85
|
+
"homepage": "https://sigmap.io/",
|
|
86
86
|
"bugs": {
|
|
87
87
|
"url": "https://github.com/manojmallick/sigmap/issues"
|
|
88
88
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sigmap-cli",
|
|
3
|
-
"version": "7.0
|
|
3
|
+
"version": "7.1.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.1.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 };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SigMap pricing table — input-token $/Mtok assumptions for the `gain` dashboard.
|
|
5
|
+
*
|
|
6
|
+
* These are ASSUMPTIONS used only to translate "tokens saved" into an estimated
|
|
7
|
+
* dollar figure. They are deliberately conservative and configurable via
|
|
8
|
+
* --model <name> or config.pricingModel
|
|
9
|
+
* The `gain` views always print the model + rate inline so the $ is never
|
|
10
|
+
* presented as exact. Zero npm dependencies.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// USD per 1,000,000 input tokens.
|
|
14
|
+
const PRICES = {
|
|
15
|
+
'claude-sonnet': 3.0,
|
|
16
|
+
'claude-opus': 15.0,
|
|
17
|
+
'claude-haiku': 0.8,
|
|
18
|
+
'gpt-4o': 2.5,
|
|
19
|
+
'gpt-4o-mini': 0.15,
|
|
20
|
+
'gemini-1.5-pro': 1.25,
|
|
21
|
+
'gemini-1.5-flash': 0.075,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const DEFAULT_MODEL = 'claude-sonnet';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Resolve a price (USD per token) for a model name.
|
|
28
|
+
* @param {string} [model]
|
|
29
|
+
* @returns {{ model: string, perMtok: number, perToken: number }}
|
|
30
|
+
*/
|
|
31
|
+
function resolvePrice(model) {
|
|
32
|
+
const key = (model || DEFAULT_MODEL).toLowerCase();
|
|
33
|
+
const perMtok = PRICES[key] != null ? PRICES[key] : PRICES[DEFAULT_MODEL];
|
|
34
|
+
const resolved = PRICES[key] != null ? key : DEFAULT_MODEL;
|
|
35
|
+
return { model: resolved, perMtok, perToken: perMtok / 1_000_000 };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** @returns {string[]} known model keys */
|
|
39
|
+
function listModels() {
|
|
40
|
+
return Object.keys(PRICES);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = { PRICES, DEFAULT_MODEL, resolvePrice, listModels };
|