@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,236 @@
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
+ join("/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
+ join("/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
+ join("/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
+ skills: commit, review
143
+ ---
144
+ Be precise.`);
145
+
146
+ const partial = toPartialAgentConfig(config, {
147
+ availableTools: [readFiles],
148
+ });
149
+
150
+ expect(partial.modelId).toBe("claude-sonnet-4-6");
151
+ expect(partial.systemPrompt).toBe("Be precise.");
152
+ expect(partial.tools).toEqual([readFiles]);
153
+ expect(partial.skills).toEqual(["commit", "review"]);
154
+ });
155
+
156
+ it("throws when tool overrides are configured without available tools", () => {
157
+ expect(() =>
158
+ parsePartialAgentConfigFromYaml(`---
159
+ name: Reader
160
+ description: Reads files
161
+ tools: read_files
162
+ ---
163
+ Be precise.`),
164
+ ).toThrow(
165
+ "Configured tools cannot be converted into AgentConfig.tools without availableTools.",
166
+ );
167
+ });
168
+
169
+ it("reads agent configs from ~/.cline/data/settings/agents-compatible directory", async () => {
170
+ const tempRoot = await mkdtemp(join(tmpdir(), "core-agent-config-loader-"));
171
+ const agentsDir = join(tempRoot, "settings", AGENT_CONFIG_DIRECTORY_NAME);
172
+ await mkdir(agentsDir, { recursive: true });
173
+ try {
174
+ await writeFile(
175
+ join(agentsDir, "reviewer.yaml"),
176
+ `---
177
+ name: Reviewer
178
+ description: Reviews patches
179
+ tools: read_files
180
+ ---
181
+ Review code for regressions.`,
182
+ );
183
+ await writeFile(
184
+ join(agentsDir, "invalid.yaml"),
185
+ `---
186
+ name:
187
+ ---
188
+ `,
189
+ );
190
+
191
+ const loaded = await readAgentConfigsFromDisk(agentsDir);
192
+ expect([...loaded.keys()]).toEqual(["reviewer"]);
193
+ expect(loaded.get("reviewer")?.systemPrompt).toBe(
194
+ "Review code for regressions.",
195
+ );
196
+ } finally {
197
+ await rm(tempRoot, { recursive: true, force: true });
198
+ }
199
+ });
200
+
201
+ it("reads from both documents and settings directories", async () => {
202
+ const tempRoot = await mkdtemp(join(tmpdir(), "core-agent-config-loader-"));
203
+ const documentsDir = join(tempRoot, "Documents", "Cline", "Agents");
204
+ const settingsDir = join(tempRoot, "settings", AGENT_CONFIG_DIRECTORY_NAME);
205
+ await mkdir(documentsDir, { recursive: true });
206
+ await mkdir(settingsDir, { recursive: true });
207
+ try {
208
+ await writeFile(
209
+ join(documentsDir, "legacy.yaml"),
210
+ `---
211
+ name: LegacyAgent
212
+ description: legacy
213
+ ---
214
+ legacy prompt`,
215
+ );
216
+ await writeFile(
217
+ join(settingsDir, "new.yaml"),
218
+ `---
219
+ name: NewAgent
220
+ description: new
221
+ ---
222
+ new prompt`,
223
+ );
224
+
225
+ const loaded = await readAgentConfigsFromDisk([
226
+ documentsDir,
227
+ settingsDir,
228
+ ]);
229
+ expect([...loaded.keys()].sort()).toEqual(["legacyagent", "newagent"]);
230
+ expect(loaded.get("legacyagent")?.systemPrompt).toBe("legacy prompt");
231
+ expect(loaded.get("newagent")?.systemPrompt).toBe("new prompt");
232
+ } finally {
233
+ await rm(tempRoot, { recursive: true, force: true });
234
+ }
235
+ });
236
+ });
@@ -0,0 +1,108 @@
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
+ PartialAgentConfigOverrides,
24
+ } from "./agent-config-parser";
25
+ export {
26
+ parseAgentConfigFromYaml,
27
+ parsePartialAgentConfigFromYaml,
28
+ resolveAgentTools,
29
+ toPartialAgentConfig,
30
+ } from "./agent-config-parser";
31
+
32
+ export type AgentConfigWatcher = UnifiedConfigFileWatcher<
33
+ "agent",
34
+ AgentYamlConfig
35
+ >;
36
+ export type AgentConfigWatcherEvent = UnifiedConfigWatcherEvent<
37
+ "agent",
38
+ AgentYamlConfig
39
+ >;
40
+
41
+ export {
42
+ AGENT_CONFIG_DIRECTORY_NAME,
43
+ resolveDocumentsAgentConfigDirectoryPath,
44
+ };
45
+
46
+ export function resolveAgentsConfigDirPath(): string {
47
+ return resolveAgentsConfigDirPathFromShared();
48
+ }
49
+
50
+ export function resolveAgentConfigSearchPaths(): string[] {
51
+ // Documents path first, then settings path so settings location takes precedence.
52
+ return resolveAgentConfigSearchPathsFromShared();
53
+ }
54
+
55
+ export interface CreateAgentConfigWatcherOptions {
56
+ directoryPathOrPaths?: string | ReadonlyArray<string>;
57
+ debounceMs?: number;
58
+ emitParseErrors?: boolean;
59
+ }
60
+
61
+ function toDirectoryPaths(
62
+ directoryPathOrPaths?: string | ReadonlyArray<string>,
63
+ ): string[] {
64
+ if (Array.isArray(directoryPathOrPaths)) {
65
+ return [...directoryPathOrPaths];
66
+ }
67
+ if (typeof directoryPathOrPaths === "string") {
68
+ return [directoryPathOrPaths];
69
+ }
70
+ return resolveAgentConfigSearchPaths();
71
+ }
72
+
73
+ export function createAgentConfigDefinition(
74
+ directoryPathOrPaths?: string | ReadonlyArray<string>,
75
+ ): UnifiedConfigDefinition<"agent", AgentYamlConfig> {
76
+ return {
77
+ type: "agent",
78
+ directories: toDirectoryPaths(directoryPathOrPaths),
79
+ includeFile: (fileName) => isAgentConfigYamlFile(fileName),
80
+ parseFile: (context) => parseAgentConfigFromYaml(context.content),
81
+ resolveId: (config) => normalizeAgentConfigName(config.name),
82
+ };
83
+ }
84
+
85
+ export function createAgentConfigWatcher(
86
+ options?: CreateAgentConfigWatcherOptions,
87
+ ): AgentConfigWatcher {
88
+ return new UnifiedConfigFileWatcher(
89
+ [createAgentConfigDefinition(options?.directoryPathOrPaths)],
90
+ {
91
+ debounceMs: options?.debounceMs,
92
+ emitParseErrors: options?.emitParseErrors,
93
+ },
94
+ );
95
+ }
96
+
97
+ export async function readAgentConfigsFromDisk(
98
+ directoryPathOrPaths?: string | ReadonlyArray<string>,
99
+ ): Promise<Map<string, AgentYamlConfig>> {
100
+ const watcher = new UnifiedConfigFileWatcher([
101
+ createAgentConfigDefinition(directoryPathOrPaths),
102
+ ]);
103
+ await watcher.refreshAll();
104
+ const snapshot = watcher.getSnapshot("agent");
105
+ return new Map(
106
+ [...snapshot.entries()].map(([id, record]) => [id, record.item]),
107
+ );
108
+ }
@@ -0,0 +1,198 @@
1
+ import type { AgentConfig, Tool } from "@clinebot/agents";
2
+ import YAML from "yaml";
3
+ import { z } from "zod";
4
+ import { ALL_DEFAULT_TOOL_NAMES, type DefaultToolName } from "../tools";
5
+
6
+ const AgentConfigFrontmatterSchema = z.object({
7
+ name: z.string().trim().min(1),
8
+ description: z.string().trim().min(1),
9
+ modelId: z.string().trim().min(1).optional(),
10
+ tools: z.union([z.string(), z.array(z.string())]).optional(),
11
+ skills: z.union([z.string(), z.array(z.string())]).optional(),
12
+ });
13
+
14
+ const allowedToolNames = new Set<string>(ALL_DEFAULT_TOOL_NAMES);
15
+
16
+ export interface AgentYamlConfig {
17
+ name: string;
18
+ description: string;
19
+ modelId?: string;
20
+ tools: DefaultToolName[];
21
+ skills?: string[];
22
+ systemPrompt: string;
23
+ }
24
+
25
+ export interface ParseYamlFrontmatterResult {
26
+ data: Record<string, unknown>;
27
+ body: string;
28
+ hadFrontmatter: boolean;
29
+ parseError?: string;
30
+ }
31
+
32
+ export interface BuildAgentConfigOverridesOptions {
33
+ availableTools?: ReadonlyArray<Tool>;
34
+ }
35
+
36
+ export interface PartialAgentConfigOverrides
37
+ extends Partial<Pick<AgentConfig, "modelId" | "systemPrompt" | "tools">> {
38
+ skills?: string[];
39
+ }
40
+
41
+ export function isAgentConfigYamlFile(fileName: string): boolean {
42
+ return /\.(yaml|yml)$/i.test(fileName);
43
+ }
44
+
45
+ export function normalizeAgentConfigName(name: string): string {
46
+ return name.trim().toLowerCase();
47
+ }
48
+
49
+ function parseYamlFrontmatter(markdown: string): ParseYamlFrontmatterResult {
50
+ const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
51
+ const match = markdown.match(frontmatterRegex);
52
+ if (!match) {
53
+ return { data: {}, body: markdown, hadFrontmatter: false };
54
+ }
55
+
56
+ const [, yamlContent, body] = match;
57
+ try {
58
+ const parsed = YAML.parse(yamlContent);
59
+ const data =
60
+ parsed && typeof parsed === "object" && !Array.isArray(parsed)
61
+ ? (parsed as Record<string, unknown>)
62
+ : {};
63
+ return { data, body, hadFrontmatter: true };
64
+ } catch (error) {
65
+ const message = error instanceof Error ? error.message : String(error);
66
+ return {
67
+ data: {},
68
+ body: markdown,
69
+ hadFrontmatter: true,
70
+ parseError: message,
71
+ };
72
+ }
73
+ }
74
+
75
+ function normalizeToolName(toolName: string): DefaultToolName {
76
+ const trimmed = toolName.trim();
77
+ if (!trimmed) {
78
+ throw new Error("Tool name cannot be empty.");
79
+ }
80
+ if (!allowedToolNames.has(trimmed)) {
81
+ throw new Error(
82
+ `Unknown tool '${trimmed}'. Expected one of: ${ALL_DEFAULT_TOOL_NAMES.join(", ")}.`,
83
+ );
84
+ }
85
+ return trimmed as DefaultToolName;
86
+ }
87
+
88
+ function parseToolNames(
89
+ tools: string | string[] | undefined,
90
+ ): DefaultToolName[] {
91
+ if (!tools) {
92
+ return [];
93
+ }
94
+ const rawTools = Array.isArray(tools) ? tools : tools.split(",");
95
+ return Array.from(new Set(rawTools.map(normalizeToolName)));
96
+ }
97
+
98
+ function normalizeSkillName(skillName: string): string {
99
+ const trimmed = skillName.trim();
100
+ if (!trimmed) {
101
+ throw new Error("Skill name cannot be empty.");
102
+ }
103
+ return trimmed;
104
+ }
105
+
106
+ function parseSkills(
107
+ skills: string | string[] | undefined,
108
+ ): string[] | undefined {
109
+ if (skills === undefined) {
110
+ return undefined;
111
+ }
112
+ const rawSkills = Array.isArray(skills) ? skills : skills.split(",");
113
+ return Array.from(new Set(rawSkills.map(normalizeSkillName)));
114
+ }
115
+
116
+ export function parseAgentConfigFromYaml(content: string): AgentYamlConfig {
117
+ const { data, body, hadFrontmatter, parseError } =
118
+ parseYamlFrontmatter(content);
119
+ if (parseError) {
120
+ throw new Error(`Failed to parse YAML frontmatter: ${parseError}`);
121
+ }
122
+ if (!hadFrontmatter) {
123
+ throw new Error("Missing YAML frontmatter block in agent config file.");
124
+ }
125
+
126
+ const parsedFrontmatter = AgentConfigFrontmatterSchema.parse(data);
127
+ const systemPrompt = body.trim();
128
+ if (!systemPrompt) {
129
+ throw new Error("Missing system prompt body in agent config file.");
130
+ }
131
+
132
+ return {
133
+ name: parsedFrontmatter.name,
134
+ description: parsedFrontmatter.description,
135
+ modelId: parsedFrontmatter.modelId,
136
+ tools: parseToolNames(parsedFrontmatter.tools),
137
+ skills: parseSkills(parsedFrontmatter.skills),
138
+ systemPrompt,
139
+ };
140
+ }
141
+
142
+ export function resolveAgentTools(
143
+ toolNames: ReadonlyArray<DefaultToolName>,
144
+ availableTools: ReadonlyArray<Tool>,
145
+ ): Tool[] {
146
+ if (toolNames.length === 0) {
147
+ return [];
148
+ }
149
+
150
+ const toolIndex = new Map<string, Tool>(
151
+ availableTools.map((tool) => [tool.name, tool]),
152
+ );
153
+ return toolNames.map((toolName) => {
154
+ const resolved = toolIndex.get(toolName);
155
+ if (!resolved) {
156
+ throw new Error(
157
+ `Configured tool '${toolName}' is unavailable. Available tools: ${availableTools.map((tool) => tool.name).join(", ")}.`,
158
+ );
159
+ }
160
+ return resolved;
161
+ });
162
+ }
163
+
164
+ export function toPartialAgentConfig(
165
+ config: AgentYamlConfig,
166
+ options?: BuildAgentConfigOverridesOptions,
167
+ ): PartialAgentConfigOverrides {
168
+ const partial: PartialAgentConfigOverrides = {
169
+ systemPrompt: config.systemPrompt,
170
+ };
171
+
172
+ if (config.modelId) {
173
+ partial.modelId = config.modelId;
174
+ }
175
+
176
+ if (config.tools.length > 0) {
177
+ if (!options?.availableTools) {
178
+ throw new Error(
179
+ "Configured tools cannot be converted into AgentConfig.tools without availableTools.",
180
+ );
181
+ }
182
+ partial.tools = resolveAgentTools(config.tools, options.availableTools);
183
+ }
184
+
185
+ if (config.skills !== undefined) {
186
+ partial.skills = [...config.skills];
187
+ }
188
+
189
+ return partial;
190
+ }
191
+
192
+ export function parsePartialAgentConfigFromYaml(
193
+ content: string,
194
+ options?: BuildAgentConfigOverridesOptions,
195
+ ): PartialAgentConfigOverrides {
196
+ const parsed = parseAgentConfigFromYaml(content);
197
+ return toPartialAgentConfig(parsed, options);
198
+ }
@@ -0,0 +1,20 @@
1
+ import { afterEach, describe, expect, it } from "vitest";
2
+ import {
3
+ HookConfigFileName,
4
+ toHookConfigFileName,
5
+ } from "./hooks-config-loader";
6
+
7
+ describe("hooks config loader", () => {
8
+ afterEach(() => {
9
+ delete process.env.CLINE_DATA_DIR;
10
+ });
11
+
12
+ it("recognizes PowerShell hook files", () => {
13
+ expect(toHookConfigFileName("PreToolUse.ps1")).toBe(
14
+ HookConfigFileName.PreToolUse,
15
+ );
16
+ expect(toHookConfigFileName("TaskError.ps1")).toBe(
17
+ HookConfigFileName.TaskError,
18
+ );
19
+ });
20
+ });
@@ -0,0 +1,118 @@
1
+ import { existsSync, readdirSync } from "node:fs";
2
+ import { basename, extname, join } from "node:path";
3
+ import type { HookEventName } from "@clinebot/agents";
4
+ import {
5
+ HOOKS_CONFIG_DIRECTORY_NAME,
6
+ resolveDocumentsHooksDirectoryPath,
7
+ resolveHooksConfigSearchPaths as resolveHooksConfigSearchPathsFromShared,
8
+ } from "@clinebot/shared/storage";
9
+
10
+ export { HOOKS_CONFIG_DIRECTORY_NAME, resolveDocumentsHooksDirectoryPath };
11
+
12
+ export function resolveHooksConfigSearchPaths(
13
+ workspacePath?: string,
14
+ ): string[] {
15
+ return resolveHooksConfigSearchPathsFromShared(workspacePath);
16
+ }
17
+
18
+ export enum HookConfigFileName {
19
+ TaskStart = "TaskStart",
20
+ TaskResume = "TaskResume",
21
+ TaskCancel = "TaskCancel",
22
+ TaskComplete = "TaskComplete",
23
+ TaskError = "TaskError",
24
+ PreToolUse = "PreToolUse",
25
+ PostToolUse = "PostToolUse",
26
+ UserPromptSubmit = "UserPromptSubmit",
27
+ PreCompact = "PreCompact",
28
+ SessionShutdown = "SessionShutdown",
29
+ }
30
+
31
+ export const HOOK_CONFIG_FILE_EVENT_MAP: Readonly<
32
+ Record<HookConfigFileName, HookEventName | undefined>
33
+ > = {
34
+ [HookConfigFileName.TaskStart]: "agent_start",
35
+ [HookConfigFileName.TaskResume]: "agent_resume",
36
+ [HookConfigFileName.TaskCancel]: "agent_abort",
37
+ [HookConfigFileName.TaskComplete]: "agent_end",
38
+ [HookConfigFileName.TaskError]: "agent_error",
39
+ [HookConfigFileName.PreToolUse]: "tool_call",
40
+ [HookConfigFileName.PostToolUse]: "tool_result",
41
+ [HookConfigFileName.UserPromptSubmit]: "prompt_submit",
42
+ [HookConfigFileName.PreCompact]: undefined,
43
+ [HookConfigFileName.SessionShutdown]: "session_shutdown",
44
+ };
45
+
46
+ const HOOK_CONFIG_FILE_LOOKUP = new Map<string, HookConfigFileName>(
47
+ Object.values(HookConfigFileName).map((name) => [name.toLowerCase(), name]),
48
+ );
49
+
50
+ const SUPPORTED_HOOK_FILE_EXTENSIONS = new Set([
51
+ "",
52
+ ".sh",
53
+ ".bash",
54
+ ".zsh",
55
+ ".js",
56
+ ".mjs",
57
+ ".cjs",
58
+ ".ts",
59
+ ".mts",
60
+ ".cts",
61
+ ".py",
62
+ ".ps1",
63
+ ]);
64
+
65
+ export function toHookConfigFileName(
66
+ fileName: string,
67
+ ): HookConfigFileName | undefined {
68
+ const extension = extname(fileName).toLowerCase();
69
+ if (!SUPPORTED_HOOK_FILE_EXTENSIONS.has(extension)) {
70
+ return undefined;
71
+ }
72
+ const key = basename(fileName, extension).trim().toLowerCase();
73
+ return HOOK_CONFIG_FILE_LOOKUP.get(key);
74
+ }
75
+
76
+ export interface HookConfigFileEntry {
77
+ fileName: HookConfigFileName;
78
+ hookEventName?: HookEventName;
79
+ path: string;
80
+ }
81
+
82
+ export function listHookConfigFiles(
83
+ workspacePath?: string,
84
+ ): HookConfigFileEntry[] {
85
+ const entries: HookConfigFileEntry[] = [];
86
+ const seen = new Set<string>();
87
+ const directories = resolveHooksConfigSearchPaths(workspacePath).filter(
88
+ (directory) => existsSync(directory),
89
+ );
90
+
91
+ for (const directory of directories) {
92
+ try {
93
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
94
+ if (!entry.isFile()) {
95
+ continue;
96
+ }
97
+ const fileName = toHookConfigFileName(entry.name);
98
+ if (!fileName) {
99
+ continue;
100
+ }
101
+ const path = join(directory, entry.name);
102
+ if (seen.has(path)) {
103
+ continue;
104
+ }
105
+ seen.add(path);
106
+ entries.push({
107
+ fileName,
108
+ hookEventName: HOOK_CONFIG_FILE_EVENT_MAP[fileName],
109
+ path,
110
+ });
111
+ }
112
+ } catch {
113
+ // Best-effort listing across config roots.
114
+ }
115
+ }
116
+
117
+ return entries.sort((a, b) => a.path.localeCompare(b.path));
118
+ }