@clinebot/core 0.0.0

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 (200) hide show
  1. package/README.md +88 -0
  2. package/dist/account/cline-account-service.d.ts +34 -0
  3. package/dist/account/index.d.ts +3 -0
  4. package/dist/account/rpc.d.ts +38 -0
  5. package/dist/account/types.d.ts +74 -0
  6. package/dist/agents/agent-config-loader.d.ts +18 -0
  7. package/dist/agents/agent-config-parser.d.ts +25 -0
  8. package/dist/agents/hooks-config-loader.d.ts +23 -0
  9. package/dist/agents/index.d.ts +11 -0
  10. package/dist/agents/plugin-config-loader.d.ts +22 -0
  11. package/dist/agents/plugin-loader.d.ts +9 -0
  12. package/dist/agents/plugin-sandbox.d.ts +12 -0
  13. package/dist/agents/unified-config-file-watcher.d.ts +77 -0
  14. package/dist/agents/user-instruction-config-loader.d.ts +63 -0
  15. package/dist/auth/client.d.ts +11 -0
  16. package/dist/auth/cline.d.ts +41 -0
  17. package/dist/auth/codex.d.ts +39 -0
  18. package/dist/auth/oca.d.ts +22 -0
  19. package/dist/auth/server.d.ts +22 -0
  20. package/dist/auth/types.d.ts +72 -0
  21. package/dist/auth/utils.d.ts +32 -0
  22. package/dist/chat/chat-schema.d.ts +145 -0
  23. package/dist/default-tools/constants.d.ts +23 -0
  24. package/dist/default-tools/definitions.d.ts +96 -0
  25. package/dist/default-tools/executors/apply-patch-parser.d.ts +68 -0
  26. package/dist/default-tools/executors/apply-patch.d.ts +26 -0
  27. package/dist/default-tools/executors/bash.d.ts +49 -0
  28. package/dist/default-tools/executors/editor.d.ts +31 -0
  29. package/dist/default-tools/executors/file-read.d.ts +40 -0
  30. package/dist/default-tools/executors/index.d.ts +44 -0
  31. package/dist/default-tools/executors/search.d.ts +50 -0
  32. package/dist/default-tools/executors/web-fetch.d.ts +58 -0
  33. package/dist/default-tools/index.d.ts +57 -0
  34. package/dist/default-tools/presets.d.ts +124 -0
  35. package/dist/default-tools/schemas.d.ts +121 -0
  36. package/dist/default-tools/types.d.ts +237 -0
  37. package/dist/index.d.ts +23 -0
  38. package/dist/index.js +220 -0
  39. package/dist/input/file-indexer.d.ts +5 -0
  40. package/dist/input/index.d.ts +4 -0
  41. package/dist/input/mention-enricher.d.ts +12 -0
  42. package/dist/mcp/config-loader.d.ts +15 -0
  43. package/dist/mcp/index.d.ts +4 -0
  44. package/dist/mcp/manager.d.ts +24 -0
  45. package/dist/mcp/types.d.ts +66 -0
  46. package/dist/runtime/hook-file-hooks.d.ts +18 -0
  47. package/dist/runtime/rules.d.ts +5 -0
  48. package/dist/runtime/runtime-builder.d.ts +5 -0
  49. package/dist/runtime/sandbox/subprocess-sandbox.d.ts +19 -0
  50. package/dist/runtime/session-runtime.d.ts +36 -0
  51. package/dist/runtime/tool-approval.d.ts +9 -0
  52. package/dist/runtime/workflows.d.ts +13 -0
  53. package/dist/server/index.d.ts +47 -0
  54. package/dist/server/index.js +641 -0
  55. package/dist/session/default-session-manager.d.ts +77 -0
  56. package/dist/session/rpc-session-service.d.ts +12 -0
  57. package/dist/session/runtime-oauth-token-manager.d.ts +28 -0
  58. package/dist/session/session-artifacts.d.ts +19 -0
  59. package/dist/session/session-graph.d.ts +15 -0
  60. package/dist/session/session-host.d.ts +21 -0
  61. package/dist/session/session-manager.d.ts +50 -0
  62. package/dist/session/session-manifest.d.ts +30 -0
  63. package/dist/session/session-service.d.ts +113 -0
  64. package/dist/session/sqlite-rpc-session-backend.d.ts +30 -0
  65. package/dist/session/unified-session-persistence-service.d.ts +93 -0
  66. package/dist/session/workspace-manager.d.ts +28 -0
  67. package/dist/session/workspace-manifest.d.ts +25 -0
  68. package/dist/storage/provider-settings-legacy-migration.d.ts +13 -0
  69. package/dist/storage/provider-settings-manager.d.ts +20 -0
  70. package/dist/storage/sqlite-session-store.d.ts +29 -0
  71. package/dist/storage/sqlite-team-store.d.ts +31 -0
  72. package/dist/storage/team-store.d.ts +2 -0
  73. package/dist/team/index.d.ts +1 -0
  74. package/dist/team/projections.d.ts +8 -0
  75. package/dist/types/common.d.ts +10 -0
  76. package/dist/types/config.d.ts +37 -0
  77. package/dist/types/events.d.ts +54 -0
  78. package/dist/types/provider-settings.d.ts +20 -0
  79. package/dist/types/sessions.d.ts +9 -0
  80. package/dist/types/storage.d.ts +37 -0
  81. package/dist/types/workspace.d.ts +7 -0
  82. package/dist/types.d.ts +26 -0
  83. package/package.json +63 -0
  84. package/src/account/cline-account-service.test.ts +101 -0
  85. package/src/account/cline-account-service.ts +267 -0
  86. package/src/account/index.ts +20 -0
  87. package/src/account/rpc.test.ts +62 -0
  88. package/src/account/rpc.ts +172 -0
  89. package/src/account/types.ts +80 -0
  90. package/src/agents/agent-config-loader.test.ts +234 -0
  91. package/src/agents/agent-config-loader.ts +107 -0
  92. package/src/agents/agent-config-parser.ts +191 -0
  93. package/src/agents/hooks-config-loader.ts +97 -0
  94. package/src/agents/index.ts +84 -0
  95. package/src/agents/plugin-config-loader.test.ts +91 -0
  96. package/src/agents/plugin-config-loader.ts +160 -0
  97. package/src/agents/plugin-loader.test.ts +102 -0
  98. package/src/agents/plugin-loader.ts +105 -0
  99. package/src/agents/plugin-sandbox.test.ts +120 -0
  100. package/src/agents/plugin-sandbox.ts +471 -0
  101. package/src/agents/unified-config-file-watcher.test.ts +196 -0
  102. package/src/agents/unified-config-file-watcher.ts +483 -0
  103. package/src/agents/user-instruction-config-loader.test.ts +158 -0
  104. package/src/agents/user-instruction-config-loader.ts +438 -0
  105. package/src/auth/client.test.ts +40 -0
  106. package/src/auth/client.ts +25 -0
  107. package/src/auth/cline.test.ts +130 -0
  108. package/src/auth/cline.ts +414 -0
  109. package/src/auth/codex.test.ts +170 -0
  110. package/src/auth/codex.ts +466 -0
  111. package/src/auth/oca.test.ts +215 -0
  112. package/src/auth/oca.ts +546 -0
  113. package/src/auth/server.ts +216 -0
  114. package/src/auth/types.ts +78 -0
  115. package/src/auth/utils.test.ts +128 -0
  116. package/src/auth/utils.ts +247 -0
  117. package/src/chat/chat-schema.ts +82 -0
  118. package/src/default-tools/constants.ts +35 -0
  119. package/src/default-tools/definitions.test.ts +233 -0
  120. package/src/default-tools/definitions.ts +632 -0
  121. package/src/default-tools/executors/apply-patch-parser.ts +520 -0
  122. package/src/default-tools/executors/apply-patch.ts +359 -0
  123. package/src/default-tools/executors/bash.ts +205 -0
  124. package/src/default-tools/executors/editor.ts +231 -0
  125. package/src/default-tools/executors/file-read.test.ts +25 -0
  126. package/src/default-tools/executors/file-read.ts +94 -0
  127. package/src/default-tools/executors/index.ts +75 -0
  128. package/src/default-tools/executors/search.ts +278 -0
  129. package/src/default-tools/executors/web-fetch.ts +259 -0
  130. package/src/default-tools/index.ts +161 -0
  131. package/src/default-tools/presets.test.ts +63 -0
  132. package/src/default-tools/presets.ts +168 -0
  133. package/src/default-tools/schemas.ts +228 -0
  134. package/src/default-tools/types.ts +324 -0
  135. package/src/index.ts +119 -0
  136. package/src/input/file-indexer.d.ts +11 -0
  137. package/src/input/file-indexer.test.ts +87 -0
  138. package/src/input/file-indexer.ts +280 -0
  139. package/src/input/index.ts +7 -0
  140. package/src/input/mention-enricher.test.ts +82 -0
  141. package/src/input/mention-enricher.ts +119 -0
  142. package/src/mcp/config-loader.test.ts +238 -0
  143. package/src/mcp/config-loader.ts +219 -0
  144. package/src/mcp/index.ts +26 -0
  145. package/src/mcp/manager.test.ts +106 -0
  146. package/src/mcp/manager.ts +262 -0
  147. package/src/mcp/types.ts +88 -0
  148. package/src/runtime/hook-file-hooks.test.ts +106 -0
  149. package/src/runtime/hook-file-hooks.ts +736 -0
  150. package/src/runtime/index.ts +27 -0
  151. package/src/runtime/rules.ts +34 -0
  152. package/src/runtime/runtime-builder.team-persistence.test.ts +203 -0
  153. package/src/runtime/runtime-builder.test.ts +215 -0
  154. package/src/runtime/runtime-builder.ts +515 -0
  155. package/src/runtime/runtime-parity.test.ts +132 -0
  156. package/src/runtime/sandbox/subprocess-sandbox.ts +207 -0
  157. package/src/runtime/session-runtime.ts +44 -0
  158. package/src/runtime/tool-approval.ts +104 -0
  159. package/src/runtime/workflows.test.ts +119 -0
  160. package/src/runtime/workflows.ts +54 -0
  161. package/src/server/index.ts +282 -0
  162. package/src/session/default-session-manager.e2e.test.ts +354 -0
  163. package/src/session/default-session-manager.test.ts +816 -0
  164. package/src/session/default-session-manager.ts +1286 -0
  165. package/src/session/index.ts +37 -0
  166. package/src/session/rpc-session-service.ts +189 -0
  167. package/src/session/runtime-oauth-token-manager.test.ts +137 -0
  168. package/src/session/runtime-oauth-token-manager.ts +265 -0
  169. package/src/session/session-artifacts.ts +106 -0
  170. package/src/session/session-graph.ts +90 -0
  171. package/src/session/session-host.ts +190 -0
  172. package/src/session/session-manager.ts +56 -0
  173. package/src/session/session-manifest.ts +29 -0
  174. package/src/session/session-service.team-persistence.test.ts +48 -0
  175. package/src/session/session-service.ts +610 -0
  176. package/src/session/sqlite-rpc-session-backend.ts +303 -0
  177. package/src/session/unified-session-persistence-service.ts +781 -0
  178. package/src/session/workspace-manager.ts +98 -0
  179. package/src/session/workspace-manifest.ts +100 -0
  180. package/src/storage/artifact-store.ts +1 -0
  181. package/src/storage/index.ts +11 -0
  182. package/src/storage/provider-settings-legacy-migration.test.ts +175 -0
  183. package/src/storage/provider-settings-legacy-migration.ts +637 -0
  184. package/src/storage/provider-settings-manager.test.ts +111 -0
  185. package/src/storage/provider-settings-manager.ts +129 -0
  186. package/src/storage/session-store.ts +1 -0
  187. package/src/storage/sqlite-session-store.ts +270 -0
  188. package/src/storage/sqlite-team-store.ts +443 -0
  189. package/src/storage/team-store.ts +5 -0
  190. package/src/team/index.ts +4 -0
  191. package/src/team/projections.ts +285 -0
  192. package/src/types/common.ts +14 -0
  193. package/src/types/config.ts +64 -0
  194. package/src/types/events.ts +46 -0
  195. package/src/types/index.ts +24 -0
  196. package/src/types/provider-settings.ts +43 -0
  197. package/src/types/sessions.ts +16 -0
  198. package/src/types/storage.ts +64 -0
  199. package/src/types/workspace.ts +7 -0
  200. package/src/types.ts +127 -0
@@ -0,0 +1,62 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import {
3
+ type ClineAccountOperations,
4
+ executeRpcClineAccountAction,
5
+ RpcClineAccountService,
6
+ } from "./rpc";
7
+
8
+ describe("executeRpcClineAccountAction", () => {
9
+ it("dispatches fetchMe", async () => {
10
+ const service: ClineAccountOperations = {
11
+ fetchMe: vi.fn(async () => ({
12
+ id: "u1",
13
+ email: "user1@example.com",
14
+ displayName: "User 1",
15
+ photoUrl: "",
16
+ createdAt: "2025-01-01T00:00:00Z",
17
+ updatedAt: "2025-01-01T00:00:00Z",
18
+ organizations: [],
19
+ })),
20
+ fetchBalance: vi.fn(async () => ({ balance: 1, userId: "u1" })),
21
+ fetchUsageTransactions: vi.fn(async () => []),
22
+ fetchPaymentTransactions: vi.fn(async () => []),
23
+ fetchUserOrganizations: vi.fn(async () => []),
24
+ fetchOrganizationBalance: vi.fn(async () => ({
25
+ balance: 1,
26
+ organizationId: "org-1",
27
+ })),
28
+ fetchOrganizationUsageTransactions: vi.fn(async () => []),
29
+ switchAccount: vi.fn(async () => {}),
30
+ };
31
+
32
+ const result = await executeRpcClineAccountAction(
33
+ { action: "clineAccount", operation: "fetchMe" },
34
+ service,
35
+ );
36
+ expect(service.fetchMe).toHaveBeenCalledTimes(1);
37
+ expect(result).toMatchObject({ id: "u1" });
38
+ });
39
+ });
40
+
41
+ describe("RpcClineAccountService", () => {
42
+ it("sends provider action payload and parses response", async () => {
43
+ const runProviderAction = vi.fn(async (request: unknown) => {
44
+ const parsed = request as {
45
+ action: string;
46
+ operation: string;
47
+ };
48
+ expect(parsed).toEqual({
49
+ action: "clineAccount",
50
+ operation: "fetchMe",
51
+ });
52
+ return {
53
+ result: { id: "u2", email: "u2@example.com" },
54
+ };
55
+ });
56
+ const service = new RpcClineAccountService({ runProviderAction });
57
+
58
+ const me = await service.fetchMe();
59
+ expect(runProviderAction).toHaveBeenCalledTimes(1);
60
+ expect(me).toEqual({ id: "u2", email: "u2@example.com" });
61
+ });
62
+ });
@@ -0,0 +1,172 @@
1
+ import type {
2
+ RpcClineAccountActionRequest,
3
+ RpcProviderActionRequest,
4
+ } from "@clinebot/shared";
5
+ import type {
6
+ ClineAccountBalance,
7
+ ClineAccountOrganization,
8
+ ClineAccountOrganizationBalance,
9
+ ClineAccountOrganizationUsageTransaction,
10
+ ClineAccountPaymentTransaction,
11
+ ClineAccountUsageTransaction,
12
+ ClineAccountUser,
13
+ } from "./types";
14
+
15
+ export interface ClineAccountOperations {
16
+ fetchMe(): Promise<ClineAccountUser>;
17
+ fetchBalance(userId?: string): Promise<ClineAccountBalance>;
18
+ fetchUsageTransactions(
19
+ userId?: string,
20
+ ): Promise<ClineAccountUsageTransaction[]>;
21
+ fetchPaymentTransactions(
22
+ userId?: string,
23
+ ): Promise<ClineAccountPaymentTransaction[]>;
24
+ fetchUserOrganizations(): Promise<ClineAccountOrganization[]>;
25
+ fetchOrganizationBalance(
26
+ organizationId: string,
27
+ ): Promise<ClineAccountOrganizationBalance>;
28
+ fetchOrganizationUsageTransactions(input: {
29
+ organizationId: string;
30
+ memberId?: string;
31
+ }): Promise<ClineAccountOrganizationUsageTransaction[]>;
32
+ switchAccount(organizationId?: string | null): Promise<void>;
33
+ }
34
+
35
+ export function isRpcClineAccountActionRequest(
36
+ request: RpcProviderActionRequest,
37
+ ): request is RpcClineAccountActionRequest {
38
+ return request.action === "clineAccount";
39
+ }
40
+
41
+ export async function executeRpcClineAccountAction(
42
+ request: RpcClineAccountActionRequest,
43
+ service: ClineAccountOperations,
44
+ ): Promise<unknown> {
45
+ switch (request.operation) {
46
+ case "fetchMe":
47
+ return service.fetchMe();
48
+ case "fetchBalance":
49
+ return service.fetchBalance(request.userId);
50
+ case "fetchUsageTransactions":
51
+ return service.fetchUsageTransactions(request.userId);
52
+ case "fetchPaymentTransactions":
53
+ return service.fetchPaymentTransactions(request.userId);
54
+ case "fetchUserOrganizations":
55
+ return service.fetchUserOrganizations();
56
+ case "fetchOrganizationBalance":
57
+ return service.fetchOrganizationBalance(request.organizationId);
58
+ case "fetchOrganizationUsageTransactions":
59
+ return service.fetchOrganizationUsageTransactions({
60
+ organizationId: request.organizationId,
61
+ memberId: request.memberId,
62
+ });
63
+ case "switchAccount":
64
+ await service.switchAccount(request.organizationId);
65
+ return { updated: true };
66
+ default: {
67
+ const exhaustive: never = request;
68
+ throw new Error(
69
+ `Unsupported Cline account operation: ${String(exhaustive)}`,
70
+ );
71
+ }
72
+ }
73
+ }
74
+
75
+ export interface RpcProviderActionExecutor {
76
+ runProviderAction(request: RpcProviderActionRequest): Promise<{
77
+ result: unknown;
78
+ }>;
79
+ }
80
+
81
+ export class RpcClineAccountService implements ClineAccountOperations {
82
+ private readonly executor: RpcProviderActionExecutor;
83
+
84
+ constructor(executor: RpcProviderActionExecutor) {
85
+ this.executor = executor;
86
+ }
87
+
88
+ public async fetchMe(): Promise<ClineAccountUser> {
89
+ return this.request<ClineAccountUser>({
90
+ action: "clineAccount",
91
+ operation: "fetchMe",
92
+ });
93
+ }
94
+
95
+ public async fetchBalance(userId?: string): Promise<ClineAccountBalance> {
96
+ return this.request<ClineAccountBalance>({
97
+ action: "clineAccount",
98
+ operation: "fetchBalance",
99
+ ...(userId?.trim() ? { userId: userId.trim() } : {}),
100
+ });
101
+ }
102
+
103
+ public async fetchUsageTransactions(
104
+ userId?: string,
105
+ ): Promise<ClineAccountUsageTransaction[]> {
106
+ return this.request<ClineAccountUsageTransaction[]>({
107
+ action: "clineAccount",
108
+ operation: "fetchUsageTransactions",
109
+ ...(userId?.trim() ? { userId: userId.trim() } : {}),
110
+ });
111
+ }
112
+
113
+ public async fetchPaymentTransactions(
114
+ userId?: string,
115
+ ): Promise<ClineAccountPaymentTransaction[]> {
116
+ return this.request<ClineAccountPaymentTransaction[]>({
117
+ action: "clineAccount",
118
+ operation: "fetchPaymentTransactions",
119
+ ...(userId?.trim() ? { userId: userId.trim() } : {}),
120
+ });
121
+ }
122
+
123
+ public async fetchUserOrganizations(): Promise<ClineAccountOrganization[]> {
124
+ return this.request<ClineAccountOrganization[]>({
125
+ action: "clineAccount",
126
+ operation: "fetchUserOrganizations",
127
+ });
128
+ }
129
+
130
+ public async fetchOrganizationBalance(
131
+ organizationId: string,
132
+ ): Promise<ClineAccountOrganizationBalance> {
133
+ const orgId = organizationId.trim();
134
+ if (!orgId) {
135
+ throw new Error("organizationId is required");
136
+ }
137
+ return this.request<ClineAccountOrganizationBalance>({
138
+ action: "clineAccount",
139
+ operation: "fetchOrganizationBalance",
140
+ organizationId: orgId,
141
+ });
142
+ }
143
+
144
+ public async fetchOrganizationUsageTransactions(input: {
145
+ organizationId: string;
146
+ memberId?: string;
147
+ }): Promise<ClineAccountOrganizationUsageTransaction[]> {
148
+ const orgId = input.organizationId.trim();
149
+ if (!orgId) {
150
+ throw new Error("organizationId is required");
151
+ }
152
+ return this.request<ClineAccountOrganizationUsageTransaction[]>({
153
+ action: "clineAccount",
154
+ operation: "fetchOrganizationUsageTransactions",
155
+ organizationId: orgId,
156
+ ...(input.memberId?.trim() ? { memberId: input.memberId.trim() } : {}),
157
+ });
158
+ }
159
+
160
+ public async switchAccount(organizationId?: string | null): Promise<void> {
161
+ await this.request<{ updated: boolean }>({
162
+ action: "clineAccount",
163
+ operation: "switchAccount",
164
+ organizationId: organizationId?.trim() || null,
165
+ });
166
+ }
167
+
168
+ private async request<T>(request: RpcClineAccountActionRequest): Promise<T> {
169
+ const response = await this.executor.runProviderAction(request);
170
+ return response.result as T;
171
+ }
172
+ }
@@ -0,0 +1,80 @@
1
+ export interface ClineAccountOrganization {
2
+ active: boolean;
3
+ memberId: string;
4
+ name: string;
5
+ organizationId: string;
6
+ roles: Array<"admin" | "member" | "owner">;
7
+ }
8
+
9
+ export interface ClineAccountUser {
10
+ id: string;
11
+ email: string;
12
+ displayName: string;
13
+ photoUrl: string;
14
+ createdAt: string;
15
+ updatedAt: string;
16
+ organizations: ClineAccountOrganization[];
17
+ }
18
+
19
+ export interface ClineAccountBalance {
20
+ balance: number;
21
+ userId: string;
22
+ }
23
+
24
+ export interface ClineAccountUsageTransaction {
25
+ aiInferenceProviderName: string;
26
+ aiModelName: string;
27
+ aiModelTypeName: string;
28
+ completionTokens: number;
29
+ costUsd: number;
30
+ createdAt: string;
31
+ creditsUsed: number;
32
+ generationId: string;
33
+ id: string;
34
+ metadata: {
35
+ additionalProp1: string;
36
+ additionalProp2: string;
37
+ additionalProp3: string;
38
+ };
39
+ operation?: string;
40
+ organizationId: string;
41
+ promptTokens: number;
42
+ totalTokens: number;
43
+ userId: string;
44
+ }
45
+
46
+ export interface ClineAccountPaymentTransaction {
47
+ paidAt: string;
48
+ creatorId: string;
49
+ amountCents: number;
50
+ credits: number;
51
+ }
52
+
53
+ export interface ClineAccountOrganizationBalance {
54
+ balance: number;
55
+ organizationId: string;
56
+ }
57
+
58
+ export interface ClineAccountOrganizationUsageTransaction {
59
+ aiInferenceProviderName: string;
60
+ aiModelName: string;
61
+ aiModelTypeName: string;
62
+ completionTokens: number;
63
+ costUsd: number;
64
+ createdAt: string;
65
+ creditsUsed: number;
66
+ generationId: string;
67
+ id: string;
68
+ memberDisplayName: string;
69
+ memberEmail: string;
70
+ metadata: {
71
+ additionalProp1: string;
72
+ additionalProp2: string;
73
+ additionalProp3: string;
74
+ };
75
+ operation?: string;
76
+ organizationId: string;
77
+ promptTokens: number;
78
+ totalTokens: number;
79
+ userId: string;
80
+ }
@@ -0,0 +1,234 @@
1
+ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import type { Tool, ToolContext } from "@clinebot/agents";
5
+ import { afterEach, describe, expect, it } from "vitest";
6
+ import {
7
+ AGENT_CONFIG_DIRECTORY_NAME,
8
+ createAgentConfigDefinition,
9
+ parseAgentConfigFromYaml,
10
+ parsePartialAgentConfigFromYaml,
11
+ readAgentConfigsFromDisk,
12
+ resolveAgentConfigSearchPaths,
13
+ resolveAgentsConfigDirPath,
14
+ resolveAgentTools,
15
+ resolveDocumentsAgentConfigDirectoryPath,
16
+ toPartialAgentConfig,
17
+ } from "./agent-config-loader";
18
+
19
+ function createMockTool(name: string): Tool {
20
+ return {
21
+ name,
22
+ description: `${name} tool`,
23
+ inputSchema: {
24
+ type: "object",
25
+ properties: {},
26
+ },
27
+ execute: async (_input: unknown, _context: ToolContext) => null,
28
+ };
29
+ }
30
+
31
+ describe("agent config YAML loader", () => {
32
+ const envSnapshot = {
33
+ CLINE_DATA_DIR: process.env.CLINE_DATA_DIR,
34
+ };
35
+ afterEach(() => {
36
+ process.env.CLINE_DATA_DIR = envSnapshot.CLINE_DATA_DIR;
37
+ });
38
+
39
+ it("resolves default agents settings directory from CLINE_DATA_DIR", () => {
40
+ process.env.CLINE_DATA_DIR = "/tmp/cline-data";
41
+ expect(resolveAgentsConfigDirPath()).toBe(
42
+ `/tmp/cline-data/settings/${AGENT_CONFIG_DIRECTORY_NAME}`,
43
+ );
44
+ });
45
+
46
+ it("includes documents and settings search paths", () => {
47
+ process.env.CLINE_DATA_DIR = "/tmp/cline-data";
48
+ expect(resolveAgentConfigSearchPaths()).toEqual([
49
+ resolveDocumentsAgentConfigDirectoryPath(),
50
+ `/tmp/cline-data/settings/${AGENT_CONFIG_DIRECTORY_NAME}`,
51
+ ]);
52
+ });
53
+
54
+ it("builds a reusable unified watcher definition with expected defaults", () => {
55
+ process.env.CLINE_DATA_DIR = "/tmp/cline-data";
56
+ const definition = createAgentConfigDefinition();
57
+ expect(definition.type).toBe("agent");
58
+ expect(definition.directories).toEqual([
59
+ resolveDocumentsAgentConfigDirectoryPath(),
60
+ `/tmp/cline-data/settings/${AGENT_CONFIG_DIRECTORY_NAME}`,
61
+ ]);
62
+ expect(definition.includeFile?.("agent.yaml", "/tmp/agent.yaml")).toBe(
63
+ true,
64
+ );
65
+ expect(definition.includeFile?.("agent.md", "/tmp/agent.md")).toBe(false);
66
+ });
67
+
68
+ it("parses yaml frontmatter and prompt body", () => {
69
+ const content = `---
70
+ name: Researcher
71
+ description: Focus on repository analysis
72
+ modelId: claude-sonnet-4-6
73
+ tools:
74
+ - read_files
75
+ - search_codebase
76
+ skills:
77
+ - context-gathering
78
+ ---
79
+ You are a focused codebase researcher.`;
80
+
81
+ const parsed = parseAgentConfigFromYaml(content);
82
+
83
+ expect(parsed).toEqual({
84
+ name: "Researcher",
85
+ description: "Focus on repository analysis",
86
+ modelId: "claude-sonnet-4-6",
87
+ tools: ["read_files", "search_codebase"],
88
+ skills: ["context-gathering"],
89
+ systemPrompt: "You are a focused codebase researcher.",
90
+ });
91
+ });
92
+
93
+ it("supports comma-separated tool and skill values", () => {
94
+ const parsed = parseAgentConfigFromYaml(`---
95
+ name: Reviewer
96
+ description: Reviews diffs
97
+ tools: read_files,search_codebase,read_files
98
+ skills: quality, quality,architecture
99
+ ---
100
+ Review every patch for regressions.`);
101
+
102
+ expect(parsed.tools).toEqual(["read_files", "search_codebase"]);
103
+ expect(parsed.skills).toEqual(["quality", "architecture"]);
104
+ });
105
+
106
+ it("throws when frontmatter is missing", () => {
107
+ expect(() => parseAgentConfigFromYaml("No frontmatter")).toThrow(
108
+ "Missing YAML frontmatter block in agent config file.",
109
+ );
110
+ });
111
+
112
+ it("throws for unknown tools", () => {
113
+ expect(() =>
114
+ parseAgentConfigFromYaml(`---
115
+ name: UnknownTool
116
+ description: test
117
+ tools: invalid_tool
118
+ ---
119
+ prompt`),
120
+ ).toThrow("Unknown tool 'invalid_tool'.");
121
+ });
122
+
123
+ it("resolves configured tool names from available tools", () => {
124
+ const readFiles = createMockTool("read_files");
125
+ const searchCodebase = createMockTool("search_codebase");
126
+
127
+ expect(
128
+ resolveAgentTools(
129
+ ["read_files", "search_codebase"],
130
+ [searchCodebase, readFiles],
131
+ ),
132
+ ).toEqual([readFiles, searchCodebase]);
133
+ });
134
+
135
+ it("converts parsed config to partial AgentConfig", () => {
136
+ const readFiles = createMockTool("read_files");
137
+ const config = parseAgentConfigFromYaml(`---
138
+ name: Reader
139
+ description: Reads files
140
+ modelId: claude-sonnet-4-6
141
+ tools: read_files
142
+ ---
143
+ Be precise.`);
144
+
145
+ const partial = toPartialAgentConfig(config, {
146
+ availableTools: [readFiles],
147
+ });
148
+
149
+ expect(partial.modelId).toBe("claude-sonnet-4-6");
150
+ expect(partial.systemPrompt).toBe("Be precise.");
151
+ expect(partial.tools).toEqual([readFiles]);
152
+ });
153
+
154
+ it("throws when tool overrides are configured without available tools", () => {
155
+ expect(() =>
156
+ parsePartialAgentConfigFromYaml(`---
157
+ name: Reader
158
+ description: Reads files
159
+ tools: read_files
160
+ ---
161
+ Be precise.`),
162
+ ).toThrow(
163
+ "Configured tools cannot be converted into AgentConfig.tools without availableTools.",
164
+ );
165
+ });
166
+
167
+ it("reads agent configs from ~/.cline/data/settings/agents-compatible directory", async () => {
168
+ const tempRoot = await mkdtemp(join(tmpdir(), "core-agent-config-loader-"));
169
+ const agentsDir = join(tempRoot, "settings", AGENT_CONFIG_DIRECTORY_NAME);
170
+ await mkdir(agentsDir, { recursive: true });
171
+ try {
172
+ await writeFile(
173
+ join(agentsDir, "reviewer.yaml"),
174
+ `---
175
+ name: Reviewer
176
+ description: Reviews patches
177
+ tools: read_files
178
+ ---
179
+ Review code for regressions.`,
180
+ );
181
+ await writeFile(
182
+ join(agentsDir, "invalid.yaml"),
183
+ `---
184
+ name:
185
+ ---
186
+ `,
187
+ );
188
+
189
+ const loaded = await readAgentConfigsFromDisk(agentsDir);
190
+ expect([...loaded.keys()]).toEqual(["reviewer"]);
191
+ expect(loaded.get("reviewer")?.systemPrompt).toBe(
192
+ "Review code for regressions.",
193
+ );
194
+ } finally {
195
+ await rm(tempRoot, { recursive: true, force: true });
196
+ }
197
+ });
198
+
199
+ it("reads from both documents and settings directories", async () => {
200
+ const tempRoot = await mkdtemp(join(tmpdir(), "core-agent-config-loader-"));
201
+ const documentsDir = join(tempRoot, "Documents", "Cline", "Agents");
202
+ const settingsDir = join(tempRoot, "settings", AGENT_CONFIG_DIRECTORY_NAME);
203
+ await mkdir(documentsDir, { recursive: true });
204
+ await mkdir(settingsDir, { recursive: true });
205
+ try {
206
+ await writeFile(
207
+ join(documentsDir, "legacy.yaml"),
208
+ `---
209
+ name: LegacyAgent
210
+ description: legacy
211
+ ---
212
+ legacy prompt`,
213
+ );
214
+ await writeFile(
215
+ join(settingsDir, "new.yaml"),
216
+ `---
217
+ name: NewAgent
218
+ description: new
219
+ ---
220
+ new prompt`,
221
+ );
222
+
223
+ const loaded = await readAgentConfigsFromDisk([
224
+ documentsDir,
225
+ settingsDir,
226
+ ]);
227
+ expect([...loaded.keys()].sort()).toEqual(["legacyagent", "newagent"]);
228
+ expect(loaded.get("legacyagent")?.systemPrompt).toBe("legacy prompt");
229
+ expect(loaded.get("newagent")?.systemPrompt).toBe("new prompt");
230
+ } finally {
231
+ await rm(tempRoot, { recursive: true, force: true });
232
+ }
233
+ });
234
+ });
@@ -0,0 +1,107 @@
1
+ import {
2
+ AGENT_CONFIG_DIRECTORY_NAME,
3
+ resolveAgentConfigSearchPaths as resolveAgentConfigSearchPathsFromShared,
4
+ resolveAgentsConfigDirPath as resolveAgentsConfigDirPathFromShared,
5
+ resolveDocumentsAgentConfigDirectoryPath,
6
+ } from "@clinebot/shared/storage";
7
+ import {
8
+ type AgentYamlConfig,
9
+ isAgentConfigYamlFile,
10
+ normalizeAgentConfigName,
11
+ parseAgentConfigFromYaml,
12
+ } from "./agent-config-parser";
13
+ import {
14
+ type UnifiedConfigDefinition,
15
+ UnifiedConfigFileWatcher,
16
+ type UnifiedConfigWatcherEvent,
17
+ } from "./unified-config-file-watcher";
18
+
19
+ export type {
20
+ AgentYamlConfig,
21
+ BuildAgentConfigOverridesOptions,
22
+ ParseYamlFrontmatterResult,
23
+ } from "./agent-config-parser";
24
+ export {
25
+ parseAgentConfigFromYaml,
26
+ parsePartialAgentConfigFromYaml,
27
+ resolveAgentTools,
28
+ toPartialAgentConfig,
29
+ } from "./agent-config-parser";
30
+
31
+ export type AgentConfigWatcher = UnifiedConfigFileWatcher<
32
+ "agent",
33
+ AgentYamlConfig
34
+ >;
35
+ export type AgentConfigWatcherEvent = UnifiedConfigWatcherEvent<
36
+ "agent",
37
+ AgentYamlConfig
38
+ >;
39
+
40
+ export {
41
+ AGENT_CONFIG_DIRECTORY_NAME,
42
+ resolveDocumentsAgentConfigDirectoryPath,
43
+ };
44
+
45
+ export function resolveAgentsConfigDirPath(): string {
46
+ return resolveAgentsConfigDirPathFromShared();
47
+ }
48
+
49
+ export function resolveAgentConfigSearchPaths(): string[] {
50
+ // Documents path first, then settings path so settings location takes precedence.
51
+ return resolveAgentConfigSearchPathsFromShared();
52
+ }
53
+
54
+ export interface CreateAgentConfigWatcherOptions {
55
+ directoryPathOrPaths?: string | ReadonlyArray<string>;
56
+ debounceMs?: number;
57
+ emitParseErrors?: boolean;
58
+ }
59
+
60
+ function toDirectoryPaths(
61
+ directoryPathOrPaths?: string | ReadonlyArray<string>,
62
+ ): string[] {
63
+ if (Array.isArray(directoryPathOrPaths)) {
64
+ return [...directoryPathOrPaths];
65
+ }
66
+ if (typeof directoryPathOrPaths === "string") {
67
+ return [directoryPathOrPaths];
68
+ }
69
+ return resolveAgentConfigSearchPaths();
70
+ }
71
+
72
+ export function createAgentConfigDefinition(
73
+ directoryPathOrPaths?: string | ReadonlyArray<string>,
74
+ ): UnifiedConfigDefinition<"agent", AgentYamlConfig> {
75
+ return {
76
+ type: "agent",
77
+ directories: toDirectoryPaths(directoryPathOrPaths),
78
+ includeFile: (fileName) => isAgentConfigYamlFile(fileName),
79
+ parseFile: (context) => parseAgentConfigFromYaml(context.content),
80
+ resolveId: (config) => normalizeAgentConfigName(config.name),
81
+ };
82
+ }
83
+
84
+ export function createAgentConfigWatcher(
85
+ options?: CreateAgentConfigWatcherOptions,
86
+ ): AgentConfigWatcher {
87
+ return new UnifiedConfigFileWatcher(
88
+ [createAgentConfigDefinition(options?.directoryPathOrPaths)],
89
+ {
90
+ debounceMs: options?.debounceMs,
91
+ emitParseErrors: options?.emitParseErrors,
92
+ },
93
+ );
94
+ }
95
+
96
+ export async function readAgentConfigsFromDisk(
97
+ directoryPathOrPaths?: string | ReadonlyArray<string>,
98
+ ): Promise<Map<string, AgentYamlConfig>> {
99
+ const watcher = new UnifiedConfigFileWatcher([
100
+ createAgentConfigDefinition(directoryPathOrPaths),
101
+ ]);
102
+ await watcher.refreshAll();
103
+ const snapshot = watcher.getSnapshot("agent");
104
+ return new Map(
105
+ [...snapshot.entries()].map(([id, record]) => [id, record.item]),
106
+ );
107
+ }