sigmap 8.22.0 → 8.24.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 +24 -0
- package/README.md +4 -4
- package/gen-context.js +323 -5
- package/llms-full.txt +17 -4
- package/llms.txt +3 -3
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/core/package.json +1 -1
- package/src/config/defaults.js +8 -0
- package/src/mcp/handlers.js +36 -1
- package/src/mcp/server.js +3 -2
- package/src/mcp/tools.js +26 -0
- package/src/security/redact.js +56 -0
- package/src/tracking/budget.js +113 -0
- package/src/tracking/logger.js +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,30 @@ Format: [Semantic Versioning](https://semver.org/)
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
+
## [8.24.0] — 2026-07-28
|
|
14
|
+
|
|
15
|
+
Minor release — **"Trust Quick Wins I" (v8.24, G3a)**: the secret-redaction engine that already protects signatures, `get_lines`, and evidence packs becomes a standalone command for arbitrary text.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- **`sigmap redact` (#511, PR #512):** new `src/security/redact.js` — `redactText()` masks only the matched secret substring (`[REDACTED:<pattern name>]`), preserving surrounding text (unlike the generation-time scanner's whole-line replacement, which is unchanged). Findings carry 1-based line numbers with per-pattern counts. CLI: `sigmap redact [file] [--json]` — file argument or stdin; redacted text on stdout (pipe-clean), summary on stderr. Reuses the existing 10-pattern bank verbatim; zero new dependencies; never throws. 6 new integration tests including an every-pattern mask sweep (130 test files).
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- Test fixtures for secret patterns are assembled at runtime (no secret-shaped literals in committed blobs) — GitHub Push Protection rejected the first push over the fake samples, which validated the detection class this command implements.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## [8.23.0] — 2026-07-28
|
|
26
|
+
|
|
27
|
+
Minor release — **"Agent Economy I" (v8.23, F1)**: SigMap's token savings become queryable *during* a session. A spend ledger over the existing gain log, an optional budget threshold, and context-freshness age — as a CLI command and the 21st MCP tool.
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
- **`sigmap budget` + MCP `get_budget` (#508, PR #509):** new `src/tracking/budget.js` — `budgetStatus()` sums the session's gain-log entries (estimated SigMap-emitted tokens: spent/baseline/saved, op count), computes remaining/pct against an optional budget, and reports generated-context age with a stale flag. Session identity: `SIGMAP_SESSION` env override, else UTC day bucket; `recordUsage` now stamps every entry with `session` (legacy entries match day buckets by timestamp prefix). New opt-in config keys `sessionBudgetTokens` and `contextTtlDays` (both default `null`). CLI: `sigmap budget [--json] [--session <key>] [--budget <tokens>]`. The MCP tool advises degrade-gracefully tactics (terse encoding, `squeeze`, summarize-then-drop) at ≥80% budget. 7 new integration tests (129 test files); tool-count guards advanced 20→21.
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
- **Scope honesty by design:** the ledger counts tokens **SigMap emitted** (chars/4, labeled `estimated-tokens` on every surface) — not the host chat's total spend, which a CLI cannot see. Context freshness replaces the originally-planned prompt-cache "injection TTL", which was unverifiable and got cut in the plan audit.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
13
37
|
## [8.22.0] — 2026-07-28
|
|
14
38
|
|
|
15
39
|
Minor release — **"Hard Corpus" (v8.22)**: the benchmark corpus gains a no-leakage hard split with a deterministic leakage gate, and per-repo-size buckets stop tiny repos from flattering the average. The headline retrieval number gets harder — and honest.
|
package/README.md
CHANGED
|
@@ -122,8 +122,8 @@ Ask → Rank → Context → Validate → Judge → Learn
|
|
|
122
122
|
|
|
123
123
|
<!--SM:benchmarkBlock-->
|
|
124
124
|
```
|
|
125
|
-
Benchmark : sigmap-v8.
|
|
126
|
-
Date : 2026-07-
|
|
125
|
+
Benchmark : sigmap-v8.24-main (21 repositories, including R language)
|
|
126
|
+
Date : 2026-07-28
|
|
127
127
|
|
|
128
128
|
Hit@5 : 82.2% (grep-agent baseline 44.8% — 1.59× lift)
|
|
129
129
|
Token reduction: 96.8% (across 21 repos)
|
|
@@ -222,7 +222,7 @@ Use SigMap with open-source tools and fully self-hosted setups:
|
|
|
222
222
|
| **JetBrains** | [Marketplace](https://plugins.jetbrains.com/plugin/31109-sigmap--ai-context-engine/) | [github.com/manojmallick/sigmap-jetbrains](https://github.com/manojmallick/sigmap-jetbrains) | IntelliJ IDEA, WebStorm, PyCharm, GoLand — tool window + actions |
|
|
223
223
|
| **Neovim** | lazy.nvim / packer / vim-plug | [github.com/manojmallick/sigmap.nvim](https://github.com/manojmallick/sigmap.nvim) | `:SigMap`, `:SigMapQuery` float window, statusline widget |
|
|
224
224
|
|
|
225
|
-
**MCP server** —
|
|
225
|
+
**MCP server** — 21 on-demand tools for Claude Code and Cursor:
|
|
226
226
|
|
|
227
227
|
```bash
|
|
228
228
|
sigmap --mcp
|
|
@@ -269,7 +269,7 @@ SigMap treats coding agents as **consumers, not competitors**: it hands them a d
|
|
|
269
269
|
|
|
270
270
|
| Agent | One-time setup | How it consumes SigMap |
|
|
271
271
|
|---|---|---|
|
|
272
|
-
| **Claude Code** | `sigmap mcp install claude` |
|
|
272
|
+
| **Claude Code** | `sigmap mcp install claude` | 21 MCP tools (`search_signatures`, `get_lines`, `get_diff_context`, `squeeze_output`…) |
|
|
273
273
|
| **Cursor** | `sigmap mcp install cursor` | MCP tools, plus the `cursor` adapter writes `.cursorrules` |
|
|
274
274
|
| **Cline** | `sigmap mcp install cursor` | Reads `.cursorrules`; same MCP server |
|
|
275
275
|
| **Continue** | `sigmap mcp install vscode` | MCP tools inside the Continue extension |
|
package/gen-context.js
CHANGED
|
@@ -1490,6 +1490,14 @@ __factories["./src/config/defaults"] = function(module, exports) {
|
|
|
1490
1490
|
// Append run metrics to .context/usage.ndjson after each generate
|
|
1491
1491
|
tracking: false,
|
|
1492
1492
|
|
|
1493
|
+
// Session spend ledger (`sigmap budget` / MCP get_budget). Estimates only —
|
|
1494
|
+
// counts tokens SigMap emitted (chars/4), not the host chat's total spend.
|
|
1495
|
+
// Number → warn threshold for estimated SigMap-emitted tokens per session.
|
|
1496
|
+
sessionBudgetTokens: null,
|
|
1497
|
+
|
|
1498
|
+
// Number of days before generated context counts as stale in budget output.
|
|
1499
|
+
contextTtlDays: null,
|
|
1500
|
+
|
|
1493
1501
|
// MCP server configuration
|
|
1494
1502
|
mcp: {
|
|
1495
1503
|
autoRegister: true,
|
|
@@ -14374,6 +14382,41 @@ __factories["./src/mcp/handlers"] = function(module, exports) {
|
|
|
14374
14382
|
return out.join('\n');
|
|
14375
14383
|
}
|
|
14376
14384
|
|
|
14385
|
+
/**
|
|
14386
|
+
* get_budget({ session?, budgetTokens? }) → string
|
|
14387
|
+
*
|
|
14388
|
+
* Session spend ledger (F1): estimated tokens SigMap emitted this session,
|
|
14389
|
+
* optional budget remaining, and context freshness. Estimates only (chars/4);
|
|
14390
|
+
* does NOT see the host chat's total spend.
|
|
14391
|
+
*/
|
|
14392
|
+
function getBudget(args, cwd) {
|
|
14393
|
+
const { budgetStatus } = __require('./src/tracking/budget');
|
|
14394
|
+
let config = {};
|
|
14395
|
+
try { config = __require('./src/config/loader').loadConfig(cwd) || {}; } catch (_) {}
|
|
14396
|
+
const opts = { config };
|
|
14397
|
+
if (args && args.session) opts.session = String(args.session);
|
|
14398
|
+
if (args && args.budgetTokens != null) opts.budgetTokens = Number(args.budgetTokens);
|
|
14399
|
+
const s = budgetStatus(cwd, opts);
|
|
14400
|
+
|
|
14401
|
+
const out = ['# SigMap session spend (estimates — chars/4; SigMap-emitted tokens only)'];
|
|
14402
|
+
out.push('');
|
|
14403
|
+
out.push(`Session : ${s.session}`);
|
|
14404
|
+
out.push(`Ops : ${s.ops}`);
|
|
14405
|
+
out.push(`Spent : ~${s.spentTokens.toLocaleString()} tokens (baseline ~${s.baselineTokens.toLocaleString()}, saved ~${s.savedTokens.toLocaleString()})`);
|
|
14406
|
+
if (s.budgetTokens != null) {
|
|
14407
|
+
out.push(`Budget : ${s.budgetTokens.toLocaleString()} → remaining ~${s.remainingTokens.toLocaleString()} (${s.pctUsed}% used)${s.overBudget ? ' ⚠ OVER BUDGET' : ''}`);
|
|
14408
|
+
if (s.overBudget || (s.pctUsed != null && s.pctUsed >= 80)) {
|
|
14409
|
+
out.push('Advice : prefer terse output, squeeze large inputs, summarize-then-drop context.');
|
|
14410
|
+
}
|
|
14411
|
+
} else {
|
|
14412
|
+
out.push('Budget : none set (config sessionBudgetTokens or budgetTokens arg)');
|
|
14413
|
+
}
|
|
14414
|
+
out.push(s.context.exists
|
|
14415
|
+
? `Context : ${s.context.ageDays} day(s) old${s.context.stale ? ` — STALE (> ${s.context.ttlDays}d TTL); re-run sigmap` : ''}`
|
|
14416
|
+
: 'Context : no generated context found — run sigmap first');
|
|
14417
|
+
return out.join('\n');
|
|
14418
|
+
}
|
|
14419
|
+
|
|
14377
14420
|
/**
|
|
14378
14421
|
* get_callee_signatures — return the exact defining signature(s) of named
|
|
14379
14422
|
* symbols from the index, so an agent never guesses a callee's parameter types.
|
|
@@ -14767,7 +14810,7 @@ __factories["./src/mcp/handlers"] = function(module, exports) {
|
|
|
14767
14810
|
return header + sq.squeezed;
|
|
14768
14811
|
}
|
|
14769
14812
|
|
|
14770
|
-
module.exports = { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getMethodImpact, getImpact, getLines, readMemory, getCalleeSignatures, notifyFileCreated, notifySymbolAdded, notifyFileDeleted, getDiffContext, getArchitectureOverview, verifySuggestion, squeezeOutput };
|
|
14813
|
+
module.exports = { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getMethodImpact, getImpact, getLines, readMemory, getCalleeSignatures, notifyFileCreated, notifySymbolAdded, notifyFileDeleted, getDiffContext, getArchitectureOverview, verifySuggestion, squeezeOutput, getBudget };
|
|
14771
14814
|
|
|
14772
14815
|
};
|
|
14773
14816
|
|
|
@@ -14934,11 +14977,11 @@ __factories["./src/mcp/server"] = function(module, exports) {
|
|
|
14934
14977
|
|
|
14935
14978
|
const readline = require('readline');
|
|
14936
14979
|
const { TOOLS } = __require('./src/mcp/tools');
|
|
14937
|
-
const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getMethodImpact, getImpact, getLines, readMemory, getCalleeSignatures, notifyFileCreated, notifySymbolAdded, notifyFileDeleted, getDiffContext, getArchitectureOverview, verifySuggestion, squeezeOutput } = __require('./src/mcp/handlers');
|
|
14980
|
+
const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getMethodImpact, getImpact, getLines, readMemory, getCalleeSignatures, notifyFileCreated, notifySymbolAdded, notifyFileDeleted, getDiffContext, getArchitectureOverview, verifySuggestion, squeezeOutput, getBudget } = __require('./src/mcp/handlers');
|
|
14938
14981
|
|
|
14939
14982
|
const SERVER_INFO = {
|
|
14940
14983
|
name: 'sigmap',
|
|
14941
|
-
version: '8.
|
|
14984
|
+
version: '8.24.0',
|
|
14942
14985
|
description: 'SigMap MCP server — code signatures on demand',
|
|
14943
14986
|
};
|
|
14944
14987
|
|
|
@@ -15006,6 +15049,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
|
|
|
15006
15049
|
else if (name === 'get_architecture_overview') text = getArchitectureOverview(args, cwd);
|
|
15007
15050
|
else if (name === 'verify_suggestion') text = verifySuggestion(args, cwd);
|
|
15008
15051
|
else if (name === 'squeeze_output') text = squeezeOutput(args, cwd);
|
|
15052
|
+
else if (name === 'get_budget') text = getBudget(args, cwd);
|
|
15009
15053
|
else {
|
|
15010
15054
|
respondError(id, -32601, `Unknown tool: ${name}`);
|
|
15011
15055
|
return;
|
|
@@ -15452,6 +15496,32 @@ __factories["./src/mcp/tools"] = function(module, exports) {
|
|
|
15452
15496
|
required: ['content'],
|
|
15453
15497
|
},
|
|
15454
15498
|
},
|
|
15499
|
+
{
|
|
15500
|
+
name: 'get_budget',
|
|
15501
|
+
description:
|
|
15502
|
+
'Session spend ledger — estimated tokens SigMap has emitted this session ' +
|
|
15503
|
+
'(chars/4 estimates from the local gain log), optional budget remaining, and ' +
|
|
15504
|
+
'context freshness. Scope honesty: counts only SigMap output, NOT the whole ' +
|
|
15505
|
+
"chat's spend. Check this mid-session and degrade gracefully when near budget: " +
|
|
15506
|
+
'prefer terse encoding, squeeze large outputs, summarize-then-drop context. ' +
|
|
15507
|
+
'Local-only; no LLM, no network.',
|
|
15508
|
+
inputSchema: {
|
|
15509
|
+
type: 'object',
|
|
15510
|
+
properties: {
|
|
15511
|
+
session: {
|
|
15512
|
+
type: 'string',
|
|
15513
|
+
description:
|
|
15514
|
+
'Session key to report on (default: SIGMAP_SESSION env or the UTC day bucket).',
|
|
15515
|
+
},
|
|
15516
|
+
budgetTokens: {
|
|
15517
|
+
type: 'number',
|
|
15518
|
+
description:
|
|
15519
|
+
'Budget override in estimated tokens (default: config sessionBudgetTokens).',
|
|
15520
|
+
},
|
|
15521
|
+
},
|
|
15522
|
+
required: [],
|
|
15523
|
+
},
|
|
15524
|
+
},
|
|
15455
15525
|
];
|
|
15456
15526
|
|
|
15457
15527
|
module.exports = { TOOLS };
|
|
@@ -17458,6 +17528,66 @@ __factories["./src/security/patterns"] = function(module, exports) {
|
|
|
17458
17528
|
|
|
17459
17529
|
};
|
|
17460
17530
|
|
|
17531
|
+
// ── ./src/security/redact ──
|
|
17532
|
+
__factories["./src/security/redact"] = function(module, exports) {
|
|
17533
|
+
|
|
17534
|
+
/**
|
|
17535
|
+
* Standalone text redaction (v8.24 G3a) — the same pattern bank the
|
|
17536
|
+
* generation-time scanner uses (security/patterns.js), applied to arbitrary
|
|
17537
|
+
* text. Unlike scanner.js (which replaces whole signature lines), this masks
|
|
17538
|
+
* only the matched secret substring so surrounding text stays readable.
|
|
17539
|
+
*
|
|
17540
|
+
* Zero dependencies; never throws — on any error the original text is
|
|
17541
|
+
* returned unredacted.
|
|
17542
|
+
*/
|
|
17543
|
+
|
|
17544
|
+
const { PATTERNS } = __require('./src/security/patterns');
|
|
17545
|
+
|
|
17546
|
+
// Global variants of the pattern regexes (needed for replace-all per line).
|
|
17547
|
+
const GLOBAL_PATTERNS = PATTERNS.map((p) => ({
|
|
17548
|
+
name: p.name,
|
|
17549
|
+
regex: new RegExp(p.regex.source, p.regex.flags.includes('g') ? p.regex.flags : p.regex.flags + 'g'),
|
|
17550
|
+
}));
|
|
17551
|
+
|
|
17552
|
+
/**
|
|
17553
|
+
* Redact secrets in arbitrary text.
|
|
17554
|
+
* @param {string} text
|
|
17555
|
+
* @returns {{
|
|
17556
|
+
* text: string, redacted: boolean,
|
|
17557
|
+
* findings: Array<{ line: number, pattern: string }>,
|
|
17558
|
+
* counts: Object<string, number>
|
|
17559
|
+
* }}
|
|
17560
|
+
*/
|
|
17561
|
+
function redactText(text) {
|
|
17562
|
+
if (typeof text !== 'string' || text.length === 0) {
|
|
17563
|
+
return { text: typeof text === 'string' ? text : '', redacted: false, findings: [], counts: {} };
|
|
17564
|
+
}
|
|
17565
|
+
try {
|
|
17566
|
+
const findings = [];
|
|
17567
|
+
const counts = {};
|
|
17568
|
+
const lines = text.split('\n');
|
|
17569
|
+
const out = lines.map((line, i) => {
|
|
17570
|
+
let masked = line;
|
|
17571
|
+
for (const p of GLOBAL_PATTERNS) {
|
|
17572
|
+
p.regex.lastIndex = 0;
|
|
17573
|
+
if (!p.regex.test(masked)) continue;
|
|
17574
|
+
p.regex.lastIndex = 0;
|
|
17575
|
+
masked = masked.replace(p.regex, `[REDACTED:${p.name}]`);
|
|
17576
|
+
findings.push({ line: i + 1, pattern: p.name });
|
|
17577
|
+
counts[p.name] = (counts[p.name] || 0) + 1;
|
|
17578
|
+
}
|
|
17579
|
+
return masked;
|
|
17580
|
+
});
|
|
17581
|
+
return { text: out.join('\n'), redacted: findings.length > 0, findings, counts };
|
|
17582
|
+
} catch (_) {
|
|
17583
|
+
return { text, redacted: false, findings: [], counts: {} };
|
|
17584
|
+
}
|
|
17585
|
+
}
|
|
17586
|
+
|
|
17587
|
+
module.exports = { redactText };
|
|
17588
|
+
|
|
17589
|
+
};
|
|
17590
|
+
|
|
17461
17591
|
// ── ./src/security/scanner ──
|
|
17462
17592
|
__factories["./src/security/scanner"] = function(module, exports) {
|
|
17463
17593
|
|
|
@@ -18408,6 +18538,123 @@ __factories["./src/tracking/aggregate"] = function(module, exports) {
|
|
|
18408
18538
|
|
|
18409
18539
|
};
|
|
18410
18540
|
|
|
18541
|
+
// ── ./src/tracking/budget ──
|
|
18542
|
+
__factories["./src/tracking/budget"] = function(module, exports) {
|
|
18543
|
+
|
|
18544
|
+
/**
|
|
18545
|
+
* Session spend ledger (v8.23 F1) — a queryable view over the existing gain
|
|
18546
|
+
* log (.context/gain.ndjson, written by recordUsage).
|
|
18547
|
+
*
|
|
18548
|
+
* Honesty scope: this ledger counts tokens **SigMap emitted** (chars/4
|
|
18549
|
+
* estimates), not the host chat's total spend — conversation history, model
|
|
18550
|
+
* output, and other tools are invisible to a CLI. Every number here is an
|
|
18551
|
+
* estimate and is labeled as such in output.
|
|
18552
|
+
*
|
|
18553
|
+
* Session identity: `SIGMAP_SESSION` env var when the host sets one, else a
|
|
18554
|
+
* UTC day bucket (YYYY-MM-DD). Legacy gain entries (written before the
|
|
18555
|
+
* `session` field existed) match day-bucket sessions by timestamp prefix.
|
|
18556
|
+
*
|
|
18557
|
+
* Zero dependencies; local JSON only.
|
|
18558
|
+
*/
|
|
18559
|
+
|
|
18560
|
+
const fs = require('fs');
|
|
18561
|
+
const path = require('path');
|
|
18562
|
+
const { readGainLog } = __require('./src/tracking/logger');
|
|
18563
|
+
|
|
18564
|
+
// Same generated-context surfaces cache/freshen.js watches.
|
|
18565
|
+
const CONTEXT_PATHS = [
|
|
18566
|
+
['.github', 'copilot-instructions.md'],
|
|
18567
|
+
['CLAUDE.md'], ['AGENTS.md'], ['.github', 'context-cold.md'],
|
|
18568
|
+
];
|
|
18569
|
+
|
|
18570
|
+
/** Session key: SIGMAP_SESSION override, else UTC day bucket. */
|
|
18571
|
+
function sessionKey(env) {
|
|
18572
|
+
const e = env || process.env;
|
|
18573
|
+
if (e.SIGMAP_SESSION) return String(e.SIGMAP_SESSION);
|
|
18574
|
+
return new Date().toISOString().slice(0, 10);
|
|
18575
|
+
}
|
|
18576
|
+
|
|
18577
|
+
/** Newest mtime (ms) among generated context files, or 0 if none exist. */
|
|
18578
|
+
function contextMtime(cwd) {
|
|
18579
|
+
let newest = 0;
|
|
18580
|
+
for (const parts of CONTEXT_PATHS) {
|
|
18581
|
+
try { newest = Math.max(newest, fs.statSync(path.join(cwd, ...parts)).mtimeMs); } catch (_) {}
|
|
18582
|
+
}
|
|
18583
|
+
return newest;
|
|
18584
|
+
}
|
|
18585
|
+
|
|
18586
|
+
/** Does a gain-log entry belong to this session? */
|
|
18587
|
+
function entryInSession(entry, session) {
|
|
18588
|
+
if (entry.session) return entry.session === session;
|
|
18589
|
+
// Legacy entry: match day-bucket sessions on the timestamp date.
|
|
18590
|
+
return /^\d{4}-\d{2}-\d{2}$/.test(session) && String(entry.ts || '').startsWith(session);
|
|
18591
|
+
}
|
|
18592
|
+
|
|
18593
|
+
/**
|
|
18594
|
+
* Session spend status.
|
|
18595
|
+
* @param {string} cwd
|
|
18596
|
+
* @param {object} [opts]
|
|
18597
|
+
* @param {string} [opts.session] session key (default: sessionKey())
|
|
18598
|
+
* @param {number} [opts.budgetTokens] budget override (else config sessionBudgetTokens)
|
|
18599
|
+
* @param {number} [opts.contextTtlDays] TTL override (else config contextTtlDays)
|
|
18600
|
+
* @param {object} [opts.config] loaded config (for the two keys above)
|
|
18601
|
+
* @param {number} [opts.now] clock override for tests (ms)
|
|
18602
|
+
* @returns {{
|
|
18603
|
+
* session: string, unit: 'estimated-tokens', ops: number,
|
|
18604
|
+
* spentTokens: number, baselineTokens: number, savedTokens: number,
|
|
18605
|
+
* budgetTokens: number|null, remainingTokens: number|null, pctUsed: number|null,
|
|
18606
|
+
* overBudget: boolean,
|
|
18607
|
+
* context: { exists: boolean, ageMs: number|null, ageDays: number|null,
|
|
18608
|
+
* ttlDays: number|null, stale: boolean }
|
|
18609
|
+
* }}
|
|
18610
|
+
*/
|
|
18611
|
+
function budgetStatus(cwd, opts = {}) {
|
|
18612
|
+
const session = opts.session || sessionKey();
|
|
18613
|
+
const cfg = opts.config || {};
|
|
18614
|
+
const budget = opts.budgetTokens != null ? Number(opts.budgetTokens)
|
|
18615
|
+
: (Number.isFinite(cfg.sessionBudgetTokens) ? cfg.sessionBudgetTokens : null);
|
|
18616
|
+
const ttlDays = opts.contextTtlDays != null ? Number(opts.contextTtlDays)
|
|
18617
|
+
: (Number.isFinite(cfg.contextTtlDays) ? cfg.contextTtlDays : null);
|
|
18618
|
+
const now = opts.now != null ? opts.now : Date.now();
|
|
18619
|
+
|
|
18620
|
+
let ops = 0, spent = 0, baseline = 0, saved = 0;
|
|
18621
|
+
for (const e of readGainLog(cwd)) {
|
|
18622
|
+
if (!entryInSession(e, session)) continue;
|
|
18623
|
+
ops++;
|
|
18624
|
+
spent += Number(e.actualTokens) || 0;
|
|
18625
|
+
baseline += Number(e.baselineTokens) || 0;
|
|
18626
|
+
saved += Number(e.savedTokens) || 0;
|
|
18627
|
+
}
|
|
18628
|
+
|
|
18629
|
+
const mtime = contextMtime(cwd);
|
|
18630
|
+
const ageMs = mtime > 0 ? Math.max(0, now - mtime) : null;
|
|
18631
|
+
const ageDays = ageMs != null ? ageMs / 86400000 : null;
|
|
18632
|
+
|
|
18633
|
+
return {
|
|
18634
|
+
session,
|
|
18635
|
+
unit: 'estimated-tokens',
|
|
18636
|
+
ops,
|
|
18637
|
+
spentTokens: spent,
|
|
18638
|
+
baselineTokens: baseline,
|
|
18639
|
+
savedTokens: saved,
|
|
18640
|
+
budgetTokens: budget,
|
|
18641
|
+
remainingTokens: budget != null ? Math.max(0, budget - spent) : null,
|
|
18642
|
+
pctUsed: budget > 0 ? Math.round((spent / budget) * 1000) / 10 : null,
|
|
18643
|
+
overBudget: budget != null && spent > budget,
|
|
18644
|
+
context: {
|
|
18645
|
+
exists: mtime > 0,
|
|
18646
|
+
ageMs,
|
|
18647
|
+
ageDays: ageDays != null ? Math.round(ageDays * 10) / 10 : null,
|
|
18648
|
+
ttlDays,
|
|
18649
|
+
stale: ttlDays != null && ageDays != null && ageDays > ttlDays,
|
|
18650
|
+
},
|
|
18651
|
+
};
|
|
18652
|
+
}
|
|
18653
|
+
|
|
18654
|
+
module.exports = { sessionKey, budgetStatus, contextMtime, entryInSession };
|
|
18655
|
+
|
|
18656
|
+
};
|
|
18657
|
+
|
|
18411
18658
|
// ── ./src/tracking/logger ──
|
|
18412
18659
|
__factories["./src/tracking/logger"] = function(module, exports) {
|
|
18413
18660
|
|
|
@@ -18586,10 +18833,12 @@ __factories["./src/tracking/logger"] = function(module, exports) {
|
|
|
18586
18833
|
const baseline = Math.max(0, Number(entry.baselineTokens) || 0);
|
|
18587
18834
|
const actual = Math.max(0, Number(entry.actualTokens) || 0);
|
|
18588
18835
|
const saved = Math.max(0, baseline - actual);
|
|
18836
|
+
const ts = new Date().toISOString();
|
|
18589
18837
|
const record = {
|
|
18590
|
-
ts
|
|
18838
|
+
ts,
|
|
18591
18839
|
v: entry.version || '0.9.0',
|
|
18592
18840
|
op: entry.op || 'generate',
|
|
18841
|
+
session: entry.session || process.env.SIGMAP_SESSION || ts.slice(0, 10),
|
|
18593
18842
|
baselineTokens: baseline,
|
|
18594
18843
|
actualTokens: actual,
|
|
18595
18844
|
savedTokens: saved,
|
|
@@ -20158,7 +20407,7 @@ function __tryGit(args, opts = {}) {
|
|
|
20158
20407
|
catch (_) { return ''; }
|
|
20159
20408
|
}
|
|
20160
20409
|
|
|
20161
|
-
const VERSION = '8.
|
|
20410
|
+
const VERSION = '8.24.0';
|
|
20162
20411
|
const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
|
|
20163
20412
|
|
|
20164
20413
|
function requireSourceOrBundled(key) {
|
|
@@ -22043,6 +22292,9 @@ Usage:
|
|
|
22043
22292
|
${cmd} evidence "<query>" --top <n> --budget <n> --out <path> Tune ranked files / token budget / write rendered output
|
|
22044
22293
|
${cmd} memory List cross-session stores (.context/) — entries, size, age
|
|
22045
22294
|
${cmd} memory --clear <store> Clear one store: session|notes|weights|evidence|all (--json supported)
|
|
22295
|
+
${cmd} budget Session spend ledger — estimated SigMap-emitted tokens, budget, context age (--json)
|
|
22296
|
+
${cmd} budget --budget <tokens> One-off budget override (config: sessionBudgetTokens, contextTtlDays)
|
|
22297
|
+
${cmd} redact [file] Mask secrets in a file or stdin (10-pattern bank); redacted text to stdout (--json)
|
|
22046
22298
|
${cmd} note "<text>" Append a note to the cross-session decision log
|
|
22047
22299
|
${cmd} note List recent notes (also: note --list <N>)
|
|
22048
22300
|
${cmd} status Show repo state — branch, dirty files, index freshness, notes
|
|
@@ -23494,6 +23746,72 @@ function main() {
|
|
|
23494
23746
|
process.exit(0);
|
|
23495
23747
|
}
|
|
23496
23748
|
|
|
23749
|
+
if (args[0] === 'budget') {
|
|
23750
|
+
const jsonOut = args.includes('--json');
|
|
23751
|
+
const { budgetStatus } = requireSourceOrBundled('./src/tracking/budget');
|
|
23752
|
+
let config = {};
|
|
23753
|
+
try { config = requireSourceOrBundled('./src/config/loader').loadConfig(cwd) || {}; } catch (_) {}
|
|
23754
|
+
const opts = { config };
|
|
23755
|
+
const sIdx = args.indexOf('--session');
|
|
23756
|
+
if (sIdx !== -1 && args[sIdx + 1] && !args[sIdx + 1].startsWith('--')) opts.session = args[sIdx + 1];
|
|
23757
|
+
const bIdx = args.indexOf('--budget');
|
|
23758
|
+
if (bIdx !== -1 && args[bIdx + 1] && !args[bIdx + 1].startsWith('--')) opts.budgetTokens = Number(args[bIdx + 1]);
|
|
23759
|
+
const st = budgetStatus(cwd, opts);
|
|
23760
|
+
if (jsonOut) {
|
|
23761
|
+
process.stdout.write(JSON.stringify(st) + '\n');
|
|
23762
|
+
process.exit(0);
|
|
23763
|
+
}
|
|
23764
|
+
console.log('[sigmap] session spend (estimates — chars/4; SigMap-emitted tokens only)');
|
|
23765
|
+
console.log(` session ${st.session}`);
|
|
23766
|
+
console.log(` ops ${st.ops}`);
|
|
23767
|
+
console.log(` spent ~${st.spentTokens.toLocaleString()} tokens (baseline ~${st.baselineTokens.toLocaleString()}, saved ~${st.savedTokens.toLocaleString()})`);
|
|
23768
|
+
if (st.budgetTokens != null) {
|
|
23769
|
+
console.log(` budget ${st.budgetTokens.toLocaleString()} → remaining ~${st.remainingTokens.toLocaleString()} (${st.pctUsed}% used)${st.overBudget ? ' ⚠ OVER BUDGET' : ''}`);
|
|
23770
|
+
} else {
|
|
23771
|
+
console.log(' budget none set (config sessionBudgetTokens or --budget <tokens>)');
|
|
23772
|
+
}
|
|
23773
|
+
console.log(st.context.exists
|
|
23774
|
+
? ` context ${st.context.ageDays} day(s) old${st.context.stale ? ` ⚠ STALE (> ${st.context.ttlDays}d TTL) — re-run sigmap` : ''}`
|
|
23775
|
+
: ' context none generated yet — run sigmap first');
|
|
23776
|
+
process.exit(0);
|
|
23777
|
+
}
|
|
23778
|
+
|
|
23779
|
+
if (args[0] === 'redact') {
|
|
23780
|
+
const jsonOut = args.includes('--json');
|
|
23781
|
+
const { redactText } = requireSourceOrBundled('./src/security/redact');
|
|
23782
|
+
const fileArg = args.slice(1).find((a) => !a.startsWith('--'));
|
|
23783
|
+
let input;
|
|
23784
|
+
if (fileArg) {
|
|
23785
|
+
try {
|
|
23786
|
+
input = fs.readFileSync(path.resolve(cwd, fileArg), 'utf8');
|
|
23787
|
+
} catch (err) {
|
|
23788
|
+
console.error(`[sigmap] redact: cannot read ${fileArg}: ${err.message}`);
|
|
23789
|
+
process.exit(1);
|
|
23790
|
+
}
|
|
23791
|
+
} else if (!process.stdin.isTTY) {
|
|
23792
|
+
try {
|
|
23793
|
+
input = fs.readFileSync(0, 'utf8');
|
|
23794
|
+
} catch (err) {
|
|
23795
|
+
console.error(`[sigmap] redact: cannot read stdin: ${err.message}`);
|
|
23796
|
+
process.exit(1);
|
|
23797
|
+
}
|
|
23798
|
+
} else {
|
|
23799
|
+
console.error('[sigmap] usage: sigmap redact <file> or <cmd> | sigmap redact');
|
|
23800
|
+
process.exit(1);
|
|
23801
|
+
}
|
|
23802
|
+
const r = redactText(input);
|
|
23803
|
+
if (jsonOut) {
|
|
23804
|
+
process.stdout.write(JSON.stringify(r) + '\n');
|
|
23805
|
+
process.exit(0);
|
|
23806
|
+
}
|
|
23807
|
+
process.stdout.write(r.text);
|
|
23808
|
+
const parts = Object.entries(r.counts).map(([k, v]) => `${k}×${v}`);
|
|
23809
|
+
console.error(r.redacted
|
|
23810
|
+
? `[sigmap] redact: masked ${r.findings.length} secret(s) — ${parts.join(', ')}`
|
|
23811
|
+
: '[sigmap] redact: no secrets found');
|
|
23812
|
+
process.exit(0);
|
|
23813
|
+
}
|
|
23814
|
+
|
|
23497
23815
|
if (args[0] === 'note') {
|
|
23498
23816
|
const jsonOut = args.includes('--json');
|
|
23499
23817
|
const { addNote, readNotes, formatNotes } = requireSourceOrBundled('./src/session/notes');
|
package/llms-full.txt
CHANGED
|
@@ -11,13 +11,13 @@ ranking keeps the relevant context in scope (cutting tokens ~97% as a side
|
|
|
11
11
|
effect), with no LLM calls, embeddings, or vector database. Works with Claude,
|
|
12
12
|
Cursor, GitHub Copilot, Aider, Windsurf, local LLMs, and MCP.
|
|
13
13
|
|
|
14
|
-
# Version: 8.
|
|
14
|
+
# Version: 8.24.0 | Benchmark: sigmap-v8.24-main (2026-07-28)
|
|
15
15
|
# Source: auto-generated from package.json, version.json, benchmarks/latest.json, src/mcp/tools.js, src/config/defaults.js
|
|
16
16
|
# Regenerate: npm run generate:llms | Validate: npm run validate:llms
|
|
17
17
|
|
|
18
18
|
---
|
|
19
19
|
|
|
20
|
-
## Core metrics (benchmark: sigmap-v8.
|
|
20
|
+
## Core metrics (benchmark: sigmap-v8.24-main, 2026-07-28)
|
|
21
21
|
|
|
22
22
|
| Metric | Without SigMap | With SigMap |
|
|
23
23
|
|--------|----------------|-------------|
|
|
@@ -26,7 +26,7 @@ Cursor, GitHub Copilot, Aider, Windsurf, local LLMs, and MCP.
|
|
|
26
26
|
| Task-success proxy (modeled) | — | 64.8% |
|
|
27
27
|
| Prompts per task | 2.84 | 1.53 (46.1% fewer) |
|
|
28
28
|
| Supported languages | — | 33 |
|
|
29
|
-
| MCP tools | — |
|
|
29
|
+
| MCP tools | — | 21 |
|
|
30
30
|
| npm runtime dependencies | — | 0 |
|
|
31
31
|
|
|
32
32
|
---
|
|
@@ -125,6 +125,9 @@ sigmap evidence "<query>" --markdown Emit the Markdown handoff rendering to
|
|
|
125
125
|
sigmap evidence "<query>" --top <n> --budget <n> --out <path> Tune ranked files / token budget / write rendered output
|
|
126
126
|
sigmap memory List cross-session stores (.context/) — entries, size, age
|
|
127
127
|
sigmap memory --clear <store> Clear one store: session|notes|weights|evidence|all (--json supported)
|
|
128
|
+
sigmap budget Session spend ledger — estimated SigMap-emitted tokens, budget, context age (--json)
|
|
129
|
+
sigmap budget --budget <tokens> One-off budget override (config: sessionBudgetTokens, contextTtlDays)
|
|
130
|
+
sigmap redact [file] Mask secrets in a file or stdin (10-pattern bank); redacted text to stdout (--json)
|
|
128
131
|
sigmap note "<text>" Append a note to the cross-session decision log
|
|
129
132
|
sigmap note List recent notes (also: note --list <N>)
|
|
130
133
|
sigmap status Show repo state — branch, dirty files, index freshness, notes
|
|
@@ -138,7 +141,7 @@ sigmap --version Show version
|
|
|
138
141
|
|
|
139
142
|
---
|
|
140
143
|
|
|
141
|
-
## MCP server —
|
|
144
|
+
## MCP server — 21 tools
|
|
142
145
|
|
|
143
146
|
Start with `sigmap --mcp` (stdio JSON-RPC). Configure once:
|
|
144
147
|
|
|
@@ -306,6 +309,14 @@ Compress noisy tool, command, or agent output before it enters context — a sta
|
|
|
306
309
|
Input: { content: string }
|
|
307
310
|
```
|
|
308
311
|
|
|
312
|
+
### get_budget
|
|
313
|
+
|
|
314
|
+
Session spend ledger — estimated tokens SigMap has emitted this session (chars/4 estimates from the local gain log), optional budget remaining, and context freshness. Scope honesty: counts only SigMap output, NOT the whole chat's spend. Check this mid-session and degrade gracefully when near budget: prefer terse encoding, squeeze large outputs, summarize-then-drop context. Local-only; no LLM, no network.
|
|
315
|
+
|
|
316
|
+
```
|
|
317
|
+
Input: { session?: string, budgetTokens?: number }
|
|
318
|
+
```
|
|
319
|
+
|
|
309
320
|
---
|
|
310
321
|
|
|
311
322
|
## Configuration (gen-context.config.json)
|
|
@@ -334,6 +345,8 @@ watchDebounce = 300
|
|
|
334
345
|
routing = false
|
|
335
346
|
format = default
|
|
336
347
|
tracking = false
|
|
348
|
+
sessionBudgetTokens = null
|
|
349
|
+
contextTtlDays = null
|
|
337
350
|
mcp = {"autoRegister":true}
|
|
338
351
|
depMap = true
|
|
339
352
|
versionPins = true
|
package/llms.txt
CHANGED
|
@@ -11,7 +11,7 @@ ranking keeps the relevant context in scope (cutting tokens ~97% as a side
|
|
|
11
11
|
effect), with no LLM calls, embeddings, or vector database. Works with Claude,
|
|
12
12
|
Cursor, GitHub Copilot, Aider, Windsurf, local LLMs, and MCP.
|
|
13
13
|
|
|
14
|
-
# Version: 8.
|
|
14
|
+
# Version: 8.24.0 | Benchmark: sigmap-v8.24-main (2026-07-28)
|
|
15
15
|
# Source: auto-generated from package.json, version.json, benchmarks/latest.json, src/mcp/tools.js, src/config/defaults.js
|
|
16
16
|
# Regenerate: npm run generate:llms | Validate: npm run validate:llms
|
|
17
17
|
|
|
@@ -23,13 +23,13 @@ Cursor, GitHub Copilot, Aider, Windsurf, local LLMs, and MCP.
|
|
|
23
23
|
- No blast-radius awareness before editing a hub file — `--impact` shows every file a change touches.
|
|
24
24
|
- Pasted stack traces, CI logs, and JSON bloat the prompt — `squeeze` minimizes them and enriches the top frame from the symbol index.
|
|
25
25
|
|
|
26
|
-
## Core metrics (benchmark: sigmap-v8.
|
|
26
|
+
## Core metrics (benchmark: sigmap-v8.24-main, 2026-07-28)
|
|
27
27
|
|
|
28
28
|
- hit@5 retrieval: 82.2% vs 44.8% single-shot grep baseline (1.59× lift)
|
|
29
29
|
- Token reduction: 96.8% average across benchmark repos
|
|
30
30
|
- Task-success proxy: 64.8% (modeled from retrieval tiers, not measured LLM sessions)
|
|
31
31
|
- Prompts per task: 1.53 vs 2.84 baseline (46.1% fewer, modeled)
|
|
32
|
-
- Languages: 33 supported · MCP tools:
|
|
32
|
+
- Languages: 33 supported · MCP tools: 21
|
|
33
33
|
- Dependencies: zero npm runtime dependencies · fully offline
|
|
34
34
|
|
|
35
35
|
## Quick start
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sigmap",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.24.0",
|
|
4
4
|
"description": "The deterministic, verifiable grounding layer for AI code work — a zero-dependency signature-and-evidence map that grounds Claude, Cursor, Copilot, Aider, Windsurf, local LLMs & MCP agents against your real code (repo + installed libraries) so they stop hallucinating files, imports & APIs. Runs offline via npx; byte-stable output; ~97% token reduction as proof.",
|
|
5
5
|
"main": "packages/core/index.js",
|
|
6
6
|
"exports": {
|
package/src/config/defaults.js
CHANGED
|
@@ -106,6 +106,14 @@ const DEFAULTS = {
|
|
|
106
106
|
// Append run metrics to .context/usage.ndjson after each generate
|
|
107
107
|
tracking: false,
|
|
108
108
|
|
|
109
|
+
// Session spend ledger (`sigmap budget` / MCP get_budget). Estimates only —
|
|
110
|
+
// counts tokens SigMap emitted (chars/4), not the host chat's total spend.
|
|
111
|
+
// Number → warn threshold for estimated SigMap-emitted tokens per session.
|
|
112
|
+
sessionBudgetTokens: null,
|
|
113
|
+
|
|
114
|
+
// Number of days before generated context counts as stale in budget output.
|
|
115
|
+
contextTtlDays: null,
|
|
116
|
+
|
|
109
117
|
// MCP server configuration
|
|
110
118
|
mcp: {
|
|
111
119
|
autoRegister: true,
|
package/src/mcp/handlers.js
CHANGED
|
@@ -586,6 +586,41 @@ function readMemory(args, cwd) {
|
|
|
586
586
|
return out.join('\n');
|
|
587
587
|
}
|
|
588
588
|
|
|
589
|
+
/**
|
|
590
|
+
* get_budget({ session?, budgetTokens? }) → string
|
|
591
|
+
*
|
|
592
|
+
* Session spend ledger (F1): estimated tokens SigMap emitted this session,
|
|
593
|
+
* optional budget remaining, and context freshness. Estimates only (chars/4);
|
|
594
|
+
* does NOT see the host chat's total spend.
|
|
595
|
+
*/
|
|
596
|
+
function getBudget(args, cwd) {
|
|
597
|
+
const { budgetStatus } = require('../tracking/budget');
|
|
598
|
+
let config = {};
|
|
599
|
+
try { config = require('../config/loader').loadConfig(cwd) || {}; } catch (_) {}
|
|
600
|
+
const opts = { config };
|
|
601
|
+
if (args && args.session) opts.session = String(args.session);
|
|
602
|
+
if (args && args.budgetTokens != null) opts.budgetTokens = Number(args.budgetTokens);
|
|
603
|
+
const s = budgetStatus(cwd, opts);
|
|
604
|
+
|
|
605
|
+
const out = ['# SigMap session spend (estimates — chars/4; SigMap-emitted tokens only)'];
|
|
606
|
+
out.push('');
|
|
607
|
+
out.push(`Session : ${s.session}`);
|
|
608
|
+
out.push(`Ops : ${s.ops}`);
|
|
609
|
+
out.push(`Spent : ~${s.spentTokens.toLocaleString()} tokens (baseline ~${s.baselineTokens.toLocaleString()}, saved ~${s.savedTokens.toLocaleString()})`);
|
|
610
|
+
if (s.budgetTokens != null) {
|
|
611
|
+
out.push(`Budget : ${s.budgetTokens.toLocaleString()} → remaining ~${s.remainingTokens.toLocaleString()} (${s.pctUsed}% used)${s.overBudget ? ' ⚠ OVER BUDGET' : ''}`);
|
|
612
|
+
if (s.overBudget || (s.pctUsed != null && s.pctUsed >= 80)) {
|
|
613
|
+
out.push('Advice : prefer terse output, squeeze large inputs, summarize-then-drop context.');
|
|
614
|
+
}
|
|
615
|
+
} else {
|
|
616
|
+
out.push('Budget : none set (config sessionBudgetTokens or budgetTokens arg)');
|
|
617
|
+
}
|
|
618
|
+
out.push(s.context.exists
|
|
619
|
+
? `Context : ${s.context.ageDays} day(s) old${s.context.stale ? ` — STALE (> ${s.context.ttlDays}d TTL); re-run sigmap` : ''}`
|
|
620
|
+
: 'Context : no generated context found — run sigmap first');
|
|
621
|
+
return out.join('\n');
|
|
622
|
+
}
|
|
623
|
+
|
|
589
624
|
/**
|
|
590
625
|
* get_callee_signatures — return the exact defining signature(s) of named
|
|
591
626
|
* symbols from the index, so an agent never guesses a callee's parameter types.
|
|
@@ -979,4 +1014,4 @@ function squeezeOutput(args, cwd) {
|
|
|
979
1014
|
return header + sq.squeezed;
|
|
980
1015
|
}
|
|
981
1016
|
|
|
982
|
-
module.exports = { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getMethodImpact, getImpact, getLines, readMemory, getCalleeSignatures, notifyFileCreated, notifySymbolAdded, notifyFileDeleted, getDiffContext, getArchitectureOverview, verifySuggestion, squeezeOutput };
|
|
1017
|
+
module.exports = { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getMethodImpact, getImpact, getLines, readMemory, getCalleeSignatures, notifyFileCreated, notifySymbolAdded, notifyFileDeleted, getDiffContext, getArchitectureOverview, verifySuggestion, squeezeOutput, getBudget };
|
package/src/mcp/server.js
CHANGED
|
@@ -14,11 +14,11 @@
|
|
|
14
14
|
|
|
15
15
|
const readline = require('readline');
|
|
16
16
|
const { TOOLS } = require('./tools');
|
|
17
|
-
const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getMethodImpact, getImpact, getLines, readMemory, getCalleeSignatures, notifyFileCreated, notifySymbolAdded, notifyFileDeleted, getDiffContext, getArchitectureOverview, verifySuggestion, squeezeOutput } = require('./handlers');
|
|
17
|
+
const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getMethodImpact, getImpact, getLines, readMemory, getCalleeSignatures, notifyFileCreated, notifySymbolAdded, notifyFileDeleted, getDiffContext, getArchitectureOverview, verifySuggestion, squeezeOutput, getBudget } = require('./handlers');
|
|
18
18
|
|
|
19
19
|
const SERVER_INFO = {
|
|
20
20
|
name: 'sigmap',
|
|
21
|
-
version: '8.
|
|
21
|
+
version: '8.24.0',
|
|
22
22
|
description: 'SigMap MCP server — code signatures on demand',
|
|
23
23
|
};
|
|
24
24
|
|
|
@@ -86,6 +86,7 @@ function dispatch(msg, cwd) {
|
|
|
86
86
|
else if (name === 'get_architecture_overview') text = getArchitectureOverview(args, cwd);
|
|
87
87
|
else if (name === 'verify_suggestion') text = verifySuggestion(args, cwd);
|
|
88
88
|
else if (name === 'squeeze_output') text = squeezeOutput(args, cwd);
|
|
89
|
+
else if (name === 'get_budget') text = getBudget(args, cwd);
|
|
89
90
|
else {
|
|
90
91
|
respondError(id, -32601, `Unknown tool: ${name}`);
|
|
91
92
|
return;
|
package/src/mcp/tools.js
CHANGED
|
@@ -387,6 +387,32 @@ const TOOLS = [
|
|
|
387
387
|
required: ['content'],
|
|
388
388
|
},
|
|
389
389
|
},
|
|
390
|
+
{
|
|
391
|
+
name: 'get_budget',
|
|
392
|
+
description:
|
|
393
|
+
'Session spend ledger — estimated tokens SigMap has emitted this session ' +
|
|
394
|
+
'(chars/4 estimates from the local gain log), optional budget remaining, and ' +
|
|
395
|
+
'context freshness. Scope honesty: counts only SigMap output, NOT the whole ' +
|
|
396
|
+
"chat's spend. Check this mid-session and degrade gracefully when near budget: " +
|
|
397
|
+
'prefer terse encoding, squeeze large outputs, summarize-then-drop context. ' +
|
|
398
|
+
'Local-only; no LLM, no network.',
|
|
399
|
+
inputSchema: {
|
|
400
|
+
type: 'object',
|
|
401
|
+
properties: {
|
|
402
|
+
session: {
|
|
403
|
+
type: 'string',
|
|
404
|
+
description:
|
|
405
|
+
'Session key to report on (default: SIGMAP_SESSION env or the UTC day bucket).',
|
|
406
|
+
},
|
|
407
|
+
budgetTokens: {
|
|
408
|
+
type: 'number',
|
|
409
|
+
description:
|
|
410
|
+
'Budget override in estimated tokens (default: config sessionBudgetTokens).',
|
|
411
|
+
},
|
|
412
|
+
},
|
|
413
|
+
required: [],
|
|
414
|
+
},
|
|
415
|
+
},
|
|
390
416
|
];
|
|
391
417
|
|
|
392
418
|
module.exports = { TOOLS };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Standalone text redaction (v8.24 G3a) — the same pattern bank the
|
|
5
|
+
* generation-time scanner uses (security/patterns.js), applied to arbitrary
|
|
6
|
+
* text. Unlike scanner.js (which replaces whole signature lines), this masks
|
|
7
|
+
* only the matched secret substring so surrounding text stays readable.
|
|
8
|
+
*
|
|
9
|
+
* Zero dependencies; never throws — on any error the original text is
|
|
10
|
+
* returned unredacted.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const { PATTERNS } = require('./patterns');
|
|
14
|
+
|
|
15
|
+
// Global variants of the pattern regexes (needed for replace-all per line).
|
|
16
|
+
const GLOBAL_PATTERNS = PATTERNS.map((p) => ({
|
|
17
|
+
name: p.name,
|
|
18
|
+
regex: new RegExp(p.regex.source, p.regex.flags.includes('g') ? p.regex.flags : p.regex.flags + 'g'),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Redact secrets in arbitrary text.
|
|
23
|
+
* @param {string} text
|
|
24
|
+
* @returns {{
|
|
25
|
+
* text: string, redacted: boolean,
|
|
26
|
+
* findings: Array<{ line: number, pattern: string }>,
|
|
27
|
+
* counts: Object<string, number>
|
|
28
|
+
* }}
|
|
29
|
+
*/
|
|
30
|
+
function redactText(text) {
|
|
31
|
+
if (typeof text !== 'string' || text.length === 0) {
|
|
32
|
+
return { text: typeof text === 'string' ? text : '', redacted: false, findings: [], counts: {} };
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
const findings = [];
|
|
36
|
+
const counts = {};
|
|
37
|
+
const lines = text.split('\n');
|
|
38
|
+
const out = lines.map((line, i) => {
|
|
39
|
+
let masked = line;
|
|
40
|
+
for (const p of GLOBAL_PATTERNS) {
|
|
41
|
+
p.regex.lastIndex = 0;
|
|
42
|
+
if (!p.regex.test(masked)) continue;
|
|
43
|
+
p.regex.lastIndex = 0;
|
|
44
|
+
masked = masked.replace(p.regex, `[REDACTED:${p.name}]`);
|
|
45
|
+
findings.push({ line: i + 1, pattern: p.name });
|
|
46
|
+
counts[p.name] = (counts[p.name] || 0) + 1;
|
|
47
|
+
}
|
|
48
|
+
return masked;
|
|
49
|
+
});
|
|
50
|
+
return { text: out.join('\n'), redacted: findings.length > 0, findings, counts };
|
|
51
|
+
} catch (_) {
|
|
52
|
+
return { text, redacted: false, findings: [], counts: {} };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = { redactText };
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Session spend ledger (v8.23 F1) — a queryable view over the existing gain
|
|
5
|
+
* log (.context/gain.ndjson, written by recordUsage).
|
|
6
|
+
*
|
|
7
|
+
* Honesty scope: this ledger counts tokens **SigMap emitted** (chars/4
|
|
8
|
+
* estimates), not the host chat's total spend — conversation history, model
|
|
9
|
+
* output, and other tools are invisible to a CLI. Every number here is an
|
|
10
|
+
* estimate and is labeled as such in output.
|
|
11
|
+
*
|
|
12
|
+
* Session identity: `SIGMAP_SESSION` env var when the host sets one, else a
|
|
13
|
+
* UTC day bucket (YYYY-MM-DD). Legacy gain entries (written before the
|
|
14
|
+
* `session` field existed) match day-bucket sessions by timestamp prefix.
|
|
15
|
+
*
|
|
16
|
+
* Zero dependencies; local JSON only.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
const { readGainLog } = require('./logger');
|
|
22
|
+
|
|
23
|
+
// Same generated-context surfaces cache/freshen.js watches.
|
|
24
|
+
const CONTEXT_PATHS = [
|
|
25
|
+
['.github', 'copilot-instructions.md'],
|
|
26
|
+
['CLAUDE.md'], ['AGENTS.md'], ['.github', 'context-cold.md'],
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
/** Session key: SIGMAP_SESSION override, else UTC day bucket. */
|
|
30
|
+
function sessionKey(env) {
|
|
31
|
+
const e = env || process.env;
|
|
32
|
+
if (e.SIGMAP_SESSION) return String(e.SIGMAP_SESSION);
|
|
33
|
+
return new Date().toISOString().slice(0, 10);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Newest mtime (ms) among generated context files, or 0 if none exist. */
|
|
37
|
+
function contextMtime(cwd) {
|
|
38
|
+
let newest = 0;
|
|
39
|
+
for (const parts of CONTEXT_PATHS) {
|
|
40
|
+
try { newest = Math.max(newest, fs.statSync(path.join(cwd, ...parts)).mtimeMs); } catch (_) {}
|
|
41
|
+
}
|
|
42
|
+
return newest;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Does a gain-log entry belong to this session? */
|
|
46
|
+
function entryInSession(entry, session) {
|
|
47
|
+
if (entry.session) return entry.session === session;
|
|
48
|
+
// Legacy entry: match day-bucket sessions on the timestamp date.
|
|
49
|
+
return /^\d{4}-\d{2}-\d{2}$/.test(session) && String(entry.ts || '').startsWith(session);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Session spend status.
|
|
54
|
+
* @param {string} cwd
|
|
55
|
+
* @param {object} [opts]
|
|
56
|
+
* @param {string} [opts.session] session key (default: sessionKey())
|
|
57
|
+
* @param {number} [opts.budgetTokens] budget override (else config sessionBudgetTokens)
|
|
58
|
+
* @param {number} [opts.contextTtlDays] TTL override (else config contextTtlDays)
|
|
59
|
+
* @param {object} [opts.config] loaded config (for the two keys above)
|
|
60
|
+
* @param {number} [opts.now] clock override for tests (ms)
|
|
61
|
+
* @returns {{
|
|
62
|
+
* session: string, unit: 'estimated-tokens', ops: number,
|
|
63
|
+
* spentTokens: number, baselineTokens: number, savedTokens: number,
|
|
64
|
+
* budgetTokens: number|null, remainingTokens: number|null, pctUsed: number|null,
|
|
65
|
+
* overBudget: boolean,
|
|
66
|
+
* context: { exists: boolean, ageMs: number|null, ageDays: number|null,
|
|
67
|
+
* ttlDays: number|null, stale: boolean }
|
|
68
|
+
* }}
|
|
69
|
+
*/
|
|
70
|
+
function budgetStatus(cwd, opts = {}) {
|
|
71
|
+
const session = opts.session || sessionKey();
|
|
72
|
+
const cfg = opts.config || {};
|
|
73
|
+
const budget = opts.budgetTokens != null ? Number(opts.budgetTokens)
|
|
74
|
+
: (Number.isFinite(cfg.sessionBudgetTokens) ? cfg.sessionBudgetTokens : null);
|
|
75
|
+
const ttlDays = opts.contextTtlDays != null ? Number(opts.contextTtlDays)
|
|
76
|
+
: (Number.isFinite(cfg.contextTtlDays) ? cfg.contextTtlDays : null);
|
|
77
|
+
const now = opts.now != null ? opts.now : Date.now();
|
|
78
|
+
|
|
79
|
+
let ops = 0, spent = 0, baseline = 0, saved = 0;
|
|
80
|
+
for (const e of readGainLog(cwd)) {
|
|
81
|
+
if (!entryInSession(e, session)) continue;
|
|
82
|
+
ops++;
|
|
83
|
+
spent += Number(e.actualTokens) || 0;
|
|
84
|
+
baseline += Number(e.baselineTokens) || 0;
|
|
85
|
+
saved += Number(e.savedTokens) || 0;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const mtime = contextMtime(cwd);
|
|
89
|
+
const ageMs = mtime > 0 ? Math.max(0, now - mtime) : null;
|
|
90
|
+
const ageDays = ageMs != null ? ageMs / 86400000 : null;
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
session,
|
|
94
|
+
unit: 'estimated-tokens',
|
|
95
|
+
ops,
|
|
96
|
+
spentTokens: spent,
|
|
97
|
+
baselineTokens: baseline,
|
|
98
|
+
savedTokens: saved,
|
|
99
|
+
budgetTokens: budget,
|
|
100
|
+
remainingTokens: budget != null ? Math.max(0, budget - spent) : null,
|
|
101
|
+
pctUsed: budget > 0 ? Math.round((spent / budget) * 1000) / 10 : null,
|
|
102
|
+
overBudget: budget != null && spent > budget,
|
|
103
|
+
context: {
|
|
104
|
+
exists: mtime > 0,
|
|
105
|
+
ageMs,
|
|
106
|
+
ageDays: ageDays != null ? Math.round(ageDays * 10) / 10 : null,
|
|
107
|
+
ttlDays,
|
|
108
|
+
stale: ttlDays != null && ageDays != null && ageDays > ttlDays,
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
module.exports = { sessionKey, budgetStatus, contextMtime, entryInSession };
|
package/src/tracking/logger.js
CHANGED
|
@@ -175,10 +175,12 @@ function recordUsage(entry, cwd) {
|
|
|
175
175
|
const baseline = Math.max(0, Number(entry.baselineTokens) || 0);
|
|
176
176
|
const actual = Math.max(0, Number(entry.actualTokens) || 0);
|
|
177
177
|
const saved = Math.max(0, baseline - actual);
|
|
178
|
+
const ts = new Date().toISOString();
|
|
178
179
|
const record = {
|
|
179
|
-
ts
|
|
180
|
+
ts,
|
|
180
181
|
v: entry.version || '0.9.0',
|
|
181
182
|
op: entry.op || 'generate',
|
|
183
|
+
session: entry.session || process.env.SIGMAP_SESSION || ts.slice(0, 10),
|
|
182
184
|
baselineTokens: baseline,
|
|
183
185
|
actualTokens: actual,
|
|
184
186
|
savedTokens: saved,
|