@clinebot/core 0.0.11 → 0.0.13
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/README.md +1 -1
- 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-loader.d.ts +1 -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 +5 -0
- package/dist/index.node.js +685 -413
- package/dist/runtime/commands.d.ts +11 -0
- package/dist/runtime/sandbox/subprocess-sandbox.d.ts +8 -1
- package/dist/runtime/skills.d.ts +13 -0
- 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/session-service.d.ts +22 -22
- package/dist/session/unified-session-persistence-service.d.ts +12 -6
- package/dist/session/utils/helpers.d.ts +2 -2
- package/dist/session/utils/types.d.ts +9 -0
- package/dist/tools/definitions.d.ts +2 -2
- package/dist/tools/presets.d.ts +3 -3
- package/dist/tools/schemas.d.ts +15 -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 +127 -1
- package/src/agents/plugin-loader.ts +72 -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 +14 -0
- package/src/runtime/commands.test.ts +98 -0
- package/src/runtime/commands.ts +83 -0
- package/src/runtime/hook-file-hooks.test.ts +1 -1
- package/src/runtime/hook-file-hooks.ts +16 -6
- package/src/runtime/index.ts +10 -0
- 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/runtime/skills.ts +44 -0
- package/src/runtime/workflows.ts +20 -29
- package/src/session/default-session-manager.e2e.test.ts +52 -33
- package/src/session/default-session-manager.test.ts +453 -1
- package/src/session/default-session-manager.ts +210 -12
- package/src/session/rpc-session-service.ts +14 -96
- package/src/session/session-config-builder.ts +2 -0
- package/src/session/session-manager.ts +1 -0
- package/src/session/session-service.ts +127 -64
- package/src/session/session-team-coordination.ts +30 -0
- package/src/session/unified-session-persistence-service.test.ts +3 -3
- package/src/session/unified-session-persistence-service.ts +159 -141
- package/src/session/utils/helpers.ts +22 -41
- package/src/session/utils/types.ts +10 -0
- package/src/storage/sqlite-team-store.ts +16 -5
- package/src/tools/definitions.test.ts +137 -8
- package/src/tools/definitions.ts +115 -70
- package/src/tools/presets.test.ts +2 -3
- package/src/tools/presets.ts +3 -3
- package/src/tools/schemas.ts +28 -28
- package/src/types/config.ts +5 -0
- package/src/types/events.ts +23 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
5
|
+
import { createUserInstructionConfigWatcher } from "../agents";
|
|
6
|
+
import {
|
|
7
|
+
listAvailableRuntimeCommandsFromWatcher,
|
|
8
|
+
resolveRuntimeSlashCommandFromWatcher,
|
|
9
|
+
} from "./commands";
|
|
10
|
+
|
|
11
|
+
describe("runtime command registry", () => {
|
|
12
|
+
const tempRoots: string[] = [];
|
|
13
|
+
|
|
14
|
+
afterEach(async () => {
|
|
15
|
+
await Promise.all(
|
|
16
|
+
tempRoots.map((dir) => rm(dir, { recursive: true, force: true })),
|
|
17
|
+
);
|
|
18
|
+
tempRoots.length = 0;
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("lists workflow and skill commands together", async () => {
|
|
22
|
+
const tempRoot = await mkdtemp(join(tmpdir(), "core-runtime-commands-"));
|
|
23
|
+
tempRoots.push(tempRoot);
|
|
24
|
+
const skillDir = join(tempRoot, "skills", "debug");
|
|
25
|
+
const workflowsDir = join(tempRoot, "workflows");
|
|
26
|
+
await mkdir(skillDir, { recursive: true });
|
|
27
|
+
await mkdir(workflowsDir, { recursive: true });
|
|
28
|
+
await writeFile(join(skillDir, "SKILL.md"), "Use the debugging skill.");
|
|
29
|
+
await writeFile(
|
|
30
|
+
join(workflowsDir, "release.md"),
|
|
31
|
+
`---
|
|
32
|
+
name: release
|
|
33
|
+
---
|
|
34
|
+
Run the release workflow.`,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const watcher = createUserInstructionConfigWatcher({
|
|
38
|
+
skills: { directories: [join(tempRoot, "skills")] },
|
|
39
|
+
rules: { directories: [] },
|
|
40
|
+
workflows: { directories: [workflowsDir] },
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
await watcher.start();
|
|
45
|
+
expect(listAvailableRuntimeCommandsFromWatcher(watcher)).toEqual([
|
|
46
|
+
{
|
|
47
|
+
id: "debug",
|
|
48
|
+
name: "debug",
|
|
49
|
+
instructions: "Use the debugging skill.",
|
|
50
|
+
kind: "skill",
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: "release",
|
|
54
|
+
name: "release",
|
|
55
|
+
instructions: "Run the release workflow.",
|
|
56
|
+
kind: "workflow",
|
|
57
|
+
},
|
|
58
|
+
]);
|
|
59
|
+
} finally {
|
|
60
|
+
watcher.stop();
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("expands skill and workflow slash commands with workflow precedence", async () => {
|
|
65
|
+
const tempRoot = await mkdtemp(join(tmpdir(), "core-runtime-commands-"));
|
|
66
|
+
tempRoots.push(tempRoot);
|
|
67
|
+
const skillDir = join(tempRoot, "skills", "ship");
|
|
68
|
+
const workflowsDir = join(tempRoot, "workflows");
|
|
69
|
+
await mkdir(skillDir, { recursive: true });
|
|
70
|
+
await mkdir(workflowsDir, { recursive: true });
|
|
71
|
+
await writeFile(join(skillDir, "SKILL.md"), "Use the ship skill.");
|
|
72
|
+
await writeFile(
|
|
73
|
+
join(workflowsDir, "ship.md"),
|
|
74
|
+
`---
|
|
75
|
+
name: ship
|
|
76
|
+
---
|
|
77
|
+
Run the ship workflow.`,
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const watcher = createUserInstructionConfigWatcher({
|
|
81
|
+
skills: { directories: [join(tempRoot, "skills")] },
|
|
82
|
+
rules: { directories: [] },
|
|
83
|
+
workflows: { directories: [workflowsDir] },
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
await watcher.start();
|
|
88
|
+
expect(resolveRuntimeSlashCommandFromWatcher("/ship", watcher)).toBe(
|
|
89
|
+
"Run the ship workflow.",
|
|
90
|
+
);
|
|
91
|
+
expect(resolveRuntimeSlashCommandFromWatcher("/ship now", watcher)).toBe(
|
|
92
|
+
"Run the ship workflow. now",
|
|
93
|
+
);
|
|
94
|
+
} finally {
|
|
95
|
+
watcher.stop();
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
SkillConfig,
|
|
3
|
+
UserInstructionConfigWatcher,
|
|
4
|
+
WorkflowConfig,
|
|
5
|
+
} from "../agents";
|
|
6
|
+
|
|
7
|
+
export type RuntimeCommandKind = "skill" | "workflow";
|
|
8
|
+
|
|
9
|
+
export type AvailableRuntimeCommand = {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
instructions: string;
|
|
13
|
+
kind: RuntimeCommandKind;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type CommandRecord = {
|
|
17
|
+
item: SkillConfig | WorkflowConfig;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function isCommandEnabled(command: SkillConfig | WorkflowConfig): boolean {
|
|
21
|
+
return command.disabled !== true;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function listCommandsForKind(
|
|
25
|
+
watcher: UserInstructionConfigWatcher,
|
|
26
|
+
kind: RuntimeCommandKind,
|
|
27
|
+
): AvailableRuntimeCommand[] {
|
|
28
|
+
return [...watcher.getSnapshot(kind).entries()]
|
|
29
|
+
.map(([id, record]) => ({ id, record: record as CommandRecord }))
|
|
30
|
+
.filter(({ record }) => isCommandEnabled(record.item))
|
|
31
|
+
.map(({ id, record }) => ({
|
|
32
|
+
id,
|
|
33
|
+
name: record.item.name,
|
|
34
|
+
instructions: record.item.instructions,
|
|
35
|
+
kind,
|
|
36
|
+
}))
|
|
37
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function listAvailableRuntimeCommandsFromWatcher(
|
|
41
|
+
watcher: UserInstructionConfigWatcher,
|
|
42
|
+
): AvailableRuntimeCommand[] {
|
|
43
|
+
const byName = new Map<string, AvailableRuntimeCommand>();
|
|
44
|
+
for (const command of [
|
|
45
|
+
...listCommandsForKind(watcher, "workflow"),
|
|
46
|
+
...listCommandsForKind(watcher, "skill"),
|
|
47
|
+
]) {
|
|
48
|
+
if (!byName.has(command.name)) {
|
|
49
|
+
byName.set(command.name, command);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function resolveRuntimeSlashCommandFromWatcher(
|
|
56
|
+
input: string,
|
|
57
|
+
watcher: UserInstructionConfigWatcher,
|
|
58
|
+
): string {
|
|
59
|
+
if (!input.startsWith("/") || input.length < 2) {
|
|
60
|
+
return input;
|
|
61
|
+
}
|
|
62
|
+
const match = input.match(/^\/(\S+)/);
|
|
63
|
+
if (!match) {
|
|
64
|
+
return input;
|
|
65
|
+
}
|
|
66
|
+
const name = match[1];
|
|
67
|
+
if (!name) {
|
|
68
|
+
return input;
|
|
69
|
+
}
|
|
70
|
+
const commandLength = name.length + 1;
|
|
71
|
+
const remainder = input.slice(commandLength);
|
|
72
|
+
const matched = listAvailableRuntimeCommandsFromWatcher(watcher).find(
|
|
73
|
+
(command) => command.name === name,
|
|
74
|
+
);
|
|
75
|
+
return matched ? `${matched.instructions}${remainder}` : input;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function listAvailableRuntimeCommandsForKindFromWatcher(
|
|
79
|
+
watcher: UserInstructionConfigWatcher,
|
|
80
|
+
kind: RuntimeCommandKind,
|
|
81
|
+
): AvailableRuntimeCommand[] {
|
|
82
|
+
return listCommandsForKind(watcher, kind);
|
|
83
|
+
}
|
|
@@ -57,7 +57,7 @@ describe("createHookConfigFileHooks", () => {
|
|
|
57
57
|
it("executes extensionless legacy hook files via bash fallback", async () => {
|
|
58
58
|
const { workspace } = await createWorkspaceWithHook(
|
|
59
59
|
"PreToolUse",
|
|
60
|
-
'echo \'HOOK_CONTROL\t{"cancel":true,"context":"legacy-ok"}\'\n',
|
|
60
|
+
'echo \'HOOK_CONTROL\t{"cancel":true,"context":"legacy-ok"}\'\nexit 0\n',
|
|
61
61
|
);
|
|
62
62
|
try {
|
|
63
63
|
const hooks = createHookConfigFileHooks({
|
|
@@ -215,13 +215,17 @@ async function writeToChildStdin(
|
|
|
215
215
|
|
|
216
216
|
await new Promise<void>((resolve, reject) => {
|
|
217
217
|
let settled = false;
|
|
218
|
+
const cleanup = () => {
|
|
219
|
+
stdin.off("error", onError);
|
|
220
|
+
stdin.off("finish", onFinish);
|
|
221
|
+
child.off("close", onChildClose);
|
|
222
|
+
};
|
|
218
223
|
const finish = (error?: Error | null) => {
|
|
219
224
|
if (settled) {
|
|
220
225
|
return;
|
|
221
226
|
}
|
|
222
227
|
settled = true;
|
|
223
|
-
|
|
224
|
-
stdin.off("close", onClose);
|
|
228
|
+
cleanup();
|
|
225
229
|
if (error) {
|
|
226
230
|
const code = (error as Error & { code?: string }).code;
|
|
227
231
|
if (code === "EPIPE" || code === "ERR_STREAM_DESTROYED") {
|
|
@@ -234,10 +238,16 @@ async function writeToChildStdin(
|
|
|
234
238
|
resolve();
|
|
235
239
|
};
|
|
236
240
|
const onError = (error: Error) => finish(error);
|
|
237
|
-
const
|
|
241
|
+
const onFinish = () => finish();
|
|
242
|
+
const onChildClose = () => finish();
|
|
238
243
|
stdin.on("error", onError);
|
|
239
|
-
stdin.once("
|
|
240
|
-
|
|
244
|
+
stdin.once("finish", onFinish);
|
|
245
|
+
child.once("close", onChildClose);
|
|
246
|
+
try {
|
|
247
|
+
stdin.end(body);
|
|
248
|
+
} catch (error) {
|
|
249
|
+
finish(error as Error);
|
|
250
|
+
}
|
|
241
251
|
});
|
|
242
252
|
}
|
|
243
253
|
|
|
@@ -270,10 +280,10 @@ async function runHookCommand(
|
|
|
270
280
|
});
|
|
271
281
|
|
|
272
282
|
const body = JSON.stringify(payload);
|
|
283
|
+
await Promise.race([spawned, childError]);
|
|
273
284
|
await writeToChildStdin(child, body);
|
|
274
285
|
|
|
275
286
|
if (options.detached) {
|
|
276
|
-
await Promise.race([spawned, childError]);
|
|
277
287
|
child.unref();
|
|
278
288
|
return;
|
|
279
289
|
}
|
package/src/runtime/index.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
export {
|
|
2
|
+
type AvailableRuntimeCommand,
|
|
3
|
+
listAvailableRuntimeCommandsFromWatcher,
|
|
4
|
+
resolveRuntimeSlashCommandFromWatcher,
|
|
5
|
+
} from "./commands";
|
|
1
6
|
export {
|
|
2
7
|
formatRulesForSystemPrompt,
|
|
3
8
|
isRuleEnabled,
|
|
@@ -16,6 +21,11 @@ export type {
|
|
|
16
21
|
RuntimeBuilderInput,
|
|
17
22
|
SessionRuntime,
|
|
18
23
|
} from "./session-runtime";
|
|
24
|
+
export {
|
|
25
|
+
type AvailableSkill,
|
|
26
|
+
listAvailableSkillsFromWatcher,
|
|
27
|
+
resolveSkillsSlashCommandFromWatcher,
|
|
28
|
+
} from "./skills";
|
|
19
29
|
export {
|
|
20
30
|
type DesktopToolApprovalOptions,
|
|
21
31
|
requestDesktopToolApproval,
|
|
@@ -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);
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { UserInstructionConfigWatcher } from "../agents";
|
|
2
|
+
import {
|
|
3
|
+
listAvailableRuntimeCommandsForKindFromWatcher,
|
|
4
|
+
resolveRuntimeSlashCommandFromWatcher,
|
|
5
|
+
} from "./commands";
|
|
6
|
+
|
|
7
|
+
export type AvailableSkill = {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
instructions: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function matchesLeadingSlashCommand(input: string, name: string): boolean {
|
|
14
|
+
const match = input.match(/^\/(\S+)/);
|
|
15
|
+
return match?.[1] === name;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function listAvailableSkillsFromWatcher(
|
|
19
|
+
watcher: UserInstructionConfigWatcher,
|
|
20
|
+
): AvailableSkill[] {
|
|
21
|
+
return listAvailableRuntimeCommandsForKindFromWatcher(watcher, "skill").map(
|
|
22
|
+
(skill) => ({
|
|
23
|
+
id: skill.id,
|
|
24
|
+
name: skill.name,
|
|
25
|
+
instructions: skill.instructions,
|
|
26
|
+
}),
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Expands a leading slash command (e.g. "/release") to skill instructions.
|
|
32
|
+
* If the input starts with "/<skill-name>", that prefix is replaced and the
|
|
33
|
+
* remaining input is preserved unchanged.
|
|
34
|
+
*/
|
|
35
|
+
export function resolveSkillsSlashCommandFromWatcher(
|
|
36
|
+
input: string,
|
|
37
|
+
watcher: UserInstructionConfigWatcher,
|
|
38
|
+
): string {
|
|
39
|
+
const resolved = resolveRuntimeSlashCommandFromWatcher(input, watcher);
|
|
40
|
+
const matched = listAvailableSkillsFromWatcher(watcher).some((skill) =>
|
|
41
|
+
matchesLeadingSlashCommand(input, skill.name),
|
|
42
|
+
);
|
|
43
|
+
return matched ? resolved : input;
|
|
44
|
+
}
|
package/src/runtime/workflows.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import type { UserInstructionConfigWatcher
|
|
1
|
+
import type { UserInstructionConfigWatcher } from "../agents";
|
|
2
|
+
import {
|
|
3
|
+
listAvailableRuntimeCommandsForKindFromWatcher,
|
|
4
|
+
resolveRuntimeSlashCommandFromWatcher,
|
|
5
|
+
} from "./commands";
|
|
2
6
|
|
|
3
7
|
export type AvailableWorkflow = {
|
|
4
8
|
id: string;
|
|
@@ -6,23 +10,22 @@ export type AvailableWorkflow = {
|
|
|
6
10
|
instructions: string;
|
|
7
11
|
};
|
|
8
12
|
|
|
9
|
-
function
|
|
10
|
-
|
|
13
|
+
function matchesLeadingSlashCommand(input: string, name: string): boolean {
|
|
14
|
+
const match = input.match(/^\/(\S+)/);
|
|
15
|
+
return match?.[1] === name;
|
|
11
16
|
}
|
|
12
17
|
|
|
13
18
|
export function listAvailableWorkflowsFromWatcher(
|
|
14
19
|
watcher: UserInstructionConfigWatcher,
|
|
15
20
|
): AvailableWorkflow[] {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
.
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}))
|
|
25
|
-
.sort((a, b) => a.name.localeCompare(b.name));
|
|
21
|
+
return listAvailableRuntimeCommandsForKindFromWatcher(
|
|
22
|
+
watcher,
|
|
23
|
+
"workflow",
|
|
24
|
+
).map((workflow) => ({
|
|
25
|
+
id: workflow.id,
|
|
26
|
+
name: workflow.name,
|
|
27
|
+
instructions: workflow.instructions,
|
|
28
|
+
}));
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
/**
|
|
@@ -34,21 +37,9 @@ export function resolveWorkflowSlashCommandFromWatcher(
|
|
|
34
37
|
input: string,
|
|
35
38
|
watcher: UserInstructionConfigWatcher,
|
|
36
39
|
): string {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const match = input.match(/^\/(\S+)/);
|
|
41
|
-
if (!match) {
|
|
42
|
-
return input;
|
|
43
|
-
}
|
|
44
|
-
const workflowName = match[1];
|
|
45
|
-
if (!workflowName) {
|
|
46
|
-
return input;
|
|
47
|
-
}
|
|
48
|
-
const commandLength = workflowName.length + 1;
|
|
49
|
-
const remainder = input.slice(commandLength);
|
|
50
|
-
const matched = listAvailableWorkflowsFromWatcher(watcher).find(
|
|
51
|
-
(workflow) => workflow.name === workflowName,
|
|
40
|
+
const resolved = resolveRuntimeSlashCommandFromWatcher(input, watcher);
|
|
41
|
+
const matched = listAvailableWorkflowsFromWatcher(watcher).some((workflow) =>
|
|
42
|
+
matchesLeadingSlashCommand(input, workflow.name),
|
|
52
43
|
);
|
|
53
|
-
return matched ?
|
|
44
|
+
return matched ? resolved : input;
|
|
54
45
|
}
|