@clinebot/core 0.0.10 → 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.
- package/dist/agents/agent-config-loader.d.ts +1 -1
- package/dist/agents/agent-config-parser.d.ts +5 -2
- package/dist/agents/index.d.ts +1 -1
- package/dist/agents/plugin-config-loader.d.ts +4 -0
- package/dist/agents/plugin-sandbox-bootstrap.js +446 -0
- package/dist/agents/plugin-sandbox.d.ts +4 -0
- package/dist/index.node.d.ts +1 -1
- package/dist/index.node.js +658 -407
- package/dist/runtime/sandbox/subprocess-sandbox.d.ts +8 -1
- package/dist/session/default-session-manager.d.ts +5 -0
- package/dist/session/session-config-builder.d.ts +4 -1
- package/dist/session/session-manager.d.ts +1 -0
- package/dist/session/unified-session-persistence-service.d.ts +6 -0
- package/dist/session/utils/helpers.d.ts +1 -1
- package/dist/session/utils/types.d.ts +10 -0
- package/dist/tools/definitions.d.ts +2 -2
- package/dist/tools/presets.d.ts +3 -3
- package/dist/tools/schemas.d.ts +14 -14
- package/dist/types/config.d.ts +5 -0
- package/dist/types/events.d.ts +22 -0
- package/package.json +5 -4
- package/src/agents/agent-config-loader.test.ts +2 -0
- package/src/agents/agent-config-loader.ts +1 -0
- package/src/agents/agent-config-parser.ts +12 -5
- package/src/agents/index.ts +1 -0
- package/src/agents/plugin-config-loader.test.ts +49 -0
- package/src/agents/plugin-config-loader.ts +10 -73
- package/src/agents/plugin-loader.test.ts +128 -2
- package/src/agents/plugin-loader.ts +70 -5
- package/src/agents/plugin-sandbox-bootstrap.ts +445 -0
- package/src/agents/plugin-sandbox.test.ts +198 -1
- package/src/agents/plugin-sandbox.ts +223 -353
- package/src/index.node.ts +4 -0
- package/src/runtime/hook-file-hooks.test.ts +1 -1
- package/src/runtime/hook-file-hooks.ts +16 -6
- package/src/runtime/runtime-builder.test.ts +67 -0
- package/src/runtime/runtime-builder.ts +70 -16
- package/src/runtime/sandbox/subprocess-sandbox.ts +35 -11
- package/src/session/default-session-manager.e2e.test.ts +20 -1
- package/src/session/default-session-manager.test.ts +584 -1
- package/src/session/default-session-manager.ts +205 -1
- package/src/session/session-config-builder.ts +2 -0
- package/src/session/session-manager.ts +1 -0
- package/src/session/session-team-coordination.ts +30 -0
- package/src/session/unified-session-persistence-service.ts +45 -0
- package/src/session/utils/helpers.ts +13 -3
- package/src/session/utils/types.ts +11 -0
- package/src/storage/sqlite-team-store.ts +16 -5
- package/src/tools/definitions.test.ts +87 -8
- package/src/tools/definitions.ts +89 -70
- package/src/tools/presets.test.ts +2 -3
- package/src/tools/presets.ts +3 -3
- package/src/tools/schemas.ts +23 -22
- package/src/types/config.ts +5 -0
- 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()]
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
|
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 = [...
|
|
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
|
-
|
|
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
|
|
56
|
-
|
|
57
|
-
["-e", this.options.bootstrapScript]
|
|
58
|
-
|
|
59
|
-
|
|
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(
|
|
175
|
-
|
|
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 () => {
|