@caoruhua/open-claude-remote 0.1.6 → 0.2.3

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 (132) hide show
  1. package/README.md +514 -11
  2. package/dist/backend/src/api/config-routes.d.ts +2 -2
  3. package/dist/backend/src/api/config-routes.d.ts.map +1 -1
  4. package/dist/backend/src/api/config-routes.js +21 -6
  5. package/dist/backend/src/api/config-routes.js.map +1 -1
  6. package/dist/backend/src/api/health-routes.d.ts +4 -1
  7. package/dist/backend/src/api/health-routes.d.ts.map +1 -1
  8. package/dist/backend/src/api/health-routes.js +20 -2
  9. package/dist/backend/src/api/health-routes.js.map +1 -1
  10. package/dist/backend/src/api/hook-routes.d.ts +2 -2
  11. package/dist/backend/src/api/hook-routes.d.ts.map +1 -1
  12. package/dist/backend/src/api/hook-routes.js +10 -3
  13. package/dist/backend/src/api/hook-routes.js.map +1 -1
  14. package/dist/backend/src/api/instance-routes.d.ts +5 -3
  15. package/dist/backend/src/api/instance-routes.d.ts.map +1 -1
  16. package/dist/backend/src/api/instance-routes.js +48 -45
  17. package/dist/backend/src/api/instance-routes.js.map +1 -1
  18. package/dist/backend/src/api/router.d.ts +4 -12
  19. package/dist/backend/src/api/router.d.ts.map +1 -1
  20. package/dist/backend/src/api/router.js +28 -20
  21. package/dist/backend/src/api/router.js.map +1 -1
  22. package/dist/backend/src/api/status-routes.d.ts +2 -2
  23. package/dist/backend/src/api/status-routes.d.ts.map +1 -1
  24. package/dist/backend/src/api/status-routes.js +11 -6
  25. package/dist/backend/src/api/status-routes.js.map +1 -1
  26. package/dist/backend/src/attach.d.ts +1 -1
  27. package/dist/backend/src/attach.d.ts.map +1 -1
  28. package/dist/backend/src/attach.js +25 -71
  29. package/dist/backend/src/attach.js.map +1 -1
  30. package/dist/backend/src/cli-utils.d.ts +24 -1
  31. package/dist/backend/src/cli-utils.d.ts.map +1 -1
  32. package/dist/backend/src/cli-utils.js +73 -37
  33. package/dist/backend/src/cli-utils.js.map +1 -1
  34. package/dist/backend/src/cli.d.ts +22 -2
  35. package/dist/backend/src/cli.d.ts.map +1 -1
  36. package/dist/backend/src/cli.js +328 -23
  37. package/dist/backend/src/cli.js.map +1 -1
  38. package/dist/backend/src/config.d.ts +48 -15
  39. package/dist/backend/src/config.d.ts.map +1 -1
  40. package/dist/backend/src/config.js +126 -27
  41. package/dist/backend/src/config.js.map +1 -1
  42. package/dist/backend/src/daemon/daemon-client.d.ts +69 -0
  43. package/dist/backend/src/daemon/daemon-client.d.ts.map +1 -0
  44. package/dist/backend/src/daemon/daemon-client.js +209 -0
  45. package/dist/backend/src/daemon/daemon-client.js.map +1 -0
  46. package/dist/backend/src/daemon/daemon-entry.d.ts +8 -0
  47. package/dist/backend/src/daemon/daemon-entry.d.ts.map +1 -0
  48. package/dist/backend/src/daemon/daemon-entry.js +37 -0
  49. package/dist/backend/src/daemon/daemon-entry.js.map +1 -0
  50. package/dist/backend/src/daemon/daemon-launcher.d.ts +13 -0
  51. package/dist/backend/src/daemon/daemon-launcher.d.ts.map +1 -0
  52. package/dist/backend/src/daemon/daemon-launcher.js +62 -0
  53. package/dist/backend/src/daemon/daemon-launcher.js.map +1 -0
  54. package/dist/backend/src/index.d.ts.map +1 -1
  55. package/dist/backend/src/index.js +115 -251
  56. package/dist/backend/src/index.js.map +1 -1
  57. package/dist/backend/src/instance/index.d.ts +4 -0
  58. package/dist/backend/src/instance/index.d.ts.map +1 -0
  59. package/dist/backend/src/instance/index.js +3 -0
  60. package/dist/backend/src/instance/index.js.map +1 -0
  61. package/dist/backend/src/instance/instance-manager.d.ts +62 -0
  62. package/dist/backend/src/instance/instance-manager.d.ts.map +1 -0
  63. package/dist/backend/src/instance/instance-manager.js +194 -0
  64. package/dist/backend/src/instance/instance-manager.js.map +1 -0
  65. package/dist/backend/src/instance/instance-session.d.ts +87 -0
  66. package/dist/backend/src/instance/instance-session.d.ts.map +1 -0
  67. package/dist/backend/src/instance/instance-session.js +359 -0
  68. package/dist/backend/src/instance/instance-session.js.map +1 -0
  69. package/dist/backend/src/instance/types.d.ts +39 -0
  70. package/dist/backend/src/instance/types.d.ts.map +1 -0
  71. package/dist/backend/src/instance/types.js +2 -0
  72. package/dist/backend/src/instance/types.js.map +1 -0
  73. package/dist/backend/src/notification/notification-manager.js +1 -1
  74. package/dist/backend/src/notification/notification-manager.js.map +1 -1
  75. package/dist/backend/src/notification/notification-service-factory.js +1 -1
  76. package/dist/backend/src/notification/notification-service-factory.js.map +1 -1
  77. package/dist/backend/src/pty/virtual-pty.d.ts.map +1 -1
  78. package/dist/backend/src/pty/virtual-pty.js +6 -0
  79. package/dist/backend/src/pty/virtual-pty.js.map +1 -1
  80. package/dist/backend/src/registry/shared-token.d.ts +2 -2
  81. package/dist/backend/src/registry/shared-token.js +11 -11
  82. package/dist/backend/src/registry/shared-token.js.map +1 -1
  83. package/dist/backend/src/registry/stop-instances.d.ts +0 -25
  84. package/dist/backend/src/registry/stop-instances.d.ts.map +1 -1
  85. package/dist/backend/src/registry/stop-instances.js +6 -206
  86. package/dist/backend/src/registry/stop-instances.js.map +1 -1
  87. package/dist/backend/src/skills/index.d.ts +4 -0
  88. package/dist/backend/src/skills/index.d.ts.map +1 -0
  89. package/dist/backend/src/skills/index.js +4 -0
  90. package/dist/backend/src/skills/index.js.map +1 -0
  91. package/dist/backend/src/skills/skill-command-merger.d.ts +32 -0
  92. package/dist/backend/src/skills/skill-command-merger.d.ts.map +1 -0
  93. package/dist/backend/src/skills/skill-command-merger.js +78 -0
  94. package/dist/backend/src/skills/skill-command-merger.js.map +1 -0
  95. package/dist/backend/src/skills/skill-commands.d.ts +25 -0
  96. package/dist/backend/src/skills/skill-commands.d.ts.map +1 -0
  97. package/dist/backend/src/skills/skill-commands.js +56 -0
  98. package/dist/backend/src/skills/skill-commands.js.map +1 -0
  99. package/dist/backend/src/skills/skill-scanner.d.ts +36 -0
  100. package/dist/backend/src/skills/skill-scanner.d.ts.map +1 -0
  101. package/dist/backend/src/skills/skill-scanner.js +143 -0
  102. package/dist/backend/src/skills/skill-scanner.js.map +1 -0
  103. package/dist/backend/src/terminal/terminal-relay.d.ts +1 -0
  104. package/dist/backend/src/terminal/terminal-relay.d.ts.map +1 -1
  105. package/dist/backend/src/terminal/terminal-relay.js +6 -0
  106. package/dist/backend/src/terminal/terminal-relay.js.map +1 -1
  107. package/dist/backend/src/update.d.ts.map +1 -1
  108. package/dist/backend/src/update.js +46 -1
  109. package/dist/backend/src/update.js.map +1 -1
  110. package/dist/backend/src/utils/banner.d.ts +15 -0
  111. package/dist/backend/src/utils/banner.d.ts.map +1 -0
  112. package/dist/backend/src/utils/banner.js +95 -0
  113. package/dist/backend/src/utils/banner.js.map +1 -0
  114. package/dist/backend/src/ws/ws-server.d.ts +13 -54
  115. package/dist/backend/src/ws/ws-server.d.ts.map +1 -1
  116. package/dist/backend/src/ws/ws-server.js +57 -155
  117. package/dist/backend/src/ws/ws-server.js.map +1 -1
  118. package/dist/shared/constants.d.ts +2 -1
  119. package/dist/shared/constants.d.ts.map +1 -1
  120. package/dist/shared/constants.js +2 -1
  121. package/dist/shared/constants.js.map +1 -1
  122. package/dist/shared/defaults.d.ts.map +1 -1
  123. package/dist/shared/defaults.js +1 -5
  124. package/dist/shared/defaults.js.map +1 -1
  125. package/dist/shared/instance.d.ts +4 -9
  126. package/dist/shared/instance.d.ts.map +1 -1
  127. package/dist/shared/instance.js +0 -2
  128. package/dist/shared/instance.js.map +1 -1
  129. package/frontend-dist/assets/index-CM2xfmS8.js +152 -0
  130. package/frontend-dist/index.html +1 -1
  131. package/package.json +2 -2
  132. package/frontend-dist/assets/index-Cfhr3h3e.js +0 -152
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ /**
3
+ * Daemon 子进程入口点
4
+ *
5
+ * 由 daemon-launcher.ts 通过 fork() 启动。
6
+ * 以 daemonMode 运行 startServer(不创建 firstSession,不接管 stdin/stdout)。
7
+ * 启动成功后通过 IPC 通知父进程,然后断开 IPC。
8
+ */
9
+ // Daemon 不需要 CLI 模式的 logger 配置
10
+ delete process.env.CLI_MODE;
11
+ // 标记为无终端模式
12
+ process.env.NO_TERMINAL = 'true';
13
+ void (async () => {
14
+ try {
15
+ // 从环境变量读取 CLI overrides
16
+ const overridesJson = process.env.DAEMON_OVERRIDES;
17
+ const overrides = overridesJson ? JSON.parse(overridesJson) : {};
18
+ // 强制 daemon 模式
19
+ overrides.daemonMode = true;
20
+ const { startServer } = await import('../index.js');
21
+ await startServer(overrides);
22
+ // 服务启动成功,通知父进程
23
+ if (process.send) {
24
+ process.send({ type: 'ready', pid: process.pid });
25
+ // 断开 IPC 通道,让父进程可以独立退出
26
+ process.disconnect();
27
+ }
28
+ }
29
+ catch (err) {
30
+ const message = err instanceof Error ? err.message : String(err);
31
+ if (process.send) {
32
+ process.send({ type: 'error', message });
33
+ }
34
+ process.exit(1);
35
+ }
36
+ })();
37
+ //# sourceMappingURL=daemon-entry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"daemon-entry.js","sourceRoot":"","sources":["../../../../backend/src/daemon/daemon-entry.ts"],"names":[],"mappings":";AAAA;;;;;;GAMG;AAEH,+BAA+B;AAC/B,OAAO,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC;AAC5B,WAAW;AACX,OAAO,CAAC,GAAG,CAAC,WAAW,GAAG,MAAM,CAAC;AAEjC,KAAK,CAAC,KAAK,IAAI,EAAE;IACf,IAAI,CAAC;QACH,wBAAwB;QACxB,MAAM,aAAa,GAAG,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC;QACnD,MAAM,SAAS,GAAG,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAEjE,eAAe;QACf,SAAS,CAAC,UAAU,GAAG,IAAI,CAAC;QAE5B,MAAM,EAAE,WAAW,EAAE,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC,CAAC;QACpD,MAAM,WAAW,CAAC,SAAS,CAAC,CAAC;QAE7B,eAAe;QACf,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC;YACjB,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;YAClD,uBAAuB;YACvB,OAAO,CAAC,UAAU,EAAE,CAAC;QACvB,CAAC;IACH,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACjE,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC;YACjB,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC;QAC3C,CAAC;QACD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;AACH,CAAC,CAAC,EAAE,CAAC"}
@@ -0,0 +1,13 @@
1
+ import type { CliOverrides } from '../config.js';
2
+ export interface DaemonLaunchResult {
3
+ pid: number;
4
+ }
5
+ /**
6
+ * Fork 一个 daemon 子进程并等待其就绪
7
+ *
8
+ * @param overrides CLI 覆盖参数(传给 daemon 的 startServer)
9
+ * @returns daemon 进程的 PID
10
+ * @throws 启动超时或 daemon 报错
11
+ */
12
+ export declare function launchDaemon(overrides?: Omit<CliOverrides, 'daemonMode'>): Promise<DaemonLaunchResult>;
13
+ //# sourceMappingURL=daemon-launcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"daemon-launcher.d.ts","sourceRoot":"","sources":["../../../../backend/src/daemon/daemon-launcher.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAOjD,MAAM,WAAW,kBAAkB;IACjC,GAAG,EAAE,MAAM,CAAC;CACb;AAED;;;;;;GAMG;AACH,wBAAsB,YAAY,CAAC,SAAS,GAAE,IAAI,CAAC,YAAY,EAAE,YAAY,CAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAgDhH"}
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Daemon 启动器
3
+ *
4
+ * 使用 fork() 启动 daemon-entry.ts 作为独立子进程。
5
+ * 等待 IPC "ready" 消息确认启动成功,然后 unref 子进程让 CLI 可以独立退出。
6
+ */
7
+ import { fork } from 'node:child_process';
8
+ import { resolve, dirname } from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
10
+ const __dirname = dirname(fileURLToPath(import.meta.url));
11
+ /** daemon 启动超时(毫秒) */
12
+ const DAEMON_READY_TIMEOUT_MS = 15_000;
13
+ /**
14
+ * Fork 一个 daemon 子进程并等待其就绪
15
+ *
16
+ * @param overrides CLI 覆盖参数(传给 daemon 的 startServer)
17
+ * @returns daemon 进程的 PID
18
+ * @throws 启动超时或 daemon 报错
19
+ */
20
+ export async function launchDaemon(overrides = {}) {
21
+ const entryPath = resolve(__dirname, 'daemon-entry.js');
22
+ // 判断是否运行在 tsx/ts-node 下(开发模式),使用 .ts 后缀
23
+ const isDevMode = process.execArgv.some(arg => arg.includes('tsx') || arg.includes('ts-node') || arg.includes('loader'));
24
+ const actualEntry = isDevMode
25
+ ? resolve(__dirname, 'daemon-entry.ts')
26
+ : entryPath;
27
+ const child = fork(actualEntry, [], {
28
+ detached: true,
29
+ stdio: ['ignore', 'ignore', 'ignore', 'ipc'],
30
+ env: {
31
+ ...process.env,
32
+ DAEMON_OVERRIDES: JSON.stringify(overrides),
33
+ CLI_MODE: undefined, // daemon 不是 CLI 模式
34
+ },
35
+ });
36
+ return new Promise((promiseResolve, reject) => {
37
+ const timeout = setTimeout(() => {
38
+ child.kill();
39
+ reject(new Error(`Daemon failed to start within ${DAEMON_READY_TIMEOUT_MS / 1000}s`));
40
+ }, DAEMON_READY_TIMEOUT_MS);
41
+ child.on('message', (msg) => {
42
+ clearTimeout(timeout);
43
+ if (msg.type === 'ready') {
44
+ // Daemon 启动成功,解除引用让 CLI 可以退出
45
+ child.unref();
46
+ promiseResolve({ pid: msg.pid });
47
+ }
48
+ else if (msg.type === 'error') {
49
+ reject(new Error(`Daemon startup failed: ${msg.message}`));
50
+ }
51
+ });
52
+ child.on('error', (err) => {
53
+ clearTimeout(timeout);
54
+ reject(new Error(`Failed to fork daemon: ${err.message}`));
55
+ });
56
+ child.on('exit', (code) => {
57
+ clearTimeout(timeout);
58
+ reject(new Error(`Daemon exited unexpectedly with code ${code}`));
59
+ });
60
+ });
61
+ }
62
+ //# sourceMappingURL=daemon-launcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"daemon-launcher.js","sourceRoot":"","sources":["../../../../backend/src/daemon/daemon-launcher.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,IAAI,EAAE,MAAM,oBAAoB,CAAC;AAC1C,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAGzC,MAAM,SAAS,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAE1D,sBAAsB;AACtB,MAAM,uBAAuB,GAAG,MAAM,CAAC;AAMvC;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,YAA8C,EAAE;IACjF,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,EAAE,iBAAiB,CAAC,CAAC;IAExD,wCAAwC;IACxC,MAAM,SAAS,GAAG,OAAO,CAAC,QAAQ,CAAC,IAAI,CACrC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,GAAG,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAChF,CAAC;IACF,MAAM,WAAW,GAAG,SAAS;QAC3B,CAAC,CAAC,OAAO,CAAC,SAAS,EAAE,iBAAiB,CAAC;QACvC,CAAC,CAAC,SAAS,CAAC;IAEd,MAAM,KAAK,GAAG,IAAI,CAAC,WAAW,EAAE,EAAE,EAAE;QAClC,QAAQ,EAAE,IAAI;QACd,KAAK,EAAE,CAAC,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,CAAC;QAC5C,GAAG,EAAE;YACH,GAAG,OAAO,CAAC,GAAG;YACd,gBAAgB,EAAE,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC;YAC3C,QAAQ,EAAE,SAAS,EAAE,mBAAmB;SACzC;KACF,CAAC,CAAC;IAEH,OAAO,IAAI,OAAO,CAAqB,CAAC,cAAc,EAAE,MAAM,EAAE,EAAE;QAChE,MAAM,OAAO,GAAG,UAAU,CAAC,GAAG,EAAE;YAC9B,KAAK,CAAC,IAAI,EAAE,CAAC;YACb,MAAM,CAAC,IAAI,KAAK,CAAC,iCAAiC,uBAAuB,GAAG,IAAI,GAAG,CAAC,CAAC,CAAC;QACxF,CAAC,EAAE,uBAAuB,CAAC,CAAC;QAE5B,KAAK,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,GAAQ,EAAE,EAAE;YAC/B,YAAY,CAAC,OAAO,CAAC,CAAC;YACtB,IAAI,GAAG,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;gBACzB,6BAA6B;gBAC7B,KAAK,CAAC,KAAK,EAAE,CAAC;gBACd,cAAc,CAAC,EAAE,GAAG,EAAE,GAAG,CAAC,GAAG,EAAE,CAAC,CAAC;YACnC,CAAC;iBAAM,IAAI,GAAG,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;gBAChC,MAAM,CAAC,IAAI,KAAK,CAAC,0BAA0B,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;YAC7D,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE;YACxB,YAAY,CAAC,OAAO,CAAC,CAAC;YACtB,MAAM,CAAC,IAAI,KAAK,CAAC,0BAA0B,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;QAC7D,CAAC,CAAC,CAAC;QAEH,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE;YACxB,YAAY,CAAC,OAAO,CAAC,CAAC;YACtB,MAAM,CAAC,IAAI,KAAK,CAAC,wCAAwC,IAAI,EAAE,CAAC,CAAC,CAAC;QACpE,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../backend/src/index.ts"],"names":[],"mappings":"AASA,OAAO,EAAmI,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AAoBjL,wBAAsB,WAAW,CAAC,YAAY,GAAE,YAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CA+YhF"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../backend/src/index.ts"],"names":[],"mappings":"AAOA,OAAO,EAAuC,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AAgBrF,wBAAsB,WAAW,CAAC,YAAY,GAAE,YAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAmQhF"}
@@ -1,70 +1,47 @@
1
1
  import { createServer } from 'node:http';
2
2
  import { resolve, dirname } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
- import { existsSync, readdirSync, unlinkSync } from 'node:fs';
5
- import { homedir } from 'node:os';
6
- import { randomUUID } from 'node:crypto';
4
+ import { existsSync } from 'node:fs';
7
5
  import express from 'express';
8
6
  import cors from 'cors';
9
- import { CLAUDE_REMOTE_DIR, SETTINGS_DIR } from '../../shared/index.js';
10
- import { loadConfig, createSessionCookieName, createClaudeSettings, extractSettingsFromArgs, saveClaudeSettings, ensureDefaultUserConfig } from './config.js';
7
+ import { loadConfig, ensureDefaultUserConfig } from './config.js';
8
+ import { PortInUseError } from './cli-utils.js';
11
9
  import { AuthModule } from './auth/auth-middleware.js';
12
- import { PtyManager } from './pty/pty-manager.js';
13
10
  import { WsServer } from './ws/ws-server.js';
14
- import { HookReceiver } from './hooks/hook-receiver.js';
15
- import { SessionController } from './session/session-controller.js';
11
+ import { InstanceManager } from './instance/instance-manager.js';
16
12
  import { TerminalRelay } from './terminal/terminal-relay.js';
17
13
  import { createApiRouter } from './api/router.js';
14
+ import { setDaemonStartTime } from './api/health-routes.js';
18
15
  import { PushService } from './push/push-service.js';
19
16
  import { createNotificationManager } from './notification/notification-manager.js';
20
17
  import { createNotificationServiceFactory } from './notification/notification-service-factory.js';
21
18
  import { logger, setInstanceContext } from './logger/logger.js';
22
19
  import { getOrCreateSharedToken } from './registry/shared-token.js';
23
- import { findAvailablePort } from './registry/port-finder.js';
24
- import { InstanceRegistryManager } from './registry/instance-registry.js';
25
- import { InstanceSpawner } from './registry/instance-spawner.js';
26
20
  import { IpMonitor } from './utils/ip-monitor.js';
27
- import { generateQRCodeLines } from './utils/qrcode-banner.js';
28
- import { getCurrentVersion } from './update.js';
21
+ import { printBanner } from './utils/banner.js';
29
22
  export async function startServer(cliOverrides = {}) {
30
- // 1. Load configuration
23
+ // 1. Load configuration (global + workdir merged)
31
24
  const config = loadConfig(cliOverrides);
32
- const projectRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
33
25
  // 1.5. Ensure default shortcuts/commands in user config
34
26
  await ensureDefaultUserConfig();
35
27
  // 2. Shared config directory and token
36
- const sharedConfigDir = resolve(homedir(), CLAUDE_REMOTE_DIR);
37
- const { token, source: tokenSource } = getOrCreateSharedToken(sharedConfigDir, config.token ?? undefined);
38
- // 3. Instance ID and registry
39
- const instanceId = randomUUID();
40
- const registry = new InstanceRegistryManager(sharedConfigDir);
41
- // 4. Find available port (auto-increment if preferred port is occupied)
42
- const actualPort = await findAvailablePort(config.port, config.host);
43
- // Update config to reflect the actual port (cookie name must match actual port
44
- // to prevent cross-instance cookie collision when port auto-increments)
45
- if (actualPort !== config.port) {
46
- config.sessionCookieName = createSessionCookieName(actualPort);
47
- config.port = actualPort;
48
- logger.info({
49
- actualPort,
50
- sessionCookieName: config.sessionCookieName,
51
- }, 'Config updated for actual port');
52
- }
53
- // 设置日志实例上下文,后续所有日志自动包含 instancePort 字段
54
- setInstanceContext(actualPort);
28
+ const homeConfigDir = resolve(process.env.HOME ?? process.env.USERPROFILE ?? '', '.claude-remote');
29
+ const { token, source: tokenSource } = getOrCreateSharedToken(homeConfigDir, config.token ?? undefined);
30
+ // 3. Fixed port (8866)
31
+ const port = config.port; // DEFAULT_PORT
32
+ // 设置日志实例上下文
33
+ setInstanceContext(port);
55
34
  // 5. Create Express app
56
35
  const app = express();
57
36
  app.use(express.json());
58
37
  app.use(cors({
59
38
  origin: (origin, callback) => {
60
- // Allow same-origin requests (origin is undefined) and requests from LAN
61
39
  if (!origin) {
62
40
  callback(null, true);
63
41
  return;
64
42
  }
65
43
  try {
66
44
  const url = new URL(origin);
67
- // 允许同一 host 的任意端口(多实例跨端口访问)
68
45
  const allowedHosts = [config.displayIp, 'localhost', '127.0.0.1'];
69
46
  if (allowedHosts.includes(url.hostname)) {
70
47
  callback(null, true);
@@ -80,49 +57,45 @@ export async function startServer(cliOverrides = {}) {
80
57
  }));
81
58
  // 6. Create HTTP server
82
59
  const httpServer = createServer(app);
83
- // 7. Setup auth module
60
+ // 7. Setup auth module (fixed cookie name: session_id)
84
61
  const authModule = new AuthModule({
85
62
  token,
86
63
  sessionTtlMs: config.sessionTtlMs,
87
64
  rateLimitPerMinute: config.authRateLimit,
88
65
  cookieName: config.sessionCookieName,
89
66
  });
90
- // 8. Setup Hook receiver
91
- const hookReceiver = new HookReceiver();
92
- // 9. Setup Push service
93
- const pushService = new PushService(sharedConfigDir);
94
- // 9.1. Create NotificationManager for dynamic enabled status checking
67
+ // 8. Setup Push service
68
+ const pushService = new PushService(homeConfigDir);
69
+ // 8.1. Create NotificationManager for dynamic enabled status checking
95
70
  const notificationManager = createNotificationManager();
96
- // 9.2. Create NotificationServiceFactory for lazy-loading notification services
71
+ // 8.2. Create NotificationServiceFactory for lazy-loading notification services
97
72
  const notificationServiceFactory = createNotificationServiceFactory();
98
- // 10. Session controller reference (set after PTY spawn)
99
- let sessionController = null;
100
- // 10.5. Create Instance Spawner (for creating new instances via API)
101
- const instanceSpawner = new InstanceSpawner();
102
- // 11. Create WebSocket server (needed for API routes)
73
+ // 9. InstanceManager (replaces SessionController)
74
+ const instanceManager = new InstanceManager();
75
+ instanceManager.setSharedServices({
76
+ pushService,
77
+ notificationManager,
78
+ notificationServiceFactory,
79
+ displayIp: config.displayIp,
80
+ });
81
+ // 10. Create WebSocket server (with InstanceManager routing)
103
82
  const wsServer = new WsServer(httpServer, authModule);
104
- // 12. Mount REST API (with instance routes)
83
+ wsServer.setInstanceManager(instanceManager);
84
+ // 11. Mount REST API
105
85
  app.use('/api', createApiRouter({
106
86
  authModule,
107
- hookReceiver,
108
- getController: () => sessionController,
87
+ instanceManager,
109
88
  pushService,
110
- listInstances: () => registry.list(),
111
- currentInstanceId: instanceId,
112
- instanceSpawner,
113
89
  notificationManager,
114
90
  notificationServiceFactory,
115
- wsServer,
91
+ onShutdown: () => shutdown(0),
116
92
  }));
117
- // 13. Serve frontend static files (if built)
93
+ // 12. Serve frontend static files (if built)
118
94
  const __dirname = dirname(fileURLToPath(import.meta.url));
119
- // 开发模式 (tsx backend/src/index.ts): __dirname = backend/src/ → ../../frontend-dist
120
- // 生产构建 (node dist/backend/src/index.js): __dirname = dist/backend/src/ → ../../../frontend-dist
121
95
  const isDistBuild = __dirname.includes('/dist/');
122
96
  const frontendDist = resolve(__dirname, isDistBuild ? '../../../frontend-dist' : '../../frontend-dist');
123
97
  if (existsSync(frontendDist)) {
124
98
  app.use(express.static(frontendDist));
125
- // SPA fallback — skip /api and /ws paths to avoid hijacking backend routes
126
99
  app.get('*', (req, res, next) => {
127
100
  if (req.path.startsWith('/api') || req.path.startsWith('/ws')) {
128
101
  return next();
@@ -134,139 +107,86 @@ export async function startServer(cliOverrides = {}) {
134
107
  else {
135
108
  logger.warn('Frontend dist not found, skipping static file serving');
136
109
  }
137
- // 14. Spawn PTY with Claude Code CLI
138
- const ptyManager = new PtyManager();
139
- // 15. Terminal Relay (条件启动,headless 模式跳过)
140
- const noTerminal = process.env.NO_TERMINAL === 'true';
141
- const relay = (!noTerminal && process.stdin.isTTY) ? new TerminalRelay(ptyManager) : undefined;
142
- // 16. Create Session Controller (with relay for dynamic master switch)
143
- sessionController = new SessionController(ptyManager, wsServer, hookReceiver, config.maxBufferLines, relay);
144
- sessionController.setPushService(pushService);
145
- sessionController.setNotificationManager(notificationManager);
146
- sessionController.setNotificationServiceFactory(notificationServiceFactory);
147
- // 设置实例 URL(初始化)
148
- const instanceUrl = `http://${config.displayIp}:${actualPort}`;
149
- sessionController.setInstanceUrl(instanceUrl);
150
- // 17. Spawn Claude Code with instance-specific hook settings
151
- // 检查用户是否传了 --settings 参数,如果有则合并 hooks
152
- const extracted = extractSettingsFromArgs(config.claudeArgs);
153
- let finalArgs;
154
- // 生成最终的 settings 对象
155
- const finalSettings = createClaudeSettings(actualPort, extracted?.settingsValue);
156
- // 保存到文件并通过文件路径传递给 Claude
157
- const settingsPath = saveClaudeSettings(finalSettings, actualPort, sharedConfigDir);
158
- if (extracted) {
159
- finalArgs = [...extracted.otherArgs, '--settings', settingsPath];
160
- logger.info({ port: actualPort, originalSettingsPath: extracted.settingsPath, savedSettingsPath: settingsPath }, 'Merged user settings with hooks');
161
- }
162
- else {
163
- finalArgs = [...config.claudeArgs, '--settings', settingsPath];
164
- logger.info({ port: actualPort, savedSettingsPath: settingsPath }, 'Generated Claude settings with instance-specific hook URL');
165
- }
166
- ptyManager.on('error', (err) => {
167
- logger.error({ err }, 'PTY process error');
168
- process.stderr.write(`\n[ERROR] Failed to start Claude CLI: ${err.message}\n`);
169
- registry.unregister(instanceId);
170
- process.exit(1);
171
- });
172
- ptyManager.spawn({
173
- command: config.claudeCommand,
174
- args: finalArgs,
175
- cwd: config.claudeCwd,
176
- });
177
- // Start relay after spawn (if not headless)
178
- if (relay) {
179
- relay.start();
180
- }
181
- sessionController.setStatus('running');
182
- // 18. Register instance in shared registry
183
- registry.register({
184
- instanceId,
185
- name: config.instanceName,
186
- host: config.displayIp,
187
- port: actualPort,
188
- pid: process.pid,
189
- cwd: config.claudeCwd,
190
- startedAt: new Date().toISOString(),
191
- headless: noTerminal,
192
- claudeArgs: config.claudeArgs.length > 0 ? config.claudeArgs : undefined,
193
- });
194
- // 18.5. Clean up stale settings files for dead instances
195
- try {
196
- const settingsDir = resolve(sharedConfigDir, SETTINGS_DIR);
197
- if (existsSync(settingsDir)) {
198
- const alivePorts = new Set((await registry.list()).map(i => i.port));
199
- for (const file of readdirSync(settingsDir)) {
200
- const match = file.match(/^(\d+)\.json$/);
201
- if (match) {
202
- const port = parseInt(match[1], 10);
203
- if (!alivePorts.has(port)) {
204
- try {
205
- unlinkSync(resolve(settingsDir, file));
206
- logger.info({ file, port }, 'Cleaned up stale settings file');
207
- }
208
- catch {
209
- // 静默处理:清理失败不影响启动
210
- }
211
- }
212
- }
213
- }
110
+ // 13. Create first instance (skip in daemon mode — instances created via API)
111
+ const daemonMode = cliOverrides.daemonMode === true;
112
+ let firstSession;
113
+ let relay;
114
+ let ptyDataHandler;
115
+ if (!daemonMode) {
116
+ const noTerminal = process.env.NO_TERMINAL === 'true';
117
+ firstSession = instanceManager.createInstance({
118
+ cwd: config.claudeCwd,
119
+ name: config.instanceName,
120
+ claudeCommand: config.claudeCommand,
121
+ claudeArgs: config.claudeArgs,
122
+ headless: noTerminal,
123
+ });
124
+ // 14. Terminal Relay (条件启动,headless 模式跳过)
125
+ // PTY output stdout: InstanceSession 只广播给 WS 客户端,CLI 需要额外管道
126
+ const isTTY = !noTerminal && process.stdin.isTTY;
127
+ if (isTTY) {
128
+ ptyDataHandler = (data) => {
129
+ process.stdout.write(data);
130
+ };
131
+ firstSession.ptyManager.on('data', ptyDataHandler);
132
+ }
133
+ relay = isTTY ? new TerminalRelay(firstSession.ptyManager) : undefined;
134
+ if (relay) {
135
+ relay.start();
214
136
  }
215
137
  }
216
- catch (err) {
217
- logger.debug({ err }, 'Settings cleanup skipped');
218
- }
219
- // 18.6. Start IP monitor
138
+ // 15. IP monitor
220
139
  const ipMonitor = new IpMonitor((newIp, oldIp) => {
221
- const newUrl = `http://${newIp}:${actualPort}`;
222
- logger.info({ oldIp, newIp, newUrl }, 'IP change detected');
223
- // Update config
140
+ logger.info({ oldIp, newIp }, 'IP change detected');
224
141
  config.displayIp = newIp;
225
- // Update registry
226
- registry.updateHost(instanceId, newIp);
227
- // Update instance URL in session controller
228
- sessionController?.setInstanceUrl(newUrl);
229
- // Broadcast to all connected clients
230
- wsServer.broadcast({
231
- type: 'ip_changed',
232
- oldIp,
233
- newIp,
234
- newUrl,
235
- });
236
- }, 30000, 2); // 30s interval, 2 consecutive detections required
142
+ instanceManager.updateDisplayIp(newIp);
143
+ }, 30000, 2);
237
144
  ipMonitor.start(config.displayIp);
238
- // 19. Unified graceful shutdown
145
+ // 16. Unified graceful shutdown
239
146
  let shuttingDown = false;
240
147
  const shutdown = (exitCode = 0) => {
241
148
  if (shuttingDown)
242
149
  return;
243
150
  shuttingDown = true;
244
151
  logger.info({ exitCode }, 'Shutting down...');
245
- // Unregister from shared registry
246
- registry.unregister(instanceId);
247
152
  if (relay) {
248
153
  relay.stop();
249
154
  }
250
155
  ipMonitor.stop();
251
- // Pause stdin to remove it as an active event loop handle
252
156
  if (!process.stdin.isTTY) {
253
157
  process.stdin.pause();
254
158
  }
255
- ptyManager.destroy();
159
+ instanceManager.destroyAll();
256
160
  wsServer.destroy();
257
161
  authModule.destroy();
258
162
  httpServer.close(() => {
259
163
  process.exit(exitCode);
260
164
  });
261
- // Force exit if httpServer.close hangs (ref'd to guarantee it fires)
262
165
  setTimeout(() => process.exit(exitCode), 2000);
263
166
  };
264
- // Handle PTY exit graceful shutdown
265
- ptyManager.on('exit', (exitCode) => {
266
- logger.info({ exitCode }, 'Claude Code exited');
267
- // Give time for WS messages to flush, then shutdown
268
- setTimeout(() => shutdown(exitCode), 500);
269
- });
167
+ // Handle first instance PTY exit (only in non-daemon mode)
168
+ if (firstSession) {
169
+ const session = firstSession;
170
+ session.on('exit', (exitCode) => {
171
+ logger.info({ exitCode, instanceId: session.instanceId }, 'First instance PTY exited');
172
+ // 打印提示后 CLI 退出,daemon 后台运行
173
+ // 注意:先打印提示再停止 relay,确保输出可见
174
+ process.stderr.write(`\nInstance "${session.name}" exited with code ${exitCode}.\n`);
175
+ process.stderr.write('Daemon is still running. Use "claude-remote stop" to shut down.\n\n');
176
+ // 清理 PTY 输出监听器(避免事件循环句柄残留)
177
+ if (ptyDataHandler) {
178
+ session.ptyManager.removeListener('data', ptyDataHandler);
179
+ }
180
+ // 停止 terminal relay(恢复终端状态)
181
+ if (relay) {
182
+ relay.stop();
183
+ }
184
+ // CLI 退出,daemon 继续后台运行
185
+ // 注意:不调用 shutdown(),只移除 CLI 相关的事件监听并退出进程
186
+ process.exitCode = exitCode;
187
+ process.exit(exitCode);
188
+ });
189
+ }
270
190
  process.on('SIGINT', () => {
271
191
  logger.info('SIGINT received');
272
192
  shutdown(0);
@@ -275,23 +195,16 @@ export async function startServer(cliOverrides = {}) {
275
195
  logger.info('SIGTERM received');
276
196
  shutdown(0);
277
197
  });
278
- // When running via pnpm dev (stdin is a pipe, not TTY), detect parent process exit
279
- // via stdin close event — this fires when concurrently/pnpm terminates
280
- // Also handle Ctrl+C which may come through stdin in pipe mode
281
- if (!process.stdin.isTTY) {
198
+ // When running via pnpm dev (stdin is a pipe, not TTY) skip in daemon mode
199
+ if (!daemonMode && !process.stdin.isTTY) {
282
200
  process.stdin.resume();
283
- // Kitty keyboard protocol CSI u variants for Ctrl+C:
284
- // Match press/repeat (event type 1 or 2) but not release (3)
285
201
  const CTRL_C = '\x03';
286
202
  const KITTY_CTRL_C_RE = /\x1b\[99;5(?::(?:[12]))?(?:;\d+)*u/;
287
- // Use 'readable' event for immediate reads without waiting for newline
288
- // In pipe mode, 'data' event is line-buffered and waits for Enter
289
203
  process.stdin.on('readable', () => {
290
204
  let chunk;
291
205
  while ((chunk = process.stdin.read()) !== null) {
292
206
  const str = chunk.toString();
293
207
  logger.info({ hex: chunk.toString('hex'), len: chunk.length, str: str.replace(/\x1b/g, 'ESC') }, 'stdin data received in non-TTY mode');
294
- // Single Ctrl+C (classic ETX or Kitty protocol) → shutdown immediately
295
208
  if (str === CTRL_C || KITTY_CTRL_C_RE.test(str)) {
296
209
  logger.info('Ctrl+C detected in pipe mode, initiating shutdown');
297
210
  shutdown(0);
@@ -303,86 +216,37 @@ export async function startServer(cliOverrides = {}) {
303
216
  shutdown(0);
304
217
  });
305
218
  }
306
- // 20. Handle port collision (TOCTOU between findAvailablePort and listen)
307
- httpServer.on('error', (err) => {
308
- if (err.code === 'EADDRINUSE') {
309
- logger.error({ port: actualPort, host: config.host }, 'Port is already in use (TOCTOU race). Another process claimed it between port check and listen.');
310
- process.exit(1);
311
- }
312
- logger.error({ err }, 'HTTP server error');
313
- process.exit(1);
314
- });
315
- // 21. Start listening
316
- httpServer.listen(actualPort, config.host, () => {
317
- const url = `http://${config.displayIp}:${actualPort}`;
318
- const qrUrl = `${url}?token=${token}`;
319
- const qrLines = generateQRCodeLines(qrUrl);
320
- // Version (with fallback)
321
- let version = '';
322
- try {
323
- version = getCurrentVersion();
324
- }
325
- catch { /* ignore */ }
326
- // Left column: instance info + commands
327
- const leftLines = [];
328
- leftLines.push(`Instance: ${config.instanceName}`);
329
- leftLines.push(`URL: ${url}`);
330
- leftLines.push(`PID: ${process.pid}`);
331
- leftLines.push(`Logs: ${config.logDir}`);
332
- leftLines.push('');
333
- leftLines.push('Commands:');
334
- leftLines.push(` attach: claude-remote attach ${actualPort}`);
335
- leftLines.push(' update: claude-remote update');
336
- // Right column: QR lines + blank + centered label
337
- const qrLabel = 'Scan QR to connect';
338
- const rightLines = [...qrLines, ''];
339
- const targetHeight = Math.max(leftLines.length, rightLines.length + 1);
340
- while (rightLines.length < targetHeight - 1) {
341
- rightLines.push('');
342
- }
343
- rightLines.push(qrLabel);
344
- // Width calculations
345
- const qrWidth = Math.max(qrLines[0]?.length || 0, qrLabel.length);
346
- const leftWidth = Math.max(...leftLines.map(l => l.length), 35);
347
- const totalWidth = leftWidth + qrWidth + 6;
348
- // Border components
349
- const topBorder = '╔' + '═'.repeat(totalWidth - 2) + '╗';
350
- const title = version ? `Claude Code Remote v${version}` : 'Claude Code Remote';
351
- const titleLine = '║' + title.padStart(Math.floor((totalWidth - 2 + title.length) / 2)).padEnd(totalWidth - 2) + '║';
352
- const sepLine = '╠' + '═'.repeat(leftWidth + 1) + '╤' + '═'.repeat(qrWidth + 2) + '╣';
353
- const midSep = '╠' + '═'.repeat(leftWidth + 1) + '╧' + '═'.repeat(qrWidth + 2) + '╣';
354
- const tokenLine = '║ ' + `Token: ${token}`.padEnd(totalWidth - 4) + ' ║';
355
- const bottomBorder = '╚' + '═'.repeat(totalWidth - 2) + '╝';
356
- // Render three-section banner
357
- process.stderr.write('\n');
358
- process.stderr.write(topBorder + '\n');
359
- process.stderr.write(titleLine + '\n');
360
- process.stderr.write(sepLine + '\n');
361
- const maxLines = Math.max(leftLines.length, rightLines.length);
362
- for (let i = 0; i < maxLines; i++) {
363
- const left = (leftLines[i] || '').padEnd(leftWidth);
364
- let right;
365
- if (i < qrLines.length && rightLines[i]) {
366
- right = ` ${rightLines[i]} `.padEnd(qrWidth + 2);
367
- }
368
- else if (rightLines[i]) {
369
- const centered = rightLines[i].padStart(Math.floor((qrWidth + rightLines[i].length) / 2)).padEnd(qrWidth);
370
- right = ` ${centered} `;
219
+ // 17. Start listening (awaitable: resolves on success, rejects on error)
220
+ await new Promise((resolve, reject) => {
221
+ httpServer.on('error', (err) => {
222
+ if (err.code === 'EADDRINUSE') {
223
+ logger.warn({ port, host: config.host }, 'Port is already in use, will attempt fallback');
224
+ reject(new PortInUseError(port));
225
+ return;
371
226
  }
372
- else {
373
- right = ' '.repeat(qrWidth + 2);
227
+ logger.error({ err }, 'HTTP server error');
228
+ reject(err);
229
+ });
230
+ httpServer.listen(port, config.host, () => {
231
+ // Record daemon start time
232
+ setDaemonStartTime(new Date().toISOString());
233
+ const url = `http://${config.displayIp}:${port}`;
234
+ if (!daemonMode) {
235
+ // CLI 直接启动模式(pnpm dev 等):打印 banner
236
+ printBanner({
237
+ url,
238
+ token,
239
+ instanceName: config.instanceName,
240
+ logDir: config.logDir,
241
+ pid: process.pid,
242
+ });
374
243
  }
375
- process.stderr.write(`║ ${left}│${right}║\n`);
376
- }
377
- process.stderr.write(midSep + '\n');
378
- process.stderr.write(tokenLine + '\n');
379
- process.stderr.write(bottomBorder + '\n');
380
- process.stderr.write('\n');
381
- logger.info({ url, host: config.host, port: actualPort, instanceName: config.instanceName, tokenSource }, 'Server started');
244
+ logger.info({ url, host: config.host, port, instanceName: config.instanceName, tokenSource, daemonMode }, 'Server started');
245
+ resolve();
246
+ });
382
247
  });
383
248
  }
384
249
  // Auto-start when run directly (non-CLI mode)
385
- // CLI mode: cli.ts imports startServer() directly
386
250
  if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
387
251
  startServer().catch((err) => {
388
252
  logger.error({ err }, 'Failed to start');