@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.
Files changed (3) hide show
  1. package/bin/get-chunk.js +135 -0
  2. package/index.js +353 -33
  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,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 (!fs.existsSync(credentialsPath)) {
28
- return {
29
- authenticated: false,
30
- error: 'Claude CLI not logged in. Run: npx @anthropic-ai/claude-code login'
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
- const credentials = JSON.parse(fs.readFileSync(credentialsPath, 'utf8'))
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: false,
39
- error: 'No Claude OAuth credentials found'
55
+ authenticated: true,
56
+ subscriptionType,
57
+ expiresAt,
58
+ method: 'credentials file'
40
59
  }
41
60
  }
42
61
 
43
- const { expiresAt, subscriptionType } = credentials.claudeAiOauth
44
- const isExpired = new Date(expiresAt) < new Date()
45
-
46
- if (isExpired) {
47
- return {
48
- authenticated: false,
49
- error: `Claude credentials expired at: ${expiresAt}`
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: true,
55
- subscriptionType,
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 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
+ }
73
122
  },
74
123
  response: {
75
124
  type: 'string',
76
- 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...").'
77
126
  },
78
127
  action: {
79
128
  type: 'string',
80
129
  enum: ['append', 'replace_all', 'replace_specific', 'remove_specific', 'none'],
81
- 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'
82
131
  },
83
132
  targetSelectors: {
84
133
  type: 'array',
85
- description: 'Selectors to target for replace/remove actions.',
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
- 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
+
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', 'plan',
107
- '--tools', '',
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
- console.log(`Adding JSON schema for structured DOM changes output`)
128
- 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))
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
- console.log(`✓ Authenticated (${authStatus.subscriptionType} subscription)`)
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.1",
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"