@absmartly/claude-code-bridge 1.0.1 → 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 +353 -33
- 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,41 +20,67 @@ 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 {
|
|
25
31
|
const credentialsPath = path.join(os.homedir(), '.claude', '.credentials.json')
|
|
32
|
+
const claudeDir = path.join(os.homedir(), '.claude')
|
|
26
33
|
|
|
27
|
-
if (
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
34
|
+
if (fs.existsSync(credentialsPath)) {
|
|
35
|
+
const credentials = JSON.parse(fs.readFileSync(credentialsPath, 'utf8'))
|
|
36
|
+
|
|
37
|
+
if (!credentials.claudeAiOauth) {
|
|
38
|
+
return {
|
|
39
|
+
authenticated: false,
|
|
40
|
+
error: 'No Claude OAuth credentials found'
|
|
41
|
+
}
|
|
31
42
|
}
|
|
32
|
-
}
|
|
33
43
|
|
|
34
|
-
|
|
44
|
+
const { expiresAt, subscriptionType } = credentials.claudeAiOauth
|
|
45
|
+
const isExpired = new Date(expiresAt) < new Date()
|
|
46
|
+
|
|
47
|
+
if (isExpired) {
|
|
48
|
+
return {
|
|
49
|
+
authenticated: false,
|
|
50
|
+
error: `Claude credentials expired at: ${expiresAt}`
|
|
51
|
+
}
|
|
52
|
+
}
|
|
35
53
|
|
|
36
|
-
if (!credentials.claudeAiOauth) {
|
|
37
54
|
return {
|
|
38
|
-
authenticated:
|
|
39
|
-
|
|
55
|
+
authenticated: true,
|
|
56
|
+
subscriptionType,
|
|
57
|
+
expiresAt,
|
|
58
|
+
method: 'credentials file'
|
|
40
59
|
}
|
|
41
60
|
}
|
|
42
61
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
62
|
+
if (fs.existsSync(claudeDir)) {
|
|
63
|
+
const historyPath = path.join(claudeDir, 'history.jsonl')
|
|
64
|
+
const sessionEnvPath = path.join(claudeDir, 'session-env')
|
|
65
|
+
|
|
66
|
+
if (fs.existsSync(historyPath) || fs.existsSync(sessionEnvPath)) {
|
|
67
|
+
const stats = fs.existsSync(historyPath) ? fs.statSync(historyPath) : null
|
|
68
|
+
const recentlyUsed = stats && (Date.now() - stats.mtimeMs) < 24 * 60 * 60 * 1000
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
authenticated: true,
|
|
72
|
+
subscriptionType: null,
|
|
73
|
+
subscriptionNote: 'For subscription details, run: npx @anthropic-ai/claude-code login',
|
|
74
|
+
method: 'session detection',
|
|
75
|
+
lastActivity: stats ? new Date(stats.mtime).toISOString() : 'unknown',
|
|
76
|
+
recentlyUsed
|
|
77
|
+
}
|
|
50
78
|
}
|
|
51
79
|
}
|
|
52
80
|
|
|
53
81
|
return {
|
|
54
|
-
authenticated:
|
|
55
|
-
|
|
56
|
-
expiresAt
|
|
82
|
+
authenticated: false,
|
|
83
|
+
error: 'Claude CLI not logged in. Run: npx @anthropic-ai/claude-code login'
|
|
57
84
|
}
|
|
58
85
|
} catch (error) {
|
|
59
86
|
return {
|
|
@@ -69,33 +96,58 @@ const DOM_CHANGES_SCHEMA = {
|
|
|
69
96
|
properties: {
|
|
70
97
|
domChanges: {
|
|
71
98
|
type: 'array',
|
|
72
|
-
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
|
+
}
|
|
73
122
|
},
|
|
74
123
|
response: {
|
|
75
124
|
type: 'string',
|
|
76
|
-
description: '
|
|
125
|
+
description: 'Markdown explanation of what you changed and why. No action descriptions (no "I\'ll click..." or "Let me navigate...").'
|
|
77
126
|
},
|
|
78
127
|
action: {
|
|
79
128
|
type: 'string',
|
|
80
129
|
enum: ['append', 'replace_all', 'replace_specific', 'remove_specific', 'none'],
|
|
81
|
-
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'
|
|
82
131
|
},
|
|
83
132
|
targetSelectors: {
|
|
84
133
|
type: 'array',
|
|
85
|
-
description: '
|
|
134
|
+
description: 'CSS selectors to target when action is replace_specific or remove_specific',
|
|
86
135
|
items: { type: 'string' }
|
|
87
136
|
}
|
|
88
137
|
},
|
|
89
138
|
required: ['domChanges', 'response', 'action']
|
|
90
139
|
}
|
|
91
140
|
|
|
92
|
-
function spawnClaudeForConversation(conversationId, systemPrompt, sessionId, isResume = false) {
|
|
141
|
+
function spawnClaudeForConversation(conversationId, systemPrompt, sessionId, isResume = false, model = null) {
|
|
93
142
|
if (claudeProcesses.has(conversationId)) {
|
|
94
143
|
console.log(`Claude CLI already running for conversation ${conversationId}`)
|
|
95
144
|
return claudeProcesses.get(conversationId)
|
|
96
145
|
}
|
|
97
146
|
|
|
98
|
-
|
|
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
|
+
|
|
99
151
|
const args = [
|
|
100
152
|
'@anthropic-ai/claude-code',
|
|
101
153
|
'--print',
|
|
@@ -103,8 +155,10 @@ function spawnClaudeForConversation(conversationId, systemPrompt, sessionId, isR
|
|
|
103
155
|
'--output-format', 'stream-json',
|
|
104
156
|
'--input-format', 'stream-json',
|
|
105
157
|
'--replay-user-messages',
|
|
106
|
-
'--permission-mode', '
|
|
107
|
-
'--
|
|
158
|
+
'--permission-mode', 'default',
|
|
159
|
+
'--allowedTools', 'Bash(curl:*),Bash(npx:*)', // Allow curl and npx for chunk retrieval
|
|
160
|
+
'--strict-mcp-config',
|
|
161
|
+
'--model', selectedModel, // Use selected model (sonnet, opus, or haiku)
|
|
108
162
|
'--settings', JSON.stringify({ disableClaudeMd: true })
|
|
109
163
|
]
|
|
110
164
|
|
|
@@ -123,9 +177,10 @@ function spawnClaudeForConversation(conversationId, systemPrompt, sessionId, isR
|
|
|
123
177
|
args.push('--system-prompt', systemPrompt)
|
|
124
178
|
}
|
|
125
179
|
|
|
126
|
-
// Add JSON schema for structured output
|
|
127
|
-
|
|
128
|
-
|
|
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))
|
|
129
184
|
|
|
130
185
|
console.log(`[${conversationId}] ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`)
|
|
131
186
|
console.log(`[${conversationId}] 🚀 SPAWNING CLAUDE CLI WITH ARGUMENTS:`)
|
|
@@ -266,11 +321,31 @@ app.get('/auth/status', (req, res) => {
|
|
|
266
321
|
})
|
|
267
322
|
|
|
268
323
|
app.post('/conversations', (req, res) => {
|
|
269
|
-
const { session_id } = req.body
|
|
324
|
+
const { session_id, jsonSchema, html, model } = req.body
|
|
270
325
|
const conversationId = session_id || `conv_${Date.now()}`
|
|
271
326
|
|
|
272
327
|
conversationMessages.set(conversationId, [])
|
|
273
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
|
+
|
|
274
349
|
if (session_id) {
|
|
275
350
|
const isResume = sessionTracking.has(session_id)
|
|
276
351
|
sessionTracking.set(session_id, { conversationId, isResume })
|
|
@@ -285,7 +360,13 @@ app.post('/conversations', (req, res) => {
|
|
|
285
360
|
|
|
286
361
|
app.post('/conversations/:id/messages', (req, res) => {
|
|
287
362
|
const { id } = req.params
|
|
288
|
-
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
|
+
}
|
|
289
370
|
|
|
290
371
|
if (!claudeProcesses.has(id)) {
|
|
291
372
|
let sessionId = null
|
|
@@ -375,6 +456,227 @@ app.get('/conversations/:id/stream', (req, res) => {
|
|
|
375
456
|
})
|
|
376
457
|
})
|
|
377
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
|
+
|
|
378
680
|
app.post('/conversations/:id/approve', (req, res) => {
|
|
379
681
|
const { id } = req.params
|
|
380
682
|
const { requestId, data } = req.body
|
|
@@ -432,7 +734,23 @@ function tryStartServer(ports, index = 0) {
|
|
|
432
734
|
console.log(`\nAuth Status:`)
|
|
433
735
|
const authStatus = checkClaudeAuth()
|
|
434
736
|
if (authStatus.authenticated) {
|
|
435
|
-
|
|
737
|
+
if (authStatus.subscriptionType) {
|
|
738
|
+
console.log(`✓ Authenticated (${authStatus.subscriptionType})`)
|
|
739
|
+
} else {
|
|
740
|
+
console.log(`✓ Authenticated`)
|
|
741
|
+
}
|
|
742
|
+
if (authStatus.method) {
|
|
743
|
+
console.log(` Method: ${authStatus.method}`)
|
|
744
|
+
}
|
|
745
|
+
if (authStatus.lastActivity) {
|
|
746
|
+
console.log(` Last activity: ${authStatus.lastActivity}`)
|
|
747
|
+
}
|
|
748
|
+
if (authStatus.expiresAt) {
|
|
749
|
+
console.log(` Expires: ${authStatus.expiresAt}`)
|
|
750
|
+
}
|
|
751
|
+
if (authStatus.subscriptionNote) {
|
|
752
|
+
console.log(` ${authStatus.subscriptionNote}`)
|
|
753
|
+
}
|
|
436
754
|
} else {
|
|
437
755
|
console.log(`✗ Not authenticated`)
|
|
438
756
|
console.log(` ${authStatus.error}`)
|
|
@@ -444,6 +762,8 @@ function tryStartServer(ports, index = 0) {
|
|
|
444
762
|
console.log(` POST /conversations`)
|
|
445
763
|
console.log(` POST /conversations/:id/messages`)
|
|
446
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)`)
|
|
447
767
|
console.log(` POST /conversations/:id/approve`)
|
|
448
768
|
console.log(` POST /conversations/:id/deny`)
|
|
449
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"
|