@clinebot/core 0.0.21 → 0.0.23

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 (260) hide show
  1. package/dist/ClineCore.d.ts +110 -0
  2. package/dist/ClineCore.d.ts.map +1 -0
  3. package/dist/account/cline-account-service.d.ts +2 -1
  4. package/dist/account/cline-account-service.d.ts.map +1 -1
  5. package/dist/account/index.d.ts +1 -1
  6. package/dist/account/index.d.ts.map +1 -1
  7. package/dist/account/rpc.d.ts +3 -1
  8. package/dist/account/rpc.d.ts.map +1 -1
  9. package/dist/account/types.d.ts +3 -0
  10. package/dist/account/types.d.ts.map +1 -1
  11. package/dist/agents/plugin-loader.d.ts.map +1 -1
  12. package/dist/agents/plugin-sandbox-bootstrap.js +17 -17
  13. package/dist/auth/client.d.ts +1 -1
  14. package/dist/auth/client.d.ts.map +1 -1
  15. package/dist/auth/cline.d.ts +1 -1
  16. package/dist/auth/cline.d.ts.map +1 -1
  17. package/dist/auth/codex.d.ts +1 -1
  18. package/dist/auth/codex.d.ts.map +1 -1
  19. package/dist/auth/oca.d.ts +1 -1
  20. package/dist/auth/oca.d.ts.map +1 -1
  21. package/dist/auth/utils.d.ts +2 -2
  22. package/dist/auth/utils.d.ts.map +1 -1
  23. package/dist/index.d.ts +50 -5
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +949 -0
  26. package/dist/providers/local-provider-service.d.ts +4 -4
  27. package/dist/providers/local-provider-service.d.ts.map +1 -1
  28. package/dist/runtime/runtime-builder.d.ts +1 -0
  29. package/dist/runtime/runtime-builder.d.ts.map +1 -1
  30. package/dist/runtime/session-runtime.d.ts +2 -1
  31. package/dist/runtime/session-runtime.d.ts.map +1 -1
  32. package/dist/runtime/team-runtime-registry.d.ts +13 -0
  33. package/dist/runtime/team-runtime-registry.d.ts.map +1 -0
  34. package/dist/session/default-session-manager.d.ts +2 -2
  35. package/dist/session/default-session-manager.d.ts.map +1 -1
  36. package/dist/session/rpc-runtime-ensure.d.ts +53 -0
  37. package/dist/session/rpc-runtime-ensure.d.ts.map +1 -0
  38. package/dist/session/session-config-builder.d.ts +2 -3
  39. package/dist/session/session-config-builder.d.ts.map +1 -1
  40. package/dist/session/session-host.d.ts +8 -18
  41. package/dist/session/session-host.d.ts.map +1 -1
  42. package/dist/session/session-manager.d.ts +1 -1
  43. package/dist/session/session-manager.d.ts.map +1 -1
  44. package/dist/session/session-manifest.d.ts +1 -2
  45. package/dist/session/session-manifest.d.ts.map +1 -1
  46. package/dist/session/unified-session-persistence-service.d.ts +2 -2
  47. package/dist/session/unified-session-persistence-service.d.ts.map +1 -1
  48. package/dist/session/utils/helpers.d.ts +1 -1
  49. package/dist/session/utils/helpers.d.ts.map +1 -1
  50. package/dist/session/utils/types.d.ts +1 -1
  51. package/dist/session/utils/types.d.ts.map +1 -1
  52. package/dist/storage/provider-settings-legacy-migration.d.ts.map +1 -1
  53. package/dist/telemetry/OpenTelemetryProvider.d.ts.map +1 -1
  54. package/dist/telemetry/distinct-id.d.ts +2 -0
  55. package/dist/telemetry/distinct-id.d.ts.map +1 -0
  56. package/dist/telemetry/{opentelemetry.d.ts → index.d.ts} +1 -1
  57. package/dist/telemetry/index.d.ts.map +1 -0
  58. package/dist/telemetry/index.js +28 -0
  59. package/dist/tools/constants.d.ts +1 -1
  60. package/dist/tools/constants.d.ts.map +1 -1
  61. package/dist/tools/definitions.d.ts +3 -3
  62. package/dist/tools/definitions.d.ts.map +1 -1
  63. package/dist/tools/executors/apply-patch.d.ts +1 -1
  64. package/dist/tools/executors/apply-patch.d.ts.map +1 -1
  65. package/dist/tools/executors/bash.d.ts +1 -1
  66. package/dist/tools/executors/bash.d.ts.map +1 -1
  67. package/dist/tools/executors/editor.d.ts +1 -1
  68. package/dist/tools/executors/editor.d.ts.map +1 -1
  69. package/dist/tools/executors/file-read.d.ts +1 -1
  70. package/dist/tools/executors/file-read.d.ts.map +1 -1
  71. package/dist/tools/executors/index.d.ts +14 -14
  72. package/dist/tools/executors/index.d.ts.map +1 -1
  73. package/dist/tools/executors/search.d.ts +1 -1
  74. package/dist/tools/executors/search.d.ts.map +1 -1
  75. package/dist/tools/executors/web-fetch.d.ts +1 -1
  76. package/dist/tools/executors/web-fetch.d.ts.map +1 -1
  77. package/dist/tools/helpers.d.ts +1 -1
  78. package/dist/tools/helpers.d.ts.map +1 -1
  79. package/dist/tools/index.d.ts +10 -10
  80. package/dist/tools/index.d.ts.map +1 -1
  81. package/dist/tools/model-tool-routing.d.ts +1 -1
  82. package/dist/tools/model-tool-routing.d.ts.map +1 -1
  83. package/dist/tools/presets.d.ts +1 -1
  84. package/dist/tools/presets.d.ts.map +1 -1
  85. package/dist/types/common.d.ts +17 -8
  86. package/dist/types/common.d.ts.map +1 -1
  87. package/dist/types/config.d.ts +4 -3
  88. package/dist/types/config.d.ts.map +1 -1
  89. package/dist/types/provider-settings.d.ts +1 -1
  90. package/dist/types/provider-settings.d.ts.map +1 -1
  91. package/dist/types.d.ts +5 -2
  92. package/dist/types.d.ts.map +1 -1
  93. package/dist/version.d.ts +2 -0
  94. package/dist/version.d.ts.map +1 -0
  95. package/package.json +44 -38
  96. package/src/ClineCore.ts +137 -0
  97. package/src/account/cline-account-service.test.ts +101 -0
  98. package/src/account/cline-account-service.ts +300 -0
  99. package/src/account/featurebase-token.test.ts +175 -0
  100. package/src/account/index.ts +23 -0
  101. package/src/account/rpc.test.ts +63 -0
  102. package/src/account/rpc.ts +185 -0
  103. package/src/account/types.ts +102 -0
  104. package/src/agents/agent-config-loader.test.ts +236 -0
  105. package/src/agents/agent-config-loader.ts +108 -0
  106. package/src/agents/agent-config-parser.ts +198 -0
  107. package/src/agents/hooks-config-loader.test.ts +20 -0
  108. package/src/agents/hooks-config-loader.ts +118 -0
  109. package/src/agents/index.ts +85 -0
  110. package/src/agents/plugin-config-loader.test.ts +140 -0
  111. package/src/agents/plugin-config-loader.ts +97 -0
  112. package/src/agents/plugin-loader.test.ts +210 -0
  113. package/src/agents/plugin-loader.ts +175 -0
  114. package/src/agents/plugin-sandbox-bootstrap.ts +448 -0
  115. package/src/agents/plugin-sandbox.test.ts +296 -0
  116. package/src/agents/plugin-sandbox.ts +341 -0
  117. package/src/agents/unified-config-file-watcher.test.ts +196 -0
  118. package/src/agents/unified-config-file-watcher.ts +483 -0
  119. package/src/agents/user-instruction-config-loader.test.ts +158 -0
  120. package/src/agents/user-instruction-config-loader.ts +438 -0
  121. package/src/auth/client.test.ts +40 -0
  122. package/src/auth/client.ts +25 -0
  123. package/src/auth/cline.test.ts +130 -0
  124. package/src/auth/cline.ts +420 -0
  125. package/src/auth/codex.test.ts +170 -0
  126. package/src/auth/codex.ts +491 -0
  127. package/src/auth/oca.test.ts +215 -0
  128. package/src/auth/oca.ts +573 -0
  129. package/src/auth/server.ts +216 -0
  130. package/src/auth/types.ts +81 -0
  131. package/src/auth/utils.test.ts +128 -0
  132. package/src/auth/utils.ts +247 -0
  133. package/src/chat/chat-schema.ts +82 -0
  134. package/src/index.ts +479 -0
  135. package/src/input/file-indexer.d.ts +11 -0
  136. package/src/input/file-indexer.test.ts +127 -0
  137. package/src/input/file-indexer.ts +327 -0
  138. package/src/input/index.ts +7 -0
  139. package/src/input/mention-enricher.test.ts +85 -0
  140. package/src/input/mention-enricher.ts +122 -0
  141. package/src/mcp/config-loader.test.ts +238 -0
  142. package/src/mcp/config-loader.ts +219 -0
  143. package/src/mcp/index.ts +26 -0
  144. package/src/mcp/manager.test.ts +106 -0
  145. package/src/mcp/manager.ts +262 -0
  146. package/src/mcp/types.ts +88 -0
  147. package/src/providers/local-provider-registry.ts +232 -0
  148. package/src/providers/local-provider-service.test.ts +783 -0
  149. package/src/providers/local-provider-service.ts +471 -0
  150. package/src/runtime/commands.test.ts +98 -0
  151. package/src/runtime/commands.ts +83 -0
  152. package/src/runtime/hook-file-hooks.test.ts +237 -0
  153. package/src/runtime/hook-file-hooks.ts +859 -0
  154. package/src/runtime/index.ts +37 -0
  155. package/src/runtime/rules.ts +34 -0
  156. package/src/runtime/runtime-builder.team-persistence.test.ts +245 -0
  157. package/src/runtime/runtime-builder.test.ts +371 -0
  158. package/src/runtime/runtime-builder.ts +631 -0
  159. package/src/runtime/runtime-parity.test.ts +143 -0
  160. package/src/runtime/sandbox/subprocess-sandbox.ts +231 -0
  161. package/src/runtime/session-runtime.ts +49 -0
  162. package/src/runtime/skills.ts +44 -0
  163. package/src/runtime/team-runtime-registry.ts +46 -0
  164. package/src/runtime/tool-approval.ts +104 -0
  165. package/src/runtime/workflows.test.ts +119 -0
  166. package/src/runtime/workflows.ts +45 -0
  167. package/src/session/default-session-manager.e2e.test.ts +384 -0
  168. package/src/session/default-session-manager.test.ts +1931 -0
  169. package/src/session/default-session-manager.ts +1422 -0
  170. package/src/session/file-session-service.ts +280 -0
  171. package/src/session/index.ts +45 -0
  172. package/src/session/rpc-runtime-ensure.ts +521 -0
  173. package/src/session/rpc-session-service.ts +107 -0
  174. package/src/session/rpc-spawn-lease.test.ts +49 -0
  175. package/src/session/rpc-spawn-lease.ts +122 -0
  176. package/src/session/runtime-oauth-token-manager.test.ts +137 -0
  177. package/src/session/runtime-oauth-token-manager.ts +272 -0
  178. package/src/session/session-agent-events.ts +248 -0
  179. package/src/session/session-artifacts.ts +106 -0
  180. package/src/session/session-config-builder.ts +113 -0
  181. package/src/session/session-graph.ts +92 -0
  182. package/src/session/session-host.test.ts +89 -0
  183. package/src/session/session-host.ts +205 -0
  184. package/src/session/session-manager.ts +69 -0
  185. package/src/session/session-manifest.ts +29 -0
  186. package/src/session/session-service.team-persistence.test.ts +48 -0
  187. package/src/session/session-service.ts +673 -0
  188. package/src/session/session-team-coordination.ts +229 -0
  189. package/src/session/session-telemetry.ts +100 -0
  190. package/src/session/sqlite-rpc-session-backend.ts +303 -0
  191. package/src/session/unified-session-persistence-service.test.ts +85 -0
  192. package/src/session/unified-session-persistence-service.ts +994 -0
  193. package/src/session/utils/helpers.ts +139 -0
  194. package/src/session/utils/types.ts +57 -0
  195. package/src/session/utils/usage.ts +32 -0
  196. package/src/session/workspace-manager.ts +98 -0
  197. package/src/session/workspace-manifest.ts +100 -0
  198. package/src/storage/artifact-store.ts +1 -0
  199. package/src/storage/file-team-store.ts +257 -0
  200. package/src/storage/index.ts +11 -0
  201. package/src/storage/provider-settings-legacy-migration.test.ts +424 -0
  202. package/src/storage/provider-settings-legacy-migration.ts +826 -0
  203. package/src/storage/provider-settings-manager.test.ts +191 -0
  204. package/src/storage/provider-settings-manager.ts +152 -0
  205. package/src/storage/session-store.ts +1 -0
  206. package/src/storage/sqlite-session-store.ts +275 -0
  207. package/src/storage/sqlite-team-store.ts +454 -0
  208. package/src/storage/team-store.ts +40 -0
  209. package/src/team/index.ts +4 -0
  210. package/src/team/projections.ts +285 -0
  211. package/src/telemetry/ITelemetryAdapter.ts +94 -0
  212. package/src/telemetry/LoggerTelemetryAdapter.test.ts +42 -0
  213. package/src/telemetry/LoggerTelemetryAdapter.ts +114 -0
  214. package/src/telemetry/OpenTelemetryAdapter.test.ts +157 -0
  215. package/src/telemetry/OpenTelemetryAdapter.ts +348 -0
  216. package/src/telemetry/OpenTelemetryProvider.test.ts +113 -0
  217. package/src/telemetry/OpenTelemetryProvider.ts +325 -0
  218. package/src/telemetry/TelemetryService.test.ts +134 -0
  219. package/src/telemetry/TelemetryService.ts +141 -0
  220. package/src/telemetry/core-events.ts +400 -0
  221. package/src/telemetry/distinct-id.test.ts +57 -0
  222. package/src/telemetry/distinct-id.ts +58 -0
  223. package/src/telemetry/index.ts +20 -0
  224. package/src/tools/constants.ts +35 -0
  225. package/src/tools/definitions.test.ts +704 -0
  226. package/src/tools/definitions.ts +709 -0
  227. package/src/tools/executors/apply-patch-parser.ts +520 -0
  228. package/src/tools/executors/apply-patch.ts +359 -0
  229. package/src/tools/executors/bash.test.ts +87 -0
  230. package/src/tools/executors/bash.ts +207 -0
  231. package/src/tools/executors/editor.test.ts +35 -0
  232. package/src/tools/executors/editor.ts +219 -0
  233. package/src/tools/executors/file-read.test.ts +49 -0
  234. package/src/tools/executors/file-read.ts +110 -0
  235. package/src/tools/executors/index.ts +87 -0
  236. package/src/tools/executors/search.ts +278 -0
  237. package/src/tools/executors/web-fetch.ts +259 -0
  238. package/src/tools/helpers.ts +130 -0
  239. package/src/tools/index.ts +169 -0
  240. package/src/tools/model-tool-routing.test.ts +86 -0
  241. package/src/tools/model-tool-routing.ts +132 -0
  242. package/src/tools/presets.test.ts +62 -0
  243. package/src/tools/presets.ts +168 -0
  244. package/src/tools/schemas.ts +327 -0
  245. package/src/tools/types.ts +329 -0
  246. package/src/types/common.ts +26 -0
  247. package/src/types/config.ts +86 -0
  248. package/src/types/events.ts +74 -0
  249. package/src/types/index.ts +24 -0
  250. package/src/types/provider-settings.ts +43 -0
  251. package/src/types/sessions.ts +16 -0
  252. package/src/types/storage.ts +64 -0
  253. package/src/types/workspace.ts +7 -0
  254. package/src/types.ts +132 -0
  255. package/src/version.ts +3 -0
  256. package/dist/index.node.d.ts +0 -47
  257. package/dist/index.node.d.ts.map +0 -1
  258. package/dist/index.node.js +0 -948
  259. package/dist/telemetry/opentelemetry.d.ts.map +0 -1
  260. package/dist/telemetry/opentelemetry.js +0 -27
@@ -0,0 +1,521 @@
1
+ import { createHash } from "node:crypto";
2
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
3
+ import { createServer } from "node:net";
4
+ import { dirname, join, resolve } from "node:path";
5
+ import {
6
+ getRpcServerHealth,
7
+ RPC_BUILD_VERSION,
8
+ RPC_PROTOCOL_VERSION,
9
+ RpcSessionClient,
10
+ requestRpcServerShutdown,
11
+ } from "@clinebot/rpc";
12
+ import { resolveClineDataDir } from "@clinebot/shared/storage";
13
+ import { CORE_BUILD_VERSION } from "../version";
14
+
15
+ const RPC_STARTUP_LOCK_MAX_AGE_MS = 30_000;
16
+ const RPC_STARTUP_LOCK_WAIT_MS = 15_000;
17
+ const RPC_STARTUP_LOCK_POLL_MS = 100;
18
+ export const RPC_STARTUP_LOCK_BYPASS_ENV = "CLINE_RPC_STARTUP_LOCK_HELD";
19
+ export const RPC_OWNER_ID_ENV = "CLINE_RPC_OWNER_ID";
20
+ export const RPC_BUILD_ID_ENV = "CLINE_RPC_BUILD_ID";
21
+ export const RPC_DISCOVERY_PATH_ENV = "CLINE_RPC_DISCOVERY_PATH";
22
+
23
+ type RpcStartupLockRecord = {
24
+ pid: number;
25
+ address: string;
26
+ acquiredAt: string;
27
+ };
28
+
29
+ export type RpcDiscoveryRecord = {
30
+ ownerId: string;
31
+ buildId: string;
32
+ entryPath?: string;
33
+ address: string;
34
+ pid?: number;
35
+ serverId?: string;
36
+ startedAt?: string;
37
+ protocolVersion: string;
38
+ updatedAt: string;
39
+ };
40
+
41
+ export type RpcOwnerContext = {
42
+ ownerId: string;
43
+ buildId: string;
44
+ entryPath?: string;
45
+ discoveryPath: string;
46
+ };
47
+
48
+ export type ResolveRpcRuntimeResult = {
49
+ address: string;
50
+ action: "reuse" | "new-port" | "started";
51
+ owner: RpcOwnerContext;
52
+ };
53
+
54
+ export type EnsureRpcRuntimeOptions = {
55
+ owner?: RpcOwnerContext;
56
+ resolveOwner?: () => RpcOwnerContext;
57
+ spawnIfNeeded: (
58
+ address: string,
59
+ owner: RpcOwnerContext,
60
+ ) => void | Promise<void>;
61
+ readinessCheck?: (address: string) => Promise<boolean>;
62
+ };
63
+
64
+ export type ResolveRpcOwnerContextOptions = {
65
+ discoveryPath?: string;
66
+ hostBuildKey?: string;
67
+ identityPath?: string;
68
+ ownerBasis?: string;
69
+ ownerId?: string;
70
+ ownerPrefix?: string;
71
+ };
72
+
73
+ const sleep = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));
74
+
75
+ function sanitizeKey(value: string): string {
76
+ return value.replace(/[^a-zA-Z0-9_.-]+/g, "_");
77
+ }
78
+
79
+ function hashValue(value: string): string {
80
+ return createHash("sha256").update(value).digest("hex").slice(0, 12);
81
+ }
82
+
83
+ function errorCode(error: unknown): string {
84
+ return error && typeof error === "object" && "code" in error
85
+ ? String((error as { code?: unknown }).code)
86
+ : "";
87
+ }
88
+
89
+ function parseRpcAddress(address: string): { host: string; port: number } {
90
+ const trimmed = address.trim();
91
+ const idx = trimmed.lastIndexOf(":");
92
+ if (idx <= 0 || idx >= trimmed.length - 1) {
93
+ throw new Error(`invalid rpc address: ${address}`);
94
+ }
95
+ const host = trimmed.slice(0, idx);
96
+ const port = Number.parseInt(trimmed.slice(idx + 1), 10);
97
+ if (!Number.isInteger(port) || port <= 0 || port > 65535) {
98
+ throw new Error(`invalid rpc port in address: ${address}`);
99
+ }
100
+ return { host, port };
101
+ }
102
+
103
+ function defaultIdentityPath(): string | undefined {
104
+ const entryArg = process.argv[1]?.trim();
105
+ if (!entryArg) return undefined;
106
+ return resolve(process.cwd(), entryArg);
107
+ }
108
+
109
+ export function resolveRpcRuntimeBuildKey(hostBuildKey?: string): string {
110
+ const base = `core=${CORE_BUILD_VERSION}:rpc=${RPC_BUILD_VERSION}`;
111
+ return hostBuildKey?.trim() ? `${base}:host=${hostBuildKey.trim()}` : base;
112
+ }
113
+
114
+ export function resolveRpcOwnerContext(
115
+ options: ResolveRpcOwnerContextOptions = {},
116
+ ): RpcOwnerContext {
117
+ const entryPath = options.identityPath?.trim() || defaultIdentityPath();
118
+ const defaultOwnerBasis =
119
+ options.ownerBasis?.trim() ||
120
+ (entryPath ? `${entryPath}` : `pid:${process.pid}:cwd:${process.cwd()}`);
121
+ const ownerPrefix = options.ownerPrefix?.trim() || "rpc";
122
+ const ownerId =
123
+ options.ownerId?.trim() ||
124
+ process.env[RPC_OWNER_ID_ENV]?.trim() ||
125
+ `${ownerPrefix}-${hashValue(defaultOwnerBasis)}`;
126
+ const defaultBuildBasis = `${defaultOwnerBasis}:${resolveRpcRuntimeBuildKey(options.hostBuildKey)}`;
127
+ const buildId =
128
+ process.env[RPC_BUILD_ID_ENV]?.trim() ||
129
+ `build-${hashValue(defaultBuildBasis)}`;
130
+ const discoveryPath =
131
+ options.discoveryPath?.trim() ||
132
+ process.env[RPC_DISCOVERY_PATH_ENV]?.trim() ||
133
+ join(
134
+ resolveClineDataDir(),
135
+ "rpc",
136
+ "owners",
137
+ `${sanitizeKey(ownerId)}.json`,
138
+ );
139
+ return { ownerId, buildId, entryPath, discoveryPath };
140
+ }
141
+
142
+ async function readRpcDiscovery(
143
+ owner: RpcOwnerContext,
144
+ ): Promise<RpcDiscoveryRecord | undefined> {
145
+ try {
146
+ const parsed = JSON.parse(
147
+ await readFile(owner.discoveryPath, "utf8"),
148
+ ) as Partial<RpcDiscoveryRecord>;
149
+ if (
150
+ typeof parsed.ownerId !== "string" ||
151
+ typeof parsed.buildId !== "string" ||
152
+ typeof parsed.address !== "string" ||
153
+ typeof parsed.protocolVersion !== "string"
154
+ ) {
155
+ return undefined;
156
+ }
157
+ return {
158
+ ownerId: parsed.ownerId,
159
+ buildId: parsed.buildId,
160
+ address: parsed.address,
161
+ protocolVersion: parsed.protocolVersion,
162
+ updatedAt:
163
+ typeof parsed.updatedAt === "string"
164
+ ? parsed.updatedAt
165
+ : new Date().toISOString(),
166
+ entryPath:
167
+ typeof parsed.entryPath === "string" ? parsed.entryPath : undefined,
168
+ pid: typeof parsed.pid === "number" ? parsed.pid : undefined,
169
+ serverId:
170
+ typeof parsed.serverId === "string" ? parsed.serverId : undefined,
171
+ startedAt:
172
+ typeof parsed.startedAt === "string" ? parsed.startedAt : undefined,
173
+ };
174
+ } catch {
175
+ return undefined;
176
+ }
177
+ }
178
+
179
+ export async function recordRpcDiscovery(
180
+ owner: RpcOwnerContext,
181
+ record: Omit<RpcDiscoveryRecord, "ownerId" | "buildId" | "updatedAt">,
182
+ ): Promise<void> {
183
+ await mkdir(dirname(owner.discoveryPath), { recursive: true });
184
+ await writeFile(
185
+ owner.discoveryPath,
186
+ JSON.stringify(
187
+ {
188
+ ownerId: owner.ownerId,
189
+ buildId: owner.buildId,
190
+ updatedAt: new Date().toISOString(),
191
+ ...record,
192
+ } satisfies RpcDiscoveryRecord,
193
+ null,
194
+ 2,
195
+ ),
196
+ "utf8",
197
+ );
198
+ }
199
+
200
+ async function clearRpcDiscovery(owner: RpcOwnerContext): Promise<void> {
201
+ await rm(owner.discoveryPath, { force: true }).catch(() => undefined);
202
+ }
203
+
204
+ export async function clearRpcDiscoveryIfAddressMatches(
205
+ owner: RpcOwnerContext,
206
+ address: string,
207
+ ): Promise<void> {
208
+ const current = await readRpcDiscovery(owner);
209
+ if (current?.address === address) await clearRpcDiscovery(owner);
210
+ }
211
+
212
+ function isHealthCompatible(
213
+ health: Awaited<ReturnType<typeof getRpcServerHealth>> | undefined,
214
+ ): boolean {
215
+ const serverVersion = health?.rpcVersion?.trim() || "";
216
+ return !!serverVersion && serverVersion === RPC_PROTOCOL_VERSION;
217
+ }
218
+
219
+ function isUnimplementedError(error: unknown): boolean {
220
+ if (Number(errorCode(error)) === 12) return true;
221
+ const message = error instanceof Error ? error.message : String(error);
222
+ return message.toUpperCase().includes("UNIMPLEMENTED");
223
+ }
224
+
225
+ function isAuthenticationError(error: unknown): boolean {
226
+ const message = error instanceof Error ? error.message : String(error);
227
+ return /missing authentication header|401\b|unauthenticated|unauthorized/i.test(
228
+ message,
229
+ );
230
+ }
231
+
232
+ function isProbeBlocked(error: unknown): boolean {
233
+ return isUnimplementedError(error) || isAuthenticationError(error);
234
+ }
235
+
236
+ async function hasRuntimeMethods(address: string): Promise<boolean> {
237
+ const client = new RpcSessionClient({ address });
238
+ try {
239
+ try {
240
+ await client.stopRuntimeSession("__rpc_probe__");
241
+ } catch (error) {
242
+ if (isProbeBlocked(error)) return false;
243
+ }
244
+ return true;
245
+ } catch (error) {
246
+ return !isProbeBlocked(error);
247
+ } finally {
248
+ client.close();
249
+ }
250
+ }
251
+
252
+ export async function isCompatibleRuntime(address: string): Promise<boolean> {
253
+ const health = await getRpcServerHealth(address);
254
+ return (
255
+ !!health?.running &&
256
+ isHealthCompatible(health) &&
257
+ (await hasRuntimeMethods(address))
258
+ );
259
+ }
260
+
261
+ async function isPortFree(host: string, port: number): Promise<boolean> {
262
+ return new Promise<boolean>((resolve) => {
263
+ const server = createServer();
264
+ server.once("error", () => resolve(false));
265
+ server.once("listening", () => server.close(() => resolve(true)));
266
+ server.listen({ host, port });
267
+ });
268
+ }
269
+
270
+ async function findAvailableAddress(baseAddress: string): Promise<string> {
271
+ const { host, port } = parseRpcAddress(baseAddress);
272
+ for (let offset = 1; offset <= 40; offset += 1) {
273
+ const candidate = port + offset;
274
+ if (candidate > 65535) break;
275
+ if (await isPortFree(host, candidate)) return `${host}:${candidate}`;
276
+ }
277
+ throw new Error(`no available rpc port near ${baseAddress}`);
278
+ }
279
+
280
+ function getRpcStartupLockDir(address: string): string {
281
+ const normalized = address.trim().replace(/[^a-zA-Z0-9_.-]+/g, "_");
282
+ return join(resolveClineDataDir(), "locks", `rpc-start-${normalized}.lock`);
283
+ }
284
+
285
+ function isPidAlive(pid: number | undefined): boolean {
286
+ if (!Number.isInteger(pid) || !pid || pid <= 0) return false;
287
+ try {
288
+ process.kill(pid, 0);
289
+ return true;
290
+ } catch (error) {
291
+ return errorCode(error) === "EPERM";
292
+ }
293
+ }
294
+
295
+ async function writeRpcStartupLockRecord(
296
+ lockDir: string,
297
+ address: string,
298
+ ): Promise<void> {
299
+ const record: RpcStartupLockRecord = {
300
+ pid: process.pid,
301
+ address,
302
+ acquiredAt: new Date().toISOString(),
303
+ };
304
+ await writeFile(
305
+ join(lockDir, "owner.json"),
306
+ JSON.stringify(record, null, 2),
307
+ "utf8",
308
+ );
309
+ }
310
+
311
+ async function readRpcStartupLockRecord(
312
+ lockDir: string,
313
+ ): Promise<RpcStartupLockRecord | undefined> {
314
+ try {
315
+ const parsed = JSON.parse(
316
+ await readFile(join(lockDir, "owner.json"), "utf8"),
317
+ ) as Partial<RpcStartupLockRecord>;
318
+ if (
319
+ typeof parsed.pid !== "number" ||
320
+ typeof parsed.address !== "string" ||
321
+ typeof parsed.acquiredAt !== "string"
322
+ ) {
323
+ return undefined;
324
+ }
325
+ return {
326
+ pid: parsed.pid,
327
+ address: parsed.address,
328
+ acquiredAt: parsed.acquiredAt,
329
+ };
330
+ } catch {
331
+ return undefined;
332
+ }
333
+ }
334
+
335
+ async function removeRpcStartupLockDir(lockDir: string): Promise<void> {
336
+ await rm(lockDir, { recursive: true, force: true }).catch(() => {});
337
+ }
338
+
339
+ export async function withRpcStartupLock<T>(
340
+ address: string,
341
+ action: () => Promise<T>,
342
+ ): Promise<T> {
343
+ if (process.env[RPC_STARTUP_LOCK_BYPASS_ENV] === "1") {
344
+ return await action();
345
+ }
346
+
347
+ const lockDir = getRpcStartupLockDir(address);
348
+ const startedAt = Date.now();
349
+ await mkdir(dirname(lockDir), { recursive: true });
350
+
351
+ while (true) {
352
+ try {
353
+ await mkdir(lockDir, { recursive: false });
354
+ await writeRpcStartupLockRecord(lockDir, address);
355
+ try {
356
+ return await action();
357
+ } finally {
358
+ await removeRpcStartupLockDir(lockDir);
359
+ }
360
+ } catch (error) {
361
+ if (errorCode(error) !== "EEXIST") throw error;
362
+
363
+ const existing = await readRpcStartupLockRecord(lockDir);
364
+ const acquiredAtMs = existing
365
+ ? new Date(existing.acquiredAt).getTime()
366
+ : Number.NaN;
367
+ const isStale =
368
+ !existing ||
369
+ !Number.isFinite(acquiredAtMs) ||
370
+ Date.now() - acquiredAtMs > RPC_STARTUP_LOCK_MAX_AGE_MS ||
371
+ !isPidAlive(existing.pid);
372
+ if (isStale) {
373
+ await removeRpcStartupLockDir(lockDir);
374
+ continue;
375
+ }
376
+
377
+ if (Date.now() - startedAt >= RPC_STARTUP_LOCK_WAIT_MS) {
378
+ throw new Error(
379
+ `timed out waiting for rpc startup lock at ${address} (owner pid=${existing.pid})`,
380
+ );
381
+ }
382
+ await sleep(RPC_STARTUP_LOCK_POLL_MS);
383
+ }
384
+ }
385
+ }
386
+
387
+ async function tryShutdownOwnedServer(
388
+ owner: RpcOwnerContext,
389
+ discovery: RpcDiscoveryRecord | undefined,
390
+ ): Promise<void> {
391
+ const address = discovery?.address?.trim();
392
+ if (!address) {
393
+ await clearRpcDiscovery(owner);
394
+ return;
395
+ }
396
+ const shutdown = await requestRpcServerShutdown(address).catch(
397
+ () => undefined,
398
+ );
399
+ if (shutdown?.accepted) {
400
+ for (let i = 0; i < 20; i += 1) {
401
+ if (!(await getRpcServerHealth(address))?.running) {
402
+ await clearRpcDiscovery(owner);
403
+ return;
404
+ }
405
+ await sleep(100);
406
+ }
407
+ }
408
+ if (!(await getRpcServerHealth(address))?.running) {
409
+ await clearRpcDiscovery(owner);
410
+ }
411
+ }
412
+
413
+ export async function resolveEnsuredRpcRuntime(
414
+ requestedAddress: string,
415
+ options: {
416
+ owner?: RpcOwnerContext;
417
+ resolveOwner?: () => RpcOwnerContext;
418
+ } = {},
419
+ ): Promise<ResolveRpcRuntimeResult> {
420
+ const owner =
421
+ options.owner ?? options.resolveOwner?.() ?? resolveRpcOwnerContext();
422
+ return await withRpcStartupLock(requestedAddress, async () => {
423
+ const discovery = await readRpcDiscovery(owner);
424
+ if (discovery?.address) {
425
+ const discoveredHealth = await getRpcServerHealth(discovery.address);
426
+ if (
427
+ discoveredHealth?.running &&
428
+ discovery.buildId === owner.buildId &&
429
+ discovery.protocolVersion === RPC_PROTOCOL_VERSION &&
430
+ isHealthCompatible(discoveredHealth) &&
431
+ (await hasRuntimeMethods(discovery.address))
432
+ ) {
433
+ return {
434
+ address: discovery.address,
435
+ action: "reuse",
436
+ owner,
437
+ } satisfies ResolveRpcRuntimeResult;
438
+ }
439
+ await tryShutdownOwnedServer(owner, discovery);
440
+ }
441
+
442
+ const requestedHealth = await getRpcServerHealth(requestedAddress);
443
+ const { host, port } = parseRpcAddress(requestedAddress);
444
+
445
+ if (!requestedHealth?.running) {
446
+ if (await isPortFree(host, port)) {
447
+ return {
448
+ address: requestedAddress,
449
+ action: "started",
450
+ owner,
451
+ } satisfies ResolveRpcRuntimeResult;
452
+ }
453
+ return {
454
+ address: await findAvailableAddress(requestedAddress),
455
+ action: "new-port",
456
+ owner,
457
+ } satisfies ResolveRpcRuntimeResult;
458
+ }
459
+
460
+ if (
461
+ isHealthCompatible(requestedHealth) &&
462
+ (await hasRuntimeMethods(requestedAddress))
463
+ ) {
464
+ if (!discovery) {
465
+ return {
466
+ address: requestedAddress,
467
+ action: "reuse",
468
+ owner,
469
+ } satisfies ResolveRpcRuntimeResult;
470
+ }
471
+ }
472
+
473
+ return {
474
+ address: await findAvailableAddress(requestedAddress),
475
+ action: "new-port",
476
+ owner,
477
+ } satisfies ResolveRpcRuntimeResult;
478
+ });
479
+ }
480
+
481
+ export async function waitForCompatibleRpcRuntime(
482
+ address: string,
483
+ readinessCheck: (address: string) => Promise<boolean> = isCompatibleRuntime,
484
+ ): Promise<boolean> {
485
+ for (let attempt = 0; attempt < 60; attempt += 1) {
486
+ if (await readinessCheck(address)) return true;
487
+ await sleep(100);
488
+ }
489
+ return false;
490
+ }
491
+
492
+ export async function ensureRpcRuntimeAddress(
493
+ requestedAddress: string,
494
+ options: EnsureRpcRuntimeOptions,
495
+ ): Promise<ResolveRpcRuntimeResult> {
496
+ const resolved = await resolveEnsuredRpcRuntime(requestedAddress, {
497
+ owner: options.owner,
498
+ resolveOwner: options.resolveOwner,
499
+ });
500
+ if (resolved.action !== "reuse") {
501
+ await options.spawnIfNeeded(resolved.address, resolved.owner);
502
+ }
503
+ if (
504
+ !(await waitForCompatibleRpcRuntime(
505
+ resolved.address,
506
+ options.readinessCheck,
507
+ ))
508
+ ) {
509
+ throw new Error(`failed to ensure rpc runtime at ${resolved.address}`);
510
+ }
511
+ const health = await getRpcServerHealth(resolved.address);
512
+ await recordRpcDiscovery(resolved.owner, {
513
+ address: resolved.address,
514
+ pid: undefined,
515
+ serverId: health?.serverId,
516
+ startedAt: health?.startedAt,
517
+ protocolVersion: RPC_PROTOCOL_VERSION,
518
+ entryPath: resolved.owner.entryPath,
519
+ });
520
+ return resolved;
521
+ }
@@ -0,0 +1,107 @@
1
+ import { existsSync, mkdirSync } from "node:fs";
2
+ import { RpcSessionClient, type RpcSessionRow } from "@clinebot/rpc";
3
+ import type { SessionRow } from "./session-service";
4
+ import type {
5
+ PersistedSessionUpdateInput,
6
+ SessionPersistenceAdapter,
7
+ } from "./unified-session-persistence-service";
8
+ import { UnifiedSessionPersistenceService } from "./unified-session-persistence-service";
9
+
10
+ // ── Adapter ──────────────────────────────────────────────────────────
11
+
12
+ class RpcSessionPersistenceAdapter implements SessionPersistenceAdapter {
13
+ constructor(private readonly client: RpcSessionClient) {}
14
+
15
+ ensureSessionsDir(): string {
16
+ return "";
17
+ }
18
+
19
+ async upsertSession(row: SessionRow): Promise<void> {
20
+ await this.client.upsertSession(row as RpcSessionRow);
21
+ }
22
+
23
+ async getSession(sessionId: string): Promise<SessionRow | undefined> {
24
+ const row = await this.client.getSession(sessionId);
25
+ return (row as SessionRow | undefined) ?? undefined;
26
+ }
27
+
28
+ async listSessions(options: {
29
+ limit: number;
30
+ parentSessionId?: string;
31
+ status?: string;
32
+ }): Promise<SessionRow[]> {
33
+ const rows = await this.client.listSessions(options);
34
+ return rows as SessionRow[];
35
+ }
36
+
37
+ async updateSession(
38
+ input: PersistedSessionUpdateInput,
39
+ ): Promise<{ updated: boolean; statusLock: number }> {
40
+ return this.client.updateSession({
41
+ sessionId: input.sessionId,
42
+ status: input.status,
43
+ endedAt: input.endedAt,
44
+ exitCode: input.exitCode,
45
+ prompt: input.prompt,
46
+ metadata: input.metadata,
47
+ parentSessionId: input.parentSessionId,
48
+ parentAgentId: input.parentAgentId,
49
+ agentId: input.agentId,
50
+ conversationId: input.conversationId,
51
+ expectedStatusLock: input.expectedStatusLock,
52
+ setRunning: input.setRunning,
53
+ });
54
+ }
55
+
56
+ async deleteSession(sessionId: string, cascade: boolean): Promise<boolean> {
57
+ return this.client.deleteSession(sessionId, cascade);
58
+ }
59
+
60
+ async enqueueSpawnRequest(input: {
61
+ rootSessionId: string;
62
+ parentAgentId: string;
63
+ task?: string;
64
+ systemPrompt?: string;
65
+ }): Promise<void> {
66
+ await this.client.enqueueSpawnRequest(input);
67
+ }
68
+
69
+ async claimSpawnRequest(
70
+ rootSessionId: string,
71
+ parentAgentId: string,
72
+ ): Promise<string | undefined> {
73
+ return this.client.claimSpawnRequest(rootSessionId, parentAgentId);
74
+ }
75
+ }
76
+
77
+ // ── Service ──────────────────────────────────────────────────────────
78
+
79
+ export interface RpcCoreSessionServiceOptions {
80
+ address?: string;
81
+ sessionsDir: string;
82
+ }
83
+
84
+ export class RpcCoreSessionService extends UnifiedSessionPersistenceService {
85
+ private readonly sessionsDirPath: string;
86
+ private readonly client: RpcSessionClient;
87
+
88
+ constructor(options: RpcCoreSessionServiceOptions) {
89
+ const client = new RpcSessionClient({
90
+ address: options.address?.trim() || "127.0.0.1:4317",
91
+ });
92
+ super(new RpcSessionPersistenceAdapter(client));
93
+ this.sessionsDirPath = options.sessionsDir;
94
+ this.client = client;
95
+ }
96
+
97
+ override ensureSessionsDir(): string {
98
+ if (!existsSync(this.sessionsDirPath)) {
99
+ mkdirSync(this.sessionsDirPath, { recursive: true });
100
+ }
101
+ return this.sessionsDirPath;
102
+ }
103
+
104
+ close(): void {
105
+ this.client.close();
106
+ }
107
+ }
@@ -0,0 +1,49 @@
1
+ import { mkdtempSync, rmSync } from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { afterEach, describe, expect, it } from "vitest";
5
+ import { tryAcquireRpcSpawnLease } from "./rpc-spawn-lease";
6
+
7
+ describe("tryAcquireRpcSpawnLease", () => {
8
+ const tempDirs: string[] = [];
9
+
10
+ afterEach(() => {
11
+ delete process.env.CLINE_DATA_DIR;
12
+ for (const dir of tempDirs.splice(0)) {
13
+ rmSync(dir, { recursive: true, force: true });
14
+ }
15
+ });
16
+
17
+ it("allows only one active lease per address", () => {
18
+ const dataDir = mkdtempSync(path.join(os.tmpdir(), "rpc-spawn-lease-"));
19
+ tempDirs.push(dataDir);
20
+ process.env.CLINE_DATA_DIR = dataDir;
21
+
22
+ const first = tryAcquireRpcSpawnLease("127.0.0.1:4317");
23
+ const second = tryAcquireRpcSpawnLease("127.0.0.1:4317");
24
+
25
+ expect(first).toBeDefined();
26
+ expect(second).toBeUndefined();
27
+
28
+ first?.release();
29
+
30
+ const third = tryAcquireRpcSpawnLease("127.0.0.1:4317");
31
+ expect(third).toBeDefined();
32
+ third?.release();
33
+ });
34
+
35
+ it("lets different addresses acquire independent leases", () => {
36
+ const dataDir = mkdtempSync(path.join(os.tmpdir(), "rpc-spawn-lease-"));
37
+ tempDirs.push(dataDir);
38
+ process.env.CLINE_DATA_DIR = dataDir;
39
+
40
+ const first = tryAcquireRpcSpawnLease("127.0.0.1:4317");
41
+ const second = tryAcquireRpcSpawnLease("127.0.0.1:4318");
42
+
43
+ expect(first).toBeDefined();
44
+ expect(second).toBeDefined();
45
+
46
+ first?.release();
47
+ second?.release();
48
+ });
49
+ });