@kb-labs/workflow-runtime 1.1.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.
package/dist/index.js ADDED
@@ -0,0 +1,873 @@
1
+ import { resolve, dirname, join, relative } from 'path';
2
+ import { access, readFile, mkdir, writeFile } from 'fs/promises';
3
+ import { execaCommand } from 'execa';
4
+ import { randomUUID } from 'crypto';
5
+ import { getHandlerPermissions } from '@kb-labs/plugin-contracts';
6
+ import { z } from 'zod';
7
+ import fg from 'fast-glob';
8
+ import { parse } from 'yaml';
9
+ import { WorkflowSpecSchema } from '@kb-labs/workflow-contracts';
10
+ export { FileSystemArtifactClient, createFileSystemArtifactClient } from '@kb-labs/workflow-artifacts';
11
+
12
+ var __defProp = Object.defineProperty;
13
+ var __getOwnPropNames = Object.getOwnPropertyNames;
14
+ var __esm = (fn, res) => function __init() {
15
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
16
+ };
17
+ var __export = (target, all) => {
18
+ for (var name in all)
19
+ __defProp(target, name, { get: all[name], enumerable: true });
20
+ };
21
+
22
+ // src/registry/plugin-workflows.ts
23
+ var plugin_workflows_exports = {};
24
+ __export(plugin_workflows_exports, {
25
+ extractWorkflows: () => extractWorkflows,
26
+ findWorkflow: () => findWorkflow
27
+ });
28
+ function extractSnapshotManifests(snapshot) {
29
+ return (snapshot.manifests || []).map((entry) => ({
30
+ pluginId: entry.pluginId,
31
+ manifest: entry.manifest,
32
+ pluginRoot: entry.pluginRoot
33
+ }));
34
+ }
35
+ async function extractWorkflows(snapshot) {
36
+ const manifests = extractSnapshotManifests(snapshot);
37
+ const workflows = [];
38
+ for (const entry of manifests) {
39
+ const { manifest, pluginRoot } = entry;
40
+ const workflowHandlers = manifest.workflows?.handlers ?? [];
41
+ if (workflowHandlers.length === 0) {
42
+ continue;
43
+ }
44
+ let packageRoot = pluginRoot;
45
+ let currentDir = resolve(pluginRoot);
46
+ while (currentDir !== dirname(currentDir)) {
47
+ try {
48
+ const pkgPath = join(currentDir, "package.json");
49
+ await access(pkgPath);
50
+ packageRoot = currentDir;
51
+ break;
52
+ } catch {
53
+ currentDir = dirname(currentDir);
54
+ }
55
+ }
56
+ for (const wfHandler of workflowHandlers) {
57
+ const id = `plugin:${entry.pluginId}/${wfHandler.id}`;
58
+ const filePath = resolve(packageRoot, wfHandler.handler);
59
+ workflows.push({
60
+ id,
61
+ source: "plugin",
62
+ filePath,
63
+ description: wfHandler.describe,
64
+ tags: void 0,
65
+ // V3 doesn't have tags on workflow handlers
66
+ metadata: {
67
+ pluginId: entry.pluginId,
68
+ pluginVersion: manifest.version
69
+ }
70
+ });
71
+ }
72
+ }
73
+ return workflows;
74
+ }
75
+ async function findWorkflow(snapshot, id) {
76
+ const cleanId = id.startsWith("plugin:") ? id.slice("plugin:".length) : id;
77
+ const workflows = await extractWorkflows(snapshot);
78
+ return workflows.find((w) => w.id === id || w.id.endsWith(":" + cleanId)) ?? null;
79
+ }
80
+ var init_plugin_workflows = __esm({
81
+ "src/registry/plugin-workflows.ts"() {
82
+ }
83
+ });
84
+
85
+ // src/context.ts
86
+ function createStepContext(input) {
87
+ return {
88
+ runId: input.runId,
89
+ jobId: input.jobId,
90
+ stepId: input.stepId,
91
+ attempt: input.attempt ?? 0,
92
+ env: { ...process.env, ...input.env ?? {} },
93
+ secrets: input.secrets ?? {},
94
+ artifacts: input.artifacts,
95
+ events: input.events,
96
+ logger: input.logger,
97
+ // Use provided logger (platform.logger)
98
+ trace: input.trace
99
+ };
100
+ }
101
+ function resolveCommand(step) {
102
+ const withBlock = step.with ?? {};
103
+ const commandField = withBlock.command ?? withBlock.run ?? withBlock.script;
104
+ return typeof commandField === "string" ? commandField : null;
105
+ }
106
+ var LocalRunner = class {
107
+ shell;
108
+ constructor(options = {}) {
109
+ this.shell = options.shell ?? process.env.SHELL ?? "bash";
110
+ }
111
+ async execute(request) {
112
+ const { spec, context } = request;
113
+ const command = resolveCommand(spec);
114
+ if (spec.uses && spec.uses !== "builtin:shell") {
115
+ context.logger.error(`LocalRunner cannot execute ${spec.uses}`, {
116
+ stepId: context.stepId
117
+ });
118
+ return {
119
+ status: "failed",
120
+ error: {
121
+ message: `Local runner cannot execute step with uses="${spec.uses}"`,
122
+ code: "UNSUPPORTED_STEP"
123
+ }
124
+ };
125
+ }
126
+ if (!command) {
127
+ context.logger.error("LocalRunner missing command", {
128
+ stepId: context.stepId
129
+ });
130
+ return {
131
+ status: "failed",
132
+ error: {
133
+ message: 'Local runner requires "with.command" (or with.run/with.script) to be specified',
134
+ code: "INVALID_STEP"
135
+ }
136
+ };
137
+ }
138
+ const cwd = request.workspace ?? process.cwd();
139
+ const env = {
140
+ ...process.env,
141
+ ...context.env,
142
+ ...context.secrets
143
+ };
144
+ if (request.signal?.aborted) {
145
+ return buildCancelledResult(request.signal);
146
+ }
147
+ context.logger.info("Executing builtin shell step", {
148
+ command,
149
+ cwd,
150
+ stepId: context.stepId
151
+ });
152
+ try {
153
+ const result = await execaCommand(command, {
154
+ cwd,
155
+ shell: this.shell,
156
+ env,
157
+ stdio: "pipe",
158
+ signal: request.signal
159
+ });
160
+ context.logger.info("Shell step completed", {
161
+ stepId: context.stepId,
162
+ exitCode: result.exitCode
163
+ });
164
+ return {
165
+ status: "success",
166
+ outputs: {
167
+ stdout: result.stdout,
168
+ stderr: result.stderr,
169
+ exitCode: result.exitCode
170
+ }
171
+ };
172
+ } catch (error) {
173
+ if (request.signal?.aborted) {
174
+ return buildCancelledResult(request.signal, error);
175
+ }
176
+ const message = error instanceof Error ? error.message : "Shell step failed";
177
+ const exitCode = typeof error?.exitCode === "number" ? error.exitCode : void 0;
178
+ context.logger.error("Shell step failed", {
179
+ stepId: context.stepId,
180
+ error: message,
181
+ exitCode
182
+ });
183
+ return {
184
+ status: "failed",
185
+ error: {
186
+ message,
187
+ code: "STEP_EXECUTION_FAILED",
188
+ details: {
189
+ exitCode
190
+ }
191
+ }
192
+ };
193
+ }
194
+ }
195
+ };
196
+ function buildCancelledResult(signal, error) {
197
+ const reason = error instanceof Error ? error.message : signalReason(signal) ?? "Step execution cancelled";
198
+ return {
199
+ status: "cancelled",
200
+ error: {
201
+ message: reason,
202
+ code: "STEP_CANCELLED"
203
+ }
204
+ };
205
+ }
206
+ function signalReason(signal) {
207
+ if (!signal.aborted) {
208
+ return void 0;
209
+ }
210
+ const reason = signal.reason;
211
+ if (reason instanceof Error) {
212
+ return reason.message;
213
+ }
214
+ if (typeof reason === "string") {
215
+ return reason;
216
+ }
217
+ return void 0;
218
+ }
219
+
220
+ // src/runners/output-normalizer.ts
221
+ function isCommandResult(value) {
222
+ return typeof value === "object" && value !== null && "exitCode" in value && "result" in value && typeof value.exitCode === "number";
223
+ }
224
+ function toWorkflowOutputs(data) {
225
+ if (isCommandResult(data)) {
226
+ const inner = data.result;
227
+ if (typeof inner === "object" && inner !== null) {
228
+ return inner;
229
+ }
230
+ return inner !== void 0 && inner !== null ? { result: inner } : {};
231
+ }
232
+ if (typeof data === "object" && data !== null) {
233
+ return data;
234
+ }
235
+ if (data !== void 0 && data !== null) {
236
+ return { result: data };
237
+ }
238
+ return {};
239
+ }
240
+
241
+ // src/runners/sandbox-runner.ts
242
+ var SandboxRunner = class {
243
+ backend;
244
+ cliApi;
245
+ workspaceRoot;
246
+ defaultTimeout;
247
+ analytics;
248
+ logger;
249
+ constructor(options) {
250
+ this.backend = options.backend;
251
+ this.cliApi = options.cliApi;
252
+ this.workspaceRoot = options.workspaceRoot ?? process.cwd();
253
+ this.defaultTimeout = options.defaultTimeout ?? 12e4;
254
+ this.analytics = options.analytics;
255
+ }
256
+ async execute(request) {
257
+ const { spec, context, signal } = request;
258
+ const startTime = Date.now();
259
+ if (signal?.aborted) {
260
+ return buildCancelledResult2(signal);
261
+ }
262
+ if (!spec.uses) {
263
+ return this.buildValidationError(context, 'Sandbox runner requires "uses" field to specify plugin handler');
264
+ }
265
+ const resolution = await this.tryResolveCommand(spec, request, context);
266
+ if (!resolution.ok) {
267
+ return resolution.error;
268
+ }
269
+ const executionRequest = this.buildExecutionRequest(resolution.value, request, context);
270
+ context.logger.info("Executing plugin handler", {
271
+ stepId: context.stepId,
272
+ pluginId: resolution.value.pluginId,
273
+ handler: resolution.value.handler,
274
+ executionId: executionRequest.executionId
275
+ });
276
+ this.analytics?.track("workflow.sandbox.execution.started", {
277
+ stepId: context.stepId,
278
+ pluginId: resolution.value.pluginId,
279
+ handler: resolution.value.handler,
280
+ uses: spec.uses
281
+ }).catch(() => {
282
+ });
283
+ const result = await this.backend.execute(executionRequest, { signal, onLog: context.onLog });
284
+ const duration = Date.now() - startTime;
285
+ if (result.ok) {
286
+ this.analytics?.track("workflow.sandbox.execution.completed", {
287
+ stepId: context.stepId,
288
+ pluginId: resolution.value.pluginId,
289
+ handler: resolution.value.handler,
290
+ durationMs: duration
291
+ }).catch(() => {
292
+ });
293
+ } else {
294
+ this.analytics?.track("workflow.sandbox.execution.failed", {
295
+ stepId: context.stepId,
296
+ pluginId: resolution.value.pluginId,
297
+ handler: resolution.value.handler,
298
+ errorCode: result.error?.code,
299
+ errorMessage: result.error?.message,
300
+ durationMs: duration
301
+ }).catch(() => {
302
+ });
303
+ }
304
+ return this.mapExecutionResult(result, executionRequest.executionId, context, signal);
305
+ }
306
+ /**
307
+ * Validate step spec and build error result if invalid
308
+ */
309
+ buildValidationError(context, message) {
310
+ context.logger.error("SandboxRunner validation failed", { stepId: context.stepId, message });
311
+ return {
312
+ status: "failed",
313
+ error: { message, code: "INVALID_STEP" }
314
+ };
315
+ }
316
+ /**
317
+ * Try to resolve command, returning result wrapper
318
+ */
319
+ async tryResolveCommand(spec, request, context) {
320
+ try {
321
+ const resolution = await this.resolveCommand(spec, request);
322
+ return { ok: true, value: resolution };
323
+ } catch (error) {
324
+ const message = error instanceof Error ? error.message : "Failed to resolve plugin command";
325
+ context.logger.error("Plugin command resolution failed", {
326
+ stepId: context.stepId,
327
+ uses: spec.uses,
328
+ error: message
329
+ });
330
+ return {
331
+ ok: false,
332
+ error: {
333
+ status: "failed",
334
+ error: { message, code: "COMMAND_RESOLUTION_FAILED" }
335
+ }
336
+ };
337
+ }
338
+ }
339
+ /**
340
+ * Build ExecutionRequest from resolved command
341
+ */
342
+ buildExecutionRequest(resolution, request, context) {
343
+ const requestId = context.trace?.traceId ?? randomUUID();
344
+ const traceId = context.trace?.traceId ?? requestId;
345
+ const executionId = `exec_${context.stepId}_${Date.now()}_${randomUUID().slice(0, 8)}`;
346
+ const spanId = executionId;
347
+ const invocationId = executionId;
348
+ const hostContext = {
349
+ host: "workflow",
350
+ workflowId: context.runId,
351
+ runId: context.runId,
352
+ jobId: context.jobId,
353
+ stepId: context.stepId,
354
+ attempt: context.attempt,
355
+ input: resolution.input
356
+ };
357
+ const descriptor = {
358
+ hostType: "workflow",
359
+ pluginId: resolution.pluginId,
360
+ pluginVersion: resolution.pluginVersion,
361
+ requestId,
362
+ permissions: resolution.permissions,
363
+ hostContext,
364
+ configSection: resolution.configSection
365
+ // For useConfig() auto-detection
366
+ };
367
+ Object.assign(descriptor, {
368
+ traceId,
369
+ spanId,
370
+ invocationId,
371
+ executionId
372
+ });
373
+ return {
374
+ executionId,
375
+ descriptor,
376
+ pluginRoot: resolution.pluginRoot,
377
+ handlerRef: resolution.handler,
378
+ input: resolution.input,
379
+ workspace: request.workspace ? {
380
+ type: "local",
381
+ cwd: request.workspace
382
+ } : void 0,
383
+ timeoutMs: resolution.permissions.quotas?.timeoutMs ?? this.defaultTimeout,
384
+ target: request.target
385
+ };
386
+ }
387
+ /**
388
+ * Map ExecutionResult to StepExecutionResult
389
+ */
390
+ // eslint-disable-next-line sonarjs/cognitive-complexity -- Result mapping logic: handles success/failure/cancelled states, conditional stdout/stderr logging, debug metadata extraction, and error code translation
391
+ mapExecutionResult(result, executionId, context, signal) {
392
+ if (result.ok) {
393
+ const data = result.data;
394
+ context.logger.debug("mapExecutionResult received data", {
395
+ stepId: context.stepId,
396
+ executionId,
397
+ dataType: typeof data,
398
+ dataKeys: data && typeof data === "object" ? Object.keys(data) : [],
399
+ hasStdout: !!(data && typeof data === "object" && data.stdout),
400
+ hasStderr: !!(data && typeof data === "object" && data.stderr)
401
+ });
402
+ const logMeta = {
403
+ stepId: context.stepId,
404
+ executionId,
405
+ executionTimeMs: result.executionTimeMs
406
+ };
407
+ if (data && typeof data === "object") {
408
+ if (data.stdout) {
409
+ logMeta.stdout = data.stdout;
410
+ }
411
+ if (data.stderr) {
412
+ logMeta.stderr = data.stderr;
413
+ }
414
+ if (data.exitCode !== void 0) {
415
+ logMeta.exitCode = data.exitCode;
416
+ }
417
+ }
418
+ context.logger.info("Plugin handler completed", logMeta);
419
+ if (data && typeof data === "object" && data.ok === false) {
420
+ const message = data.stderr ? String(data.stderr).slice(0, 500) : `Step handler reported failure (exitCode: ${data.exitCode ?? "unknown"})`;
421
+ context.logger.error("Plugin handler reported failure via ok:false", {
422
+ stepId: context.stepId,
423
+ exitCode: data.exitCode,
424
+ stderr: data.stderr
425
+ });
426
+ return {
427
+ status: "failed",
428
+ error: {
429
+ message,
430
+ code: "HANDLER_REPORTED_FAILURE"
431
+ }
432
+ };
433
+ }
434
+ return {
435
+ status: "success",
436
+ outputs: toWorkflowOutputs(result.data)
437
+ };
438
+ }
439
+ if (signal?.aborted || result.error?.code === "ABORTED") {
440
+ return buildCancelledResult2(signal, result.error);
441
+ }
442
+ context.logger.error("Plugin handler failed", {
443
+ stepId: context.stepId,
444
+ executionId,
445
+ error: result.error?.message,
446
+ code: result.error?.code
447
+ });
448
+ return {
449
+ status: "failed",
450
+ error: {
451
+ message: result.error?.message ?? "Plugin execution failed",
452
+ code: result.error?.code ?? "UNKNOWN_ERROR",
453
+ stack: result.error?.stack,
454
+ details: result.error?.details
455
+ }
456
+ };
457
+ }
458
+ /**
459
+ * Resolve command reference to plugin handler.
460
+ *
461
+ * Supports three formats:
462
+ * - `plugin:id/handler` - workflow handler (native)
463
+ * - `command:name` - CLI command (via adapter)
464
+ * - `builtin:shell` - built-in shell execution
465
+ */
466
+ async resolveCommand(spec, request) {
467
+ const uses = spec.uses;
468
+ const input = spec.with ?? {};
469
+ if (uses.startsWith("plugin:")) {
470
+ return this.resolvePluginHandler(uses, input);
471
+ }
472
+ if (uses.startsWith("command:")) {
473
+ return this.resolveCLICommand(uses, input, request);
474
+ }
475
+ if (uses === "builtin:shell") {
476
+ return this.resolveBuiltinShell(spec);
477
+ }
478
+ if (uses === "builtin:approval" || uses === "builtin:gate") {
479
+ throw new Error(`${uses} is handled by the workflow worker, not the sandbox runner`);
480
+ }
481
+ throw new Error(`Unsupported uses format: ${uses}. Expected "plugin:...", "command:...", or "builtin:shell"`);
482
+ }
483
+ /**
484
+ * Resolve plugin handler reference.
485
+ * Format: `plugin:id/handler` or `plugin:id/path/to/handler`
486
+ */
487
+ async resolvePluginHandler(uses, input) {
488
+ const pluginRef = uses.slice("plugin:".length);
489
+ const [pluginId, ...handlerParts] = pluginRef.split("/");
490
+ if (!pluginId || handlerParts.length === 0) {
491
+ throw new Error(`Invalid plugin reference: ${uses}. Expected "plugin:id/handler"`);
492
+ }
493
+ const handlerName = handlerParts.join("/");
494
+ const snapshot = this.cliApi.snapshot();
495
+ const entry = snapshot.manifests?.find(
496
+ (m) => m.pluginId === pluginId || m.pluginId.endsWith(`/${pluginId}`)
497
+ );
498
+ if (!entry) {
499
+ throw new Error(`Plugin not found: ${pluginId}`);
500
+ }
501
+ const workflowHandlers = entry.manifest.workflows?.handlers ?? [];
502
+ const handler = workflowHandlers.find((h) => h.id === handlerName);
503
+ if (!handler) {
504
+ throw new Error(`Workflow handler not found: ${handlerName} in plugin ${pluginId}`);
505
+ }
506
+ return {
507
+ pluginId,
508
+ pluginVersion: entry.manifest.version,
509
+ pluginRoot: entry.pluginRoot,
510
+ handler: handler.handler,
511
+ // File path from manifest
512
+ input,
513
+ permissions: getHandlerPermissions(entry.manifest, "workflow", handlerName)
514
+ };
515
+ }
516
+ /**
517
+ * Resolve CLI command to plugin handler (with adapter).
518
+ *
519
+ * Format: `command:name` (e.g., `command:mind:rag-index`)
520
+ *
521
+ * This uses the CLI Adapter pattern to make CLI commands work in workflow context:
522
+ * - Searches for CLI command in plugin manifests
523
+ * - Wraps workflow input in CLI-compatible format { argv, flags, cwd }
524
+ * - Allows reusing existing CLI commands without writing workflow handlers
525
+ */
526
+ async resolveCLICommand(uses, input, request) {
527
+ const commandName = uses.slice("command:".length);
528
+ const snapshot = this.cliApi.snapshot();
529
+ for (const entry of snapshot.manifests ?? []) {
530
+ const commands = entry.manifest.cli?.commands ?? [];
531
+ const command = commands.find((c) => c.id === commandName);
532
+ if (command) {
533
+ return {
534
+ pluginId: entry.pluginId,
535
+ pluginVersion: entry.manifest.version,
536
+ pluginRoot: entry.pluginRoot,
537
+ handler: command.handler,
538
+ input: this.adaptToCLIFormat(input, request),
539
+ // CLI Adapter
540
+ permissions: getHandlerPermissions(entry.manifest, "cli", commandName),
541
+ configSection: entry.manifest.configSection
542
+ // For useConfig() auto-detection
543
+ };
544
+ }
545
+ }
546
+ throw new Error(`CLI command not found: ${commandName}`);
547
+ }
548
+ /**
549
+ * CLI Adapter: Convert workflow input to CLI-compatible format.
550
+ *
551
+ * Transforms:
552
+ * { scope: "default", incremental: true }
553
+ * Into:
554
+ * { argv: [], flags: { scope: "default", incremental: true }, cwd: "/workspace" }
555
+ *
556
+ * This allows CLI commands to work in workflow context without modification.
557
+ */
558
+ adaptToCLIFormat(input, request) {
559
+ if (input && typeof input === "object" && ("argv" in input || "flags" in input)) {
560
+ return input;
561
+ }
562
+ return {
563
+ argv: [],
564
+ flags: input || {},
565
+ cwd: request.workspace || this.workspaceRoot
566
+ };
567
+ }
568
+ /**
569
+ * Resolve builtin:shell to built-in shell handler.
570
+ *
571
+ * Returns a resolution pointing to the builtin-handlers/shell.js file
572
+ * that will be executed through ExecutionBackend.
573
+ */
574
+ async resolveBuiltinShell(spec) {
575
+ const builtinsUrl = await import.meta.resolve("@kb-labs/workflow-builtins");
576
+ const builtinsPath = builtinsUrl.replace("file://", "").replace("/dist/index.js", "");
577
+ const withBlock = spec.with ?? {};
578
+ const command = withBlock.command ?? withBlock.run ?? withBlock.script;
579
+ if (typeof command !== "string") {
580
+ throw new Error(
581
+ 'builtin:shell requires "with.command" (or with.run/with.script) to be a string'
582
+ );
583
+ }
584
+ const shellInput = {
585
+ command,
586
+ env: typeof withBlock.env === "object" ? withBlock.env : void 0,
587
+ timeout: typeof withBlock.timeout === "number" ? withBlock.timeout : void 0,
588
+ throwOnError: typeof withBlock.throwOnError === "boolean" ? withBlock.throwOnError : false
589
+ };
590
+ return {
591
+ pluginId: "@kb-labs/workflow-builtins",
592
+ pluginVersion: "0.1.0",
593
+ pluginRoot: builtinsPath,
594
+ handler: "dist/shell.js",
595
+ // Relative path from pluginRoot
596
+ input: shellInput,
597
+ permissions: {
598
+ shell: { allow: ["*"] }
599
+ // builtin:shell needs shell access by definition
600
+ }
601
+ };
602
+ }
603
+ };
604
+ function buildCancelledResult2(signal, error) {
605
+ const reason = error?.message ?? signalReason2(signal) ?? "Step execution cancelled";
606
+ return {
607
+ status: "cancelled",
608
+ error: {
609
+ message: reason,
610
+ code: "STEP_CANCELLED"
611
+ }
612
+ };
613
+ }
614
+ function signalReason2(signal) {
615
+ if (!signal?.aborted) {
616
+ return void 0;
617
+ }
618
+ const reason = signal.reason;
619
+ if (reason instanceof Error) {
620
+ return reason.message;
621
+ }
622
+ if (typeof reason === "string") {
623
+ return reason;
624
+ }
625
+ return void 0;
626
+ }
627
+ var RemoteMarketplaceSourceSchema = z.object({
628
+ name: z.string().min(1),
629
+ url: z.string().url(),
630
+ ref: z.string().optional(),
631
+ // branch/tag, default: 'main'
632
+ path: z.string().optional()
633
+ // subdirectory in repo, default: '/'
634
+ });
635
+ var BudgetConfigSchema = z.object({
636
+ enabled: z.boolean().default(false),
637
+ limit: z.number().positive().optional(),
638
+ // Total budget limit (in cost units)
639
+ period: z.enum(["run", "day", "week", "month"]).default("run"),
640
+ action: z.enum(["warn", "fail", "cancel"]).default("warn"),
641
+ // Extension point: custom cost calculator plugin
642
+ costCalculator: z.string().optional()
643
+ });
644
+ var WorkflowConfigSchema = z.object({
645
+ workspaces: z.array(z.string()).default([".kb/workflows/**/*.yml"]),
646
+ plugins: z.boolean().default(true),
647
+ remotes: z.array(RemoteMarketplaceSourceSchema).optional(),
648
+ maxDepth: z.number().int().positive().default(2),
649
+ budget: BudgetConfigSchema.optional(),
650
+ defaults: z.object({
651
+ mode: z.enum(["wait", "fire-and-forget"]).default("wait"),
652
+ inheritEnv: z.boolean().default(true)
653
+ }).optional()
654
+ });
655
+ async function loadWorkflowConfig(workspaceRoot) {
656
+ const configPath = join(workspaceRoot, "kb.config.json");
657
+ try {
658
+ const raw = await readFile(configPath, "utf-8");
659
+ const config = JSON.parse(raw);
660
+ if (!config.workflow) {
661
+ return WorkflowConfigSchema.parse({});
662
+ }
663
+ return WorkflowConfigSchema.parse(config.workflow);
664
+ } catch (error) {
665
+ if (error instanceof Error && (error.message.includes("ENOENT") || error.message.includes("Unexpected token"))) {
666
+ return WorkflowConfigSchema.parse({});
667
+ }
668
+ throw error;
669
+ }
670
+ }
671
+ async function saveWorkflowConfig(workspaceRoot, updates) {
672
+ const configPath = join(workspaceRoot, "kb.config.json");
673
+ let existingConfig = {};
674
+ try {
675
+ const raw = await readFile(configPath, "utf-8");
676
+ existingConfig = JSON.parse(raw);
677
+ } catch {
678
+ existingConfig = {};
679
+ }
680
+ const currentWorkflow = existingConfig.workflow ?? {};
681
+ const updatedWorkflow = {
682
+ ...currentWorkflow,
683
+ ...updates,
684
+ // Deep merge for arrays (remotes)
685
+ remotes: updates.remotes ?? currentWorkflow.remotes
686
+ };
687
+ const updatedConfig = {
688
+ ...existingConfig,
689
+ workflow: updatedWorkflow
690
+ };
691
+ await mkdir(dirname(configPath), { recursive: true });
692
+ await writeFile(
693
+ configPath,
694
+ JSON.stringify(updatedConfig, null, 2) + "\n",
695
+ "utf-8"
696
+ );
697
+ }
698
+ var WorkspaceWorkflowRegistry = class {
699
+ constructor(config) {
700
+ this.config = config;
701
+ }
702
+ cache = null;
703
+ async list() {
704
+ if (this.cache) {
705
+ return this.cache;
706
+ }
707
+ const workflows = [];
708
+ const files = await fg(this.config.patterns, {
709
+ cwd: this.config.workspaceRoot,
710
+ absolute: true,
711
+ onlyFiles: true,
712
+ ignore: ["node_modules/**", "dist/**", ".git/**"]
713
+ });
714
+ const results = await Promise.allSettled(
715
+ files.map(async (file) => {
716
+ const spec = await this.loadWorkflowSpec(file);
717
+ if (!spec) {
718
+ return null;
719
+ }
720
+ const relativePath = relative(this.config.workspaceRoot, file);
721
+ const id = this.generateId(relativePath);
722
+ return {
723
+ id,
724
+ source: "workspace",
725
+ filePath: file,
726
+ description: spec.description
727
+ // tags: spec.tags, // TODO: Add tags to WorkflowSpec if needed
728
+ };
729
+ })
730
+ );
731
+ for (const result of results) {
732
+ if (result.status === "fulfilled" && result.value !== null) {
733
+ workflows.push(result.value);
734
+ } else if (result.status === "rejected") {
735
+ console.warn(
736
+ "[WorkspaceWorkflowRegistry] Failed to load workflow:",
737
+ result.reason instanceof Error ? result.reason.message : String(result.reason)
738
+ );
739
+ }
740
+ }
741
+ this.cache = workflows;
742
+ return workflows;
743
+ }
744
+ async resolve(id) {
745
+ const cleanId = id.startsWith("workspace:") ? id.slice("workspace:".length) : id;
746
+ const all = await this.list();
747
+ return all.find((w) => w.id === id || w.id.endsWith(":" + cleanId)) ?? null;
748
+ }
749
+ async refresh() {
750
+ this.cache = null;
751
+ }
752
+ async dispose() {
753
+ }
754
+ async loadWorkflowSpec(filePath) {
755
+ try {
756
+ const raw = await readFile(filePath, "utf-8");
757
+ const parsed = filePath.endsWith(".json") ? JSON.parse(raw) : parse(raw);
758
+ const result = WorkflowSpecSchema.safeParse(parsed);
759
+ if (!result.success) {
760
+ return null;
761
+ }
762
+ return result.data;
763
+ } catch {
764
+ return null;
765
+ }
766
+ }
767
+ generateId(relativePath) {
768
+ const withoutExt = relativePath.replace(/\.(yml|yaml|json)$/, "");
769
+ const normalized = withoutExt.replace(/\\/g, "/");
770
+ return `workspace:${normalized}`;
771
+ }
772
+ };
773
+
774
+ // src/registry/composite-registry.ts
775
+ init_plugin_workflows();
776
+
777
+ // src/registry/errors.ts
778
+ var WorkflowRegistryError = class extends Error {
779
+ constructor(message, workflowId) {
780
+ super(message);
781
+ this.workflowId = workflowId;
782
+ this.name = "WorkflowRegistryError";
783
+ }
784
+ };
785
+
786
+ // src/registry/composite-registry.ts
787
+ var CompositeWorkflowRegistry = class {
788
+ constructor(workspace, cliApi, remote) {
789
+ this.workspace = workspace;
790
+ this.cliApi = cliApi;
791
+ this.remote = remote;
792
+ }
793
+ cache = null;
794
+ async list() {
795
+ if (this.cache) {
796
+ return this.cache;
797
+ }
798
+ const registries = [
799
+ this.workspace.list()
800
+ ];
801
+ if (this.cliApi) {
802
+ const snapshot = this.cliApi.snapshot();
803
+ registries.push(extractWorkflows(snapshot));
804
+ }
805
+ if (this.remote) {
806
+ registries.push(this.remote.list());
807
+ }
808
+ const results = await Promise.all(registries);
809
+ const allWorkflows = results.flat();
810
+ const ids = /* @__PURE__ */ new Set();
811
+ const conflicts = [];
812
+ for (const wf of allWorkflows) {
813
+ if (ids.has(wf.id)) {
814
+ conflicts.push(wf.id);
815
+ }
816
+ ids.add(wf.id);
817
+ }
818
+ if (conflicts.length > 0) {
819
+ throw new WorkflowRegistryError(
820
+ `Workflow ID conflicts detected: ${conflicts.join(", ")}. Use explicit prefixes (workspace:, plugin:, or remote:) to disambiguate.`
821
+ );
822
+ }
823
+ this.cache = allWorkflows;
824
+ return this.cache;
825
+ }
826
+ async resolve(id) {
827
+ if (id.startsWith("workspace:")) {
828
+ return this.workspace.resolve(id);
829
+ }
830
+ if (id.startsWith("plugin:") && this.cliApi) {
831
+ const snapshot = this.cliApi.snapshot();
832
+ const { findWorkflow: findWorkflow2 } = await Promise.resolve().then(() => (init_plugin_workflows(), plugin_workflows_exports));
833
+ return findWorkflow2(snapshot, id);
834
+ }
835
+ if (id.startsWith("remote:") && this.remote) {
836
+ return this.remote.resolve(id);
837
+ }
838
+ const all = await this.list();
839
+ const matches = all.filter(
840
+ (w) => w.id === id || w.id.endsWith(":" + id)
841
+ );
842
+ if (matches.length > 1) {
843
+ throw new WorkflowRegistryError(
844
+ `Ambiguous workflow ID "${id}". Multiple matches: ${matches.map((m) => m.id).join(", ")}. Use explicit prefix (workspace:, plugin:, or remote:).`,
845
+ id
846
+ );
847
+ }
848
+ return matches[0] ?? null;
849
+ }
850
+ async refresh() {
851
+ this.cache = null;
852
+ const refreshTasks = [
853
+ this.workspace.refresh()
854
+ ];
855
+ if (this.remote) {
856
+ refreshTasks.push(this.remote.refresh());
857
+ }
858
+ await Promise.all(refreshTasks);
859
+ }
860
+ async dispose() {
861
+ await Promise.all([
862
+ this.workspace.dispose?.() ?? Promise.resolve(),
863
+ this.remote?.dispose?.() ?? Promise.resolve()
864
+ ]);
865
+ }
866
+ };
867
+
868
+ // src/registry/index.ts
869
+ init_plugin_workflows();
870
+
871
+ export { BudgetConfigSchema, CompositeWorkflowRegistry, LocalRunner, RemoteMarketplaceSourceSchema, SandboxRunner, WorkflowConfigSchema, WorkflowRegistryError, WorkspaceWorkflowRegistry, createStepContext, extractWorkflows, findWorkflow, loadWorkflowConfig, saveWorkflowConfig };
872
+ //# sourceMappingURL=index.js.map
873
+ //# sourceMappingURL=index.js.map