sigmap 7.0.0 → 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 CHANGED
@@ -10,6 +10,35 @@ 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
+
28
+ ## [7.0.1] — 2026-06-14
29
+
30
+ Patch release — supply-chain hardening and package hygiene, plus a wider star nudge.
31
+
32
+ ### Fixed
33
+ - **Eliminated system-shell access (#252):** every `child_process.execSync` call (which runs via `/bin/sh -c`) was converted to shell-free `execFileSync` with an arguments array. Several commands had previously interpolated values into the command string (`git diff ${range}`, `HEAD~${n}`, `printf '%s' … | ${clipCmd}`, `node -e "…http.get…"`) — a real shell-injection surface. New `src/util/git.js` (`git()`/`tryGit()`) centralizes shell-free git; the `extends` config fetch passes the URL as an argv to node; `compare` spawns node by argv; clipboard copy writes via stdin. Net: zero `execSync`/`exec`/`shell:true` in the published surface, which clears Socket's "Shell access" capability alert.
34
+
35
+ ### Changed
36
+ - **Star nudge now counts plain `sigmap` runs (#251):** the one-time GitHub-star nudge previously only counted `ask`/`squeeze`. Users who only run `sigmap` to generate the context file now also reach the 10-run threshold. Counted once per process at the end of generation (monorepo-safe, tracked at the repo root); interactive-only — silent under `--json`/`--report`/`--quiet`/non-TTY.
37
+ - **`main` points at the importable core (#252):** `package.json` `main` changed from the CLI bundle (`gen-context.js`, which runs `main()` + exits on `require`) to `packages/core/index.js`, matching `exports["."]`. Bundlephobia and legacy resolvers now see the real zero-dep API.
38
+ - **Removed unused device fingerprint (#252):** the star nudge no longer records `machineId = sha256(os.hostname())` in `.context/usage.json` — it was never read or transmitted. Dropped the now-unused `os`/`crypto` requires.
39
+
40
+ ---
41
+
13
42
  ## [7.0.0] — 2026-06-14
14
43
 
15
44
  Major release — **Squeeze** makes `ask` minimize pasted input by default, a behavioral change to the core command.
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://manojmallick.github.io/sigmap/guide/benchmark.html)
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://manojmallick.github.io/sigmap/guide/agents)** — OpenCode, Aider, OpenHands, Cline
176
- - **[Local LLMs guide →](https://manojmallick.github.io/sigmap/guide/local-llms)** — Ollama, llama.cpp, vLLM (no API keys, full privacy)
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://manojmallick.github.io/sigmap/guide/config.html) · [CI setup](docs/readmes/ENTERPRISE_SETUP.md) |
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
- **[manojmallick.github.io/sigmap](https://manojmallick.github.io/sigmap)**
228
+ **[sigmap.io](https://sigmap.io)**
229
229
 
230
230
  | Section | Link |
231
231
  |---|---|
232
- | CLI reference (32 commands) | [cli.html](https://manojmallick.github.io/sigmap/guide/cli.html) |
233
- | Benchmark methodology | [benchmark.html](https://manojmallick.github.io/sigmap/guide/benchmark.html) |
234
- | Config reference | [config.html](https://manojmallick.github.io/sigmap/guide/config.html) |
235
- | Roadmap | [roadmap.html](https://manojmallick.github.io/sigmap/guide/roadmap.html) |
236
- | 31 languages | [generalization.html](https://manojmallick.github.io/sigmap/guide/generalization.html) |
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://manojmallick.github.io/sigmap/guide/generalization.html)
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://manojmallick.github.io/sigmap) · [Changelog](CHANGELOG.md) · [Roadmap](https://manojmallick.github.io/sigmap/roadmap.html) · [npm](https://www.npmjs.com/package/sigmap)**
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
@@ -236,12 +236,11 @@ __factories["./src/config/loader"] = function(module, exports) {
236
236
  }
237
237
  }
238
238
  try {
239
- const { execSync } = require('child_process');
240
- const proto = extendsVal.startsWith('https') ? 'https' : 'http';
241
- const out = execSync(
242
- `node -e "const h=require('${proto}');let d='';h.get(${JSON.stringify(extendsVal)},r=>{r.on('data',c=>d+=c);r.on('end',()=>process.stdout.write(d))}).on('error',()=>process.exit(1))"`,
243
- { timeout: 10000, encoding: 'utf8' }
244
- );
239
+ // Shell-free: run the node binary directly with the URL passed as an argv
240
+ // value (never interpolated into a command string, never via /bin/sh).
241
+ const { execFileSync } = require('child_process');
242
+ const SYNC_FETCH = "const u=process.argv[1];const h=require(u.startsWith('https')?'https':'http');let d='';h.get(u,r=>{r.on('data',c=>d+=c);r.on('end',()=>process.stdout.write(d))}).on('error',()=>process.exit(1))";
243
+ const out = execFileSync(process.execPath, ['-e', SYNC_FETCH, extendsVal], { timeout: 10000, encoding: 'utf8' });
245
244
  const parsed = JSON.parse(out);
246
245
  if (!fs.existsSync(cacheDir)) fs.mkdirSync(cacheDir, { recursive: true });
247
246
  fs.writeFileSync(cachePath, JSON.stringify(parsed), 'utf8');
@@ -5533,7 +5532,6 @@ __factories["./src/mcp/handlers"] = function(module, exports) {
5533
5532
 
5534
5533
  const fs = require('fs');
5535
5534
  const path = require('path');
5536
- const { execSync } = require('child_process');
5537
5535
 
5538
5536
  const CONTEXT_FILE = path.join('.github', 'copilot-instructions.md');
5539
5537
  const CONTEXT_COLD_FILE = path.join('.github', 'context-cold.md');
@@ -5680,19 +5678,14 @@ function createCheckpoint(args, cwd) {
5680
5678
  // ── Git info ────────────────────────────────────────────────────────────
5681
5679
  lines.push('## Git state');
5682
5680
  try {
5683
- const branch = execSync('git rev-parse --abbrev-ref HEAD', {
5684
- cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
5685
- }).trim();
5681
+ const branch = __git(['rev-parse', '--abbrev-ref', 'HEAD'], { cwd }).trim();
5686
5682
  lines.push(`**Branch:** ${branch}`);
5687
5683
  } catch (_) {
5688
5684
  lines.push('**Branch:** (not a git repo)');
5689
5685
  }
5690
5686
 
5691
5687
  try {
5692
- const log = execSync(
5693
- 'git log --oneline -5 --no-decorate 2>/dev/null',
5694
- { cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
5695
- ).trim();
5688
+ const log = __git(['log', '--oneline', '-5', '--no-decorate'], { cwd }).trim();
5696
5689
  if (log) {
5697
5690
  lines.push('');
5698
5691
  lines.push('**Recent commits:**');
@@ -6261,7 +6254,7 @@ const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, exp
6261
6254
 
6262
6255
  const SERVER_INFO = {
6263
6256
  name: 'sigmap',
6264
- version: '7.0.0',
6257
+ version: '7.1.0',
6265
6258
  description: 'SigMap MCP server — code signatures on demand',
6266
6259
  };
6267
6260
 
@@ -7152,8 +7145,6 @@ __factories["./src/session/notes"] = function(module, exports) {
7152
7145
 
7153
7146
  const fs = require('fs');
7154
7147
  const path = require('path');
7155
- const { execSync } = require('child_process');
7156
-
7157
7148
  const NOTES_FILE = path.join('.context', 'notes.ndjson');
7158
7149
  const MAX_TEXT = 2000;
7159
7150
 
@@ -7162,13 +7153,7 @@ function notesPath(cwd) {
7162
7153
  }
7163
7154
 
7164
7155
  function _currentBranch(cwd) {
7165
- try {
7166
- return execSync('git rev-parse --abbrev-ref HEAD', {
7167
- cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
7168
- }).trim() || null;
7169
- } catch (_) {
7170
- return null;
7171
- }
7156
+ return __tryGit(['rev-parse', '--abbrev-ref', 'HEAD'], { cwd }) || null;
7172
7157
  }
7173
7158
 
7174
7159
  /**
@@ -8703,10 +8688,9 @@ __factories["./src/format/llms-txt"] = function(module, exports) {
8703
8688
  'use strict';
8704
8689
  const path = require('path');
8705
8690
  const fs = require('fs');
8706
- const { execSync } = require('child_process');
8707
8691
  function outputPath(cwd) { return path.join(cwd, 'llms.txt'); }
8708
8692
  function getShortCommit(cwd) {
8709
- try { return execSync('git rev-parse --short HEAD', { cwd, timeout: 2000 }).toString().trim(); }
8693
+ try { return __tryGit(['rev-parse', '--short', 'HEAD'], { cwd, timeout: 2000 }); }
8710
8694
  catch (_) { return ''; }
8711
8695
  }
8712
8696
  function detectVersion(cwd) {
@@ -8944,6 +8928,20 @@ __factories["./src/discovery/source-root-registry"] = function(module, exports)
8944
8928
  module.exports = { REGISTRY };
8945
8929
  };
8946
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
+
8947
8945
  // ── ./src/discovery/sigmapignore ──
8948
8946
  __factories["./src/discovery/sigmapignore"] = function(module, exports) {
8949
8947
  'use strict';
@@ -9031,14 +9029,13 @@ __factories["./src/discovery/source-root-scorer"] = function(module, exports) {
9031
9029
  'use strict';
9032
9030
  const fs = require('fs');
9033
9031
  const path = require('path');
9034
- const { execSync } = require('child_process');
9035
9032
  const CODE_EXTS = new Set(['.js','.mjs','.cjs','.ts','.tsx','.jsx','.py','.rb','.go','.rs','.java','.kt','.cs','.cpp','.c','.h','.swift','.dart','.scala','.php']);
9036
9033
  const AUTO_SKIP = new Set(['node_modules','dist','build','.git','.next','.nuxt','vendor','DerivedData','Pods','target','coverage','__pycache__','.venv','venv','.build','Carthage','storybook-static','.gradle','bin','obj','.vs']);
9037
9034
  const PENALTY_DIRS = new Set(['test','tests','spec','__tests__','e2e','docs','doc','docs-vp','examples','example','fixtures','mocks','__mocks__','demo','samples','migrations','benchmarks','scripts']);
9038
9035
  const ROOT_ENTRYPOINTS = { go: ['main.go'], python: ['app.py','main.py','wsgi.py','asgi.py'], javascript: ['index.js','server.js','app.js'], typescript: ['index.ts','main.ts'], rust: [], php: ['index.php'] };
9039
9036
  function getRecentlyChangedDirs(cwd) {
9040
9037
  try {
9041
- const out = execSync('git log --name-only --format="" HEAD~10 2>/dev/null', { cwd, timeout: 3000 }).toString();
9038
+ const out = __git(['log', '--name-only', '--format=', 'HEAD~10'], { cwd, timeout: 3000 }).toString();
9042
9039
  return new Set(out.split('\n').filter(Boolean).map(f => f.split('/')[0]));
9043
9040
  } catch { return new Set(); }
9044
9041
  }
@@ -10027,8 +10024,6 @@ __factories["./src/nudge"] = function(module, exports) {
10027
10024
 
10028
10025
  const fs = require('fs');
10029
10026
  const path = require('path');
10030
- const os = require('os');
10031
- const crypto = require('crypto');
10032
10027
 
10033
10028
  const RUN_THRESHOLD = 10;
10034
10029
  const SUCCESS_THRESHOLD = 8;
@@ -10038,7 +10033,7 @@ function usagePath(cwd) { return path.join(cwd, '.context', 'usage.json'); }
10038
10033
  function defaultUsage() {
10039
10034
  return {
10040
10035
  totalRuns: 0, successfulRuns: 0, squeezeOffered: 0, squeezeAccepted: 0,
10041
- starNudgeShown: false, machineId: '', firstRunDate: null, lastRunDate: null,
10036
+ starNudgeShown: false, firstRunDate: null, lastRunDate: null,
10042
10037
  };
10043
10038
  }
10044
10039
 
@@ -10091,9 +10086,6 @@ function checkStarNudge(cwd, runSuccess, opts = {}) {
10091
10086
  const today = opts.today || new Date().toISOString().slice(0, 10);
10092
10087
  if (!usage.firstRunDate) usage.firstRunDate = today;
10093
10088
  usage.lastRunDate = today;
10094
- if (!usage.machineId) {
10095
- try { usage.machineId = 'sha256-' + crypto.createHash('sha256').update(os.hostname()).digest('hex').slice(0, 16); } catch (_) {}
10096
- }
10097
10089
 
10098
10090
  let nudged = false;
10099
10091
  if (!usage.starNudgeShown && usage.totalRuns >= RUN_THRESHOLD && usage.successfulRuns >= SUCCESS_THRESHOLD) {
@@ -10956,9 +10948,20 @@ module.exports = { verify, buildSymbolSet, loadDeps, loadScripts, isTestPath };
10956
10948
  const fs = require('fs');
10957
10949
  const path = require('path');
10958
10950
  const os = require('os');
10959
- const { execSync } = require('child_process');
10960
10951
 
10961
- const VERSION = '7.0.0';
10952
+ // Shell-free subprocess helpers. execFileSync runs the binary directly (never
10953
+ // /bin/sh), so there is no shell-injection surface and no "Shell access"
10954
+ // capability for supply-chain scanners. stderr is discarded by default.
10955
+ function __git(args, opts = {}) {
10956
+ const { execFileSync } = require('child_process');
10957
+ return execFileSync('git', args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], ...opts });
10958
+ }
10959
+ function __tryGit(args, opts = {}) {
10960
+ try { return __git(args, opts).toString().trim(); }
10961
+ catch (_) { return ''; }
10962
+ }
10963
+
10964
+ const VERSION = '7.1.0';
10962
10965
  const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
10963
10966
 
10964
10967
  function requireSourceOrBundled(key) {
@@ -11340,9 +11343,7 @@ function applyTokenBudget(fileEntries, maxTokens) {
11340
11343
  function getRecentlyCommittedFiles(cwd, count) {
11341
11344
  const n = count || 10;
11342
11345
  try {
11343
- const out = execSync(`git log --name-only --format="" -n ${n}`, {
11344
- cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
11345
- });
11346
+ const out = __git(['log', '--name-only', '--format=', '-n', String(n)], { cwd });
11346
11347
  return new Set(out.split('\n').map((f) => f.trim()).filter(Boolean).map((f) => path.resolve(cwd, f)));
11347
11348
  } catch (_) {
11348
11349
  return new Set();
@@ -11354,8 +11355,8 @@ function getRecentlyCommittedFiles(cwd, count) {
11354
11355
  // ---------------------------------------------------------------------------
11355
11356
  function getDiffFiles(cwd, stagedOnly) {
11356
11357
  try {
11357
- const cmd = stagedOnly ? 'git diff --cached --name-only' : 'git diff HEAD --name-only';
11358
- const out = execSync(cmd, { cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
11358
+ const gitArgs = stagedOnly ? ['diff', '--cached', '--name-only'] : ['diff', 'HEAD', '--name-only'];
11359
+ const out = __git(gitArgs, { cwd });
11359
11360
  return new Set(
11360
11361
  out.split('\n').map((f) => f.trim()).filter(Boolean).map((f) => path.resolve(cwd, f))
11361
11362
  );
@@ -11453,17 +11454,13 @@ function buildChangesSection(cwd, config, fileEntries) {
11453
11454
  try {
11454
11455
  let range = `HEAD~${n}..HEAD`;
11455
11456
  try {
11456
- execSync(`git rev-parse --verify HEAD~${n} 2>/dev/null`, {
11457
- cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
11458
- });
11457
+ __git(['rev-parse', '--verify', `HEAD~${n}`], { cwd });
11459
11458
  } catch (_) {
11460
11459
  // HEAD~n doesn't exist (shallow repo) — clamp to deepest valid ancestor
11461
11460
  let best = 1;
11462
11461
  for (let k = n - 1; k >= 2; k--) {
11463
11462
  try {
11464
- execSync(`git rev-parse --verify HEAD~${k} 2>/dev/null`, {
11465
- cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
11466
- });
11463
+ __git(['rev-parse', '--verify', `HEAD~${k}`], { cwd });
11467
11464
  best = k;
11468
11465
  break;
11469
11466
  } catch (_) {}
@@ -11471,15 +11468,11 @@ function buildChangesSection(cwd, config, fileEntries) {
11471
11468
  range = `HEAD~${best}..HEAD`;
11472
11469
  }
11473
11470
 
11474
- const namesOnly = execSync(`git diff ${range} --name-only 2>/dev/null`, {
11475
- cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
11476
- });
11471
+ const namesOnly = __git(['diff', range, '--name-only'], { cwd });
11477
11472
  const changed = new Set(namesOnly.split('\n').map((l) => l.trim()).filter(Boolean).map((f) => path.resolve(cwd, f)));
11478
11473
  if (changed.size === 0) return [];
11479
11474
 
11480
- const timeAgo = execSync('git log -1 --format="%cr" 2>/dev/null', {
11481
- cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
11482
- }).trim();
11475
+ const timeAgo = __git(['log', '-1', '--format=%cr'], { cwd }).trim();
11483
11476
 
11484
11477
  const lines = [`## changes (last ${n} commits — ${timeAgo})`, '```'];
11485
11478
  let hasDelta = false;
@@ -11488,9 +11481,7 @@ function buildChangesSection(cwd, config, fileEntries) {
11488
11481
  const rel = path.relative(cwd, entry.filePath);
11489
11482
  let diff = '';
11490
11483
  try {
11491
- diff = execSync(`git diff ${range} -- "${rel}" 2>/dev/null`, {
11492
- cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
11493
- });
11484
+ diff = __git(['diff', range, '--', rel], { cwd });
11494
11485
  } catch (_) {
11495
11486
  continue;
11496
11487
  }
@@ -12203,6 +12194,7 @@ function runDiff(cwd, config, stagedOnly, baseRef) {
12203
12194
  // Core generate pipeline
12204
12195
  // ---------------------------------------------------------------------------
12205
12196
  function runGenerate(cwd, config, reportMode, reportJson = false) {
12197
+ const __genT0 = Date.now();
12206
12198
  const ignorePatterns = loadIgnorePatterns(cwd);
12207
12199
  let allFiles = buildFileList(cwd, config);
12208
12200
 
@@ -12402,7 +12394,7 @@ function runGenerate(cwd, config, reportMode, reportJson = false) {
12402
12394
  printReport(result.inputTokenTotal, result.finalTokens, result.fileCount, result.droppedCount, reportJson, effectiveMaxTokens, result.coverageResult, config.autoMaxTokens !== false && effectiveMaxTokens !== config.maxTokens, config.maxTokens);
12403
12395
  }
12404
12396
 
12405
- // Usage tracking (v0.9) — optional append-only NDJSON log
12397
+ // Usage tracking (v0.9) — legacy health log, opt-in via config.tracking / --track.
12406
12398
  const trackingEnabled = !!(config.tracking || process.argv.includes('--track'));
12407
12399
  if (trackingEnabled && !reportMode) {
12408
12400
  try {
@@ -12422,6 +12414,22 @@ function runGenerate(cwd, config, reportMode, reportJson = false) {
12422
12414
  }
12423
12415
  }
12424
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
+
12425
12433
  // Feature 8: post-run summary — 6-line stdout after every normal run
12426
12434
  if (!reportMode
12427
12435
  && !process.argv.includes('--json')
@@ -12470,6 +12478,23 @@ function runGenerate(cwd, config, reportMode, reportJson = false) {
12470
12478
  }
12471
12479
  }
12472
12480
 
12481
+ // v7.0.1: count a plain context-generation run for the one-time star nudge.
12482
+ // Most users only ever run `sigmap` to build the context file — count that too,
12483
+ // not just ask/squeeze. Guard so a monorepo (many runGenerate calls per process)
12484
+ // counts as a single run. Interactive only — silent under --json/--report or non-TTY.
12485
+ if (!global.__sigmapGenCounted) {
12486
+ global.__sigmapGenCounted = true;
12487
+ try {
12488
+ const { checkStarNudge } = requireSourceOrBundled('./src/nudge');
12489
+ const showNudge = !!process.stderr.isTTY
12490
+ && !reportJson
12491
+ && !process.argv.includes('--json')
12492
+ && !process.argv.includes('--report')
12493
+ && !process.argv.includes('--quiet');
12494
+ checkStarNudge(global.__sigmapRootCwd || cwd, true, { silent: !showNudge });
12495
+ } catch (_) {}
12496
+ }
12497
+
12473
12498
  return result;
12474
12499
  }
12475
12500
 
@@ -12651,11 +12676,7 @@ function suggestTool(description) {
12651
12676
 
12652
12677
  function resolveProjectRoot(startDir) {
12653
12678
  try {
12654
- const gitRoot = execSync('git rev-parse --show-toplevel', {
12655
- cwd: startDir,
12656
- encoding: 'utf8',
12657
- stdio: ['ignore', 'pipe', 'ignore'],
12658
- }).trim();
12679
+ const gitRoot = __git(['rev-parse', '--show-toplevel'], { cwd: startDir }).trim();
12659
12680
  if (gitRoot) return gitRoot;
12660
12681
  } catch (_) {}
12661
12682
  return startDir;
@@ -12705,6 +12726,13 @@ Usage:
12705
12726
  ${cmd} --suggest-tool "<task>" --json Machine-readable tier recommendation
12706
12727
  ${cmd} --health Print composite health score
12707
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
12708
12736
  ${cmd} --diff Generate context for git-changed files only
12709
12737
  ${cmd} --diff <base-ref> Generate context + structural diff vs base ref (e.g. main)
12710
12738
  ${cmd} --diff --staged Generate context for staged files only
@@ -12926,8 +12954,7 @@ function buildIndexContext(ranked, cwd) {
12926
12954
 
12927
12955
  function computeCurrentRisk(cwd) {
12928
12956
  try {
12929
- const { execSync } = require('child_process');
12930
- const out = execSync('git diff --name-only HEAD', { cwd, timeout: 3000, encoding: 'utf8' });
12957
+ const out = __git(['diff', '--name-only', 'HEAD'], { cwd, timeout: 3000 });
12931
12958
  const count = out.trim().split('\n').filter(Boolean).length;
12932
12959
  if (count === 0) return 'NONE';
12933
12960
  if (count <= 3) return 'LOW';
@@ -13011,6 +13038,9 @@ function main() {
13011
13038
  const cwd = cwdFlag
13012
13039
  ? path.resolve(invokedFrom, cwdFlag)
13013
13040
  : resolveProjectRoot(invokedFrom);
13041
+ // v7.0.1: remember the invocation root so the generation star-nudge always tracks
13042
+ // usage at the repo root, even when runGenerate is called per-package (monorepo).
13043
+ global.__sigmapRootCwd = cwd;
13014
13044
  const scriptPath = process.argv[1] || path.join(invokedFrom, 'gen-context.js');
13015
13045
 
13016
13046
  if (cwdFlag) {
@@ -13102,6 +13132,7 @@ function main() {
13102
13132
 
13103
13133
  // v4.2: `sigmap ask "<query>"` — unified pipeline
13104
13134
  if (args[0] === 'ask') {
13135
+ const __askT0 = Date.now();
13105
13136
  // v6.8: Handle --followup flag which may appear before or after query
13106
13137
  let query = args[1];
13107
13138
  if (query === '--followup' && args[2]) {
@@ -13269,6 +13300,21 @@ function main() {
13269
13300
  bar,
13270
13301
  ].join('\n'));
13271
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
+
13272
13318
  // v7.0.0: record the run and show the one-time star nudge (interactive only).
13273
13319
  try {
13274
13320
  const { checkStarNudge } = requireSourceOrBundled('./src/nudge');
@@ -13281,14 +13327,51 @@ function main() {
13281
13327
  process.exit(0);
13282
13328
  }
13283
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
+
13284
13368
  // v4.2: `sigmap suggest-profile` — auto-detect task type from git state
13285
13369
  if (args[0] === 'suggest-profile') {
13286
13370
  const short = args.includes('--short');
13287
13371
  let msg = '', diff = '';
13288
13372
  try {
13289
- const { execSync } = require('child_process');
13290
- msg = execSync('git log -1 --format=%s', { cwd, timeout: 3000, encoding: 'utf8' }).trim();
13291
- diff = execSync('git diff --cached --name-only', { cwd, timeout: 3000, encoding: 'utf8' });
13373
+ msg = __git(['log', '-1', '--format=%s'], { cwd, timeout: 3000 }).trim();
13374
+ diff = __git(['diff', '--cached', '--name-only'], { cwd, timeout: 3000 });
13292
13375
  } catch (_) {}
13293
13376
 
13294
13377
  let profile = 'default';
@@ -13309,13 +13392,15 @@ function main() {
13309
13392
 
13310
13393
  // v4.2: `sigmap compare` — human-readable benchmark CLI
13311
13394
  if (args[0] === 'compare') {
13312
- const { execSync } = require('child_process');
13395
+ const { execFileSync } = require('child_process');
13313
13396
  console.log('[sigmap] Running comparison benchmark (this may take ~30s)...\n');
13314
13397
 
13315
13398
  let raw = '';
13316
13399
  try {
13317
- raw = execSync(
13318
- `node ${JSON.stringify(path.join(__dirname, 'scripts', 'run-retrieval-benchmark.mjs'))} --compare`,
13400
+ // Shell-free: run the node binary directly with the script path as an argv.
13401
+ raw = execFileSync(
13402
+ process.execPath,
13403
+ [path.join(__dirname, 'scripts', 'run-retrieval-benchmark.mjs'), '--compare'],
13319
13404
  { cwd, timeout: 90_000, encoding: 'utf8' }
13320
13405
  );
13321
13406
  } catch (e) { raw = (e && e.stdout) ? e.stdout : ''; }
@@ -13366,15 +13451,19 @@ function main() {
13366
13451
  const shareText = [
13367
13452
  'Generated with SigMap — zero-dependency AI context engine',
13368
13453
  `${reduction}% fewer tokens · ${hitAt5}% retrieval accuracy · 6× better results`,
13369
- 'https://sigmap.dev',
13454
+ 'https://sigmap.io',
13370
13455
  ].join('\n');
13371
13456
 
13372
13457
  console.log(shareText);
13373
13458
 
13374
13459
  try {
13375
- const { execSync } = require('child_process');
13376
- const clipCmd = process.platform === 'darwin' ? 'pbcopy' : 'xclip -selection clipboard';
13377
- execSync(`printf '%s' ${JSON.stringify(shareText)} | ${clipCmd}`, { timeout: 2000 });
13460
+ // Shell-free: spawn the clipboard binary directly and write the text to its
13461
+ // stdin no shell, no pipe, no string-built command.
13462
+ const { execFileSync } = require('child_process');
13463
+ const [clipBin, clipArgs] = process.platform === 'darwin'
13464
+ ? ['pbcopy', []]
13465
+ : ['xclip', ['-selection', 'clipboard']];
13466
+ execFileSync(clipBin, clipArgs, { input: shareText, timeout: 2000 });
13378
13467
  console.log('\n[sigmap] Copied to clipboard.');
13379
13468
  } catch (_) {}
13380
13469
 
@@ -13955,16 +14044,16 @@ function main() {
13955
14044
  // `sigmap status` — environment / repo state at a glance.
13956
14045
  if (args[0] === 'status') {
13957
14046
  const jsonOut = args.includes('--json');
13958
- const gitOpts = { cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] };
14047
+ const gitOpts = { cwd };
13959
14048
  const st = { branch: null, dirty: 0, lastIndex: null, indexVersion: null, indexFiles: null, changedSinceIndex: null, notes: 0, lastNote: null };
13960
14049
 
13961
- try { st.branch = execSync('git rev-parse --abbrev-ref HEAD', gitOpts).trim() || null; } catch (_) {}
14050
+ st.branch = __tryGit(['rev-parse', '--abbrev-ref', 'HEAD'], gitOpts) || null;
13962
14051
  // Fallback for an unborn branch (fresh repo, no commits yet).
13963
14052
  if (!st.branch || st.branch === 'HEAD') {
13964
- try { st.branch = execSync('git symbolic-ref --short HEAD', gitOpts).trim() || st.branch; } catch (_) {}
14053
+ st.branch = __tryGit(['symbolic-ref', '--short', 'HEAD'], gitOpts) || st.branch;
13965
14054
  }
13966
14055
  try {
13967
- const porcelain = execSync('git status --porcelain', gitOpts).trim();
14056
+ const porcelain = __git(['status', '--porcelain'], gitOpts).trim();
13968
14057
  st.dirty = porcelain ? porcelain.split('\n').filter(Boolean).length : 0;
13969
14058
  } catch (_) {}
13970
14059
 
@@ -13983,7 +14072,7 @@ function main() {
13983
14072
  if (st.lastIndex) {
13984
14073
  try {
13985
14074
  const since = Date.parse(st.lastIndex);
13986
- const tracked = execSync('git ls-files', gitOpts).split('\n').filter(Boolean);
14075
+ const tracked = __git(['ls-files'], gitOpts).split('\n').filter(Boolean);
13987
14076
  let changed = 0;
13988
14077
  for (const f of tracked.slice(0, 5000)) {
13989
14078
  try { if (fs.statSync(path.join(cwd, f)).mtimeMs > since) changed++; } catch (_) {}