@abraca/mcp 1.0.15 → 1.0.18
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/dist/abracadabra-mcp.cjs +511 -102
- package/dist/abracadabra-mcp.cjs.map +1 -1
- package/dist/abracadabra-mcp.esm.js +510 -102
- package/dist/abracadabra-mcp.esm.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/package.json +1 -1
- package/src/converters/markdownToYjs.ts +52 -2
- package/src/converters/types.ts +54 -4
- package/src/converters/yjsToMarkdown.ts +5 -0
- package/src/hook-bridge.ts +204 -0
- package/src/index.ts +17 -1
- package/src/resources/agent-guide.ts +203 -92
- package/src/server.ts +18 -3
- package/src/tools/content.ts +1 -1
- package/src/tools/hooks.ts +42 -0
- package/src/tools/meta.ts +1 -1
- package/src/tools/tree.ts +4 -1
package/dist/index.d.ts
CHANGED
|
@@ -8783,8 +8783,10 @@ declare class AbracadabraMCPServer {
|
|
|
8783
8783
|
private _handleStatelessChat;
|
|
8784
8784
|
/**
|
|
8785
8785
|
* Set the agent's status in root awareness with auto-clear after idle.
|
|
8786
|
+
* @param statusContext — scopes the status to a specific channel/context so the
|
|
8787
|
+
* dashboard only shows it in the relevant chat. Defaults to `_lastChatChannel`.
|
|
8786
8788
|
*/
|
|
8787
|
-
setAutoStatus(status: string | null, docId?: string): void;
|
|
8789
|
+
setAutoStatus(status: string | null, docId?: string, statusContext?: string | null): void;
|
|
8788
8790
|
/** Re-send typing indicator every 2s so dashboard keeps showing it (expires at 3s). */
|
|
8789
8791
|
private _startTypingInterval;
|
|
8790
8792
|
private _stopTypingInterval;
|
package/package.json
CHANGED
|
@@ -113,6 +113,10 @@ export function parseFrontmatter(markdown: string): FrontmatterResult {
|
|
|
113
113
|
if (subtitle) meta.subtitle = subtitle
|
|
114
114
|
const url = getStr(['url'])
|
|
115
115
|
if (url) meta.url = url
|
|
116
|
+
const email = getStr(['email'])
|
|
117
|
+
if (email) meta.email = email
|
|
118
|
+
const phone = getStr(['phone'])
|
|
119
|
+
if (phone) meta.phone = phone
|
|
116
120
|
|
|
117
121
|
const ratingRaw = getStr(['rating'])
|
|
118
122
|
if (ratingRaw !== undefined) {
|
|
@@ -120,6 +124,36 @@ export function parseFrontmatter(markdown: string): FrontmatterResult {
|
|
|
120
124
|
if (!Number.isNaN(n)) meta.rating = Math.min(5, Math.max(0, n))
|
|
121
125
|
}
|
|
122
126
|
|
|
127
|
+
// Datetime fields
|
|
128
|
+
const datetimeStart = getStr(['datetimeStart'])
|
|
129
|
+
if (datetimeStart) meta.datetimeStart = datetimeStart
|
|
130
|
+
const datetimeEnd = getStr(['datetimeEnd'])
|
|
131
|
+
if (datetimeEnd) meta.datetimeEnd = datetimeEnd
|
|
132
|
+
const allDayRaw = raw['allDay']
|
|
133
|
+
if (allDayRaw !== undefined) meta.allDay = allDayRaw === 'true' || allDayRaw === true
|
|
134
|
+
|
|
135
|
+
// Geo fields
|
|
136
|
+
const geoLatRaw = getStr(['geoLat'])
|
|
137
|
+
if (geoLatRaw !== undefined) { const n = Number(geoLatRaw); if (!Number.isNaN(n)) meta.geoLat = n }
|
|
138
|
+
const geoLngRaw = getStr(['geoLng'])
|
|
139
|
+
if (geoLngRaw !== undefined) { const n = Number(geoLngRaw); if (!Number.isNaN(n)) meta.geoLng = n }
|
|
140
|
+
const geoType = getStr(['geoType'])
|
|
141
|
+
if (geoType && (geoType === 'marker' || geoType === 'line' || geoType === 'measure')) {
|
|
142
|
+
meta.geoType = geoType
|
|
143
|
+
}
|
|
144
|
+
const geoDescription = getStr(['geoDescription'])
|
|
145
|
+
if (geoDescription) meta.geoDescription = geoDescription
|
|
146
|
+
|
|
147
|
+
// Numeric fields
|
|
148
|
+
const numberRaw = getStr(['number'])
|
|
149
|
+
if (numberRaw !== undefined) { const n = Number(numberRaw); if (!Number.isNaN(n)) meta.number = n }
|
|
150
|
+
const unit = getStr(['unit'])
|
|
151
|
+
if (unit) meta.unit = unit
|
|
152
|
+
|
|
153
|
+
// Note
|
|
154
|
+
const note = getStr(['note'])
|
|
155
|
+
if (note) meta.note = note
|
|
156
|
+
|
|
123
157
|
const title = typeof raw['title'] === 'string' ? raw['title'] : undefined
|
|
124
158
|
|
|
125
159
|
return { title, meta, body }
|
|
@@ -156,8 +190,10 @@ function parseInline(text: string): InlineToken[] {
|
|
|
156
190
|
const kbdProps = parseMdcProps(`{${match[4]}}`)
|
|
157
191
|
tokens.push({ text: kbdProps['value'] || '', attrs: { kbd: { value: kbdProps['value'] || '' } } })
|
|
158
192
|
} else if (match[5] !== undefined) {
|
|
159
|
-
|
|
160
|
-
|
|
193
|
+
// Inline wikilink [[docId]] or [[docId|label]] → link to /doc/docId
|
|
194
|
+
const docId = match[5]
|
|
195
|
+
const displayText = match[6] ?? docId
|
|
196
|
+
tokens.push({ text: displayText!, attrs: { link: { href: `/doc/${docId}` } } })
|
|
161
197
|
} else if (match[7] !== undefined) {
|
|
162
198
|
tokens.push({ text: match[7], attrs: { strike: true } })
|
|
163
199
|
} else if (match[8] !== undefined) {
|
|
@@ -211,6 +247,7 @@ type Block =
|
|
|
211
247
|
| { type: 'field'; name: string; fieldType: string; required: boolean; innerBlocks: Block[] }
|
|
212
248
|
| { type: 'fieldGroup'; fields: Block[] }
|
|
213
249
|
| { type: 'image'; src: string; alt: string; width?: string; height?: string }
|
|
250
|
+
| { type: 'docEmbed'; docId: string }
|
|
214
251
|
|
|
215
252
|
function parseTableRow(line: string): string[] {
|
|
216
253
|
const parts = line.split('|')
|
|
@@ -339,6 +376,13 @@ function parseBlocks(markdown: string): Block[] {
|
|
|
339
376
|
continue
|
|
340
377
|
}
|
|
341
378
|
|
|
379
|
+
const docEmbedMatch = line.match(/^!\[\[([^\]|]+?)(?:\|[^\]]*?)?\]\]\s*$/)
|
|
380
|
+
if (docEmbedMatch) {
|
|
381
|
+
blocks.push({ type: 'docEmbed', docId: docEmbedMatch[1]! })
|
|
382
|
+
i++
|
|
383
|
+
continue
|
|
384
|
+
}
|
|
385
|
+
|
|
342
386
|
const imgMatch = line.match(/^!\[([^\]]*)\]\(([^)]+)\)(\{[^}]*\})?\s*$/)
|
|
343
387
|
if (imgMatch) {
|
|
344
388
|
const alt = imgMatch[1] ?? ''
|
|
@@ -590,6 +634,7 @@ function blockElName(b: Block): string {
|
|
|
590
634
|
case 'field': return 'field'
|
|
591
635
|
case 'fieldGroup': return 'fieldGroup'
|
|
592
636
|
case 'image': return 'image'
|
|
637
|
+
case 'docEmbed': return 'docEmbed'
|
|
593
638
|
}
|
|
594
639
|
}
|
|
595
640
|
|
|
@@ -783,6 +828,10 @@ function fillBlock(el: Y.XmlElement, block: Block): void {
|
|
|
783
828
|
if (block.height) el.setAttribute('height', block.height)
|
|
784
829
|
break
|
|
785
830
|
}
|
|
831
|
+
case 'docEmbed': {
|
|
832
|
+
el.setAttribute('docId', block.docId)
|
|
833
|
+
break
|
|
834
|
+
}
|
|
786
835
|
}
|
|
787
836
|
}
|
|
788
837
|
|
|
@@ -838,6 +887,7 @@ export function populateYDocFromMarkdown(
|
|
|
838
887
|
case 'field': return new Y.XmlElement('field')
|
|
839
888
|
case 'fieldGroup': return new Y.XmlElement('fieldGroup')
|
|
840
889
|
case 'image': return new Y.XmlElement('image')
|
|
890
|
+
case 'docEmbed': return new Y.XmlElement('docEmbed')
|
|
841
891
|
}
|
|
842
892
|
})
|
|
843
893
|
|
package/src/converters/types.ts
CHANGED
|
@@ -17,8 +17,13 @@ export interface UserMetaField {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export interface PageMeta extends Record<string, unknown> {
|
|
20
|
+
// Universal display
|
|
20
21
|
color?: string
|
|
21
22
|
icon?: string
|
|
23
|
+
subtitle?: string
|
|
24
|
+
note?: string
|
|
25
|
+
|
|
26
|
+
// Datetime
|
|
22
27
|
datetimeStart?: string
|
|
23
28
|
datetimeEnd?: string
|
|
24
29
|
allDay?: boolean
|
|
@@ -26,39 +31,84 @@ export interface PageMeta extends Record<string, unknown> {
|
|
|
26
31
|
dateEnd?: string
|
|
27
32
|
timeStart?: string
|
|
28
33
|
timeEnd?: string
|
|
29
|
-
|
|
30
|
-
|
|
34
|
+
|
|
35
|
+
// Task/status
|
|
31
36
|
checked?: boolean
|
|
32
37
|
priority?: number
|
|
33
38
|
status?: string
|
|
39
|
+
taskProgress?: number
|
|
34
40
|
rating?: number
|
|
41
|
+
tags?: string[]
|
|
42
|
+
members?: { id: string; label: string }[]
|
|
43
|
+
|
|
44
|
+
// Contact/value
|
|
35
45
|
url?: string
|
|
36
46
|
email?: string
|
|
37
47
|
phone?: string
|
|
38
48
|
number?: number
|
|
39
49
|
unit?: string
|
|
40
|
-
|
|
41
|
-
|
|
50
|
+
|
|
51
|
+
// Cover image
|
|
42
52
|
coverUploadId?: string
|
|
43
53
|
coverDocId?: string
|
|
44
54
|
coverMimeType?: string
|
|
55
|
+
|
|
56
|
+
// Geo/Map
|
|
45
57
|
geoType?: 'marker' | 'line' | 'measure'
|
|
46
58
|
geoLat?: number
|
|
47
59
|
geoLng?: number
|
|
48
60
|
geoDescription?: string
|
|
61
|
+
|
|
62
|
+
// Whiteboard
|
|
49
63
|
wbX?: number
|
|
50
64
|
wbY?: number
|
|
51
65
|
wbW?: number
|
|
52
66
|
wbH?: number
|
|
53
67
|
wbBg?: string
|
|
68
|
+
|
|
69
|
+
// Dashboard
|
|
54
70
|
deskX?: number
|
|
55
71
|
deskY?: number
|
|
72
|
+
deskZ?: number
|
|
56
73
|
deskMode?: string
|
|
74
|
+
|
|
75
|
+
// Mindmap
|
|
57
76
|
mmX?: number
|
|
58
77
|
mmY?: number
|
|
78
|
+
|
|
79
|
+
// Graph
|
|
59
80
|
graphX?: number
|
|
60
81
|
graphY?: number
|
|
61
82
|
graphPinned?: boolean
|
|
83
|
+
showRefEdges?: boolean
|
|
84
|
+
|
|
85
|
+
// Spatial (3D)
|
|
86
|
+
spShape?: string
|
|
87
|
+
spColor?: string
|
|
88
|
+
spOpacity?: number
|
|
89
|
+
spX?: number
|
|
90
|
+
spY?: number
|
|
91
|
+
spZ?: number
|
|
92
|
+
spRX?: number
|
|
93
|
+
spRY?: number
|
|
94
|
+
spRZ?: number
|
|
95
|
+
spSX?: number
|
|
96
|
+
spSY?: number
|
|
97
|
+
spSZ?: number
|
|
98
|
+
spModelUploadId?: string
|
|
99
|
+
spModelDocId?: string
|
|
100
|
+
|
|
101
|
+
// Renderer config (set on the page doc itself, not children)
|
|
102
|
+
kanbanColumnWidth?: string
|
|
103
|
+
galleryColumns?: number
|
|
104
|
+
galleryAspect?: string
|
|
105
|
+
calendarView?: string
|
|
106
|
+
calendarWeekStart?: string
|
|
107
|
+
calendarShowWeekNumbers?: boolean
|
|
108
|
+
tableMode?: string
|
|
109
|
+
timelineZoom?: string
|
|
110
|
+
|
|
111
|
+
// Internal
|
|
62
112
|
_metaFields?: UserMetaField[]
|
|
63
113
|
_metaInitialized?: boolean
|
|
64
114
|
}
|
|
@@ -95,6 +95,11 @@ function serializeElement(el: Y.XmlElement, indent = ''): string {
|
|
|
95
95
|
case 'table':
|
|
96
96
|
return serializeTable(el)
|
|
97
97
|
|
|
98
|
+
case 'docEmbed': {
|
|
99
|
+
const docId = el.getAttribute('docId')
|
|
100
|
+
return docId ? `![[${docId}]]` : ''
|
|
101
|
+
}
|
|
102
|
+
|
|
98
103
|
case 'image': {
|
|
99
104
|
const src = el.getAttribute('src') || ''
|
|
100
105
|
const alt = el.getAttribute('alt') || ''
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HookBridge — lightweight HTTP server that receives Claude Code hook events
|
|
3
|
+
* and translates them into Yjs awareness updates via AbracadabraMCPServer.
|
|
4
|
+
*
|
|
5
|
+
* Claude Code hooks (PreToolUse, PostToolUse, SubagentStart, SubagentStop, Stop)
|
|
6
|
+
* POST JSON to http://127.0.0.1:{port}/hook. The bridge maps these to awareness
|
|
7
|
+
* fields (status, activeToolCall) so the cou-sh dashboard shows real-time activity.
|
|
8
|
+
*/
|
|
9
|
+
import * as http from 'node:http'
|
|
10
|
+
import * as fs from 'node:fs'
|
|
11
|
+
import * as os from 'node:os'
|
|
12
|
+
import * as path from 'node:path'
|
|
13
|
+
import type { AbracadabraMCPServer } from './server.ts'
|
|
14
|
+
|
|
15
|
+
/** Map Claude Code tool names to awareness-friendly names + extract a target string. */
|
|
16
|
+
function mapToolCall(toolName: string, toolInput: Record<string, any>): { name: string; target?: string } | null {
|
|
17
|
+
switch (toolName) {
|
|
18
|
+
case 'Bash':
|
|
19
|
+
return { name: 'bash', target: truncate(toolInput.command ?? toolInput.description, 60) }
|
|
20
|
+
case 'Read':
|
|
21
|
+
return { name: 'read_file', target: basename(toolInput.file_path) }
|
|
22
|
+
case 'Edit':
|
|
23
|
+
return { name: 'edit_file', target: basename(toolInput.file_path) }
|
|
24
|
+
case 'Write':
|
|
25
|
+
return { name: 'write_file', target: basename(toolInput.file_path) }
|
|
26
|
+
case 'Grep':
|
|
27
|
+
return { name: 'grep', target: truncate(toolInput.pattern, 40) }
|
|
28
|
+
case 'Glob':
|
|
29
|
+
return { name: 'glob', target: truncate(toolInput.pattern, 40) }
|
|
30
|
+
case 'Agent':
|
|
31
|
+
return { name: 'subagent', target: toolInput.description || toolInput.subagent_type || 'agent' }
|
|
32
|
+
case 'WebFetch':
|
|
33
|
+
return { name: 'web_fetch', target: hostname(toolInput.url) }
|
|
34
|
+
case 'WebSearch':
|
|
35
|
+
return { name: 'web_search', target: truncate(toolInput.query, 40) }
|
|
36
|
+
default:
|
|
37
|
+
// Unknown tool — use the name as-is, lowercased with underscores
|
|
38
|
+
return { name: toolName.replace(/([a-z])([A-Z])/g, '$1_$2').toLowerCase() }
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function truncate(str: string | undefined, max: number): string | undefined {
|
|
43
|
+
if (!str) return undefined
|
|
44
|
+
return str.length > max ? str.slice(0, max) + '...' : str
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function basename(filePath: string | undefined): string | undefined {
|
|
48
|
+
if (!filePath) return undefined
|
|
49
|
+
return path.basename(filePath)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function hostname(url: string | undefined): string | undefined {
|
|
53
|
+
if (!url) return undefined
|
|
54
|
+
try {
|
|
55
|
+
return new URL(url).hostname
|
|
56
|
+
} catch {
|
|
57
|
+
return url.slice(0, 30)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export class HookBridge {
|
|
62
|
+
private httpServer: http.Server | null = null
|
|
63
|
+
private _port: number | null = null
|
|
64
|
+
private portFilePath: string
|
|
65
|
+
|
|
66
|
+
constructor(private server: AbracadabraMCPServer) {
|
|
67
|
+
this.portFilePath = process.env.ABRA_HOOK_PORT_FILE
|
|
68
|
+
|| path.join(os.tmpdir(), 'abracadabra-mcp-hook.port')
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
get port(): number | null {
|
|
72
|
+
return this._port
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Start the HTTP server on a random port and write the port file. */
|
|
76
|
+
async start(): Promise<number> {
|
|
77
|
+
return new Promise((resolve, reject) => {
|
|
78
|
+
const srv = http.createServer((req, res) => this.handleRequest(req, res))
|
|
79
|
+
|
|
80
|
+
srv.on('error', reject)
|
|
81
|
+
|
|
82
|
+
srv.listen(0, '127.0.0.1', () => {
|
|
83
|
+
const addr = srv.address()
|
|
84
|
+
if (!addr || typeof addr === 'string') {
|
|
85
|
+
reject(new Error('Failed to get server address'))
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
this._port = addr.port
|
|
90
|
+
this.httpServer = srv
|
|
91
|
+
|
|
92
|
+
// Write port file for hook discovery
|
|
93
|
+
try {
|
|
94
|
+
fs.writeFileSync(this.portFilePath, String(this._port), 'utf-8')
|
|
95
|
+
} catch (err: any) {
|
|
96
|
+
console.error(`[hook-bridge] Warning: could not write port file: ${err.message}`)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
console.error(`[hook-bridge] Listening on 127.0.0.1:${this._port}`)
|
|
100
|
+
console.error(`[hook-bridge] Port file: ${this.portFilePath}`)
|
|
101
|
+
resolve(this._port)
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Shut down the HTTP server and remove the port file. */
|
|
107
|
+
async destroy(): Promise<void> {
|
|
108
|
+
if (this.httpServer) {
|
|
109
|
+
await new Promise<void>((resolve) => {
|
|
110
|
+
this.httpServer!.close(() => resolve())
|
|
111
|
+
})
|
|
112
|
+
this.httpServer = null
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Remove port file
|
|
116
|
+
try {
|
|
117
|
+
fs.unlinkSync(this.portFilePath)
|
|
118
|
+
} catch {
|
|
119
|
+
// Already gone or never written
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
this._port = null
|
|
123
|
+
console.error('[hook-bridge] Shut down')
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private handleRequest(req: http.IncomingMessage, res: http.ServerResponse): void {
|
|
127
|
+
// Only accept POST /hook
|
|
128
|
+
if (req.method !== 'POST' || req.url !== '/hook') {
|
|
129
|
+
res.writeHead(404)
|
|
130
|
+
res.end()
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let body = ''
|
|
135
|
+
req.on('data', (chunk: Buffer) => { body += chunk.toString() })
|
|
136
|
+
req.on('end', () => {
|
|
137
|
+
res.writeHead(200, { 'Content-Type': 'application/json' })
|
|
138
|
+
res.end('{}')
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
const payload = JSON.parse(body)
|
|
142
|
+
this.routeEvent(payload)
|
|
143
|
+
} catch {
|
|
144
|
+
// Invalid JSON — ignore silently (fire-and-forget)
|
|
145
|
+
}
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private routeEvent(payload: Record<string, any>): void {
|
|
150
|
+
const event = payload.hook_event_name
|
|
151
|
+
switch (event) {
|
|
152
|
+
case 'PreToolUse':
|
|
153
|
+
this.onPreToolUse(payload)
|
|
154
|
+
break
|
|
155
|
+
case 'PostToolUse':
|
|
156
|
+
this.onPostToolUse(payload)
|
|
157
|
+
break
|
|
158
|
+
case 'SubagentStart':
|
|
159
|
+
this.onSubagentStart(payload)
|
|
160
|
+
break
|
|
161
|
+
case 'SubagentStop':
|
|
162
|
+
this.onSubagentStop(payload)
|
|
163
|
+
break
|
|
164
|
+
case 'Stop':
|
|
165
|
+
this.onStop()
|
|
166
|
+
break
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private onPreToolUse(payload: Record<string, any>): void {
|
|
171
|
+
const toolName: string = payload.tool_name ?? ''
|
|
172
|
+
// Skip Abracadabra MCP tools — they set awareness themselves
|
|
173
|
+
if (toolName.startsWith('mcp__abracadabra__')) return
|
|
174
|
+
|
|
175
|
+
const toolInput = payload.tool_input ?? {}
|
|
176
|
+
const mapped = mapToolCall(toolName, toolInput)
|
|
177
|
+
if (mapped) {
|
|
178
|
+
this.server.setActiveToolCall(mapped)
|
|
179
|
+
this.server.setAutoStatus('working')
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private onPostToolUse(payload: Record<string, any>): void {
|
|
184
|
+
const toolName: string = payload.tool_name ?? ''
|
|
185
|
+
if (toolName.startsWith('mcp__abracadabra__')) return
|
|
186
|
+
|
|
187
|
+
this.server.setActiveToolCall(null)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private onSubagentStart(payload: Record<string, any>): void {
|
|
191
|
+
const agentType: string = payload.agent_type ?? 'agent'
|
|
192
|
+
this.server.setActiveToolCall({ name: 'subagent', target: agentType })
|
|
193
|
+
this.server.setAutoStatus('thinking')
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private onSubagentStop(_payload: Record<string, any>): void {
|
|
197
|
+
this.server.setActiveToolCall(null)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private onStop(): void {
|
|
201
|
+
this.server.setAutoStatus(null)
|
|
202
|
+
this.server.setActiveToolCall(null)
|
|
203
|
+
}
|
|
204
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -21,6 +21,8 @@ import { registerChannelTools } from './tools/channel.ts'
|
|
|
21
21
|
import { registerAgentGuide } from './resources/agent-guide.ts'
|
|
22
22
|
import { registerTreeResource } from './resources/tree-resource.ts'
|
|
23
23
|
import { registerServerInfoResource } from './resources/server-info.ts'
|
|
24
|
+
import { registerHookTools } from './tools/hooks.ts'
|
|
25
|
+
import { HookBridge } from './hook-bridge.ts'
|
|
24
26
|
|
|
25
27
|
async function main() {
|
|
26
28
|
const url = process.env.ABRA_URL
|
|
@@ -55,8 +57,9 @@ async function main() {
|
|
|
55
57
|
## Key Concepts
|
|
56
58
|
- Documents form a tree. A kanban board's columns are child documents; cards are grandchildren.
|
|
57
59
|
- A document's label IS its display name everywhere. Children ARE the content (not just the body text).
|
|
58
|
-
- Page types (doc, kanban, table, calendar, timeline, outline, etc.) are views over the SAME tree — switching types preserves data.
|
|
60
|
+
- Page types (doc, kanban, table, calendar, timeline, checklist, outline, gallery, map, graph, dashboard, spatial, media, mindmap, etc.) are views over the SAME tree — switching types preserves data.
|
|
59
61
|
- An empty markdown body does NOT mean empty content — always check the children array.
|
|
62
|
+
- Use ![[docId]] in content to embed another document, or [[docId|label]] for inline links.
|
|
60
63
|
|
|
61
64
|
## Finding Documents
|
|
62
65
|
- list_documents only shows ONE level of children. If you don't find what you need, use find_document to search the entire tree by name, or get_document_tree to see the full hierarchy.
|
|
@@ -101,6 +104,18 @@ Read the resource at abracadabra://agent-guide for the complete guide covering p
|
|
|
101
104
|
process.exit(1)
|
|
102
105
|
}
|
|
103
106
|
|
|
107
|
+
// Start Claude Code hook bridge (HTTP server for activity events)
|
|
108
|
+
const hookBridge = new HookBridge(server)
|
|
109
|
+
try {
|
|
110
|
+
const hookPort = await hookBridge.start()
|
|
111
|
+
console.error(`[abracadabra-mcp] Hook bridge listening on port ${hookPort}`)
|
|
112
|
+
} catch (error: any) {
|
|
113
|
+
console.error(`[abracadabra-mcp] Hook bridge failed to start: ${error.message}`)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Register hook config tool (must be after bridge starts so port is available)
|
|
117
|
+
registerHookTools(mcp, hookBridge)
|
|
118
|
+
|
|
104
119
|
// Start MCP stdio transport
|
|
105
120
|
const transport = new StdioServerTransport()
|
|
106
121
|
await mcp.connect(transport)
|
|
@@ -112,6 +127,7 @@ Read the resource at abracadabra://agent-guide for the complete guide covering p
|
|
|
112
127
|
// Graceful shutdown
|
|
113
128
|
const shutdown = async () => {
|
|
114
129
|
console.error('[abracadabra-mcp] Shutting down...')
|
|
130
|
+
await hookBridge.destroy()
|
|
115
131
|
await server.destroy()
|
|
116
132
|
process.exit(0)
|
|
117
133
|
}
|