@absmartly/claude-code-bridge 1.0.2 → 1.1.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/bin/get-chunk.js +135 -0
- package/index.js +294 -12
- package/package.json +6 -4
package/bin/get-chunk.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const http = require('http')
|
|
4
|
+
const https = require('https')
|
|
5
|
+
|
|
6
|
+
function parseArgs(args) {
|
|
7
|
+
const result = {
|
|
8
|
+
conversationId: null,
|
|
9
|
+
selectors: [],
|
|
10
|
+
bridgeUrl: 'http://localhost:3000'
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
for (let i = 0; i < args.length; i++) {
|
|
14
|
+
const arg = args[i]
|
|
15
|
+
if (arg === '--conversation-id' && args[i + 1]) {
|
|
16
|
+
result.conversationId = args[++i]
|
|
17
|
+
} else if (arg === '--selector' && args[i + 1]) {
|
|
18
|
+
result.selectors.push(args[++i])
|
|
19
|
+
} else if (arg === '--selectors' && args[i + 1]) {
|
|
20
|
+
// Support comma-separated list
|
|
21
|
+
result.selectors.push(...args[++i].split(',').map(s => s.trim()).filter(s => s))
|
|
22
|
+
} else if (arg === '--bridge-url' && args[i + 1]) {
|
|
23
|
+
result.bridgeUrl = args[++i]
|
|
24
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
25
|
+
console.log(`
|
|
26
|
+
Usage: get-chunk --conversation-id <id> --selector <selector> [--selector <selector2>...] [options]
|
|
27
|
+
|
|
28
|
+
Options:
|
|
29
|
+
--conversation-id <id> Required. The conversation ID to retrieve HTML from.
|
|
30
|
+
--selector <selector> Required. CSS selector for the element to retrieve.
|
|
31
|
+
Can be specified multiple times for multiple selectors.
|
|
32
|
+
--selectors <list> Comma-separated list of CSS selectors (alternative to multiple --selector).
|
|
33
|
+
--bridge-url <url> Bridge server URL (default: http://localhost:3000)
|
|
34
|
+
--help, -h Show this help message.
|
|
35
|
+
|
|
36
|
+
Examples:
|
|
37
|
+
# Single selector
|
|
38
|
+
get-chunk --conversation-id conv-123 --selector "#main-content"
|
|
39
|
+
|
|
40
|
+
# Multiple selectors (using --selector multiple times)
|
|
41
|
+
get-chunk --conversation-id conv-123 --selector ".hero-section" --selector "header" --selector "#main"
|
|
42
|
+
|
|
43
|
+
# Multiple selectors (using comma-separated --selectors)
|
|
44
|
+
get-chunk --conversation-id conv-123 --selectors ".hero-section,header,#main"
|
|
45
|
+
`)
|
|
46
|
+
process.exit(0)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return result
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function fetch(url) {
|
|
54
|
+
return new Promise((resolve, reject) => {
|
|
55
|
+
const client = url.startsWith('https') ? https : http
|
|
56
|
+
|
|
57
|
+
client.get(url, (res) => {
|
|
58
|
+
let data = ''
|
|
59
|
+
res.on('data', chunk => data += chunk)
|
|
60
|
+
res.on('end', () => {
|
|
61
|
+
try {
|
|
62
|
+
const json = JSON.parse(data)
|
|
63
|
+
resolve({ status: res.statusCode, data: json })
|
|
64
|
+
} catch (e) {
|
|
65
|
+
resolve({ status: res.statusCode, data: data })
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
}).on('error', reject)
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function main() {
|
|
73
|
+
const args = process.argv.slice(2)
|
|
74
|
+
const opts = parseArgs(args)
|
|
75
|
+
|
|
76
|
+
if (!opts.conversationId) {
|
|
77
|
+
console.error('Error: --conversation-id is required')
|
|
78
|
+
process.exit(1)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (opts.selectors.length === 0) {
|
|
82
|
+
console.error('Error: --selector is required')
|
|
83
|
+
process.exit(1)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Use comma-separated selectors in query param
|
|
87
|
+
const selectorsParam = opts.selectors.length === 1
|
|
88
|
+
? `selector=${encodeURIComponent(opts.selectors[0])}`
|
|
89
|
+
: `selectors=${encodeURIComponent(opts.selectors.join(','))}`
|
|
90
|
+
|
|
91
|
+
const url = `${opts.bridgeUrl}/conversations/${encodeURIComponent(opts.conversationId)}/chunk?${selectorsParam}`
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const response = await fetch(url)
|
|
95
|
+
|
|
96
|
+
if (response.status === 404) {
|
|
97
|
+
console.error(`Error: ${response.data.error || 'Element not found'}`)
|
|
98
|
+
process.exit(1)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (response.status !== 200) {
|
|
102
|
+
console.error(`Error: ${response.data.error || 'Request failed'}`)
|
|
103
|
+
process.exit(1)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Handle single selector response (backward compatible)
|
|
107
|
+
if (response.data.found !== undefined) {
|
|
108
|
+
if (response.data.found) {
|
|
109
|
+
console.log(response.data.html)
|
|
110
|
+
} else {
|
|
111
|
+
console.error(`Error: Element not found for selector: ${opts.selectors[0]}`)
|
|
112
|
+
process.exit(1)
|
|
113
|
+
}
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Handle multiple selectors response
|
|
118
|
+
if (response.data.results) {
|
|
119
|
+
for (const result of response.data.results) {
|
|
120
|
+
console.log(`\n## ${result.selector}`)
|
|
121
|
+
if (result.found) {
|
|
122
|
+
console.log(result.html)
|
|
123
|
+
} else {
|
|
124
|
+
console.log(`Error: ${result.error || 'Element not found'}`)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
} catch (error) {
|
|
129
|
+
console.error(`Error: Failed to connect to bridge server at ${opts.bridgeUrl}`)
|
|
130
|
+
console.error(`Make sure the bridge is running: npx @absmartly/claude-code-bridge`)
|
|
131
|
+
process.exit(1)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
main()
|
package/index.js
CHANGED
|
@@ -6,6 +6,7 @@ const { spawn } = require('child_process')
|
|
|
6
6
|
const fs = require('fs')
|
|
7
7
|
const path = require('path')
|
|
8
8
|
const os = require('os')
|
|
9
|
+
const { JSDOM } = require('jsdom')
|
|
9
10
|
|
|
10
11
|
const PREFERRED_PORTS = [3000, 3001, 3002, 3003, 3004]
|
|
11
12
|
const PORT = process.env.PORT ? parseInt(process.env.PORT) : null
|
|
@@ -19,6 +20,11 @@ const activeStreams = new Map()
|
|
|
19
20
|
const conversationMessages = new Map()
|
|
20
21
|
const sessionTracking = new Map()
|
|
21
22
|
const outputBuffers = new Map()
|
|
23
|
+
const conversationHtml = new Map() // Stores HTML for chunk retrieval
|
|
24
|
+
const conversationModels = new Map() // Stores model selection per conversation
|
|
25
|
+
|
|
26
|
+
// Global JSON schema - set by extension on first conversation
|
|
27
|
+
let globalJsonSchema = null
|
|
22
28
|
|
|
23
29
|
function checkClaudeAuth() {
|
|
24
30
|
try {
|
|
@@ -90,33 +96,58 @@ const DOM_CHANGES_SCHEMA = {
|
|
|
90
96
|
properties: {
|
|
91
97
|
domChanges: {
|
|
92
98
|
type: 'array',
|
|
93
|
-
description: 'Array of DOM change
|
|
99
|
+
description: 'Array of DOM change objects. Each must have: selector (CSS), type (text|html|style|styleRules|class|attribute|javascript|move|create|delete), and type-specific properties.',
|
|
100
|
+
items: {
|
|
101
|
+
type: 'object',
|
|
102
|
+
properties: {
|
|
103
|
+
selector: { type: 'string', description: 'CSS selector for target element(s)' },
|
|
104
|
+
type: {
|
|
105
|
+
type: 'string',
|
|
106
|
+
enum: ['text', 'html', 'style', 'styleRules', 'class', 'attribute', 'javascript', 'move', 'create', 'delete'],
|
|
107
|
+
description: 'Type of DOM change to apply'
|
|
108
|
+
},
|
|
109
|
+
value: { description: 'Value for text/html/attribute changes, or CSS object for style changes' },
|
|
110
|
+
css: { type: 'object', description: 'CSS properties object for style type (alternative to value)' },
|
|
111
|
+
states: { type: 'object', description: 'CSS states for styleRules type (normal, hover, active, focus)' },
|
|
112
|
+
add: { type: 'array', items: { type: 'string' }, description: 'Classes to add (for class type)' },
|
|
113
|
+
remove: { type: 'array', items: { type: 'string' }, description: 'Classes to remove (for class type)' },
|
|
114
|
+
element: { type: 'string', description: 'HTML to create (for create type)' },
|
|
115
|
+
targetSelector: { type: 'string', description: 'Target location (for move/create types)' },
|
|
116
|
+
position: { type: 'string', enum: ['before', 'after', 'firstChild', 'lastChild'], description: 'Position relative to target' },
|
|
117
|
+
important: { type: 'boolean', description: 'Add !important flag to styles' },
|
|
118
|
+
waitForElement: { type: 'boolean', description: 'Wait for element to appear (SPA mode)' }
|
|
119
|
+
},
|
|
120
|
+
required: ['selector', 'type']
|
|
121
|
+
}
|
|
94
122
|
},
|
|
95
123
|
response: {
|
|
96
124
|
type: 'string',
|
|
97
|
-
description: '
|
|
125
|
+
description: 'Markdown explanation of what you changed and why. No action descriptions (no "I\'ll click..." or "Let me navigate...").'
|
|
98
126
|
},
|
|
99
127
|
action: {
|
|
100
128
|
type: 'string',
|
|
101
129
|
enum: ['append', 'replace_all', 'replace_specific', 'remove_specific', 'none'],
|
|
102
|
-
description: 'How
|
|
130
|
+
description: 'How to apply changes: append=add to existing, replace_all=clear all first, replace_specific=replace specific selectors, remove_specific=remove specific selectors, none=no changes'
|
|
103
131
|
},
|
|
104
132
|
targetSelectors: {
|
|
105
133
|
type: 'array',
|
|
106
|
-
description: '
|
|
134
|
+
description: 'CSS selectors to target when action is replace_specific or remove_specific',
|
|
107
135
|
items: { type: 'string' }
|
|
108
136
|
}
|
|
109
137
|
},
|
|
110
138
|
required: ['domChanges', 'response', 'action']
|
|
111
139
|
}
|
|
112
140
|
|
|
113
|
-
function spawnClaudeForConversation(conversationId, systemPrompt, sessionId, isResume = false) {
|
|
141
|
+
function spawnClaudeForConversation(conversationId, systemPrompt, sessionId, isResume = false, model = null) {
|
|
114
142
|
if (claudeProcesses.has(conversationId)) {
|
|
115
143
|
console.log(`Claude CLI already running for conversation ${conversationId}`)
|
|
116
144
|
return claudeProcesses.get(conversationId)
|
|
117
145
|
}
|
|
118
146
|
|
|
119
|
-
|
|
147
|
+
// Get model from stored settings or use default (sonnet)
|
|
148
|
+
const selectedModel = model || conversationModels.get(conversationId) || 'sonnet'
|
|
149
|
+
console.log(`Spawning Claude CLI process for conversation ${conversationId} with model: ${selectedModel}...`)
|
|
150
|
+
|
|
120
151
|
const args = [
|
|
121
152
|
'@anthropic-ai/claude-code',
|
|
122
153
|
'--print',
|
|
@@ -125,8 +156,9 @@ function spawnClaudeForConversation(conversationId, systemPrompt, sessionId, isR
|
|
|
125
156
|
'--input-format', 'stream-json',
|
|
126
157
|
'--replay-user-messages',
|
|
127
158
|
'--permission-mode', 'default',
|
|
128
|
-
'--
|
|
159
|
+
'--allowedTools', 'Bash(curl:*),Bash(npx:*)', // Allow curl and npx for chunk retrieval
|
|
129
160
|
'--strict-mcp-config',
|
|
161
|
+
'--model', selectedModel, // Use selected model (sonnet, opus, or haiku)
|
|
130
162
|
'--settings', JSON.stringify({ disableClaudeMd: true })
|
|
131
163
|
]
|
|
132
164
|
|
|
@@ -145,9 +177,10 @@ function spawnClaudeForConversation(conversationId, systemPrompt, sessionId, isR
|
|
|
145
177
|
args.push('--system-prompt', systemPrompt)
|
|
146
178
|
}
|
|
147
179
|
|
|
148
|
-
// Add JSON schema for structured output
|
|
149
|
-
|
|
150
|
-
|
|
180
|
+
// Add JSON schema for structured output (use global if provided by extension, otherwise fallback)
|
|
181
|
+
const schemaToUse = globalJsonSchema || DOM_CHANGES_SCHEMA
|
|
182
|
+
console.log(`Adding JSON schema for structured DOM changes output (source: ${globalJsonSchema ? 'extension' : 'fallback'})`)
|
|
183
|
+
args.push('--json-schema', JSON.stringify(schemaToUse))
|
|
151
184
|
|
|
152
185
|
console.log(`[${conversationId}] ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`)
|
|
153
186
|
console.log(`[${conversationId}] 🚀 SPAWNING CLAUDE CLI WITH ARGUMENTS:`)
|
|
@@ -288,11 +321,31 @@ app.get('/auth/status', (req, res) => {
|
|
|
288
321
|
})
|
|
289
322
|
|
|
290
323
|
app.post('/conversations', (req, res) => {
|
|
291
|
-
const { session_id } = req.body
|
|
324
|
+
const { session_id, jsonSchema, html, model } = req.body
|
|
292
325
|
const conversationId = session_id || `conv_${Date.now()}`
|
|
293
326
|
|
|
294
327
|
conversationMessages.set(conversationId, [])
|
|
295
328
|
|
|
329
|
+
// Store HTML for chunk retrieval if provided
|
|
330
|
+
if (html) {
|
|
331
|
+
conversationHtml.set(conversationId, {
|
|
332
|
+
html,
|
|
333
|
+
timestamp: Date.now()
|
|
334
|
+
})
|
|
335
|
+
console.log(`📄 Stored HTML for conversation ${conversationId} (${html.length} chars)`)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Store model selection if provided (defaults to sonnet)
|
|
339
|
+
const selectedModel = model || 'sonnet'
|
|
340
|
+
conversationModels.set(conversationId, selectedModel)
|
|
341
|
+
console.log(`🤖 Model for conversation ${conversationId}: ${selectedModel}`)
|
|
342
|
+
|
|
343
|
+
// Accept JSON schema from extension if provided (always update to stay in sync)
|
|
344
|
+
if (jsonSchema) {
|
|
345
|
+
console.log('📋 Updating JSON schema from extension')
|
|
346
|
+
globalJsonSchema = jsonSchema
|
|
347
|
+
}
|
|
348
|
+
|
|
296
349
|
if (session_id) {
|
|
297
350
|
const isResume = sessionTracking.has(session_id)
|
|
298
351
|
sessionTracking.set(session_id, { conversationId, isResume })
|
|
@@ -307,7 +360,13 @@ app.post('/conversations', (req, res) => {
|
|
|
307
360
|
|
|
308
361
|
app.post('/conversations/:id/messages', (req, res) => {
|
|
309
362
|
const { id } = req.params
|
|
310
|
-
const { content, files, systemPrompt } = req.body
|
|
363
|
+
const { content, files, systemPrompt, jsonSchema } = req.body
|
|
364
|
+
|
|
365
|
+
// Accept JSON schema if provided (for bridge restarts / schema updates)
|
|
366
|
+
if (jsonSchema) {
|
|
367
|
+
console.log('📋 Updating JSON schema from extension (via /messages)')
|
|
368
|
+
globalJsonSchema = jsonSchema
|
|
369
|
+
}
|
|
311
370
|
|
|
312
371
|
if (!claudeProcesses.has(id)) {
|
|
313
372
|
let sessionId = null
|
|
@@ -397,6 +456,227 @@ app.get('/conversations/:id/stream', (req, res) => {
|
|
|
397
456
|
})
|
|
398
457
|
})
|
|
399
458
|
|
|
459
|
+
// Extract a single HTML chunk by selector using jsdom
|
|
460
|
+
function extractChunk(html, selector, dom = null) {
|
|
461
|
+
try {
|
|
462
|
+
// Reuse DOM if provided, otherwise create new one
|
|
463
|
+
const jsdom = dom || new JSDOM(html)
|
|
464
|
+
const document = jsdom.window.document
|
|
465
|
+
|
|
466
|
+
const element = document.querySelector(selector)
|
|
467
|
+
if (element) {
|
|
468
|
+
return { selector, html: element.outerHTML, found: true }
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
return { selector, html: '', found: false, error: `Element not found: ${selector}` }
|
|
472
|
+
} catch (error) {
|
|
473
|
+
return { selector, html: '', found: false, error: `Invalid selector: ${error.message}` }
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Get HTML chunk(s) by CSS selector(s)
|
|
478
|
+
// GET with single selector: ?selector=.hero
|
|
479
|
+
// GET with multiple selectors: ?selectors=.hero,header,#main
|
|
480
|
+
app.get('/conversations/:id/chunk', (req, res) => {
|
|
481
|
+
const { id } = req.params
|
|
482
|
+
const { selector, selectors } = req.query
|
|
483
|
+
|
|
484
|
+
// Support both single selector and multiple selectors
|
|
485
|
+
let selectorList = []
|
|
486
|
+
if (selectors) {
|
|
487
|
+
selectorList = selectors.split(',').map(s => s.trim()).filter(s => s)
|
|
488
|
+
} else if (selector) {
|
|
489
|
+
selectorList = [selector]
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (selectorList.length === 0) {
|
|
493
|
+
return res.status(400).json({ error: 'Missing selector or selectors query parameter' })
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const stored = conversationHtml.get(id)
|
|
497
|
+
if (!stored) {
|
|
498
|
+
return res.status(404).json({ error: 'Conversation not found or no HTML stored' })
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
try {
|
|
502
|
+
const html = stored.html
|
|
503
|
+
// Parse DOM once and reuse for all selectors
|
|
504
|
+
const dom = new JSDOM(html)
|
|
505
|
+
const results = selectorList.map(sel => extractChunk(html, sel, dom))
|
|
506
|
+
|
|
507
|
+
// For single selector (backward compatibility), return single object
|
|
508
|
+
if (selectorList.length === 1) {
|
|
509
|
+
const result = results[0]
|
|
510
|
+
if (!result.found) {
|
|
511
|
+
return res.status(404).json(result)
|
|
512
|
+
}
|
|
513
|
+
return res.json(result)
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// For multiple selectors, return array
|
|
517
|
+
return res.json({ results })
|
|
518
|
+
} catch (error) {
|
|
519
|
+
return res.status(500).json({ error: `Failed to extract chunk: ${error.message}` })
|
|
520
|
+
}
|
|
521
|
+
})
|
|
522
|
+
|
|
523
|
+
// POST endpoint for multiple selectors (preferred for complex requests)
|
|
524
|
+
app.post('/conversations/:id/chunks', (req, res) => {
|
|
525
|
+
const { id } = req.params
|
|
526
|
+
const { selectors } = req.body
|
|
527
|
+
|
|
528
|
+
if (!selectors || !Array.isArray(selectors) || selectors.length === 0) {
|
|
529
|
+
return res.status(400).json({ error: 'Missing or invalid selectors array in request body' })
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const stored = conversationHtml.get(id)
|
|
533
|
+
if (!stored) {
|
|
534
|
+
return res.status(404).json({ error: 'Conversation not found or no HTML stored' })
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
try {
|
|
538
|
+
const html = stored.html
|
|
539
|
+
// Parse DOM once and reuse for all selectors
|
|
540
|
+
const dom = new JSDOM(html)
|
|
541
|
+
const results = selectors.map(sel => extractChunk(html, sel, dom))
|
|
542
|
+
return res.json({ results })
|
|
543
|
+
} catch (error) {
|
|
544
|
+
return res.status(500).json({ error: `Failed to extract chunks: ${error.message}` })
|
|
545
|
+
}
|
|
546
|
+
})
|
|
547
|
+
|
|
548
|
+
// Execute XPath query on stored HTML
|
|
549
|
+
function executeXPath(html, xpath, maxResults = 10, dom = null) {
|
|
550
|
+
try {
|
|
551
|
+
const jsdom = dom || new JSDOM(html)
|
|
552
|
+
const document = jsdom.window.document
|
|
553
|
+
const window = jsdom.window
|
|
554
|
+
|
|
555
|
+
// Helper to generate a CSS selector for an element
|
|
556
|
+
const generateSelector = (element) => {
|
|
557
|
+
if (element.id) {
|
|
558
|
+
return `#${element.id}`
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const tagName = element.tagName.toLowerCase()
|
|
562
|
+
const classes = Array.from(element.classList || []).filter(c => c && !c.includes(':'))
|
|
563
|
+
|
|
564
|
+
if (classes.length > 0) {
|
|
565
|
+
const classSelector = `${tagName}.${classes.slice(0, 2).join('.')}`
|
|
566
|
+
if (document.querySelectorAll(classSelector).length === 1) {
|
|
567
|
+
return classSelector
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const parent = element.parentElement
|
|
572
|
+
if (parent) {
|
|
573
|
+
const siblings = Array.from(parent.children).filter(c => c.tagName === element.tagName)
|
|
574
|
+
if (siblings.length > 1) {
|
|
575
|
+
const index = siblings.indexOf(element) + 1
|
|
576
|
+
const parentSelector = generateSelector(parent)
|
|
577
|
+
return `${parentSelector} > ${tagName}:nth-of-type(${index})`
|
|
578
|
+
}
|
|
579
|
+
const parentSelector = generateSelector(parent)
|
|
580
|
+
return `${parentSelector} > ${tagName}`
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
return tagName
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const matches = []
|
|
587
|
+
const xpathResult = document.evaluate(
|
|
588
|
+
xpath,
|
|
589
|
+
document,
|
|
590
|
+
null,
|
|
591
|
+
window.XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
|
|
592
|
+
null
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
for (let i = 0; i < Math.min(xpathResult.snapshotLength, maxResults); i++) {
|
|
596
|
+
const node = xpathResult.snapshotItem(i)
|
|
597
|
+
if (!node) continue
|
|
598
|
+
|
|
599
|
+
if (node.nodeType === window.Node.ELEMENT_NODE) {
|
|
600
|
+
matches.push({
|
|
601
|
+
selector: generateSelector(node),
|
|
602
|
+
html: node.outerHTML.slice(0, 2000),
|
|
603
|
+
textContent: (node.textContent || '').slice(0, 200),
|
|
604
|
+
nodeType: 'element'
|
|
605
|
+
})
|
|
606
|
+
} else if (node.nodeType === window.Node.TEXT_NODE) {
|
|
607
|
+
const parentElement = node.parentElement
|
|
608
|
+
if (parentElement) {
|
|
609
|
+
matches.push({
|
|
610
|
+
selector: generateSelector(parentElement),
|
|
611
|
+
html: parentElement.outerHTML.slice(0, 2000),
|
|
612
|
+
textContent: (node.textContent || '').slice(0, 200),
|
|
613
|
+
nodeType: 'text'
|
|
614
|
+
})
|
|
615
|
+
}
|
|
616
|
+
} else if (node.nodeType === window.Node.ATTRIBUTE_NODE) {
|
|
617
|
+
matches.push({
|
|
618
|
+
selector: '',
|
|
619
|
+
html: `${node.name}="${node.value}"`,
|
|
620
|
+
textContent: node.value,
|
|
621
|
+
nodeType: 'attribute'
|
|
622
|
+
})
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
return { xpath, matches, found: matches.length > 0 }
|
|
627
|
+
} catch (error) {
|
|
628
|
+
return { xpath, matches: [], found: false, error: `XPath error: ${error.message}` }
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// XPath query endpoint
|
|
633
|
+
app.post('/conversations/:id/xpath', (req, res) => {
|
|
634
|
+
const { id } = req.params
|
|
635
|
+
const { xpath, maxResults = 10 } = req.body
|
|
636
|
+
|
|
637
|
+
if (!xpath) {
|
|
638
|
+
return res.status(400).json({ error: 'Missing xpath in request body' })
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
const stored = conversationHtml.get(id)
|
|
642
|
+
if (!stored) {
|
|
643
|
+
return res.status(404).json({ error: 'Conversation not found or no HTML stored' })
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
try {
|
|
647
|
+
const result = executeXPath(stored.html, xpath, maxResults)
|
|
648
|
+
if (!result.found && result.error) {
|
|
649
|
+
return res.status(400).json(result)
|
|
650
|
+
}
|
|
651
|
+
return res.json(result)
|
|
652
|
+
} catch (error) {
|
|
653
|
+
return res.status(500).json({ error: `Failed to execute XPath: ${error.message}` })
|
|
654
|
+
}
|
|
655
|
+
})
|
|
656
|
+
|
|
657
|
+
// Refresh stored HTML for a conversation
|
|
658
|
+
app.post('/conversations/:id/refresh', (req, res) => {
|
|
659
|
+
const { id } = req.params
|
|
660
|
+
const { html } = req.body
|
|
661
|
+
|
|
662
|
+
if (!html) {
|
|
663
|
+
return res.status(400).json({ error: 'Missing html in request body' })
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
const existing = conversationHtml.get(id)
|
|
667
|
+
if (!existing) {
|
|
668
|
+
return res.status(404).json({ error: 'Conversation not found' })
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
conversationHtml.set(id, {
|
|
672
|
+
html,
|
|
673
|
+
timestamp: Date.now()
|
|
674
|
+
})
|
|
675
|
+
|
|
676
|
+
console.log(`🔄 Refreshed HTML for conversation ${id} (${html.length} chars)`)
|
|
677
|
+
res.json({ success: true })
|
|
678
|
+
})
|
|
679
|
+
|
|
400
680
|
app.post('/conversations/:id/approve', (req, res) => {
|
|
401
681
|
const { id } = req.params
|
|
402
682
|
const { requestId, data } = req.body
|
|
@@ -482,6 +762,8 @@ function tryStartServer(ports, index = 0) {
|
|
|
482
762
|
console.log(` POST /conversations`)
|
|
483
763
|
console.log(` POST /conversations/:id/messages`)
|
|
484
764
|
console.log(` GET /conversations/:id/stream`)
|
|
765
|
+
console.log(` GET /conversations/:id/chunk (HTML chunk retrieval)`)
|
|
766
|
+
console.log(` POST /conversations/:id/refresh (Update stored HTML)`)
|
|
485
767
|
console.log(` POST /conversations/:id/approve`)
|
|
486
768
|
console.log(` POST /conversations/:id/deny`)
|
|
487
769
|
console.log(`\nReady for connections from ABsmartly extension 🚀\n`)
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@absmartly/claude-code-bridge",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "HTTP bridge server for ABsmartly Extension to communicate with Claude Code CLI",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
7
|
-
"claude-code-bridge": "
|
|
7
|
+
"claude-code-bridge": "index.js",
|
|
8
|
+
"get-chunk": "bin/get-chunk.js"
|
|
8
9
|
},
|
|
9
10
|
"scripts": {
|
|
10
11
|
"start": "nodemon --watch index.js --watch package.json --exitcrash index.js"
|
|
@@ -18,9 +19,10 @@
|
|
|
18
19
|
"author": "ABsmartly",
|
|
19
20
|
"license": "MIT",
|
|
20
21
|
"dependencies": {
|
|
21
|
-
"express": "^4.18.2",
|
|
22
22
|
"cors": "^2.8.5",
|
|
23
|
-
"dotenv": "^16.3.1"
|
|
23
|
+
"dotenv": "^16.3.1",
|
|
24
|
+
"express": "^4.18.2",
|
|
25
|
+
"jsdom": "^27.3.0"
|
|
24
26
|
},
|
|
25
27
|
"devDependencies": {
|
|
26
28
|
"nodemon": "^3.0.1"
|