@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.
Files changed (55) 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 -1
  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/helpers.d.ts +1 -1
  15. package/dist/session/utils/types.d.ts +10 -0
  16. package/dist/tools/definitions.d.ts +2 -2
  17. package/dist/tools/presets.d.ts +3 -3
  18. package/dist/tools/schemas.d.ts +14 -14
  19. package/dist/types/config.d.ts +5 -0
  20. package/dist/types/events.d.ts +22 -0
  21. package/package.json +5 -4
  22. package/src/agents/agent-config-loader.test.ts +2 -0
  23. package/src/agents/agent-config-loader.ts +1 -0
  24. package/src/agents/agent-config-parser.ts +12 -5
  25. package/src/agents/index.ts +1 -0
  26. package/src/agents/plugin-config-loader.test.ts +49 -0
  27. package/src/agents/plugin-config-loader.ts +10 -73
  28. package/src/agents/plugin-loader.test.ts +128 -2
  29. package/src/agents/plugin-loader.ts +70 -5
  30. package/src/agents/plugin-sandbox-bootstrap.ts +445 -0
  31. package/src/agents/plugin-sandbox.test.ts +198 -1
  32. package/src/agents/plugin-sandbox.ts +223 -353
  33. package/src/index.node.ts +4 -0
  34. package/src/runtime/hook-file-hooks.test.ts +1 -1
  35. package/src/runtime/hook-file-hooks.ts +16 -6
  36. package/src/runtime/runtime-builder.test.ts +67 -0
  37. package/src/runtime/runtime-builder.ts +70 -16
  38. package/src/runtime/sandbox/subprocess-sandbox.ts +35 -11
  39. package/src/session/default-session-manager.e2e.test.ts +20 -1
  40. package/src/session/default-session-manager.test.ts +584 -1
  41. package/src/session/default-session-manager.ts +205 -1
  42. package/src/session/session-config-builder.ts +2 -0
  43. package/src/session/session-manager.ts +1 -0
  44. package/src/session/session-team-coordination.ts +30 -0
  45. package/src/session/unified-session-persistence-service.ts +45 -0
  46. package/src/session/utils/helpers.ts +13 -3
  47. package/src/session/utils/types.ts +11 -0
  48. package/src/storage/sqlite-team-store.ts +16 -5
  49. package/src/tools/definitions.test.ts +87 -8
  50. package/src/tools/definitions.ts +89 -70
  51. package/src/tools/presets.test.ts +2 -3
  52. package/src/tools/presets.ts +3 -3
  53. package/src/tools/schemas.ts +23 -22
  54. package/src/types/config.ts +5 -0
  55. package/src/types/events.ts +23 -0
@@ -1,6 +1,13 @@
1
1
  export interface SubprocessSandboxOptions {
2
- bootstrapScript: string;
2
+ /** Inline script to execute via `node -e`. Mutually exclusive with {@link bootstrapFile}. */
3
+ bootstrapScript?: string;
4
+ /** Path to a JavaScript file to execute via `node <file>`. Mutually exclusive with {@link bootstrapScript}. */
5
+ bootstrapFile?: string;
3
6
  name?: string;
7
+ onEvent?: (event: {
8
+ name: string;
9
+ payload?: unknown;
10
+ }) => void;
4
11
  }
5
12
  export interface SandboxCallOptions {
6
13
  timeoutMs?: number;
@@ -60,7 +60,12 @@ export declare class DefaultSessionManager implements SessionManager {
60
60
  private failSession;
61
61
  private shutdownSession;
62
62
  private updateStatus;
63
+ private handlePluginEvent;
64
+ private enqueuePendingPrompt;
65
+ private drainPendingPrompts;
63
66
  private onAgentEvent;
67
+ private emitPendingPrompts;
68
+ private emitPendingPromptSubmitted;
64
69
  private createSpawnTool;
65
70
  private handleTeamEvent;
66
71
  private runWithAuthRetry;
@@ -5,7 +5,10 @@ import type { CoreSessionConfig } from "../types/config";
5
5
  import { type ProviderSettings } from "../types/provider-settings";
6
6
  import type { StartSessionInput } from "./session-manager";
7
7
  export declare function resolveWorkspacePath(config: CoreSessionConfig): string;
8
- export declare function buildEffectiveConfig(input: StartSessionInput, hookPath: string, sessionId: string, defaultTelemetry: ITelemetryService | undefined): Promise<{
8
+ export declare function buildEffectiveConfig(input: StartSessionInput, hookPath: string, sessionId: string, defaultTelemetry: ITelemetryService | undefined, onPluginEvent?: (event: {
9
+ name: string;
10
+ payload?: unknown;
11
+ }) => void): Promise<{
9
12
  config: CoreSessionConfig;
10
13
  pluginSandboxShutdown?: () => Promise<void>;
11
14
  }>;
@@ -33,6 +33,7 @@ export interface SendSessionInput {
33
33
  prompt: string;
34
34
  userImages?: string[];
35
35
  userFiles?: string[];
36
+ delivery?: "queue" | "steer";
36
37
  }
37
38
  export interface SessionAccumulatedUsage {
38
39
  inputTokens: number;
@@ -44,9 +44,12 @@ export interface SessionPersistenceAdapter {
44
44
  export declare class UnifiedSessionPersistenceService {
45
45
  private readonly adapter;
46
46
  private readonly teamTaskSessionsByAgent;
47
+ private readonly teamTaskLastHeartbeatBySession;
48
+ private readonly teamTaskLastProgressLineBySession;
47
49
  protected readonly artifacts: SessionArtifacts;
48
50
  private static readonly STALE_REASON;
49
51
  private static readonly STALE_SOURCE;
52
+ private static readonly TEAM_HEARTBEAT_LOG_INTERVAL_MS;
50
53
  constructor(adapter: SessionPersistenceAdapter);
51
54
  ensureSessionsDir(): string;
52
55
  private writeManifestFile;
@@ -81,6 +84,9 @@ export declare class UnifiedSessionPersistenceService {
81
84
  applyStatusToRunningChildSessions(parentSessionId: string, status: Exclude<SessionStatus, "running">): Promise<void>;
82
85
  onTeamTaskStart(rootSessionId: string, agentId: string, message: string): Promise<void>;
83
86
  onTeamTaskEnd(rootSessionId: string, agentId: string, status: SessionStatus, summary?: string, messages?: LlmsProviders.Message[]): Promise<void>;
87
+ onTeamTaskProgress(rootSessionId: string, agentId: string, progress: string, options?: {
88
+ kind?: "heartbeat" | "progress" | "text";
89
+ }): Promise<void>;
84
90
  handleSubAgentStart(rootSessionId: string, context: SubAgentStartContext): Promise<void>;
85
91
  handleSubAgentEnd(rootSessionId: string, context: SubAgentEndContext): Promise<void>;
86
92
  private isPidAlive;
@@ -7,5 +7,5 @@ export declare function extractWorkspaceMetadataFromSystemPrompt(systemPrompt: s
7
7
  export declare function hasRuntimeHooks(hooks: AgentConfig["hooks"]): boolean;
8
8
  export declare function mergeAgentExtensions(explicitExtensions: AgentConfig["extensions"] | undefined, loadedExtensions: AgentConfig["extensions"] | undefined): AgentConfig["extensions"];
9
9
  export declare function serializeAgentEvent(event: AgentEvent): string;
10
- export declare function withLatestAssistantTurnMetadata(messages: LlmsProviders.Message[], result: AgentResult): StoredMessageWithMetadata[];
10
+ export declare function withLatestAssistantTurnMetadata(messages: LlmsProviders.Message[], result: AgentResult, previousMessages?: LlmsProviders.MessageWithMetadata[]): StoredMessageWithMetadata[];
11
11
  export declare function toSessionRecord(row: SessionRowShape): SessionRecord;
@@ -17,12 +17,22 @@ export type ActiveSession = {
17
17
  started: boolean;
18
18
  aborting: boolean;
19
19
  interactive: boolean;
20
+ persistedMessages?: LlmsProviders.MessageWithMetadata[];
20
21
  activeTeamRunIds: Set<string>;
21
22
  pendingTeamRunUpdates: TeamRunUpdate[];
22
23
  teamRunWaiters: Array<() => void>;
24
+ pendingPrompts: PendingPrompt[];
25
+ drainingPendingPrompts: boolean;
23
26
  pluginSandboxShutdown?: () => Promise<void>;
24
27
  turnUsageBaseline?: SessionAccumulatedUsage;
25
28
  };
29
+ export type PendingPrompt = {
30
+ id: string;
31
+ prompt: string;
32
+ delivery: "queue" | "steer";
33
+ userImages?: string[];
34
+ userFiles?: string[];
35
+ };
26
36
  export type TeamRunUpdate = {
27
37
  runId: string;
28
38
  agentId: string;
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import { type Tool } from "@clinebot/agents";
7
7
  import { type ApplyPatchInput, type AskQuestionInput, type EditFileInput, type FetchWebContentInput, type ReadFilesInput, type RunCommandsInput, type SearchCodebaseInput, type SkillsInput } from "./schemas.js";
8
- import type { ApplyPatchExecutor, AskQuestionExecutor, BashExecutor, CreateDefaultToolsOptions, DefaultToolsConfig, EditorExecutor, FileReadExecutor, SearchExecutor, SkillsExecutor, ToolOperationResult, WebFetchExecutor } from "./types.js";
8
+ import type { ApplyPatchExecutor, AskQuestionExecutor, BashExecutor, CreateDefaultToolsOptions, DefaultToolsConfig, EditorExecutor, FileReadExecutor, SearchExecutor, SkillsExecutorWithMetadata, ToolOperationResult, WebFetchExecutor } from "./types.js";
9
9
  /**
10
10
  * Create the read_files tool
11
11
  *
@@ -47,7 +47,7 @@ export declare function createEditorTool(executor: EditorExecutor, config?: Pick
47
47
  *
48
48
  * Invokes a configured skill by name and optional arguments.
49
49
  */
50
- export declare function createSkillsTool(executor: SkillsExecutor, config?: Pick<DefaultToolsConfig, "skillsTimeoutMs">): Tool<SkillsInput, string>;
50
+ export declare function createSkillsTool(executor: SkillsExecutorWithMetadata, config?: Pick<DefaultToolsConfig, "skillsTimeoutMs">): Tool<SkillsInput, string>;
51
51
  /**
52
52
  * Create the ask_question tool
53
53
  *
@@ -79,7 +79,7 @@ export declare const ToolPresets: {
79
79
  readonly enableAskQuestion: true;
80
80
  };
81
81
  /**
82
- * YOLO mode (everything enabled + no approval required)
82
+ * YOLO mode (automation-focused tools + no approval required)
83
83
  * Good for trusted local automation workflows.
84
84
  */
85
85
  readonly yolo: {
@@ -90,7 +90,7 @@ export declare const ToolPresets: {
90
90
  readonly enableApplyPatch: false;
91
91
  readonly enableEditor: true;
92
92
  readonly enableSkills: true;
93
- readonly enableAskQuestion: true;
93
+ readonly enableAskQuestion: false;
94
94
  };
95
95
  };
96
96
  /**
@@ -103,7 +103,7 @@ export type ToolPresetName = keyof typeof ToolPresets;
103
103
  export type ToolPolicyPresetName = "default" | "yolo";
104
104
  /**
105
105
  * Build tool policies for a preset.
106
- * `yolo` guarantees all tools are enabled and auto-approved.
106
+ * `yolo` guarantees tool policies are enabled and auto-approved.
107
107
  */
108
108
  export declare function createToolPoliciesWithPreset(presetName: ToolPolicyPresetName): Record<string, ToolPolicy>;
109
109
  /**
@@ -6,13 +6,13 @@
6
6
  */
7
7
  import { z } from "zod";
8
8
  export declare const ReadFileLineRangeSchema: z.ZodObject<{
9
- start_line: z.ZodOptional<z.ZodNumber>;
10
- end_line: z.ZodOptional<z.ZodNumber>;
9
+ start_line: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
10
+ end_line: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
11
11
  }, z.core.$strip>;
12
12
  export declare const ReadFileRequestSchema: z.ZodObject<{
13
13
  path: z.ZodString;
14
- start_line: z.ZodOptional<z.ZodNumber>;
15
- end_line: z.ZodOptional<z.ZodNumber>;
14
+ start_line: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
15
+ end_line: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
16
16
  }, z.core.$strip>;
17
17
  /**
18
18
  * Schema for read_files tool input
@@ -20,8 +20,8 @@ export declare const ReadFileRequestSchema: z.ZodObject<{
20
20
  export declare const ReadFilesInputSchema: z.ZodObject<{
21
21
  files: z.ZodArray<z.ZodObject<{
22
22
  path: z.ZodString;
23
- start_line: z.ZodOptional<z.ZodNumber>;
24
- end_line: z.ZodOptional<z.ZodNumber>;
23
+ start_line: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
24
+ end_line: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
25
25
  }, z.core.$strip>>;
26
26
  }, z.core.$strip>;
27
27
  /**
@@ -30,22 +30,22 @@ export declare const ReadFilesInputSchema: z.ZodObject<{
30
30
  export declare const ReadFilesInputUnionSchema: z.ZodUnion<readonly [z.ZodObject<{
31
31
  files: z.ZodArray<z.ZodObject<{
32
32
  path: z.ZodString;
33
- start_line: z.ZodOptional<z.ZodNumber>;
34
- end_line: z.ZodOptional<z.ZodNumber>;
33
+ start_line: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
34
+ end_line: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
35
35
  }, z.core.$strip>>;
36
36
  }, z.core.$strip>, z.ZodObject<{
37
37
  path: z.ZodString;
38
- start_line: z.ZodOptional<z.ZodNumber>;
39
- end_line: z.ZodOptional<z.ZodNumber>;
38
+ start_line: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
39
+ end_line: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
40
40
  }, z.core.$strip>, z.ZodArray<z.ZodObject<{
41
41
  path: z.ZodString;
42
- start_line: z.ZodOptional<z.ZodNumber>;
43
- end_line: z.ZodOptional<z.ZodNumber>;
42
+ start_line: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
43
+ end_line: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
44
44
  }, z.core.$strip>>, z.ZodArray<z.ZodString>, z.ZodString, z.ZodObject<{
45
45
  files: z.ZodObject<{
46
46
  path: z.ZodString;
47
- start_line: z.ZodOptional<z.ZodNumber>;
48
- end_line: z.ZodOptional<z.ZodNumber>;
47
+ start_line: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
48
+ end_line: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
49
49
  }, z.core.$strip>;
50
50
  }, z.core.$strip>, z.ZodObject<{
51
51
  file_paths: z.ZodArray<z.ZodString>;
@@ -42,4 +42,9 @@ export interface CoreSessionConfig extends CoreModelConfig, CoreRuntimeFeatures,
42
42
  onTeamEvent?: (event: TeamEvent) => void;
43
43
  onConsecutiveMistakeLimitReached?: (context: ConsecutiveMistakeLimitContext) => Promise<ConsecutiveMistakeLimitDecision> | ConsecutiveMistakeLimitDecision;
44
44
  toolRoutingRules?: ToolRoutingRule[];
45
+ /**
46
+ * Optional skill allowlist for the `skills` tool. When provided, only these
47
+ * skills are surfaced in tool metadata and invocable by name.
48
+ */
49
+ skills?: string[];
45
50
  }
@@ -27,6 +27,22 @@ export interface SessionTeamProgressEvent {
27
27
  lifecycle: import("@clinebot/shared").TeamProgressLifecycleEvent;
28
28
  summary: import("@clinebot/shared").TeamProgressSummary;
29
29
  }
30
+ export interface SessionPendingPromptsEvent {
31
+ sessionId: string;
32
+ prompts: Array<{
33
+ id: string;
34
+ prompt: string;
35
+ delivery: "queue" | "steer";
36
+ attachmentCount: number;
37
+ }>;
38
+ }
39
+ export interface SessionPendingPromptSubmittedEvent {
40
+ sessionId: string;
41
+ id: string;
42
+ prompt: string;
43
+ delivery: "queue" | "steer";
44
+ attachmentCount: number;
45
+ }
30
46
  export type CoreSessionEvent = {
31
47
  type: "chunk";
32
48
  payload: SessionChunkEvent;
@@ -39,6 +55,12 @@ export type CoreSessionEvent = {
39
55
  } | {
40
56
  type: "team_progress";
41
57
  payload: SessionTeamProgressEvent;
58
+ } | {
59
+ type: "pending_prompts";
60
+ payload: SessionPendingPromptsEvent;
61
+ } | {
62
+ type: "pending_prompt_submitted";
63
+ payload: SessionPendingPromptSubmittedEvent;
42
64
  } | {
43
65
  type: "ended";
44
66
  payload: SessionEndedEvent;
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@clinebot/core",
3
- "version": "0.0.10",
3
+ "version": "0.0.12",
4
4
  "main": "./dist/index.node.js",
5
5
  "dependencies": {
6
- "@clinebot/agents": "0.0.10",
7
- "@clinebot/llms": "0.0.10",
8
- "@clinebot/shared": "0.0.10",
6
+ "@clinebot/agents": "0.0.12",
7
+ "@clinebot/llms": "0.0.12",
8
+ "@clinebot/shared": "0.0.12",
9
9
  "@opentelemetry/api": "^1.9.0",
10
10
  "@opentelemetry/api-logs": "^0.56.0",
11
11
  "@opentelemetry/exporter-logs-otlp-http": "^0.56.0",
@@ -15,6 +15,7 @@
15
15
  "@opentelemetry/sdk-metrics": "^1.30.1",
16
16
  "@opentelemetry/semantic-conventions": "^1.37.0",
17
17
  "better-sqlite3": "^11.10.0",
18
+ "jiti": "^1.21.7",
18
19
  "nanoid": "^5.1.7",
19
20
  "simple-git": "^3.32.3",
20
21
  "yaml": "^2.8.2",
@@ -139,6 +139,7 @@ name: Reader
139
139
  description: Reads files
140
140
  modelId: claude-sonnet-4-6
141
141
  tools: read_files
142
+ skills: commit, review
142
143
  ---
143
144
  Be precise.`);
144
145
 
@@ -149,6 +150,7 @@ Be precise.`);
149
150
  expect(partial.modelId).toBe("claude-sonnet-4-6");
150
151
  expect(partial.systemPrompt).toBe("Be precise.");
151
152
  expect(partial.tools).toEqual([readFiles]);
153
+ expect(partial.skills).toEqual(["commit", "review"]);
152
154
  });
153
155
 
154
156
  it("throws when tool overrides are configured without available tools", () => {
@@ -20,6 +20,7 @@ export type {
20
20
  AgentYamlConfig,
21
21
  BuildAgentConfigOverridesOptions,
22
22
  ParseYamlFrontmatterResult,
23
+ PartialAgentConfigOverrides,
23
24
  } from "./agent-config-parser";
24
25
  export {
25
26
  parseAgentConfigFromYaml,
@@ -33,6 +33,11 @@ export interface BuildAgentConfigOverridesOptions {
33
33
  availableTools?: ReadonlyArray<Tool>;
34
34
  }
35
35
 
36
+ export interface PartialAgentConfigOverrides
37
+ extends Partial<Pick<AgentConfig, "modelId" | "systemPrompt" | "tools">> {
38
+ skills?: string[];
39
+ }
40
+
36
41
  export function isAgentConfigYamlFile(fileName: string): boolean {
37
42
  return /\.(yaml|yml)$/i.test(fileName);
38
43
  }
@@ -159,10 +164,8 @@ export function resolveAgentTools(
159
164
  export function toPartialAgentConfig(
160
165
  config: AgentYamlConfig,
161
166
  options?: BuildAgentConfigOverridesOptions,
162
- ): Partial<Pick<AgentConfig, "modelId" | "systemPrompt" | "tools">> {
163
- const partial: Partial<
164
- Pick<AgentConfig, "modelId" | "systemPrompt" | "tools">
165
- > = {
167
+ ): PartialAgentConfigOverrides {
168
+ const partial: PartialAgentConfigOverrides = {
166
169
  systemPrompt: config.systemPrompt,
167
170
  };
168
171
 
@@ -179,13 +182,17 @@ export function toPartialAgentConfig(
179
182
  partial.tools = resolveAgentTools(config.tools, options.availableTools);
180
183
  }
181
184
 
185
+ if (config.skills !== undefined) {
186
+ partial.skills = [...config.skills];
187
+ }
188
+
182
189
  return partial;
183
190
  }
184
191
 
185
192
  export function parsePartialAgentConfigFromYaml(
186
193
  content: string,
187
194
  options?: BuildAgentConfigOverridesOptions,
188
- ): Partial<Pick<AgentConfig, "modelId" | "systemPrompt" | "tools">> {
195
+ ): PartialAgentConfigOverrides {
189
196
  const parsed = parseAgentConfigFromYaml(content);
190
197
  return toPartialAgentConfig(parsed, options);
191
198
  }
@@ -5,6 +5,7 @@ export type {
5
5
  BuildAgentConfigOverridesOptions,
6
6
  CreateAgentConfigWatcherOptions,
7
7
  ParseYamlFrontmatterResult,
8
+ PartialAgentConfigOverrides,
8
9
  } from "./agent-config-loader";
9
10
  export {
10
11
  AGENT_CONFIG_DIRECTORY_NAME,
@@ -26,6 +26,11 @@ describe("plugin-config-loader", () => {
26
26
  await mkdir(nested, { recursive: true });
27
27
  await writeFile(join(root, "a.mjs"), "export default {}", "utf8");
28
28
  await writeFile(join(nested, "b.ts"), "export default {}", "utf8");
29
+ await writeFile(
30
+ join(root, ".a.mjs.cline-plugin.mjs"),
31
+ "export default {}",
32
+ "utf8",
33
+ );
29
34
  await writeFile(join(root, "ignore.txt"), "noop", "utf8");
30
35
 
31
36
  const discovered = discoverPluginModulePaths(root);
@@ -38,6 +43,8 @@ describe("plugin-config-loader", () => {
38
43
  it("resolves plugin paths from explicit files/directories", async () => {
39
44
  const root = await mkdtemp(join(tmpdir(), "core-plugin-config-loader-"));
40
45
  try {
46
+ process.env.HOME = root;
47
+ setHomeDir(root);
41
48
  const pluginsDir = join(root, "plugins");
42
49
  await mkdir(pluginsDir, { recursive: true });
43
50
  const filePath = join(root, "direct.mjs");
@@ -57,6 +64,41 @@ describe("plugin-config-loader", () => {
57
64
  }
58
65
  });
59
66
 
67
+ it("prefers package manifest plugin entries for configured directories", async () => {
68
+ const root = await mkdtemp(join(tmpdir(), "core-plugin-config-loader-"));
69
+ try {
70
+ const pluginDir = join(root, "plugin-package");
71
+ const srcDir = join(pluginDir, "src");
72
+ await mkdir(srcDir, { recursive: true });
73
+ const declaredEntry = join(srcDir, "index.ts");
74
+ const ignoredEntry = join(pluginDir, "ignored.mjs");
75
+ await writeFile(
76
+ join(pluginDir, "package.json"),
77
+ JSON.stringify({
78
+ name: "plugin-package",
79
+ private: true,
80
+ cline: {
81
+ plugins: ["./src/index.ts"],
82
+ },
83
+ }),
84
+ "utf8",
85
+ );
86
+ await writeFile(declaredEntry, "export default {}", "utf8");
87
+ await writeFile(ignoredEntry, "export default {}", "utf8");
88
+
89
+ const resolved = resolveAgentPluginPaths({
90
+ pluginPaths: ["./plugin-package"],
91
+ cwd: root,
92
+ workspacePath: join(root, "workspace"),
93
+ });
94
+
95
+ expect(resolved).toContain(declaredEntry);
96
+ expect(resolved).not.toContain(ignoredEntry);
97
+ } finally {
98
+ await rm(root, { recursive: true, force: true });
99
+ }
100
+ });
101
+
60
102
  it("includes shared search-path plugins", async () => {
61
103
  const home = await mkdtemp(
62
104
  join(tmpdir(), "core-plugin-config-loader-home-"),
@@ -69,20 +111,27 @@ describe("plugin-config-loader", () => {
69
111
  setHomeDir(home);
70
112
  const workspacePlugins = join(workspace, ".clinerules", "plugins");
71
113
  const userPlugins = join(home, ".cline", "plugins");
114
+ const documentsPlugins = join(home, "Documents", "Plugins");
72
115
  await mkdir(workspacePlugins, { recursive: true });
73
116
  await mkdir(userPlugins, { recursive: true });
117
+ await mkdir(documentsPlugins, { recursive: true });
74
118
  const workspacePlugin = join(workspacePlugins, "workspace.mjs");
75
119
  const userPlugin = join(userPlugins, "user.mjs");
120
+ const documentsPlugin = join(documentsPlugins, "documents.mjs");
76
121
  await writeFile(workspacePlugin, "export default {}", "utf8");
77
122
  await writeFile(userPlugin, "export default {}", "utf8");
123
+ await writeFile(documentsPlugin, "export default {}", "utf8");
78
124
 
79
125
  const searchPaths = resolvePluginConfigSearchPaths(workspace);
126
+ expect(searchPaths).toHaveLength(3);
80
127
  expect(searchPaths).toContain(workspacePlugins);
81
128
  expect(searchPaths).toContain(userPlugins);
129
+ expect(searchPaths).toContain(documentsPlugins);
82
130
 
83
131
  const resolved = resolveAgentPluginPaths({ workspacePath: workspace });
84
132
  expect(resolved).toContain(workspacePlugin);
85
133
  expect(resolved).toContain(userPlugin);
134
+ expect(resolved).toContain(documentsPlugin);
86
135
  } finally {
87
136
  await rm(home, { recursive: true, force: true });
88
137
  await rm(workspace, { recursive: true, force: true });
@@ -1,19 +1,13 @@
1
- import { existsSync, readdirSync, statSync } from "node:fs";
2
- import { join, resolve } from "node:path";
1
+ import { existsSync } from "node:fs";
3
2
  import type { AgentConfig } from "@clinebot/agents";
4
- import { resolvePluginConfigSearchPaths as resolvePluginConfigSearchPathsFromShared } from "@clinebot/shared/storage";
3
+ import {
4
+ discoverPluginModulePaths as discoverPluginModulePathsFromShared,
5
+ resolveConfiguredPluginModulePaths,
6
+ resolvePluginConfigSearchPaths as resolvePluginConfigSearchPathsFromShared,
7
+ } from "@clinebot/shared/storage";
5
8
  import { loadAgentPluginsFromPaths } from "./plugin-loader";
6
9
  import { loadSandboxedPlugins } from "./plugin-sandbox";
7
10
 
8
- const PLUGIN_MODULE_EXTENSIONS = new Set([
9
- ".js",
10
- ".mjs",
11
- ".cjs",
12
- ".ts",
13
- ".mts",
14
- ".cts",
15
- ]);
16
-
17
11
  type AgentPlugin = NonNullable<AgentConfig["extensions"]>[number];
18
12
 
19
13
  export function resolvePluginConfigSearchPaths(
@@ -22,67 +16,8 @@ export function resolvePluginConfigSearchPaths(
22
16
  return resolvePluginConfigSearchPathsFromShared(workspacePath);
23
17
  }
24
18
 
25
- function hasPluginModuleExtension(path: string): boolean {
26
- const dot = path.lastIndexOf(".");
27
- if (dot === -1) {
28
- return false;
29
- }
30
- return PLUGIN_MODULE_EXTENSIONS.has(path.slice(dot));
31
- }
32
-
33
19
  export function discoverPluginModulePaths(directoryPath: string): string[] {
34
- const root = resolve(directoryPath);
35
- if (!existsSync(root)) {
36
- return [];
37
- }
38
- const discovered: string[] = [];
39
- const stack = [root];
40
- while (stack.length > 0) {
41
- const current = stack.pop();
42
- if (!current) {
43
- continue;
44
- }
45
- for (const entry of readdirSync(current, { withFileTypes: true })) {
46
- const candidate = join(current, entry.name);
47
- if (entry.isDirectory()) {
48
- stack.push(candidate);
49
- continue;
50
- }
51
- if (entry.isFile() && hasPluginModuleExtension(candidate)) {
52
- discovered.push(candidate);
53
- }
54
- }
55
- }
56
- return discovered.sort((a, b) => a.localeCompare(b));
57
- }
58
-
59
- function resolveConfiguredPluginPaths(
60
- pluginPaths: ReadonlyArray<string>,
61
- cwd: string,
62
- ): string[] {
63
- const resolvedPaths: string[] = [];
64
- for (const pluginPath of pluginPaths) {
65
- const trimmed = pluginPath.trim();
66
- if (!trimmed) {
67
- continue;
68
- }
69
- const absolutePath = resolve(cwd, trimmed);
70
- if (!existsSync(absolutePath)) {
71
- throw new Error(`Plugin path does not exist: ${absolutePath}`);
72
- }
73
- const stats = statSync(absolutePath);
74
- if (stats.isDirectory()) {
75
- resolvedPaths.push(...discoverPluginModulePaths(absolutePath));
76
- continue;
77
- }
78
- if (!hasPluginModuleExtension(absolutePath)) {
79
- throw new Error(
80
- `Plugin file must use a supported extension (${[...PLUGIN_MODULE_EXTENSIONS].join(", ")}): ${absolutePath}`,
81
- );
82
- }
83
- resolvedPaths.push(absolutePath);
84
- }
85
- return resolvedPaths;
20
+ return discoverPluginModulePathsFromShared(directoryPath);
86
21
  }
87
22
 
88
23
  export interface ResolveAgentPluginPathsOptions {
@@ -100,7 +35,7 @@ export function resolveAgentPluginPaths(
100
35
  )
101
36
  .flatMap((directoryPath) => discoverPluginModulePaths(directoryPath))
102
37
  .filter((path) => existsSync(path));
103
- const configuredPaths = resolveConfiguredPluginPaths(
38
+ const configuredPaths = resolveConfiguredPluginModulePaths(
104
39
  options.pluginPaths ?? [],
105
40
  cwd,
106
41
  );
@@ -124,6 +59,7 @@ export interface ResolveAndLoadAgentPluginsOptions
124
59
  importTimeoutMs?: number;
125
60
  hookTimeoutMs?: number;
126
61
  contributionTimeoutMs?: number;
62
+ onEvent?: (event: { name: string; payload?: unknown }) => void;
127
63
  }
128
64
 
129
65
  export async function resolveAndLoadAgentPlugins(
@@ -152,6 +88,7 @@ export async function resolveAndLoadAgentPlugins(
152
88
  importTimeoutMs: options.importTimeoutMs,
153
89
  hookTimeoutMs: options.hookTimeoutMs,
154
90
  contributionTimeoutMs: options.contributionTimeoutMs,
91
+ onEvent: options.onEvent,
155
92
  });
156
93
  return {
157
94
  extensions: sandboxed.extensions ?? [],