sigmap 6.10.1 → 6.10.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/AGENTS.md +466 -208
- package/CHANGELOG.md +15 -0
- package/README.md +22 -9
- package/gen-context.js +144 -2
- package/package.json +1 -1
- package/packages/adapters/index.js +4 -2
- package/packages/adapters/willow.js +200 -0
- package/packages/cli/package.json +1 -1
- package/packages/core/index.js +1 -0
- package/packages/core/package.json +1 -1
- package/src/discovery/language-detector.js +1 -0
- package/src/eval/analyzer.js +1 -0
- package/src/extractors/gdscript.js +131 -0
- package/src/map/import-graph.js +1 -1
- package/src/mcp/server.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,21 @@ Format: [Semantic Versioning](https://semver.org/)
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
+
## [6.10.2] — 2026-05-11
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- **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.
|
|
18
|
+
- **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.
|
|
19
|
+
- **Integrations sidebar** — New VitePress navigation section highlighting open-source agents, local LLMs, MCP server, and Repomix integration.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- **README model-agnostic messaging** — Updated to clarify support for cloud LLMs, open-source agents, and local models with full privacy. Removed proprietary-focused language.
|
|
24
|
+
- **Quick-start guide** — Added links to new agent and local-LLM guides in "Next steps" section.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
13
28
|
## [6.10.1] — 2026-05-10
|
|
14
29
|
|
|
15
30
|
### 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
|
|
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
|
-
- **
|
|
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
|
|
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
|
|
163
|
+
| `codex` | `AGENTS.md` | OpenAI Codex (legacy) |
|
|
157
164
|
|
|
158
165
|
```bash
|
|
159
|
-
sigmap --adapter copilot # default
|
|
160
|
-
sigmap --adapter
|
|
161
|
-
sigmap --adapter
|
|
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
|
@@ -2980,6 +2980,148 @@ __factories["./src/extractors/prdiff"] = function(module, exports) {
|
|
|
2980
2980
|
|
|
2981
2981
|
};
|
|
2982
2982
|
|
|
2983
|
+
// ── ./src/extractors/r ──
|
|
2984
|
+
__factories["./src/extractors/r"] = function(module, exports) {
|
|
2985
|
+
|
|
2986
|
+
'use strict';
|
|
2987
|
+
|
|
2988
|
+
/**
|
|
2989
|
+
* Extract signatures from R source code.
|
|
2990
|
+
* @param {string} src - Raw file content
|
|
2991
|
+
* @returns {string[]} Array of signature strings
|
|
2992
|
+
*/
|
|
2993
|
+
function extract(src) {
|
|
2994
|
+
if (!src || typeof src !== 'string') return [];
|
|
2995
|
+
const sigs = [];
|
|
2996
|
+
|
|
2997
|
+
// Strip line comments. R uses # comments. Roxygen2 (#') comments are
|
|
2998
|
+
// stripped along with regular ones; Phase 2 may parse them.
|
|
2999
|
+
const stripped = src.replace(/#.*$/gm, '');
|
|
3000
|
+
|
|
3001
|
+
// Function definitions:
|
|
3002
|
+
// name <- function(args) { ... }
|
|
3003
|
+
// name = function(args) { ... }
|
|
3004
|
+
// name <<- function(args) { ... }
|
|
3005
|
+
// Args may span multiple lines and contain default values, so we need to
|
|
3006
|
+
// match a balanced parenthesis group rather than a single line.
|
|
3007
|
+
const funcRe = /^(?:[ \t]*)([\w.]+)\s*(?:<<-|<-|=)\s*function\s*\(/gm;
|
|
3008
|
+
let m;
|
|
3009
|
+
while ((m = funcRe.exec(stripped)) !== null) {
|
|
3010
|
+
const name = m[1];
|
|
3011
|
+
if (name.startsWith('.')) continue; // private convention
|
|
3012
|
+
const argsStart = funcRe.lastIndex;
|
|
3013
|
+
const args = readBalancedParens(stripped, argsStart - 1);
|
|
3014
|
+
if (args === null) continue;
|
|
3015
|
+
sigs.push(`${name} <- function(${normalizeParams(args)})`);
|
|
3016
|
+
}
|
|
3017
|
+
|
|
3018
|
+
// S4 setMethod / setGeneric:
|
|
3019
|
+
// setGeneric("name", function(args) standardGeneric("name"))
|
|
3020
|
+
// setMethod("name", "ClassName", function(args) { ... })
|
|
3021
|
+
for (const sm of stripped.matchAll(/^[ \t]*setGeneric\s*\(\s*["']([\w.]+)["']/gm)) {
|
|
3022
|
+
sigs.push(`setGeneric("${sm[1]}")`);
|
|
3023
|
+
}
|
|
3024
|
+
for (const sm of stripped.matchAll(/^[ \t]*setMethod\s*\(\s*["']([\w.]+)["']\s*,\s*["']([\w.]+)["']/gm)) {
|
|
3025
|
+
sigs.push(`setMethod("${sm[1]}", "${sm[2]}")`);
|
|
3026
|
+
}
|
|
3027
|
+
|
|
3028
|
+
// S4 class definitions:
|
|
3029
|
+
// setClass("Name", representation(...), ...)
|
|
3030
|
+
for (const sm of stripped.matchAll(/^[ \t]*setClass\s*\(\s*["']([\w.]+)["']/gm)) {
|
|
3031
|
+
sigs.push(`setClass("${sm[1]}")`);
|
|
3032
|
+
}
|
|
3033
|
+
|
|
3034
|
+
return sigs.slice(0, 30);
|
|
3035
|
+
}
|
|
3036
|
+
|
|
3037
|
+
/**
|
|
3038
|
+
* Read a parenthesis-balanced substring starting at the position of the
|
|
3039
|
+
* opening '(' character, returning the inner content (without the outer
|
|
3040
|
+
* parens). Returns null if no matching close paren is found within `cap`
|
|
3041
|
+
* characters, which guards against runaway scans on malformed input.
|
|
3042
|
+
*/
|
|
3043
|
+
function readBalancedParens(src, openIdx, cap = 4096) {
|
|
3044
|
+
if (src[openIdx] !== '(') return null;
|
|
3045
|
+
let depth = 1;
|
|
3046
|
+
let i = openIdx + 1;
|
|
3047
|
+
const end = Math.min(src.length, openIdx + cap);
|
|
3048
|
+
let inString = null; // null | '"' | "'"
|
|
3049
|
+
while (i < end) {
|
|
3050
|
+
const ch = src[i];
|
|
3051
|
+
if (inString) {
|
|
3052
|
+
if (ch === '\\') { i += 2; continue; }
|
|
3053
|
+
if (ch === inString) inString = null;
|
|
3054
|
+
i++;
|
|
3055
|
+
continue;
|
|
3056
|
+
}
|
|
3057
|
+
if (ch === '"' || ch === "'") { inString = ch; i++; continue; }
|
|
3058
|
+
if (ch === '(') depth++;
|
|
3059
|
+
else if (ch === ')') {
|
|
3060
|
+
depth--;
|
|
3061
|
+
if (depth === 0) return src.slice(openIdx + 1, i);
|
|
3062
|
+
}
|
|
3063
|
+
i++;
|
|
3064
|
+
}
|
|
3065
|
+
return null;
|
|
3066
|
+
}
|
|
3067
|
+
|
|
3068
|
+
/**
|
|
3069
|
+
* Compress whitespace inside a parameter list, collapse multi-line default
|
|
3070
|
+
* expressions onto a single line, and trim. The goal is one-line readable
|
|
3071
|
+
* signatures, not a faithful AST.
|
|
3072
|
+
*
|
|
3073
|
+
* String literals are protected so that commas/equals inside default values
|
|
3074
|
+
* like sep = "," don't get respaced.
|
|
3075
|
+
*/
|
|
3076
|
+
function normalizeParams(raw) {
|
|
3077
|
+
const tokens = [];
|
|
3078
|
+
let buf = '';
|
|
3079
|
+
let inString = null;
|
|
3080
|
+
for (let i = 0; i < raw.length; i++) {
|
|
3081
|
+
const ch = raw[i];
|
|
3082
|
+
if (inString) {
|
|
3083
|
+
buf += ch;
|
|
3084
|
+
if (ch === '\\' && i + 1 < raw.length) { buf += raw[i + 1]; i++; continue; }
|
|
3085
|
+
if (ch === inString) inString = null;
|
|
3086
|
+
continue;
|
|
3087
|
+
}
|
|
3088
|
+
if (ch === '"' || ch === "'") { inString = ch; buf += ch; continue; }
|
|
3089
|
+
buf += ch;
|
|
3090
|
+
}
|
|
3091
|
+
// Now buf === raw with strings preserved character-for-character.
|
|
3092
|
+
// Walk again: collapse non-string runs of whitespace, normalize ', ' and ' = '.
|
|
3093
|
+
let out = '';
|
|
3094
|
+
inString = null;
|
|
3095
|
+
for (let i = 0; i < buf.length; i++) {
|
|
3096
|
+
const ch = buf[i];
|
|
3097
|
+
if (inString) {
|
|
3098
|
+
out += ch;
|
|
3099
|
+
if (ch === '\\' && i + 1 < buf.length) { out += buf[i + 1]; i++; continue; }
|
|
3100
|
+
if (ch === inString) inString = null;
|
|
3101
|
+
continue;
|
|
3102
|
+
}
|
|
3103
|
+
if (ch === '"' || ch === "'") { inString = ch; out += ch; continue; }
|
|
3104
|
+
if (/\s/.test(ch)) {
|
|
3105
|
+
if (out.length && !/\s$/.test(out)) out += ' ';
|
|
3106
|
+
continue;
|
|
3107
|
+
}
|
|
3108
|
+
if (ch === ',') {
|
|
3109
|
+
out = out.replace(/\s+$/, '') + ', ';
|
|
3110
|
+
continue;
|
|
3111
|
+
}
|
|
3112
|
+
if (ch === '=') {
|
|
3113
|
+
out = out.replace(/\s+$/, '') + ' = ';
|
|
3114
|
+
continue;
|
|
3115
|
+
}
|
|
3116
|
+
out += ch;
|
|
3117
|
+
}
|
|
3118
|
+
return out.trim();
|
|
3119
|
+
}
|
|
3120
|
+
|
|
3121
|
+
module.exports = { extract };
|
|
3122
|
+
|
|
3123
|
+
};
|
|
3124
|
+
|
|
2983
3125
|
// ── ./src/format/cache ──
|
|
2984
3126
|
__factories["./src/format/cache"] = function(module, exports) {
|
|
2985
3127
|
|
|
@@ -5387,7 +5529,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
|
|
|
5387
5529
|
|
|
5388
5530
|
const SERVER_INFO = {
|
|
5389
5531
|
name: 'sigmap',
|
|
5390
|
-
version: '6.10.
|
|
5532
|
+
version: '6.10.2',
|
|
5391
5533
|
description: 'SigMap MCP server — code signatures on demand',
|
|
5392
5534
|
};
|
|
5393
5535
|
|
|
@@ -8014,7 +8156,7 @@ const path = require('path');
|
|
|
8014
8156
|
const os = require('os');
|
|
8015
8157
|
const { execSync } = require('child_process');
|
|
8016
8158
|
|
|
8017
|
-
const VERSION = '6.10.
|
|
8159
|
+
const VERSION = '6.10.2';
|
|
8018
8160
|
const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
|
|
8019
8161
|
|
|
8020
8162
|
function requireSourceOrBundled(key) {
|
package/package.json
CHANGED
|
@@ -11,14 +11,16 @@
|
|
|
11
11
|
|
|
12
12
|
const path = require('path');
|
|
13
13
|
|
|
14
|
-
|
|
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 };
|
package/packages/core/index.js
CHANGED
|
@@ -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
|
|
package/src/eval/analyzer.js
CHANGED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Extract signatures from Godot GDScript source code.
|
|
5
|
+
* @param {string} src - Raw file content
|
|
6
|
+
* @returns {string[]} Array of signature strings
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
function extract(src) {
|
|
10
|
+
if (!src || typeof src !== 'string') return [];
|
|
11
|
+
const sigs = [];
|
|
12
|
+
|
|
13
|
+
const stripped = src.replace(/#.*$/gm, '');
|
|
14
|
+
|
|
15
|
+
let className = null;
|
|
16
|
+
let baseName = null;
|
|
17
|
+
const addedClasses = new Set();
|
|
18
|
+
|
|
19
|
+
const cm = stripped.match(/^class_name\s+(\w+)(?:\s+extends\s+([\w.]+))?/m);
|
|
20
|
+
if (cm) {
|
|
21
|
+
className = cm[1];
|
|
22
|
+
if (cm[2]) baseName = cm[2];
|
|
23
|
+
}
|
|
24
|
+
if (!baseName) {
|
|
25
|
+
const em = stripped.match(/^extends\s+([\w."/]+)/m);
|
|
26
|
+
if (em) baseName = em[1];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (className) {
|
|
30
|
+
sigs.push(baseName ? `class ${className}(${baseName})` : `class ${className}`);
|
|
31
|
+
addedClasses.add(className);
|
|
32
|
+
} else if (baseName) {
|
|
33
|
+
sigs.push(`extends ${baseName}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const indent = (className || baseName) ? ' ' : '';
|
|
37
|
+
|
|
38
|
+
for (const m of stripped.matchAll(/^signal\s+(\w+)(?:\s*\(([^)]*)\))?/gm)) {
|
|
39
|
+
sigs.push(`${indent}signal ${m[1]}(${normalizeParams(m[2] || '')})`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
for (const m of stripped.matchAll(/^enum\s+(\w+)\s*\{([^}]*)\}/gm)) {
|
|
43
|
+
const members = m[2]
|
|
44
|
+
.split(',')
|
|
45
|
+
.map((s) => s.trim().split(/\s*=/)[0].trim())
|
|
46
|
+
.filter(Boolean);
|
|
47
|
+
sigs.push(`${indent}enum ${m[1]} { ${members.slice(0, 6).join(', ')} }`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let constCount = 0;
|
|
51
|
+
for (const m of stripped.matchAll(/^const\s+(\w+)(?:\s*:\s*[^=\n]+)?\s*:?=\s*([^\n]+)$/gm)) {
|
|
52
|
+
let val = m[2].trim();
|
|
53
|
+
const preloadMatch = val.match(/^preload\s*\(([^)]+)\)/);
|
|
54
|
+
if (preloadMatch) {
|
|
55
|
+
val = `preload(${preloadMatch[1]})`;
|
|
56
|
+
} else if (val.length > 40) {
|
|
57
|
+
val = val.slice(0, 37) + '...';
|
|
58
|
+
}
|
|
59
|
+
sigs.push(`${indent}const ${m[1]} = ${val}`);
|
|
60
|
+
if (++constCount >= 5) break;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
for (const m of stripped.matchAll(/^((?:@\w+(?:\([^)]*\))?\s+)*)var\s+(\w+)(?:\s*:\s*([^=\n]+?))?(?:\s*:?=\s*[^\n]+)?$/gm)) {
|
|
64
|
+
const decorators = m[1] || '';
|
|
65
|
+
const name = m[2];
|
|
66
|
+
const hasDecorator = /@\w+/.test(decorators);
|
|
67
|
+
if (!hasDecorator && name.startsWith('_')) continue;
|
|
68
|
+
|
|
69
|
+
let prefix = decorators.replace(/\([^)]*\)/g, '').trim().split(/\s+/).join(' ');
|
|
70
|
+
if (prefix) prefix += ' ';
|
|
71
|
+
|
|
72
|
+
const type = (m[3] || '').trim();
|
|
73
|
+
const typeStr = type ? `: ${type}` : '';
|
|
74
|
+
sigs.push(`${indent}${prefix}var ${name}${typeStr}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
for (const m of stripped.matchAll(/^(static\s+)?func\s+(\w+)\s*\(([^)]*)\)(?:\s*->\s*([^:\n]+))?\s*:/gm)) {
|
|
78
|
+
const params = normalizeParams(m[3]);
|
|
79
|
+
const ret = (m[4] || '').trim();
|
|
80
|
+
const retStr = ret ? ` → ${ret.slice(0, 30)}` : '';
|
|
81
|
+
const staticKw = m[1] ? 'static ' : '';
|
|
82
|
+
sigs.push(`${indent}${staticKw}func ${m[2]}(${params})${retStr}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
for (const m of stripped.matchAll(/^class\s+(\w+)(?:\s+extends\s+(\w+))?\s*:/gm)) {
|
|
86
|
+
if (addedClasses.has(m[1])) continue;
|
|
87
|
+
addedClasses.add(m[1]);
|
|
88
|
+
sigs.push(m[2] ? `class ${m[1]}(${m[2]})` : `class ${m[1]}`);
|
|
89
|
+
const startIdx = m.index + m[0].length;
|
|
90
|
+
for (const meth of extractInnerMembers(stripped, startIdx)) {
|
|
91
|
+
sigs.push(` ${meth}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return sigs.slice(0, 25);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function extractInnerMembers(stripped, startIndex) {
|
|
99
|
+
const members = [];
|
|
100
|
+
const lines = stripped.slice(startIndex).split('\n');
|
|
101
|
+
for (const line of lines) {
|
|
102
|
+
if (line.trim() === '') continue;
|
|
103
|
+
if (!/^\s+/.test(line)) break;
|
|
104
|
+
const fm = line.match(/^\s+(static\s+)?func\s+(\w+)\s*\(([^)]*)\)(?:\s*->\s*([^:\n]+))?\s*:/);
|
|
105
|
+
if (fm) {
|
|
106
|
+
const params = normalizeParams(fm[3]);
|
|
107
|
+
const ret = (fm[4] || '').trim();
|
|
108
|
+
const retStr = ret ? ` → ${ret.slice(0, 30)}` : '';
|
|
109
|
+
const staticKw = fm[1] ? 'static ' : '';
|
|
110
|
+
members.push(`${staticKw}func ${fm[2]}(${params})${retStr}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return members.slice(0, 6);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function normalizeParams(params) {
|
|
117
|
+
if (!params) return '';
|
|
118
|
+
return params.trim()
|
|
119
|
+
.split(',')
|
|
120
|
+
.map((p) => {
|
|
121
|
+
const part = p.trim();
|
|
122
|
+
if (!part) return '';
|
|
123
|
+
const eqIdx = part.indexOf('=');
|
|
124
|
+
const noDefault = eqIdx !== -1 ? part.slice(0, eqIdx).trim() : part;
|
|
125
|
+
return noDefault.split(':')[0].trim();
|
|
126
|
+
})
|
|
127
|
+
.filter(Boolean)
|
|
128
|
+
.join(', ');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
module.exports = { extract };
|
package/src/map/import-graph.js
CHANGED
package/src/mcp/server.js
CHANGED