@clinebot/core 0.0.11 → 0.0.12

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 (53) hide show
  1. package/dist/agents/agent-config-loader.d.ts +1 -1
  2. package/dist/agents/agent-config-parser.d.ts +5 -2
  3. package/dist/agents/index.d.ts +1 -1
  4. package/dist/agents/plugin-config-loader.d.ts +4 -0
  5. package/dist/agents/plugin-sandbox-bootstrap.js +446 -0
  6. package/dist/agents/plugin-sandbox.d.ts +4 -0
  7. package/dist/index.node.d.ts +1 -0
  8. package/dist/index.node.js +658 -407
  9. package/dist/runtime/sandbox/subprocess-sandbox.d.ts +8 -1
  10. package/dist/session/default-session-manager.d.ts +5 -0
  11. package/dist/session/session-config-builder.d.ts +4 -1
  12. package/dist/session/session-manager.d.ts +1 -0
  13. package/dist/session/unified-session-persistence-service.d.ts +6 -0
  14. package/dist/session/utils/types.d.ts +9 -0
  15. package/dist/tools/definitions.d.ts +2 -2
  16. package/dist/tools/presets.d.ts +3 -3
  17. package/dist/tools/schemas.d.ts +14 -14
  18. package/dist/types/config.d.ts +5 -0
  19. package/dist/types/events.d.ts +22 -0
  20. package/package.json +5 -4
  21. package/src/agents/agent-config-loader.test.ts +2 -0
  22. package/src/agents/agent-config-loader.ts +1 -0
  23. package/src/agents/agent-config-parser.ts +12 -5
  24. package/src/agents/index.ts +1 -0
  25. package/src/agents/plugin-config-loader.test.ts +49 -0
  26. package/src/agents/plugin-config-loader.ts +10 -73
  27. package/src/agents/plugin-loader.test.ts +128 -2
  28. package/src/agents/plugin-loader.ts +70 -5
  29. package/src/agents/plugin-sandbox-bootstrap.ts +445 -0
  30. package/src/agents/plugin-sandbox.test.ts +198 -1
  31. package/src/agents/plugin-sandbox.ts +223 -353
  32. package/src/index.node.ts +4 -0
  33. package/src/runtime/hook-file-hooks.test.ts +1 -1
  34. package/src/runtime/hook-file-hooks.ts +16 -6
  35. package/src/runtime/runtime-builder.test.ts +67 -0
  36. package/src/runtime/runtime-builder.ts +70 -16
  37. package/src/runtime/sandbox/subprocess-sandbox.ts +35 -11
  38. package/src/session/default-session-manager.e2e.test.ts +20 -1
  39. package/src/session/default-session-manager.test.ts +453 -1
  40. package/src/session/default-session-manager.ts +200 -0
  41. package/src/session/session-config-builder.ts +2 -0
  42. package/src/session/session-manager.ts +1 -0
  43. package/src/session/session-team-coordination.ts +30 -0
  44. package/src/session/unified-session-persistence-service.ts +45 -0
  45. package/src/session/utils/types.ts +10 -0
  46. package/src/storage/sqlite-team-store.ts +16 -5
  47. package/src/tools/definitions.test.ts +87 -8
  48. package/src/tools/definitions.ts +89 -70
  49. package/src/tools/presets.test.ts +2 -3
  50. package/src/tools/presets.ts +3 -3
  51. package/src/tools/schemas.ts +23 -22
  52. package/src/types/config.ts +5 -0
  53. package/src/types/events.ts +23 -0
@@ -301,4 +301,71 @@ Disabled skill.`,
301
301
 
302
302
  runtime.shutdown("test");
303
303
  });
304
+
305
+ it("scopes skills tool to session-configured skills", async () => {
306
+ const cwd = mkdtempSync(join(tmpdir(), "runtime-builder-skills-scoped-"));
307
+ const commitDir = join(cwd, ".cline", "skills", "commit");
308
+ const reviewDir = join(cwd, ".cline", "skills", "review");
309
+ mkdirSync(commitDir, { recursive: true });
310
+ mkdirSync(reviewDir, { recursive: true });
311
+ writeFileSync(
312
+ join(commitDir, "SKILL.md"),
313
+ `---
314
+ name: commit
315
+ ---
316
+ Commit skill.`,
317
+ "utf8",
318
+ );
319
+ writeFileSync(
320
+ join(reviewDir, "SKILL.md"),
321
+ `---
322
+ name: review
323
+ ---
324
+ Review skill.`,
325
+ "utf8",
326
+ );
327
+
328
+ const runtime = new DefaultRuntimeBuilder().build({
329
+ config: {
330
+ providerId: "anthropic",
331
+ modelId: "claude-sonnet-4-6",
332
+ apiKey: "key",
333
+ systemPrompt: "test",
334
+ cwd,
335
+ enableTools: true,
336
+ enableSpawnAgent: false,
337
+ enableAgentTeams: false,
338
+ skills: ["commit"],
339
+ },
340
+ });
341
+
342
+ const skillsTool = runtime.tools.find((tool) => tool.name === "skills");
343
+ expect(skillsTool).toBeDefined();
344
+ if (!skillsTool) {
345
+ throw new Error("Expected skills tool.");
346
+ }
347
+
348
+ const known = await skillsTool.execute(
349
+ { skill: "commit" },
350
+ {
351
+ agentId: "agent-1",
352
+ conversationId: "conv-1",
353
+ iteration: 1,
354
+ },
355
+ );
356
+ expect(known).toContain("<command-name>commit</command-name>");
357
+
358
+ const blocked = await skillsTool.execute(
359
+ { skill: "review" },
360
+ {
361
+ agentId: "agent-1",
362
+ conversationId: "conv-1",
363
+ iteration: 1,
364
+ },
365
+ );
366
+ expect(blocked).toContain('Skill "review" not found.');
367
+ expect(blocked).toContain("Available skills: commit");
368
+
369
+ runtime.shutdown("test");
370
+ });
304
371
  });
@@ -86,27 +86,72 @@ const SKILL_FILE_NAME = "SKILL.md";
86
86
 
87
87
  function listAvailableSkillNames(
88
88
  watcher: UserInstructionConfigWatcher,
89
+ allowedSkillNames?: ReadonlyArray<string>,
89
90
  ): string[] {
90
- return listConfiguredSkills(watcher)
91
+ return listConfiguredSkills(watcher, allowedSkillNames)
91
92
  .filter((skill) => !skill.disabled)
92
93
  .map((skill) => skill.name.trim())
93
94
  .filter((name) => name.length > 0)
94
95
  .sort((a, b) => a.localeCompare(b));
95
96
  }
96
97
 
98
+ function normalizeSkillToken(token: string): string {
99
+ return token.trim().replace(/^\/+/, "").toLowerCase();
100
+ }
101
+
102
+ function toAllowedSkillSet(
103
+ allowedSkillNames?: ReadonlyArray<string>,
104
+ ): Set<string> | undefined {
105
+ if (!allowedSkillNames || allowedSkillNames.length === 0) {
106
+ return undefined;
107
+ }
108
+ const normalized = allowedSkillNames
109
+ .map(normalizeSkillToken)
110
+ .filter((token) => token.length > 0);
111
+ return normalized.length > 0 ? new Set(normalized) : undefined;
112
+ }
113
+
114
+ function isSkillAllowed(
115
+ skillId: string,
116
+ skillName: string,
117
+ allowedSkills?: Set<string>,
118
+ ): boolean {
119
+ if (!allowedSkills) {
120
+ return true;
121
+ }
122
+ const normalizedId = normalizeSkillToken(skillId);
123
+ const normalizedName = normalizeSkillToken(skillName);
124
+ const bareId = normalizedId.includes(":")
125
+ ? (normalizedId.split(":").at(-1) ?? normalizedId)
126
+ : normalizedId;
127
+ const bareName = normalizedName.includes(":")
128
+ ? (normalizedName.split(":").at(-1) ?? normalizedName)
129
+ : normalizedName;
130
+ return (
131
+ allowedSkills.has(normalizedId) ||
132
+ allowedSkills.has(normalizedName) ||
133
+ allowedSkills.has(bareId) ||
134
+ allowedSkills.has(bareName)
135
+ );
136
+ }
137
+
97
138
  function listConfiguredSkills(
98
139
  watcher: UserInstructionConfigWatcher,
140
+ allowedSkillNames?: ReadonlyArray<string>,
99
141
  ): SkillsExecutorMetadataItem[] {
142
+ const allowedSkills = toAllowedSkillSet(allowedSkillNames);
100
143
  const snapshot = watcher.getSnapshot("skill");
101
- return [...snapshot.entries()].map(([id, record]) => {
102
- const skill = record.item as SkillConfig;
103
- return {
104
- id,
105
- name: skill.name.trim(),
106
- description: skill.description?.trim(),
107
- disabled: skill.disabled === true,
108
- };
109
- });
144
+ return [...snapshot.entries()]
145
+ .map(([id, record]) => {
146
+ const skill = record.item as SkillConfig;
147
+ return {
148
+ id,
149
+ name: skill.name.trim(),
150
+ description: skill.description?.trim(),
151
+ disabled: skill.disabled === true,
152
+ };
153
+ })
154
+ .filter((skill) => isSkillAllowed(skill.id, skill.name, allowedSkills));
110
155
  }
111
156
 
112
157
  function hasSkillsFiles(workspacePath: string): boolean {
@@ -141,14 +186,21 @@ function hasSkillsFiles(workspacePath: string): boolean {
141
186
  function resolveSkillRecord(
142
187
  watcher: UserInstructionConfigWatcher,
143
188
  requestedSkill: string,
189
+ allowedSkillNames?: ReadonlyArray<string>,
144
190
  ): { id: string; skill: SkillConfig } | { error: string } {
191
+ const allowedSkills = toAllowedSkillSet(allowedSkillNames);
145
192
  const normalized = requestedSkill.trim().replace(/^\/+/, "").toLowerCase();
146
193
  if (!normalized) {
147
194
  return { error: "Missing skill name." };
148
195
  }
149
196
 
150
197
  const snapshot = watcher.getSnapshot("skill");
151
- const exact = snapshot.get(normalized);
198
+ const scopedEntries = [...snapshot.entries()].filter(([id, record]) => {
199
+ const skill = record.item as SkillConfig;
200
+ return isSkillAllowed(id, skill.name, allowedSkills);
201
+ });
202
+ const scopedSnapshot = new Map(scopedEntries);
203
+ const exact = scopedSnapshot.get(normalized);
152
204
  if (exact) {
153
205
  const skill = exact.item as SkillConfig;
154
206
  if (skill.disabled === true) {
@@ -166,7 +218,7 @@ function resolveSkillRecord(
166
218
  ? (normalized.split(":").at(-1) ?? normalized)
167
219
  : normalized;
168
220
 
169
- const suffixMatches = [...snapshot.entries()].filter(([id]) => {
221
+ const suffixMatches = [...scopedSnapshot.entries()].filter(([id]) => {
170
222
  if (id === bareName) {
171
223
  return true;
172
224
  }
@@ -193,7 +245,7 @@ function resolveSkillRecord(
193
245
  };
194
246
  }
195
247
 
196
- const available = listAvailableSkillNames(watcher);
248
+ const available = listAvailableSkillNames(watcher, allowedSkillNames);
197
249
  return {
198
250
  error:
199
251
  available.length > 0
@@ -205,11 +257,12 @@ function resolveSkillRecord(
205
257
  function createSkillsExecutor(
206
258
  watcher: UserInstructionConfigWatcher,
207
259
  watcherReady: Promise<void>,
260
+ allowedSkillNames?: ReadonlyArray<string>,
208
261
  ): SkillsExecutorWithMetadata {
209
262
  const runningSkills = new Set<string>();
210
263
  const executor: SkillsExecutorWithMetadata = async (skillName, args) => {
211
264
  await watcherReady;
212
- const resolved = resolveSkillRecord(watcher, skillName);
265
+ const resolved = resolveSkillRecord(watcher, skillName, allowedSkillNames);
213
266
  if ("error" in resolved) {
214
267
  return resolved.error;
215
268
  }
@@ -235,7 +288,7 @@ function createSkillsExecutor(
235
288
  }
236
289
  };
237
290
  Object.defineProperty(executor, "configuredSkills", {
238
- get: () => listConfiguredSkills(watcher),
291
+ get: () => listConfiguredSkills(watcher, allowedSkillNames),
239
292
  enumerable: true,
240
293
  configurable: false,
241
294
  });
@@ -340,11 +393,12 @@ export class DefaultRuntimeBuilder implements RuntimeBuilder {
340
393
  userInstructionWatcher &&
341
394
  (watcherProvided ||
342
395
  hasSkillsFiles(config.cwd) ||
343
- listConfiguredSkills(userInstructionWatcher).length > 0)
396
+ listConfiguredSkills(userInstructionWatcher, config.skills).length > 0)
344
397
  ) {
345
398
  skillsExecutor = createSkillsExecutor(
346
399
  userInstructionWatcher,
347
400
  watcherReady,
401
+ config.skills,
348
402
  );
349
403
  }
350
404
 
@@ -15,9 +15,19 @@ interface SandboxResponseMessage {
15
15
  error?: { message: string; stack?: string };
16
16
  }
17
17
 
18
+ interface SandboxEventMessage {
19
+ type: "event";
20
+ name: string;
21
+ payload?: unknown;
22
+ }
23
+
18
24
  export interface SubprocessSandboxOptions {
19
- bootstrapScript: string;
25
+ /** Inline script to execute via `node -e`. Mutually exclusive with {@link bootstrapFile}. */
26
+ bootstrapScript?: string;
27
+ /** Path to a JavaScript file to execute via `node <file>`. Mutually exclusive with {@link bootstrapScript}. */
28
+ bootstrapFile?: string;
20
29
  name?: string;
30
+ onEvent?: (event: { name: string; payload?: unknown }) => void;
21
31
  }
22
32
 
23
33
  export interface SandboxCallOptions {
@@ -52,16 +62,16 @@ export class SubprocessSandbox {
52
62
  return;
53
63
  }
54
64
 
55
- const child = spawn(
56
- process.execPath,
57
- ["-e", this.options.bootstrapScript],
58
- {
59
- stdio: ["ignore", "ignore", "ignore", "ipc"],
60
- },
61
- );
65
+ const args = this.options.bootstrapFile
66
+ ? [this.options.bootstrapFile]
67
+ : ["-e", this.options.bootstrapScript ?? ""];
68
+
69
+ const child = spawn(process.execPath, args, {
70
+ stdio: ["ignore", "ignore", "ignore", "ipc"],
71
+ });
62
72
  this.process = child;
63
73
  child.on("message", (message) => {
64
- this.onMessage(message as SandboxResponseMessage);
74
+ this.onMessage(message as SandboxResponseMessage | SandboxEventMessage);
65
75
  });
66
76
  child.on("error", (error) => {
67
77
  this.failPending(
@@ -171,8 +181,22 @@ export class SubprocessSandbox {
171
181
  this.failPending(new Error(`${this.options.name ?? "sandbox"} shutdown`));
172
182
  }
173
183
 
174
- private onMessage(message: SandboxResponseMessage): void {
175
- if (!message || message.type !== "response" || !message.id) {
184
+ private onMessage(
185
+ message: SandboxResponseMessage | SandboxEventMessage,
186
+ ): void {
187
+ if (!message) {
188
+ return;
189
+ }
190
+ if (message.type === "event") {
191
+ if (typeof message.name === "string" && message.name.length > 0) {
192
+ this.options.onEvent?.({
193
+ name: message.name,
194
+ payload: message.payload,
195
+ });
196
+ }
197
+ return;
198
+ }
199
+ if (message.type !== "response" || !message.id) {
176
200
  return;
177
201
  }
178
202
  const pending = this.pending.get(message.id);
@@ -11,8 +11,9 @@ import { tmpdir } from "node:os";
11
11
  import { join } from "node:path";
12
12
  import type { AgentResult } from "@clinebot/agents";
13
13
  import type { LlmsProviders } from "@clinebot/llms";
14
+ import { setClineDir, setHomeDir } from "@clinebot/shared/storage";
14
15
  import { nanoid } from "nanoid";
15
- import { afterEach, describe, expect, it, vi } from "vitest";
16
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
16
17
  import type { SessionSource, SessionStatus } from "../types/common";
17
18
  import { DefaultSessionManager } from "./default-session-manager";
18
19
  import type { SessionManifest } from "./session-manifest";
@@ -222,12 +223,30 @@ class LocalFileSessionService {
222
223
  }
223
224
 
224
225
  describe("DefaultSessionManager e2e", () => {
226
+ const envSnapshot = {
227
+ HOME: process.env.HOME,
228
+ CLINE_DIR: process.env.CLINE_DIR,
229
+ };
225
230
  const tempDirs: string[] = [];
231
+ let isolatedHomeDir = "";
232
+
233
+ beforeEach(() => {
234
+ isolatedHomeDir = mkdtempSync(join(tmpdir(), "core-session-home-"));
235
+ process.env.HOME = isolatedHomeDir;
236
+ process.env.CLINE_DIR = join(isolatedHomeDir, ".cline");
237
+ setHomeDir(isolatedHomeDir);
238
+ setClineDir(process.env.CLINE_DIR);
239
+ });
226
240
 
227
241
  afterEach(() => {
242
+ process.env.HOME = envSnapshot.HOME;
243
+ process.env.CLINE_DIR = envSnapshot.CLINE_DIR;
244
+ setHomeDir(envSnapshot.HOME ?? "~");
245
+ setClineDir(envSnapshot.CLINE_DIR ?? join("~", ".cline"));
228
246
  for (const dir of tempDirs.splice(0)) {
229
247
  rmSync(dir, { recursive: true, force: true });
230
248
  }
249
+ rmSync(isolatedHomeDir, { recursive: true, force: true });
231
250
  });
232
251
 
233
252
  it("runs an interactive lifecycle with real artifact files", async () => {