hivecode-mcp 0.14.1 → 0.14.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/HIVE_RULES.md +98 -98
- package/LICENSE +21 -21
- package/README.md +421 -421
- package/core.js +91 -91
- package/docs/ALWAYS_ON.md +310 -310
- package/docs/GITHUB_WORKFLOW.md +394 -394
- package/docs/HIVE_RULES.md +41 -41
- package/docs/MCP.md +157 -157
- package/docs/QUICKSTART.md +266 -266
- package/docs/SELF_HOSTING.md +256 -256
- package/docs/SLACK_SETUP.md +474 -474
- package/docs/TROUBLESHOOTING.md +106 -105
- package/hive-activity.js +336 -336
- package/hive-authors.js +46 -44
- package/hive-await.js +230 -230
- package/hive-branch.js +674 -607
- package/hive-busy.js +88 -88
- package/hive-coord.js +110 -110
- package/hive-gate.js +135 -135
- package/hive-github.js +3 -1
- package/hive-hook-install.js +179 -179
- package/hive-instructions.js +74 -74
- package/hive-land.js +709 -691
- package/hive-listen.js +81 -81
- package/hive-mailbox.js +520 -520
- package/hive-mcp-http.js +591 -591
- package/hive-mcp.js +92 -12
- package/hive-mux-proto.js +58 -58
- package/hive-mux.js +354 -351
- package/hive-notify.js +234 -233
- package/hive-obligations.js +247 -247
- package/hive-poll-proto.js +50 -50
- package/hive-pr-staleness.js +189 -189
- package/hive-pr.js +637 -512
- package/hive-privacy.js +327 -327
- package/hive-receipts.js +233 -233
- package/hive-relay-health.js +234 -234
- package/hive-repos.js +430 -430
- package/hive-room-key.js +210 -210
- package/hive-room-mode.js +1761 -1668
- package/hive-room-watcher.js +623 -561
- package/hive-secrets.js +89 -89
- package/hive-session.js +114 -114
- package/hive-setup.js +560 -560
- package/hive-slack-agent.js +408 -408
- package/hive-slack.js +2417 -2367
- package/hive-stdio.js +172 -172
- package/hive-stop-hook.js +293 -293
- package/hive-throttle.js +118 -118
- package/hive-time.js +71 -71
- package/hive-transport.js +617 -578
- package/hive-update.js +58 -58
- package/hive-version.js +110 -110
- package/hive-webhook.js +55 -54
- package/icr-merge.js +58 -58
- package/icr.js +856 -851
- package/lang-brace.js +305 -305
- package/lang-js.js +695 -651
- package/lang-json.js +211 -211
- package/lang-python.js +588 -588
- package/lang-ruby.js +230 -230
- package/lang-toml.js +136 -136
- package/lang-yaml.js +183 -183
- package/limits.js +413 -413
- package/package.json +11 -3
- package/reconcile.cjs +621 -621
- package/slack-app-manifest-agent.json +61 -61
- package/slack-app-manifest-inbound.json +36 -36
- package/slack-app-manifest.json +27 -27
- package/substrate.js +252 -252
- package/sync.js +434 -60
- package/token.js +201 -201
package/hive-update.js
CHANGED
|
@@ -1,58 +1,58 @@
|
|
|
1
|
-
// hive-update.js -- tells an agent, and through it its human, that a newer hivecode is out.
|
|
2
|
-
//
|
|
3
|
-
// WHY THIS EXISTS. `npx -y hivecode-mcp@latest setup` points agent apps at a runtime COPY
|
|
4
|
-
// (~/.hivecode/runtime/<version>), which is what made starting fast, offline and free of npx.
|
|
5
|
-
// It also means nothing updates any more: every start used to fetch @latest, and now an agent
|
|
6
|
-
// stays on the version it was set up with until someone runs setup again. A room with mixed
|
|
7
|
-
// builds is the one condition hive_join itself warns can lose data, so the gap must be visible.
|
|
8
|
-
//
|
|
9
|
-
// IT NEVER GETS IN THE WAY. The check starts with the server, carries a short timeout, and
|
|
10
|
-
// hive_join and hive_status use whatever answer has arrived by then. A failed check says
|
|
11
|
-
// nothing -- no network is not news. HIVE_PRIVATE=1 and HIVE_NO_UPDATE_CHECK=1 both switch it
|
|
12
|
-
// off: a user who asked for privacy did not ask for a request to npm on every start.
|
|
13
|
-
import { VERSION } from './hive-version.js'
|
|
14
|
-
|
|
15
|
-
const REGISTRY_LATEST = 'https://registry.npmjs.org/hivecode-mcp/latest'
|
|
16
|
-
|
|
17
|
-
/** True when version `a` is newer than `b` (x.y.z; anything unparseable is never "newer"). */
|
|
18
|
-
export function newer(a, b) {
|
|
19
|
-
const parse = (v) => String(v).split('-')[0].split('.').map((n) => Number(n))
|
|
20
|
-
const pa = parse(a)
|
|
21
|
-
const pb = parse(b)
|
|
22
|
-
for (let i = 0; i < 3; i++) {
|
|
23
|
-
const x = pa[i] ?? 0
|
|
24
|
-
const y = pb[i] ?? 0
|
|
25
|
-
if (!Number.isFinite(x) || !Number.isFinite(y)) return false
|
|
26
|
-
if (x !== y) return x > y
|
|
27
|
-
}
|
|
28
|
-
return false
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
let latest = null
|
|
32
|
-
let started = false
|
|
33
|
-
|
|
34
|
-
export function startUpdateCheck({ fetchImpl = globalThis.fetch, timeoutMs = 4000, env = process.env } = {}) {
|
|
35
|
-
if (started) return
|
|
36
|
-
started = true
|
|
37
|
-
if (env.HIVE_NO_UPDATE_CHECK === '1' || env.HIVE_PRIVATE === '1' || typeof fetchImpl !== 'function') return
|
|
38
|
-
Promise.resolve()
|
|
39
|
-
// HIVE_UPDATE_URL exists for tests/update-notice-test.mjs, which serves a fake registry so the
|
|
40
|
-
// wiring is proven end to end through a real server process.
|
|
41
|
-
.then(() => fetchImpl(env.HIVE_UPDATE_URL || REGISTRY_LATEST, { signal: AbortSignal.timeout(timeoutMs), headers: { accept: 'application/json' } }))
|
|
42
|
-
.then((r) => (r && r.ok ? r.json() : null))
|
|
43
|
-
.then((j) => { if (j && typeof j.version === 'string') latest = j.version })
|
|
44
|
-
.catch(() => { /* offline, blocked, or slow: say nothing */ })
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** The line to show, or '' when this agent is current (or nobody knows yet). */
|
|
48
|
-
export function updateNotice(current = VERSION, known = latest) {
|
|
49
|
-
if (!known || !newer(known, current)) return ''
|
|
50
|
-
return `UPDATE AVAILABLE: hivecode ${known} is out; this agent runs ${current}. Tell your human to run ` +
|
|
51
|
-
'`npx -y hivecode-mcp@latest setup` and reload this agent. Mixed versions in one room can lose data.'
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export const _test = {
|
|
55
|
-
reset() { latest = null; started = false },
|
|
56
|
-
setLatest(v) { latest = v; started = true },
|
|
57
|
-
latest: () => latest,
|
|
58
|
-
}
|
|
1
|
+
// hive-update.js -- tells an agent, and through it its human, that a newer hivecode is out.
|
|
2
|
+
//
|
|
3
|
+
// WHY THIS EXISTS. `npx -y hivecode-mcp@latest setup` points agent apps at a runtime COPY
|
|
4
|
+
// (~/.hivecode/runtime/<version>), which is what made starting fast, offline and free of npx.
|
|
5
|
+
// It also means nothing updates any more: every start used to fetch @latest, and now an agent
|
|
6
|
+
// stays on the version it was set up with until someone runs setup again. A room with mixed
|
|
7
|
+
// builds is the one condition hive_join itself warns can lose data, so the gap must be visible.
|
|
8
|
+
//
|
|
9
|
+
// IT NEVER GETS IN THE WAY. The check starts with the server, carries a short timeout, and
|
|
10
|
+
// hive_join and hive_status use whatever answer has arrived by then. A failed check says
|
|
11
|
+
// nothing -- no network is not news. HIVE_PRIVATE=1 and HIVE_NO_UPDATE_CHECK=1 both switch it
|
|
12
|
+
// off: a user who asked for privacy did not ask for a request to npm on every start.
|
|
13
|
+
import { VERSION } from './hive-version.js'
|
|
14
|
+
|
|
15
|
+
const REGISTRY_LATEST = 'https://registry.npmjs.org/hivecode-mcp/latest'
|
|
16
|
+
|
|
17
|
+
/** True when version `a` is newer than `b` (x.y.z; anything unparseable is never "newer"). */
|
|
18
|
+
export function newer(a, b) {
|
|
19
|
+
const parse = (v) => String(v).split('-')[0].split('.').map((n) => Number(n))
|
|
20
|
+
const pa = parse(a)
|
|
21
|
+
const pb = parse(b)
|
|
22
|
+
for (let i = 0; i < 3; i++) {
|
|
23
|
+
const x = pa[i] ?? 0
|
|
24
|
+
const y = pb[i] ?? 0
|
|
25
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) return false
|
|
26
|
+
if (x !== y) return x > y
|
|
27
|
+
}
|
|
28
|
+
return false
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let latest = null
|
|
32
|
+
let started = false
|
|
33
|
+
|
|
34
|
+
export function startUpdateCheck({ fetchImpl = globalThis.fetch, timeoutMs = 4000, env = process.env } = {}) {
|
|
35
|
+
if (started) return
|
|
36
|
+
started = true
|
|
37
|
+
if (env.HIVE_NO_UPDATE_CHECK === '1' || env.HIVE_PRIVATE === '1' || typeof fetchImpl !== 'function') return
|
|
38
|
+
Promise.resolve()
|
|
39
|
+
// HIVE_UPDATE_URL exists for tests/update-notice-test.mjs, which serves a fake registry so the
|
|
40
|
+
// wiring is proven end to end through a real server process.
|
|
41
|
+
.then(() => fetchImpl(env.HIVE_UPDATE_URL || REGISTRY_LATEST, { signal: AbortSignal.timeout(timeoutMs), headers: { accept: 'application/json' } }))
|
|
42
|
+
.then((r) => (r && r.ok ? r.json() : null))
|
|
43
|
+
.then((j) => { if (j && typeof j.version === 'string') latest = j.version })
|
|
44
|
+
.catch(() => { /* offline, blocked, or slow: say nothing */ })
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The line to show, or '' when this agent is current (or nobody knows yet). */
|
|
48
|
+
export function updateNotice(current = VERSION, known = latest) {
|
|
49
|
+
if (!known || !newer(known, current)) return ''
|
|
50
|
+
return `UPDATE AVAILABLE: hivecode ${known} is out; this agent runs ${current}. Tell your human to run ` +
|
|
51
|
+
'`npx -y hivecode-mcp@latest setup` and reload this agent. Mixed versions in one room can lose data.'
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const _test = {
|
|
55
|
+
reset() { latest = null; started = false },
|
|
56
|
+
setLatest(v) { latest = v; started = true },
|
|
57
|
+
latest: () => latest,
|
|
58
|
+
}
|
package/hive-version.js
CHANGED
|
@@ -1,110 +1,110 @@
|
|
|
1
|
-
// hive-version.js — one place that knows what build this is.
|
|
2
|
-
//
|
|
3
|
-
// WHY THIS FILE EXISTS, and it is not tidiness.
|
|
4
|
-
//
|
|
5
|
-
// VERSION is load-bearing, not cosmetic. hive_join instructs every arriving agent to announce its
|
|
6
|
-
// version in chat, because two clients running different builds in one room corrupt each other's
|
|
7
|
-
// files with no error on either side — both writes succeed, both are green, and the disagreement
|
|
8
|
-
// surfaces only as lost bytes. That announcement is the ONLY channel that makes a mixed build
|
|
9
|
-
// visible from inside the room. So a version string is not a label; it is the input to the one
|
|
10
|
-
// detector we have.
|
|
11
|
-
//
|
|
12
|
-
// On 20 Sep 2026 three entry points were found reporting a version package.json does not contain:
|
|
13
|
-
// remote-mcp.js said '0.11.5' while live on the hosted relay, hive-mcp-http.js and hive-linear.js
|
|
14
|
-
// both said '1.0.0'. hive-mcp.js had already been fixed months earlier, with a comment explaining
|
|
15
|
-
// exactly why hardcoding it is dangerous — and that fix was never carried to the other three.
|
|
16
|
-
// A detector that lies is strictly worse than no detector, because it is trusted.
|
|
17
|
-
//
|
|
18
|
-
// HIV-P7 closed that by giving each of the four files its own runtime read. Which produced the
|
|
19
|
-
// next defect, and it was measured rather than predicted: with the reader duplicated four times,
|
|
20
|
-
// corrupting ONE copy's fallback string to '9.9.9-DRIFTED' left tests/version-truth-test.mjs at
|
|
21
|
-
// 16 passed, 0 failed. The property guards the version that gets REPORTED and says nothing about
|
|
22
|
-
// the mechanism that produces it, so three of four copies could rot in silence.
|
|
23
|
-
//
|
|
24
|
-
// This codebase has watched that happen before. The extension's reconcile() and the engine's were
|
|
25
|
-
// the same function once; they now share 34 lines and behave differently. Eight lines is not too
|
|
26
|
-
// small to drift — it is exactly small enough that nobody feels the need to check.
|
|
27
|
-
//
|
|
28
|
-
// So the fix is DELETION, not another assertion. One definition, imported by all four. The
|
|
29
|
-
// property becomes true by construction instead of by policing, and a fifth entry point written
|
|
30
|
-
// next year gets it right by importing rather than by remembering.
|
|
31
|
-
//
|
|
32
|
-
// TWO DECISIONS WORTH KEEPING:
|
|
33
|
-
//
|
|
34
|
-
// It READS package.json rather than being told. package.json always ships beside these files —
|
|
35
|
-
// in the repo and in the published tarball — so `new URL('./package.json', import.meta.url)`
|
|
36
|
-
// resolves in both. Anything else (a constant, a build step, a generated file) can disagree with
|
|
37
|
-
// the package the moment `npm version` runs, which is precisely the failure being fixed.
|
|
38
|
-
//
|
|
39
|
-
// IT NEVER THROWS. A version read is not worth a server that will not boot, and a server that
|
|
40
|
-
// refuses to start is a worse outcome than one that says it does not know which build it is. So
|
|
41
|
-
// an unreadable or malformed package.json yields '0.0.0-unknown' — a value that is obviously
|
|
42
|
-
// wrong to a human, will never match a real release, and cannot be mistaken for a true answer.
|
|
43
|
-
// Silence or an empty string could be read as "no mixed build here"; this cannot.
|
|
44
|
-
|
|
45
|
-
import fs from 'fs'
|
|
46
|
-
import path from 'path'
|
|
47
|
-
import { fileURLToPath } from 'url'
|
|
48
|
-
|
|
49
|
-
/** Recomputed never: a running process cannot change which build it is. */
|
|
50
|
-
export const VERSION = (() => {
|
|
51
|
-
try {
|
|
52
|
-
const v = JSON.parse(fs.readFileSync(new URL('./package.json', import.meta.url), 'utf8')).version
|
|
53
|
-
if (typeof v === 'string' && v) return v
|
|
54
|
-
} catch { /* fall through — see IT NEVER THROWS above */ }
|
|
55
|
-
return '0.0.0-unknown'
|
|
56
|
-
})()
|
|
57
|
-
|
|
58
|
-
/** The sentinel, exported so callers and tests can name it instead of retyping the literal. */
|
|
59
|
-
export const UNKNOWN_VERSION = '0.0.0-unknown'
|
|
60
|
-
|
|
61
|
-
// BUILD: WHICH COMMIT, not just which version (29 Sep 2026).
|
|
62
|
-
//
|
|
63
|
-
// VERSION alone could not see a mixed build. On 28-29 Sep release-gate HEAD and the published
|
|
64
|
-
// tarball both said "0.12.2" while differing by a night of fixes, and the hosted relay's /health
|
|
65
|
-
// reported no version at all -- "is the fix deployed?" was answered from its uptime. So a build
|
|
66
|
-
// carries its commit when it knows one:
|
|
67
|
-
// RENDER_GIT_COMMIT set by Render for a deployed relay (its image has no .git)
|
|
68
|
-
// a git checkout .git beside package.json: a folder, or a worktree's `gitdir:` file
|
|
69
|
-
// otherwise '' an npm install: VERSION is the whole identity, and it is the true one
|
|
70
|
-
// VERSION itself is untouched -- the update notice compares it with the registry's -- so the
|
|
71
|
-
// commit travels separately, and VERSION_LABEL is what people and agents are shown.
|
|
72
|
-
//
|
|
73
|
-
// It never walks UP. hivecode-mcp installed into someone's repo would otherwise report THAT repo's
|
|
74
|
-
// commit as its own build, which is the lie this exists to prevent. And like VERSION it never throws.
|
|
75
|
-
const HEX = /^[0-9a-f]{7,40}$/
|
|
76
|
-
export function buildOf(root, env = process.env) {
|
|
77
|
-
try {
|
|
78
|
-
const rc = String(env.RENDER_GIT_COMMIT || '').trim().toLowerCase()
|
|
79
|
-
if (HEX.test(rc)) return rc.slice(0, 7)
|
|
80
|
-
const dotGit = path.join(root, '.git')
|
|
81
|
-
let gitDir = dotGit
|
|
82
|
-
if (fs.statSync(dotGit).isFile()) { // a worktree: ".git" is a file holding "gitdir: <path>"
|
|
83
|
-
const m = /^gitdir:\s*(.+?)\s*$/m.exec(fs.readFileSync(dotGit, 'utf8'))
|
|
84
|
-
if (!m) return ''
|
|
85
|
-
gitDir = path.resolve(root, m[1])
|
|
86
|
-
}
|
|
87
|
-
const head = fs.readFileSync(path.join(gitDir, 'HEAD'), 'utf8').trim()
|
|
88
|
-
if (/^[0-9a-f]{40}$/.test(head)) return head.slice(0, 7) // detached
|
|
89
|
-
const ref = /^ref:\s*(\S+)$/.exec(head)
|
|
90
|
-
if (!ref) return ''
|
|
91
|
-
let common = gitDir // a worktree keeps its branches in the main repo's .git
|
|
92
|
-
try { common = path.resolve(gitDir, fs.readFileSync(path.join(gitDir, 'commondir'), 'utf8').trim()) } catch { }
|
|
93
|
-
for (const d of [gitDir, common]) {
|
|
94
|
-
try { const h = fs.readFileSync(path.join(d, ref[1]), 'utf8').trim(); if (/^[0-9a-f]{40}$/.test(h)) return h.slice(0, 7) } catch { }
|
|
95
|
-
}
|
|
96
|
-
for (const line of fs.readFileSync(path.join(common, 'packed-refs'), 'utf8').split('\n')) {
|
|
97
|
-
const [h, r] = line.trim().split(/\s+/)
|
|
98
|
-
if (r === ref[1] && /^[0-9a-f]{40}$/.test(h)) return h.slice(0, 7)
|
|
99
|
-
}
|
|
100
|
-
} catch { /* no .git, unreadable, malformed: no commit to report -- see IT NEVER THROWS */ }
|
|
101
|
-
return ''
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/** This build's commit, or '' when it has none to report (an npm install). */
|
|
105
|
-
export const BUILD = buildOf(fileURLToPath(new URL('.', import.meta.url)))
|
|
106
|
-
|
|
107
|
-
/** What people and agents are shown: "0.12.2+fe1d89e" from a checkout, "0.12.2" from npm. */
|
|
108
|
-
export const VERSION_LABEL = BUILD ? `${VERSION}+${BUILD}` : VERSION
|
|
109
|
-
|
|
110
|
-
export default VERSION
|
|
1
|
+
// hive-version.js — one place that knows what build this is.
|
|
2
|
+
//
|
|
3
|
+
// WHY THIS FILE EXISTS, and it is not tidiness.
|
|
4
|
+
//
|
|
5
|
+
// VERSION is load-bearing, not cosmetic. hive_join instructs every arriving agent to announce its
|
|
6
|
+
// version in chat, because two clients running different builds in one room corrupt each other's
|
|
7
|
+
// files with no error on either side — both writes succeed, both are green, and the disagreement
|
|
8
|
+
// surfaces only as lost bytes. That announcement is the ONLY channel that makes a mixed build
|
|
9
|
+
// visible from inside the room. So a version string is not a label; it is the input to the one
|
|
10
|
+
// detector we have.
|
|
11
|
+
//
|
|
12
|
+
// On 20 Sep 2026 three entry points were found reporting a version package.json does not contain:
|
|
13
|
+
// remote-mcp.js said '0.11.5' while live on the hosted relay, hive-mcp-http.js and hive-linear.js
|
|
14
|
+
// both said '1.0.0'. hive-mcp.js had already been fixed months earlier, with a comment explaining
|
|
15
|
+
// exactly why hardcoding it is dangerous — and that fix was never carried to the other three.
|
|
16
|
+
// A detector that lies is strictly worse than no detector, because it is trusted.
|
|
17
|
+
//
|
|
18
|
+
// HIV-P7 closed that by giving each of the four files its own runtime read. Which produced the
|
|
19
|
+
// next defect, and it was measured rather than predicted: with the reader duplicated four times,
|
|
20
|
+
// corrupting ONE copy's fallback string to '9.9.9-DRIFTED' left tests/version-truth-test.mjs at
|
|
21
|
+
// 16 passed, 0 failed. The property guards the version that gets REPORTED and says nothing about
|
|
22
|
+
// the mechanism that produces it, so three of four copies could rot in silence.
|
|
23
|
+
//
|
|
24
|
+
// This codebase has watched that happen before. The extension's reconcile() and the engine's were
|
|
25
|
+
// the same function once; they now share 34 lines and behave differently. Eight lines is not too
|
|
26
|
+
// small to drift — it is exactly small enough that nobody feels the need to check.
|
|
27
|
+
//
|
|
28
|
+
// So the fix is DELETION, not another assertion. One definition, imported by all four. The
|
|
29
|
+
// property becomes true by construction instead of by policing, and a fifth entry point written
|
|
30
|
+
// next year gets it right by importing rather than by remembering.
|
|
31
|
+
//
|
|
32
|
+
// TWO DECISIONS WORTH KEEPING:
|
|
33
|
+
//
|
|
34
|
+
// It READS package.json rather than being told. package.json always ships beside these files —
|
|
35
|
+
// in the repo and in the published tarball — so `new URL('./package.json', import.meta.url)`
|
|
36
|
+
// resolves in both. Anything else (a constant, a build step, a generated file) can disagree with
|
|
37
|
+
// the package the moment `npm version` runs, which is precisely the failure being fixed.
|
|
38
|
+
//
|
|
39
|
+
// IT NEVER THROWS. A version read is not worth a server that will not boot, and a server that
|
|
40
|
+
// refuses to start is a worse outcome than one that says it does not know which build it is. So
|
|
41
|
+
// an unreadable or malformed package.json yields '0.0.0-unknown' — a value that is obviously
|
|
42
|
+
// wrong to a human, will never match a real release, and cannot be mistaken for a true answer.
|
|
43
|
+
// Silence or an empty string could be read as "no mixed build here"; this cannot.
|
|
44
|
+
|
|
45
|
+
import fs from 'fs'
|
|
46
|
+
import path from 'path'
|
|
47
|
+
import { fileURLToPath } from 'url'
|
|
48
|
+
|
|
49
|
+
/** Recomputed never: a running process cannot change which build it is. */
|
|
50
|
+
export const VERSION = (() => {
|
|
51
|
+
try {
|
|
52
|
+
const v = JSON.parse(fs.readFileSync(new URL('./package.json', import.meta.url), 'utf8')).version
|
|
53
|
+
if (typeof v === 'string' && v) return v
|
|
54
|
+
} catch { /* fall through — see IT NEVER THROWS above */ }
|
|
55
|
+
return '0.0.0-unknown'
|
|
56
|
+
})()
|
|
57
|
+
|
|
58
|
+
/** The sentinel, exported so callers and tests can name it instead of retyping the literal. */
|
|
59
|
+
export const UNKNOWN_VERSION = '0.0.0-unknown'
|
|
60
|
+
|
|
61
|
+
// BUILD: WHICH COMMIT, not just which version (29 Sep 2026).
|
|
62
|
+
//
|
|
63
|
+
// VERSION alone could not see a mixed build. On 28-29 Sep release-gate HEAD and the published
|
|
64
|
+
// tarball both said "0.12.2" while differing by a night of fixes, and the hosted relay's /health
|
|
65
|
+
// reported no version at all -- "is the fix deployed?" was answered from its uptime. So a build
|
|
66
|
+
// carries its commit when it knows one:
|
|
67
|
+
// RENDER_GIT_COMMIT set by Render for a deployed relay (its image has no .git)
|
|
68
|
+
// a git checkout .git beside package.json: a folder, or a worktree's `gitdir:` file
|
|
69
|
+
// otherwise '' an npm install: VERSION is the whole identity, and it is the true one
|
|
70
|
+
// VERSION itself is untouched -- the update notice compares it with the registry's -- so the
|
|
71
|
+
// commit travels separately, and VERSION_LABEL is what people and agents are shown.
|
|
72
|
+
//
|
|
73
|
+
// It never walks UP. hivecode-mcp installed into someone's repo would otherwise report THAT repo's
|
|
74
|
+
// commit as its own build, which is the lie this exists to prevent. And like VERSION it never throws.
|
|
75
|
+
const HEX = /^[0-9a-f]{7,40}$/
|
|
76
|
+
export function buildOf(root, env = process.env) {
|
|
77
|
+
try {
|
|
78
|
+
const rc = String(env.RENDER_GIT_COMMIT || '').trim().toLowerCase()
|
|
79
|
+
if (HEX.test(rc)) return rc.slice(0, 7)
|
|
80
|
+
const dotGit = path.join(root, '.git')
|
|
81
|
+
let gitDir = dotGit
|
|
82
|
+
if (fs.statSync(dotGit).isFile()) { // a worktree: ".git" is a file holding "gitdir: <path>"
|
|
83
|
+
const m = /^gitdir:\s*(.+?)\s*$/m.exec(fs.readFileSync(dotGit, 'utf8'))
|
|
84
|
+
if (!m) return ''
|
|
85
|
+
gitDir = path.resolve(root, m[1])
|
|
86
|
+
}
|
|
87
|
+
const head = fs.readFileSync(path.join(gitDir, 'HEAD'), 'utf8').trim()
|
|
88
|
+
if (/^[0-9a-f]{40}$/.test(head)) return head.slice(0, 7) // detached
|
|
89
|
+
const ref = /^ref:\s*(\S+)$/.exec(head)
|
|
90
|
+
if (!ref) return ''
|
|
91
|
+
let common = gitDir // a worktree keeps its branches in the main repo's .git
|
|
92
|
+
try { common = path.resolve(gitDir, fs.readFileSync(path.join(gitDir, 'commondir'), 'utf8').trim()) } catch { }
|
|
93
|
+
for (const d of [gitDir, common]) {
|
|
94
|
+
try { const h = fs.readFileSync(path.join(d, ref[1]), 'utf8').trim(); if (/^[0-9a-f]{40}$/.test(h)) return h.slice(0, 7) } catch { }
|
|
95
|
+
}
|
|
96
|
+
for (const line of fs.readFileSync(path.join(common, 'packed-refs'), 'utf8').split('\n')) {
|
|
97
|
+
const [h, r] = line.trim().split(/\s+/)
|
|
98
|
+
if (r === ref[1] && /^[0-9a-f]{40}$/.test(h)) return h.slice(0, 7)
|
|
99
|
+
}
|
|
100
|
+
} catch { /* no .git, unreadable, malformed: no commit to report -- see IT NEVER THROWS */ }
|
|
101
|
+
return ''
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** This build's commit, or '' when it has none to report (an npm install). */
|
|
105
|
+
export const BUILD = buildOf(fileURLToPath(new URL('.', import.meta.url)))
|
|
106
|
+
|
|
107
|
+
/** What people and agents are shown: "0.12.2+fe1d89e" from a checkout, "0.12.2" from npm. */
|
|
108
|
+
export const VERSION_LABEL = BUILD ? `${VERSION}+${BUILD}` : VERSION
|
|
109
|
+
|
|
110
|
+
export default VERSION
|
package/hive-webhook.js
CHANGED
|
@@ -1,54 +1,55 @@
|
|
|
1
|
-
// hive-webhook -- one Slack or Discord incoming-webhook URL, and the rule for what may be sent to it.
|
|
2
|
-
//
|
|
3
|
-
// Shared by the GitHub watcher (hive-github.js) and the room notifier (hive-notify.js): the same
|
|
4
|
-
// URL works for both, and the same two promises hold for both.
|
|
5
|
-
// 1. Nothing with a key or token in it leaves: every message is scrubbed first.
|
|
6
|
-
// 2. A notification that fails is logged and dropped, never retried in a loop and never a reason
|
|
7
|
-
// to stop: it is a courtesy, the work is the point.
|
|
8
|
-
import { findSecrets } from './hive-secrets.js'
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
s = s
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
return
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
send.
|
|
53
|
-
|
|
54
|
-
|
|
1
|
+
// hive-webhook -- one Slack or Discord incoming-webhook URL, and the rule for what may be sent to it.
|
|
2
|
+
//
|
|
3
|
+
// Shared by the GitHub watcher (hive-github.js) and the room notifier (hive-notify.js): the same
|
|
4
|
+
// URL works for both, and the same two promises hold for both.
|
|
5
|
+
// 1. Nothing with a key or token in it leaves: every message is scrubbed first.
|
|
6
|
+
// 2. A notification that fails is logged and dropped, never retried in a loop and never a reason
|
|
7
|
+
// to stop: it is a courtesy, the work is the point.
|
|
8
|
+
import { findSecrets } from './hive-secrets.js'
|
|
9
|
+
import { relayFetch } from './hive-transport.js'
|
|
10
|
+
|
|
11
|
+
/** Every key or token in `text` replaced. Run on everything that goes to GitHub or a webhook. */
|
|
12
|
+
export function scrub(text) {
|
|
13
|
+
let s = String(text || '')
|
|
14
|
+
for (const f of findSecrets(s, { tokens: true })) s = s.split(f.value).join(`[${f.kind} removed]`)
|
|
15
|
+
return s
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function cleanError(e, url) {
|
|
19
|
+
let s = String(e && (e.message || e) || 'unknown error')
|
|
20
|
+
if (url) s = s.split(url).join('[webhook url removed]')
|
|
21
|
+
try {
|
|
22
|
+
const u = new URL(url)
|
|
23
|
+
if (u.pathname && u.pathname !== '/') s = s.split(u.pathname).join('[path removed]')
|
|
24
|
+
} catch { }
|
|
25
|
+
s = scrub(s)
|
|
26
|
+
s = s.replace(/Failed to parse URL from .+/i, 'Failed to parse URL')
|
|
27
|
+
return s
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** `send(text)` resolves true when the webhook took it. Slack wants {text}, Discord {content}; any
|
|
31
|
+
* other host gets Slack format ({text}). */
|
|
32
|
+
export function makeNotifier(url, { fetchImpl = relayFetch, log = () => {} } = {}) {
|
|
33
|
+
if (!url) return async () => false
|
|
34
|
+
let host = ''
|
|
35
|
+
try { host = new URL(url).hostname.toLowerCase() } catch { host = '' }
|
|
36
|
+
const slack = /(^|\.)slack\.com$/.test(host)
|
|
37
|
+
const discord = /(^|\.)discord(app)?\.com$/.test(host)
|
|
38
|
+
const kind = discord ? 'discord' : slack ? 'slack' : 'other'
|
|
39
|
+
const send = async (text) => {
|
|
40
|
+
const msg = scrub(text)
|
|
41
|
+
// allowed_mentions: an issue titled "@everyone" must not page a whole Discord server.
|
|
42
|
+
const body = discord ? { content: msg.slice(0, 1900), allowed_mentions: { parse: [] } } : { text: msg }
|
|
43
|
+
try {
|
|
44
|
+
const r = await fetchImpl(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), signal: AbortSignal.timeout(10000) })
|
|
45
|
+
if (!r.ok) { log(`notification not delivered: HTTP ${r.status}`); return false }
|
|
46
|
+
return true
|
|
47
|
+
} catch (e) {
|
|
48
|
+
log(`notification not delivered: ${cleanError(e, url)}`)
|
|
49
|
+
return false
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
send.host = host
|
|
53
|
+
send.kind = kind
|
|
54
|
+
return send
|
|
55
|
+
}
|
package/icr-merge.js
CHANGED
|
@@ -1,58 +1,58 @@
|
|
|
1
|
-
// icr-merge.js — the bridge that brings ICR into the live product, SAFELY.
|
|
2
|
-
//
|
|
3
|
-
// Hivecode's reconcile() merges concurrent edits with merge3() (a line-level 3-way
|
|
4
|
-
// merge). merge3 is convergent: across many peers it always settles on one canonical
|
|
5
|
-
// text. ICR's structural merge produces a DIFFERENT canonical form (it may reorder or
|
|
6
|
-
// reformat). Mixing two merge algorithms in a live multi-peer sync means the same logical
|
|
7
|
-
// content has two byte-forms, and peers ping-pong between them — which corrupts files
|
|
8
|
-
// (duplicated/*dropped* lines). So we do NOT change merge bytes with ICR here.
|
|
9
|
-
//
|
|
10
|
-
// Instead ICR runs in ADVISORY mode: merge3 still produces the bytes (full convergence
|
|
11
|
-
// preserved, identical to pre-ICR behavior), and ICR runs alongside purely to DETECT the
|
|
12
|
-
// meaning-level problems a line merge can't see — a dangling reference, a rename, a
|
|
13
|
-
// same-declaration clash — and surface them as a warning on the activity feed. This ships
|
|
14
|
-
// ICR's core value (catching semantic breaks) with zero divergence risk.
|
|
15
|
-
//
|
|
16
|
-
// ICR's full structural/rename AUTO-MERGE lives in the library (icr.js) and is proven by
|
|
17
|
-
// the test suite + fuzzer; turning it on in the product is a later step that needs a
|
|
18
|
-
// format-preserving, single-canonical-form merge so peers can't diverge.
|
|
19
|
-
|
|
20
|
-
import { structuralMerge, supports } from './icr.js'
|
|
21
|
-
import { merge3 } from './core.js'
|
|
22
|
-
|
|
23
|
-
// Turn ICR's machine conflict keys into a human sentence for the activity feed / chat.
|
|
24
|
-
function describeConflicts(conflicts) {
|
|
25
|
-
return (conflicts || []).map((c) => {
|
|
26
|
-
if (c.startsWith('ref:')) return `'${c.slice(4)}' was removed or renamed but is still used`
|
|
27
|
-
if (c.startsWith('fn:')) return `both sides changed function ${c.slice(3)}`
|
|
28
|
-
if (c.startsWith('class:')) return `both sides changed class ${c.slice(6)}`
|
|
29
|
-
if (c.startsWith('var:')) return `both sides changed ${c.slice(4)}`
|
|
30
|
-
return `both sides changed ${c}`
|
|
31
|
-
}).join('; ')
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Same shape as merge3 — { text, conflict } — plus an `icr` tag ('structural'|'rename')
|
|
35
|
-
// and an optional `icrWarning`. ICR's format-preserving merge is convergent (symmetric +
|
|
36
|
-
// fixed-point, verified by icr-converge-test.js), so its 'auto' result is used as the
|
|
37
|
-
// merged bytes for supported files; merge3 covers conflicts, fallbacks, and other languages.
|
|
38
|
-
export function icrMerge3(base, mine, theirs, relPath) {
|
|
39
|
-
const lm = merge3(base, mine, theirs)
|
|
40
|
-
if (!supports(relPath)) return lm
|
|
41
|
-
|
|
42
|
-
let r
|
|
43
|
-
try { r = structuralMerge(base, mine, theirs, { filename: relPath }) }
|
|
44
|
-
catch { return lm } // ICR must never break a sync
|
|
45
|
-
|
|
46
|
-
if (r.status === 'auto') {
|
|
47
|
-
const renamed = r.renames && r.renames.length
|
|
48
|
-
return {
|
|
49
|
-
text: r.text, conflict: false, icr: renamed ? 'rename' : 'structural',
|
|
50
|
-
icrWarning: renamed ? `auto-applied rename ${r.renames.join(', ')} — call sites updated` : undefined,
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
// A meaning-level conflict the line merge can't represent (dangling ref / clash): keep
|
|
54
|
-
// the safe line-merge bytes, but surface what ICR caught so it isn't shipped silently.
|
|
55
|
-
if (r.status === 'semantic-conflict')
|
|
56
|
-
return { ...lm, icrWarning: describeConflicts(r.conflicts) }
|
|
57
|
-
return lm // 'fallback'
|
|
58
|
-
}
|
|
1
|
+
// icr-merge.js — the bridge that brings ICR into the live product, SAFELY.
|
|
2
|
+
//
|
|
3
|
+
// Hivecode's reconcile() merges concurrent edits with merge3() (a line-level 3-way
|
|
4
|
+
// merge). merge3 is convergent: across many peers it always settles on one canonical
|
|
5
|
+
// text. ICR's structural merge produces a DIFFERENT canonical form (it may reorder or
|
|
6
|
+
// reformat). Mixing two merge algorithms in a live multi-peer sync means the same logical
|
|
7
|
+
// content has two byte-forms, and peers ping-pong between them — which corrupts files
|
|
8
|
+
// (duplicated/*dropped* lines). So we do NOT change merge bytes with ICR here.
|
|
9
|
+
//
|
|
10
|
+
// Instead ICR runs in ADVISORY mode: merge3 still produces the bytes (full convergence
|
|
11
|
+
// preserved, identical to pre-ICR behavior), and ICR runs alongside purely to DETECT the
|
|
12
|
+
// meaning-level problems a line merge can't see — a dangling reference, a rename, a
|
|
13
|
+
// same-declaration clash — and surface them as a warning on the activity feed. This ships
|
|
14
|
+
// ICR's core value (catching semantic breaks) with zero divergence risk.
|
|
15
|
+
//
|
|
16
|
+
// ICR's full structural/rename AUTO-MERGE lives in the library (icr.js) and is proven by
|
|
17
|
+
// the test suite + fuzzer; turning it on in the product is a later step that needs a
|
|
18
|
+
// format-preserving, single-canonical-form merge so peers can't diverge.
|
|
19
|
+
|
|
20
|
+
import { structuralMerge, supports } from './icr.js'
|
|
21
|
+
import { merge3 } from './core.js'
|
|
22
|
+
|
|
23
|
+
// Turn ICR's machine conflict keys into a human sentence for the activity feed / chat.
|
|
24
|
+
function describeConflicts(conflicts) {
|
|
25
|
+
return (conflicts || []).map((c) => {
|
|
26
|
+
if (c.startsWith('ref:')) return `'${c.slice(4)}' was removed or renamed but is still used`
|
|
27
|
+
if (c.startsWith('fn:')) return `both sides changed function ${c.slice(3)}`
|
|
28
|
+
if (c.startsWith('class:')) return `both sides changed class ${c.slice(6)}`
|
|
29
|
+
if (c.startsWith('var:')) return `both sides changed ${c.slice(4)}`
|
|
30
|
+
return `both sides changed ${c}`
|
|
31
|
+
}).join('; ')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Same shape as merge3 — { text, conflict } — plus an `icr` tag ('structural'|'rename')
|
|
35
|
+
// and an optional `icrWarning`. ICR's format-preserving merge is convergent (symmetric +
|
|
36
|
+
// fixed-point, verified by icr-converge-test.js), so its 'auto' result is used as the
|
|
37
|
+
// merged bytes for supported files; merge3 covers conflicts, fallbacks, and other languages.
|
|
38
|
+
export function icrMerge3(base, mine, theirs, relPath) {
|
|
39
|
+
const lm = merge3(base, mine, theirs)
|
|
40
|
+
if (!supports(relPath)) return lm
|
|
41
|
+
|
|
42
|
+
let r
|
|
43
|
+
try { r = structuralMerge(base, mine, theirs, { filename: relPath }) }
|
|
44
|
+
catch { return lm } // ICR must never break a sync
|
|
45
|
+
|
|
46
|
+
if (r.status === 'auto') {
|
|
47
|
+
const renamed = r.renames && r.renames.length
|
|
48
|
+
return {
|
|
49
|
+
text: r.text, conflict: false, icr: renamed ? 'rename' : 'structural',
|
|
50
|
+
icrWarning: renamed ? `auto-applied rename ${r.renames.join(', ')} — call sites updated` : undefined,
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// A meaning-level conflict the line merge can't represent (dangling ref / clash): keep
|
|
54
|
+
// the safe line-merge bytes, but surface what ICR caught so it isn't shipped silently.
|
|
55
|
+
if (r.status === 'semantic-conflict')
|
|
56
|
+
return { ...lm, icrWarning: describeConflicts(r.conflicts) }
|
|
57
|
+
return lm // 'fallback'
|
|
58
|
+
}
|