sigmap 4.1.1 → 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,46 @@ 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
+
13
53
  ## [4.1.1] — 2026-04-16 — Fix: --query works with any adapter output
14
54
 
15
55
  ### Fixed
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
@@ -5479,8 +5479,19 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
5479
5479
  return index;
5480
5480
  }
5481
5481
  function buildSigIndex(cwd, opts) {
5482
- const path = require('path');
5482
+ const fs = require('fs'); const path = require('path');
5483
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 (_) {}
5484
5495
  for (const parts of ADAPTER_OUTPUT_PATHS) {
5485
5496
  const contextPath = path.join(cwd, ...parts);
5486
5497
  const index = _parseContextFile(contextPath);
@@ -6238,7 +6249,7 @@ const path = require('path');
6238
6249
  const os = require('os');
6239
6250
  const { execSync } = require('child_process');
6240
6251
 
6241
- const VERSION = '4.1.0';
6252
+ const VERSION = '4.1.2';
6242
6253
  const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
6243
6254
 
6244
6255
  function requireSourceOrBundled(key) {
@@ -6926,6 +6937,19 @@ function writeOutputs(content, targets, cwd, config) {
6926
6937
  if (ADAPTER_TARGETS.has(target)) {
6927
6938
  try {
6928
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
+ }
6929
6953
  if (typeof adapterMod.write === 'function') {
6930
6954
  adapterMod.write(content, cwd, { version: VERSION });
6931
6955
  const outPath = adapterMod.outputPath(cwd);
@@ -7507,6 +7531,25 @@ function runGenerate(cwd, config, reportMode, reportJson = false) {
7507
7531
  writeOutputs(content, config.outputs, cwd, config);
7508
7532
  }
7509
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
+
7510
7553
  result = { inputTokenTotal, finalTokens, fileCount: beforeCount, droppedCount };
7511
7554
  }
7512
7555
  } else {
@@ -7931,6 +7974,39 @@ function main() {
7931
7974
 
7932
7975
  const config = loadConfig(cwd);
7933
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
+
7934
8010
  // Feature 2: `--mode fast|full|both`
7935
8011
  const modeIdx = args.indexOf('--mode');
7936
8012
  const mode = modeIdx !== -1
@@ -8355,19 +8431,28 @@ function main() {
8355
8431
  }
8356
8432
  const { rank, buildSigIndex, formatRankTable, formatRankJSON } = requireSourceOrBundled('./src/retrieval/ranker');
8357
8433
 
8358
- // Resolve an explicit context file path when --adapter is present.
8359
- // This lets `--adapter claude --query "..."` read CLAUDE.md instead of
8360
- // falling through to the default copilot-instructions.md probe.
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)
8361
8437
  let queryOpts;
8362
- const adpIdx = args.indexOf('--adapter');
8363
- if (adpIdx >= 0) {
8364
- const adapterName = (args[adpIdx + 1] || '').trim().toLowerCase();
8365
- const VALID_ADAPTERS = ['copilot', 'claude', 'cursor', 'windsurf', 'openai', 'gemini', 'codex'];
8366
- if (VALID_ADAPTERS.includes(adapterName)) {
8367
- try {
8368
- const adapterMod = __require('./packages/adapters/' + adapterName);
8369
- queryOpts = { contextPath: adapterMod.outputPath(cwd) };
8370
- } catch (_) {}
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
+ }
8371
8456
  }
8372
8457
  }
8373
8458
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap",
3
- "version": "4.1.1",
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": {
@@ -206,26 +206,39 @@ function _parseContextFile(contextPath) {
206
206
  * Returns Map<filePath, string[]> where filePath is the relative path
207
207
  * as it appears in the ### headers of the context file.
208
208
  *
209
- * When `opts.contextPath` is provided, that specific file is used.
210
- * This is the case when the caller already knows the path (e.g. via
211
- * --adapter <name> or --output <file>).
212
- *
213
- * Otherwise all known adapter output paths are probed in order and the
214
- * first file that produces a non-empty index is returned.
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)
215
214
  *
216
215
  * @param {string} cwd
217
216
  * @param {{ contextPath?: string }} [opts]
218
217
  * @returns {Map<string, string[]>}
219
218
  */
220
219
  function buildSigIndex(cwd, opts) {
220
+ const fs = require('fs');
221
221
  const path = require('path');
222
222
 
223
- // Caller supplied an explicit path — use it directly.
223
+ // 1. Caller supplied an explicit path — use it directly.
224
224
  if (opts && opts.contextPath) {
225
225
  return _parseContextFile(opts.contextPath);
226
226
  }
227
227
 
228
- // Probe all known adapter output paths; return first non-empty index.
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.
229
242
  for (const parts of ADAPTER_OUTPUT_PATHS) {
230
243
  const contextPath = path.join(cwd, ...parts);
231
244
  const index = _parseContextFile(contextPath);