claude-simple 0.2.27 → 0.2.30

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 (79) hide show
  1. package/README.md +17 -1
  2. package/bin/edge-ctl.js +151 -4
  3. package/client.js +29 -4
  4. package/dist/addon-fit.min.js +0 -0
  5. package/dist/app.0.2.30.min.js +1308 -0
  6. package/dist/index.html +2 -2
  7. package/dist/style.0.2.30.min.css +2012 -0
  8. package/dist/xterm.min.css +0 -0
  9. package/dist/xterm.min.js +0 -0
  10. package/gateway.js +0 -0
  11. package/package.json +2 -2
  12. package/public/addon-fit.min.js +0 -0
  13. package/public/agent-panel.js +54 -22
  14. package/public/api.js +8 -5
  15. package/public/app.js +7 -5
  16. package/public/base.css +0 -0
  17. package/public/cache.js +19 -0
  18. package/public/chat.js +110 -19
  19. package/public/files.js +26 -10
  20. package/public/git.js +2 -2
  21. package/public/icons/icon-180.png +0 -0
  22. package/public/icons/icon-192.png +0 -0
  23. package/public/icons/icon-512.png +0 -0
  24. package/public/icons/icon-maskable-512.png +0 -0
  25. package/public/index.html +2 -2
  26. package/public/lib.js +0 -0
  27. package/public/manifest.webmanifest +0 -0
  28. package/public/markdown.js +16 -0
  29. package/public/math.js +145 -7
  30. package/public/mobile.js +0 -0
  31. package/public/notes.js +316 -0
  32. package/public/notifications.js +0 -0
  33. package/public/offline.html +0 -0
  34. package/public/pwa.js +1 -1
  35. package/public/router.js +4 -0
  36. package/public/scroll-follow.js +78 -0
  37. package/public/settings.js +41 -0
  38. package/public/shell/boot.js +0 -0
  39. package/public/shell/dashboard.js +0 -0
  40. package/public/shell/hub.js +0 -0
  41. package/public/shell/layout.js +0 -0
  42. package/public/shell/notes.js +0 -0
  43. package/public/shell/session-context.js +0 -0
  44. package/public/shell/session-list.js +79 -21
  45. package/public/shell/session-nav.js +70 -21
  46. package/public/shell/templates.js +3 -0
  47. package/public/shell/util.js +0 -0
  48. package/public/shell.js +15 -2
  49. package/public/state.js +7 -7
  50. package/public/style.css +161 -0
  51. package/public/sw.js +10 -5
  52. package/public/terminal.js +0 -0
  53. package/public/wake-lock.js +0 -0
  54. package/public/workspace-path.js +61 -0
  55. package/public/xterm.min.css +0 -0
  56. package/public/xterm.min.js +0 -0
  57. package/src/agent-quota.js +228 -0
  58. package/src/agents/claude.js +0 -0
  59. package/src/agents/codex.js +2 -0
  60. package/src/agents/cursor.js +2 -1
  61. package/src/agents/gemini.js +2 -1
  62. package/src/agents/grok.js +22 -1
  63. package/src/agents/meta-agent.js +7 -4
  64. package/src/agents/models.js +21 -12
  65. package/src/agents/sessions.js +270 -81
  66. package/src/agents/stream-batch.js +0 -0
  67. package/src/app.js +111 -51
  68. package/src/auto-update.js +2 -0
  69. package/src/collab.js +3 -0
  70. package/src/platform.js +133 -0
  71. package/src/project-api-path.js +14 -0
  72. package/src/runtime/approvals.js +0 -0
  73. package/src/runtime/chat-broadcast.js +12 -0
  74. package/src/runtime/outbox.js +0 -0
  75. package/src/runtime/port-websocket.js +80 -0
  76. package/src/runtime/protocol.js +0 -0
  77. package/src/session-history.js +31 -0
  78. package/dist/app.0.2.27.min.js +0 -1182
  79. package/dist/style.0.2.27.min.css +0 -1851
package/README.md CHANGED
@@ -109,6 +109,22 @@ Edge-only env (when connecting to a hub):
109
109
 
110
110
  `claude-edge migrate-user-agent` 可将旧的 macOS system Edge 迁移到用户会话。对只使用无用户凭据的后台 agent,system service 仍可选;默认建议使用用户 LaunchAgent。
111
111
 
112
+ ### Windows Edge
113
+
114
+ Windows 没有 launchd/systemd。在业务机安装 Node 后:
115
+
116
+ ```bat
117
+ set GATEWAY_URL=wss://your-hub/machine-connect
118
+ set MACHINE_TOKEN=long-shared-secret
119
+ set MACHINE_ID=win-box
120
+ claude-edge start
121
+ claude-edge status
122
+ ```
123
+
124
+ 进程、日志和 env 落在 `%USERPROFILE%\.claude-simple-edge\`。Shell 标签使用 PowerShell/cmd(不走 unix pty)。Session / Files 路径按 Windows 盘符与大小写不敏感比较。
125
+
126
+ 计划任务必须跑在**当前登录用户**(`LogonType=Interactive` / 用户登录时),不要用 SYSTEM / Session 0,否则 Grok/Codex 会读不到用户的 `auth.json` / Credential Manager,网页上会反复要求登录。
127
+
112
128
  ## Features
113
129
 
114
130
  ### Chat
@@ -260,6 +276,6 @@ The Memory tab lists files for the selected session's agent and loads their cont
260
276
  | `npm start` / `npm run serve` | Production standalone |
261
277
  | `npm run build` | Bundle frontend into `dist/` |
262
278
  | `npm run client` | Edge worker (requires hub) |
263
- | `claude-edge status\|start\|stop\|restart\|logs` | Supervised edge (launchd / systemd). Also `claude-simple-edge`, `edge-daemon` |
279
+ | `claude-edge status\|start\|stop\|restart\|logs` | Supervised edge (launchd / systemd / Windows user process). Also `claude-simple-edge`, `edge-daemon` |
264
280
  | `npm run gateway` | Legacy Node hub |
265
281
  | `npm run gateway:build` | Build Go hub via `gateway/Makefile` |
package/bin/edge-ctl.js CHANGED
@@ -4,7 +4,7 @@
4
4
  * claude-edge status|start|stop|restart|logs
5
5
  * claude-edge daemon <same>
6
6
  *
7
- * Detects launchd (macOS com.claude-simple.edge) or systemd (claude-simple-edge).
7
+ * Detects launchd (macOS), systemd (Linux), or a user-session process on Windows.
8
8
  */
9
9
  import { spawn, spawnSync } from 'child_process';
10
10
  import fs from 'fs';
@@ -22,6 +22,17 @@ const AGENT_PLIST = path.join(os.homedir(), 'Library', 'LaunchAgents', `${AGENT_
22
22
  const SYSTEMD_UNIT = 'claude-simple-edge.service';
23
23
  const MAC_OUT = path.join(os.homedir(), '.claude-simple-edge', 'edge.out.log');
24
24
  const MAC_ERR = path.join(os.homedir(), '.claude-simple-edge', 'edge.err.log');
25
+ const WIN_DIR = path.join(os.homedir(), '.claude-simple-edge');
26
+ const WIN_PID = path.join(WIN_DIR, 'edge.pid');
27
+ const WIN_OUT = path.join(WIN_DIR, 'edge.out.log');
28
+ const WIN_ERR = path.join(WIN_DIR, 'edge.err.log');
29
+ const WIN_ENV = path.join(WIN_DIR, 'edge.env.json');
30
+ const WIN_TASK = 'ClaudeSimpleEdge';
31
+ const WIN_ENV_KEYS = [
32
+ 'GATEWAY_URL', 'MACHINE_TOKEN', 'MACHINE_ID', 'LOCAL_PORT', 'AGENTS',
33
+ 'CLAUDE_CONFIG_DIRS', 'CODEX_HOME', 'GROK_HOME', 'GEMINI_HOME', 'CURSOR_HOME',
34
+ 'AUTO_UPDATE',
35
+ ];
25
36
 
26
37
  function usage(code = 0) {
27
38
  const text = `Usage: claude-edge <command>
@@ -35,6 +46,9 @@ function usage(code = 0) {
35
46
  logs err Stderr only (launchd)
36
47
  migrate-user-agent Move macOS Edge into the login session (required for Keychain-backed AGY auth)
37
48
 
49
+ Windows uses a user-session process (pid + logs under %USERPROFILE%\\.claude-simple-edge).
50
+ start persists GATEWAY_URL / MACHINE_TOKEN from the current environment.
51
+
38
52
  Also installed as: claude-simple-edge, edge-daemon
39
53
  `;
40
54
  if (code) process.stderr.write(text);
@@ -70,6 +84,7 @@ function detectBackend() {
70
84
  }
71
85
  if (process.platform === 'darwin') return 'launchd-user';
72
86
  if (process.platform === 'linux') return 'systemd';
87
+ if (process.platform === 'win32') return 'win32';
73
88
  return null;
74
89
  }
75
90
 
@@ -177,6 +192,13 @@ function parseLaunchdPrint(text) {
177
192
 
178
193
  function listenPort() {
179
194
  const port = process.env.LOCAL_PORT || '13000';
195
+ if (process.platform === 'win32') {
196
+ const r = run('netstat', ['-ano']);
197
+ if (r.status === 0 && r.stdout && new RegExp(`:${port}\\s+.*LISTENING`, 'i').test(r.stdout)) {
198
+ return `127.0.0.1:${port}`;
199
+ }
200
+ return 'down';
201
+ }
180
202
  const r = run('lsof', ['-nP', `-iTCP:${port}`, '-sTCP:LISTEN']);
181
203
  if (r.status === 0 && r.stdout.trim()) return `127.0.0.1:${port}`;
182
204
  const ss = run('ss', ['-lnt']);
@@ -186,11 +208,128 @@ function listenPort() {
186
208
 
187
209
  function tailFile(file, n) {
188
210
  if (!fs.existsSync(file)) return '';
189
- const r = run('tail', ['-n', String(n), file]);
190
- return (r.stdout || '').trimEnd();
211
+ try {
212
+ const text = fs.readFileSync(file, 'utf8');
213
+ const lines = text.split(/\r?\n/);
214
+ return lines.slice(Math.max(0, lines.length - n)).join('\n').trimEnd();
215
+ } catch {
216
+ return '';
217
+ }
218
+ }
219
+
220
+ function pidAlive(pid) {
221
+ if (!pid) return false;
222
+ try {
223
+ process.kill(pid, 0);
224
+ return true;
225
+ } catch {
226
+ return false;
227
+ }
228
+ }
229
+
230
+ function readWinPid() {
231
+ try {
232
+ const n = parseInt(fs.readFileSync(WIN_PID, 'utf8').trim(), 10);
233
+ return Number.isFinite(n) ? n : 0;
234
+ } catch {
235
+ return 0;
236
+ }
237
+ }
238
+
239
+ function loadWinEnv() {
240
+ try {
241
+ const parsed = JSON.parse(fs.readFileSync(WIN_ENV, 'utf8'));
242
+ return parsed && typeof parsed === 'object' ? parsed : {};
243
+ } catch {
244
+ return {};
245
+ }
246
+ }
247
+
248
+ function persistWinEnv() {
249
+ fs.mkdirSync(WIN_DIR, { recursive: true });
250
+ const prev = loadWinEnv();
251
+ const next = { ...prev };
252
+ for (const key of WIN_ENV_KEYS) {
253
+ if (process.env[key]) next[key] = process.env[key];
254
+ }
255
+ fs.writeFileSync(WIN_ENV, JSON.stringify(next, null, 2));
256
+ return next;
257
+ }
258
+
259
+ function clientScript() {
260
+ return path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'client.js');
261
+ }
262
+
263
+ function tryWinStartupTask(nodePath, client) {
264
+ const tr = `"${nodePath}" "${client}"`;
265
+ run('schtasks', ['/Create', '/TN', WIN_TASK, '/SC', 'ONLOGON', '/RL', 'LIMITED', '/F', '/TR', tr]);
266
+ }
267
+
268
+ function cmdWinStatus() {
269
+ const pid = readWinPid();
270
+ const alive = pidAlive(pid);
271
+ const env = loadWinEnv();
272
+ console.log('service: windows-user');
273
+ console.log(`machine: ${env.MACHINE_ID || process.env.MACHINE_ID || '-'}`);
274
+ console.log(`state: ${alive ? 'running' : 'stopped'}`);
275
+ console.log(`pid: ${alive ? pid : '-'}`);
276
+ console.log(`listen: ${listenPort()}`);
277
+ console.log(`dir: ${WIN_DIR}`);
278
+ console.log(`stdout: ${WIN_OUT}`);
279
+ console.log(`stderr: ${WIN_ERR}`);
280
+ const last = tailFile(WIN_OUT, 8);
281
+ if (last) console.log(`\n--- last logs ---\n${last}`);
282
+ }
283
+
284
+ function cmdWinStart() {
285
+ const existing = readWinPid();
286
+ if (pidAlive(existing)) {
287
+ console.log(`already running (pid ${existing})`);
288
+ return;
289
+ }
290
+ const envFile = persistWinEnv();
291
+ if (!envFile.GATEWAY_URL || !envFile.MACHINE_TOKEN) {
292
+ fail('set GATEWAY_URL and MACHINE_TOKEN in this shell (they are saved under .claude-simple-edge\\edge.env.json)');
293
+ }
294
+ fs.mkdirSync(WIN_DIR, { recursive: true });
295
+ const out = fs.openSync(WIN_OUT, 'a');
296
+ const err = fs.openSync(WIN_ERR, 'a');
297
+ const child = spawn(process.execPath, [clientScript()], {
298
+ cwd: WIN_DIR,
299
+ env: { ...process.env, ...envFile },
300
+ detached: true,
301
+ stdio: ['ignore', out, err],
302
+ windowsHide: true,
303
+ });
304
+ fs.writeFileSync(WIN_PID, String(child.pid));
305
+ child.unref();
306
+ tryWinStartupTask(process.execPath, clientScript());
307
+ console.log(`started pid ${child.pid}`);
308
+ }
309
+
310
+ function cmdWinStop() {
311
+ const pid = readWinPid();
312
+ if (pidAlive(pid)) {
313
+ const killed = run('taskkill', ['/PID', String(pid), '/T', '/F']);
314
+ if (killed.status !== 0) {
315
+ try { process.kill(pid, 'SIGTERM'); } catch {}
316
+ }
317
+ }
318
+ try { fs.unlinkSync(WIN_PID); } catch {}
319
+ run('schtasks', ['/End', '/TN', WIN_TASK]);
320
+ console.log('stopped');
321
+ }
322
+
323
+ function cmdWinLogs(mode) {
324
+ if (mode === '-f' || mode === '--follow') {
325
+ fail('log follow is not supported on Windows; use `claude-edge logs` and re-run');
326
+ }
327
+ const file = (mode === 'err' || mode === 'error' || mode === 'stderr') ? WIN_ERR : WIN_OUT;
328
+ process.stdout.write(tailFile(file, 80) + '\n');
191
329
  }
192
330
 
193
331
  function cmdStatus(backend) {
332
+ if (backend === 'win32') return cmdWinStatus();
194
333
  if (backend === 'launchd') ensureRoot();
195
334
  if (backend === 'launchd' || backend === 'launchd-user') {
196
335
  const target = macTarget(backend);
@@ -241,6 +380,7 @@ function cmdStatus(backend) {
241
380
  }
242
381
 
243
382
  function cmdStart(backend) {
383
+ if (backend === 'win32') return cmdWinStart();
244
384
  if (backend === 'launchd') ensureRoot();
245
385
  if (backend === 'launchd' || backend === 'launchd-user') {
246
386
  const target = macTarget(backend);
@@ -261,6 +401,7 @@ function cmdStart(backend) {
261
401
  }
262
402
 
263
403
  function cmdStop(backend) {
404
+ if (backend === 'win32') return cmdWinStop();
264
405
  if (backend === 'launchd') ensureRoot();
265
406
  if (backend === 'launchd' || backend === 'launchd-user') {
266
407
  const target = macTarget(backend);
@@ -275,6 +416,11 @@ function cmdStop(backend) {
275
416
  }
276
417
 
277
418
  function cmdRestart(backend) {
419
+ if (backend === 'win32') {
420
+ cmdWinStop();
421
+ cmdWinStart();
422
+ return;
423
+ }
278
424
  if (backend === 'launchd') ensureRoot();
279
425
  if (backend === 'launchd' || backend === 'launchd-user') {
280
426
  const target = macTarget(backend);
@@ -295,6 +441,7 @@ function cmdRestart(backend) {
295
441
  }
296
442
 
297
443
  function cmdLogs(backend, mode) {
444
+ if (backend === 'win32') return cmdWinLogs(mode);
298
445
  if (backend === 'systemd') ensureRoot();
299
446
  if (backend === 'launchd' || backend === 'launchd-user') {
300
447
  if (mode === '-f' || mode === '--follow') {
@@ -328,7 +475,7 @@ const rest = raw.slice(1);
328
475
  if (['-h', '--help', 'help'].includes(cmd)) usage(0);
329
476
 
330
477
  const backend = detectBackend();
331
- if (!backend) fail('no launchd/systemd edge service found');
478
+ if (!backend) fail('no launchd/systemd/windows edge service found');
332
479
 
333
480
  switch (cmd) {
334
481
  case 'migrate-user-agent':
package/client.js CHANGED
@@ -2,8 +2,10 @@
2
2
  import { createApp, init, hasBusyAgent } from './src/app.js';
3
3
  import { startAutoUpdate } from './src/auto-update.js';
4
4
  import { WebSocket } from 'ws';
5
+ import { createPortWebSocketRelay } from './src/runtime/port-websocket.js';
5
6
  import os from 'os';
6
7
  import crypto from 'crypto';
8
+ import zlib from 'zlib';
7
9
  import { promises as fs, watch as watchFs } from 'fs';
8
10
  import path from 'path';
9
11
 
@@ -13,6 +15,7 @@ const MACHINE_TOKEN = process.env.MACHINE_TOKEN; // shared secret with gateway
13
15
  const LOCAL_PORT = parseInt(process.env.LOCAL_PORT || '13000');
14
16
  const SYNC_DIR = process.env.EDGE_SYNC_DIR || path.join(os.homedir(), '.claude-simple-edge');
15
17
  const SYNC_FILE = path.join(SYNC_DIR, 'hub-stream-outbox.json');
18
+ const portWebSockets = createPortWebSocketRelay(sendControl, { allowRemote: process.env.PORT_PROXY_ALLOW_REMOTE === '1' });
16
19
 
17
20
  if (!GATEWAY_URL) { console.error('GATEWAY_URL is required'); process.exit(1); }
18
21
  if (!MACHINE_TOKEN) { console.error('MACHINE_TOKEN is required'); process.exit(1); }
@@ -361,6 +364,7 @@ function connect() {
361
364
  method: msg.method,
362
365
  headers: reqHeaders,
363
366
  body: reqBody,
367
+ redirect: 'manual',
364
368
  });
365
369
  const headers = sanitizeProxyHeaders(Object.fromEntries(res.headers.entries()));
366
370
  const contentType = res.headers.get('content-type') || '';
@@ -408,8 +412,26 @@ function connect() {
408
412
  }
409
413
 
410
414
  const body = await res.text();
411
- // Body is always decoded text here — length must match UTF-8 bytes.
412
- headers['content-length'] = String(Buffer.byteLength(body));
415
+ const raw = Buffer.from(body, 'utf8');
416
+ // Large JSON (session history) is gzip+base64 on the control channel so
417
+ // Hub→phone does not wait on an uncompressed multi-MB WS frame.
418
+ if (raw.length >= 4096) {
419
+ try {
420
+ const zipped = zlib.gzipSync(raw, { level: 4 });
421
+ if (zipped.length < raw.length * 0.95) {
422
+ sendControl({
423
+ type: 'http-res',
424
+ reqId: msg.reqId,
425
+ status: res.status,
426
+ headers,
427
+ body: zipped.toString('base64'),
428
+ encoding: 'gzip',
429
+ });
430
+ return;
431
+ }
432
+ } catch { /* fall through to plain text */ }
433
+ }
434
+ headers['content-length'] = String(raw.length);
413
435
  sendControl({
414
436
  type: 'http-res',
415
437
  reqId: msg.reqId,
@@ -423,6 +445,8 @@ function connect() {
423
445
  return;
424
446
  }
425
447
 
448
+ if (portWebSockets.handle(msg)) return;
449
+
426
450
  // ── WS tunnel open ────────────────────────────────────────────────────────
427
451
  if (msg.type === 'ws-open') {
428
452
  const { tunnelId, path, query } = msg;
@@ -480,11 +504,12 @@ function connect() {
480
504
  });
481
505
 
482
506
  controlWs.on('close', () => {
507
+ portWebSockets.closeAll();
483
508
  syncSent = new Set();
484
509
  console.log(`[client] Disconnected. Reconnecting in ${reconnectDelay}ms ...`);
485
510
  // Close all open tunnels
486
- for (const localWs of tunnels.values()) {
487
- try { localWs.close(); } catch {}
511
+ for (const tunnel of tunnels.values()) {
512
+ try { tunnel.ws.terminate(); } catch {}
488
513
  }
489
514
  tunnels.clear();
490
515
  setTimeout(connect, reconnectDelay);
File without changes