claudelink-bridge 0.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/cli.js +178 -0
- package/bridge.js +92 -0
- package/lib/platform.js +51 -0
- package/lib/server.js +203 -0
- package/lib/service.js +151 -0
- package/package.json +26 -0
package/bin/cli.js
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync } from 'fs';
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { dirname, join } from 'path';
|
|
6
|
+
|
|
7
|
+
// Auto-install dependencies if ws is missing (happens when running from source)
|
|
8
|
+
const __dir = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const bridgeRoot = join(__dir, '..');
|
|
10
|
+
if (!existsSync(join(bridgeRoot, 'node_modules', 'ws'))) {
|
|
11
|
+
console.log('\x1b[36m→\x1b[0m Installing bridge dependencies…');
|
|
12
|
+
execSync('npm install --silent', { cwd: bridgeRoot, stdio: 'inherit' });
|
|
13
|
+
console.log('\x1b[32m✓\x1b[0m Dependencies installed\n');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
import { isClaudeInstalled, isNodeInstalled, OS } from '../lib/platform.js';
|
|
17
|
+
import { install, uninstall, status } from '../lib/service.js';
|
|
18
|
+
|
|
19
|
+
const cmd = process.argv[2];
|
|
20
|
+
|
|
21
|
+
const BOLD = '\x1b[1m';
|
|
22
|
+
const GREEN = '\x1b[32m';
|
|
23
|
+
const RED = '\x1b[31m';
|
|
24
|
+
const YELLOW= '\x1b[33m';
|
|
25
|
+
const CYAN = '\x1b[36m';
|
|
26
|
+
const RESET = '\x1b[0m';
|
|
27
|
+
|
|
28
|
+
function ok(msg) { console.log(`${GREEN}✓${RESET} ${msg}`); }
|
|
29
|
+
function fail(msg) { console.log(`${RED}✗${RESET} ${msg}`); }
|
|
30
|
+
function info(msg) { console.log(`${CYAN}→${RESET} ${msg}`); }
|
|
31
|
+
function warn(msg) { console.log(`${YELLOW}!${RESET} ${msg}`); }
|
|
32
|
+
function title(msg){ console.log(`\n${BOLD}${msg}${RESET}`); }
|
|
33
|
+
|
|
34
|
+
// ─── setup ────────────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
if (!cmd || cmd === 'setup') {
|
|
37
|
+
title('⚡ ClaudeLink Bridge Setup');
|
|
38
|
+
console.log('This installs the bridge as a background service that starts\nautomatically on login — no manual steps after this.\n');
|
|
39
|
+
|
|
40
|
+
// Check prerequisites
|
|
41
|
+
title('Checking prerequisites…');
|
|
42
|
+
|
|
43
|
+
if (!isNodeInstalled()) {
|
|
44
|
+
fail('Node.js not found. Install it from https://nodejs.org (v18+)');
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
ok('Node.js installed');
|
|
48
|
+
|
|
49
|
+
if (!isClaudeInstalled()) {
|
|
50
|
+
fail('Claude Code CLI not found.');
|
|
51
|
+
info('Install it with: npm install -g @anthropic-ai/claude-code');
|
|
52
|
+
info('Then run: claude (to log in)');
|
|
53
|
+
info('Then re-run: npx claudelink-bridge setup');
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
ok('Claude Code CLI installed');
|
|
57
|
+
|
|
58
|
+
// Install service
|
|
59
|
+
title('Installing bridge service…');
|
|
60
|
+
try {
|
|
61
|
+
install();
|
|
62
|
+
ok('Bridge service installed and started');
|
|
63
|
+
} catch (err) {
|
|
64
|
+
fail(`Service install failed: ${err.message}`);
|
|
65
|
+
info('You can still run the bridge manually: npx claudelink-bridge start');
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Done
|
|
70
|
+
console.log(`
|
|
71
|
+
${GREEN}${BOLD}Setup complete!${RESET}
|
|
72
|
+
|
|
73
|
+
The bridge is now running on ${BOLD}ws://localhost:9999${RESET} and will
|
|
74
|
+
auto-start every time you log in.
|
|
75
|
+
|
|
76
|
+
${BOLD}Next steps:${RESET}
|
|
77
|
+
1. Open Chrome and pin the ClaudeLink extension
|
|
78
|
+
2. The status indicator should be ${GREEN}green${RESET} — you're ready to go
|
|
79
|
+
|
|
80
|
+
${BOLD}Useful commands:${RESET}
|
|
81
|
+
npx claudelink-bridge status Check if the bridge is running
|
|
82
|
+
npx claudelink-bridge stop Stop the service
|
|
83
|
+
npx claudelink-bridge start Start the service
|
|
84
|
+
npx claudelink-bridge uninstall Remove the service
|
|
85
|
+
`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ─── start (manual run) ───────────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
else if (cmd === 'start') {
|
|
91
|
+
// Check if already running before starting
|
|
92
|
+
const port = parseInt(process.env.CLAUDELINK_PORT ?? '9999');
|
|
93
|
+
const alreadyRunning = await checkPortRunning(port);
|
|
94
|
+
if (alreadyRunning) {
|
|
95
|
+
ok(`Bridge is already running on port ${port}. Nothing to do.`);
|
|
96
|
+
process.exit(0);
|
|
97
|
+
}
|
|
98
|
+
info('Starting bridge server (Ctrl+C to stop)…');
|
|
99
|
+
await import('../lib/server.js');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ─── kill (force kill whatever is on the port) ───────────────────────────────
|
|
103
|
+
|
|
104
|
+
else if (cmd === 'kill') {
|
|
105
|
+
const port = parseInt(process.env.CLAUDELINK_PORT ?? '9999');
|
|
106
|
+
try {
|
|
107
|
+
const { execSync } = await import('child_process');
|
|
108
|
+
if (OS === 'win32') {
|
|
109
|
+
execSync(`for /f "tokens=5" %a in ('netstat -aon ^| findstr :${port}') do taskkill /F /PID %a`, { shell: true, stdio: 'ignore' });
|
|
110
|
+
} else {
|
|
111
|
+
execSync(`lsof -ti:${port} | xargs kill -9 2>/dev/null || true`, { shell: true, stdio: 'ignore' });
|
|
112
|
+
}
|
|
113
|
+
ok(`Killed process on port ${port}`);
|
|
114
|
+
} catch { warn(`Nothing was running on port ${port}`); }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ─── status ───────────────────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
else if (cmd === 'status') {
|
|
120
|
+
const s = status();
|
|
121
|
+
if (s === 'running') ok(`Bridge service is ${GREEN}running${RESET}`);
|
|
122
|
+
else warn(`Bridge service is ${RED}stopped${RESET} — run: npx claudelink-bridge setup`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ─── stop ─────────────────────────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
else if (cmd === 'stop') {
|
|
128
|
+
try {
|
|
129
|
+
if (OS === 'darwin') { const { execSync } = await import('child_process'); execSync(`launchctl stop com.devops-monk.claudelink`); }
|
|
130
|
+
if (OS === 'linux') { const { execSync } = await import('child_process'); execSync('systemctl --user stop claudelink'); }
|
|
131
|
+
if (OS === 'win32') { const { execSync } = await import('child_process'); execSync('schtasks /End /TN ClaudeLinkBridge', { shell: true }); }
|
|
132
|
+
ok('Bridge service stopped');
|
|
133
|
+
} catch (e) { fail(`Could not stop: ${e.message}`); }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ─── uninstall ────────────────────────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
else if (cmd === 'uninstall') {
|
|
139
|
+
try {
|
|
140
|
+
uninstall();
|
|
141
|
+
ok('Bridge service removed');
|
|
142
|
+
} catch (e) { fail(`Uninstall failed: ${e.message}`); }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ─── unknown ──────────────────────────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
else {
|
|
148
|
+
console.log(`Usage: npx claudelink-bridge <command>
|
|
149
|
+
|
|
150
|
+
Commands:
|
|
151
|
+
setup Install bridge as a background auto-start service (run once)
|
|
152
|
+
start Run the bridge manually in the foreground (Ctrl+C to stop)
|
|
153
|
+
kill Force-kill whatever is running on port 9999
|
|
154
|
+
status Check if the background service is running
|
|
155
|
+
stop Stop the background service
|
|
156
|
+
uninstall Remove the background service
|
|
157
|
+
`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ─── Helper ───────────────────────────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
async function checkPortRunning(port) {
|
|
163
|
+
return new Promise((resolve) => {
|
|
164
|
+
import('ws').then(({ default: WS }) => {
|
|
165
|
+
const ws = new WS(`ws://localhost:${port}`);
|
|
166
|
+
const t = setTimeout(() => { try { ws.terminate(); } catch {} resolve(false); }, 800);
|
|
167
|
+
ws.on('open', () => ws.send(JSON.stringify({ type: 'ping' })));
|
|
168
|
+
ws.on('message', (data) => {
|
|
169
|
+
try {
|
|
170
|
+
if (JSON.parse(data.toString()).type === 'pong') {
|
|
171
|
+
clearTimeout(t); ws.terminate(); resolve(true);
|
|
172
|
+
}
|
|
173
|
+
} catch { resolve(false); }
|
|
174
|
+
});
|
|
175
|
+
ws.on('error', () => { clearTimeout(t); resolve(false); });
|
|
176
|
+
}).catch(() => resolve(false));
|
|
177
|
+
});
|
|
178
|
+
}
|
package/bridge.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ClaudeLink bridge — run once: node bridge/bridge.js
|
|
3
|
+
// Connects Claude Code CLI to the browser extension via WebSocket.
|
|
4
|
+
|
|
5
|
+
import { WebSocketServer } from 'ws';
|
|
6
|
+
import { spawn } from 'child_process';
|
|
7
|
+
import { writeFileSync, unlinkSync } from 'fs';
|
|
8
|
+
import { tmpdir } from 'os';
|
|
9
|
+
import { join } from 'path';
|
|
10
|
+
|
|
11
|
+
const PORT = process.env.CLAUDELINK_PORT ? parseInt(process.env.CLAUDELINK_PORT) : 9999;
|
|
12
|
+
const ALLOWED_ORIGIN = 'chrome-extension://';
|
|
13
|
+
|
|
14
|
+
const wss = new WebSocketServer({ port: PORT });
|
|
15
|
+
|
|
16
|
+
console.log(`\n⚡ ClaudeLink bridge running on ws://localhost:${PORT}`);
|
|
17
|
+
console.log(` Waiting for extension to connect…\n`);
|
|
18
|
+
|
|
19
|
+
wss.on('connection', (ws, req) => {
|
|
20
|
+
const origin = req.headers['origin'] ?? '';
|
|
21
|
+
if (!origin.startsWith(ALLOWED_ORIGIN)) {
|
|
22
|
+
ws.close(1008, 'Forbidden');
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
console.log(`[+] Extension connected`);
|
|
27
|
+
|
|
28
|
+
ws.on('message', async (raw) => {
|
|
29
|
+
let msg;
|
|
30
|
+
try { msg = JSON.parse(raw.toString()); }
|
|
31
|
+
catch { ws.send(JSON.stringify({ type: 'error', text: 'Invalid JSON' })); return; }
|
|
32
|
+
|
|
33
|
+
if (msg.type === 'ping') {
|
|
34
|
+
ws.send(JSON.stringify({ type: 'pong' }));
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (msg.type === 'prompt') {
|
|
39
|
+
console.log(`[>] Prompt: ${String(msg.prompt).slice(0, 80)}…`);
|
|
40
|
+
runClaude(['--print', msg.prompt], ws);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (msg.type === 'file-prompt') {
|
|
45
|
+
const tmpFile = join(tmpdir(), `claudelink-${Date.now()}.md`);
|
|
46
|
+
writeFileSync(tmpFile, msg.content);
|
|
47
|
+
console.log(`[>] File prompt: ${tmpFile}`);
|
|
48
|
+
runClaude(['-p', tmpFile], ws, () => { try { unlinkSync(tmpFile); } catch {} });
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (msg.type === 'run-script') {
|
|
53
|
+
const tmpScript = join(tmpdir(), `claudelink-script-${Date.now()}.sh`);
|
|
54
|
+
writeFileSync(tmpScript, msg.script, { mode: 0o755 });
|
|
55
|
+
console.log(`[>] Running script: ${tmpScript}`);
|
|
56
|
+
runScript(tmpScript, ws, () => { try { unlinkSync(tmpScript); } catch {} });
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
ws.on('close', () => console.log(`[-] Extension disconnected`));
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
function runClaude(args, ws, onClose) {
|
|
65
|
+
const proc = spawn('claude', args, { env: { ...process.env } });
|
|
66
|
+
proc.stdout.on('data', (d) => ws.send(JSON.stringify({ type: 'chunk', text: d.toString() })));
|
|
67
|
+
proc.stderr.on('data', (d) => ws.send(JSON.stringify({ type: 'error', text: d.toString() })));
|
|
68
|
+
proc.on('close', (code) => {
|
|
69
|
+
onClose?.();
|
|
70
|
+
ws.send(JSON.stringify({ type: 'done', exitCode: code }));
|
|
71
|
+
console.log(`[✓] Done (exit ${code})`);
|
|
72
|
+
});
|
|
73
|
+
proc.on('error', (e) => {
|
|
74
|
+
ws.send(JSON.stringify({ type: 'error', text: `Failed to start claude: ${e.message}\nMake sure Claude Code CLI is installed: npm install -g @anthropic-ai/claude-code` }));
|
|
75
|
+
ws.send(JSON.stringify({ type: 'done', exitCode: 1 }));
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function runScript(scriptPath, ws, onClose) {
|
|
80
|
+
const proc = spawn('bash', [scriptPath], { env: { ...process.env } });
|
|
81
|
+
proc.stdout.on('data', (d) => ws.send(JSON.stringify({ type: 'chunk', text: d.toString() })));
|
|
82
|
+
proc.stderr.on('data', (d) => ws.send(JSON.stringify({ type: 'error', text: d.toString() })));
|
|
83
|
+
proc.on('close', (code) => {
|
|
84
|
+
onClose?.();
|
|
85
|
+
ws.send(JSON.stringify({ type: 'done', exitCode: code }));
|
|
86
|
+
console.log(`[✓] Script done (exit ${code})`);
|
|
87
|
+
});
|
|
88
|
+
proc.on('error', (e) => {
|
|
89
|
+
ws.send(JSON.stringify({ type: 'error', text: `Script error: ${e.message}` }));
|
|
90
|
+
ws.send(JSON.stringify({ type: 'done', exitCode: 1 }));
|
|
91
|
+
});
|
|
92
|
+
}
|
package/lib/platform.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { platform, homedir } from 'os';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { existsSync } from 'fs';
|
|
4
|
+
import { execSync } from 'child_process';
|
|
5
|
+
|
|
6
|
+
export const OS = platform(); // 'darwin' | 'linux' | 'win32'
|
|
7
|
+
|
|
8
|
+
export const HOME = homedir();
|
|
9
|
+
export const PORT = parseInt(process.env.CLAUDELINK_PORT ?? '9999');
|
|
10
|
+
|
|
11
|
+
export const SERVICE_NAME = 'com.devops-monk.claudelink';
|
|
12
|
+
|
|
13
|
+
// Path to this package's server entry point
|
|
14
|
+
export const SERVER_SCRIPT = new URL('../lib/server.js', import.meta.url).pathname;
|
|
15
|
+
|
|
16
|
+
export function getServicePaths() {
|
|
17
|
+
if (OS === 'darwin') {
|
|
18
|
+
return {
|
|
19
|
+
plist: join(HOME, 'Library', 'LaunchAgents', `${SERVICE_NAME}.plist`),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
if (OS === 'linux') {
|
|
23
|
+
return {
|
|
24
|
+
service: join(HOME, '.config', 'systemd', 'user', 'claudelink.service'),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
if (OS === 'win32') {
|
|
28
|
+
return {
|
|
29
|
+
taskName: 'ClaudeLinkBridge',
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
return {};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function isClaudeInstalled() {
|
|
36
|
+
try {
|
|
37
|
+
execSync('claude --version', { stdio: 'ignore' });
|
|
38
|
+
return true;
|
|
39
|
+
} catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function isNodeInstalled() {
|
|
45
|
+
try {
|
|
46
|
+
execSync('node --version', { stdio: 'ignore' });
|
|
47
|
+
return true;
|
|
48
|
+
} catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
package/lib/server.js
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { WebSocketServer } from 'ws';
|
|
3
|
+
import { spawn } from 'child_process';
|
|
4
|
+
import { writeFileSync, unlinkSync, mkdirSync } from 'fs';
|
|
5
|
+
import { tmpdir, homedir } from 'os';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
|
|
8
|
+
const PORT = process.env.CLAUDELINK_PORT ? parseInt(process.env.CLAUDELINK_PORT) : 9999;
|
|
9
|
+
const ALLOWED_ORIGIN = 'chrome-extension://';
|
|
10
|
+
|
|
11
|
+
// Persistent screenshot dir so images survive long sessions
|
|
12
|
+
const SCREENSHOT_DIR = join(homedir(), '.claudelink', 'screenshots');
|
|
13
|
+
mkdirSync(SCREENSHOT_DIR, { recursive: true });
|
|
14
|
+
|
|
15
|
+
// Increase max payload to 50MB to handle large screenshots
|
|
16
|
+
const wss = new WebSocketServer({ port: PORT, maxPayload: 50 * 1024 * 1024 });
|
|
17
|
+
|
|
18
|
+
wss.on('error', (err) => {
|
|
19
|
+
if (err.code === 'EADDRINUSE') {
|
|
20
|
+
// Check if it's already our bridge running on that port
|
|
21
|
+
checkIfAlreadyRunning(PORT).then(running => {
|
|
22
|
+
if (running) {
|
|
23
|
+
console.log(`\n✓ ClaudeLink bridge is already running on port ${PORT}.`);
|
|
24
|
+
console.log(` Nothing to do — the extension is connected.\n`);
|
|
25
|
+
} else {
|
|
26
|
+
console.error(`\n✗ Port ${PORT} is already in use by another process.`);
|
|
27
|
+
console.error(`\n To fix this, run one of:`);
|
|
28
|
+
console.error(` lsof -ti:${PORT} | xargs kill -9 # kill whatever is on the port`);
|
|
29
|
+
console.error(` CLAUDELINK_PORT=9998 node bridge/lib/server.js # use a different port\n`);
|
|
30
|
+
}
|
|
31
|
+
process.exit(0);
|
|
32
|
+
});
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
console.error('[ClaudeLink] Server error:', err);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
function checkIfAlreadyRunning(port) {
|
|
39
|
+
return new Promise((resolve) => {
|
|
40
|
+
import('ws').then(({ default: WS }) => {
|
|
41
|
+
const ws = new WS(`ws://localhost:${port}`);
|
|
42
|
+
const t = setTimeout(() => { ws.terminate(); resolve(false); }, 1000);
|
|
43
|
+
ws.on('open', () => {
|
|
44
|
+
ws.send(JSON.stringify({ type: 'ping' }));
|
|
45
|
+
});
|
|
46
|
+
ws.on('message', (data) => {
|
|
47
|
+
try {
|
|
48
|
+
const msg = JSON.parse(data.toString());
|
|
49
|
+
if (msg.type === 'pong') { clearTimeout(t); ws.terminate(); resolve(true); }
|
|
50
|
+
} catch { resolve(false); }
|
|
51
|
+
});
|
|
52
|
+
ws.on('error', () => { clearTimeout(t); resolve(false); });
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
console.log(`\n⚡ ClaudeLink bridge running on ws://localhost:${PORT}`);
|
|
58
|
+
console.log(` Waiting for extension to connect…\n`);
|
|
59
|
+
|
|
60
|
+
wss.on('connection', (ws, req) => {
|
|
61
|
+
const origin = req.headers['origin'] ?? '';
|
|
62
|
+
if (!origin.startsWith(ALLOWED_ORIGIN)) {
|
|
63
|
+
ws.close(1008, 'Forbidden');
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
console.log(`[+] Extension connected`);
|
|
67
|
+
|
|
68
|
+
ws.on('message', async (raw) => {
|
|
69
|
+
let msg;
|
|
70
|
+
try { msg = JSON.parse(raw.toString()); }
|
|
71
|
+
catch { ws.send(JSON.stringify({ type: 'error', text: 'Invalid JSON from extension' })); return; }
|
|
72
|
+
|
|
73
|
+
if (msg.type === 'ping') {
|
|
74
|
+
ws.send(JSON.stringify({ type: 'pong' }));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (msg.type === 'prompt') {
|
|
79
|
+
console.log(`[>] Prompt: "${String(msg.prompt).slice(0, 80)}"`);
|
|
80
|
+
runClaude(msg.prompt, ws);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (msg.type === 'file-prompt') {
|
|
85
|
+
const tmpFile = join(tmpdir(), `claudelink-${Date.now()}.md`);
|
|
86
|
+
writeFileSync(tmpFile, msg.content);
|
|
87
|
+
console.log(`[>] File prompt: ${tmpFile}`);
|
|
88
|
+
runClaudeFile(tmpFile, ws, () => cleanup(tmpFile));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (msg.type === 'image-prompt') {
|
|
93
|
+
console.log(`[>] Image prompt received (${(raw.length / 1024).toFixed(0)}KB)`);
|
|
94
|
+
const imgFile = join(SCREENSHOT_DIR, `screenshot-${Date.now()}.png`);
|
|
95
|
+
try {
|
|
96
|
+
const base64 = msg.imageDataUrl.replace(/^data:image\/\w+;base64,/, '');
|
|
97
|
+
writeFileSync(imgFile, Buffer.from(base64, 'base64'));
|
|
98
|
+
console.log(`[>] Screenshot saved: ${imgFile}`);
|
|
99
|
+
} catch (e) {
|
|
100
|
+
safeSend(ws, { type: 'error', text: `Failed to save screenshot: ${e.message}` });
|
|
101
|
+
safeSend(ws, { type: 'done', exitCode: 1 });
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const promptText = (msg.prompt || '').trim() || 'Describe this screenshot in detail.';
|
|
106
|
+
// Write a markdown prompt file with the image embedded as a data URL
|
|
107
|
+
// This lets Claude Code see the image inline without needing file read permissions
|
|
108
|
+
const base64Data = msg.imageDataUrl;
|
|
109
|
+
const mdFile = join(SCREENSHOT_DIR, `prompt-${Date.now()}.md`);
|
|
110
|
+
writeFileSync(mdFile, `${promptText}\n\n\n`);
|
|
111
|
+
console.log(`[>] Running image prompt via -p`);
|
|
112
|
+
runClaudeFile(mdFile, ws, () => cleanup(imgFile, mdFile));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (msg.type === 'run-script') {
|
|
117
|
+
const tmpScript = join(tmpdir(), `claudelink-script-${Date.now()}.sh`);
|
|
118
|
+
writeFileSync(tmpScript, msg.script, { mode: 0o755 });
|
|
119
|
+
console.log(`[>] Running script`);
|
|
120
|
+
runScript(tmpScript, ws, () => cleanup(tmpScript));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
ws.on('close', () => console.log(`[-] Extension disconnected`));
|
|
126
|
+
ws.on('error', (e) => console.error(`[!] WS error: ${e.message}`));
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// ─── Runners ──────────────────────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
function runClaude(prompt, ws, onClose) {
|
|
132
|
+
const proc = spawn('claude', ['--print', '--dangerously-skip-permissions', prompt], {
|
|
133
|
+
env: { ...process.env },
|
|
134
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
proc.stdout.on('data', (d) => safeSend(ws, { type: 'chunk', text: d.toString() }));
|
|
138
|
+
proc.stderr.on('data', (d) => {
|
|
139
|
+
const text = d.toString();
|
|
140
|
+
// Filter the known benign stdin warning — user already sees output
|
|
141
|
+
if (!text.includes('no stdin data received')) {
|
|
142
|
+
safeSend(ws, { type: 'error', text });
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
proc.on('close', (code) => {
|
|
146
|
+
onClose?.();
|
|
147
|
+
safeSend(ws, { type: 'done', exitCode: code });
|
|
148
|
+
console.log(`[✓] Done (exit ${code})`);
|
|
149
|
+
});
|
|
150
|
+
proc.on('error', (e) => {
|
|
151
|
+
safeSend(ws, { type: 'error', text: `Failed to start claude: ${e.message}\n\nIs Claude Code CLI installed?\n npm install -g @anthropic-ai/claude-code` });
|
|
152
|
+
safeSend(ws, { type: 'done', exitCode: 1 });
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function runClaudeFile(file, ws, onClose) {
|
|
157
|
+
const proc = spawn('claude', ['-p', file, '--dangerously-skip-permissions'], {
|
|
158
|
+
env: { ...process.env },
|
|
159
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
160
|
+
});
|
|
161
|
+
proc.stdout.on('data', (d) => safeSend(ws, { type: 'chunk', text: d.toString() }));
|
|
162
|
+
proc.stderr.on('data', (d) => {
|
|
163
|
+
const text = d.toString();
|
|
164
|
+
if (!text.includes('no stdin data received')) safeSend(ws, { type: 'error', text });
|
|
165
|
+
});
|
|
166
|
+
proc.on('close', (code) => {
|
|
167
|
+
onClose?.();
|
|
168
|
+
safeSend(ws, { type: 'done', exitCode: code });
|
|
169
|
+
console.log(`[✓] Done (exit ${code})`);
|
|
170
|
+
});
|
|
171
|
+
proc.on('error', (e) => {
|
|
172
|
+
safeSend(ws, { type: 'error', text: `Failed to start claude: ${e.message}` });
|
|
173
|
+
safeSend(ws, { type: 'done', exitCode: 1 });
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function runScript(scriptPath, ws, onClose) {
|
|
178
|
+
const proc = spawn('bash', [scriptPath], {
|
|
179
|
+
env: { ...process.env },
|
|
180
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
181
|
+
});
|
|
182
|
+
proc.stdout.on('data', (d) => safeSend(ws, { type: 'chunk', text: d.toString() }));
|
|
183
|
+
proc.stderr.on('data', (d) => safeSend(ws, { type: 'error', text: d.toString() }));
|
|
184
|
+
proc.on('close', (code) => {
|
|
185
|
+
onClose?.();
|
|
186
|
+
safeSend(ws, { type: 'done', exitCode: code });
|
|
187
|
+
console.log(`[✓] Script done (exit ${code})`);
|
|
188
|
+
});
|
|
189
|
+
proc.on('error', (e) => {
|
|
190
|
+
safeSend(ws, { type: 'error', text: `Script error: ${e.message}` });
|
|
191
|
+
safeSend(ws, { type: 'done', exitCode: 1 });
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
196
|
+
|
|
197
|
+
function safeSend(ws, obj) {
|
|
198
|
+
if (ws.readyState === ws.OPEN) ws.send(JSON.stringify(obj));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function cleanup(...files) {
|
|
202
|
+
for (const f of files) { try { unlinkSync(f); } catch {} }
|
|
203
|
+
}
|
package/lib/service.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { writeFileSync, mkdirSync, existsSync, unlinkSync } from 'fs';
|
|
2
|
+
import { execSync, spawnSync } from 'child_process';
|
|
3
|
+
import { dirname } from 'path';
|
|
4
|
+
import { OS, HOME, PORT, SERVICE_NAME, SERVER_SCRIPT, getServicePaths } from './platform.js';
|
|
5
|
+
|
|
6
|
+
// ─── Install ──────────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
export function install() {
|
|
9
|
+
if (OS === 'darwin') return installMac();
|
|
10
|
+
if (OS === 'linux') return installLinux();
|
|
11
|
+
if (OS === 'win32') return installWindows();
|
|
12
|
+
throw new Error(`Unsupported platform: ${OS}`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function installMac() {
|
|
16
|
+
const { plist } = getServicePaths();
|
|
17
|
+
const nodePath = execSync('which node').toString().trim();
|
|
18
|
+
|
|
19
|
+
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
20
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
21
|
+
<plist version="1.0">
|
|
22
|
+
<dict>
|
|
23
|
+
<key>Label</key>
|
|
24
|
+
<string>${SERVICE_NAME}</string>
|
|
25
|
+
<key>ProgramArguments</key>
|
|
26
|
+
<array>
|
|
27
|
+
<string>${nodePath}</string>
|
|
28
|
+
<string>${SERVER_SCRIPT}</string>
|
|
29
|
+
</array>
|
|
30
|
+
<key>EnvironmentVariables</key>
|
|
31
|
+
<dict>
|
|
32
|
+
<key>CLAUDELINK_PORT</key>
|
|
33
|
+
<string>${PORT}</string>
|
|
34
|
+
<key>PATH</key>
|
|
35
|
+
<string>${process.env.PATH}</string>
|
|
36
|
+
</dict>
|
|
37
|
+
<key>RunAtLoad</key>
|
|
38
|
+
<true/>
|
|
39
|
+
<key>KeepAlive</key>
|
|
40
|
+
<true/>
|
|
41
|
+
<key>StandardOutPath</key>
|
|
42
|
+
<string>${HOME}/.claudelink/bridge.log</string>
|
|
43
|
+
<key>StandardErrorPath</key>
|
|
44
|
+
<string>${HOME}/.claudelink/bridge-error.log</string>
|
|
45
|
+
</dict>
|
|
46
|
+
</plist>`;
|
|
47
|
+
|
|
48
|
+
mkdirSync(`${HOME}/.claudelink`, { recursive: true });
|
|
49
|
+
mkdirSync(dirname(plist), { recursive: true });
|
|
50
|
+
writeFileSync(plist, xml);
|
|
51
|
+
|
|
52
|
+
// Unload if already loaded, then load fresh
|
|
53
|
+
spawnSync('launchctl', ['unload', plist], { stdio: 'ignore' });
|
|
54
|
+
const result = spawnSync('launchctl', ['load', '-w', plist]);
|
|
55
|
+
if (result.status !== 0) throw new Error('launchctl load failed');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function installLinux() {
|
|
59
|
+
const { service } = getServicePaths();
|
|
60
|
+
const nodePath = execSync('which node').toString().trim();
|
|
61
|
+
|
|
62
|
+
const unit = `[Unit]
|
|
63
|
+
Description=ClaudeLink Bridge — Claude Code CLI ↔ Browser
|
|
64
|
+
After=network.target
|
|
65
|
+
|
|
66
|
+
[Service]
|
|
67
|
+
Type=simple
|
|
68
|
+
ExecStart=${nodePath} ${SERVER_SCRIPT}
|
|
69
|
+
Restart=always
|
|
70
|
+
RestartSec=3
|
|
71
|
+
Environment=CLAUDELINK_PORT=${PORT}
|
|
72
|
+
Environment=PATH=${process.env.PATH}
|
|
73
|
+
|
|
74
|
+
[Install]
|
|
75
|
+
WantedBy=default.target`;
|
|
76
|
+
|
|
77
|
+
mkdirSync(dirname(service), { recursive: true });
|
|
78
|
+
writeFileSync(service, unit);
|
|
79
|
+
|
|
80
|
+
execSync('systemctl --user daemon-reload');
|
|
81
|
+
execSync('systemctl --user enable --now claudelink');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function installWindows() {
|
|
85
|
+
const { taskName } = getServicePaths();
|
|
86
|
+
const nodePath = execSync('where node').toString().trim().split('\n')[0].trim();
|
|
87
|
+
|
|
88
|
+
// Create a scheduled task that starts at logon and runs indefinitely
|
|
89
|
+
const cmd = [
|
|
90
|
+
'schtasks', '/Create', '/F',
|
|
91
|
+
'/TN', taskName,
|
|
92
|
+
'/TR', `"${nodePath}" "${SERVER_SCRIPT}"`,
|
|
93
|
+
'/SC', 'ONLOGON',
|
|
94
|
+
'/RL', 'HIGHEST',
|
|
95
|
+
].join(' ');
|
|
96
|
+
|
|
97
|
+
execSync(cmd, { shell: true });
|
|
98
|
+
|
|
99
|
+
// Start it immediately too
|
|
100
|
+
execSync(`schtasks /Run /TN "${taskName}"`, { shell: true });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ─── Uninstall ────────────────────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
export function uninstall() {
|
|
106
|
+
if (OS === 'darwin') return uninstallMac();
|
|
107
|
+
if (OS === 'linux') return uninstallLinux();
|
|
108
|
+
if (OS === 'win32') return uninstallWindows();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function uninstallMac() {
|
|
112
|
+
const { plist } = getServicePaths();
|
|
113
|
+
if (existsSync(plist)) {
|
|
114
|
+
spawnSync('launchctl', ['unload', plist]);
|
|
115
|
+
unlinkSync(plist);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function uninstallLinux() {
|
|
120
|
+
execSync('systemctl --user disable --now claudelink', { stdio: 'ignore' });
|
|
121
|
+
const { service } = getServicePaths();
|
|
122
|
+
if (existsSync(service)) unlinkSync(service);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function uninstallWindows() {
|
|
126
|
+
const { taskName } = getServicePaths();
|
|
127
|
+
execSync(`schtasks /Delete /F /TN "${taskName}"`, { shell: true, stdio: 'ignore' });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ─── Status ───────────────────────────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
export function status() {
|
|
133
|
+
try {
|
|
134
|
+
if (OS === 'darwin') {
|
|
135
|
+
const out = execSync(`launchctl list | grep ${SERVICE_NAME}`, { stdio: 'pipe' }).toString();
|
|
136
|
+
return out.trim().length > 0 ? 'running' : 'stopped';
|
|
137
|
+
}
|
|
138
|
+
if (OS === 'linux') {
|
|
139
|
+
const out = execSync('systemctl --user is-active claudelink', { stdio: 'pipe' }).toString().trim();
|
|
140
|
+
return out === 'active' ? 'running' : 'stopped';
|
|
141
|
+
}
|
|
142
|
+
if (OS === 'win32') {
|
|
143
|
+
const { taskName } = getServicePaths();
|
|
144
|
+
const out = execSync(`schtasks /Query /TN "${taskName}"`, { stdio: 'pipe', shell: true }).toString();
|
|
145
|
+
return out.includes('Running') ? 'running' : 'stopped';
|
|
146
|
+
}
|
|
147
|
+
} catch {
|
|
148
|
+
return 'stopped';
|
|
149
|
+
}
|
|
150
|
+
return 'unknown';
|
|
151
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "claudelink-bridge",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Local bridge server connecting ClaudeLink Chrome extension to Claude Code CLI",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"claudelink-bridge": "bin/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node lib/server.js"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"ws": "^8.18.0"
|
|
14
|
+
},
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=18"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"claude",
|
|
20
|
+
"claude-code",
|
|
21
|
+
"cli",
|
|
22
|
+
"chrome-extension"
|
|
23
|
+
],
|
|
24
|
+
"author": "DevOps-Monk",
|
|
25
|
+
"license": "MIT"
|
|
26
|
+
}
|