sigmap 6.10.1 → 6.10.3

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,34 @@ Format: [Semantic Versioning](https://semver.org/)
10
10
 
11
11
  ---
12
12
 
13
+ ## [6.10.3] — 2026-05-11
14
+
15
+ ### Fixed
16
+
17
+ - **MCP tools import graph analysis** — Fixed `extractImports` not being exported in bundled gen-context.js, which caused `explain_file` (imports/callers), `get_impact`, and `get_routing` tools to fail with "extractImports is not a function" error. Now all three tools correctly analyze file dependencies and impact blast radius.
18
+ - **Contributor attribution** — Added direct author commits for Denis Solonenko (GDScript extractor), Sean Campbell (Willow adapter, Python AST extractor), kumamaki (Claude adapter per-module), and Matt Van Horn (R language support) so they appear in GitHub contributors graph.
19
+
20
+ ### Changed
21
+
22
+ - **Auto-sync workflow** — Added GitHub Actions workflow to automatically sync `develop` branch with `main` after each release, preventing future branch drift.
23
+
24
+ ---
25
+
26
+ ## [6.10.2] — 2026-05-11
27
+
28
+ ### Added
29
+
30
+ - **Open-source agents documentation** — Comprehensive integration guides for OpenCode, Aider, OpenHands, and Cline with setup examples and context injection patterns. Clear separation of coding agents from inference backends.
31
+ - **Local LLM workflows guide** — Complete setup guide for Ollama, llama.cpp, vLLM with model recommendations, performance tuning, and benchmarking. Emphasizes model-agnostic nature: no API costs, full privacy, offline capability.
32
+ - **Integrations sidebar** — New VitePress navigation section highlighting open-source agents, local LLMs, MCP server, and Repomix integration.
33
+
34
+ ### Changed
35
+
36
+ - **README model-agnostic messaging** — Updated to clarify support for cloud LLMs, open-source agents, and local models with full privacy. Removed proprietary-focused language.
37
+ - **Quick-start guide** — Added links to new agent and local-LLM guides in "Next steps" section.
38
+
39
+ ---
40
+
13
41
  ## [6.10.1] — 2026-05-10
14
42
 
15
43
  ### Added
package/README.md CHANGED
@@ -36,7 +36,12 @@ Zero config. Zero dependencies. Under 10 seconds.
36
36
 
37
37
  SigMap extracts function and class signatures from your codebase and feeds the right files — not the whole repo — to your AI.
38
38
 
39
- Works with Copilot, Claude, Cursor, Windsurf, and any LLM.
39
+ **Model-agnostic.** Works with:
40
+ - **Cloud LLMs:** Claude, GPT-4, Copilot, Gemini
41
+ - **Open-source agents:** OpenCode, Aider, OpenHands, Cline
42
+ - **Local LLMs:** Ollama, llama.cpp, vLLM (no API keys, full privacy)
43
+ - **Any editor:** VS Code, Cursor, Windsurf, Neovim, JetBrains
44
+ - **Any model:** Use what you want, no vendor lock-in
40
45
 
41
46
  ---
42
47
 
@@ -46,7 +51,9 @@ Works with Copilot, Claude, Cursor, Windsurf, and any LLM.
46
51
  - **40–98% token reduction** — 2K–4K tokens instead of 80K+
47
52
  - **52.2% task success rate** — up from 10% without context
48
53
  - **1.68 prompts per task** — down from 2.84
49
- - **Works with any LLM** — no API key, no cloud, no accounts
54
+ - **No vendor lock-in** — works with any AI assistant or local LLM
55
+ - **No API costs** — use local models (Ollama, llama.cpp, vLLM) with zero token fees
56
+ - **Full privacy** — keep your code and context on your machine
50
57
  - **Zero npm dependencies** — `npx sigmap` on any machine
51
58
 
52
59
  ---
@@ -147,20 +154,26 @@ volta install sigmap
147
154
 
148
155
  | Adapter | Output file | Used by |
149
156
  |---|---|---|
150
- | `copilot` | `.github/copilot-instructions.md` | GitHub Copilot |
157
+ | `copilot` | `.github/copilot-instructions.md` | GitHub Copilot, OpenCode |
151
158
  | `claude` | `CLAUDE.md` | Claude / Claude Code |
152
- | `cursor` | `.cursorrules` | Cursor |
159
+ | `cursor` | `.cursorrules` | Cursor, Cline |
153
160
  | `windsurf` | `.windsurfrules` | Windsurf |
154
- | `openai` | `.github/openai-context.md` | OpenAI models |
161
+ | `openai` | `.github/openai-context.md` | OpenAI API, Aider, local Ollama/llama.cpp |
155
162
  | `gemini` | `.github/gemini-context.md` | Google Gemini |
156
- | `codex` | `AGENTS.md` | OpenAI Codex · OpenCode |
163
+ | `codex` | `AGENTS.md` | OpenAI Codex (legacy) |
157
164
 
158
165
  ```bash
159
- sigmap --adapter copilot # default
160
- sigmap --adapter claude
161
- sigmap --adapter cursor
166
+ sigmap --adapter copilot # default — works with Copilot, OpenCode
167
+ sigmap --adapter openai # works with Ollama, llama.cpp, vLLM, Aider
168
+ sigmap --adapter claude # works with Claude Code
162
169
  ```
163
170
 
171
+ **Open-source agents & local LLMs:**
172
+
173
+ Use SigMap with open-source tools and fully self-hosted setups:
174
+ - **[Open-source agents guide →](https://manojmallick.github.io/sigmap/guide/agents)** — OpenCode, Aider, OpenHands, Cline
175
+ - **[Local LLMs guide →](https://manojmallick.github.io/sigmap/guide/local-llms)** — Ollama, llama.cpp, vLLM (no API keys, full privacy)
176
+
164
177
  **IDE extensions:**
165
178
 
166
179
  | IDE | Install | Source | Features |
package/gen-context.js CHANGED
@@ -729,6 +729,84 @@ __factories["./src/extractors/go"] = function(module, exports) {
729
729
 
730
730
  };
731
731
 
732
+ // ── ./src/extractors/gdscript ──
733
+ __factories["./src/extractors/gdscript"] = function(module, exports) {
734
+
735
+ /**
736
+ * Extract signatures from Godot GDScript source code.
737
+ * @param {string} src - Raw file content
738
+ * @returns {string[]} Array of signature strings
739
+ */
740
+
741
+ function extract(src) {
742
+ if (!src || typeof src !== 'string') return [];
743
+ const sigs = [];
744
+
745
+ const stripped = src.replace(/#.*$/gm, '');
746
+
747
+ let className = null;
748
+ let baseName = null;
749
+ const addedClasses = new Set();
750
+
751
+ const cm = stripped.match(/^class_name\s+(\w+)(?:\s+extends\s+([\w.]+))?/m);
752
+ if (cm) {
753
+ className = cm[1];
754
+ if (cm[2]) baseName = cm[2];
755
+ }
756
+ if (!baseName) {
757
+ const em = stripped.match(/^extends\s+([\w."/]+)/m);
758
+ if (em) baseName = em[1];
759
+ }
760
+
761
+ if (className) {
762
+ sigs.push(baseName ? `class ${className}(${baseName})` : `class ${className}`);
763
+ addedClasses.add(className);
764
+ } else if (baseName) {
765
+ sigs.push(`extends ${baseName}`);
766
+ }
767
+
768
+ const indent = (className || baseName) ? ' ' : '';
769
+
770
+ for (const m of stripped.matchAll(/^signal\s+(\w+)(?:\s*\(([^)]*)\))?/gm)) {
771
+ sigs.push(`${indent}signal ${m[1]}(${normalizeParams(m[2] || '')})`);
772
+ }
773
+
774
+ for (const m of stripped.matchAll(/^enum\s+(\w+)\s*\{([^}]*)\}/gm)) {
775
+ const members = m[2]
776
+ .split(',')
777
+ .map((s) => s.trim().split(/\s*=/)[0].trim())
778
+ .filter(Boolean);
779
+ sigs.push(`${indent}enum ${m[1]} { ${members.slice(0, 6).join(', ')} }`);
780
+ }
781
+
782
+ let constCount = 0;
783
+ for (const m of stripped.matchAll(/^(?:const|var)\s+([A-Z_]\w*)\s*(?::\s*\w+)?\s*=/gm)) {
784
+ if (constCount++ > 15) break;
785
+ sigs.push(`${indent}const ${m[1]}`);
786
+ }
787
+
788
+ for (const m of stripped.matchAll(/^(?:static\s+)?func\s+(_\w+|\w+)\s*\(([^)]*)\)(?:\s*->\s*(\w+))?/gm)) {
789
+ if (m[1].startsWith('_')) continue;
790
+ const retStr = m[3] ? ` → ${m[3]}` : '';
791
+ sigs.push(`${indent}func ${m[1]}(${normalizeParams(m[2])})${retStr}`);
792
+ if (sigs.length > 30) break;
793
+ }
794
+
795
+ return sigs;
796
+ }
797
+
798
+ function normalizeParams(raw) {
799
+ if (!raw) return '';
800
+ return raw
801
+ .split(',')
802
+ .map((p) => p.trim())
803
+ .join(', ');
804
+ }
805
+
806
+ module.exports = { extract };
807
+
808
+ };
809
+
732
810
  // ── ./src/extractors/html ──
733
811
  __factories["./src/extractors/html"] = function(module, exports) {
734
812
 
@@ -2980,6 +3058,148 @@ __factories["./src/extractors/prdiff"] = function(module, exports) {
2980
3058
 
2981
3059
  };
2982
3060
 
3061
+ // ── ./src/extractors/r ──
3062
+ __factories["./src/extractors/r"] = function(module, exports) {
3063
+
3064
+ 'use strict';
3065
+
3066
+ /**
3067
+ * Extract signatures from R source code.
3068
+ * @param {string} src - Raw file content
3069
+ * @returns {string[]} Array of signature strings
3070
+ */
3071
+ function extract(src) {
3072
+ if (!src || typeof src !== 'string') return [];
3073
+ const sigs = [];
3074
+
3075
+ // Strip line comments. R uses # comments. Roxygen2 (#') comments are
3076
+ // stripped along with regular ones; Phase 2 may parse them.
3077
+ const stripped = src.replace(/#.*$/gm, '');
3078
+
3079
+ // Function definitions:
3080
+ // name <- function(args) { ... }
3081
+ // name = function(args) { ... }
3082
+ // name <<- function(args) { ... }
3083
+ // Args may span multiple lines and contain default values, so we need to
3084
+ // match a balanced parenthesis group rather than a single line.
3085
+ const funcRe = /^(?:[ \t]*)([\w.]+)\s*(?:<<-|<-|=)\s*function\s*\(/gm;
3086
+ let m;
3087
+ while ((m = funcRe.exec(stripped)) !== null) {
3088
+ const name = m[1];
3089
+ if (name.startsWith('.')) continue; // private convention
3090
+ const argsStart = funcRe.lastIndex;
3091
+ const args = readBalancedParens(stripped, argsStart - 1);
3092
+ if (args === null) continue;
3093
+ sigs.push(`${name} <- function(${normalizeParams(args)})`);
3094
+ }
3095
+
3096
+ // S4 setMethod / setGeneric:
3097
+ // setGeneric("name", function(args) standardGeneric("name"))
3098
+ // setMethod("name", "ClassName", function(args) { ... })
3099
+ for (const sm of stripped.matchAll(/^[ \t]*setGeneric\s*\(\s*["']([\w.]+)["']/gm)) {
3100
+ sigs.push(`setGeneric("${sm[1]}")`);
3101
+ }
3102
+ for (const sm of stripped.matchAll(/^[ \t]*setMethod\s*\(\s*["']([\w.]+)["']\s*,\s*["']([\w.]+)["']/gm)) {
3103
+ sigs.push(`setMethod("${sm[1]}", "${sm[2]}")`);
3104
+ }
3105
+
3106
+ // S4 class definitions:
3107
+ // setClass("Name", representation(...), ...)
3108
+ for (const sm of stripped.matchAll(/^[ \t]*setClass\s*\(\s*["']([\w.]+)["']/gm)) {
3109
+ sigs.push(`setClass("${sm[1]}")`);
3110
+ }
3111
+
3112
+ return sigs.slice(0, 30);
3113
+ }
3114
+
3115
+ /**
3116
+ * Read a parenthesis-balanced substring starting at the position of the
3117
+ * opening '(' character, returning the inner content (without the outer
3118
+ * parens). Returns null if no matching close paren is found within `cap`
3119
+ * characters, which guards against runaway scans on malformed input.
3120
+ */
3121
+ function readBalancedParens(src, openIdx, cap = 4096) {
3122
+ if (src[openIdx] !== '(') return null;
3123
+ let depth = 1;
3124
+ let i = openIdx + 1;
3125
+ const end = Math.min(src.length, openIdx + cap);
3126
+ let inString = null; // null | '"' | "'"
3127
+ while (i < end) {
3128
+ const ch = src[i];
3129
+ if (inString) {
3130
+ if (ch === '\\') { i += 2; continue; }
3131
+ if (ch === inString) inString = null;
3132
+ i++;
3133
+ continue;
3134
+ }
3135
+ if (ch === '"' || ch === "'") { inString = ch; i++; continue; }
3136
+ if (ch === '(') depth++;
3137
+ else if (ch === ')') {
3138
+ depth--;
3139
+ if (depth === 0) return src.slice(openIdx + 1, i);
3140
+ }
3141
+ i++;
3142
+ }
3143
+ return null;
3144
+ }
3145
+
3146
+ /**
3147
+ * Compress whitespace inside a parameter list, collapse multi-line default
3148
+ * expressions onto a single line, and trim. The goal is one-line readable
3149
+ * signatures, not a faithful AST.
3150
+ *
3151
+ * String literals are protected so that commas/equals inside default values
3152
+ * like sep = "," don't get respaced.
3153
+ */
3154
+ function normalizeParams(raw) {
3155
+ const tokens = [];
3156
+ let buf = '';
3157
+ let inString = null;
3158
+ for (let i = 0; i < raw.length; i++) {
3159
+ const ch = raw[i];
3160
+ if (inString) {
3161
+ buf += ch;
3162
+ if (ch === '\\' && i + 1 < raw.length) { buf += raw[i + 1]; i++; continue; }
3163
+ if (ch === inString) inString = null;
3164
+ continue;
3165
+ }
3166
+ if (ch === '"' || ch === "'") { inString = ch; buf += ch; continue; }
3167
+ buf += ch;
3168
+ }
3169
+ // Now buf === raw with strings preserved character-for-character.
3170
+ // Walk again: collapse non-string runs of whitespace, normalize ', ' and ' = '.
3171
+ let out = '';
3172
+ inString = null;
3173
+ for (let i = 0; i < buf.length; i++) {
3174
+ const ch = buf[i];
3175
+ if (inString) {
3176
+ out += ch;
3177
+ if (ch === '\\' && i + 1 < buf.length) { out += buf[i + 1]; i++; continue; }
3178
+ if (ch === inString) inString = null;
3179
+ continue;
3180
+ }
3181
+ if (ch === '"' || ch === "'") { inString = ch; out += ch; continue; }
3182
+ if (/\s/.test(ch)) {
3183
+ if (out.length && !/\s$/.test(out)) out += ' ';
3184
+ continue;
3185
+ }
3186
+ if (ch === ',') {
3187
+ out = out.replace(/\s+$/, '') + ', ';
3188
+ continue;
3189
+ }
3190
+ if (ch === '=') {
3191
+ out = out.replace(/\s+$/, '') + ' = ';
3192
+ continue;
3193
+ }
3194
+ out += ch;
3195
+ }
3196
+ return out.trim();
3197
+ }
3198
+
3199
+ module.exports = { extract };
3200
+
3201
+ };
3202
+
2983
3203
  // ── ./src/format/cache ──
2984
3204
  __factories["./src/format/cache"] = function(module, exports) {
2985
3205
 
@@ -4239,7 +4459,7 @@ __factories["./src/map/class-hierarchy"] = function(module, exports) {
4239
4459
  .join('\n');
4240
4460
  }
4241
4461
 
4242
- module.exports = { analyze };
4462
+ module.exports = { analyze, extractImports };
4243
4463
 
4244
4464
  };
4245
4465
 
@@ -5387,7 +5607,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
5387
5607
 
5388
5608
  const SERVER_INFO = {
5389
5609
  name: 'sigmap',
5390
- version: '6.10.1',
5610
+ version: '6.10.3',
5391
5611
  description: 'SigMap MCP server — code signatures on demand',
5392
5612
  };
5393
5613
 
@@ -8014,7 +8234,7 @@ const path = require('path');
8014
8234
  const os = require('os');
8015
8235
  const { execSync } = require('child_process');
8016
8236
 
8017
- const VERSION = '6.10.1';
8237
+ const VERSION = '6.10.3';
8018
8238
  const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
8019
8239
 
8020
8240
  function requireSourceOrBundled(key) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap",
3
- "version": "6.10.1",
3
+ "version": "6.10.3",
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": {
@@ -11,14 +11,16 @@
11
11
 
12
12
  const path = require('path');
13
13
 
14
- const ADAPTER_NAMES = ['copilot', 'claude', 'cursor', 'windsurf', 'openai', 'gemini', 'codex'];
14
+ // Third-party adapters: 'willow' writes atoms to a Willow MCP knowledge store
15
+ // instead of a flat file (see willow.js for configuration).
16
+ const ADAPTER_NAMES = ['copilot', 'claude', 'cursor', 'windsurf', 'openai', 'gemini', 'codex', 'willow'];
15
17
 
16
18
  // Lazy-load adapters so unused ones don't pay any require() cost
17
19
  const _cache = {};
18
20
 
19
21
  /**
20
22
  * Load and return an adapter module by name.
21
- * @param {string} name - Adapter name (copilot|claude|cursor|windsurf|openai|gemini|codex)
23
+ * @param {string} name - Adapter name (copilot|claude|cursor|windsurf|openai|gemini|codex|willow)
22
24
  * @returns {{ name: string, format: Function, outputPath: Function }|null}
23
25
  */
24
26
  function getAdapter(name) {
@@ -0,0 +1,200 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Willow adapter — writes SigMap context to Willow MCP knowledge store.
5
+ *
6
+ * Instead of writing a flat .willow-context.md file, this adapter sends
7
+ * signature atoms to a Willow MCP server (https://github.com/rudi193-cmd/willow-1.9)
8
+ * via HTTP POST. Each indexed file becomes a searchable knowledge atom.
9
+ *
10
+ * Contract:
11
+ * format(context, opts?) → string (markdown for display/debug)
12
+ * outputPath(cwd) → string (placeholder — no file written)
13
+ * write(context, cwd, opts?) → Promise<void> (POSTs to Willow MCP, must await)
14
+ *
15
+ * Configuration (env vars or opts):
16
+ * WILLOW_MCP_URL — MCP server base URL (default: http://localhost:8000)
17
+ * WILLOW_AGENT — agent namespace (default: sigmap)
18
+ * WILLOW_TIMEOUT — fetch timeout in ms (default: 30000)
19
+ * WILLOW_MAX_ATOM_SIZE — max atom size in bytes (default: 100000)
20
+ * WILLOW_RETRIES — max retry attempts for transient failures (default: 3)
21
+ */
22
+
23
+ const crypto = require('crypto');
24
+ const name = 'willow';
25
+ const DEFAULT_MCP_URL = 'http://localhost:8000';
26
+ const DEFAULT_TIMEOUT_MS = 30000;
27
+ const DEFAULT_MAX_ATOM_SIZE = 100000;
28
+ const DEFAULT_RETRIES = 3;
29
+
30
+ /**
31
+ * Format SigMap context as markdown for display or debug.
32
+ * @param {string} context - Raw SigMap context string
33
+ * @param {object} [opts] - Unused; reserved for future options
34
+ * @returns {string}
35
+ */
36
+ function format(context, opts = {}) {
37
+ if (!context || typeof context !== 'string') return '';
38
+ const ts = new Date().toISOString();
39
+ return `<!-- SigMap Willow context — ${ts} -->\n\n${context}`;
40
+ }
41
+
42
+ /**
43
+ * Return the placeholder output path (no file is written by this adapter).
44
+ * @param {string} cwd - Working directory
45
+ * @returns {string}
46
+ */
47
+ function outputPath(cwd) {
48
+ return '.willow-context.md';
49
+ }
50
+
51
+ /**
52
+ * Generate a cryptographically strong ID for an atom.
53
+ * Uses SHA256 hash of the filepath to ensure uniqueness and prevent collisions.
54
+ * @param {string} filepath - File path to hash
55
+ * @returns {string} - sigmap-{32-char-hex}
56
+ */
57
+ function generateAtomId(filepath) {
58
+ const hash = crypto
59
+ .createHash('sha256')
60
+ .update(filepath)
61
+ .digest('hex');
62
+ return `sigmap-${hash}`;
63
+ }
64
+
65
+ /**
66
+ * Fetch with timeout support.
67
+ * @param {string} url - URL to fetch
68
+ * @param {object} opts - Fetch options
69
+ * @param {number} timeoutMs - Timeout in milliseconds
70
+ * @returns {Promise<Response>}
71
+ */
72
+ async function fetchWithTimeout(url, opts, timeoutMs) {
73
+ const controller = new AbortController();
74
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
75
+ try {
76
+ return await fetch(url, { ...opts, signal: controller.signal });
77
+ } finally {
78
+ clearTimeout(timeoutId);
79
+ }
80
+ }
81
+
82
+ /**
83
+ * POST an atom to Willow with exponential backoff retry.
84
+ * @param {object} atom - Atom to ingest
85
+ * @param {string} mcpUrl - MCP server URL
86
+ * @param {number} timeoutMs - Fetch timeout
87
+ * @param {number} maxRetries - Max retry attempts
88
+ * @returns {Promise<boolean>} - True if succeeded, false if all retries exhausted
89
+ */
90
+ async function postAtomWithRetry(atom, mcpUrl, timeoutMs, maxRetries) {
91
+ let lastErr;
92
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
93
+ try {
94
+ const resp = await fetchWithTimeout(
95
+ `${mcpUrl}/tools/call`,
96
+ {
97
+ method: 'POST',
98
+ headers: { 'Content-Type': 'application/json' },
99
+ body: JSON.stringify({
100
+ name: 'willow_knowledge_ingest',
101
+ arguments: {
102
+ app_id: atom.agent,
103
+ title: atom.title,
104
+ summary: atom.summary,
105
+ domain: atom.domain,
106
+ source_type: atom.source_type,
107
+ category: 'code',
108
+ record_id: atom.id,
109
+ },
110
+ }),
111
+ },
112
+ timeoutMs,
113
+ );
114
+
115
+ if (resp.ok) {
116
+ return true;
117
+ }
118
+
119
+ if (resp.status >= 500) {
120
+ lastErr = new Error(`HTTP ${resp.status}`);
121
+ if (attempt < maxRetries - 1) {
122
+ await new Promise((resolve) => setTimeout(resolve, 100 * Math.pow(2, attempt)));
123
+ continue;
124
+ }
125
+ } else {
126
+ process.stderr.write(`[willow-adapter] ${atom.id}: HTTP ${resp.status} (not retryable)\n`);
127
+ return false;
128
+ }
129
+ } catch (err) {
130
+ lastErr = err;
131
+ if (attempt < maxRetries - 1) {
132
+ await new Promise((resolve) => setTimeout(resolve, 100 * Math.pow(2, attempt)));
133
+ continue;
134
+ }
135
+ }
136
+ }
137
+
138
+ process.stderr.write(
139
+ `[willow-adapter] ${atom.id}: failed after ${maxRetries} attempts: ${lastErr?.message || 'unknown'}\n`,
140
+ );
141
+ return false;
142
+ }
143
+
144
+ /**
145
+ * POST each file section from SigMap context to the Willow MCP knowledge store.
146
+ * Each `## filepath` section becomes one searchable knowledge atom.
147
+ * Failures are per-atom and logged to stderr; function never throws.
148
+ * IMPORTANT: This is async — caller MUST await write() before process exit.
149
+ *
150
+ * @param {string} context - Raw SigMap context string
151
+ * @param {string} cwd - Working directory (used as project label)
152
+ * @param {object} [opts] - Optional overrides: { mcpUrl, agent, timeoutMs, maxAtomSize, maxRetries }
153
+ * @returns {Promise<void>}
154
+ */
155
+ async function write(context, cwd, opts = {}) {
156
+ if (!context) return;
157
+
158
+ const mcpUrl = opts.mcpUrl || process.env.WILLOW_MCP_URL || DEFAULT_MCP_URL;
159
+ const agent = opts.agent || process.env.WILLOW_AGENT || 'sigmap';
160
+ const timeoutMs = opts.timeoutMs || parseInt(process.env.WILLOW_TIMEOUT, 10) || DEFAULT_TIMEOUT_MS;
161
+ const maxAtomSize = opts.maxAtomSize || parseInt(process.env.WILLOW_MAX_ATOM_SIZE, 10) || DEFAULT_MAX_ATOM_SIZE;
162
+ const maxRetries = opts.maxRetries || parseInt(process.env.WILLOW_RETRIES, 10) || DEFAULT_RETRIES;
163
+
164
+ const sections = context.split(/\n(?=##\s)/);
165
+ const atoms = sections
166
+ .map((section) => {
167
+ const titleMatch = section.match(/^##\s+(.+)/);
168
+ if (!titleMatch) return null;
169
+
170
+ const title = titleMatch[1].trim();
171
+ const contentSize = section.length;
172
+
173
+ if (contentSize > maxAtomSize) {
174
+ process.stderr.write(`[willow-adapter] ${title}: oversized (${contentSize} > ${maxAtomSize} bytes)\n`);
175
+ return null;
176
+ }
177
+
178
+ return {
179
+ id: generateAtomId(title),
180
+ title,
181
+ summary: `${title} (${contentSize} bytes)`,
182
+ content: section.trim(),
183
+ domain: 'code',
184
+ source_type: 'sigmap',
185
+ agent,
186
+ project: cwd ? require('path').basename(cwd) : 'unknown',
187
+ };
188
+ })
189
+ .filter(Boolean);
190
+
191
+ if (!atoms.length) return;
192
+
193
+ await Promise.all(
194
+ atoms.map((atom) => postAtomWithRetry(atom, mcpUrl, timeoutMs, maxRetries).catch((err) => {
195
+ process.stderr.write(`[willow-adapter] ${atom.id}: unexpected error: ${err.message}\n`);
196
+ })),
197
+ );
198
+ }
199
+
200
+ module.exports = { name, format, outputPath, write };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap-cli",
3
- "version": "6.10.1",
3
+ "version": "6.10.3",
4
4
  "description": "SigMap CLI wrapper — thin adapter for programmatic CLI invocation",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -29,6 +29,7 @@ const EXT_MAP = {
29
29
  '.swift': 'swift',
30
30
  '.dart': 'dart',
31
31
  '.scala': 'scala', '.sc': 'scala',
32
+ '.gd': 'gdscript',
32
33
  '.r': 'r', '.R': 'r',
33
34
  '.vue': 'vue',
34
35
  '.svelte': 'svelte',
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap-core",
3
- "version": "6.10.1",
3
+ "version": "6.10.3",
4
4
  "description": "SigMap core library — zero-dependency code signature extraction, retrieval, and security scanning",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -19,6 +19,7 @@ const EXT_TO_LANG = {
19
19
  '.java': 'java', '.kt': 'kotlin', '.cs': 'csharp', '.cpp': 'cpp',
20
20
  '.c': 'cpp', '.h': 'cpp', '.hpp': 'cpp', '.swift': 'swift',
21
21
  '.dart': 'dart', '.scala': 'scala', '.php': 'php',
22
+ '.gd': 'gdscript',
22
23
  '.r': 'r', '.R': 'r',
23
24
  };
24
25
 
@@ -29,6 +29,7 @@ const EXT_MAP = {
29
29
  '.swift': 'swift',
30
30
  '.dart': 'dart',
31
31
  '.scala': 'scala', '.sc': 'scala',
32
+ '.gd': 'gdscript',
32
33
  '.r': 'r', '.R': 'r',
33
34
  '.vue': 'vue',
34
35
  '.svelte': 'svelte',