@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.
Files changed (68) hide show
  1. package/README.md +1 -1
  2. package/dist/agents/agent-config-loader.d.ts +1 -1
  3. package/dist/agents/agent-config-parser.d.ts +5 -2
  4. package/dist/agents/index.d.ts +1 -1
  5. package/dist/agents/plugin-config-loader.d.ts +4 -0
  6. package/dist/agents/plugin-loader.d.ts +1 -0
  7. package/dist/agents/plugin-sandbox-bootstrap.js +446 -0
  8. package/dist/agents/plugin-sandbox.d.ts +4 -0
  9. package/dist/index.node.d.ts +5 -0
  10. package/dist/index.node.js +685 -413
  11. package/dist/runtime/commands.d.ts +11 -0
  12. package/dist/runtime/sandbox/subprocess-sandbox.d.ts +8 -1
  13. package/dist/runtime/skills.d.ts +13 -0
  14. package/dist/session/default-session-manager.d.ts +5 -0
  15. package/dist/session/session-config-builder.d.ts +4 -1
  16. package/dist/session/session-manager.d.ts +1 -0
  17. package/dist/session/session-service.d.ts +22 -22
  18. package/dist/session/unified-session-persistence-service.d.ts +12 -6
  19. package/dist/session/utils/helpers.d.ts +2 -2
  20. package/dist/session/utils/types.d.ts +9 -0
  21. package/dist/tools/definitions.d.ts +2 -2
  22. package/dist/tools/presets.d.ts +3 -3
  23. package/dist/tools/schemas.d.ts +15 -14
  24. package/dist/types/config.d.ts +5 -0
  25. package/dist/types/events.d.ts +22 -0
  26. package/package.json +5 -4
  27. package/src/agents/agent-config-loader.test.ts +2 -0
  28. package/src/agents/agent-config-loader.ts +1 -0
  29. package/src/agents/agent-config-parser.ts +12 -5
  30. package/src/agents/index.ts +1 -0
  31. package/src/agents/plugin-config-loader.test.ts +49 -0
  32. package/src/agents/plugin-config-loader.ts +10 -73
  33. package/src/agents/plugin-loader.test.ts +127 -1
  34. package/src/agents/plugin-loader.ts +72 -5
  35. package/src/agents/plugin-sandbox-bootstrap.ts +445 -0
  36. package/src/agents/plugin-sandbox.test.ts +198 -1
  37. package/src/agents/plugin-sandbox.ts +223 -353
  38. package/src/index.node.ts +14 -0
  39. package/src/runtime/commands.test.ts +98 -0
  40. package/src/runtime/commands.ts +83 -0
  41. package/src/runtime/hook-file-hooks.test.ts +1 -1
  42. package/src/runtime/hook-file-hooks.ts +16 -6
  43. package/src/runtime/index.ts +10 -0
  44. package/src/runtime/runtime-builder.test.ts +67 -0
  45. package/src/runtime/runtime-builder.ts +70 -16
  46. package/src/runtime/sandbox/subprocess-sandbox.ts +35 -11
  47. package/src/runtime/skills.ts +44 -0
  48. package/src/runtime/workflows.ts +20 -29
  49. package/src/session/default-session-manager.e2e.test.ts +52 -33
  50. package/src/session/default-session-manager.test.ts +453 -1
  51. package/src/session/default-session-manager.ts +210 -12
  52. package/src/session/rpc-session-service.ts +14 -96
  53. package/src/session/session-config-builder.ts +2 -0
  54. package/src/session/session-manager.ts +1 -0
  55. package/src/session/session-service.ts +127 -64
  56. package/src/session/session-team-coordination.ts +30 -0
  57. package/src/session/unified-session-persistence-service.test.ts +3 -3
  58. package/src/session/unified-session-persistence-service.ts +159 -141
  59. package/src/session/utils/helpers.ts +22 -41
  60. package/src/session/utils/types.ts +10 -0
  61. package/src/storage/sqlite-team-store.ts +16 -5
  62. package/src/tools/definitions.test.ts +137 -8
  63. package/src/tools/definitions.ts +115 -70
  64. package/src/tools/presets.test.ts +2 -3
  65. package/src/tools/presets.ts +3 -3
  66. package/src/tools/schemas.ts +28 -28
  67. package/src/types/config.ts +5 -0
  68. 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
- stdin.off("error", onError);
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 onClose = () => finish();
241
+ const onFinish = () => finish();
242
+ const onChildClose = () => finish();
238
243
  stdin.on("error", onError);
239
- stdin.once("close", onClose);
240
- stdin.end(body, (error?: Error | null) => finish(error));
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
  }
@@ -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()].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);
@@ -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
+ }
@@ -1,4 +1,8 @@
1
- import type { UserInstructionConfigWatcher, WorkflowConfig } from "../agents";
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 isWorkflowEnabled(workflow: WorkflowConfig): boolean {
10
- return workflow.disabled !== true;
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
- const snapshot = watcher.getSnapshot("workflow");
17
- return [...snapshot.entries()]
18
- .map(([id, record]) => ({ id, workflow: record.item as WorkflowConfig }))
19
- .filter(({ workflow }) => isWorkflowEnabled(workflow))
20
- .map(({ id, workflow }) => ({
21
- id,
22
- name: workflow.name,
23
- instructions: workflow.instructions,
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
- if (!input.startsWith("/") || input.length < 2) {
38
- return input;
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 ? `${matched.instructions}${remainder}` : input;
44
+ return matched ? resolved : input;
54
45
  }