claude-simple 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.
Files changed (50) hide show
  1. package/README.md +225 -0
  2. package/client.js +139 -0
  3. package/dist/addon-fit.min.js +2 -0
  4. package/dist/app.0.1.0.min.js +825 -0
  5. package/dist/highlight.min.css +10 -0
  6. package/dist/highlight.min.js +1244 -0
  7. package/dist/index.html +16 -0
  8. package/dist/marked.min.js +6 -0
  9. package/dist/style.0.1.0.min.css +1050 -0
  10. package/dist/xterm.min.css +209 -0
  11. package/dist/xterm.min.js +2 -0
  12. package/gateway.js +349 -0
  13. package/package.json +41 -0
  14. package/public/addon-fit.min.js +2 -0
  15. package/public/agent-panel.js +1079 -0
  16. package/public/api.js +78 -0
  17. package/public/app.js +95 -0
  18. package/public/base.css +4 -0
  19. package/public/cache.js +27 -0
  20. package/public/chat.js +1087 -0
  21. package/public/files.js +476 -0
  22. package/public/git.js +130 -0
  23. package/public/index.html +61 -0
  24. package/public/lib.js +2 -0
  25. package/public/router.js +71 -0
  26. package/public/settings.js +90 -0
  27. package/public/shell/boot.js +159 -0
  28. package/public/shell/dashboard.js +269 -0
  29. package/public/shell/hub.js +192 -0
  30. package/public/shell/layout.js +195 -0
  31. package/public/shell/notes.js +323 -0
  32. package/public/shell/session-context.js +19 -0
  33. package/public/shell/session-list.js +442 -0
  34. package/public/shell/session-nav.js +299 -0
  35. package/public/shell/templates.js +342 -0
  36. package/public/shell/util.js +63 -0
  37. package/public/shell.js +589 -0
  38. package/public/state.js +362 -0
  39. package/public/style.css +1048 -0
  40. package/public/terminal.js +177 -0
  41. package/public/xterm.min.css +209 -0
  42. package/public/xterm.min.js +2 -0
  43. package/server.js +12 -0
  44. package/src/agents/claude.js +58 -0
  45. package/src/agents/codex.js +284 -0
  46. package/src/agents/grok.js +318 -0
  47. package/src/agents/meta-agent.js +1952 -0
  48. package/src/agents/models.js +339 -0
  49. package/src/agents/sessions.js +1508 -0
  50. package/src/app.js +1282 -0
package/gateway.js ADDED
@@ -0,0 +1,349 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * gateway.js — LEGACY Node relay (prefer Go binary in ./gateway/)
4
+ *
5
+ * cd gateway && make build && MACHINE_TOKEN=… ./dist/claude-gateway
6
+ *
7
+ * Machines (client.js) connect via WS to /machine-connect and register themselves.
8
+ * Browser users visit the gateway, pick a machine, and all traffic is forwarded
9
+ * transparently through the machine's control WebSocket.
10
+ *
11
+ * Control channel message protocol (gateway ↔ client):
12
+ *
13
+ * register: client → gateway { type:'register', machineId, meta }
14
+ * ping/pong: gateway ↔ client { type:'ping' } / { type:'pong' }
15
+ *
16
+ * HTTP proxy:
17
+ * gateway → client { type:'http-req', reqId, method, path, headers, body }
18
+ * client → gateway { type:'http-res', reqId, status, headers, body }
19
+ *
20
+ * WS tunnel:
21
+ * gateway → client { type:'ws-open', tunnelId, path, query }
22
+ * client → gateway { type:'ws-ready', tunnelId }
23
+ * client → gateway { type:'ws-error', tunnelId, message }
24
+ * gateway ↔ client { type:'ws-msg', tunnelId, data }
25
+ * gateway ↔ client { type:'ws-close', tunnelId }
26
+ */
27
+
28
+ import express from 'express';
29
+ import http from 'http';
30
+ import { WebSocketServer, WebSocket } from 'ws';
31
+ import crypto from 'crypto';
32
+
33
+ const PORT = parseInt(process.env.GATEWAY_PORT || '8080');
34
+ const MACHINE_TOKEN = process.env.MACHINE_TOKEN; // shared secret all clients must send
35
+ if (!MACHINE_TOKEN) { console.error('MACHINE_TOKEN is required'); process.exit(1); }
36
+
37
+ // ─── Machine registry ─────────────────────────────────────────────────────────
38
+ // machineId → { ws, meta, connectedAt, lastPong }
39
+ const machines = new Map();
40
+
41
+ // Pending HTTP responses: reqId → { resolve, reject, timer }
42
+ const pendingHttp = new Map();
43
+
44
+ // Pending WS tunnel handshakes: tunnelId → { resolve, reject, timer }
45
+ const pendingTunnels = new Map();
46
+
47
+ // Active WS tunnels: tunnelId → browser WebSocket
48
+ const activeTunnels = new Map();
49
+
50
+ function getMachine(machineId) {
51
+ const m = machines.get(machineId);
52
+ if (!m || m.ws.readyState !== WebSocket.OPEN) return null;
53
+ return m;
54
+ }
55
+
56
+ function sendToMachine(machineId, msg) {
57
+ const m = getMachine(machineId);
58
+ if (!m) throw new Error(`Machine "${machineId}" not connected`);
59
+ m.ws.send(JSON.stringify(msg));
60
+ }
61
+
62
+ // ─── HTTP forwarding ──────────────────────────────────────────────────────────
63
+ function forwardHttp(machineId, method, path, headers, body) {
64
+ return new Promise((resolve, reject) => {
65
+ const reqId = crypto.randomUUID();
66
+ const timer = setTimeout(() => {
67
+ pendingHttp.delete(reqId);
68
+ reject(new Error('Machine request timed out'));
69
+ }, 30_000);
70
+ pendingHttp.set(reqId, { resolve, reject, timer });
71
+ try {
72
+ sendToMachine(machineId, { type: 'http-req', reqId, method, path, headers, body });
73
+ } catch (e) {
74
+ clearTimeout(timer);
75
+ pendingHttp.delete(reqId);
76
+ reject(e);
77
+ }
78
+ });
79
+ }
80
+
81
+ // ─── WS tunnel handshake ──────────────────────────────────────────────────────
82
+ function openTunnel(machineId, tunnelId, path, query) {
83
+ return new Promise((resolve, reject) => {
84
+ const timer = setTimeout(() => {
85
+ pendingTunnels.delete(tunnelId);
86
+ reject(new Error('Tunnel open timed out'));
87
+ }, 10_000);
88
+ pendingTunnels.set(tunnelId, { resolve, reject, timer });
89
+ try {
90
+ sendToMachine(machineId, { type: 'ws-open', tunnelId, path, query });
91
+ } catch (e) {
92
+ clearTimeout(timer);
93
+ pendingTunnels.delete(tunnelId);
94
+ reject(e);
95
+ }
96
+ });
97
+ }
98
+
99
+ // ─── Handle messages from a machine's control WS ─────────────────────────────
100
+ function handleMachineMessage(msg) {
101
+ if (msg.type === 'pong') return;
102
+
103
+ if (msg.type === 'http-res') {
104
+ const pending = pendingHttp.get(msg.reqId);
105
+ if (!pending) return;
106
+ clearTimeout(pending.timer);
107
+ pendingHttp.delete(msg.reqId);
108
+ pending.resolve(msg);
109
+ return;
110
+ }
111
+
112
+ if (msg.type === 'ws-ready') {
113
+ const pending = pendingTunnels.get(msg.tunnelId);
114
+ if (!pending) return;
115
+ clearTimeout(pending.timer);
116
+ pendingTunnels.delete(msg.tunnelId);
117
+ pending.resolve();
118
+ return;
119
+ }
120
+
121
+ if (msg.type === 'ws-error') {
122
+ const pending = pendingTunnels.get(msg.tunnelId);
123
+ if (pending) {
124
+ clearTimeout(pending.timer);
125
+ pendingTunnels.delete(msg.tunnelId);
126
+ pending.reject(new Error(msg.message || 'Tunnel error'));
127
+ }
128
+ return;
129
+ }
130
+
131
+ if (msg.type === 'ws-msg') {
132
+ const browserWs = activeTunnels.get(msg.tunnelId);
133
+ if (browserWs?.readyState === WebSocket.OPEN) {
134
+ browserWs.send(msg.data);
135
+ }
136
+ return;
137
+ }
138
+
139
+ if (msg.type === 'ws-close') {
140
+ const browserWs = activeTunnels.get(msg.tunnelId);
141
+ activeTunnels.delete(msg.tunnelId);
142
+ if (browserWs?.readyState === WebSocket.OPEN) browserWs.close();
143
+ return;
144
+ }
145
+ }
146
+
147
+ // ─── Express app ──────────────────────────────────────────────────────────────
148
+ const app = express();
149
+ // Large enough for base64 file previews (office/pdf ≤15MB → ~20MB JSON)
150
+ app.use(express.json({ limit: '25mb' }));
151
+
152
+ // Machine list API (used by the frontend picker)
153
+ app.get('/api/machines', (req, res) => {
154
+ const list = [...machines.entries()].map(([id, m]) => ({
155
+ id,
156
+ ...m.meta,
157
+ connectedAt: m.connectedAt,
158
+ online: m.ws.readyState === WebSocket.OPEN,
159
+ }));
160
+ res.json(list);
161
+ });
162
+
163
+ // Machine picker page — minimal HTML, loads the real UI from the selected machine
164
+ app.get('/', (req, res) => {
165
+ if (req.query.machine) {
166
+ // Redirect into the machine's proxied app
167
+ return res.redirect(`/machine/${req.query.machine}/`);
168
+ }
169
+ res.send(`<!DOCTYPE html>
170
+ <html lang="en">
171
+ <head>
172
+ <meta charset="utf-8">
173
+ <title>Select machine</title>
174
+ <style>
175
+ body { font-family: system-ui, sans-serif; max-width: 480px; margin: 80px auto; padding: 0 16px; }
176
+ h1 { font-size: 1.2rem; margin-bottom: 24px; }
177
+ .machine { display:flex; justify-content:space-between; align-items:center;
178
+ border:1px solid #ddd; border-radius:8px; padding:12px 16px; margin-bottom:12px; }
179
+ .machine a { text-decoration:none; font-weight:600; color:#0070f3; }
180
+ .dot { width:8px; height:8px; border-radius:50%; background:#22c55e; }
181
+ .dot.offline { background:#ef4444; }
182
+ #empty { color:#888; }
183
+ </style>
184
+ </head>
185
+ <body>
186
+ <h1>Choose a machine</h1>
187
+ <div id="list"><p id="empty">Loading...</p></div>
188
+ <script>
189
+ async function load() {
190
+ const res = await fetch('/api/machines');
191
+ const machines = await res.json();
192
+ const el = document.getElementById('list');
193
+ if (!machines.length) { el.innerHTML = '<p id="empty">No machines connected.</p>'; return; }
194
+ el.innerHTML = machines.map(m => \`
195
+ <div class="machine">
196
+ <a href="/machine/\${m.id}/">\${m.id}</a>
197
+ <span>\${m.hostname || ''} · \${m.platform || ''}</span>
198
+ <span class="dot \${m.online ? '' : 'offline'}"></span>
199
+ </div>
200
+ \`).join('');
201
+ }
202
+ load();
203
+ setInterval(load, 5000);
204
+ </script>
205
+ </body>
206
+ </html>`);
207
+ });
208
+
209
+ // ─── HTTP proxy: /machine/:id/* ───────────────────────────────────────────────
210
+ app.all('/machine/:machineId/*', async (req, res) => {
211
+ const { machineId } = req.params;
212
+ if (!getMachine(machineId)) {
213
+ return res.status(503).json({ error: `Machine "${machineId}" not connected` });
214
+ }
215
+
216
+ // Strip /machine/:id prefix so the machine sees its own paths
217
+ const path = req.path.replace(`/machine/${machineId}`, '') || '/';
218
+ const qs = req.url.includes('?') ? req.url.slice(req.url.indexOf('?')) : '';
219
+
220
+ // Pass body as string for JSON; raw otherwise
221
+ const body = req.method !== 'GET' && req.method !== 'HEAD'
222
+ ? JSON.stringify(req.body)
223
+ : undefined;
224
+
225
+ try {
226
+ const result = await forwardHttp(machineId, req.method, path + qs, {
227
+ ...req.headers,
228
+ host: 'localhost',
229
+ }, body);
230
+
231
+ // Don't forward hop-by-hop headers
232
+ const skip = new Set(['transfer-encoding', 'connection', 'keep-alive']);
233
+ Object.entries(result.headers || {}).forEach(([k, v]) => {
234
+ if (!skip.has(k.toLowerCase())) res.setHeader(k, v);
235
+ });
236
+ res.status(result.status).send(result.body);
237
+ } catch (e) {
238
+ res.status(502).json({ error: e.message });
239
+ }
240
+ });
241
+
242
+ // ─── HTTP + WebSocket server ──────────────────────────────────────────────────
243
+ const server = http.createServer(app);
244
+
245
+ // Two WS servers — one for machines registering, one for browser tunnels
246
+ const wssMachines = new WebSocketServer({ noServer: true });
247
+ const wssBrowser = new WebSocketServer({ noServer: true });
248
+
249
+ server.on('upgrade', (req, socket, head) => {
250
+ const url = new URL(req.url, 'http://localhost');
251
+ const pathname = url.pathname;
252
+
253
+ if (pathname === '/machine-connect') {
254
+ wssMachines.handleUpgrade(req, socket, head, ws => wssMachines.emit('connection', ws, req));
255
+ return;
256
+ }
257
+
258
+ // /machine/:id/ws/chat or /machine/:id/ws/shell
259
+ const m = pathname.match(/^\/machine\/([^/]+)(\/ws\/.+)$/);
260
+ if (m) {
261
+ wssBrowser.handleUpgrade(req, socket, head, ws => wssBrowser.emit('connection', ws, req, m[1], m[2]));
262
+ return;
263
+ }
264
+
265
+ socket.destroy();
266
+ });
267
+
268
+ // ─── Machine control connections ──────────────────────────────────────────────
269
+ wssMachines.on('connection', (ws, req) => {
270
+ // Verify shared token
271
+ const token = req.headers['x-machine-token'];
272
+ if (token !== MACHINE_TOKEN) {
273
+ ws.close(1008, 'Unauthorized');
274
+ return;
275
+ }
276
+
277
+ let machineId = null;
278
+
279
+ // Heartbeat
280
+ const pingInterval = setInterval(() => {
281
+ if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: 'ping' }));
282
+ }, 30_000);
283
+
284
+ ws.on('message', raw => {
285
+ let msg;
286
+ try { msg = JSON.parse(raw.toString()); } catch { return; }
287
+
288
+ if (msg.type === 'register') {
289
+ machineId = msg.machineId;
290
+ machines.set(machineId, { ws, meta: msg.meta || {}, connectedAt: Date.now() });
291
+ console.log(`[gateway] Machine registered: ${machineId}`);
292
+ return;
293
+ }
294
+
295
+ handleMachineMessage(msg);
296
+ });
297
+
298
+ ws.on('close', () => {
299
+ clearInterval(pingInterval);
300
+ if (machineId) {
301
+ machines.delete(machineId);
302
+ console.log(`[gateway] Machine disconnected: ${machineId}`);
303
+ }
304
+ });
305
+
306
+ ws.on('error', e => console.error('[gateway] machine ws error:', e.message));
307
+ });
308
+
309
+ // ─── Browser WS tunnel connections ───────────────────────────────────────────
310
+ wssBrowser.on('connection', async (browserWs, req, machineId, wsPath) => {
311
+ const url = new URL(req.url, 'http://localhost');
312
+ const query = url.search; // e.g. ?token=xxx
313
+ const tunnelId = crypto.randomUUID();
314
+
315
+ if (!getMachine(machineId)) {
316
+ browserWs.close(1011, 'Machine not connected');
317
+ return;
318
+ }
319
+
320
+ try {
321
+ await openTunnel(machineId, tunnelId, wsPath, query);
322
+ } catch (e) {
323
+ console.error(`[gateway] Tunnel open failed: ${e.message}`);
324
+ browserWs.close(1011, 'Tunnel failed');
325
+ return;
326
+ }
327
+
328
+ activeTunnels.set(tunnelId, browserWs);
329
+
330
+ browserWs.on('message', raw => {
331
+ const m = getMachine(machineId);
332
+ if (!m) return;
333
+ m.ws.send(JSON.stringify({ type: 'ws-msg', tunnelId, data: raw.toString() }));
334
+ });
335
+
336
+ browserWs.on('close', () => {
337
+ activeTunnels.delete(tunnelId);
338
+ const m = getMachine(machineId);
339
+ if (m) m.ws.send(JSON.stringify({ type: 'ws-close', tunnelId }));
340
+ });
341
+
342
+ browserWs.on('error', e => console.error('[gateway] browser ws error:', e.message));
343
+ });
344
+
345
+ // ─── Start ────────────────────────────────────────────────────────────────────
346
+ server.listen(PORT, () => {
347
+ console.log(`\n Gateway running at http://localhost:${PORT}`);
348
+ console.log(` Waiting for machines to connect at ws://localhost:${PORT}/machine-connect\n`);
349
+ });
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "claude-simple",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "bin": {
6
+ "claude-simple": "server.js",
7
+ "claude-simple-client": "client.js"
8
+ },
9
+ "files": [
10
+ "server.js",
11
+ "client.js",
12
+ "gateway.js",
13
+ "src",
14
+ "dist",
15
+ "public"
16
+ ],
17
+ "scripts": {
18
+ "build": "node build.mjs",
19
+ "prepublishOnly": "node build.mjs",
20
+ "start": "NODE_ENV=production node server.js",
21
+ "dev": "NODE_ENV=development node --watch server.js",
22
+ "serve": "node server.js",
23
+ "gateway": "node gateway.js",
24
+ "gateway:go": "cd gateway && go run .",
25
+ "gateway:build": "cd gateway && make build",
26
+ "client": "node client.js"
27
+ },
28
+ "dependencies": {
29
+ "@anthropic-ai/claude-agent-sdk": "*",
30
+ "bcrypt": "^5.1.1",
31
+ "express": "^4.18.2",
32
+ "jsonwebtoken": "^9.0.2",
33
+ "ws": "^8.16.0"
34
+ },
35
+ "devDependencies": {
36
+ "@tailwindcss/cli": "^4.2.4",
37
+ "daisyui": "^5.5.19",
38
+ "esbuild": "^0.28.0",
39
+ "tailwindcss": "^4.2.4"
40
+ }
41
+ }
@@ -0,0 +1,2 @@
1
+ !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.FitAddon=t():e.FitAddon=t()}(self,(()=>(()=>{"use strict";var e={};return(()=>{var t=e;Object.defineProperty(t,"__esModule",{value:!0}),t.FitAddon=void 0,t.FitAddon=class{activate(e){this._terminal=e}dispose(){}fit(){const e=this.proposeDimensions();if(!e||!this._terminal||isNaN(e.cols)||isNaN(e.rows))return;const t=this._terminal._core;this._terminal.rows===e.rows&&this._terminal.cols===e.cols||(t._renderService.clear(),this._terminal.resize(e.cols,e.rows))}proposeDimensions(){if(!this._terminal)return;if(!this._terminal.element||!this._terminal.element.parentElement)return;const e=this._terminal._core,t=e._renderService.dimensions;if(0===t.css.cell.width||0===t.css.cell.height)return;const r=0===this._terminal.options.scrollback?0:e.viewport.scrollBarWidth,i=window.getComputedStyle(this._terminal.element.parentElement),o=parseInt(i.getPropertyValue("height")),s=Math.max(0,parseInt(i.getPropertyValue("width"))),n=window.getComputedStyle(this._terminal.element),l=o-(parseInt(n.getPropertyValue("padding-top"))+parseInt(n.getPropertyValue("padding-bottom"))),a=s-(parseInt(n.getPropertyValue("padding-right"))+parseInt(n.getPropertyValue("padding-left")))-r;return{cols:Math.max(2,Math.floor(a/t.css.cell.width)),rows:Math.max(1,Math.floor(l/t.css.cell.height))}}}})(),e})()));
2
+ //# sourceMappingURL=xterm-addon-fit.js.map