@absmartly/claude-code-bridge 1.0.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/README.md +84 -0
- package/index.js +481 -0
- package/package.json +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# @absmartly/claude-code-bridge
|
|
2
|
+
|
|
3
|
+
HTTP bridge server that enables the ABsmartly Browser Extension to communicate with Claude Code CLI for AI-powered A/B testing features.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
- Node.js 16+
|
|
8
|
+
- Claude Code CLI authenticated: `npx @anthropic-ai/claude-code login`
|
|
9
|
+
|
|
10
|
+
## Quick Start
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npx @absmartly/claude-code-bridge
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The server will start on `http://localhost:3000` by default.
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
### 1. Login to Claude CLI (one-time setup)
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npx @anthropic-ai/claude-code login
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Follow the prompts to authenticate with your Claude subscription.
|
|
27
|
+
|
|
28
|
+
### 2. Start the bridge server
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npx @absmartly/claude-code-bridge
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### 3. Configure ABsmartly Extension
|
|
35
|
+
|
|
36
|
+
In the extension settings:
|
|
37
|
+
1. Select "Claude Subscription" as your AI provider
|
|
38
|
+
2. The extension will automatically connect to `http://localhost:3000`
|
|
39
|
+
|
|
40
|
+
### Custom Port
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
PORT=3001 npx @absmartly/claude-code-bridge
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## API Endpoints
|
|
47
|
+
|
|
48
|
+
- `GET /health` - Health check and auth status
|
|
49
|
+
- `GET /auth/status` - Claude CLI authentication status
|
|
50
|
+
- `POST /conversations` - Create new conversation
|
|
51
|
+
- `POST /conversations/:id/messages` - Send message to Claude
|
|
52
|
+
- `GET /conversations/:id/stream` - Stream Claude responses (SSE)
|
|
53
|
+
- `POST /conversations/:id/approve` - Approve tool use
|
|
54
|
+
- `POST /conversations/:id/deny` - Deny tool use
|
|
55
|
+
|
|
56
|
+
## How It Works
|
|
57
|
+
|
|
58
|
+
1. **Authentication Check**: Reads your Claude credentials from `~/.claude/.credentials.json`
|
|
59
|
+
2. **Claude CLI Spawn**: Spawns `npx @anthropic-ai/claude-code --json` subprocess
|
|
60
|
+
3. **HTTP Bridge**: Provides REST API for the browser extension to communicate
|
|
61
|
+
4. **Message Forwarding**: Routes messages between the extension and Claude CLI
|
|
62
|
+
5. **Server-Sent Events**: Streams Claude responses back to the extension in real-time
|
|
63
|
+
|
|
64
|
+
## Troubleshooting
|
|
65
|
+
|
|
66
|
+
### "Claude CLI not logged in"
|
|
67
|
+
|
|
68
|
+
Run: `npx @anthropic-ai/claude-code login`
|
|
69
|
+
|
|
70
|
+
### "Port already in use"
|
|
71
|
+
|
|
72
|
+
Either:
|
|
73
|
+
- Kill the existing process: `pkill -f claude-code-bridge`
|
|
74
|
+
- Or use a different port: `PORT=3001 npx @absmartly/claude-code-bridge`
|
|
75
|
+
|
|
76
|
+
### Extension can't connect
|
|
77
|
+
|
|
78
|
+
- Ensure the bridge is running: `http://localhost:3000/health` should return JSON
|
|
79
|
+
- Check the port matches in extension settings
|
|
80
|
+
- Restart the extension
|
|
81
|
+
|
|
82
|
+
## License
|
|
83
|
+
|
|
84
|
+
MIT
|
package/index.js
ADDED
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const express = require('express')
|
|
4
|
+
const cors = require('cors')
|
|
5
|
+
const { spawn } = require('child_process')
|
|
6
|
+
const fs = require('fs')
|
|
7
|
+
const path = require('path')
|
|
8
|
+
const os = require('os')
|
|
9
|
+
|
|
10
|
+
const PREFERRED_PORTS = [3000, 3001, 3002, 3003, 3004]
|
|
11
|
+
const PORT = process.env.PORT ? parseInt(process.env.PORT) : null
|
|
12
|
+
|
|
13
|
+
const app = express()
|
|
14
|
+
app.use(cors())
|
|
15
|
+
app.use(express.json({ limit: '10mb' }))
|
|
16
|
+
|
|
17
|
+
const claudeProcesses = new Map()
|
|
18
|
+
const activeStreams = new Map()
|
|
19
|
+
const conversationMessages = new Map()
|
|
20
|
+
const sessionTracking = new Map()
|
|
21
|
+
const outputBuffers = new Map()
|
|
22
|
+
|
|
23
|
+
function checkClaudeAuth() {
|
|
24
|
+
try {
|
|
25
|
+
const credentialsPath = path.join(os.homedir(), '.claude', '.credentials.json')
|
|
26
|
+
|
|
27
|
+
if (!fs.existsSync(credentialsPath)) {
|
|
28
|
+
return {
|
|
29
|
+
authenticated: false,
|
|
30
|
+
error: 'Claude CLI not logged in. Run: npx @anthropic-ai/claude-code login'
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const credentials = JSON.parse(fs.readFileSync(credentialsPath, 'utf8'))
|
|
35
|
+
|
|
36
|
+
if (!credentials.claudeAiOauth) {
|
|
37
|
+
return {
|
|
38
|
+
authenticated: false,
|
|
39
|
+
error: 'No Claude OAuth credentials found'
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
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}`
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
authenticated: true,
|
|
55
|
+
subscriptionType,
|
|
56
|
+
expiresAt
|
|
57
|
+
}
|
|
58
|
+
} catch (error) {
|
|
59
|
+
return {
|
|
60
|
+
authenticated: false,
|
|
61
|
+
error: `Failed to check Claude credentials: ${error.message}`
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// JSON schema for structured DOM changes output
|
|
67
|
+
const DOM_CHANGES_SCHEMA = {
|
|
68
|
+
type: 'object',
|
|
69
|
+
properties: {
|
|
70
|
+
domChanges: {
|
|
71
|
+
type: 'array',
|
|
72
|
+
description: 'Array of DOM change instruction objects.'
|
|
73
|
+
},
|
|
74
|
+
response: {
|
|
75
|
+
type: 'string',
|
|
76
|
+
description: 'Conversational explanation and reasoning.'
|
|
77
|
+
},
|
|
78
|
+
action: {
|
|
79
|
+
type: 'string',
|
|
80
|
+
enum: ['append', 'replace_all', 'replace_specific', 'remove_specific', 'none'],
|
|
81
|
+
description: 'How the DOM changes should be applied.'
|
|
82
|
+
},
|
|
83
|
+
targetSelectors: {
|
|
84
|
+
type: 'array',
|
|
85
|
+
description: 'Selectors to target for replace/remove actions.',
|
|
86
|
+
items: { type: 'string' }
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
required: ['domChanges', 'response', 'action']
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function spawnClaudeForConversation(conversationId, systemPrompt, sessionId, isResume = false) {
|
|
93
|
+
if (claudeProcesses.has(conversationId)) {
|
|
94
|
+
console.log(`Claude CLI already running for conversation ${conversationId}`)
|
|
95
|
+
return claudeProcesses.get(conversationId)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
console.log(`Spawning Claude CLI process for conversation ${conversationId}...`)
|
|
99
|
+
const args = [
|
|
100
|
+
'@anthropic-ai/claude-code',
|
|
101
|
+
'--print',
|
|
102
|
+
'--verbose',
|
|
103
|
+
'--output-format', 'stream-json',
|
|
104
|
+
'--input-format', 'stream-json',
|
|
105
|
+
'--replay-user-messages',
|
|
106
|
+
'--permission-mode', 'plan',
|
|
107
|
+
'--tools', '',
|
|
108
|
+
'--settings', JSON.stringify({ disableClaudeMd: true })
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
if (sessionId) {
|
|
112
|
+
if (isResume) {
|
|
113
|
+
console.log(`Resuming session ${sessionId} for conversation ${conversationId}`)
|
|
114
|
+
args.push('--resume', sessionId)
|
|
115
|
+
} else {
|
|
116
|
+
console.log(`Starting new session ${sessionId} for conversation ${conversationId}`)
|
|
117
|
+
args.push('--session-id', sessionId)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (systemPrompt) {
|
|
122
|
+
console.log(`Using custom system prompt for conversation ${conversationId}`)
|
|
123
|
+
args.push('--system-prompt', systemPrompt)
|
|
124
|
+
}
|
|
125
|
+
|
|
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))
|
|
129
|
+
|
|
130
|
+
console.log(`[${conversationId}] āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā`)
|
|
131
|
+
console.log(`[${conversationId}] š SPAWNING CLAUDE CLI WITH ARGUMENTS:`)
|
|
132
|
+
console.log(`[${conversationId}] Command: npx ${args.join(' ')}`)
|
|
133
|
+
console.log(`[${conversationId}] Using --json-schema for structured output`)
|
|
134
|
+
console.log(`[${conversationId}] āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā`)
|
|
135
|
+
|
|
136
|
+
const claudeProcess = spawn('npx', args, {
|
|
137
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
claudeProcess.stdout.on('data', (data) => {
|
|
141
|
+
let buffer = outputBuffers.get(conversationId) || ''
|
|
142
|
+
buffer += data.toString()
|
|
143
|
+
|
|
144
|
+
const lines = buffer.split('\n')
|
|
145
|
+
buffer = lines.pop()
|
|
146
|
+
outputBuffers.set(conversationId, buffer)
|
|
147
|
+
|
|
148
|
+
for (const line of lines) {
|
|
149
|
+
if (!line.trim()) continue
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
const event = JSON.parse(line)
|
|
153
|
+
console.log(`[${conversationId}] āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā`)
|
|
154
|
+
console.log(`[${conversationId}] š¦ RAW EVENT FROM CLAUDE CLI:`)
|
|
155
|
+
console.log(JSON.stringify(event, null, 2))
|
|
156
|
+
console.log(`[${conversationId}] āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā`)
|
|
157
|
+
|
|
158
|
+
const res = activeStreams.get(conversationId)
|
|
159
|
+
if (res) {
|
|
160
|
+
if (event.type === 'assistant' && event.message?.content) {
|
|
161
|
+
console.log(`[${conversationId}] Processing assistant message with ${event.message.content.length} content blocks`)
|
|
162
|
+
for (const block of event.message.content) {
|
|
163
|
+
console.log(`[${conversationId}] Content block type: ${block.type}`)
|
|
164
|
+
if (block.type === 'text' && block.text) {
|
|
165
|
+
// Try to parse as JSON schema response
|
|
166
|
+
try {
|
|
167
|
+
const parsedJson = JSON.parse(block.text.trim())
|
|
168
|
+
// Check if it matches our schema (has required fields)
|
|
169
|
+
if (parsedJson.domChanges && parsedJson.response && parsedJson.action) {
|
|
170
|
+
console.log(`[${conversationId}] ā
Parsed JSON schema response, forwarding as structured data`)
|
|
171
|
+
console.log(`[${conversationId}] Structured data:`, JSON.stringify(parsedJson, null, 2))
|
|
172
|
+
// Send as tool_use-style event for compatibility
|
|
173
|
+
res.write(`data: ${JSON.stringify({ type: 'tool_use', data: parsedJson })}\n\n`)
|
|
174
|
+
// Also send the response text for display
|
|
175
|
+
if (parsedJson.response) {
|
|
176
|
+
res.write(`data: ${JSON.stringify({ type: 'text', data: parsedJson.response })}\n\n`)
|
|
177
|
+
}
|
|
178
|
+
} else {
|
|
179
|
+
// Not our schema format, send as regular text
|
|
180
|
+
res.write(`data: ${JSON.stringify({ type: 'text', data: block.text })}\n\n`)
|
|
181
|
+
}
|
|
182
|
+
} catch (e) {
|
|
183
|
+
// Not JSON, send as regular text
|
|
184
|
+
res.write(`data: ${JSON.stringify({ type: 'text', data: block.text })}\n\n`)
|
|
185
|
+
}
|
|
186
|
+
} else if (block.type === 'tool_use' && block.input) {
|
|
187
|
+
// Handle tool_use blocks (shouldn't happen with --json-schema, but keep for safety)
|
|
188
|
+
console.log(`[${conversationId}] ā
Found tool_use block, forwarding to client`)
|
|
189
|
+
console.log(`[${conversationId}] Tool input:`, JSON.stringify(block.input, null, 2))
|
|
190
|
+
const toolInput = block.input
|
|
191
|
+
res.write(`data: ${JSON.stringify({ type: 'tool_use', data: toolInput })}\n\n`)
|
|
192
|
+
if (toolInput.response) {
|
|
193
|
+
res.write(`data: ${JSON.stringify({ type: 'text', data: toolInput.response })}\n\n`)
|
|
194
|
+
}
|
|
195
|
+
} else {
|
|
196
|
+
console.log(`[${conversationId}] ā ļø Unknown or unhandled content block type:`, block.type)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
} else if (event.type === 'result') {
|
|
200
|
+
console.log(`[${conversationId}] Received result event - sending done`)
|
|
201
|
+
// Don't send result as text - we already sent the assistant message content
|
|
202
|
+
// Just signal that we're done
|
|
203
|
+
res.write(`data: ${JSON.stringify({ type: 'done' })}\n\n`)
|
|
204
|
+
res.end()
|
|
205
|
+
activeStreams.delete(conversationId)
|
|
206
|
+
outputBuffers.delete(conversationId)
|
|
207
|
+
} else if (event.type === 'error') {
|
|
208
|
+
res.write(`data: ${JSON.stringify({ type: 'error', data: event.error || 'Unknown error' })}\n\n`)
|
|
209
|
+
res.end()
|
|
210
|
+
activeStreams.delete(conversationId)
|
|
211
|
+
outputBuffers.delete(conversationId)
|
|
212
|
+
} else {
|
|
213
|
+
res.write(`data: ${JSON.stringify(event)}\n\n`)
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
} catch (err) {
|
|
217
|
+
console.error(`[${conversationId}] Failed to parse Claude output:`, err.message, 'Raw:', line.substring(0, 200))
|
|
218
|
+
|
|
219
|
+
const res = activeStreams.get(conversationId)
|
|
220
|
+
if (res) {
|
|
221
|
+
res.write(`data: ${JSON.stringify({
|
|
222
|
+
type: 'text',
|
|
223
|
+
data: 'Response generated but encountered parsing error. Check server logs for details.'
|
|
224
|
+
})}\n\n`)
|
|
225
|
+
res.write(`data: ${JSON.stringify({ type: 'done' })}\n\n`)
|
|
226
|
+
res.end()
|
|
227
|
+
activeStreams.delete(conversationId)
|
|
228
|
+
outputBuffers.delete(conversationId)
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
claudeProcess.stderr.on('data', (data) => {
|
|
235
|
+
console.error(`[${conversationId}] Claude CLI stderr:`, data.toString())
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
claudeProcess.on('exit', (code) => {
|
|
239
|
+
console.log(`[${conversationId}] Claude CLI exited with code ${code}`)
|
|
240
|
+
claudeProcesses.delete(conversationId)
|
|
241
|
+
outputBuffers.delete(conversationId)
|
|
242
|
+
const res = activeStreams.get(conversationId)
|
|
243
|
+
if (res) {
|
|
244
|
+
res.end()
|
|
245
|
+
activeStreams.delete(conversationId)
|
|
246
|
+
}
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
claudeProcesses.set(conversationId, claudeProcess)
|
|
250
|
+
return claudeProcess
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
app.get('/health', (req, res) => {
|
|
254
|
+
const authStatus = checkClaudeAuth()
|
|
255
|
+
res.json({
|
|
256
|
+
ok: true,
|
|
257
|
+
authenticated: authStatus.authenticated,
|
|
258
|
+
claudeProcesses: claudeProcesses.size,
|
|
259
|
+
...authStatus
|
|
260
|
+
})
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
app.get('/auth/status', (req, res) => {
|
|
264
|
+
const authStatus = checkClaudeAuth()
|
|
265
|
+
res.json(authStatus)
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
app.post('/conversations', (req, res) => {
|
|
269
|
+
const { session_id } = req.body
|
|
270
|
+
const conversationId = session_id || `conv_${Date.now()}`
|
|
271
|
+
|
|
272
|
+
conversationMessages.set(conversationId, [])
|
|
273
|
+
|
|
274
|
+
if (session_id) {
|
|
275
|
+
const isResume = sessionTracking.has(session_id)
|
|
276
|
+
sessionTracking.set(session_id, { conversationId, isResume })
|
|
277
|
+
console.log(`Conversation ${conversationId} ${isResume ? 'resuming' : 'starting'} session ${session_id}`)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
res.json({
|
|
281
|
+
success: true,
|
|
282
|
+
conversationId
|
|
283
|
+
})
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
app.post('/conversations/:id/messages', (req, res) => {
|
|
287
|
+
const { id } = req.params
|
|
288
|
+
const { content, files, systemPrompt } = req.body
|
|
289
|
+
|
|
290
|
+
if (!claudeProcesses.has(id)) {
|
|
291
|
+
let sessionId = null
|
|
292
|
+
let isResume = false
|
|
293
|
+
|
|
294
|
+
for (const [sid, info] of sessionTracking.entries()) {
|
|
295
|
+
if (info.conversationId === id) {
|
|
296
|
+
sessionId = sid
|
|
297
|
+
isResume = info.isResume
|
|
298
|
+
break
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
spawnClaudeForConversation(id, systemPrompt, sessionId, isResume)
|
|
303
|
+
setTimeout(() => {
|
|
304
|
+
sendUserMessage(id, content, files)
|
|
305
|
+
}, 1000)
|
|
306
|
+
} else {
|
|
307
|
+
sendUserMessage(id, content, files)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
res.json({
|
|
311
|
+
success: true
|
|
312
|
+
})
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
function sendUserMessage(conversationId, content, files) {
|
|
316
|
+
const claudeProcess = claudeProcesses.get(conversationId)
|
|
317
|
+
if (!claudeProcess) {
|
|
318
|
+
console.error(`[${conversationId}] No Claude process found`)
|
|
319
|
+
return
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const messages = conversationMessages.get(conversationId) || []
|
|
323
|
+
messages.push({ role: 'user', content })
|
|
324
|
+
conversationMessages.set(conversationId, messages)
|
|
325
|
+
|
|
326
|
+
let messageContent
|
|
327
|
+
|
|
328
|
+
if (files && files.length > 0) {
|
|
329
|
+
messageContent = [{ type: 'text', text: content }]
|
|
330
|
+
|
|
331
|
+
for (const file of files) {
|
|
332
|
+
const match = file.match(/^data:image\/(\w+);base64,(.+)$/)
|
|
333
|
+
if (match) {
|
|
334
|
+
const [, format, data] = match
|
|
335
|
+
messageContent.push({
|
|
336
|
+
type: 'image',
|
|
337
|
+
source: {
|
|
338
|
+
type: 'base64',
|
|
339
|
+
media_type: `image/${format}`,
|
|
340
|
+
data: data
|
|
341
|
+
}
|
|
342
|
+
})
|
|
343
|
+
} else {
|
|
344
|
+
console.warn(`[${conversationId}] Invalid data URI format: ${file.substring(0, 50)}...`)
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
console.log(`[${conversationId}] Sending message with ${files.length} image(s)`)
|
|
348
|
+
} else {
|
|
349
|
+
messageContent = content
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const userMessage = {
|
|
353
|
+
type: 'user',
|
|
354
|
+
message: {
|
|
355
|
+
role: 'user',
|
|
356
|
+
content: messageContent
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
console.log(`[${conversationId}] Sending to Claude:`, JSON.stringify(userMessage).substring(0, 200))
|
|
361
|
+
claudeProcess.stdin.write(JSON.stringify(userMessage) + '\n')
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
app.get('/conversations/:id/stream', (req, res) => {
|
|
365
|
+
const { id } = req.params
|
|
366
|
+
|
|
367
|
+
res.setHeader('Content-Type', 'text/event-stream')
|
|
368
|
+
res.setHeader('Cache-Control', 'no-cache')
|
|
369
|
+
res.setHeader('Connection', 'keep-alive')
|
|
370
|
+
|
|
371
|
+
activeStreams.set(id, res)
|
|
372
|
+
|
|
373
|
+
req.on('close', () => {
|
|
374
|
+
activeStreams.delete(id)
|
|
375
|
+
})
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
app.post('/conversations/:id/approve', (req, res) => {
|
|
379
|
+
const { id } = req.params
|
|
380
|
+
const { requestId, data } = req.body
|
|
381
|
+
|
|
382
|
+
if (!claudeProcess) {
|
|
383
|
+
return res.status(400).json({
|
|
384
|
+
error: 'Claude CLI not started'
|
|
385
|
+
})
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
claudeProcess.stdin.write(JSON.stringify({
|
|
389
|
+
type: 'control',
|
|
390
|
+
action: 'approve',
|
|
391
|
+
requestId,
|
|
392
|
+
data
|
|
393
|
+
}) + '\n')
|
|
394
|
+
|
|
395
|
+
res.json({
|
|
396
|
+
success: true
|
|
397
|
+
})
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
app.post('/conversations/:id/deny', (req, res) => {
|
|
401
|
+
const { id } = req.params
|
|
402
|
+
const { requestId, reason } = req.body
|
|
403
|
+
|
|
404
|
+
if (!claudeProcess) {
|
|
405
|
+
return res.status(400).json({
|
|
406
|
+
error: 'Claude CLI not started'
|
|
407
|
+
})
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
claudeProcess.stdin.write(JSON.stringify({
|
|
411
|
+
type: 'control',
|
|
412
|
+
action: 'deny',
|
|
413
|
+
requestId,
|
|
414
|
+
reason
|
|
415
|
+
}) + '\n')
|
|
416
|
+
|
|
417
|
+
res.json({
|
|
418
|
+
success: true
|
|
419
|
+
})
|
|
420
|
+
})
|
|
421
|
+
|
|
422
|
+
function tryStartServer(ports, index = 0) {
|
|
423
|
+
if (index >= ports.length) {
|
|
424
|
+
console.error(`\nā Failed to start server on any port (tried ${ports.join(', ')})`)
|
|
425
|
+
process.exit(1)
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const port = ports[index]
|
|
429
|
+
const server = app.listen(port)
|
|
430
|
+
.on('listening', () => {
|
|
431
|
+
console.log(`\nā
ABsmartly Claude Code Bridge running on http://localhost:${port}`)
|
|
432
|
+
console.log(`\nAuth Status:`)
|
|
433
|
+
const authStatus = checkClaudeAuth()
|
|
434
|
+
if (authStatus.authenticated) {
|
|
435
|
+
console.log(`ā Authenticated (${authStatus.subscriptionType} subscription)`)
|
|
436
|
+
} else {
|
|
437
|
+
console.log(`ā Not authenticated`)
|
|
438
|
+
console.log(` ${authStatus.error}`)
|
|
439
|
+
console.log(`\n Run: npx @anthropic-ai/claude-code login`)
|
|
440
|
+
}
|
|
441
|
+
console.log(`\nEndpoints:`)
|
|
442
|
+
console.log(` GET /health`)
|
|
443
|
+
console.log(` GET /auth/status`)
|
|
444
|
+
console.log(` POST /conversations`)
|
|
445
|
+
console.log(` POST /conversations/:id/messages`)
|
|
446
|
+
console.log(` GET /conversations/:id/stream`)
|
|
447
|
+
console.log(` POST /conversations/:id/approve`)
|
|
448
|
+
console.log(` POST /conversations/:id/deny`)
|
|
449
|
+
console.log(`\nReady for connections from ABsmartly extension š\n`)
|
|
450
|
+
|
|
451
|
+
setupShutdownHandlers(server)
|
|
452
|
+
})
|
|
453
|
+
.on('error', (err) => {
|
|
454
|
+
if (err.code === 'EADDRINUSE') {
|
|
455
|
+
console.log(`ā ļø Port ${port} is already in use, trying next port...`)
|
|
456
|
+
tryStartServer(ports, index + 1)
|
|
457
|
+
} else {
|
|
458
|
+
console.error(`\nā Error starting server:`, err)
|
|
459
|
+
process.exit(1)
|
|
460
|
+
}
|
|
461
|
+
})
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function setupShutdownHandlers(server) {
|
|
465
|
+
const shutdown = () => {
|
|
466
|
+
console.log('\nShutting down...')
|
|
467
|
+
if (claudeProcess) {
|
|
468
|
+
claudeProcess.kill()
|
|
469
|
+
}
|
|
470
|
+
server.close(() => {
|
|
471
|
+
console.log('Server closed')
|
|
472
|
+
process.exit(0)
|
|
473
|
+
})
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
process.on('SIGTERM', shutdown)
|
|
477
|
+
process.on('SIGINT', shutdown)
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const portsToTry = PORT ? [PORT] : PREFERRED_PORTS
|
|
481
|
+
tryStartServer(portsToTry)
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@absmartly/claude-code-bridge",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "HTTP bridge server for ABsmartly Extension to communicate with Claude Code CLI",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"claude-code-bridge": "./index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "nodemon --watch index.js --watch package.json --exitcrash index.js"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"claude",
|
|
14
|
+
"absmartly",
|
|
15
|
+
"ab-testing",
|
|
16
|
+
"bridge"
|
|
17
|
+
],
|
|
18
|
+
"author": "ABsmartly",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"express": "^4.18.2",
|
|
22
|
+
"cors": "^2.8.5",
|
|
23
|
+
"dotenv": "^16.3.1"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"nodemon": "^3.0.1"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=16.0.0"
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/absmartly/claude-code-bridge.git"
|
|
37
|
+
},
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/absmartly/claude-code-bridge/issues"
|
|
40
|
+
},
|
|
41
|
+
"homepage": "https://github.com/absmartly/claude-code-bridge#readme"
|
|
42
|
+
}
|