@clinebot/core 0.0.36 → 0.0.37

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 (228) hide show
  1. package/dist/ClineCore.d.ts +312 -3
  2. package/dist/ClineCore.d.ts.map +1 -1
  3. package/dist/account/cline-account-service.d.ts.map +1 -1
  4. package/dist/cron/cron-event-ingress.d.ts +38 -0
  5. package/dist/cron/cron-event-ingress.d.ts.map +1 -0
  6. package/dist/cron/cron-materializer.d.ts +36 -0
  7. package/dist/cron/cron-materializer.d.ts.map +1 -0
  8. package/dist/cron/cron-reconciler.d.ts +62 -0
  9. package/dist/cron/cron-reconciler.d.ts.map +1 -0
  10. package/dist/cron/cron-report-writer.d.ts +41 -0
  11. package/dist/cron/cron-report-writer.d.ts.map +1 -0
  12. package/dist/cron/cron-runner.d.ts +43 -0
  13. package/dist/cron/cron-runner.d.ts.map +1 -0
  14. package/dist/cron/cron-schema.d.ts +3 -0
  15. package/dist/cron/cron-schema.d.ts.map +1 -0
  16. package/dist/cron/cron-service.d.ts +57 -0
  17. package/dist/cron/cron-service.d.ts.map +1 -0
  18. package/dist/cron/cron-spec-parser.d.ts +27 -0
  19. package/dist/cron/cron-spec-parser.d.ts.map +1 -0
  20. package/dist/cron/cron-watcher.d.ts +23 -0
  21. package/dist/cron/cron-watcher.d.ts.map +1 -0
  22. package/dist/cron/scheduler.d.ts +3 -1
  23. package/dist/cron/scheduler.d.ts.map +1 -1
  24. package/dist/cron/sqlite-cron-store.d.ts +230 -0
  25. package/dist/cron/sqlite-cron-store.d.ts.map +1 -0
  26. package/dist/extensions/plugin/plugin-config-loader.d.ts +7 -1
  27. package/dist/extensions/plugin/plugin-config-loader.d.ts.map +1 -1
  28. package/dist/extensions/plugin/plugin-loader.d.ts +10 -6
  29. package/dist/extensions/plugin/plugin-loader.d.ts.map +1 -1
  30. package/dist/extensions/plugin/plugin-sandbox.d.ts +7 -1
  31. package/dist/extensions/plugin/plugin-sandbox.d.ts.map +1 -1
  32. package/dist/extensions/plugin-sandbox-bootstrap.js +236 -275
  33. package/dist/extensions/tools/constants.d.ts +1 -0
  34. package/dist/extensions/tools/constants.d.ts.map +1 -1
  35. package/dist/extensions/tools/definitions.d.ts +2 -3
  36. package/dist/extensions/tools/definitions.d.ts.map +1 -1
  37. package/dist/extensions/tools/executors/editor.d.ts.map +1 -1
  38. package/dist/extensions/tools/helpers.d.ts +1 -0
  39. package/dist/extensions/tools/helpers.d.ts.map +1 -1
  40. package/dist/extensions/tools/index.d.ts +1 -2
  41. package/dist/extensions/tools/index.d.ts.map +1 -1
  42. package/dist/extensions/tools/presets.d.ts +1 -1
  43. package/dist/extensions/tools/schemas.d.ts +25 -3
  44. package/dist/extensions/tools/schemas.d.ts.map +1 -1
  45. package/dist/extensions/tools/team/delegated-agent.d.ts +2 -2
  46. package/dist/extensions/tools/team/delegated-agent.d.ts.map +1 -1
  47. package/dist/extensions/tools/team/multi-agent.d.ts +7 -3
  48. package/dist/extensions/tools/team/multi-agent.d.ts.map +1 -1
  49. package/dist/extensions/tools/team/team-tools.d.ts.map +1 -1
  50. package/dist/extensions/tools/types.d.ts +0 -5
  51. package/dist/extensions/tools/types.d.ts.map +1 -1
  52. package/dist/hooks/hook-bridge.d.ts +118 -0
  53. package/dist/hooks/hook-bridge.d.ts.map +1 -0
  54. package/dist/hooks/hook-file-hooks.d.ts +2 -1
  55. package/dist/hooks/hook-file-hooks.d.ts.map +1 -1
  56. package/dist/hooks/hook-registry.d.ts +16 -0
  57. package/dist/hooks/hook-registry.d.ts.map +1 -0
  58. package/dist/hub/browser-websocket.d.ts.map +1 -1
  59. package/dist/hub/client.d.ts +7 -1
  60. package/dist/hub/client.d.ts.map +1 -1
  61. package/dist/hub/daemon-entry.js +721 -461
  62. package/dist/hub/daemon.d.ts.map +1 -1
  63. package/dist/hub/defaults.d.ts +8 -4
  64. package/dist/hub/defaults.d.ts.map +1 -1
  65. package/dist/hub/index.js +665 -415
  66. package/dist/hub/runtime-handlers.d.ts.map +1 -1
  67. package/dist/hub/server.d.ts +18 -0
  68. package/dist/hub/server.d.ts.map +1 -1
  69. package/dist/hub/session-client.d.ts +3 -0
  70. package/dist/hub/session-client.d.ts.map +1 -1
  71. package/dist/hub/start-shared-server.d.ts.map +1 -1
  72. package/dist/hub/ui-client.d.ts +1 -0
  73. package/dist/hub/ui-client.d.ts.map +1 -1
  74. package/dist/index.d.ts +9 -7
  75. package/dist/index.d.ts.map +1 -1
  76. package/dist/index.js +756 -467
  77. package/dist/llms/cline-recommended-models.d.ts +20 -0
  78. package/dist/llms/cline-recommended-models.d.ts.map +1 -0
  79. package/dist/llms/handler-factory.d.ts +16 -0
  80. package/dist/llms/handler-factory.d.ts.map +1 -0
  81. package/dist/llms/provider-defaults.d.ts.map +1 -1
  82. package/dist/llms/provider-settings.d.ts +45 -2
  83. package/dist/llms/provider-settings.d.ts.map +1 -1
  84. package/dist/llms/runtime-registry.d.ts.map +1 -1
  85. package/dist/runtime/agent-config-adapter.d.ts +148 -0
  86. package/dist/runtime/agent-config-adapter.d.ts.map +1 -0
  87. package/dist/runtime/agent-runtime-config-builder.d.ts +96 -0
  88. package/dist/runtime/agent-runtime-config-builder.d.ts.map +1 -0
  89. package/dist/runtime/history.d.ts +6 -0
  90. package/dist/runtime/history.d.ts.map +1 -1
  91. package/dist/runtime/host.d.ts.map +1 -1
  92. package/dist/runtime/loop-detection.d.ts +59 -0
  93. package/dist/runtime/loop-detection.d.ts.map +1 -0
  94. package/dist/runtime/mistake-tracker.d.ts +69 -0
  95. package/dist/runtime/mistake-tracker.d.ts.map +1 -0
  96. package/dist/runtime/runtime-builder.d.ts.map +1 -1
  97. package/dist/runtime/runtime-event-adapter.d.ts +102 -0
  98. package/dist/runtime/runtime-event-adapter.d.ts.map +1 -0
  99. package/dist/runtime/runtime-host.d.ts +28 -3
  100. package/dist/runtime/runtime-host.d.ts.map +1 -1
  101. package/dist/runtime/session-runtime-orchestrator.d.ts +261 -0
  102. package/dist/runtime/session-runtime-orchestrator.d.ts.map +1 -0
  103. package/dist/runtime/session-runtime.d.ts +16 -3
  104. package/dist/runtime/session-runtime.d.ts.map +1 -1
  105. package/dist/runtime/user-input-builder.d.ts +24 -0
  106. package/dist/runtime/user-input-builder.d.ts.map +1 -0
  107. package/dist/services/index.js +28 -0
  108. package/dist/services/local-runtime-bootstrap.d.ts.map +1 -1
  109. package/dist/services/plugin-tools.d.ts.map +1 -1
  110. package/dist/services/providers/local-provider-registry.d.ts +197 -21
  111. package/dist/services/providers/local-provider-registry.d.ts.map +1 -1
  112. package/dist/services/providers/local-provider-service.d.ts +3 -1
  113. package/dist/services/providers/local-provider-service.d.ts.map +1 -1
  114. package/dist/services/session-data.d.ts.map +1 -1
  115. package/dist/services/session-telemetry.d.ts +7 -2
  116. package/dist/services/session-telemetry.d.ts.map +1 -1
  117. package/dist/services/storage/file-team-store.d.ts.map +1 -1
  118. package/dist/services/storage/provider-settings-legacy-migration.d.ts.map +1 -1
  119. package/dist/services/storage/provider-settings-manager.d.ts +1 -0
  120. package/dist/services/storage/provider-settings-manager.d.ts.map +1 -1
  121. package/dist/services/storage/sqlite-team-store.d.ts.map +1 -1
  122. package/dist/session/conversation-store.d.ts +30 -0
  123. package/dist/session/conversation-store.d.ts.map +1 -0
  124. package/dist/session/message-builder.d.ts +65 -0
  125. package/dist/session/message-builder.d.ts.map +1 -0
  126. package/dist/session/session-manifest.d.ts +1 -1
  127. package/dist/transports/hub.d.ts +14 -3
  128. package/dist/transports/hub.d.ts.map +1 -1
  129. package/dist/transports/local.d.ts +14 -4
  130. package/dist/transports/local.d.ts.map +1 -1
  131. package/dist/transports/remote.d.ts.map +1 -1
  132. package/dist/types/chat-schema.d.ts +5 -5
  133. package/dist/types/config.d.ts +9 -0
  134. package/dist/types/config.d.ts.map +1 -1
  135. package/dist/types/events.d.ts +7 -6
  136. package/dist/types/events.d.ts.map +1 -1
  137. package/dist/types/provider-settings.d.ts +2 -2
  138. package/dist/types/provider-settings.d.ts.map +1 -1
  139. package/dist/types/session.d.ts +5 -2
  140. package/dist/types/session.d.ts.map +1 -1
  141. package/dist/types.d.ts +4 -4
  142. package/dist/types.d.ts.map +1 -1
  143. package/package.json +4 -4
  144. package/src/ClineCore.ts +691 -6
  145. package/src/account/cline-account-service.ts +44 -6
  146. package/src/cron/cron-event-ingress.ts +357 -0
  147. package/src/cron/cron-materializer.ts +97 -0
  148. package/src/cron/cron-reconciler.ts +241 -0
  149. package/src/cron/cron-report-writer.ts +153 -0
  150. package/src/cron/cron-runner.ts +495 -0
  151. package/src/cron/cron-schema.ts +127 -0
  152. package/src/cron/cron-service.ts +163 -0
  153. package/src/cron/cron-spec-parser.ts +489 -0
  154. package/src/cron/cron-watcher.ts +102 -0
  155. package/src/cron/index.ts +10 -0
  156. package/src/cron/scheduler.ts +141 -6
  157. package/src/cron/sqlite-cron-store.ts +1286 -0
  158. package/src/extensions/plugin/plugin-config-loader.ts +21 -1
  159. package/src/extensions/plugin/plugin-loader.ts +25 -9
  160. package/src/extensions/plugin/plugin-sandbox-bootstrap.ts +151 -1
  161. package/src/extensions/plugin/plugin-sandbox.ts +131 -7
  162. package/src/extensions/tools/constants.ts +2 -0
  163. package/src/extensions/tools/definitions.ts +31 -22
  164. package/src/extensions/tools/executors/editor.ts +4 -3
  165. package/src/extensions/tools/helpers.ts +24 -0
  166. package/src/extensions/tools/index.ts +1 -2
  167. package/src/extensions/tools/presets.ts +1 -1
  168. package/src/extensions/tools/schemas.ts +13 -18
  169. package/src/extensions/tools/team/delegated-agent.ts +8 -3
  170. package/src/extensions/tools/team/multi-agent.ts +135 -19
  171. package/src/extensions/tools/team/team-tools.ts +151 -91
  172. package/src/extensions/tools/types.ts +0 -6
  173. package/src/hooks/hook-bridge.ts +489 -0
  174. package/src/hooks/hook-file-hooks.ts +58 -3
  175. package/src/hooks/hook-registry.ts +257 -0
  176. package/src/hub/browser-websocket.ts +26 -4
  177. package/src/hub/client.ts +72 -13
  178. package/src/hub/daemon-entry.ts +35 -0
  179. package/src/hub/daemon.ts +117 -14
  180. package/src/hub/defaults.ts +39 -12
  181. package/src/hub/runtime-handlers.ts +4 -3
  182. package/src/hub/server.ts +506 -77
  183. package/src/hub/session-client.ts +43 -1
  184. package/src/hub/start-shared-server.ts +3 -0
  185. package/src/hub/ui-client.ts +4 -0
  186. package/src/index.ts +46 -1
  187. package/src/llms/cline-recommended-models.ts +167 -0
  188. package/src/llms/handler-factory.ts +56 -0
  189. package/src/llms/provider-defaults.ts +17 -1
  190. package/src/llms/provider-settings.ts +48 -1
  191. package/src/llms/runtime-registry.ts +1 -0
  192. package/src/runtime/agent-config-adapter.ts +636 -0
  193. package/src/runtime/agent-runtime-config-builder.ts +205 -0
  194. package/src/runtime/error-feedback.ts +142 -0
  195. package/src/runtime/history.ts +137 -0
  196. package/src/runtime/host.ts +22 -0
  197. package/src/runtime/loop-detection.ts +162 -0
  198. package/src/runtime/mistake-tracker.ts +221 -0
  199. package/src/runtime/runtime-builder.ts +61 -5
  200. package/src/runtime/runtime-event-adapter.ts +412 -0
  201. package/src/runtime/runtime-host.ts +45 -1
  202. package/src/runtime/session-runtime-orchestrator.ts +1253 -0
  203. package/src/runtime/session-runtime.ts +16 -2
  204. package/src/runtime/user-input-builder.ts +167 -0
  205. package/src/services/local-runtime-bootstrap.ts +128 -22
  206. package/src/services/plugin-tools.ts +1 -0
  207. package/src/services/providers/local-provider-registry.ts +273 -57
  208. package/src/services/providers/local-provider-service.ts +67 -7
  209. package/src/services/session-data.ts +16 -14
  210. package/src/services/session-telemetry.ts +6 -15
  211. package/src/services/storage/file-team-store.ts +1 -5
  212. package/src/services/storage/provider-settings-legacy-migration.ts +8 -47
  213. package/src/services/storage/provider-settings-manager.ts +16 -1
  214. package/src/services/storage/sqlite-team-store.ts +1 -5
  215. package/src/session/conversation-store.ts +77 -0
  216. package/src/session/message-builder.ts +941 -0
  217. package/src/transports/hub.ts +458 -33
  218. package/src/transports/local.ts +296 -65
  219. package/src/transports/remote.ts +1 -0
  220. package/src/types/config.ts +9 -0
  221. package/src/types/events.ts +8 -6
  222. package/src/types/index.ts +3 -0
  223. package/src/types/provider-settings.ts +8 -1
  224. package/src/types/session.ts +5 -2
  225. package/src/types.ts +15 -1
  226. package/dist/cron/index.d.ts +0 -6
  227. package/dist/cron/index.d.ts.map +0 -1
  228. package/dist/services/telemetry/index.js +0 -28
@@ -0,0 +1,257 @@
1
+ /**
2
+ * Lifecycle-hook registration wiring for the legacy `AgentHooks` and
3
+ * `AgentExtension` surfaces onto a `HookEngine`.
4
+ *
5
+ * @see PLAN.md §3.1 — moved from `packages/agents/src/runtime/hook-registry.ts`.
6
+ * @see PLAN.md §3.3.3 — OLD `config.hooks.*` → NEW placement.
7
+ *
8
+ * Verbatim port: pure wiring with no state. After the migration this is
9
+ * invoked by `HookBridge` and the legacy-agent facade's `addTools`
10
+ * pathway. Lives under `packages/core/src/hooks/` per §3.1.
11
+ */
12
+
13
+ import type {
14
+ AgentConfig,
15
+ AgentExtension,
16
+ AgentExtensionHookStage,
17
+ AgentHookControl,
18
+ HookEngine,
19
+ HookHandler,
20
+ } from "@clinebot/shared";
21
+
22
+ type LifecycleConfig = Pick<AgentConfig, "hooks" | "extensions">;
23
+
24
+ type ExtensionStageHandlerSpec = {
25
+ handler: (
26
+ extension: AgentExtension,
27
+ event: { payload: unknown },
28
+ ) => Promise<AgentHookControl | undefined> | AgentHookControl | undefined;
29
+ name: string;
30
+ };
31
+
32
+ const EXTENSION_STAGE_HANDLERS: Record<
33
+ AgentExtensionHookStage,
34
+ ExtensionStageHandlerSpec
35
+ > = {
36
+ input: {
37
+ name: "onInput",
38
+ handler: (extension, event) =>
39
+ extension.onInput?.(event.payload as never) as
40
+ | AgentHookControl
41
+ | undefined,
42
+ },
43
+ session_start: {
44
+ name: "onSessionStart",
45
+ handler: (extension, event) =>
46
+ extension.onSessionStart?.(event.payload as never) as
47
+ | AgentHookControl
48
+ | undefined,
49
+ },
50
+ run_start: {
51
+ name: "onRunStart",
52
+ handler: (extension, event) =>
53
+ extension.onRunStart?.(event.payload as never) as
54
+ | AgentHookControl
55
+ | undefined,
56
+ },
57
+ iteration_start: {
58
+ name: "onIterationStart",
59
+ handler: (extension, event) =>
60
+ extension.onIterationStart?.(event.payload as never) as
61
+ | AgentHookControl
62
+ | undefined,
63
+ },
64
+ turn_start: {
65
+ name: "onTurnStart",
66
+ handler: (extension, event) =>
67
+ extension.onTurnStart?.(event.payload as never) as
68
+ | AgentHookControl
69
+ | undefined,
70
+ },
71
+ before_agent_start: {
72
+ name: "onBeforeAgentStart",
73
+ handler: (extension, event) =>
74
+ extension.onBeforeAgentStart?.(event.payload as never) as
75
+ | AgentHookControl
76
+ | undefined,
77
+ },
78
+ tool_call_before: {
79
+ name: "onToolCall",
80
+ handler: (extension, event) =>
81
+ extension.onToolCall?.(event.payload as never) as
82
+ | AgentHookControl
83
+ | undefined,
84
+ },
85
+ tool_call_after: {
86
+ name: "onToolResult",
87
+ handler: (extension, event) =>
88
+ extension.onToolResult?.(event.payload as never) as
89
+ | AgentHookControl
90
+ | undefined,
91
+ },
92
+ turn_end: {
93
+ name: "onTurnEnd",
94
+ handler: (extension, event) =>
95
+ extension.onTurnEnd?.(event.payload as never) as
96
+ | AgentHookControl
97
+ | undefined,
98
+ },
99
+ stop_error: {
100
+ name: "onAgentError",
101
+ handler: (extension, event) =>
102
+ extension.onAgentError?.(event.payload as never) as
103
+ | AgentHookControl
104
+ | undefined,
105
+ },
106
+ iteration_end: {
107
+ name: "onIterationEnd",
108
+ handler: async (extension, event) => {
109
+ await extension.onIterationEnd?.(event.payload as never);
110
+ return undefined;
111
+ },
112
+ },
113
+ run_end: {
114
+ name: "onRunEnd",
115
+ handler: async (extension, event) => {
116
+ await extension.onRunEnd?.(event.payload as never);
117
+ return undefined;
118
+ },
119
+ },
120
+ session_shutdown: {
121
+ name: "onSessionShutdown",
122
+ handler: (extension, event) =>
123
+ extension.onSessionShutdown?.(event.payload as never) as
124
+ | AgentHookControl
125
+ | undefined,
126
+ },
127
+ error: {
128
+ name: "onError",
129
+ handler: async (extension, event) => {
130
+ await extension.onError?.(event.payload as never);
131
+ return undefined;
132
+ },
133
+ },
134
+ runtime_event: {
135
+ name: "onRuntimeEvent",
136
+ handler: async (extension, event) => {
137
+ await extension.onRuntimeEvent?.(event.payload as never);
138
+ return undefined;
139
+ },
140
+ },
141
+ };
142
+
143
+ export function registerLifecycleHandlers(
144
+ hookEngine: HookEngine,
145
+ config: LifecycleConfig,
146
+ ): void {
147
+ const register = (handler: HookHandler): void => {
148
+ hookEngine.register(handler);
149
+ };
150
+ const hooks = config.hooks;
151
+ if (hooks?.onSessionStart)
152
+ register({
153
+ name: "hooks.onSessionStart",
154
+ stage: "session_start",
155
+ handle: (event) => hooks.onSessionStart?.(event.payload as never),
156
+ });
157
+ if (hooks?.onRunStart)
158
+ register({
159
+ name: "hooks.onRunStart",
160
+ stage: "run_start",
161
+ handle: (event) => hooks.onRunStart?.(event.payload as never),
162
+ });
163
+ if (hooks?.onRunEnd)
164
+ register({
165
+ name: "hooks.onRunEnd",
166
+ stage: "run_end",
167
+ handle: async (event) => {
168
+ await hooks.onRunEnd?.(event.payload as never);
169
+ return undefined;
170
+ },
171
+ });
172
+ if (hooks?.onIterationStart)
173
+ register({
174
+ name: "hooks.onIterationStart",
175
+ stage: "iteration_start",
176
+ handle: (event) => hooks.onIterationStart?.(event.payload as never),
177
+ });
178
+ if (hooks?.onIterationEnd)
179
+ register({
180
+ name: "hooks.onIterationEnd",
181
+ stage: "iteration_end",
182
+ handle: async (event) => {
183
+ await hooks.onIterationEnd?.(event.payload as never);
184
+ return undefined;
185
+ },
186
+ });
187
+ if (hooks?.onTurnStart)
188
+ register({
189
+ name: "hooks.onTurnStart",
190
+ stage: "turn_start",
191
+ handle: (event) => hooks.onTurnStart?.(event.payload as never),
192
+ });
193
+ if (hooks?.onBeforeAgentStart) {
194
+ register({
195
+ name: "hooks.onBeforeAgentStart",
196
+ stage: "before_agent_start",
197
+ handle: (event) => hooks.onBeforeAgentStart?.(event.payload as never),
198
+ });
199
+ }
200
+ if (hooks?.onTurnEnd) {
201
+ register({
202
+ name: "hooks.onTurnEnd",
203
+ stage: "turn_end",
204
+ handle: (event) => hooks.onTurnEnd?.(event.payload as never),
205
+ });
206
+ }
207
+ if (hooks?.onStopError)
208
+ register({
209
+ name: "hooks.onStopError",
210
+ stage: "stop_error",
211
+ handle: (event) => hooks.onStopError?.(event.payload as never),
212
+ });
213
+ if (hooks?.onToolCallStart)
214
+ register({
215
+ name: "hooks.onToolCallStart",
216
+ stage: "tool_call_before",
217
+ handle: (event) => hooks.onToolCallStart?.(event.payload as never),
218
+ });
219
+ if (hooks?.onToolCallEnd)
220
+ register({
221
+ name: "hooks.onToolCallEnd",
222
+ stage: "tool_call_after",
223
+ handle: (event) => hooks.onToolCallEnd?.(event.payload as never),
224
+ });
225
+ if (hooks?.onSessionShutdown)
226
+ register({
227
+ name: "hooks.onSessionShutdown",
228
+ stage: "session_shutdown",
229
+ handle: (event) => hooks.onSessionShutdown?.(event.payload as never),
230
+ });
231
+ if (hooks?.onError)
232
+ register({
233
+ name: "hooks.onError",
234
+ stage: "error",
235
+ handle: async (event) => {
236
+ await hooks.onError?.(event.payload as never);
237
+ return undefined;
238
+ },
239
+ });
240
+ for (const [index, extension] of (config.extensions ?? []).entries()) {
241
+ if (extension.disabled) continue;
242
+ if (!extension.manifest.capabilities.includes("hooks")) continue;
243
+ const order = String(index).padStart(4, "0");
244
+ const extensionName = extension.name || `extension_${order}`;
245
+ const base = `${order}:${extensionName}`;
246
+ const subscribedStages = new Set(extension.manifest.hookStages ?? []);
247
+ for (const stage of subscribedStages) {
248
+ const stageHandler = EXTENSION_STAGE_HANDLERS[stage];
249
+ if (!stageHandler) continue;
250
+ register({
251
+ name: `${base}.${stageHandler.name}`,
252
+ stage,
253
+ handle: (event) => stageHandler.handler(extension, event),
254
+ });
255
+ }
256
+ }
257
+ }
@@ -4,6 +4,7 @@ import type {
4
4
  HubReplyEnvelope,
5
5
  HubTransportFrame,
6
6
  } from "@clinebot/shared";
7
+ import { safeJsonParse } from "@clinebot/shared";
7
8
  import type { HubCommandTransport } from "./transport";
8
9
 
9
10
  export interface BrowserHubSocketLike {
@@ -29,7 +30,17 @@ export class BrowserWebSocketHubAdapter {
29
30
  let closed = false;
30
31
 
31
32
  const sendFrame = (frame: HubTransportFrame): void => {
32
- socket.send(JSON.stringify(frame));
33
+ try {
34
+ socket.send(JSON.stringify(frame));
35
+ } catch (error) {
36
+ console.error(
37
+ `[hub] failed to send websocket frame: ${
38
+ error instanceof Error
39
+ ? error.stack || error.message
40
+ : String(error)
41
+ }`,
42
+ );
43
+ }
33
44
  };
34
45
 
35
46
  const onEvent = (envelope: HubEventEnvelope): void => {
@@ -37,8 +48,8 @@ export class BrowserWebSocketHubAdapter {
37
48
  };
38
49
 
39
50
  const onMessage = async (event: { data: string }): Promise<void> => {
40
- const frame = JSON.parse(event.data) as HubTransportFrame;
41
51
  try {
52
+ const frame = JSON.parse(event.data) as HubTransportFrame;
42
53
  switch (frame.kind) {
43
54
  case "command": {
44
55
  const reply = await this.transport.command(frame.envelope);
@@ -90,13 +101,24 @@ export class BrowserWebSocketHubAdapter {
90
101
  break;
91
102
  }
92
103
  } catch (error) {
93
- if (frame.kind !== "command") {
104
+ const parsed =
105
+ typeof event.data === "string"
106
+ ? safeJsonParse<HubTransportFrame>(event.data)
107
+ : undefined;
108
+ if (!parsed || parsed.kind !== "command") {
109
+ console.error(
110
+ `[hub] rejected malformed websocket frame: ${
111
+ error instanceof Error
112
+ ? error.stack || error.message
113
+ : String(error)
114
+ }`,
115
+ );
94
116
  return;
95
117
  }
96
118
  sendFrame({
97
119
  kind: "reply",
98
120
  envelope: {
99
- ...frame.envelope,
121
+ ...parsed.envelope,
100
122
  ok: false,
101
123
  error: {
102
124
  code: "command_failed",
package/src/hub/client.ts CHANGED
@@ -152,6 +152,7 @@ export class NodeHubClient {
152
152
  private readonly listeners = new Set<SubscriptionEntry>();
153
153
  private readonly subscriptionCounts = new Map<string, number>();
154
154
  private lastCloseMessage = "Hub connection closed";
155
+ private registered = false;
155
156
 
156
157
  constructor(private readonly options: HubClientOptions) {
157
158
  this.clientId =
@@ -253,6 +254,7 @@ export class NodeHubClient {
253
254
  cwd: this.options.cwd,
254
255
  },
255
256
  } satisfies HubClientRegistration);
257
+ this.registered = true;
256
258
  for (const key of this.subscriptionCounts.keys()) {
257
259
  this.sendSubscriptionFrame(
258
260
  "stream.subscribe",
@@ -281,27 +283,36 @@ export class NodeHubClient {
281
283
  command: HubCommandEnvelope["command"],
282
284
  payload?: Record<string, unknown>,
283
285
  sessionId?: string,
286
+ options?: { timeoutMs?: number | null },
284
287
  ): Promise<HubReplyEnvelope> {
285
288
  await this.connect();
286
289
  const requestId = createSessionId("hubreq_");
287
290
  const reply = new Promise<HubReplyEnvelope>((resolve, reject) => {
288
- const timeout = setTimeout(() => {
289
- if (!this.pendingReplies.delete(requestId)) {
290
- return;
291
- }
292
- reject(
293
- new Error(
294
- `Hub command ${command} timed out after ${HUB_COMMAND_TIMEOUT_MS}ms`,
295
- ),
296
- );
297
- }, HUB_COMMAND_TIMEOUT_MS);
291
+ const timeoutMs = options?.timeoutMs;
292
+ const timeout =
293
+ timeoutMs === null
294
+ ? undefined
295
+ : setTimeout(() => {
296
+ if (!this.pendingReplies.delete(requestId)) {
297
+ return;
298
+ }
299
+ reject(
300
+ new Error(
301
+ `Hub command ${command} timed out after ${timeoutMs ?? HUB_COMMAND_TIMEOUT_MS}ms`,
302
+ ),
303
+ );
304
+ }, timeoutMs ?? HUB_COMMAND_TIMEOUT_MS);
298
305
  this.pendingReplies.set(requestId, {
299
306
  resolve: (value) => {
300
- clearTimeout(timeout);
307
+ if (timeout) {
308
+ clearTimeout(timeout);
309
+ }
301
310
  resolve(value);
302
311
  },
303
312
  reject: (error) => {
304
- clearTimeout(timeout);
313
+ if (timeout) {
314
+ clearTimeout(timeout);
315
+ }
305
316
  reject(error);
306
317
  },
307
318
  });
@@ -328,6 +339,7 @@ export class NodeHubClient {
328
339
 
329
340
  close(): void {
330
341
  const socket = this.socket;
342
+ this.registered = false;
331
343
  if (!socket) {
332
344
  return;
333
345
  }
@@ -345,6 +357,21 @@ export class NodeHubClient {
345
357
  }
346
358
  }
347
359
 
360
+ async dispose(): Promise<void> {
361
+ const socket = this.socket;
362
+ if (socket?.readyState === 1 && this.registered) {
363
+ try {
364
+ await this.command("client.unregister", undefined, undefined, {
365
+ timeoutMs: 2_000,
366
+ });
367
+ } catch {
368
+ // Best-effort unregister during shutdown. The websocket adapter also
369
+ // unregisters clients on close, so failure here should not block teardown.
370
+ }
371
+ }
372
+ this.close();
373
+ }
374
+
348
375
  private sendFrame(frame: HubTransportFrame): void {
349
376
  if (!this.socket || this.socket.readyState !== 1) {
350
377
  throw new Error(
@@ -486,7 +513,8 @@ async function probeCompatibleHubUrl(
486
513
  };
487
514
  }
488
515
  const buildId = resolveHubBuildId();
489
- if (record.buildId?.trim() && record.buildId !== buildId) {
516
+ const recordBuildId = record.buildId?.trim();
517
+ if (!recordBuildId || recordBuildId !== buildId) {
490
518
  return {
491
519
  status: "build_mismatch",
492
520
  url: normalized,
@@ -572,3 +600,34 @@ export async function ensureCompatibleLocalHubUrl(
572
600
  spawnDetachedHubServer(options.workspaceRoot ?? process.cwd());
573
601
  return await waitForCompatibleHubUrl(owner);
574
602
  }
603
+
604
+ export async function requestHubShutdown(url: string): Promise<boolean> {
605
+ const parsed = new URL(url);
606
+ if (parsed.protocol === "ws:") {
607
+ parsed.protocol = "http:";
608
+ } else if (parsed.protocol === "wss:") {
609
+ parsed.protocol = "https:";
610
+ }
611
+ parsed.pathname = "/shutdown";
612
+ parsed.search = "";
613
+ parsed.hash = "";
614
+ const response = await fetch(parsed, { method: "POST" });
615
+ return response.ok;
616
+ }
617
+
618
+ export async function stopLocalHubServerGracefully(): Promise<boolean> {
619
+ const owner = resolveSharedHubOwnerContext();
620
+ const discovery = await readHubDiscovery(owner.discoveryPath);
621
+ if (!discovery?.url) {
622
+ return false;
623
+ }
624
+ try {
625
+ const stopped = await requestHubShutdown(discovery.url);
626
+ if (stopped) {
627
+ return true;
628
+ }
629
+ } catch {
630
+ // Fall through so callers can apply a stronger fallback.
631
+ }
632
+ return false;
633
+ }
@@ -1,8 +1,11 @@
1
+ import { initVcr } from "@clinebot/shared";
1
2
  import { resolveHubEndpointOptions } from "./defaults";
2
3
  import { createLocalHubScheduleRuntimeHandlers } from "./runtime-handlers";
3
4
  import { startHubWebSocketServer } from "./server";
4
5
  import { resolveSharedHubOwnerContext } from "./workspace";
5
6
 
7
+ initVcr(process.env.CLINE_VCR);
8
+
6
9
  function parseArgs(argv: string[]): {
7
10
  cwd: string;
8
11
  host?: string;
@@ -60,6 +63,7 @@ async function main(): Promise<void> {
60
63
  pathname: endpoint.pathname,
61
64
  owner: resolveSharedHubOwnerContext(),
62
65
  runtimeHandlers: createLocalHubScheduleRuntimeHandlers(),
66
+ cronOptions: { workspaceRoot: options.cwd },
63
67
  });
64
68
 
65
69
  const shutdown = async (): Promise<void> => {
@@ -67,12 +71,43 @@ async function main(): Promise<void> {
67
71
  process.exit(0);
68
72
  };
69
73
 
74
+ let fatalShutdownStarted = false;
75
+ const shutdownFatal = (label: string, error: unknown): void => {
76
+ if (fatalShutdownStarted) {
77
+ return;
78
+ }
79
+ fatalShutdownStarted = true;
80
+ const message =
81
+ error instanceof Error ? error.stack || error.message : String(error);
82
+ process.stderr.write(`[hub-daemon] ${label}: ${message}\n`);
83
+ void server
84
+ .close()
85
+ .catch((closeError) => {
86
+ const closeMessage =
87
+ closeError instanceof Error
88
+ ? closeError.stack || closeError.message
89
+ : String(closeError);
90
+ process.stderr.write(
91
+ `[hub-daemon] shutdown after ${label} failed: ${closeMessage}\n`,
92
+ );
93
+ })
94
+ .finally(() => {
95
+ process.exit(1);
96
+ });
97
+ };
98
+
70
99
  process.on("SIGINT", () => {
71
100
  void shutdown();
72
101
  });
73
102
  process.on("SIGTERM", () => {
74
103
  void shutdown();
75
104
  });
105
+ process.on("uncaughtException", (error) => {
106
+ shutdownFatal("uncaughtException", error);
107
+ });
108
+ process.on("unhandledRejection", (reason) => {
109
+ shutdownFatal("unhandledRejection", reason);
110
+ });
76
111
 
77
112
  await new Promise<void>(() => {
78
113
  // keep daemon process alive