claude-simple 0.2.28 → 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 (77) hide show
  1. package/README.md +17 -1
  2. package/bin/edge-ctl.js +151 -4
  3. package/client.js +8 -2
  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 +47 -20
  14. package/public/api.js +1 -1
  15. package/public/app.js +5 -5
  16. package/public/base.css +0 -0
  17. package/public/cache.js +19 -0
  18. package/public/chat.js +76 -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/notifications.js +0 -0
  32. package/public/offline.html +0 -0
  33. package/public/pwa.js +1 -1
  34. package/public/router.js +0 -0
  35. package/public/scroll-follow.js +78 -0
  36. package/public/settings.js +41 -0
  37. package/public/shell/boot.js +0 -0
  38. package/public/shell/dashboard.js +0 -0
  39. package/public/shell/hub.js +0 -0
  40. package/public/shell/layout.js +0 -0
  41. package/public/shell/notes.js +0 -0
  42. package/public/shell/session-context.js +0 -0
  43. package/public/shell/session-list.js +49 -16
  44. package/public/shell/session-nav.js +64 -17
  45. package/public/shell/templates.js +0 -0
  46. package/public/shell/util.js +0 -0
  47. package/public/shell.js +0 -0
  48. package/public/state.js +7 -7
  49. package/public/style.css +5 -0
  50. package/public/sw.js +10 -5
  51. package/public/terminal.js +0 -0
  52. package/public/wake-lock.js +0 -0
  53. package/public/workspace-path.js +61 -0
  54. package/public/xterm.min.css +0 -0
  55. package/public/xterm.min.js +0 -0
  56. package/src/agent-quota.js +228 -0
  57. package/src/agents/claude.js +0 -0
  58. package/src/agents/codex.js +2 -0
  59. package/src/agents/cursor.js +2 -1
  60. package/src/agents/gemini.js +2 -1
  61. package/src/agents/grok.js +6 -1
  62. package/src/agents/meta-agent.js +7 -4
  63. package/src/agents/models.js +21 -12
  64. package/src/agents/sessions.js +133 -69
  65. package/src/agents/stream-batch.js +0 -0
  66. package/src/app.js +80 -36
  67. package/src/auto-update.js +2 -0
  68. package/src/collab.js +3 -0
  69. package/src/platform.js +133 -0
  70. package/src/project-api-path.js +14 -0
  71. package/src/runtime/approvals.js +0 -0
  72. package/src/runtime/chat-broadcast.js +12 -0
  73. package/src/runtime/outbox.js +0 -0
  74. package/src/runtime/port-websocket.js +80 -0
  75. package/src/runtime/protocol.js +0 -0
  76. package/dist/app.0.2.28.min.js +0 -1220
  77. package/dist/style.0.2.28.min.css +0 -2007
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,6 +2,7 @@
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';
7
8
  import zlib from 'zlib';
@@ -14,6 +15,7 @@ const MACHINE_TOKEN = process.env.MACHINE_TOKEN; // shared secret with gateway
14
15
  const LOCAL_PORT = parseInt(process.env.LOCAL_PORT || '13000');
15
16
  const SYNC_DIR = process.env.EDGE_SYNC_DIR || path.join(os.homedir(), '.claude-simple-edge');
16
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' });
17
19
 
18
20
  if (!GATEWAY_URL) { console.error('GATEWAY_URL is required'); process.exit(1); }
19
21
  if (!MACHINE_TOKEN) { console.error('MACHINE_TOKEN is required'); process.exit(1); }
@@ -362,6 +364,7 @@ function connect() {
362
364
  method: msg.method,
363
365
  headers: reqHeaders,
364
366
  body: reqBody,
367
+ redirect: 'manual',
365
368
  });
366
369
  const headers = sanitizeProxyHeaders(Object.fromEntries(res.headers.entries()));
367
370
  const contentType = res.headers.get('content-type') || '';
@@ -442,6 +445,8 @@ function connect() {
442
445
  return;
443
446
  }
444
447
 
448
+ if (portWebSockets.handle(msg)) return;
449
+
445
450
  // ── WS tunnel open ────────────────────────────────────────────────────────
446
451
  if (msg.type === 'ws-open') {
447
452
  const { tunnelId, path, query } = msg;
@@ -499,11 +504,12 @@ function connect() {
499
504
  });
500
505
 
501
506
  controlWs.on('close', () => {
507
+ portWebSockets.closeAll();
502
508
  syncSent = new Set();
503
509
  console.log(`[client] Disconnected. Reconnecting in ${reconnectDelay}ms ...`);
504
510
  // Close all open tunnels
505
- for (const localWs of tunnels.values()) {
506
- try { localWs.close(); } catch {}
511
+ for (const tunnel of tunnels.values()) {
512
+ try { tunnel.ws.terminate(); } catch {}
507
513
  }
508
514
  tunnels.clear();
509
515
  setTimeout(connect, reconnectDelay);
File without changes