sigmap 4.1.0 → 4.1.2

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,91 @@ Format: [Semantic Versioning](https://semver.org/)
10
10
 
11
11
  ---
12
12
 
13
+ ## [4.1.2] — 2026-04-16 — Feat: --output <file> flag for custom context path
14
+
15
+ ### Added
16
+
17
+ - **`--output <file>` flag** — write signatures to any custom path, not just
18
+ an adapter's fixed location:
19
+ ```bash
20
+ sigmap --output .context/ai-context.md # default generation
21
+ sigmap --adapter claude --output shared/sigs.md # adapter + custom path
22
+ ```
23
+ The custom file is written **in addition to** the adapter's default output so
24
+ existing tooling is unaffected.
25
+
26
+ - **Automatic discovery for `--query`** — the resolved path is persisted to
27
+ `gen-context.config.json` as `customOutput` so subsequent `--query` runs
28
+ find it automatically without needing to pass `--output` again:
29
+ ```bash
30
+ sigmap --output .context/ai-context.md # generates + persists path
31
+ sigmap --query "add a new extractor" # auto-finds .context/ai-context.md
32
+ ```
33
+
34
+ - **Priority order for `--query` context resolution** (most specific first):
35
+ 1. `--output <file>` flag — explicit path
36
+ 2. `--adapter <name>` flag — adapter's fixed output path
37
+ 3. `customOutput` in `gen-context.config.json` — persisted from last `--output` run
38
+ 4. Probe all known adapter output paths — existing fallback behaviour
39
+
40
+ - **Nested directories created automatically** — `--output a/b/c/file.md`
41
+ creates any missing parent directories.
42
+
43
+ ### Tests
44
+
45
+ - Added `test/integration/output-flag.test.js` (13 tests) covering: custom
46
+ file creation, parseable headers, config persistence, nested dirs, missing
47
+ arg error, `--adapter` + `--output` combo, explicit `--query` with `--output`,
48
+ auto-discovery via persisted config, missing-file error, `--output` overrides
49
+ `--adapter` during `--query`.
50
+
51
+ ---
52
+
53
+ ## [4.1.1] — 2026-04-16 — Fix: --query works with any adapter output
54
+
55
+ ### Fixed
56
+
57
+ - **`--query` fails after `--adapter` generation** (`[sigmap] no context file found`):
58
+ `buildSigIndex` hardcoded `.github/copilot-instructions.md` as the only
59
+ context file path, so `--query` always failed when any adapter other than
60
+ `copilot` wrote to a different location (`CLAUDE.md`, `AGENTS.md`,
61
+ `.cursorrules`, `.windsurfrules`, etc.).
62
+
63
+ `buildSigIndex` now probes all nine known adapter output paths in priority
64
+ order and returns the first non-empty index:
65
+ ```
66
+ copilot → claude → codex → cursor → windsurf → openai → gemini → llm-full → llm
67
+ ```
68
+ Human-written preamble before the `## Auto-generated signatures` marker
69
+ (e.g. custom content in `CLAUDE.md`) is skipped so those `###` sections
70
+ don't pollute the signature index.
71
+
72
+ - **`--adapter <name> --query "..."` combination ignored the adapter flag**:
73
+ The `--query` handler now detects a co-present `--adapter` flag, resolves
74
+ that adapter's output path, and reads from it directly — so both forms work:
75
+ ```bash
76
+ # generate with claude adapter, then query without re-specifying adapter
77
+ node gen-context.js --adapter claude
78
+ node gen-context.js --query "add a new extractor"
79
+
80
+ # or pin explicitly in one command
81
+ node gen-context.js --adapter claude --query "add a new extractor"
82
+ ```
83
+
84
+ - **`--analyze --json` output truncated at ~8 KB on macOS**:
85
+ Calling `process.exit(0)` immediately after `process.stdout.write(largeJson)`
86
+ truncated output because the underlying pipe write is asynchronous even
87
+ when `write()` returns `true`. Fixed by using the write callback so the
88
+ process exits only after the OS has accepted all bytes.
89
+
90
+ ### Tests
91
+
92
+ - Added `test/integration/query-adapter.test.js` (17 tests) covering every
93
+ adapter output path (unit + CLI), probe order, marker-skipping, explicit
94
+ `opts.contextPath` override, and empty-project fallback.
95
+
96
+ ---
97
+
13
98
  ## [4.1.0] — 2026-04-15 — Smart Budget: auto-scaling token budget
14
99
 
15
100
  ### Added
package/README.md CHANGED
@@ -353,6 +353,24 @@ Configure multiple adapters at once in `gen-context.config.json`:
353
353
 
354
354
  Use SigMap as a Node.js library without spawning a subprocess. See the [full API reference](#-programmatic-api) below.
355
355
 
356
+ ### Custom output path
357
+
358
+ Write signatures to any file location — useful for shared docs folders, monorepos,
359
+ or tooling that expects context at a non-standard path:
360
+
361
+ ```bash
362
+ sigmap --output .context/ai-context.md # write to custom path
363
+ sigmap --adapter claude --output shared/sigs.md # adapter + custom path
364
+ ```
365
+
366
+ The path is persisted to `gen-context.config.json`, so `--query` finds it
367
+ automatically on subsequent runs — no need to pass `--output` again:
368
+
369
+ ```bash
370
+ sigmap --output .context/ai-context.md # generates and saves the path
371
+ sigmap --query "add an extractor" # auto-discovers .context/ai-context.md
372
+ ```
373
+
356
374
  ### Query-aware retrieval
357
375
 
358
376
  Find the most relevant files for any task without reading the whole codebase:
@@ -361,6 +379,7 @@ Find the most relevant files for any task without reading the whole codebase:
361
379
  sigmap --query "authentication middleware" # ranked file list
362
380
  sigmap --query "auth" --json # machine-readable output
363
381
  sigmap --query "auth" --top 5 # top 5 results only
382
+ sigmap --query "auth" --adapter claude # query against CLAUDE.md specifically
364
383
  ```
365
384
 
366
385
  ### Diagnostic and evaluation tools
@@ -616,9 +635,14 @@ sigmap --diff Generate context for git-changed f
616
635
  sigmap --diff --staged Staged files only (pre-commit check)
617
636
  sigmap --mcp Start MCP server on stdio
618
637
 
638
+ sigmap --output <file> Write signatures to a custom path (persists for --query)
639
+ sigmap --output <file> --adapter <name> Adapter output + custom copy
640
+
619
641
  sigmap --query "<text>" Rank files by relevance to a query
620
642
  sigmap --query "<text>" --json Ranked results as JSON
621
643
  sigmap --query "<text>" --top <n> Limit results to top N files (default 10)
644
+ sigmap --query "<text>" --adapter <name> Query against a specific adapter's output file
645
+ sigmap --query "<text>" --output <file> Query against a specific custom file
622
646
 
623
647
  sigmap --analyze Per-file breakdown (sigs / tokens / extractor / coverage)
624
648
  sigmap --analyze --json Analysis as JSON
package/gen-context.js CHANGED
@@ -5449,12 +5449,24 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
5449
5449
  scored.sort((a, b) => b.score - a.score || a.file.localeCompare(b.file));
5450
5450
  return scored.slice(0, topK);
5451
5451
  }
5452
- function buildSigIndex(cwd) {
5453
- const fs = require('fs'); const path = require('path');
5454
- const contextPath = path.join(cwd, '.github', 'copilot-instructions.md');
5452
+ const ADAPTER_OUTPUT_PATHS = [
5453
+ ['.github', 'copilot-instructions.md'],
5454
+ ['CLAUDE.md'],
5455
+ ['AGENTS.md'],
5456
+ ['.cursorrules'],
5457
+ ['.windsurfrules'],
5458
+ ['.github', 'openai-context.md'],
5459
+ ['.github', 'gemini-context.md'],
5460
+ ['llm-full.txt'],
5461
+ ['llm.txt'],
5462
+ ];
5463
+ function _parseContextFile(contextPath) {
5464
+ const fs = require('fs');
5455
5465
  const index = new Map();
5456
5466
  if (!fs.existsSync(contextPath)) return index;
5457
- const content = fs.readFileSync(contextPath, 'utf8');
5467
+ let content = fs.readFileSync(contextPath, 'utf8');
5468
+ const markerIdx = content.indexOf('## Auto-generated signatures');
5469
+ if (markerIdx !== -1) content = content.slice(markerIdx);
5458
5470
  const lines = content.split('\n');
5459
5471
  let currentFile = null; let inBlock = false; let sigs = [];
5460
5472
  for (const line of lines) {
@@ -5466,6 +5478,27 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
5466
5478
  if (currentFile !== null) index.set(currentFile, sigs);
5467
5479
  return index;
5468
5480
  }
5481
+ function buildSigIndex(cwd, opts) {
5482
+ const fs = require('fs'); const path = require('path');
5483
+ if (opts && opts.contextPath) return _parseContextFile(opts.contextPath);
5484
+ // Check gen-context.config.json for a persisted customOutput path.
5485
+ try {
5486
+ const cfgPath = path.join(cwd, 'gen-context.config.json');
5487
+ if (fs.existsSync(cfgPath)) {
5488
+ const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
5489
+ if (cfg.customOutput) {
5490
+ const idx = _parseContextFile(path.resolve(cwd, cfg.customOutput));
5491
+ if (idx.size > 0) return idx;
5492
+ }
5493
+ }
5494
+ } catch (_) {}
5495
+ for (const parts of ADAPTER_OUTPUT_PATHS) {
5496
+ const contextPath = path.join(cwd, ...parts);
5497
+ const index = _parseContextFile(contextPath);
5498
+ if (index.size > 0) return index;
5499
+ }
5500
+ return new Map();
5501
+ }
5469
5502
  function formatRankTable(results, query) {
5470
5503
  if (!results || results.length === 0) return `No matching files found for query: "${query}"\n`;
5471
5504
  const lines = [`## Query: ${query}`, '', '| Rank | File | Score | Sigs | Tokens |', '|------|------|-------|------|--------|',
@@ -6216,7 +6249,7 @@ const path = require('path');
6216
6249
  const os = require('os');
6217
6250
  const { execSync } = require('child_process');
6218
6251
 
6219
- const VERSION = '4.1.0';
6252
+ const VERSION = '4.1.2';
6220
6253
  const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
6221
6254
 
6222
6255
  function requireSourceOrBundled(key) {
@@ -6904,6 +6937,19 @@ function writeOutputs(content, targets, cwd, config) {
6904
6937
  if (ADAPTER_TARGETS.has(target)) {
6905
6938
  try {
6906
6939
  const adapterMod = __require('./packages/adapters/' + target);
6940
+ // copilot: honour config.output custom path (redirects away from default .github/copilot-instructions.md)
6941
+ if (target === 'copilot') {
6942
+ const outPath = resolveAdapterPath('copilot', cwd, config);
6943
+ const defaultPath = path.join(cwd, '.github', 'copilot-instructions.md');
6944
+ if (outPath !== defaultPath) {
6945
+ // custom path: format and write directly (no append logic)
6946
+ const formatted = adapterMod.format(content, { version: VERSION });
6947
+ ensureDir(outPath);
6948
+ fs.writeFileSync(outPath, formatted, 'utf8');
6949
+ console.warn(`[sigmap] wrote ${path.relative(cwd, outPath)}`);
6950
+ continue;
6951
+ }
6952
+ }
6907
6953
  if (typeof adapterMod.write === 'function') {
6908
6954
  adapterMod.write(content, cwd, { version: VERSION });
6909
6955
  const outPath = adapterMod.outputPath(cwd);
@@ -7485,6 +7531,25 @@ function runGenerate(cwd, config, reportMode, reportJson = false) {
7485
7531
  writeOutputs(content, config.outputs, cwd, config);
7486
7532
  }
7487
7533
  if (formatValue === 'cache') writeCacheOutput(content, cwd);
7534
+
7535
+ // --output <file>: write a copy of the formatted context to the custom path.
7536
+ if (config.customOutput) {
7537
+ try {
7538
+ const absCustom = path.resolve(cwd, config.customOutput);
7539
+ const SIGMAP_HEADER = [
7540
+ `<!-- Generated by SigMap v${VERSION} -->`,
7541
+ `<!-- Updated: ${new Date().toISOString()} -->`,
7542
+ `<!-- Regenerate: node gen-context.js --output ${config.customOutput} -->`,
7543
+ '',
7544
+ ].join('\n');
7545
+ fs.mkdirSync(path.dirname(absCustom), { recursive: true });
7546
+ fs.writeFileSync(absCustom, SIGMAP_HEADER + content, 'utf8');
7547
+ console.warn(`[sigmap] wrote ${path.relative(cwd, absCustom)} (custom output)`);
7548
+ } catch (err) {
7549
+ console.warn(`[sigmap] --output write failed: ${err.message}`);
7550
+ }
7551
+ }
7552
+
7488
7553
  result = { inputTokenTotal, finalTokens, fileCount: beforeCount, droppedCount };
7489
7554
  }
7490
7555
  } else {
@@ -7909,6 +7974,39 @@ function main() {
7909
7974
 
7910
7975
  const config = loadConfig(cwd);
7911
7976
 
7977
+ // ── --output <file> — parse early so every subsequent block can use it ─────
7978
+ // Resolves the custom output path and merges it into config.customOutput.
7979
+ // Also persists the resolved relative path to gen-context.config.json so
7980
+ // future --query calls (without --output) find the file automatically.
7981
+ (function resolveOutputFlag() {
7982
+ const outIdx = args.indexOf('--output');
7983
+ if (outIdx < 0) return;
7984
+ const raw = (args[outIdx + 1] || '').trim();
7985
+ if (!raw || raw.startsWith('--')) {
7986
+ console.error('[sigmap] --output requires a file path');
7987
+ console.error(' Example: node gen-context.js --output .context/ai-context.md');
7988
+ process.exit(1);
7989
+ }
7990
+ const abs = path.resolve(cwd, raw);
7991
+ const rel = path.relative(cwd, abs);
7992
+ config.customOutput = rel; // consumed by runGenerate
7993
+
7994
+ // Persist to gen-context.config.json for future --query calls
7995
+ const cfgPath = path.join(cwd, 'gen-context.config.json');
7996
+ try {
7997
+ let savedCfg = {};
7998
+ if (fs.existsSync(cfgPath)) {
7999
+ try { savedCfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8')); } catch (_) {}
8000
+ }
8001
+ if (savedCfg.customOutput !== rel) {
8002
+ savedCfg.customOutput = rel;
8003
+ fs.writeFileSync(cfgPath, JSON.stringify(savedCfg, null, 2) + '\n', 'utf8');
8004
+ }
8005
+ } catch (err) {
8006
+ console.warn(`[sigmap] could not persist customOutput to config: ${err.message}`);
8007
+ }
8008
+ })();
8009
+
7912
8010
  // Feature 2: `--mode fast|full|both`
7913
8011
  const modeIdx = args.indexOf('--mode');
7914
8012
  const mode = modeIdx !== -1
@@ -8220,7 +8318,13 @@ function main() {
8220
8318
  const stats = analyzeFiles(allFiles, cwd, { slow, maxSigs: cfg.maxSigsPerFile || 25 });
8221
8319
 
8222
8320
  if (args.includes('--json')) {
8223
- process.stdout.write(JSON.stringify(formatAnalysisJSON(stats)) + '\n');
8321
+ const out = JSON.stringify(formatAnalysisJSON(stats)) + '\n';
8322
+ // Use the write callback to exit only after the OS has accepted all
8323
+ // bytes. Calling process.exit(0) synchronously after write() truncates
8324
+ // large outputs because the underlying pipe write is asynchronous even
8325
+ // when write() returns true.
8326
+ process.stdout.write(out, 'utf8', () => process.exit(0));
8327
+ return; // exit is handled by the callback above
8224
8328
  } else {
8225
8329
  const table = formatAnalysisTable(stats, slow);
8226
8330
  process.stdout.write(table);
@@ -8326,9 +8430,38 @@ function main() {
8326
8430
  process.exit(1);
8327
8431
  }
8328
8432
  const { rank, buildSigIndex, formatRankTable, formatRankJSON } = requireSourceOrBundled('./src/retrieval/ranker');
8329
- const index = buildSigIndex(cwd);
8433
+
8434
+ // Resolve the context file path to query against.
8435
+ // Priority: --output flag > --adapter flag > buildSigIndex probe order
8436
+ // (customOutput from config is handled inside buildSigIndex itself)
8437
+ let queryOpts;
8438
+
8439
+ // 1. --output <file> pins to an explicit path
8440
+ if (config.customOutput) {
8441
+ queryOpts = { contextPath: path.resolve(cwd, config.customOutput) };
8442
+ }
8443
+
8444
+ // 2. --adapter <name> pins to that adapter's output path (if --output not given)
8445
+ if (!queryOpts) {
8446
+ const adpIdx = args.indexOf('--adapter');
8447
+ if (adpIdx >= 0) {
8448
+ const adapterName = (args[adpIdx + 1] || '').trim().toLowerCase();
8449
+ const VALID_ADAPTERS = ['copilot', 'claude', 'cursor', 'windsurf', 'openai', 'gemini', 'codex'];
8450
+ if (VALID_ADAPTERS.includes(adapterName)) {
8451
+ try {
8452
+ const adapterMod = __require('./packages/adapters/' + adapterName);
8453
+ queryOpts = { contextPath: adapterMod.outputPath(cwd) };
8454
+ } catch (_) {}
8455
+ }
8456
+ }
8457
+ }
8458
+
8459
+ const index = buildSigIndex(cwd, queryOpts);
8330
8460
  if (index.size === 0) {
8331
8461
  console.error('[sigmap] no context file found. Run: node gen-context.js');
8462
+ if (adpIdx >= 0) {
8463
+ console.error(' (tried the path for --adapter ' + (args[adpIdx + 1] || '') + ')');
8464
+ }
8332
8465
  process.exit(1);
8333
8466
  }
8334
8467
  const topIdx = args.indexOf('--top');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap",
3
- "version": "4.1.0",
3
+ "version": "4.1.2",
4
4
  "description": "Zero-dependency AI context engine — 97% token reduction. No npm install. Runs on Node 18+.",
5
5
  "main": "gen-context.js",
6
6
  "exports": {
@@ -141,24 +141,45 @@ function rank(query, sigIndex, opts) {
141
141
  }
142
142
 
143
143
  /**
144
- * Build a signature index from the generated context file.
145
- * Returns Map<filePath, string[]> where filePath is the relative path
146
- * as it appears in the ### headers of copilot-instructions.md.
144
+ * All paths where sigmap adapters write their context files, in probe order.
145
+ * The first existing file with a non-empty index wins when no explicit path
146
+ * is supplied.
147
+ */
148
+ const ADAPTER_OUTPUT_PATHS = [
149
+ ['.github', 'copilot-instructions.md'], // copilot (default)
150
+ ['CLAUDE.md'], // claude
151
+ ['AGENTS.md'], // codex
152
+ ['.cursorrules'], // cursor
153
+ ['.windsurfrules'], // windsurf
154
+ ['.github', 'openai-context.md'], // openai
155
+ ['.github', 'gemini-context.md'], // gemini
156
+ ['llm-full.txt'], // llm-full
157
+ ['llm.txt'], // llm
158
+ ];
159
+
160
+ /**
161
+ * Parse a single context file into a Map<filePath, string[]>.
147
162
  *
148
- * @param {string} cwd
163
+ * Files that contain human-written content before an
164
+ * "## Auto-generated signatures" marker (e.g. CLAUDE.md) are handled
165
+ * by skipping everything above the marker before scanning for ### headers.
166
+ *
167
+ * @param {string} contextPath - absolute path to the context file
149
168
  * @returns {Map<string, string[]>}
150
169
  */
151
- function buildSigIndex(cwd) {
152
- const fs = require('fs');
153
- const path = require('path');
154
- const contextPath = path.join(cwd, '.github', 'copilot-instructions.md');
170
+ function _parseContextFile(contextPath) {
171
+ const fs = require('fs');
155
172
  const index = new Map();
156
173
 
157
174
  if (!fs.existsSync(contextPath)) return index;
158
175
 
159
- const content = fs.readFileSync(contextPath, 'utf8');
160
- const lines = content.split('\n');
176
+ let content = fs.readFileSync(contextPath, 'utf8');
161
177
 
178
+ // Skip any human-written preamble that sits above the auto-generated block.
179
+ const markerIdx = content.indexOf('## Auto-generated signatures');
180
+ if (markerIdx !== -1) content = content.slice(markerIdx);
181
+
182
+ const lines = content.split('\n');
162
183
  let currentFile = null;
163
184
  let inBlock = false;
164
185
  let sigs = [];
@@ -180,6 +201,53 @@ function buildSigIndex(cwd) {
180
201
  return index;
181
202
  }
182
203
 
204
+ /**
205
+ * Build a signature index from the generated context file.
206
+ * Returns Map<filePath, string[]> where filePath is the relative path
207
+ * as it appears in the ### headers of the context file.
208
+ *
209
+ * Resolution priority:
210
+ * 1. `opts.contextPath` — explicit path from --output or --adapter flag
211
+ * 2. `customOutput` key in gen-context.config.json — persisted from a
212
+ * previous `--output <file>` generation run
213
+ * 3. All known adapter output paths probed in order (first non-empty wins)
214
+ *
215
+ * @param {string} cwd
216
+ * @param {{ contextPath?: string }} [opts]
217
+ * @returns {Map<string, string[]>}
218
+ */
219
+ function buildSigIndex(cwd, opts) {
220
+ const fs = require('fs');
221
+ const path = require('path');
222
+
223
+ // 1. Caller supplied an explicit path — use it directly.
224
+ if (opts && opts.contextPath) {
225
+ return _parseContextFile(opts.contextPath);
226
+ }
227
+
228
+ // 2. Check gen-context.config.json for a persisted customOutput path.
229
+ try {
230
+ const cfgPath = path.join(cwd, 'gen-context.config.json');
231
+ if (fs.existsSync(cfgPath)) {
232
+ const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
233
+ if (cfg.customOutput) {
234
+ const customPath = path.resolve(cwd, cfg.customOutput);
235
+ const index = _parseContextFile(customPath);
236
+ if (index.size > 0) return index;
237
+ }
238
+ }
239
+ } catch (_) {}
240
+
241
+ // 3. Probe all known adapter output paths; return first non-empty index.
242
+ for (const parts of ADAPTER_OUTPUT_PATHS) {
243
+ const contextPath = path.join(cwd, ...parts);
244
+ const index = _parseContextFile(contextPath);
245
+ if (index.size > 0) return index;
246
+ }
247
+
248
+ return new Map();
249
+ }
250
+
183
251
  /**
184
252
  * Format ranked results as a markdown table string.
185
253
  *