robrain 2.3.7 → 2.3.9

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.
Files changed (43) hide show
  1. package/dist/commands/install.d.ts.map +1 -1
  2. package/dist/commands/install.js +17 -2
  3. package/dist/commands/install.js.map +1 -1
  4. package/dist/commands/up.d.ts +14 -0
  5. package/dist/commands/up.d.ts.map +1 -1
  6. package/dist/commands/up.js +60 -2
  7. package/dist/commands/up.js.map +1 -1
  8. package/dist/commands/up.test.js +21 -3
  9. package/dist/commands/up.test.js.map +1 -1
  10. package/dist/index.js +1 -1
  11. package/dist/lib/codex-hooks.d.ts +26 -0
  12. package/dist/lib/codex-hooks.d.ts.map +1 -0
  13. package/dist/lib/codex-hooks.js +65 -0
  14. package/dist/lib/codex-hooks.js.map +1 -0
  15. package/dist/lib/codex-hooks.test.d.ts +2 -0
  16. package/dist/lib/codex-hooks.test.d.ts.map +1 -0
  17. package/dist/lib/codex-hooks.test.js +64 -0
  18. package/dist/lib/codex-hooks.test.js.map +1 -0
  19. package/dist/lib/editor.codex.test.js +20 -0
  20. package/dist/lib/editor.codex.test.js.map +1 -1
  21. package/dist/lib/editor.d.ts +2 -0
  22. package/dist/lib/editor.d.ts.map +1 -1
  23. package/dist/lib/editor.js +28 -0
  24. package/dist/lib/editor.js.map +1 -1
  25. package/dist/lib/mcp-bundle.test.js +2 -2
  26. package/dist/lib/release-guard.d.ts +22 -0
  27. package/dist/lib/release-guard.d.ts.map +1 -0
  28. package/dist/lib/release-guard.js +46 -0
  29. package/dist/lib/release-guard.js.map +1 -0
  30. package/dist/lib/release-guard.test.d.ts +2 -0
  31. package/dist/lib/release-guard.test.d.ts.map +1 -0
  32. package/dist/lib/release-guard.test.js +60 -0
  33. package/dist/lib/release-guard.test.js.map +1 -0
  34. package/package.json +10 -11
  35. package/scripts/vendor-codex-hooks.mjs +31 -0
  36. package/scripts/verify-publish-tarball.mjs +9 -0
  37. package/scripts/verify-release-artifacts.mjs +77 -0
  38. package/vendor/codex-hooks/lib.mjs +276 -0
  39. package/vendor/codex-hooks/session-start.mjs +29 -0
  40. package/vendor/codex-hooks/stop.mjs +78 -0
  41. package/vendor/codex-hooks/user-prompt-submit.mjs +95 -0
  42. package/vendor/sensing-mcp/dist/index.js +26 -1
  43. package/vendor/sensing-mcp/package.json +1 -1
@@ -0,0 +1,276 @@
1
+ // plugins/claude-code/hooks/lib.mjs
2
+ // ─────────────────────────────────────────────────────────────
3
+ // Shared plumbing for RoBrain Claude Code plugin hooks.
4
+ //
5
+ // Design constraints:
6
+ // - Zero dependencies, Node >= 18 (built-in fetch). Hooks must start fast.
7
+ // - Fail-open ALWAYS: a dead Perception, missing config, or timeout must
8
+ // never block or break the user's Claude Code session. Hooks print nothing
9
+ // and exit 0 on any failure.
10
+ // - Thin client: raw turns go to Perception POST /signals with
11
+ // needs_classification=true — classification/extraction/dedup happen
12
+ // server-side, so this plugin (and future Codex/Cursor ports) stays dumb.
13
+ // ─────────────────────────────────────────────────────────────
14
+
15
+ import { createHash } from 'node:crypto'
16
+ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from 'node:fs'
17
+ import { homedir } from 'node:os'
18
+ import { join } from 'node:path'
19
+
20
+ /** Read all of stdin (the hook input JSON). */
21
+ export async function readStdin() {
22
+ const chunks = []
23
+ for await (const chunk of process.stdin) chunks.push(chunk)
24
+ return Buffer.concat(chunks).toString('utf8')
25
+ }
26
+
27
+ export function parseHookInput(raw) {
28
+ try {
29
+ return JSON.parse(raw)
30
+ } catch {
31
+ return {}
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Perception connection: env wins, then ~/.robrain/config.json (written by
37
+ * `robrain up` / `robrain install`), then localhost default.
38
+ */
39
+ export function loadPerception() {
40
+ let url = process.env.PERCEPTION_URL?.trim() || ''
41
+ let key = process.env.PERCEPTION_API_KEY?.trim() || ''
42
+ if (!url || !key) {
43
+ try {
44
+ const cfg = JSON.parse(readFileSync(join(homedir(), '.robrain', 'config.json'), 'utf8'))
45
+ if (!url && typeof cfg.perceptionUrl === 'string') url = cfg.perceptionUrl.trim()
46
+ if (!key && typeof cfg.perceptionKey === 'string') key = cfg.perceptionKey.trim()
47
+ } catch {
48
+ // no config yet — fall through to defaults
49
+ }
50
+ }
51
+ return { url: url || 'http://localhost:3001', key }
52
+ }
53
+
54
+ /**
55
+ * Same derivation as the CLI (packages/cli/src/lib/project.ts): prefer the
56
+ * project_id init-project wrote into CLAUDE.md / AGENTS.md / the Cursor rule,
57
+ * else a stable hash of the working directory.
58
+ */
59
+ export function resolveProjectId(cwd) {
60
+ for (const rel of ['CLAUDE.md', 'AGENTS.md', join('.cursor', 'rules', 'robrain.mdc')]) {
61
+ const p = join(cwd, rel)
62
+ try {
63
+ if (!existsSync(p)) continue
64
+ const match = readFileSync(p, 'utf8').match(/project_id="([^"]+)"/)
65
+ if (match?.[1]) return match[1].trim()
66
+ } catch {
67
+ // unreadable file — try the next candidate
68
+ }
69
+ }
70
+ return createHash('sha256').update(cwd).digest('hex').slice(0, 12)
71
+ }
72
+
73
+ /** fetch with a hard timeout; returns null instead of throwing. */
74
+ export async function perceptionFetch(path, { url, key }, init = {}, timeoutMs = 2500) {
75
+ try {
76
+ const res = await fetch(`${url}${path}`, {
77
+ ...init,
78
+ headers: {
79
+ 'Content-Type': 'application/json',
80
+ ...(key ? { Authorization: `Bearer ${key}` } : {}),
81
+ ...(init.headers ?? {}),
82
+ },
83
+ signal: AbortSignal.timeout(timeoutMs),
84
+ })
85
+ if (!res.ok) return null
86
+ return await res.json()
87
+ } catch {
88
+ return null
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Extract the trailing turn from a Claude Code transcript (JSONL): the last
94
+ * user text message and every assistant text block after it, plus file paths
95
+ * touched by Edit/Write/NotebookEdit tool calls in that span.
96
+ */
97
+ export function extractLastTurn(transcriptPath) {
98
+ let lines
99
+ try {
100
+ lines = readFileSync(transcriptPath, 'utf8').split('\n').filter(Boolean)
101
+ } catch {
102
+ return null
103
+ }
104
+
105
+ let lastUserIdx = -1
106
+ const entries = []
107
+ for (const line of lines) {
108
+ let e
109
+ try {
110
+ e = JSON.parse(line)
111
+ } catch {
112
+ continue
113
+ }
114
+ entries.push(e)
115
+ }
116
+
117
+ for (let i = entries.length - 1; i >= 0; i--) {
118
+ const e = entries[i]
119
+ if (e.type === 'user' && !e.isMeta && typeof textOf(e.message?.content) === 'string' && textOf(e.message.content).trim()) {
120
+ // Skip tool_result-only user entries (tool outputs come back as user role)
121
+ if (hasOnlyToolResults(e.message?.content)) continue
122
+ lastUserIdx = i
123
+ break
124
+ }
125
+ }
126
+ if (lastUserIdx === -1) return null
127
+
128
+ const userMessage = textOf(entries[lastUserIdx].message?.content) ?? ''
129
+ const replyParts = []
130
+ const filesTouched = new Set()
131
+
132
+ for (let i = lastUserIdx + 1; i < entries.length; i++) {
133
+ const e = entries[i]
134
+ if (e.type !== 'assistant') continue
135
+ const content = e.message?.content
136
+ if (!Array.isArray(content)) continue
137
+ for (const block of content) {
138
+ if (block.type === 'text' && block.text?.trim()) replyParts.push(block.text)
139
+ if (block.type === 'tool_use') {
140
+ const fp = block.input?.file_path ?? block.input?.notebook_path
141
+ if (typeof fp === 'string' && fp) filesTouched.add(fp)
142
+ }
143
+ }
144
+ }
145
+
146
+ if (!userMessage.trim() || replyParts.length === 0) return null
147
+
148
+ // Sequence: number of user-authored messages up to and including this one —
149
+ // stable across re-reads of the same transcript.
150
+ let sequence = 0
151
+ for (let i = 0; i <= lastUserIdx; i++) {
152
+ const e = entries[i]
153
+ if (e.type === 'user' && !e.isMeta && !hasOnlyToolResults(e.message?.content) && textOf(e.message?.content)?.trim()) sequence++
154
+ }
155
+
156
+ return {
157
+ userMessage,
158
+ claudeReply: replyParts.join('\n\n'),
159
+ filesTouched: [...filesTouched].slice(0, 50),
160
+ sequence: Math.max(1, sequence),
161
+ }
162
+ }
163
+
164
+ function textOf(content) {
165
+ if (typeof content === 'string') return content
166
+ if (Array.isArray(content)) {
167
+ return content.filter(b => b.type === 'text').map(b => b.text ?? '').join('\n')
168
+ }
169
+ return null
170
+ }
171
+
172
+ function hasOnlyToolResults(content) {
173
+ return Array.isArray(content) && content.length > 0 && content.every(b => b.type === 'tool_result')
174
+ }
175
+
176
+ // ── Session-scoped warning state ──────────────────────────────
177
+ // The same veto re-injected on every on-topic prompt is context spend for
178
+ // zero new information — Control must never cost more context than it saves.
179
+
180
+ const STATE_DIR = join(homedir(), '.robrain', 'state')
181
+
182
+ function warnedFile(sessionId) {
183
+ const safe = sessionId.replace(/[^A-Za-z0-9._-]/g, '_').slice(0, 120)
184
+ return join(STATE_DIR, `cc-warned-${safe}.json`)
185
+ }
186
+
187
+ /** Decision ids already warned about in this session. */
188
+ export function loadWarnedIds(sessionId) {
189
+ if (!sessionId) return new Set()
190
+ try {
191
+ const ids = JSON.parse(readFileSync(warnedFile(sessionId), 'utf8'))
192
+ return new Set(Array.isArray(ids) ? ids : [])
193
+ } catch {
194
+ return new Set()
195
+ }
196
+ }
197
+
198
+ export function recordWarnedIds(sessionId, ids) {
199
+ if (!sessionId || ids.length === 0) return
200
+ try {
201
+ mkdirSync(STATE_DIR, { recursive: true })
202
+ const merged = [...new Set([...loadWarnedIds(sessionId), ...ids])].slice(-200)
203
+ writeFileSync(warnedFile(sessionId), JSON.stringify(merged))
204
+ cleanupStateDir()
205
+ } catch {
206
+ // fail-open — a broken state file must never block the session
207
+ }
208
+ }
209
+
210
+ /** Best-effort hygiene: drop state files older than 7 days. */
211
+ function cleanupStateDir() {
212
+ try {
213
+ const cutoff = Date.now() - 7 * 24 * 60 * 60 * 1000
214
+ for (const f of readdirSync(STATE_DIR)) {
215
+ const p = join(STATE_DIR, f)
216
+ try {
217
+ if (statSync(p).mtimeMs < cutoff) unlinkSync(p)
218
+ } catch {
219
+ // race or permissions — skip
220
+ }
221
+ }
222
+ } catch {
223
+ // dir unreadable — skip
224
+ }
225
+ }
226
+
227
+ // ── Per-session prompt stash ──────────────────────────────────
228
+ // Written by the UserPromptSubmit hook, read by stop hooks that cannot rely
229
+ // on a parseable transcript (Codex ships last_assistant_message on Stop stdin
230
+ // but its transcript format is not a contract we own). Pairing the stashed
231
+ // prompt with that message gives deterministic capture with zero transcript
232
+ // assumptions.
233
+
234
+ function stashFile(sessionId) {
235
+ const safe = sessionId.replace(/[^A-Za-z0-9._-]/g, '_').slice(0, 120)
236
+ return join(STATE_DIR, `stash-${safe}.json`)
237
+ }
238
+
239
+ export function stashPrompt(sessionId, prompt) {
240
+ if (!sessionId || !prompt) return
241
+ try {
242
+ mkdirSync(STATE_DIR, { recursive: true })
243
+ const prev = loadPromptStash(sessionId)
244
+ writeFileSync(stashFile(sessionId), JSON.stringify({
245
+ prompt: prompt.slice(0, 60_000),
246
+ sequence: (prev?.sequence ?? 0) + 1,
247
+ }))
248
+ } catch {
249
+ // fail-open
250
+ }
251
+ }
252
+
253
+ export function loadPromptStash(sessionId) {
254
+ if (!sessionId) return null
255
+ try {
256
+ const s = JSON.parse(readFileSync(stashFile(sessionId), 'utf8'))
257
+ return typeof s?.prompt === 'string' ? s : null
258
+ } catch {
259
+ return null
260
+ }
261
+ }
262
+
263
+ /** Emit hook JSON output (context injection) and exit 0. */
264
+ export function emitContext(hookEventName, additionalContext) {
265
+ if (additionalContext) {
266
+ process.stdout.write(JSON.stringify({
267
+ hookSpecificOutput: { hookEventName, additionalContext },
268
+ }))
269
+ }
270
+ process.exit(0)
271
+ }
272
+
273
+ /** Silent success — hooks must never fail the session. */
274
+ export function exitSilently() {
275
+ process.exit(0)
276
+ }
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env node
2
+ // SessionStart hook — inject the always-on project summary (top decisions with
3
+ // their rejected alternatives) at the start of every Claude Code session.
4
+ // Deterministic replacement for the model remembering to call sensing_start_session.
5
+
6
+ import {
7
+ readStdin, parseHookInput, loadPerception, resolveProjectId,
8
+ perceptionFetch, emitContext, exitSilently,
9
+ } from './lib.mjs'
10
+
11
+ const input = parseHookInput(await readStdin())
12
+ const cwd = input.cwd ?? process.cwd()
13
+ const perception = loadPerception()
14
+ const projectId = resolveProjectId(cwd)
15
+
16
+ const summary = await perceptionFetch(`/projects/${projectId}/summary`, perception, {}, 3000)
17
+ const text = summary?.summary ?? summary?.always_on_summary
18
+ if (!text || typeof text !== 'string' || !text.trim()) exitSilently()
19
+
20
+ emitContext(
21
+ 'SessionStart',
22
+ [
23
+ '## RoBrain project memory (always-on summary)',
24
+ 'Prior decisions for this project, including rejected alternatives. Respect the rejections —',
25
+ 'if a task invites a previously rejected approach, surface the rejection instead of re-proposing it.',
26
+ '',
27
+ text.trim(),
28
+ ].join('\n'),
29
+ )
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env node
2
+ // Codex Stop hook — deterministic turn capture for Codex CLI / IDE.
3
+ //
4
+ // Codex's Stop stdin carries `last_assistant_message` directly, and the
5
+ // UserPromptSubmit hook stashed the user prompt — so capture needs ZERO
6
+ // assumptions about Codex's transcript format (which is not a contract we
7
+ // own). The transcript, when it happens to parse as Claude-style JSONL, only
8
+ // enriches the signal with files touched.
9
+ //
10
+ // Ships the turn to Perception POST /signals with needs_classification=true;
11
+ // extraction, the confidence gate, and near-duplicate dedup run server-side —
12
+ // including dedup against turns captured via the Sensing MCP protocol, so
13
+ // running both double-captures nothing.
14
+
15
+ import {
16
+ readStdin, parseHookInput, loadPerception, resolveProjectId,
17
+ perceptionFetch, extractLastTurn, loadPromptStash, exitSilently,
18
+ } from './lib.mjs'
19
+
20
+ const input = parseHookInput(await readStdin())
21
+
22
+ // stop_hook_active guards re-entry when a stop hook itself continues the turn.
23
+ if (input.stop_hook_active) exitSilently()
24
+
25
+ const sessionId = typeof input.session_id === 'string' ? input.session_id : ''
26
+ const cwd = input.cwd ?? process.cwd()
27
+ if (!sessionId) exitSilently()
28
+
29
+ // Reply: stdin first; transcript parse as fallback.
30
+ let reply = typeof input.last_assistant_message === 'string' ? input.last_assistant_message : ''
31
+
32
+ // Prompt: the UserPromptSubmit stash is authoritative; transcript is fallback.
33
+ const stash = loadPromptStash(sessionId)
34
+ let userMessage = stash?.prompt ?? ''
35
+ let sequence = stash?.sequence ?? 1
36
+ let filesTouched = []
37
+
38
+ if ((!userMessage || !reply) && input.transcript_path) {
39
+ const turn = extractLastTurn(input.transcript_path)
40
+ if (turn) {
41
+ if (!userMessage) { userMessage = turn.userMessage; sequence = turn.sequence }
42
+ if (!reply) reply = turn.claudeReply
43
+ filesTouched = turn.filesTouched
44
+ }
45
+ } else if (input.transcript_path) {
46
+ // Both present — transcript only enriches with files touched, best-effort.
47
+ const turn = extractLastTurn(input.transcript_path)
48
+ if (turn) filesTouched = turn.filesTouched
49
+ }
50
+
51
+ if (!userMessage.trim() || !reply.trim()) exitSilently()
52
+
53
+ const perception = loadPerception()
54
+ const projectId = resolveProjectId(cwd)
55
+
56
+ await perceptionFetch('/signals', perception, {
57
+ method: 'POST',
58
+ headers: { 'X-Project-Id': projectId },
59
+ body: JSON.stringify({
60
+ signal: {
61
+ turn: {
62
+ session_id: `codex-${sessionId}`.slice(0, 200),
63
+ sequence,
64
+ user_message: userMessage.slice(0, 60_000),
65
+ claude_reply: reply.slice(0, 60_000),
66
+ files_touched: filesTouched,
67
+ timestamp: new Date().toISOString(),
68
+ },
69
+ decision_type: 'unclassified',
70
+ confidence: 0,
71
+ files_affected: filesTouched,
72
+ scope: 'team',
73
+ needs_classification: true,
74
+ },
75
+ }),
76
+ }, 8000)
77
+
78
+ exitSilently()
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env node
2
+ // UserPromptSubmit hook — pre-task veto injection, two tiers:
3
+ // 1. Deterministic: POST /veto-scan — does the prompt literally name an
4
+ // option an active decision rejected? Zero embeddings, zero LLM, fires
5
+ // for ANY prompt length. An exact mention warns at confidence 1.0.
6
+ // 2. Semantic: GET /decisions?query= for prompts long enough to embed.
7
+ // Gated on SIMILARITY, not planning_score — planning_score blends in
8
+ // recency decay, and a rejection must not fade from warnings just
9
+ // because it is old.
10
+ // Warnings already shown this session are not repeated (state file) —
11
+ // injected context must never cost more than it saves.
12
+
13
+ import {
14
+ readStdin, parseHookInput, loadPerception, resolveProjectId,
15
+ perceptionFetch, emitContext, exitSilently, loadWarnedIds, recordWarnedIds,
16
+ stashPrompt,
17
+ } from './lib.mjs'
18
+
19
+ const input = parseHookInput(await readStdin())
20
+ const prompt = (input.prompt ?? '').trim()
21
+
22
+ // Skip: slash commands and empty prompts. The deterministic tier is cheap
23
+ // enough to run even on short prompts ("use Pinecone now" must warn).
24
+ if (!prompt || prompt.startsWith('/')) exitSilently()
25
+
26
+ const cwd = input.cwd ?? process.cwd()
27
+ const sessionId = typeof input.session_id === 'string' ? input.session_id : ''
28
+
29
+ // Stash the prompt for stop-hook capture on clients whose transcript format
30
+ // we do not parse (Codex pairs this with last_assistant_message).
31
+ stashPrompt(sessionId, prompt)
32
+ const perception = loadPerception()
33
+ const projectId = resolveProjectId(cwd)
34
+
35
+ // Tier 1 — deterministic rejected-option scan.
36
+ const scan = await perceptionFetch('/veto-scan', perception, {
37
+ method: 'POST',
38
+ body: JSON.stringify({ project_id: projectId, text: prompt.slice(0, 2000) }),
39
+ }, 1500)
40
+ const exact = (Array.isArray(scan?.matches) ? scan.matches : [])
41
+ .map(m => ({ ...m, exact: true }))
42
+
43
+ // Tier 2 — semantic search only for prompts worth an embedding round-trip
44
+ // (greetings, "yes", … skip it).
45
+ const MIN_SIMILARITY = 0.45
46
+ let semantic = []
47
+ if (prompt.length >= 24) {
48
+ const params = new URLSearchParams({
49
+ project_id: projectId,
50
+ query: prompt.slice(0, 2000),
51
+ limit: '8',
52
+ })
53
+ const data = await perceptionFetch(`/decisions?${params}`, perception, {}, 2500)
54
+ const decisions = Array.isArray(data) ? data : data?.decisions
55
+ if (Array.isArray(decisions)) {
56
+ semantic = decisions
57
+ .filter(d => Array.isArray(d.rejected) && d.rejected.length > 0)
58
+ .filter(d => typeof d.similarity !== 'number' || d.similarity >= MIN_SIMILARITY)
59
+ .sort((a, b) => (b.similarity ?? 0) - (a.similarity ?? 0))
60
+ }
61
+ }
62
+
63
+ // Merge: exact matches first (exempt from the similarity gate), then
64
+ // semantic; skip anything already warned in this session.
65
+ const warned = loadWarnedIds(sessionId)
66
+ const seen = new Set()
67
+ const vetoes = []
68
+ for (const d of [...exact, ...semantic]) {
69
+ if (!d.id || seen.has(d.id) || warned.has(d.id)) continue
70
+ seen.add(d.id)
71
+ vetoes.push(d)
72
+ if (vetoes.length === 3) break
73
+ }
74
+
75
+ if (vetoes.length === 0) exitSilently()
76
+
77
+ const lines = vetoes.map(d => {
78
+ const rej = d.rejected
79
+ .slice(0, 2)
80
+ .map(r => `**${r.option}** (${r.reason})`)
81
+ .join('; ')
82
+ const tag = d.exact ? ' *(exact match on rejected option)*' : ''
83
+ return `- ${d.decision}${tag}\n Rejected: ${rej}`
84
+ })
85
+
86
+ recordWarnedIds(sessionId, vetoes.map(d => d.id))
87
+
88
+ emitContext(
89
+ 'UserPromptSubmit',
90
+ [
91
+ '⚠ RoBrain — this task touches decisions with previously REJECTED approaches:',
92
+ ...lines,
93
+ 'Do not re-propose a rejected option without flagging the prior rejection and why circumstances changed.',
94
+ ].join('\n'),
95
+ )
@@ -6911,6 +6911,30 @@ var require_redact = __commonJS({
6911
6911
  }
6912
6912
  });
6913
6913
 
6914
+ // ../shared/dist/veto-match.js
6915
+ var require_veto_match = __commonJS({
6916
+ "../shared/dist/veto-match.js"(exports) {
6917
+ "use strict";
6918
+ Object.defineProperty(exports, "__esModule", { value: true });
6919
+ exports.proposalMatchesRejectedOption = proposalMatchesRejectedOption;
6920
+ function proposalMatchesRejectedOption(proposalText, rejected) {
6921
+ if (!rejected?.length)
6922
+ return null;
6923
+ for (const r of rejected) {
6924
+ const option = r.option.trim();
6925
+ if (option.length < 3)
6926
+ continue;
6927
+ const escaped = option.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6928
+ const lead = /^[A-Za-z0-9_]/.test(option) ? "\\b" : "";
6929
+ const trail = /[A-Za-z0-9_]$/.test(option) ? "\\b" : "";
6930
+ if (new RegExp(`${lead}${escaped}${trail}`, "i").test(proposalText))
6931
+ return r;
6932
+ }
6933
+ return null;
6934
+ }
6935
+ }
6936
+ });
6937
+
6914
6938
  // ../shared/dist/extract-decision.js
6915
6939
  var require_extract_decision = __commonJS({
6916
6940
  "../shared/dist/extract-decision.js"(exports) {
@@ -7045,6 +7069,7 @@ var require_dist = __commonJS({
7045
7069
  __exportStar(require_llm(), exports);
7046
7070
  __exportStar(require_embeddings(), exports);
7047
7071
  __exportStar(require_redact(), exports);
7072
+ __exportStar(require_veto_match(), exports);
7048
7073
  __exportStar(require_extract_decision(), exports);
7049
7074
  __exportStar(require_db(), exports);
7050
7075
  }
@@ -23037,7 +23062,7 @@ function resolveSessionId(raw) {
23037
23062
  function buildServer() {
23038
23063
  const server = new McpServer({
23039
23064
  name: "sensing-mcp",
23040
- version: "2.3.7"
23065
+ version: "2.3.9"
23041
23066
  });
23042
23067
  server.tool(
23043
23068
  "sensing_start_session",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@robrain/sensing-mcp",
3
- "version": "2.3.7",
3
+ "version": "2.3.9",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "main": "./dist/index.js"