@clinebot/core 0.0.0

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 (200) hide show
  1. package/README.md +88 -0
  2. package/dist/account/cline-account-service.d.ts +34 -0
  3. package/dist/account/index.d.ts +3 -0
  4. package/dist/account/rpc.d.ts +38 -0
  5. package/dist/account/types.d.ts +74 -0
  6. package/dist/agents/agent-config-loader.d.ts +18 -0
  7. package/dist/agents/agent-config-parser.d.ts +25 -0
  8. package/dist/agents/hooks-config-loader.d.ts +23 -0
  9. package/dist/agents/index.d.ts +11 -0
  10. package/dist/agents/plugin-config-loader.d.ts +22 -0
  11. package/dist/agents/plugin-loader.d.ts +9 -0
  12. package/dist/agents/plugin-sandbox.d.ts +12 -0
  13. package/dist/agents/unified-config-file-watcher.d.ts +77 -0
  14. package/dist/agents/user-instruction-config-loader.d.ts +63 -0
  15. package/dist/auth/client.d.ts +11 -0
  16. package/dist/auth/cline.d.ts +41 -0
  17. package/dist/auth/codex.d.ts +39 -0
  18. package/dist/auth/oca.d.ts +22 -0
  19. package/dist/auth/server.d.ts +22 -0
  20. package/dist/auth/types.d.ts +72 -0
  21. package/dist/auth/utils.d.ts +32 -0
  22. package/dist/chat/chat-schema.d.ts +145 -0
  23. package/dist/default-tools/constants.d.ts +23 -0
  24. package/dist/default-tools/definitions.d.ts +96 -0
  25. package/dist/default-tools/executors/apply-patch-parser.d.ts +68 -0
  26. package/dist/default-tools/executors/apply-patch.d.ts +26 -0
  27. package/dist/default-tools/executors/bash.d.ts +49 -0
  28. package/dist/default-tools/executors/editor.d.ts +31 -0
  29. package/dist/default-tools/executors/file-read.d.ts +40 -0
  30. package/dist/default-tools/executors/index.d.ts +44 -0
  31. package/dist/default-tools/executors/search.d.ts +50 -0
  32. package/dist/default-tools/executors/web-fetch.d.ts +58 -0
  33. package/dist/default-tools/index.d.ts +57 -0
  34. package/dist/default-tools/presets.d.ts +124 -0
  35. package/dist/default-tools/schemas.d.ts +121 -0
  36. package/dist/default-tools/types.d.ts +237 -0
  37. package/dist/index.d.ts +23 -0
  38. package/dist/index.js +220 -0
  39. package/dist/input/file-indexer.d.ts +5 -0
  40. package/dist/input/index.d.ts +4 -0
  41. package/dist/input/mention-enricher.d.ts +12 -0
  42. package/dist/mcp/config-loader.d.ts +15 -0
  43. package/dist/mcp/index.d.ts +4 -0
  44. package/dist/mcp/manager.d.ts +24 -0
  45. package/dist/mcp/types.d.ts +66 -0
  46. package/dist/runtime/hook-file-hooks.d.ts +18 -0
  47. package/dist/runtime/rules.d.ts +5 -0
  48. package/dist/runtime/runtime-builder.d.ts +5 -0
  49. package/dist/runtime/sandbox/subprocess-sandbox.d.ts +19 -0
  50. package/dist/runtime/session-runtime.d.ts +36 -0
  51. package/dist/runtime/tool-approval.d.ts +9 -0
  52. package/dist/runtime/workflows.d.ts +13 -0
  53. package/dist/server/index.d.ts +47 -0
  54. package/dist/server/index.js +641 -0
  55. package/dist/session/default-session-manager.d.ts +77 -0
  56. package/dist/session/rpc-session-service.d.ts +12 -0
  57. package/dist/session/runtime-oauth-token-manager.d.ts +28 -0
  58. package/dist/session/session-artifacts.d.ts +19 -0
  59. package/dist/session/session-graph.d.ts +15 -0
  60. package/dist/session/session-host.d.ts +21 -0
  61. package/dist/session/session-manager.d.ts +50 -0
  62. package/dist/session/session-manifest.d.ts +30 -0
  63. package/dist/session/session-service.d.ts +113 -0
  64. package/dist/session/sqlite-rpc-session-backend.d.ts +30 -0
  65. package/dist/session/unified-session-persistence-service.d.ts +93 -0
  66. package/dist/session/workspace-manager.d.ts +28 -0
  67. package/dist/session/workspace-manifest.d.ts +25 -0
  68. package/dist/storage/provider-settings-legacy-migration.d.ts +13 -0
  69. package/dist/storage/provider-settings-manager.d.ts +20 -0
  70. package/dist/storage/sqlite-session-store.d.ts +29 -0
  71. package/dist/storage/sqlite-team-store.d.ts +31 -0
  72. package/dist/storage/team-store.d.ts +2 -0
  73. package/dist/team/index.d.ts +1 -0
  74. package/dist/team/projections.d.ts +8 -0
  75. package/dist/types/common.d.ts +10 -0
  76. package/dist/types/config.d.ts +37 -0
  77. package/dist/types/events.d.ts +54 -0
  78. package/dist/types/provider-settings.d.ts +20 -0
  79. package/dist/types/sessions.d.ts +9 -0
  80. package/dist/types/storage.d.ts +37 -0
  81. package/dist/types/workspace.d.ts +7 -0
  82. package/dist/types.d.ts +26 -0
  83. package/package.json +63 -0
  84. package/src/account/cline-account-service.test.ts +101 -0
  85. package/src/account/cline-account-service.ts +267 -0
  86. package/src/account/index.ts +20 -0
  87. package/src/account/rpc.test.ts +62 -0
  88. package/src/account/rpc.ts +172 -0
  89. package/src/account/types.ts +80 -0
  90. package/src/agents/agent-config-loader.test.ts +234 -0
  91. package/src/agents/agent-config-loader.ts +107 -0
  92. package/src/agents/agent-config-parser.ts +191 -0
  93. package/src/agents/hooks-config-loader.ts +97 -0
  94. package/src/agents/index.ts +84 -0
  95. package/src/agents/plugin-config-loader.test.ts +91 -0
  96. package/src/agents/plugin-config-loader.ts +160 -0
  97. package/src/agents/plugin-loader.test.ts +102 -0
  98. package/src/agents/plugin-loader.ts +105 -0
  99. package/src/agents/plugin-sandbox.test.ts +120 -0
  100. package/src/agents/plugin-sandbox.ts +471 -0
  101. package/src/agents/unified-config-file-watcher.test.ts +196 -0
  102. package/src/agents/unified-config-file-watcher.ts +483 -0
  103. package/src/agents/user-instruction-config-loader.test.ts +158 -0
  104. package/src/agents/user-instruction-config-loader.ts +438 -0
  105. package/src/auth/client.test.ts +40 -0
  106. package/src/auth/client.ts +25 -0
  107. package/src/auth/cline.test.ts +130 -0
  108. package/src/auth/cline.ts +414 -0
  109. package/src/auth/codex.test.ts +170 -0
  110. package/src/auth/codex.ts +466 -0
  111. package/src/auth/oca.test.ts +215 -0
  112. package/src/auth/oca.ts +546 -0
  113. package/src/auth/server.ts +216 -0
  114. package/src/auth/types.ts +78 -0
  115. package/src/auth/utils.test.ts +128 -0
  116. package/src/auth/utils.ts +247 -0
  117. package/src/chat/chat-schema.ts +82 -0
  118. package/src/default-tools/constants.ts +35 -0
  119. package/src/default-tools/definitions.test.ts +233 -0
  120. package/src/default-tools/definitions.ts +632 -0
  121. package/src/default-tools/executors/apply-patch-parser.ts +520 -0
  122. package/src/default-tools/executors/apply-patch.ts +359 -0
  123. package/src/default-tools/executors/bash.ts +205 -0
  124. package/src/default-tools/executors/editor.ts +231 -0
  125. package/src/default-tools/executors/file-read.test.ts +25 -0
  126. package/src/default-tools/executors/file-read.ts +94 -0
  127. package/src/default-tools/executors/index.ts +75 -0
  128. package/src/default-tools/executors/search.ts +278 -0
  129. package/src/default-tools/executors/web-fetch.ts +259 -0
  130. package/src/default-tools/index.ts +161 -0
  131. package/src/default-tools/presets.test.ts +63 -0
  132. package/src/default-tools/presets.ts +168 -0
  133. package/src/default-tools/schemas.ts +228 -0
  134. package/src/default-tools/types.ts +324 -0
  135. package/src/index.ts +119 -0
  136. package/src/input/file-indexer.d.ts +11 -0
  137. package/src/input/file-indexer.test.ts +87 -0
  138. package/src/input/file-indexer.ts +280 -0
  139. package/src/input/index.ts +7 -0
  140. package/src/input/mention-enricher.test.ts +82 -0
  141. package/src/input/mention-enricher.ts +119 -0
  142. package/src/mcp/config-loader.test.ts +238 -0
  143. package/src/mcp/config-loader.ts +219 -0
  144. package/src/mcp/index.ts +26 -0
  145. package/src/mcp/manager.test.ts +106 -0
  146. package/src/mcp/manager.ts +262 -0
  147. package/src/mcp/types.ts +88 -0
  148. package/src/runtime/hook-file-hooks.test.ts +106 -0
  149. package/src/runtime/hook-file-hooks.ts +736 -0
  150. package/src/runtime/index.ts +27 -0
  151. package/src/runtime/rules.ts +34 -0
  152. package/src/runtime/runtime-builder.team-persistence.test.ts +203 -0
  153. package/src/runtime/runtime-builder.test.ts +215 -0
  154. package/src/runtime/runtime-builder.ts +515 -0
  155. package/src/runtime/runtime-parity.test.ts +132 -0
  156. package/src/runtime/sandbox/subprocess-sandbox.ts +207 -0
  157. package/src/runtime/session-runtime.ts +44 -0
  158. package/src/runtime/tool-approval.ts +104 -0
  159. package/src/runtime/workflows.test.ts +119 -0
  160. package/src/runtime/workflows.ts +54 -0
  161. package/src/server/index.ts +282 -0
  162. package/src/session/default-session-manager.e2e.test.ts +354 -0
  163. package/src/session/default-session-manager.test.ts +816 -0
  164. package/src/session/default-session-manager.ts +1286 -0
  165. package/src/session/index.ts +37 -0
  166. package/src/session/rpc-session-service.ts +189 -0
  167. package/src/session/runtime-oauth-token-manager.test.ts +137 -0
  168. package/src/session/runtime-oauth-token-manager.ts +265 -0
  169. package/src/session/session-artifacts.ts +106 -0
  170. package/src/session/session-graph.ts +90 -0
  171. package/src/session/session-host.ts +190 -0
  172. package/src/session/session-manager.ts +56 -0
  173. package/src/session/session-manifest.ts +29 -0
  174. package/src/session/session-service.team-persistence.test.ts +48 -0
  175. package/src/session/session-service.ts +610 -0
  176. package/src/session/sqlite-rpc-session-backend.ts +303 -0
  177. package/src/session/unified-session-persistence-service.ts +781 -0
  178. package/src/session/workspace-manager.ts +98 -0
  179. package/src/session/workspace-manifest.ts +100 -0
  180. package/src/storage/artifact-store.ts +1 -0
  181. package/src/storage/index.ts +11 -0
  182. package/src/storage/provider-settings-legacy-migration.test.ts +175 -0
  183. package/src/storage/provider-settings-legacy-migration.ts +637 -0
  184. package/src/storage/provider-settings-manager.test.ts +111 -0
  185. package/src/storage/provider-settings-manager.ts +129 -0
  186. package/src/storage/session-store.ts +1 -0
  187. package/src/storage/sqlite-session-store.ts +270 -0
  188. package/src/storage/sqlite-team-store.ts +443 -0
  189. package/src/storage/team-store.ts +5 -0
  190. package/src/team/index.ts +4 -0
  191. package/src/team/projections.ts +285 -0
  192. package/src/types/common.ts +14 -0
  193. package/src/types/config.ts +64 -0
  194. package/src/types/events.ts +46 -0
  195. package/src/types/index.ts +24 -0
  196. package/src/types/provider-settings.ts +43 -0
  197. package/src/types/sessions.ts +16 -0
  198. package/src/types/storage.ts +64 -0
  199. package/src/types/workspace.ts +7 -0
  200. package/src/types.ts +127 -0
@@ -0,0 +1,207 @@
1
+ import { type ChildProcess, spawn } from "node:child_process";
2
+
3
+ interface SandboxCallMessage {
4
+ type: "call";
5
+ id: string;
6
+ method: string;
7
+ args: unknown;
8
+ }
9
+
10
+ interface SandboxResponseMessage {
11
+ type: "response";
12
+ id: string;
13
+ ok: boolean;
14
+ result?: unknown;
15
+ error?: { message: string; stack?: string };
16
+ }
17
+
18
+ export interface SubprocessSandboxOptions {
19
+ bootstrapScript: string;
20
+ name?: string;
21
+ }
22
+
23
+ export interface SandboxCallOptions {
24
+ timeoutMs?: number;
25
+ }
26
+
27
+ type PendingRequest = {
28
+ resolve: (value: unknown) => void;
29
+ reject: (error: Error) => void;
30
+ timeout?: NodeJS.Timeout;
31
+ };
32
+
33
+ function asError(value: unknown): Error {
34
+ if (value instanceof Error) {
35
+ return value;
36
+ }
37
+ return new Error(String(value));
38
+ }
39
+
40
+ export class SubprocessSandbox {
41
+ private readonly options: SubprocessSandboxOptions;
42
+ private process: ChildProcess | null = null;
43
+ private requestCounter = 0;
44
+ private readonly pending = new Map<string, PendingRequest>();
45
+
46
+ constructor(options: SubprocessSandboxOptions) {
47
+ this.options = options;
48
+ }
49
+
50
+ start(): void {
51
+ if (this.process && this.process.exitCode === null) {
52
+ return;
53
+ }
54
+
55
+ const child = spawn(
56
+ process.execPath,
57
+ ["-e", this.options.bootstrapScript],
58
+ {
59
+ stdio: ["ignore", "ignore", "ignore", "ipc"],
60
+ },
61
+ );
62
+ this.process = child;
63
+ child.on("message", (message) => {
64
+ this.onMessage(message as SandboxResponseMessage);
65
+ });
66
+ child.on("error", (error) => {
67
+ this.failPending(
68
+ new Error(
69
+ `${this.options.name ?? "sandbox"} process error: ${asError(error).message}`,
70
+ ),
71
+ );
72
+ });
73
+ child.on("exit", (code, signal) => {
74
+ this.process = null;
75
+ this.failPending(
76
+ new Error(
77
+ `${this.options.name ?? "sandbox"} process exited (code=${String(code)}, signal=${String(signal)})`,
78
+ ),
79
+ );
80
+ });
81
+ }
82
+
83
+ async call<TResult = unknown>(
84
+ method: string,
85
+ args: unknown,
86
+ options: SandboxCallOptions = {},
87
+ ): Promise<TResult> {
88
+ this.start();
89
+ const child = this.process;
90
+ if (!child || child.exitCode !== null) {
91
+ throw new Error(
92
+ `${this.options.name ?? "sandbox"} process is not available`,
93
+ );
94
+ }
95
+
96
+ const id = `req_${++this.requestCounter}`;
97
+ const message: SandboxCallMessage = {
98
+ type: "call",
99
+ id,
100
+ method,
101
+ args,
102
+ };
103
+
104
+ return await new Promise<TResult>((resolve, reject) => {
105
+ const pending: PendingRequest = {
106
+ resolve: (value) => resolve(value as TResult),
107
+ reject,
108
+ };
109
+ if ((options.timeoutMs ?? 0) > 0) {
110
+ pending.timeout = setTimeout(() => {
111
+ this.pending.delete(id);
112
+ this.shutdown().catch(() => {
113
+ // Best-effort process shutdown after timeout.
114
+ });
115
+ reject(
116
+ new Error(
117
+ `${this.options.name ?? "sandbox"} call timed out after ${options.timeoutMs}ms: ${method}`,
118
+ ),
119
+ );
120
+ }, options.timeoutMs);
121
+ }
122
+ this.pending.set(id, pending);
123
+ child.send(message, (error) => {
124
+ if (!error) {
125
+ return;
126
+ }
127
+ const entry = this.pending.get(id);
128
+ if (!entry) {
129
+ return;
130
+ }
131
+ this.pending.delete(id);
132
+ if (entry.timeout) {
133
+ clearTimeout(entry.timeout);
134
+ }
135
+ entry.reject(
136
+ new Error(
137
+ `${this.options.name ?? "sandbox"} failed to send call "${method}": ${asError(error).message}`,
138
+ ),
139
+ );
140
+ });
141
+ });
142
+ }
143
+
144
+ async shutdown(): Promise<void> {
145
+ const child = this.process;
146
+ this.process = null;
147
+ if (!child || child.exitCode !== null) {
148
+ this.failPending(new Error(`${this.options.name ?? "sandbox"} shutdown`));
149
+ return;
150
+ }
151
+ await new Promise<void>((resolve) => {
152
+ const timeout = setTimeout(() => {
153
+ try {
154
+ child.kill("SIGKILL");
155
+ } catch {
156
+ // Ignore kill failures.
157
+ }
158
+ resolve();
159
+ }, 300);
160
+ child.once("exit", () => {
161
+ clearTimeout(timeout);
162
+ resolve();
163
+ });
164
+ try {
165
+ child.kill("SIGTERM");
166
+ } catch {
167
+ clearTimeout(timeout);
168
+ resolve();
169
+ }
170
+ });
171
+ this.failPending(new Error(`${this.options.name ?? "sandbox"} shutdown`));
172
+ }
173
+
174
+ private onMessage(message: SandboxResponseMessage): void {
175
+ if (!message || message.type !== "response" || !message.id) {
176
+ return;
177
+ }
178
+ const pending = this.pending.get(message.id);
179
+ if (!pending) {
180
+ return;
181
+ }
182
+ this.pending.delete(message.id);
183
+ if (pending.timeout) {
184
+ clearTimeout(pending.timeout);
185
+ }
186
+ if (message.ok) {
187
+ pending.resolve(message.result);
188
+ return;
189
+ }
190
+ pending.reject(
191
+ new Error(
192
+ message.error?.message ||
193
+ `${this.options.name ?? "sandbox"} call failed`,
194
+ ),
195
+ );
196
+ }
197
+
198
+ private failPending(error: Error): void {
199
+ for (const [id, pending] of this.pending.entries()) {
200
+ this.pending.delete(id);
201
+ if (pending.timeout) {
202
+ clearTimeout(pending.timeout);
203
+ }
204
+ pending.reject(error);
205
+ }
206
+ }
207
+ }
@@ -0,0 +1,44 @@
1
+ import type {
2
+ AgentConfig,
3
+ AgentHooks,
4
+ AgentResult,
5
+ AgentTeamsRuntime,
6
+ Tool,
7
+ } from "@clinebot/agents";
8
+ import type { BasicLogger } from "@clinebot/shared";
9
+ import type { UserInstructionConfigWatcher } from "../agents";
10
+ import type { ToolExecutors } from "../default-tools";
11
+ import type { CoreSessionConfig } from "../types/config";
12
+
13
+ export interface BuiltRuntime {
14
+ tools: Tool[];
15
+ hooks?: AgentHooks;
16
+ logger?: BasicLogger;
17
+ teamRuntime?: AgentTeamsRuntime;
18
+ completionGuard?: () => string | undefined;
19
+ shutdown: (reason: string) => Promise<void> | void;
20
+ }
21
+
22
+ export interface RuntimeBuilderInput {
23
+ config: CoreSessionConfig;
24
+ hooks?: AgentHooks;
25
+ extensions?: AgentConfig["extensions"];
26
+ onTeamEvent?: (event: import("@clinebot/agents").TeamEvent) => void;
27
+ createSpawnTool?: () => Tool;
28
+ onTeamRestored?: () => void;
29
+ userInstructionWatcher?: UserInstructionConfigWatcher;
30
+ defaultToolExecutors?: Partial<ToolExecutors>;
31
+ logger?: BasicLogger;
32
+ }
33
+
34
+ export interface RuntimeBuilder {
35
+ build(input: RuntimeBuilderInput): BuiltRuntime;
36
+ }
37
+
38
+ export interface SessionRuntime {
39
+ start(config: CoreSessionConfig): Promise<{ sessionId: string }>;
40
+ send(sessionId: string, prompt: string): Promise<AgentResult | undefined>;
41
+ abort(sessionId: string): Promise<void>;
42
+ stop(sessionId: string): Promise<void>;
43
+ poll(): Promise<string[]>;
44
+ }
@@ -0,0 +1,104 @@
1
+ import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import type { ToolApprovalRequest, ToolApprovalResult } from "@clinebot/agents";
4
+
5
+ export type DesktopToolApprovalOptions = {
6
+ approvalDir?: string;
7
+ sessionId?: string;
8
+ timeoutMs?: number;
9
+ pollIntervalMs?: number;
10
+ nowIso?: () => string;
11
+ };
12
+
13
+ function sanitizeApprovalToken(value: string): string {
14
+ return value.replace(/[^a-zA-Z0-9._-]+/g, "_");
15
+ }
16
+
17
+ function delay(ms: number): Promise<void> {
18
+ return new Promise((resolve) => setTimeout(resolve, ms));
19
+ }
20
+
21
+ export async function requestDesktopToolApproval(
22
+ request: ToolApprovalRequest,
23
+ options: DesktopToolApprovalOptions = {},
24
+ ): Promise<ToolApprovalResult> {
25
+ const approvalDir = options.approvalDir?.trim();
26
+ const sessionId = options.sessionId?.trim();
27
+ if (!approvalDir || !sessionId) {
28
+ return {
29
+ approved: false,
30
+ reason: "Desktop tool approval IPC is not configured",
31
+ };
32
+ }
33
+
34
+ await mkdir(approvalDir, { recursive: true });
35
+ const requestId = sanitizeApprovalToken(`${request.toolCallId}`);
36
+ const requestPath = join(
37
+ approvalDir,
38
+ `${sessionId}.request.${requestId}.json`,
39
+ );
40
+ const decisionPath = join(
41
+ approvalDir,
42
+ `${sessionId}.decision.${requestId}.json`,
43
+ );
44
+ const nowIso = options.nowIso ?? (() => new Date().toISOString());
45
+
46
+ await writeFile(
47
+ requestPath,
48
+ `${JSON.stringify(
49
+ {
50
+ requestId,
51
+ sessionId,
52
+ createdAt: nowIso(),
53
+ toolCallId: request.toolCallId,
54
+ toolName: request.toolName,
55
+ input: request.input,
56
+ iteration: request.iteration,
57
+ agentId: request.agentId,
58
+ conversationId: request.conversationId,
59
+ },
60
+ null,
61
+ 2,
62
+ )}\n`,
63
+ "utf8",
64
+ );
65
+
66
+ const timeoutMs = options.timeoutMs ?? 5 * 60_000;
67
+ const pollIntervalMs = options.pollIntervalMs ?? 200;
68
+ const startedAt = Date.now();
69
+ while (Date.now() - startedAt < timeoutMs) {
70
+ try {
71
+ const raw = await readFile(decisionPath, "utf8");
72
+ const parsed = JSON.parse(raw) as {
73
+ approved?: boolean;
74
+ reason?: string;
75
+ };
76
+ const result = {
77
+ approved: parsed.approved === true,
78
+ reason: typeof parsed.reason === "string" ? parsed.reason : undefined,
79
+ };
80
+ try {
81
+ await unlink(decisionPath);
82
+ } catch {
83
+ // Best-effort cleanup.
84
+ }
85
+ try {
86
+ await unlink(requestPath);
87
+ } catch {
88
+ // Best-effort cleanup.
89
+ }
90
+ return result;
91
+ } catch {
92
+ // Decision not available yet.
93
+ }
94
+ await delay(pollIntervalMs);
95
+ }
96
+
97
+ try {
98
+ await unlink(requestPath);
99
+ } catch {
100
+ // Best-effort cleanup.
101
+ }
102
+
103
+ return { approved: false, reason: "Tool approval request timed out" };
104
+ }
@@ -0,0 +1,119 @@
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
+ listAvailableWorkflowsFromWatcher,
8
+ resolveWorkflowSlashCommandFromWatcher,
9
+ } from "./workflows";
10
+
11
+ describe("runtime workflows helpers", () => {
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 only enabled workflows from watcher snapshots", async () => {
22
+ const tempRoot = await mkdtemp(join(tmpdir(), "core-runtime-workflows-"));
23
+ tempRoots.push(tempRoot);
24
+ const workflowsDir = join(tempRoot, "workflows");
25
+ await mkdir(workflowsDir, { recursive: true });
26
+ await writeFile(
27
+ join(workflowsDir, "enabled.md"),
28
+ `---
29
+ name: enabled-workflow
30
+ ---
31
+ Run enabled workflow.`,
32
+ );
33
+ await writeFile(
34
+ join(workflowsDir, "disabled.md"),
35
+ `---
36
+ name: disabled-workflow
37
+ disabled: true
38
+ ---
39
+ Run disabled workflow.`,
40
+ );
41
+
42
+ const watcher = createUserInstructionConfigWatcher({
43
+ skills: { directories: [] },
44
+ rules: { directories: [] },
45
+ workflows: { directories: [workflowsDir] },
46
+ });
47
+
48
+ try {
49
+ await watcher.start();
50
+ expect(listAvailableWorkflowsFromWatcher(watcher)).toEqual([
51
+ {
52
+ id: "enabled-workflow",
53
+ name: "enabled-workflow",
54
+ instructions: "Run enabled workflow.",
55
+ },
56
+ ]);
57
+ } finally {
58
+ watcher.stop();
59
+ }
60
+ });
61
+
62
+ it("expands leading slash commands and preserves trailing user text", async () => {
63
+ const tempRoot = await mkdtemp(join(tmpdir(), "core-runtime-workflows-"));
64
+ tempRoots.push(tempRoot);
65
+ const workflowsDir = join(tempRoot, "workflows");
66
+ await mkdir(workflowsDir, { recursive: true });
67
+ await writeFile(
68
+ join(workflowsDir, "release.md"),
69
+ `---
70
+ name: release
71
+ ---
72
+ Run the release workflow.`,
73
+ );
74
+ await writeFile(
75
+ join(workflowsDir, "disabled.md"),
76
+ `---
77
+ name: disabled
78
+ disabled: true
79
+ ---
80
+ Do not run this workflow.`,
81
+ );
82
+
83
+ const watcher = createUserInstructionConfigWatcher({
84
+ skills: { directories: [] },
85
+ rules: { directories: [] },
86
+ workflows: { directories: [workflowsDir] },
87
+ });
88
+
89
+ try {
90
+ await watcher.start();
91
+ expect(resolveWorkflowSlashCommandFromWatcher("/release", watcher)).toBe(
92
+ "Run the release workflow.",
93
+ );
94
+ expect(
95
+ resolveWorkflowSlashCommandFromWatcher(" /release ", watcher),
96
+ ).toBe(" /release ");
97
+ expect(resolveWorkflowSlashCommandFromWatcher("/disabled", watcher)).toBe(
98
+ "/disabled",
99
+ );
100
+ expect(resolveWorkflowSlashCommandFromWatcher("/missing", watcher)).toBe(
101
+ "/missing",
102
+ );
103
+ expect(
104
+ resolveWorkflowSlashCommandFromWatcher("/release now", watcher),
105
+ ).toBe("Run the release workflow. now");
106
+ expect(
107
+ resolveWorkflowSlashCommandFromWatcher(
108
+ "/release use javascript",
109
+ watcher,
110
+ ),
111
+ ).toBe("Run the release workflow. use javascript");
112
+ expect(
113
+ resolveWorkflowSlashCommandFromWatcher("please run /release", watcher),
114
+ ).toBe("please run /release");
115
+ } finally {
116
+ watcher.stop();
117
+ }
118
+ });
119
+ });
@@ -0,0 +1,54 @@
1
+ import type { UserInstructionConfigWatcher, WorkflowConfig } from "../agents";
2
+
3
+ export type AvailableWorkflow = {
4
+ id: string;
5
+ name: string;
6
+ instructions: string;
7
+ };
8
+
9
+ function isWorkflowEnabled(workflow: WorkflowConfig): boolean {
10
+ return workflow.disabled !== true;
11
+ }
12
+
13
+ export function listAvailableWorkflowsFromWatcher(
14
+ watcher: UserInstructionConfigWatcher,
15
+ ): 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));
26
+ }
27
+
28
+ /**
29
+ * Expands a leading slash command (e.g. "/release") to workflow instructions.
30
+ * If the input starts with "/<workflow-name>", that prefix is replaced and the
31
+ * remaining input is preserved unchanged.
32
+ */
33
+ export function resolveWorkflowSlashCommandFromWatcher(
34
+ input: string,
35
+ watcher: UserInstructionConfigWatcher,
36
+ ): 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,
52
+ );
53
+ return matched ? `${matched.instructions}${remainder}` : input;
54
+ }