sigmap 8.8.0 → 8.9.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,25 @@ Format: [Semantic Versioning](https://semver.org/)
10
10
 
11
11
  ---
12
12
 
13
+ ## [8.9.0] — 2026-07-06
14
+
15
+ Minor release — **the watcher, detached (D1).** `sigmap --watch` keeps the signature index fresh but held a terminal in the foreground. This adds a managed background daemon so you can start it once and forget it — the roadmap's #1 friction win. Zero-dependency, shell-free, deterministic.
16
+
17
+ ### Added
18
+ - **Detached watch daemon — `sigmap daemon start|stop|status` (#447, PR #448):** runs the existing `--watch` mode as a background process, launched with `spawn(process.execPath, ['gen-context.js', '--watch'], { detached: true })` — an arguments array, never a shell command string — and tracked by a PID file under `.context/` (`daemon.pid`; output → `.context/daemon.log`). `start` does an initial generate then detaches, is idempotent (a second start reports "already running", spawns no second process), and clears a stale PID file first; `stop` sends SIGTERM and removes the PID file (a no-op when nothing is running); `status` reports the running PID and log path, exits 0 when running / 1 when not, and self-cleans a stale PID file. Every subcommand supports `--json`. New module `src/daemon/daemon.js`; CLI dispatch mirrors `sigmap mcp <sub>` and is listed in `--help`.
19
+
20
+ ## [8.8.1] — 2026-07-05
21
+
22
+ Patch release — **byte-stable context, reproducible benchmark.** Closes the determinism residual tracked in #440: `gen-context` output is now byte-identical run-to-run across all 43 benchmark repos, and the retrieval benchmark reproduces a single hit@5 (87.8%) instead of flapping 85.6–87.8%. Plus a test-count derivation fix, docs/CI serving fixes, and repo hygiene.
23
+
24
+ ### Fixed
25
+ - **gen-context determinism residual — token-budget recency boost (#440, PR #444):** the recency boost stamped `mtime = Date.now()` on every recently-committed file. On repos where nearly every file is "recently changed", consecutive files often landed on the *same millisecond* — so which equal-priority files shared a millisecond (and thus fell through to the `filePath` tie-break instead of sorting by a distinct mtime) shifted run to run, swapping which files survived at the budget cutoff and making the output non-byte-stable. The `Date.now()` value wasn't just a boost: it encoded the alphabetical walk order that the budget's best-first sort relied on; the nondeterminism was only the millisecond *collisions*. Replaced it with a deterministic monotonic counter (`nextRecentMtime`) that reproduces the exact same processing-order ranking without collisions. Result: all 43 benchmark repos are byte-identical across two clean runs (excluding the `Updated:` timestamp), and retrieval hit@5 reproduces at **87.8%** with identical per-repo results. Adds an integration regression guard (`test/integration/gen-context-determinism.test.js`) that runs gen-context twice on a committed fixture and asserts byte-equality — verified to fail on the old `Date.now()` behaviour.
26
+ - **Derived test count after test relocation (503a6e5):** the test-count derivation globbed `tests/**/*.py`; after relocating `test_python_ast_extractor.py` into `test/` it matched neither pattern and dropped the count, failing `check:metrics` in CI. Now counts `test/**/test_*.py` (unittest-named), which matches the relocated test and excludes the fixture.
27
+ - **Docs / CI serving (b236e08):** serve the OG banner image and the Google Search Console verification file; run the Python extractor tests in CI.
28
+
29
+ ### Changed
30
+ - **Repo hygiene (a49e9c5):** dropped orphaned demo GIFs and a dead script; relocated a stray test from `tests/` into `test/`.
31
+
13
32
  ## [8.8.0] — 2026-07-05
14
33
 
15
34
  Minor release — **the squeeze engine, exposed mid-session (D6).** The always-on squeeze engine (`src/squeeze/`) that powers `sigmap squeeze` was reachable only from the CLI on pasted input. This release exposes it as an MCP tool an agent can call *mid-session*, and adds a named CLI entry point for compressing an agent/tool *response*. Zero-dependency, offline, deterministic — the last A+ ceiling item (Machine 9→10): the engine already shipped, this just exposes it.
package/README.md CHANGED
@@ -120,8 +120,8 @@ Ask → Rank → Context → Validate → Judge → Learn
120
120
 
121
121
  <!--SM:benchmarkBlock-->
122
122
  ```
123
- Benchmark : sigmap-v8.8-main (21 repositories, including R language)
124
- Date : 2026-07-05
123
+ Benchmark : sigmap-v8.9-main (21 repositories, including R language)
124
+ Date : 2026-07-06
125
125
 
126
126
  Hit@5 : 87.8% (baseline 13.6% — 6.5× lift)
127
127
  Token reduction: 97.0% (across 21 repos)
package/gen-context.js CHANGED
@@ -2592,6 +2592,134 @@ __factories["./src/create/orchestrate"] = function(module, exports) {
2592
2592
 
2593
2593
  };
2594
2594
 
2595
+ // ── ./src/daemon/daemon ──
2596
+ __factories["./src/daemon/daemon"] = function(module, exports) {
2597
+
2598
+ /**
2599
+ * Detached watch daemon (D1).
2600
+ *
2601
+ * Runs the existing `--watch` mode as a background process so the index stays
2602
+ * fresh without holding a terminal. State lives under `.context/` (consistent
2603
+ * with session/usage): `daemon.pid` records the watcher's PID, `daemon.log`
2604
+ * captures its output.
2605
+ *
2606
+ * Zero-dependency and shell-free: the watcher is launched with
2607
+ * `spawn(process.execPath, [gen-context.js, '--watch'], { detached: true })` —
2608
+ * an arguments array, never a shell command string.
2609
+ */
2610
+
2611
+ const fs = require('fs');
2612
+ const path = require('path');
2613
+ const { spawn } = require('child_process');
2614
+
2615
+ module.exports = { start, stop, status, pidFile, logFile, isAlive, readPid };
2616
+
2617
+ function daemonDir(cwd) {
2618
+ return path.join(cwd, '.context');
2619
+ }
2620
+
2621
+ function pidFile(cwd) {
2622
+ return path.join(daemonDir(cwd), 'daemon.pid');
2623
+ }
2624
+
2625
+ function logFile(cwd) {
2626
+ return path.join(daemonDir(cwd), 'daemon.log');
2627
+ }
2628
+
2629
+ /** True if a process with this PID exists (signal 0 probes without killing). */
2630
+ function isAlive(pid) {
2631
+ if (!Number.isInteger(pid) || pid <= 0) return false;
2632
+ try {
2633
+ process.kill(pid, 0);
2634
+ return true;
2635
+ } catch (err) {
2636
+ // EPERM = the process exists but is owned by another user — treat as alive.
2637
+ return err.code === 'EPERM';
2638
+ }
2639
+ }
2640
+
2641
+ /** Read the recorded PID, or null if the file is missing/unparseable. */
2642
+ function readPid(cwd) {
2643
+ try {
2644
+ const raw = fs.readFileSync(pidFile(cwd), 'utf8').trim();
2645
+ const pid = parseInt(raw, 10);
2646
+ return Number.isInteger(pid) && pid > 0 ? pid : null;
2647
+ } catch (_) {
2648
+ return null;
2649
+ }
2650
+ }
2651
+
2652
+ function removePidFile(cwd) {
2653
+ try {
2654
+ fs.unlinkSync(pidFile(cwd));
2655
+ } catch (_) {}
2656
+ }
2657
+
2658
+ /**
2659
+ * @returns {{ running: boolean, pid: number|null, pidFile: string, logFile: string }}
2660
+ */
2661
+ function status(cwd) {
2662
+ const pid = readPid(cwd);
2663
+ const running = pid != null && isAlive(pid);
2664
+ // A recorded-but-dead PID is stale — clean it up so the state stays truthful.
2665
+ if (pid != null && !running) removePidFile(cwd);
2666
+ return { running, pid: running ? pid : null, pidFile: pidFile(cwd), logFile: logFile(cwd) };
2667
+ }
2668
+
2669
+ /**
2670
+ * Launch a detached `--watch` process. Idempotent: if one is already running
2671
+ * this is a no-op that reports the existing PID.
2672
+ *
2673
+ * @param {string} cwd
2674
+ * @param {{ scriptPath: string }} opts - path to gen-context.js (the CLI entry)
2675
+ * @returns {{ status: 'started'|'already', pid: number, logFile: string }}
2676
+ */
2677
+ function start(cwd, opts = {}) {
2678
+ const scriptPath = opts.scriptPath;
2679
+ if (!scriptPath) throw new Error('daemon.start requires opts.scriptPath');
2680
+
2681
+ const current = status(cwd);
2682
+ if (current.running) {
2683
+ return { status: 'already', pid: current.pid, logFile: logFile(cwd) };
2684
+ }
2685
+
2686
+ fs.mkdirSync(daemonDir(cwd), { recursive: true });
2687
+ const out = fs.openSync(logFile(cwd), 'a');
2688
+ try {
2689
+ const child = spawn(process.execPath, [scriptPath, '--watch'], {
2690
+ cwd,
2691
+ detached: true,
2692
+ stdio: ['ignore', out, out],
2693
+ });
2694
+ child.unref();
2695
+ fs.writeFileSync(pidFile(cwd), String(child.pid) + '\n');
2696
+ return { status: 'started', pid: child.pid, logFile: logFile(cwd) };
2697
+ } finally {
2698
+ try { fs.closeSync(out); } catch (_) {}
2699
+ }
2700
+ }
2701
+
2702
+ /**
2703
+ * Stop the running watcher (SIGTERM) and clear its PID file.
2704
+ *
2705
+ * @returns {{ status: 'stopped'|'not-running'|'stale', pid?: number }}
2706
+ */
2707
+ function stop(cwd) {
2708
+ const pid = readPid(cwd);
2709
+ if (pid == null) return { status: 'not-running' };
2710
+ if (!isAlive(pid)) {
2711
+ removePidFile(cwd);
2712
+ return { status: 'stale', pid };
2713
+ }
2714
+ try {
2715
+ process.kill(pid, 'SIGTERM');
2716
+ } catch (_) {}
2717
+ removePidFile(cwd);
2718
+ return { status: 'stopped', pid };
2719
+ }
2720
+
2721
+ };
2722
+
2595
2723
  // ── ./src/discovery/framework-detector ──
2596
2724
  __factories["./src/discovery/framework-detector"] = function(module, exports) {
2597
2725
 
@@ -13566,7 +13694,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
13566
13694
 
13567
13695
  const SERVER_INFO = {
13568
13696
  name: 'sigmap',
13569
- version: '8.8.0',
13697
+ version: '8.9.0',
13570
13698
  description: 'SigMap MCP server — code signatures on demand',
13571
13699
  };
13572
13700
 
@@ -18158,7 +18286,7 @@ function __tryGit(args, opts = {}) {
18158
18286
  catch (_) { return ''; }
18159
18287
  }
18160
18288
 
18161
- const VERSION = '8.8.0';
18289
+ const VERSION = '8.9.0';
18162
18290
  const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
18163
18291
 
18164
18292
  function requireSourceOrBundled(key) {
@@ -18368,6 +18496,26 @@ function annotateCoverage(sigs, testIndex, enabled) {
18368
18496
  // ---------------------------------------------------------------------------
18369
18497
  // Token budget enforcement
18370
18498
  // ---------------------------------------------------------------------------
18499
+ // Deterministic recency boost for recently-committed files so the token budget
18500
+ // doesn't drop them first. Previously this used `Date.now()`, which encoded the
18501
+ // (alphabetical) processing order as a monotonically increasing mtime — but on
18502
+ // repos where nearly every file is "recently changed", consecutive files often
18503
+ // landed on the *same* millisecond. Which files shared a millisecond (and so
18504
+ // fell through to the filePath tie-break instead of sorting by a distinct
18505
+ // mtime) shifted run to run, swapping which files survived at the budget cutoff
18506
+ // and making the output non-byte-stable. A monotonic integer counter preserves
18507
+ // the exact processing-order ranking Date.now() produced — recent files stay
18508
+ // ahead of non-recent ones, ordered among themselves by walk order — but is
18509
+ // collision-free and reproducible. RECENT_MTIME_BASE sits far above any real
18510
+ // filesystem mtime (ms since epoch ≈ 1.7e12) so boosted files always out-rank
18511
+ // non-boosted ones, and far below MAX_SAFE_INTEGER so the counter never
18512
+ // overflows. Call nextRecentMtime() once per boosted file, in walk order. (#440)
18513
+ const RECENT_MTIME_BASE = 1e15;
18514
+ let _recentMtimeSeq = 0;
18515
+ function nextRecentMtime() {
18516
+ return RECENT_MTIME_BASE + (_recentMtimeSeq++);
18517
+ }
18518
+
18371
18519
  function estimateTokens(str) {
18372
18520
  return Math.ceil(str.length / 4);
18373
18521
  }
@@ -19348,7 +19496,7 @@ function runDiff(cwd, config, stagedOnly, baseRef) {
19348
19496
 
19349
19497
  sigs = annotateCoverage(sigs, testIndex, !!config.testCoverage);
19350
19498
 
19351
- fileEntries.push({ filePath, sigs, deps: extractFileDeps(filePath, content, config), content, mtime: Date.now() });
19499
+ fileEntries.push({ filePath, sigs, deps: extractFileDeps(filePath, content, config), content, mtime: nextRecentMtime() });
19352
19500
  }
19353
19501
 
19354
19502
  if (fileEntries.length === 0) {
@@ -19496,8 +19644,11 @@ function runGenerate(cwd, config, reportMode, reportJson = false) {
19496
19644
  mtime = fs.statSync(filePath).mtimeMs;
19497
19645
  } catch (_) {}
19498
19646
 
19499
- // Boost recently committed files (give them max mtime so they aren't dropped first)
19500
- if (recentFiles.has(filePath)) mtime = Date.now();
19647
+ // Boost recently committed files (give them a max mtime so they aren't
19648
+ // dropped first). A deterministic monotonic counter — not Date.now() — keeps
19649
+ // the budget selection byte-stable when many files are recent, while
19650
+ // preserving the original processing-order ranking (see nextRecentMtime, #440).
19651
+ if (recentFiles.has(filePath)) mtime = nextRecentMtime();
19501
19652
 
19502
19653
  fileEntries.push({ filePath, sigs, deps: extractFileDeps(filePath, content, config), content, mtime });
19503
19654
  }
@@ -19939,6 +20090,7 @@ Usage:
19939
20090
  ${cmd} --track Append run metrics to .context/usage.ndjson
19940
20091
  ${cmd} --watch Generate + watch for file changes
19941
20092
  ${cmd} --setup Generate + install git hook + watch
20093
+ ${cmd} daemon start|stop|status Run --watch as a detached background daemon
19942
20094
  ${cmd} --mcp Start MCP server on stdio
19943
20095
  ${cmd} --report Token reduction stats to stdout
19944
20096
  ${cmd} --report --json Token report as JSON (for CI; exits 1 if over budget)
@@ -21480,6 +21632,46 @@ function main() {
21480
21632
  process.exit(1);
21481
21633
  }
21482
21634
 
21635
+ // `sigmap daemon start|stop|status` — run `--watch` as a detached background
21636
+ // process (D1). PID + log live under .context/. Mirrors the `mcp` sub-dispatch.
21637
+ if (args[0] === 'daemon') {
21638
+ const daemon = requireSourceOrBundled('./src/daemon/daemon');
21639
+ const sub = args[1];
21640
+ const jsonOut = args.includes('--json');
21641
+
21642
+ if (sub === 'start') {
21643
+ const r = daemon.start(cwd, { scriptPath });
21644
+ if (jsonOut) { process.stdout.write(JSON.stringify(r) + '\n'); process.exit(0); }
21645
+ if (r.status === 'already') {
21646
+ console.log(`[sigmap] daemon already running (pid ${r.pid})`);
21647
+ } else {
21648
+ console.log(`[sigmap] daemon started (pid ${r.pid}) — watching for changes`);
21649
+ console.log(`[sigmap] logs: ${_displayPath(r.logFile, cwd)} stop with: sigmap daemon stop`);
21650
+ }
21651
+ process.exit(0);
21652
+ }
21653
+
21654
+ if (sub === 'stop') {
21655
+ const r = daemon.stop(cwd);
21656
+ if (jsonOut) { process.stdout.write(JSON.stringify(r) + '\n'); process.exit(0); }
21657
+ if (r.status === 'not-running') console.log('[sigmap] daemon not running');
21658
+ else if (r.status === 'stale') console.log(`[sigmap] removed stale pid file (pid ${r.pid} no longer alive)`);
21659
+ else console.log(`[sigmap] daemon stopped (pid ${r.pid})`);
21660
+ process.exit(0);
21661
+ }
21662
+
21663
+ if (sub === 'status') {
21664
+ const r = daemon.status(cwd);
21665
+ if (jsonOut) { process.stdout.write(JSON.stringify(r) + '\n'); process.exit(r.running ? 0 : 1); }
21666
+ if (r.running) console.log(`[sigmap] daemon running (pid ${r.pid}) — logs: ${_displayPath(r.logFile, cwd)}`);
21667
+ else console.log('[sigmap] daemon not running');
21668
+ process.exit(r.running ? 0 : 1);
21669
+ }
21670
+
21671
+ console.error('[sigmap] usage: sigmap daemon start | stop | status');
21672
+ process.exit(1);
21673
+ }
21674
+
21483
21675
  // `sigmap doctor` — diagnose config, index, freshness, coverage, and MCP
21484
21676
  // wiring; print an actionable fix for anything wrong. Exit 1 on a hard
21485
21677
  // failure (no context file / invalid config) so it is usable in CI.
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.8.0 | Benchmark: sigmap-v8.8-main (2026-07-05)
14
+ # Version: 8.9.0 | Benchmark: sigmap-v8.9-main (2026-07-06)
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.8-main, 2026-07-05)
20
+ ## Core metrics (benchmark: sigmap-v8.9-main, 2026-07-06)
21
21
 
22
22
  | Metric | Without SigMap | With SigMap |
23
23
  |--------|----------------|-------------|
@@ -59,6 +59,7 @@ sigmap --format cache Also write Anthropic prompt-cache JSON
59
59
  sigmap --track Append run metrics to .context/usage.ndjson
60
60
  sigmap --watch Generate + watch for file changes
61
61
  sigmap --setup Generate + install git hook + watch
62
+ sigmap daemon start|stop|status Run --watch as a detached background daemon
62
63
  sigmap --mcp Start MCP server on stdio
63
64
  sigmap --report Token reduction stats to stdout
64
65
  sigmap --report --json Token report as JSON (for CI; exits 1 if over budget)
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.8.0 | Benchmark: sigmap-v8.8-main (2026-07-05)
14
+ # Version: 8.9.0 | Benchmark: sigmap-v8.9-main (2026-07-06)
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,7 +23,7 @@ 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.8-main, 2026-07-05)
26
+ ## Core metrics (benchmark: sigmap-v8.9-main, 2026-07-06)
27
27
 
28
28
  - hit@5 retrieval: 87.8% vs 13.6% random baseline (6.5× lift)
29
29
  - Token reduction: 97.0% average across benchmark repos
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap",
3
- "version": "8.8.0",
3
+ "version": "8.9.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": {
@@ -17,6 +17,7 @@
17
17
  "test": "node test/run.js && node test/r-language.test.js",
18
18
  "test:integration": "node test/integration/strategy.test.js && node test/integration/secret-scan.test.js && node test/integration/token-budget.test.js && node test/integration/auto-budget.test.js && node test/integration/mcp/server.test.js && node test/integration/verify-ai-output.test.js && node test/integration/memory-tools.test.js && node test/integration/squeeze.test.js && node test/integration/context-consistency.test.js && node test/integration/features/llms-current.test.js",
19
19
  "test:integration:all": "node test/integration/all.js",
20
+ "test:python": "python3 test/test_python_ast_extractor.py",
20
21
  "test:all": "node test/run.js && node test/r-language.test.js && node test/integration/strategy.test.js && node test/integration/secret-scan.test.js",
21
22
  "generate": "node gen-context.js",
22
23
  "watch": "node gen-context.js --watch",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap-cli",
3
- "version": "8.8.0",
3
+ "version": "8.9.0",
4
4
  "description": "SigMap CLI wrapper — thin adapter for programmatic CLI invocation",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap-core",
3
- "version": "8.8.0",
3
+ "version": "8.9.0",
4
4
  "description": "SigMap core library — zero-dependency code signature extraction, retrieval, and security scanning",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -0,0 +1,124 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Detached watch daemon (D1).
5
+ *
6
+ * Runs the existing `--watch` mode as a background process so the index stays
7
+ * fresh without holding a terminal. State lives under `.context/` (consistent
8
+ * with session/usage): `daemon.pid` records the watcher's PID, `daemon.log`
9
+ * captures its output.
10
+ *
11
+ * Zero-dependency and shell-free: the watcher is launched with
12
+ * `spawn(process.execPath, [gen-context.js, '--watch'], { detached: true })` —
13
+ * an arguments array, never a shell command string.
14
+ */
15
+
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+ const { spawn } = require('child_process');
19
+
20
+ module.exports = { start, stop, status, pidFile, logFile, isAlive, readPid };
21
+
22
+ function daemonDir(cwd) {
23
+ return path.join(cwd, '.context');
24
+ }
25
+
26
+ function pidFile(cwd) {
27
+ return path.join(daemonDir(cwd), 'daemon.pid');
28
+ }
29
+
30
+ function logFile(cwd) {
31
+ return path.join(daemonDir(cwd), 'daemon.log');
32
+ }
33
+
34
+ /** True if a process with this PID exists (signal 0 probes without killing). */
35
+ function isAlive(pid) {
36
+ if (!Number.isInteger(pid) || pid <= 0) return false;
37
+ try {
38
+ process.kill(pid, 0);
39
+ return true;
40
+ } catch (err) {
41
+ // EPERM = the process exists but is owned by another user — treat as alive.
42
+ return err.code === 'EPERM';
43
+ }
44
+ }
45
+
46
+ /** Read the recorded PID, or null if the file is missing/unparseable. */
47
+ function readPid(cwd) {
48
+ try {
49
+ const raw = fs.readFileSync(pidFile(cwd), 'utf8').trim();
50
+ const pid = parseInt(raw, 10);
51
+ return Number.isInteger(pid) && pid > 0 ? pid : null;
52
+ } catch (_) {
53
+ return null;
54
+ }
55
+ }
56
+
57
+ function removePidFile(cwd) {
58
+ try {
59
+ fs.unlinkSync(pidFile(cwd));
60
+ } catch (_) {}
61
+ }
62
+
63
+ /**
64
+ * @returns {{ running: boolean, pid: number|null, pidFile: string, logFile: string }}
65
+ */
66
+ function status(cwd) {
67
+ const pid = readPid(cwd);
68
+ const running = pid != null && isAlive(pid);
69
+ // A recorded-but-dead PID is stale — clean it up so the state stays truthful.
70
+ if (pid != null && !running) removePidFile(cwd);
71
+ return { running, pid: running ? pid : null, pidFile: pidFile(cwd), logFile: logFile(cwd) };
72
+ }
73
+
74
+ /**
75
+ * Launch a detached `--watch` process. Idempotent: if one is already running
76
+ * this is a no-op that reports the existing PID.
77
+ *
78
+ * @param {string} cwd
79
+ * @param {{ scriptPath: string }} opts - path to gen-context.js (the CLI entry)
80
+ * @returns {{ status: 'started'|'already', pid: number, logFile: string }}
81
+ */
82
+ function start(cwd, opts = {}) {
83
+ const scriptPath = opts.scriptPath;
84
+ if (!scriptPath) throw new Error('daemon.start requires opts.scriptPath');
85
+
86
+ const current = status(cwd);
87
+ if (current.running) {
88
+ return { status: 'already', pid: current.pid, logFile: logFile(cwd) };
89
+ }
90
+
91
+ fs.mkdirSync(daemonDir(cwd), { recursive: true });
92
+ const out = fs.openSync(logFile(cwd), 'a');
93
+ try {
94
+ const child = spawn(process.execPath, [scriptPath, '--watch'], {
95
+ cwd,
96
+ detached: true,
97
+ stdio: ['ignore', out, out],
98
+ });
99
+ child.unref();
100
+ fs.writeFileSync(pidFile(cwd), String(child.pid) + '\n');
101
+ return { status: 'started', pid: child.pid, logFile: logFile(cwd) };
102
+ } finally {
103
+ try { fs.closeSync(out); } catch (_) {}
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Stop the running watcher (SIGTERM) and clear its PID file.
109
+ *
110
+ * @returns {{ status: 'stopped'|'not-running'|'stale', pid?: number }}
111
+ */
112
+ function stop(cwd) {
113
+ const pid = readPid(cwd);
114
+ if (pid == null) return { status: 'not-running' };
115
+ if (!isAlive(pid)) {
116
+ removePidFile(cwd);
117
+ return { status: 'stale', pid };
118
+ }
119
+ try {
120
+ process.kill(pid, 'SIGTERM');
121
+ } catch (_) {}
122
+ removePidFile(cwd);
123
+ return { status: 'stopped', pid };
124
+ }
package/src/mcp/server.js CHANGED
@@ -18,7 +18,7 @@ const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, exp
18
18
 
19
19
  const SERVER_INFO = {
20
20
  name: 'sigmap',
21
- version: '8.8.0',
21
+ version: '8.9.0',
22
22
  description: 'SigMap MCP server — code signatures on demand',
23
23
  };
24
24