hivecode-mcp 0.6.0 → 0.8.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/README.md +68 -137
- package/hive-coord.js +72 -0
- package/hive-mcp.js +85 -10
- package/icr-merge.js +58 -0
- package/icr.js +272 -0
- package/lang-brace.js +174 -0
- package/lang-js.js +418 -0
- package/lang-python.js +149 -0
- package/package.json +18 -5
- package/substrate.js +252 -0
- package/sync.js +711 -19
- package/token.js +20 -0
package/lang-js.js
ADDED
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
// lang-js.js — the JavaScript language provider for ICR.
|
|
2
|
+
//
|
|
3
|
+
// ICR's merge engine (icr.js) is language-agnostic: it merges a list of keyed "units"
|
|
4
|
+
// and asks intent questions ("what names are declared / used / what's a function body").
|
|
5
|
+
// Everything that actually knows about a *language* — how to parse it, what a top-level
|
|
6
|
+
// declaration is, how to find references — lives behind a provider like this one.
|
|
7
|
+
//
|
|
8
|
+
// To add a language (Python, Go, Rust…), write another module exposing the same shape
|
|
9
|
+
// and register it with icr.js's registerLanguage(). This JS provider is just the first
|
|
10
|
+
// plugin; it happens to use `acorn` because acorn is a tiny pure-JS parser. A tree-sitter
|
|
11
|
+
// provider would implement the identical interface over tree-sitter grammars.
|
|
12
|
+
|
|
13
|
+
import * as acorn from 'acorn'
|
|
14
|
+
|
|
15
|
+
// Parse permissively: try module syntax, fall back to script (so plain snippets and
|
|
16
|
+
// ESM both work). Throws only when the code genuinely won't parse either way.
|
|
17
|
+
function parse(src) {
|
|
18
|
+
try { return acorn.parse(src, { ecmaVersion: 'latest', sourceType: 'module' }) }
|
|
19
|
+
catch { return acorn.parse(src, { ecmaVersion: 'latest', sourceType: 'script' }) }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function parses(src) { try { parse(src); return true } catch { return false } }
|
|
23
|
+
|
|
24
|
+
// A stable key per top-level node — what lets us recognize "the same declaration"
|
|
25
|
+
// across two edits. Names where we have them; position as a last resort.
|
|
26
|
+
function keyOf(node, i) {
|
|
27
|
+
let n = node
|
|
28
|
+
if (n.type === 'ExportNamedDeclaration' && n.declaration) n = n.declaration
|
|
29
|
+
if (n.type === 'ExportDefaultDeclaration') return 'export:default'
|
|
30
|
+
if (n.type === 'FunctionDeclaration' && n.id) return 'fn:' + n.id.name
|
|
31
|
+
if (n.type === 'ClassDeclaration' && n.id) return 'class:' + n.id.name
|
|
32
|
+
if (n.type === 'VariableDeclaration' && n.declarations[0] && n.declarations[0].id && n.declarations[0].id.name)
|
|
33
|
+
return 'var:' + n.declarations[0].id.name
|
|
34
|
+
// Key imports by their SOURCE MODULE, so two agents adding imports from DIFFERENT
|
|
35
|
+
// modules never collide, and two touching the SAME module are merged (specifier union).
|
|
36
|
+
if (n.type === 'ImportDeclaration') return 'import:' + (n.source && n.source.value != null ? n.source.value : i)
|
|
37
|
+
return 'stmt:' + i
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Top-level units: ordered { key, text, start, end } for each declaration/statement.
|
|
41
|
+
// start/end are byte offsets in `src` — used by the format-preserving splice so unchanged
|
|
42
|
+
// regions (and the whitespace/comments between units) survive a merge verbatim.
|
|
43
|
+
function units(src) {
|
|
44
|
+
const ast = parse(src)
|
|
45
|
+
return ast.body.map((node, i) => ({ key: keyOf(node, i), text: src.slice(node.start, node.end), start: node.start, end: node.end }))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Bare declared name (foo, Bar, VERSION) of a node, or null for anonymous statements.
|
|
49
|
+
function bareName(node) {
|
|
50
|
+
let n = node
|
|
51
|
+
if (n.type === 'ExportNamedDeclaration' && n.declaration) n = n.declaration
|
|
52
|
+
if (n.type === 'FunctionDeclaration' && n.id) return n.id.name
|
|
53
|
+
if (n.type === 'ClassDeclaration' && n.id) return n.id.name
|
|
54
|
+
if (n.type === 'VariableDeclaration' && n.declarations[0] && n.declarations[0].id && n.declarations[0].id.name) return n.declarations[0].id.name
|
|
55
|
+
return null
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function declaredNames(src) {
|
|
59
|
+
const s = new Set()
|
|
60
|
+
for (const node of parse(src).body) {
|
|
61
|
+
const nm = bareName(node); if (nm) s.add(nm)
|
|
62
|
+
// Import locals count as declared names too — so if an import is ever dropped and its
|
|
63
|
+
// binding is still used, the dangling-reference check catches it (safety net for the
|
|
64
|
+
// import merge). Keeps the never-emit-broken-code guarantee honest for imports.
|
|
65
|
+
if (node.type === 'ImportDeclaration') for (const sp of node.specifiers) s.add(sp.local.name)
|
|
66
|
+
}
|
|
67
|
+
return s
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// --- import-aware merging -------------------------------------------------------
|
|
71
|
+
// Parse a single import statement into its parts, or null if it isn't one.
|
|
72
|
+
function parseImport(src) {
|
|
73
|
+
let n
|
|
74
|
+
try { n = parse(src).body[0] } catch { return null }
|
|
75
|
+
if (!n || n.type !== 'ImportDeclaration') return null
|
|
76
|
+
let def = null, ns = null
|
|
77
|
+
const named = []
|
|
78
|
+
for (const sp of n.specifiers) {
|
|
79
|
+
if (sp.type === 'ImportDefaultSpecifier') def = sp.local.name
|
|
80
|
+
else if (sp.type === 'ImportNamespaceSpecifier') ns = sp.local.name
|
|
81
|
+
else named.push({ imported: sp.imported.name != null ? sp.imported.name : sp.imported.value, local: sp.local.name })
|
|
82
|
+
}
|
|
83
|
+
return { source: n.source.value, def, ns, named }
|
|
84
|
+
}
|
|
85
|
+
function renderImport({ source, def, ns, named }) {
|
|
86
|
+
const parts = []
|
|
87
|
+
if (def) parts.push(def)
|
|
88
|
+
if (ns) parts.push('* as ' + ns)
|
|
89
|
+
if (named.length) parts.push('{ ' + named.map((s) => s.imported === s.local ? s.local : s.imported + ' as ' + s.local).join(', ') + ' }')
|
|
90
|
+
if (!parts.length) return "import '" + source + "'"
|
|
91
|
+
return 'import ' + parts.join(', ') + " from '" + source + "'"
|
|
92
|
+
}
|
|
93
|
+
// Both sides changed an import from the SAME module → union their specifiers into one
|
|
94
|
+
// statement (the common "both agents added an import" case). Returns null — meaning
|
|
95
|
+
// "let the normal conflict path handle it" — for anything we can't safely combine
|
|
96
|
+
// (different modules, clashing defaults/namespaces, namespace-mixed-with-named).
|
|
97
|
+
function mergeUnit(baseText, aText, bText) {
|
|
98
|
+
const a = parseImport(aText), b = parseImport(bText)
|
|
99
|
+
if (!a || !b || a.source !== b.source) return null
|
|
100
|
+
let def
|
|
101
|
+
if (a.def && b.def) { if (a.def !== b.def) return null; def = a.def } else def = a.def || b.def
|
|
102
|
+
let ns
|
|
103
|
+
if (a.ns && b.ns) { if (a.ns !== b.ns) return null; ns = a.ns } else ns = a.ns || b.ns
|
|
104
|
+
if (ns && (a.named.length || b.named.length)) return null // `* as X` can't share a statement with named imports
|
|
105
|
+
const seen = new Set(), named = []
|
|
106
|
+
for (const s of [...a.named, ...b.named]) { const k = s.imported + '|' + s.local; if (!seen.has(k)) { seen.add(k); named.push(s) } }
|
|
107
|
+
return renderImport({ source: a.source, def, ns, named })
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// The BODY of a top-level declaration, by name, with the name itself excluded — so two
|
|
111
|
+
// declarations that differ only in their name compare equal (that's how we spot a rename).
|
|
112
|
+
function declBody(src, name) {
|
|
113
|
+
for (const node of parse(src).body) {
|
|
114
|
+
if (bareName(node) !== name) continue
|
|
115
|
+
let n = node
|
|
116
|
+
if (n.type === 'ExportNamedDeclaration' && n.declaration) n = n.declaration
|
|
117
|
+
if ((n.type === 'FunctionDeclaration' || n.type === 'ClassDeclaration') && n.body)
|
|
118
|
+
return src.slice(n.body.start, n.body.end)
|
|
119
|
+
if (n.type === 'VariableDeclaration' && n.declarations[0] && n.declarations[0].init) {
|
|
120
|
+
const init = n.declarations[0].init
|
|
121
|
+
return src.slice(init.start, init.end)
|
|
122
|
+
}
|
|
123
|
+
return null
|
|
124
|
+
}
|
|
125
|
+
return null
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Rewrite every reference to `oldName` as `newName` (skipping property/key positions,
|
|
129
|
+
// which aren't references to the declaration). Edits back-to-front to keep offsets valid.
|
|
130
|
+
function renameRefs(src, oldName, newName) {
|
|
131
|
+
const spots = []
|
|
132
|
+
walk(parse(src), (node, parent, key) => {
|
|
133
|
+
if (node.type !== 'Identifier' || node.name !== oldName) return
|
|
134
|
+
if (parent && parent.type === 'MemberExpression' && key === 'property' && !parent.computed) return
|
|
135
|
+
if (parent && (parent.type === 'Property' || parent.type === 'PropertyDefinition') && key === 'key' && !parent.computed) return
|
|
136
|
+
spots.push([node.start, node.end])
|
|
137
|
+
})
|
|
138
|
+
spots.sort((a, b) => b[0] - a[0])
|
|
139
|
+
let out = src
|
|
140
|
+
for (const [s, e] of spots) out = out.slice(0, s) + newName + out.slice(e)
|
|
141
|
+
return out
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Every identifier USED anywhere (skips obj.prop names and non-computed object keys).
|
|
145
|
+
// Approximate — good enough for the intent check; the real version is scope-aware.
|
|
146
|
+
function usedIdentifiers(src) {
|
|
147
|
+
const used = new Set()
|
|
148
|
+
walk(parse(src), (node, parent, key) => {
|
|
149
|
+
if (node.type !== 'Identifier') return
|
|
150
|
+
if (parent && parent.type === 'MemberExpression' && key === 'property' && !parent.computed) return
|
|
151
|
+
if (parent && (parent.type === 'Property' || parent.type === 'PropertyDefinition') && key === 'key' && !parent.computed) return
|
|
152
|
+
used.add(node.name)
|
|
153
|
+
})
|
|
154
|
+
return used
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function walk(node, visit, parent = null, key = null) {
|
|
158
|
+
if (!node || typeof node.type !== 'string') return
|
|
159
|
+
visit(node, parent, key)
|
|
160
|
+
for (const k of Object.keys(node)) {
|
|
161
|
+
if (k === 'type' || k === 'start' || k === 'end' || k === 'loc') continue
|
|
162
|
+
const v = node[k]
|
|
163
|
+
if (Array.isArray(v)) { for (const c of v) walk(c, visit, node, k) }
|
|
164
|
+
else if (v && typeof v.type === 'string') walk(v, visit, node, k)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// --- scope-aware reference analysis ---------------------------------------------
|
|
169
|
+
// The names REFERENCED in `src` that resolve to NOTHING in the file's own scopes —
|
|
170
|
+
// i.e. free/global names, including any use of a top-level declaration. This is the
|
|
171
|
+
// scope-aware upgrade of usedIdentifiers: a use of `x` that resolves to a LOCAL binding
|
|
172
|
+
// (a param, a `const x` in the same function, a catch var, a loop var…) is NOT counted,
|
|
173
|
+
// because it isn't a reference to a top-level declaration. That lets ICR tell a deleted
|
|
174
|
+
// top-level `helper` apart from an unrelated local `helper` that merely shares its name.
|
|
175
|
+
//
|
|
176
|
+
// Honest scope coverage: functions (declaration/expression/arrow), blocks, catch
|
|
177
|
+
// clauses, for/for-in/for-of loop bindings, params with simple destructuring, imports,
|
|
178
|
+
// var-hoisting to the function scope, and let/const/class/function block binding.
|
|
179
|
+
// Approximations (rare, and they only ever make the dangling check MORE cautious):
|
|
180
|
+
// default-value expressions in patterns and `with`/eval are not modeled.
|
|
181
|
+
// Walk the AST and call emit(identifierNode) for every reference that resolves to NO
|
|
182
|
+
// local binding (free / would resolve to module or global scope). Both the dangling-ref
|
|
183
|
+
// check and scope-aware renaming are built on this single traversal.
|
|
184
|
+
function walkFreeRefs(src, emit) { resolveScopes(parse(src), null, emit) }
|
|
185
|
+
|
|
186
|
+
function referencedFreeNames(src) {
|
|
187
|
+
const free = new Set()
|
|
188
|
+
walkFreeRefs(src, (node) => free.add(node.name))
|
|
189
|
+
return free
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Rename ONLY the free references to oldName (those that resolve to the top-level/global
|
|
193
|
+
// binding) — never a local variable that merely shares the name. Edits back-to-front.
|
|
194
|
+
function renameFreeRefs(src, oldName, newName) {
|
|
195
|
+
const spots = []
|
|
196
|
+
walkFreeRefs(src, (node) => { if (node.name === oldName) spots.push([node.start, node.end]) })
|
|
197
|
+
spots.sort((a, b) => b[0] - a[0])
|
|
198
|
+
let out = src
|
|
199
|
+
for (const [s, e] of spots) out = out.slice(0, s) + newName + out.slice(e)
|
|
200
|
+
return out
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const CHILD_SKIP = new Set(['type', 'start', 'end', 'loc', 'range'])
|
|
204
|
+
function eachChild(node, fn) {
|
|
205
|
+
for (const k of Object.keys(node)) {
|
|
206
|
+
if (CHILD_SKIP.has(k)) continue
|
|
207
|
+
const v = node[k]
|
|
208
|
+
if (Array.isArray(v)) { for (const c of v) if (c && typeof c.type === 'string') fn(c) }
|
|
209
|
+
else if (v && typeof v.type === 'string') fn(v)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Names introduced by a binding target (handles identifiers + simple destructuring).
|
|
214
|
+
function bindingNames(node, out) {
|
|
215
|
+
if (!node) return
|
|
216
|
+
switch (node.type) {
|
|
217
|
+
case 'Identifier': out.add(node.name); break
|
|
218
|
+
case 'ObjectPattern': for (const p of node.properties) bindingNames(p.type === 'RestElement' ? p.argument : p.value, out); break
|
|
219
|
+
case 'ArrayPattern': for (const el of node.elements) if (el) bindingNames(el, out); break
|
|
220
|
+
case 'AssignmentPattern': bindingNames(node.left, out); break
|
|
221
|
+
case 'RestElement': bindingNames(node.argument, out); break
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// A binding target's NAMES are bindings, but its default values (`a = expr`) and computed
|
|
226
|
+
// destructuring keys (`{ [k]: v }`) are REFERENCES that must be resolved against the scope.
|
|
227
|
+
// Without this, a name used only in a default value would be missed by the dangling check.
|
|
228
|
+
function resolveBindingDefaults(node, scope, emit) {
|
|
229
|
+
if (!node) return
|
|
230
|
+
switch (node.type) {
|
|
231
|
+
case 'AssignmentPattern': resolveBindingDefaults(node.left, scope, emit); resolveScopes(node.right, scope, emit); break
|
|
232
|
+
case 'ObjectPattern': for (const p of node.properties) {
|
|
233
|
+
if (p.type === 'RestElement') { resolveBindingDefaults(p.argument, scope, emit); break }
|
|
234
|
+
if (p.computed) resolveScopes(p.key, scope, emit)
|
|
235
|
+
resolveBindingDefaults(p.value, scope, emit)
|
|
236
|
+
} break
|
|
237
|
+
case 'ArrayPattern': for (const el of node.elements) if (el) resolveBindingDefaults(el, scope, emit); break
|
|
238
|
+
case 'RestElement': resolveBindingDefaults(node.argument, scope, emit); break
|
|
239
|
+
// Identifier: a pure binding — nothing to resolve.
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const makeScope = (parent, fnScope) => { const s = { vars: new Set(), parent }; s.fnScope = fnScope || s; return s }
|
|
244
|
+
const resolves = (scope, name) => { for (let s = scope; s; s = s.parent) if (s.vars.has(name)) return true; return false }
|
|
245
|
+
|
|
246
|
+
// Hoist var declarations + function declarations into the function scope (deep, but not
|
|
247
|
+
// crossing into nested functions, which own their own var scope).
|
|
248
|
+
function hoistFunctionScope(nodes, scope) { for (const n of nodes) collectHoisted(n, scope) }
|
|
249
|
+
function collectHoisted(node, scope) {
|
|
250
|
+
if (!node || typeof node.type !== 'string') return
|
|
251
|
+
if (node.type === 'FunctionDeclaration') { if (node.id) scope.fnScope.vars.add(node.id.name); return }
|
|
252
|
+
if (/Function(Expression)?$|ArrowFunctionExpression/.test(node.type)) return // nested function: stop
|
|
253
|
+
if (node.type === 'VariableDeclaration' && node.kind === 'var')
|
|
254
|
+
for (const d of node.declarations) bindingNames(d.id, scope.fnScope.vars)
|
|
255
|
+
eachChild(node, (c) => collectHoisted(c, scope))
|
|
256
|
+
}
|
|
257
|
+
// Bind let/const/class/function/import names declared directly in a block/program.
|
|
258
|
+
function hoistBlock(nodes, scope) {
|
|
259
|
+
for (const n of nodes) {
|
|
260
|
+
if (n.type === 'VariableDeclaration' && (n.kind === 'let' || n.kind === 'const'))
|
|
261
|
+
for (const d of n.declarations) bindingNames(d.id, scope.vars)
|
|
262
|
+
else if (n.type === 'ClassDeclaration' && n.id) scope.vars.add(n.id.name)
|
|
263
|
+
else if (n.type === 'FunctionDeclaration' && n.id) scope.vars.add(n.id.name)
|
|
264
|
+
else if (n.type === 'ImportDeclaration') for (const sp of n.specifiers) scope.vars.add(sp.local.name)
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function resolveScopes(node, scope, emit) {
|
|
269
|
+
if (!node || typeof node.type !== 'string') return
|
|
270
|
+
switch (node.type) {
|
|
271
|
+
case 'Program': {
|
|
272
|
+
const s = makeScope(null, null)
|
|
273
|
+
hoistFunctionScope(node.body, s); hoistBlock(node.body, s)
|
|
274
|
+
for (const c of node.body) resolveScopes(c, s, emit)
|
|
275
|
+
return
|
|
276
|
+
}
|
|
277
|
+
case 'FunctionDeclaration': case 'FunctionExpression': case 'ArrowFunctionExpression': {
|
|
278
|
+
const s = makeScope(scope, null)
|
|
279
|
+
for (const p of node.params) bindingNames(p, s.vars)
|
|
280
|
+
for (const p of node.params) resolveBindingDefaults(p, s, emit) // default values are references
|
|
281
|
+
if (node.id && node.type === 'FunctionExpression') s.vars.add(node.id.name) // named fn expr
|
|
282
|
+
if (node.body.type === 'BlockStatement') {
|
|
283
|
+
hoistFunctionScope(node.body.body, s); hoistBlock(node.body.body, s)
|
|
284
|
+
for (const c of node.body.body) resolveScopes(c, s, emit)
|
|
285
|
+
} else resolveScopes(node.body, s, emit) // arrow with expression body
|
|
286
|
+
return
|
|
287
|
+
}
|
|
288
|
+
case 'BlockStatement': {
|
|
289
|
+
const s = makeScope(scope, scope.fnScope); hoistBlock(node.body, s)
|
|
290
|
+
for (const c of node.body) resolveScopes(c, s, emit)
|
|
291
|
+
return
|
|
292
|
+
}
|
|
293
|
+
case 'CatchClause': {
|
|
294
|
+
const s = makeScope(scope, scope.fnScope)
|
|
295
|
+
if (node.param) bindingNames(node.param, s.vars)
|
|
296
|
+
resolveScopes(node.body, s, emit)
|
|
297
|
+
return
|
|
298
|
+
}
|
|
299
|
+
case 'ForStatement': {
|
|
300
|
+
const s = makeScope(scope, scope.fnScope)
|
|
301
|
+
if (node.init && node.init.type === 'VariableDeclaration') hoistBlock([node.init], s)
|
|
302
|
+
for (const k of ['init', 'test', 'update', 'body']) if (node[k]) resolveScopes(node[k], s, emit)
|
|
303
|
+
return
|
|
304
|
+
}
|
|
305
|
+
case 'ForInStatement': case 'ForOfStatement': {
|
|
306
|
+
const s = makeScope(scope, scope.fnScope)
|
|
307
|
+
if (node.left.type === 'VariableDeclaration') hoistBlock([node.left], s)
|
|
308
|
+
else resolveScopes(node.left, s, emit)
|
|
309
|
+
resolveScopes(node.right, s, emit); resolveScopes(node.body, s, emit)
|
|
310
|
+
return
|
|
311
|
+
}
|
|
312
|
+
case 'VariableDeclarator': { resolveBindingDefaults(node.id, scope, emit); if (node.init) resolveScopes(node.init, scope, emit); return } // id is a binding (defaults are refs)
|
|
313
|
+
case 'Identifier': { if (!resolves(scope, node.name)) emit(node); return }
|
|
314
|
+
case 'MemberExpression': { resolveScopes(node.object, scope, emit); if (node.computed) resolveScopes(node.property, scope, emit); return }
|
|
315
|
+
case 'Property': { if (node.computed) resolveScopes(node.key, scope, emit); resolveScopes(node.value, scope, emit); return }
|
|
316
|
+
case 'PropertyDefinition': case 'MethodDefinition': { if (node.computed) resolveScopes(node.key, scope, emit); if (node.value) resolveScopes(node.value, scope, emit); return }
|
|
317
|
+
case 'LabeledStatement': { resolveScopes(node.body, scope, emit); return }
|
|
318
|
+
case 'BreakStatement': case 'ContinueStatement': return // labels aren't value references
|
|
319
|
+
case 'ExportSpecifier': { resolveScopes(node.local, scope, emit); return }
|
|
320
|
+
case 'ImportSpecifier': case 'ImportDefaultSpecifier': case 'ImportNamespaceSpecifier': return // binding positions
|
|
321
|
+
default: eachChild(node, (c) => resolveScopes(c, scope, emit))
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// A stable key for a class member, so two agents editing DIFFERENT methods of the same
|
|
326
|
+
// class merge, while editing the SAME method conflicts.
|
|
327
|
+
function memberKey(m, i) {
|
|
328
|
+
const kind = m.type === 'MethodDefinition' ? (m.kind === 'constructor' ? 'ctor' : 'method') : 'field'
|
|
329
|
+
if (m.key && m.key.type === 'Identifier' && !m.computed)
|
|
330
|
+
return kind + ':' + (m.static ? 'static.' : '') + m.key.name
|
|
331
|
+
return 'member:' + i
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// For finer-grained merging: split a single declaration into its signature text + keyed
|
|
335
|
+
// inner units, plus how to reassemble them. Handles FUNCTIONS (body statements, keyed
|
|
336
|
+
// like top-level decls) and CLASSES (members, keyed by method/field name). Returns null
|
|
337
|
+
// for anything else (the engine then treats the declaration as indivisible). Tolerates
|
|
338
|
+
// fragments that don't parse standalone (e.g. a bare `return`).
|
|
339
|
+
const MEMBER_WRAP = 'class __ICR__{'
|
|
340
|
+
// A class member (method) doesn't parse standalone — wrap it in a throwaway class so we
|
|
341
|
+
// can recurse INTO a method body that both agents edited. Offsets are mapped back to src.
|
|
342
|
+
function splitMember(src) {
|
|
343
|
+
let ast
|
|
344
|
+
try { ast = parse(MEMBER_WRAP + src + '}') } catch { return null }
|
|
345
|
+
const cls = ast.body[0]
|
|
346
|
+
if (!cls || cls.type !== 'ClassDeclaration' || !cls.body.body.length) return null
|
|
347
|
+
const m = cls.body.body[0]
|
|
348
|
+
if (!m || m.type !== 'MethodDefinition' || !m.value || !m.value.body || m.value.body.type !== 'BlockStatement') return null
|
|
349
|
+
const off = MEMBER_WRAP.length, body = m.value.body
|
|
350
|
+
const sig = src.slice(0, body.start - off)
|
|
351
|
+
const units = body.body.map((s, i) => ({ key: keyOf(s, i), text: src.slice(s.start - off, s.end - off) }))
|
|
352
|
+
return { sig, units, open: '{\n ', join: '\n ', close: '\n}' }
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function splitUnit(src) {
|
|
356
|
+
let ast
|
|
357
|
+
try { ast = parse(src) } catch { return splitMember(src) } // maybe a class-member fragment
|
|
358
|
+
let n = ast.body[0]
|
|
359
|
+
if (!n) return null
|
|
360
|
+
if (n.type === 'ExportNamedDeclaration' && n.declaration) n = n.declaration
|
|
361
|
+
if (n.type === 'FunctionDeclaration' && n.body) {
|
|
362
|
+
const sig = src.slice(n.start, n.body.start)
|
|
363
|
+
const units = n.body.body.map((s, i) => ({ key: keyOf(s, i), text: src.slice(s.start, s.end) }))
|
|
364
|
+
return { sig, units, open: '{\n ', join: '\n ', close: '\n}' }
|
|
365
|
+
}
|
|
366
|
+
if (n.type === 'ClassDeclaration' && n.body) {
|
|
367
|
+
const sig = src.slice(n.start, n.body.start)
|
|
368
|
+
const units = n.body.body.map((m, i) => ({ key: memberKey(m, i), text: src.slice(m.start, m.end) }))
|
|
369
|
+
return { sig, units, open: '{\n ', join: '\n\n ', close: '\n}' }
|
|
370
|
+
}
|
|
371
|
+
// `const x = { ... }` / `const x = () => { ... }` / `const x = function () { ... }`
|
|
372
|
+
if (n.type === 'VariableDeclaration' && n.declarations.length === 1 && n.declarations[0].init) {
|
|
373
|
+
const init = n.declarations[0].init
|
|
374
|
+
if (init.type === 'ObjectExpression') {
|
|
375
|
+
const sig = src.slice(n.start, init.start)
|
|
376
|
+
const units = init.properties.map((p, i) => ({ key: propKey(p, i), text: src.slice(p.start, p.end) }))
|
|
377
|
+
return { sig, units, open: '{\n ', join: ',\n ', close: '\n}' }
|
|
378
|
+
}
|
|
379
|
+
if ((init.type === 'ArrowFunctionExpression' || init.type === 'FunctionExpression') && init.body && init.body.type === 'BlockStatement') {
|
|
380
|
+
const sig = src.slice(n.start, init.body.start)
|
|
381
|
+
const units = init.body.body.map((s, i) => ({ key: keyOf(s, i), text: src.slice(s.start, s.end) }))
|
|
382
|
+
return { sig, units, open: '{\n ', join: '\n ', close: '\n}' }
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return null
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Stable key for an object-literal property, so two agents adding DIFFERENT keys union.
|
|
389
|
+
function propKey(p, i) {
|
|
390
|
+
if (p.type === 'SpreadElement') return 'spread:' + i
|
|
391
|
+
if (p.key && p.key.type === 'Identifier' && !p.computed) return 'prop:' + p.key.name
|
|
392
|
+
if (p.key && p.key.type === 'Literal') return 'prop:' + String(p.key.value)
|
|
393
|
+
return 'prop:' + i
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Does `src` parse either as top-level code OR as a class member? (The inner-merge result
|
|
397
|
+
// for a method is a fragment that's only valid inside a class — this lets us validate it.)
|
|
398
|
+
function parsesUnit(src) {
|
|
399
|
+
if (parses(src)) return true
|
|
400
|
+
try { parse(MEMBER_WRAP + src + '}'); return true } catch { return false }
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// The provider contract every ICR language module implements.
|
|
404
|
+
export const javascript = {
|
|
405
|
+
id: 'javascript',
|
|
406
|
+
exts: ['.js', '.mjs', '.cjs'],
|
|
407
|
+
parses,
|
|
408
|
+
units,
|
|
409
|
+
declaredNames,
|
|
410
|
+
usedIdentifiers, // approximate (kept for reference)
|
|
411
|
+
referencedFreeNames, // scope-aware — what the engine prefers for the dangling check
|
|
412
|
+
declBody,
|
|
413
|
+
renameRefs, // rewrites every matching identifier (kept for reference)
|
|
414
|
+
renameFreeRefs, // scope-aware — only rewrites references that resolve to the binding
|
|
415
|
+
splitUnit, // finer granularity: split a function/class/method into keyed inner units
|
|
416
|
+
parsesUnit, // validity oracle that also accepts a class-member fragment
|
|
417
|
+
mergeUnit, // language-specific same-key merge (import specifier union)
|
|
418
|
+
}
|
package/lang-python.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// Python language provider for ICR — indentation-based structural splitter.
|
|
2
|
+
// Implements the ICR provider contract { id, exts, parses, units, declaredNames,
|
|
3
|
+
// splitUnit, parsesUnit } so the shared merge engine merges Python by structure:
|
|
4
|
+
// top-level defs/classes/statements are units keyed by name; editing different
|
|
5
|
+
// methods of the same class merges via splitUnit. Heuristic (tracks #-comments,
|
|
6
|
+
// triple-quoted and normal strings, bracket depth) — not a full parser; parses()
|
|
7
|
+
// is an approximate balanced-delimiter check.
|
|
8
|
+
|
|
9
|
+
function scannerAt(src) {
|
|
10
|
+
const n = src.length
|
|
11
|
+
return function at(i) {
|
|
12
|
+
if (src[i] === '#') { let j = i + 1; while (j < n && src[j] !== '\n') j++; return j }
|
|
13
|
+
for (const q of ['"""', "'''"]) if (src.startsWith(q, i)) { let j = i + 3; while (j < n && !src.startsWith(q, j)) { if (src[j] === '\\') j++; j++ } return Math.min(n, j + 3) }
|
|
14
|
+
if (src[i] === '"' || src[i] === "'") { const q = src[i]; let j = i + 1; while (j < n) { if (src[j] === '\\') { j += 2; continue } if (src[j] === q) { j++; break } if (src[j] === '\n') break; j++ } return j }
|
|
15
|
+
return -1
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Line records: { start, end (incl newline), indent, blank, text }
|
|
20
|
+
function lines(src) {
|
|
21
|
+
const out = []
|
|
22
|
+
let i = 0
|
|
23
|
+
while (i < src.length) {
|
|
24
|
+
let e = src.indexOf('\n', i); if (e < 0) e = src.length; else e = e + 1
|
|
25
|
+
const raw = src.slice(i, e)
|
|
26
|
+
const trimmed = raw.replace(/\r?\n$/, '')
|
|
27
|
+
let indent = 0; while (indent < trimmed.length && (trimmed[indent] === ' ' || trimmed[indent] === '\t')) indent++
|
|
28
|
+
const blank = trimmed.trim() === '' || trimmed.trim().startsWith('#')
|
|
29
|
+
out.push({ start: i, end: e, indent, blank, text: raw })
|
|
30
|
+
i = e
|
|
31
|
+
}
|
|
32
|
+
return out
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const HEAD = /^\s*(?:async\s+def|def)\s+([A-Za-z_]\w*)/
|
|
36
|
+
const CLS = /^\s*class\s+([A-Za-z_]\w*)/
|
|
37
|
+
|
|
38
|
+
function keyOf(text) {
|
|
39
|
+
let m = text.match(HEAD); if (m) return 'def:' + m[1]
|
|
40
|
+
m = text.match(CLS); if (m) return 'class:' + m[1]
|
|
41
|
+
return 'stmt:' + text.split('\n')[0].trim().replace(/\s+/g, ' ')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Split src into units whose heads sit at exactly `base` indentation. A def/class
|
|
45
|
+
// head owns the following blank + more-indented lines (its body). A run of leading
|
|
46
|
+
// @decorator lines attaches to the head that follows. Everything else is a
|
|
47
|
+
// one-logical-line statement (continued while bracket depth > 0).
|
|
48
|
+
function blocks(src, base = 0) {
|
|
49
|
+
const L = lines(src)
|
|
50
|
+
const at = scannerAt(src)
|
|
51
|
+
const units = []
|
|
52
|
+
let i = 0
|
|
53
|
+
// find bracket depth over a line range (to keep continued statements together)
|
|
54
|
+
const bracketDelta = (s, e) => {
|
|
55
|
+
let d = 0, k = s
|
|
56
|
+
while (k < e) { const j = at(k); if (j > k) { k = j; continue } const c = src[k]; if (c === '(' || c === '[' || c === '{') d++; else if (c === ')' || c === ']' || c === '}') d--; k++ }
|
|
57
|
+
return d
|
|
58
|
+
}
|
|
59
|
+
while (i < L.length) {
|
|
60
|
+
while (i < L.length && L[i].blank) i++ // skip blank/comment gap lines
|
|
61
|
+
if (i >= L.length) break
|
|
62
|
+
if (L[i].indent !== base) { // stray deeper line (shouldn't happen at top) — treat as its own stmt
|
|
63
|
+
const s = L[i].start; let e = L[i].end; i++
|
|
64
|
+
units.push({ start: s, end: e })
|
|
65
|
+
continue
|
|
66
|
+
}
|
|
67
|
+
const start = L[i].start
|
|
68
|
+
// consume leading decorators
|
|
69
|
+
let isDecorator = /^\s*@/.test(L[i].text)
|
|
70
|
+
while (i < L.length && L[i].indent === base && /^\s*@/.test(L[i].text)) i++
|
|
71
|
+
const headIdx = i
|
|
72
|
+
const head = i < L.length ? L[i] : null
|
|
73
|
+
if (head && (HEAD.test(head.text) || CLS.test(head.text) || isDecorator)) {
|
|
74
|
+
// a block: consume the header line, then its body (blank or deeper-indented lines)
|
|
75
|
+
i++ // header line
|
|
76
|
+
while (i < L.length && (L[i].blank || L[i].indent > base)) i++
|
|
77
|
+
units.push({ start, end: L[i - 1].end })
|
|
78
|
+
} else if (head) {
|
|
79
|
+
// simple statement: consume continued lines while bracket depth remains open
|
|
80
|
+
let e = head.end, depth = bracketDelta(head.start, head.end); i++
|
|
81
|
+
while (i < L.length && (depth > 0 || /\\\s*$/.test(L[i - 1].text.replace(/\r?\n$/, '')))) { depth += bracketDelta(L[i].start, L[i].end); e = L[i].end; i++ }
|
|
82
|
+
units.push({ start, end: e })
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return units
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function keyed(raw, src) {
|
|
89
|
+
const seen = new Map(), out = []
|
|
90
|
+
for (const u of raw) {
|
|
91
|
+
const text = src.slice(u.start, u.end)
|
|
92
|
+
let k = keyOf(text)
|
|
93
|
+
const c = (seen.get(k) || 0) + 1; seen.set(k, c)
|
|
94
|
+
if (c > 1) k += '#' + c
|
|
95
|
+
out.push({ key: k, text, start: u.start, end: u.end })
|
|
96
|
+
}
|
|
97
|
+
return out
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function balanced(src) {
|
|
101
|
+
const at = scannerAt(src)
|
|
102
|
+
const stack = []
|
|
103
|
+
const pair = { ')': '(', ']': '[', '}': '{' }
|
|
104
|
+
let i = 0
|
|
105
|
+
while (i < src.length) {
|
|
106
|
+
const j = at(i); if (j > i) { if (j > src.length) return false; i = j; continue }
|
|
107
|
+
const c = src[i]
|
|
108
|
+
if (c === '(' || c === '[' || c === '{') stack.push(c)
|
|
109
|
+
else if (pair[c]) { if (stack.pop() !== pair[c]) return false }
|
|
110
|
+
i++
|
|
111
|
+
}
|
|
112
|
+
return stack.length === 0
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export const python = {
|
|
116
|
+
id: 'python',
|
|
117
|
+
exts: ['.py', '.pyi'],
|
|
118
|
+
parses: (src) => balanced(String(src == null ? '' : src)),
|
|
119
|
+
parsesUnit: (src) => balanced(String(src)),
|
|
120
|
+
units: (src) => keyed(blocks(String(src), 0), String(src)),
|
|
121
|
+
declaredNames: (src) => {
|
|
122
|
+
const names = new Set()
|
|
123
|
+
for (const u of keyed(blocks(String(src), 0), String(src))) {
|
|
124
|
+
const m = u.key.match(/^(?:def|class):([A-Za-z_]\w*)/)
|
|
125
|
+
if (m) names.add(m[1])
|
|
126
|
+
}
|
|
127
|
+
return names
|
|
128
|
+
},
|
|
129
|
+
// Descend into a class/def body so two agents editing DIFFERENT methods merge.
|
|
130
|
+
splitUnit: (text) => {
|
|
131
|
+
const src = String(text)
|
|
132
|
+
const L = lines(src)
|
|
133
|
+
let h = 0; while (h < L.length && (L[h].blank || /^\s*@/.test(L[h].text))) h++
|
|
134
|
+
if (h >= L.length) return null
|
|
135
|
+
// Only descend into a CLASS (named methods → mergeable). A def body is bare
|
|
136
|
+
// statements with no stable keys; descending would falsely merge two edits to the
|
|
137
|
+
// same function, so we don't — same-def edits become a clean conflict instead.
|
|
138
|
+
if (!CLS.test(L[h].text)) return null
|
|
139
|
+
const headEnd = L[h].end
|
|
140
|
+
// body = everything after the header line; its base indent is the first body line's indent
|
|
141
|
+
let b = h + 1; while (b < L.length && L[b].blank) b++
|
|
142
|
+
if (b >= L.length) return null
|
|
143
|
+
const bodyIndent = L[b].indent
|
|
144
|
+
const sig = src.slice(0, headEnd)
|
|
145
|
+
const body = src.slice(headEnd)
|
|
146
|
+
const inner = keyed(blocks(body, bodyIndent), body)
|
|
147
|
+
return { sig, open: '', close: '', body, units: inner, join: '' }
|
|
148
|
+
},
|
|
149
|
+
}
|
package/package.json
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hivecode-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "MCP server that lets any AI coding agent join a Hivecode room — humans and multiple AI agents edit one live codebase with folder-scoped permissions and approval gates.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
|
-
"homepage": "https://github.com/
|
|
7
|
+
"homepage": "https://github.com/Ganesh-Kute/HiveCode",
|
|
8
8
|
"bugs": {
|
|
9
|
-
"url": "https://github.com/
|
|
9
|
+
"url": "https://github.com/Ganesh-Kute/HiveCode/issues"
|
|
10
10
|
},
|
|
11
11
|
"repository": {
|
|
12
12
|
"type": "git",
|
|
13
|
-
"url": "https://github.com/
|
|
13
|
+
"url": "https://github.com/Ganesh-Kute/HiveCode"
|
|
14
14
|
},
|
|
15
15
|
"keywords": [
|
|
16
16
|
"mcp",
|
|
@@ -30,6 +30,13 @@
|
|
|
30
30
|
"sync.js",
|
|
31
31
|
"core.js",
|
|
32
32
|
"token.js",
|
|
33
|
+
"icr.js",
|
|
34
|
+
"lang-js.js",
|
|
35
|
+
"lang-brace.js",
|
|
36
|
+
"lang-python.js",
|
|
37
|
+
"icr-merge.js",
|
|
38
|
+
"hive-coord.js",
|
|
39
|
+
"substrate.js",
|
|
33
40
|
"HIVE_RULES.md",
|
|
34
41
|
"MCP.md",
|
|
35
42
|
"README.md",
|
|
@@ -41,7 +48,12 @@
|
|
|
41
48
|
"scripts": {
|
|
42
49
|
"start": "node server.js",
|
|
43
50
|
"relay": "node server.js",
|
|
44
|
-
"test": "node test.js"
|
|
51
|
+
"test": "node test.js",
|
|
52
|
+
"test:substrate": "node substrate-test.js",
|
|
53
|
+
"test:substrate:live": "node substrate-live-test.js",
|
|
54
|
+
"test:substrate:enforce": "node substrate-enforce-test.js",
|
|
55
|
+
"test:substrate:authority": "node substrate-authority-test.js",
|
|
56
|
+
"test:substrate:all": "node substrate-test.js && node substrate-live-test.js && node substrate-enforce-test.js && node substrate-authority-test.js"
|
|
45
57
|
},
|
|
46
58
|
"engines": {
|
|
47
59
|
"node": ">=18"
|
|
@@ -49,6 +61,7 @@
|
|
|
49
61
|
"dependencies": {
|
|
50
62
|
"@anthropic-ai/sdk": "^0.104.2",
|
|
51
63
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
64
|
+
"acorn": "^8.17.0",
|
|
52
65
|
"ignore": "^7.0.5",
|
|
53
66
|
"ws": "^8.18.0",
|
|
54
67
|
"y-websocket": "^2.0.4",
|