@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.
Files changed (3) hide show
  1. package/bin/get-chunk.js +135 -0
  2. package/index.js +294 -12
  3. package/package.json +6 -4
@@ -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 instruction objects.'
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: 'Conversational explanation and reasoning.'
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 the DOM changes should be applied.'
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: 'Selectors to target for replace/remove actions.',
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
- console.log(`Spawning Claude CLI process for conversation ${conversationId}...`)
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
- '--tools', '',
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
- console.log(`Adding JSON schema for structured DOM changes output`)
150
- args.push('--json-schema', JSON.stringify(DOM_CHANGES_SCHEMA))
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.2",
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": "./index.js"
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"