hivecode-mcp 0.12.4 → 0.13.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/hive-stop-hook.js CHANGED
@@ -57,6 +57,7 @@ import fs from 'fs'
57
57
  import path from 'path'
58
58
  import { mentionsName, unansweredFor } from './hive-mailbox.js'
59
59
  import { isArmed, listenCommand, listenHow, listenOrder } from './hive-listen.js'
60
+ import { readAgents, agentForHook } from './hive-session.js'
60
61
 
61
62
  // Consecutive turns we will block purely because a task is still open, before standing down.
62
63
  // Messages are not subject to this: the watermark already makes each one block at most once.
@@ -205,12 +206,14 @@ function obligations({ chat, tasks, me, seen, taskBlocks, nudged = [] }) {
205
206
  }
206
207
 
207
208
  /** Walk up from `start` looking for the session file; the hook's cwd is usually but not always
208
- * the folder the MCP server was launched in. */
209
- function findSession(start) {
209
+ * the folder the MCP server was launched in. That folder can have started several agents (two
210
+ * Claude Code sessions in one repo), so the record is the one for THIS hook's conversation, and
211
+ * none at all when that cannot be told (hive-session.js). */
212
+ function findSession(start, hook = {}) {
210
213
  let dir = path.resolve(start)
211
214
  for (let i = 0; i < 4; i++) {
212
215
  const p = path.join(dir, SESSION_FILE)
213
- if (fs.existsSync(p)) return { root: dir, session: readJson(p) }
216
+ if (fs.existsSync(p)) return { root: dir, session: agentForHook(readAgents(p), hook) }
214
217
  const up = path.dirname(dir)
215
218
  if (up === dir) break
216
219
  dir = up
@@ -230,7 +233,7 @@ async function main() {
230
233
  // GUARD 1: this stop is already the product of a block. Never stack another on it.
231
234
  if (hook.stop_hook_active || Number(hook.executionNum) > 0) return allow()
232
235
 
233
- const found = findSession(hook.cwd || (Array.isArray(hook.workspacePaths) && hook.workspacePaths[0]) || process.cwd())
236
+ const found = findSession(hook.cwd || (Array.isArray(hook.workspacePaths) && hook.workspacePaths[0]) || process.cwd(), hook)
234
237
  if (!found || !found.session || !found.session.dir || !found.session.name) return allow() // not in a room
235
238
 
236
239
  const { root, session } = found
package/hive-time.js ADDED
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Unified timestamp parsing and formatting for Hivecode chat and receipts.
3
+ * Single source of truth used by:
4
+ * 1. public/control.html (browser Control Room ago() and chat timestamps)
5
+ * 2. hive-mailbox.js (hive_read_chat formatMessage)
6
+ * 3. sync.js (HIVE_CHAT.md renderChat)
7
+ *
8
+ * Handles:
9
+ * - Unix microseconds (> 1e14), as emitted by hive-slack.js, hive-mcp-http.js, hive-gate.js
10
+ * - Unix milliseconds (> 1e11)
11
+ * - Existing "HH:MM:SS" strings (passed through as-is per Claude's directive)
12
+ */
13
+
14
+ export function toMillis(at) {
15
+ if (at == null || at === '') return null
16
+ const num = typeof at === 'number' ? at : Number(at)
17
+ if (!isNaN(num) && num > 0) {
18
+ if (num > 1e14) {
19
+ // Unix microseconds (> 100,000,000,000,000)
20
+ return Math.round(num / 1000)
21
+ }
22
+ if (num > 1e11) {
23
+ // Unix milliseconds (> 100,000,000,000)
24
+ return Math.round(num)
25
+ }
26
+ if (num > 1e9) {
27
+ // Unix seconds (> 1,000,000,000)
28
+ return Math.round(num * 1000)
29
+ }
30
+ return Math.round(num)
31
+ }
32
+ return null
33
+ }
34
+
35
+ export function ago(at, now = Date.now()) {
36
+ if (!at) return ''
37
+ if (typeof at === 'string') {
38
+ const num = Number(at)
39
+ if (!isNaN(num) && num > 1e11) {
40
+ return ago(num, now)
41
+ }
42
+ // String with clock time or text: print as is (matches control.html behavior)
43
+ return at
44
+ }
45
+ const ms = toMillis(at)
46
+ if (ms == null) return String(at)
47
+ const d = Math.max(0, now - ms)
48
+ const m = Math.floor(d / 60000)
49
+ if (m < 1) return 'now'
50
+ if (m < 60) return `${m}m`
51
+ const h = Math.floor(m / 60)
52
+ return h < 24 ? `${h}h` : `${Math.floor(h / 24)}d`
53
+ }
54
+
55
+ export function formatTime(at, now = Date.now()) {
56
+ if (!at) return ''
57
+ if (typeof at === 'string') {
58
+ const num = Number(at)
59
+ if (!isNaN(num) && num > 1e11) {
60
+ return formatTime(num, now)
61
+ }
62
+ return at
63
+ }
64
+ const ms = toMillis(at)
65
+ if (ms == null) return String(at)
66
+ return new Date(ms).toTimeString().slice(0, 8)
67
+ }
68
+
69
+ export function formatAt(at, { ago: isAgo = false, now = Date.now() } = {}) {
70
+ return isAgo ? ago(at, now) : formatTime(at, now)
71
+ }
@@ -0,0 +1,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
+
10
+ /** Every key or token in `text` replaced. Run on everything that goes to GitHub or a webhook. */
11
+ export function scrub(text) {
12
+ let s = String(text || '')
13
+ for (const f of findSecrets(s, { tokens: true })) s = s.split(f.value).join(`[${f.kind} removed]`)
14
+ return s
15
+ }
16
+
17
+ export function cleanError(e, url) {
18
+ let s = String(e && (e.message || e) || 'unknown error')
19
+ if (url) s = s.split(url).join('[webhook url removed]')
20
+ try {
21
+ const u = new URL(url)
22
+ if (u.pathname && u.pathname !== '/') s = s.split(u.pathname).join('[path removed]')
23
+ } catch { }
24
+ s = scrub(s)
25
+ s = s.replace(/Failed to parse URL from .+/i, 'Failed to parse URL')
26
+ return s
27
+ }
28
+
29
+ /** `send(text)` resolves true when the webhook took it. Slack wants {text}, Discord {content}; any
30
+ * other host gets Slack format ({text}). */
31
+ export function makeNotifier(url, { fetchImpl = globalThis.fetch, log = () => {} } = {}) {
32
+ if (!url) return async () => false
33
+ let host = ''
34
+ try { host = new URL(url).hostname.toLowerCase() } catch { host = '' }
35
+ const slack = /(^|\.)slack\.com$/.test(host)
36
+ const discord = /(^|\.)discord(app)?\.com$/.test(host)
37
+ const kind = discord ? 'discord' : slack ? 'slack' : 'other'
38
+ const send = async (text) => {
39
+ const msg = scrub(text)
40
+ // allowed_mentions: an issue titled "@everyone" must not page a whole Discord server.
41
+ const body = discord ? { content: msg.slice(0, 1900), allowed_mentions: { parse: [] } } : { text: msg }
42
+ try {
43
+ const r = await fetchImpl(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), signal: AbortSignal.timeout(10000) })
44
+ if (!r.ok) { log(`notification not delivered: HTTP ${r.status}`); return false }
45
+ return true
46
+ } catch (e) {
47
+ log(`notification not delivered: ${cleanError(e, url)}`)
48
+ return false
49
+ }
50
+ }
51
+ send.host = host
52
+ send.kind = kind
53
+ return send
54
+ }