@synergenius/flowweaver-pack-weaver 0.6.1 → 0.7.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 (168) hide show
  1. package/dist/bot/ai-client.d.ts +1 -0
  2. package/dist/bot/ai-client.d.ts.map +1 -1
  3. package/dist/bot/ai-client.js +52 -1
  4. package/dist/bot/ai-client.js.map +1 -1
  5. package/dist/bot/audit-logger.d.ts +5 -0
  6. package/dist/bot/audit-logger.d.ts.map +1 -0
  7. package/dist/bot/audit-logger.js +42 -0
  8. package/dist/bot/audit-logger.js.map +1 -0
  9. package/dist/bot/audit-store.d.ts +13 -0
  10. package/dist/bot/audit-store.d.ts.map +1 -0
  11. package/dist/bot/audit-store.js +59 -0
  12. package/dist/bot/audit-store.js.map +1 -0
  13. package/dist/bot/cli-provider.d.ts +1 -0
  14. package/dist/bot/cli-provider.d.ts.map +1 -1
  15. package/dist/bot/cli-provider.js +86 -22
  16. package/dist/bot/cli-provider.js.map +1 -1
  17. package/dist/bot/cli-stream-parser.d.ts +11 -0
  18. package/dist/bot/cli-stream-parser.d.ts.map +1 -0
  19. package/dist/bot/cli-stream-parser.js +53 -0
  20. package/dist/bot/cli-stream-parser.js.map +1 -0
  21. package/dist/bot/file-validator.d.ts +1 -1
  22. package/dist/bot/file-validator.d.ts.map +1 -1
  23. package/dist/bot/file-validator.js +13 -27
  24. package/dist/bot/file-validator.js.map +1 -1
  25. package/dist/bot/fw-api.d.ts +8 -0
  26. package/dist/bot/fw-api.d.ts.map +1 -0
  27. package/dist/bot/fw-api.js +12 -0
  28. package/dist/bot/fw-api.js.map +1 -0
  29. package/dist/bot/runner.d.ts +2 -1
  30. package/dist/bot/runner.d.ts.map +1 -1
  31. package/dist/bot/runner.js +8 -0
  32. package/dist/bot/runner.js.map +1 -1
  33. package/dist/bot/step-executor.d.ts +3 -2
  34. package/dist/bot/step-executor.d.ts.map +1 -1
  35. package/dist/bot/step-executor.js +9 -30
  36. package/dist/bot/step-executor.js.map +1 -1
  37. package/dist/bot/system-prompt.d.ts +13 -1
  38. package/dist/bot/system-prompt.d.ts.map +1 -1
  39. package/dist/bot/system-prompt.js +28 -22
  40. package/dist/bot/system-prompt.js.map +1 -1
  41. package/dist/bot/types.d.ts +9 -1
  42. package/dist/bot/types.d.ts.map +1 -1
  43. package/dist/cli-bridge.d.ts.map +1 -1
  44. package/dist/cli-bridge.js +2 -1
  45. package/dist/cli-bridge.js.map +1 -1
  46. package/dist/cli-handlers.d.ts +2 -1
  47. package/dist/cli-handlers.d.ts.map +1 -1
  48. package/dist/cli-handlers.js +69 -0
  49. package/dist/cli-handlers.js.map +1 -1
  50. package/dist/node-types/approval-gate.d.ts.map +1 -1
  51. package/dist/node-types/approval-gate.js +4 -0
  52. package/dist/node-types/approval-gate.js.map +1 -1
  53. package/dist/node-types/exec-validate-retry.d.ts.map +1 -1
  54. package/dist/node-types/exec-validate-retry.js +10 -4
  55. package/dist/node-types/exec-validate-retry.js.map +1 -1
  56. package/dist/node-types/execute-plan.js +1 -1
  57. package/dist/node-types/execute-plan.js.map +1 -1
  58. package/dist/node-types/git-ops.d.ts.map +1 -1
  59. package/dist/node-types/git-ops.js +2 -0
  60. package/dist/node-types/git-ops.js.map +1 -1
  61. package/dist/node-types/plan-task.d.ts.map +1 -1
  62. package/dist/node-types/plan-task.js +9 -1
  63. package/dist/node-types/plan-task.js.map +1 -1
  64. package/dist/node-types/send-notify.d.ts.map +1 -1
  65. package/dist/node-types/send-notify.js +4 -1
  66. package/dist/node-types/send-notify.js.map +1 -1
  67. package/dist/node-types/validate-result.d.ts +2 -2
  68. package/dist/node-types/validate-result.d.ts.map +1 -1
  69. package/dist/node-types/validate-result.js +2 -2
  70. package/dist/node-types/validate-result.js.map +1 -1
  71. package/dist/workflows/weaver-bot-batch.d.ts +4 -1
  72. package/dist/workflows/weaver-bot-batch.d.ts.map +1 -1
  73. package/dist/workflows/weaver-bot-batch.js +1 -1
  74. package/dist/workflows/weaver-bot-batch.js.map +1 -1
  75. package/dist/workflows/weaver-bot.d.ts +4 -1
  76. package/dist/workflows/weaver-bot.d.ts.map +1 -1
  77. package/dist/workflows/weaver-bot.js +1 -1
  78. package/dist/workflows/weaver-bot.js.map +1 -1
  79. package/flowweaver.manifest.json +1 -1
  80. package/package.json +3 -2
  81. package/src/bot/agent-provider.ts +273 -0
  82. package/src/bot/ai-client.ts +109 -0
  83. package/src/bot/approvals.ts +273 -0
  84. package/src/bot/audit-logger.ts +45 -0
  85. package/src/bot/audit-store.ts +69 -0
  86. package/src/bot/bot-agent-channel.ts +99 -0
  87. package/src/bot/cli-provider.ts +169 -0
  88. package/src/bot/cli-stream-parser.ts +59 -0
  89. package/src/bot/cost-store.ts +92 -0
  90. package/src/bot/cost-tracker.ts +72 -0
  91. package/src/bot/cron-parser.ts +153 -0
  92. package/src/bot/cron-scheduler.ts +48 -0
  93. package/src/bot/dashboard.ts +658 -0
  94. package/src/bot/design-checker.ts +327 -0
  95. package/src/bot/file-lock.ts +73 -0
  96. package/src/bot/file-validator.ts +41 -0
  97. package/src/bot/file-watcher.ts +103 -0
  98. package/src/bot/fw-api.ts +18 -0
  99. package/src/bot/genesis-prompt-context.ts +135 -0
  100. package/src/bot/genesis-store.ts +180 -0
  101. package/src/bot/index.ts +127 -0
  102. package/src/bot/notifications.ts +263 -0
  103. package/src/bot/pipeline-runner.ts +324 -0
  104. package/src/bot/provider-registry.ts +236 -0
  105. package/src/bot/run-store.ts +169 -0
  106. package/src/bot/runner.ts +311 -0
  107. package/src/bot/session-state.ts +73 -0
  108. package/src/bot/steering.ts +44 -0
  109. package/src/bot/step-executor.ts +34 -0
  110. package/src/bot/system-prompt.ts +280 -0
  111. package/src/bot/task-queue.ts +111 -0
  112. package/src/bot/types.ts +571 -0
  113. package/src/bot/utils.ts +17 -0
  114. package/src/bot/watch-daemon.ts +203 -0
  115. package/src/bot/web-approval.ts +240 -0
  116. package/src/cli-bridge.ts +41 -0
  117. package/src/cli-handlers.ts +1271 -0
  118. package/src/docs/weaver-config.md +135 -0
  119. package/src/index.ts +173 -0
  120. package/src/mcp-tools.ts +274 -0
  121. package/src/node-types/abort-task.ts +31 -0
  122. package/src/node-types/approval-gate.ts +75 -0
  123. package/src/node-types/bot-report.ts +82 -0
  124. package/src/node-types/build-context.ts +65 -0
  125. package/src/node-types/detect-provider.ts +75 -0
  126. package/src/node-types/exec-validate-retry.ts +175 -0
  127. package/src/node-types/execute-plan.ts +130 -0
  128. package/src/node-types/execute-target.ts +267 -0
  129. package/src/node-types/fix-errors.ts +68 -0
  130. package/src/node-types/genesis-apply-retry.ts +138 -0
  131. package/src/node-types/genesis-apply.ts +96 -0
  132. package/src/node-types/genesis-approve.ts +73 -0
  133. package/src/node-types/genesis-check-stabilize.ts +37 -0
  134. package/src/node-types/genesis-check-threshold.ts +34 -0
  135. package/src/node-types/genesis-commit.ts +71 -0
  136. package/src/node-types/genesis-compile-validate.ts +77 -0
  137. package/src/node-types/genesis-diff-fingerprint.ts +67 -0
  138. package/src/node-types/genesis-diff-workflow.ts +71 -0
  139. package/src/node-types/genesis-escrow-grace.ts +62 -0
  140. package/src/node-types/genesis-escrow-migrate.ts +138 -0
  141. package/src/node-types/genesis-escrow-recover.ts +99 -0
  142. package/src/node-types/genesis-escrow-stage.ts +104 -0
  143. package/src/node-types/genesis-escrow-validate.ts +120 -0
  144. package/src/node-types/genesis-load-config.ts +44 -0
  145. package/src/node-types/genesis-observe.ts +119 -0
  146. package/src/node-types/genesis-propose.ts +97 -0
  147. package/src/node-types/genesis-report.ts +95 -0
  148. package/src/node-types/genesis-snapshot.ts +30 -0
  149. package/src/node-types/genesis-try-apply.ts +165 -0
  150. package/src/node-types/genesis-update-history.ts +72 -0
  151. package/src/node-types/genesis-validate-proposal.ts +124 -0
  152. package/src/node-types/git-ops.ts +72 -0
  153. package/src/node-types/index.ts +36 -0
  154. package/src/node-types/load-config.ts +27 -0
  155. package/src/node-types/plan-task.ts +77 -0
  156. package/src/node-types/read-workflow.ts +68 -0
  157. package/src/node-types/receive-task.ts +92 -0
  158. package/src/node-types/report.ts +25 -0
  159. package/src/node-types/resolve-target.ts +64 -0
  160. package/src/node-types/route-task.ts +25 -0
  161. package/src/node-types/send-notify.ts +75 -0
  162. package/src/node-types/validate-result.ts +49 -0
  163. package/src/templates/index.ts +5 -0
  164. package/src/templates/weaver-bot-template.ts +106 -0
  165. package/src/workflows/genesis-task.ts +91 -0
  166. package/src/workflows/index.ts +3 -0
  167. package/src/workflows/weaver-bot-batch.ts +65 -0
  168. package/src/workflows/weaver-bot.ts +79 -0
@@ -0,0 +1,324 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import type {
4
+ PipelineConfig,
5
+ PipelineResult,
6
+ PipelineStage,
7
+ StageCondition,
8
+ StageResult,
9
+ StageStatus,
10
+ WorkflowResult,
11
+ WeaverConfig,
12
+ ExecutionEvent,
13
+ } from './types.js';
14
+ import type { NotificationErrorHandler } from './notifications.js';
15
+ import { runWorkflow } from './runner.js';
16
+
17
+ export interface PipelineRunOptions {
18
+ verbose?: boolean;
19
+ dryRun?: boolean;
20
+ config?: WeaverConfig;
21
+ stage?: string;
22
+ onStageEvent?: (stageId: string, status: StageStatus, result?: WorkflowResult) => void;
23
+ onEvent?: (event: ExecutionEvent) => void;
24
+ onNotificationError?: NotificationErrorHandler;
25
+ }
26
+
27
+ export class PipelineRunner {
28
+ static load(configPath: string): PipelineConfig {
29
+ const absPath = path.resolve(configPath);
30
+ if (!fs.existsSync(absPath)) {
31
+ throw new Error(`Pipeline config not found: ${absPath}`);
32
+ }
33
+
34
+ const raw = JSON.parse(fs.readFileSync(absPath, 'utf-8')) as PipelineConfig;
35
+ const configDir = path.dirname(absPath);
36
+
37
+ // Resolve workflow paths relative to config file
38
+ for (const stage of raw.stages) {
39
+ if (!path.isAbsolute(stage.workflow)) {
40
+ stage.workflow = path.resolve(configDir, stage.workflow);
41
+ }
42
+ }
43
+
44
+ return raw;
45
+ }
46
+
47
+ async run(config: PipelineConfig, options?: PipelineRunOptions): Promise<PipelineResult> {
48
+ this.validate(config);
49
+
50
+ const stageMap = new Map(config.stages.map((s) => [s.id, s]));
51
+ const waves = this.topologicalWaves(config.stages);
52
+ const results: Record<string, StageResult> = {};
53
+ const stageOrder: string[] = [];
54
+ let aborted = false;
55
+ const pipelineStart = Date.now();
56
+
57
+ // If filtering to a single stage, compute transitive deps
58
+ const activeIds = options?.stage
59
+ ? this.transitiveDeps(options.stage, stageMap)
60
+ : null;
61
+
62
+ for (const wave of waves) {
63
+ const waveStages = activeIds
64
+ ? wave.filter((id) => activeIds.has(id))
65
+ : wave;
66
+
67
+ if (waveStages.length === 0) continue;
68
+
69
+ const promises = waveStages.map(async (stageId) => {
70
+ const stage = stageMap.get(stageId)!;
71
+ const condition = stage.condition ?? 'on-success';
72
+
73
+ if (aborted && condition !== 'always') {
74
+ const sr: StageResult = { id: stageId, status: 'cancelled', workflowResult: null, durationMs: 0, wave: waves.indexOf(wave) };
75
+ results[stageId] = sr;
76
+ stageOrder.push(stageId);
77
+ options?.onStageEvent?.(stageId, 'cancelled');
78
+ return;
79
+ }
80
+
81
+ if (!this.shouldRun(stage, results)) {
82
+ const sr: StageResult = { id: stageId, status: 'skipped', workflowResult: null, durationMs: 0, wave: waves.indexOf(wave) };
83
+ results[stageId] = sr;
84
+ stageOrder.push(stageId);
85
+ options?.onStageEvent?.(stageId, 'skipped');
86
+ return;
87
+ }
88
+
89
+ options?.onStageEvent?.(stageId, 'running');
90
+ const stageStart = Date.now();
91
+
92
+ // Merge params with upstream results
93
+ const params: Record<string, unknown> = {
94
+ ...(stage.params ?? {}),
95
+ __stages: this.buildStageContext(stage, results),
96
+ };
97
+
98
+ try {
99
+ const timeout = stage.timeoutSeconds ?? config.defaultTimeoutSeconds;
100
+ const workflowPromise = runWorkflow(stage.workflow, {
101
+ params,
102
+ verbose: options?.verbose,
103
+ dryRun: options?.dryRun,
104
+ config: config.config ?? options?.config,
105
+ onEvent: options?.onEvent,
106
+ onNotificationError: options?.onNotificationError,
107
+ });
108
+
109
+ let workflowResult: WorkflowResult;
110
+ if (timeout) {
111
+ workflowResult = await Promise.race([
112
+ workflowPromise,
113
+ new Promise<never>((_, reject) =>
114
+ setTimeout(() => reject(new Error(`Stage "${stageId}" timed out after ${timeout}s`)), timeout * 1000),
115
+ ),
116
+ ]);
117
+ } else {
118
+ workflowResult = await workflowPromise;
119
+ }
120
+
121
+ const status: StageStatus = workflowResult.success ? 'completed' : 'failed';
122
+ const sr: StageResult = {
123
+ id: stageId,
124
+ status,
125
+ workflowResult,
126
+ durationMs: Date.now() - stageStart,
127
+ wave: waves.indexOf(wave),
128
+ };
129
+ results[stageId] = sr;
130
+ stageOrder.push(stageId);
131
+ options?.onStageEvent?.(stageId, status, workflowResult);
132
+
133
+ if (!workflowResult.success && config.failFast !== false) {
134
+ aborted = true;
135
+ }
136
+ } catch (err: unknown) {
137
+ const msg = err instanceof Error ? err.message : String(err);
138
+ const sr: StageResult = {
139
+ id: stageId,
140
+ status: 'failed',
141
+ workflowResult: null,
142
+ durationMs: Date.now() - stageStart,
143
+ error: msg,
144
+ wave: waves.indexOf(wave),
145
+ };
146
+ results[stageId] = sr;
147
+ stageOrder.push(stageId);
148
+ options?.onStageEvent?.(stageId, 'failed');
149
+
150
+ if (config.failFast !== false) {
151
+ aborted = true;
152
+ }
153
+ }
154
+ });
155
+
156
+ await Promise.allSettled(promises);
157
+ }
158
+
159
+ const allResults = Object.values(results);
160
+ const anyFailed = allResults.some((r) => r.status === 'failed');
161
+ const anyCancelled = allResults.some((r) => r.status === 'cancelled');
162
+
163
+ return {
164
+ success: !anyFailed && !anyCancelled,
165
+ outcome: anyFailed ? 'failed' : anyCancelled ? 'cancelled' : 'completed',
166
+ durationMs: Date.now() - pipelineStart,
167
+ stages: results,
168
+ stageOrder,
169
+ };
170
+ }
171
+
172
+ private validate(config: PipelineConfig): void {
173
+ if (!config.stages || config.stages.length === 0) {
174
+ throw new Error('Pipeline must have at least one stage');
175
+ }
176
+
177
+ const ids = new Set<string>();
178
+ for (const stage of config.stages) {
179
+ if (ids.has(stage.id)) {
180
+ throw new Error(`Duplicate stage id: "${stage.id}"`);
181
+ }
182
+ ids.add(stage.id);
183
+ }
184
+
185
+ for (const stage of config.stages) {
186
+ for (const dep of stage.dependsOn ?? []) {
187
+ if (!ids.has(dep)) {
188
+ throw new Error(`Stage "${stage.id}" depends on unknown stage "${dep}"`);
189
+ }
190
+ }
191
+ }
192
+
193
+ this.detectCycle(config.stages);
194
+ }
195
+
196
+ private detectCycle(stages: PipelineStage[]): void {
197
+ const WHITE = 0, GRAY = 1, BLACK = 2;
198
+ const color = new Map<string, number>();
199
+ const parent = new Map<string, string>();
200
+ const adj = new Map<string, string[]>();
201
+
202
+ for (const s of stages) {
203
+ color.set(s.id, WHITE);
204
+ adj.set(s.id, s.dependsOn ?? []);
205
+ }
206
+
207
+ const dfs = (id: string): string | null => {
208
+ color.set(id, GRAY);
209
+ for (const dep of adj.get(id) ?? []) {
210
+ if (color.get(dep) === GRAY) {
211
+ // Reconstruct cycle
212
+ const cycle = [dep, id];
213
+ let cur = id;
214
+ while (parent.has(cur) && parent.get(cur) !== dep) {
215
+ cur = parent.get(cur)!;
216
+ cycle.push(cur);
217
+ }
218
+ return cycle.reverse().join(' -> ');
219
+ }
220
+ if (color.get(dep) === WHITE) {
221
+ parent.set(dep, id);
222
+ const result = dfs(dep);
223
+ if (result) return result;
224
+ }
225
+ }
226
+ color.set(id, BLACK);
227
+ return null;
228
+ };
229
+
230
+ for (const s of stages) {
231
+ if (color.get(s.id) === WHITE) {
232
+ const cycle = dfs(s.id);
233
+ if (cycle) {
234
+ throw new Error(`Circular dependency detected: ${cycle}`);
235
+ }
236
+ }
237
+ }
238
+ }
239
+
240
+ private topologicalWaves(stages: PipelineStage[]): string[][] {
241
+ const inDegree = new Map<string, number>();
242
+ const dependents = new Map<string, string[]>();
243
+
244
+ for (const s of stages) {
245
+ inDegree.set(s.id, (s.dependsOn ?? []).length);
246
+ for (const dep of s.dependsOn ?? []) {
247
+ const list = dependents.get(dep) ?? [];
248
+ list.push(s.id);
249
+ dependents.set(dep, list);
250
+ }
251
+ }
252
+
253
+ const waves: string[][] = [];
254
+ let remaining = stages.length;
255
+
256
+ while (remaining > 0) {
257
+ const wave: string[] = [];
258
+ for (const [id, deg] of inDegree) {
259
+ if (deg === 0) wave.push(id);
260
+ }
261
+
262
+ if (wave.length === 0) break; // should not happen after cycle check
263
+
264
+ for (const id of wave) {
265
+ inDegree.delete(id);
266
+ for (const dep of dependents.get(id) ?? []) {
267
+ inDegree.set(dep, (inDegree.get(dep) ?? 1) - 1);
268
+ }
269
+ }
270
+
271
+ waves.push(wave);
272
+ remaining -= wave.length;
273
+ }
274
+
275
+ return waves;
276
+ }
277
+
278
+ private transitiveDeps(stageId: string, stageMap: Map<string, PipelineStage>): Set<string> {
279
+ const result = new Set<string>();
280
+ const visit = (id: string) => {
281
+ if (result.has(id)) return;
282
+ result.add(id);
283
+ const stage = stageMap.get(id);
284
+ if (!stage) throw new Error(`Unknown stage: "${id}"`);
285
+ for (const dep of stage.dependsOn ?? []) {
286
+ visit(dep);
287
+ }
288
+ };
289
+ visit(stageId);
290
+ return result;
291
+ }
292
+
293
+ private shouldRun(stage: PipelineStage, results: Record<string, StageResult>): boolean {
294
+ const condition: StageCondition = stage.condition ?? 'on-success';
295
+ const deps = stage.dependsOn ?? [];
296
+
297
+ if (deps.length === 0) return true;
298
+
299
+ const allSucceeded = deps.every((d) => results[d]?.status === 'completed');
300
+ const someFailed = deps.some((d) => results[d]?.status === 'failed');
301
+
302
+ switch (condition) {
303
+ case 'on-success':
304
+ return allSucceeded;
305
+ case 'on-failure':
306
+ return someFailed;
307
+ case 'always':
308
+ return true;
309
+ default:
310
+ return allSucceeded;
311
+ }
312
+ }
313
+
314
+ private buildStageContext(
315
+ stage: PipelineStage,
316
+ results: Record<string, StageResult>,
317
+ ): Record<string, WorkflowResult | null> {
318
+ const ctx: Record<string, WorkflowResult | null> = {};
319
+ for (const dep of stage.dependsOn ?? []) {
320
+ ctx[dep] = results[dep]?.workflowResult ?? null;
321
+ }
322
+ return ctx;
323
+ }
324
+ }
@@ -0,0 +1,236 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
4
+ import type {
5
+ BotAgentProvider,
6
+ BotProviderConfig,
7
+ ProviderFactory,
8
+ ProviderFactoryConfig,
9
+ ProviderMetadata,
10
+ ProviderModule,
11
+ } from './types.js';
12
+
13
+ interface RegistryEntry {
14
+ factory: ProviderFactory;
15
+ metadata: ProviderMetadata;
16
+ }
17
+
18
+ export class ProviderRegistry {
19
+ private factories = new Map<string, RegistryEntry>();
20
+
21
+ register(name: string, factory: ProviderFactory, metadata: ProviderMetadata): void {
22
+ this.factories.set(name, { factory, metadata });
23
+ }
24
+
25
+ resolve(name: string): RegistryEntry | undefined {
26
+ return this.factories.get(name);
27
+ }
28
+
29
+ has(name: string): boolean {
30
+ return this.factories.has(name);
31
+ }
32
+
33
+ list(): Array<{ name: string; metadata: ProviderMetadata }> {
34
+ return Array.from(this.factories.entries()).map(([name, entry]) => ({
35
+ name,
36
+ metadata: entry.metadata,
37
+ }));
38
+ }
39
+ }
40
+
41
+ export const defaultRegistry = new ProviderRegistry();
42
+
43
+ defaultRegistry.register(
44
+ 'anthropic',
45
+ async (config) => {
46
+ const { AnthropicAgentProvider } = await import('./agent-provider.js');
47
+ return new AnthropicAgentProvider({ name: 'anthropic', model: config.model, maxTokens: config.maxTokens });
48
+ },
49
+ {
50
+ displayName: 'Anthropic API',
51
+ description: 'Direct Anthropic API calls via @anthropic-ai/sdk',
52
+ source: 'built-in',
53
+ requiredEnvVars: ['ANTHROPIC_API_KEY'],
54
+ },
55
+ );
56
+
57
+ defaultRegistry.register(
58
+ 'claude-cli',
59
+ async (config) => {
60
+ const { CliAgentProvider } = await import('./cli-provider.js');
61
+ return new CliAgentProvider('claude-cli', config.model);
62
+ },
63
+ {
64
+ displayName: 'Claude CLI',
65
+ description: 'Claude Code CLI (claude -p)',
66
+ source: 'built-in',
67
+ detectCliCommand: 'claude',
68
+ },
69
+ );
70
+
71
+ defaultRegistry.register(
72
+ 'copilot-cli',
73
+ async (config) => {
74
+ const { CliAgentProvider } = await import('./cli-provider.js');
75
+ return new CliAgentProvider('copilot-cli', config.model);
76
+ },
77
+ {
78
+ displayName: 'GitHub Copilot CLI',
79
+ description: 'GitHub Copilot CLI (copilot -p)',
80
+ source: 'built-in',
81
+ detectCliCommand: 'copilot',
82
+ },
83
+ );
84
+
85
+ export async function loadExternalProvider(
86
+ moduleSpec: string,
87
+ ): Promise<{ factory: ProviderFactory; metadata: ProviderMetadata }> {
88
+ let mod: Record<string, unknown>;
89
+
90
+ try {
91
+ if (moduleSpec.startsWith('.') || moduleSpec.startsWith('/')) {
92
+ const absPath = path.resolve(moduleSpec);
93
+ const fileUrl = pathToFileURL(absPath).href;
94
+ mod = await import(fileUrl);
95
+ } else {
96
+ mod = await import(moduleSpec);
97
+ }
98
+ } catch (err) {
99
+ const msg = err instanceof Error ? err.message : String(err);
100
+ const isLocal = moduleSpec.startsWith('.') || moduleSpec.startsWith('/');
101
+ const hint = isLocal
102
+ ? `Check the path exists: ${path.resolve(moduleSpec)}`
103
+ : `Install it with: npm install ${moduleSpec}`;
104
+ throw new Error(`Failed to load provider from "${moduleSpec}": ${msg}\n ${hint}`);
105
+ }
106
+
107
+ // Accept: default export object with createProvider, named createProvider, or default function
108
+ const defaultExport = (mod.default ?? mod) as Record<string, unknown>;
109
+ let factory: ProviderFactory;
110
+ let metadata: ProviderMetadata;
111
+
112
+ if (typeof defaultExport === 'function') {
113
+ factory = defaultExport as ProviderFactory;
114
+ metadata = { displayName: moduleSpec, source: moduleSpec.startsWith('.') || moduleSpec.startsWith('/') ? 'local' : 'npm' };
115
+ } else if (typeof defaultExport.createProvider === 'function') {
116
+ const providerModule = defaultExport as unknown as ProviderModule;
117
+ factory = providerModule.createProvider;
118
+ metadata = providerModule.metadata ?? {
119
+ displayName: moduleSpec,
120
+ source: moduleSpec.startsWith('.') || moduleSpec.startsWith('/') ? 'local' : 'npm',
121
+ };
122
+ } else if (typeof mod.createProvider === 'function') {
123
+ factory = mod.createProvider as ProviderFactory;
124
+ metadata = (mod.metadata as ProviderMetadata) ?? {
125
+ displayName: moduleSpec,
126
+ source: moduleSpec.startsWith('.') || moduleSpec.startsWith('/') ? 'local' : 'npm',
127
+ };
128
+ } else {
129
+ throw new Error(
130
+ `Provider module "${moduleSpec}" must export a createProvider function ` +
131
+ `(as default export, default.createProvider, or named export)`,
132
+ );
133
+ }
134
+
135
+ return { factory, metadata };
136
+ }
137
+
138
+ let discoveredProviders: Array<{ name: string; metadata: ProviderMetadata }> | null = null;
139
+
140
+ export async function discoverProviders(
141
+ registry: ProviderRegistry = defaultRegistry,
142
+ ): Promise<Array<{ name: string; metadata: ProviderMetadata }>> {
143
+ if (discoveredProviders) return discoveredProviders;
144
+
145
+ const discovered: Array<{ name: string; metadata: ProviderMetadata }> = [];
146
+ let dir = process.cwd();
147
+
148
+ for (let depth = 0; depth < 10; depth++) {
149
+ const nodeModules = path.join(dir, 'node_modules');
150
+ if (fs.existsSync(nodeModules)) {
151
+ await scanNodeModules(nodeModules, registry, discovered);
152
+ break;
153
+ }
154
+ const parent = path.dirname(dir);
155
+ if (parent === dir) break;
156
+ dir = parent;
157
+ }
158
+
159
+ discoveredProviders = discovered;
160
+ return discovered;
161
+ }
162
+
163
+ async function scanNodeModules(
164
+ nodeModulesDir: string,
165
+ registry: ProviderRegistry,
166
+ results: Array<{ name: string; metadata: ProviderMetadata }>,
167
+ ): Promise<void> {
168
+ let entries: fs.Dirent[];
169
+ try {
170
+ entries = fs.readdirSync(nodeModulesDir, { withFileTypes: true });
171
+ } catch {
172
+ return;
173
+ }
174
+
175
+ for (const entry of entries) {
176
+ if (!entry.isDirectory()) continue;
177
+
178
+ if (entry.name.startsWith('@')) {
179
+ // Scoped packages
180
+ const scopeDir = path.join(nodeModulesDir, entry.name);
181
+ let scopeEntries: fs.Dirent[];
182
+ try {
183
+ scopeEntries = fs.readdirSync(scopeDir, { withFileTypes: true });
184
+ } catch {
185
+ continue;
186
+ }
187
+ for (const scopeEntry of scopeEntries) {
188
+ if (!scopeEntry.isDirectory()) continue;
189
+ await checkProviderPackage(
190
+ path.join(scopeDir, scopeEntry.name),
191
+ `${entry.name}/${scopeEntry.name}`,
192
+ registry,
193
+ results,
194
+ );
195
+ }
196
+ } else {
197
+ await checkProviderPackage(
198
+ path.join(nodeModulesDir, entry.name),
199
+ entry.name,
200
+ registry,
201
+ results,
202
+ );
203
+ }
204
+ }
205
+ }
206
+
207
+ async function checkProviderPackage(
208
+ pkgDir: string,
209
+ pkgName: string,
210
+ registry: ProviderRegistry,
211
+ results: Array<{ name: string; metadata: ProviderMetadata }>,
212
+ ): Promise<void> {
213
+ const pkgJsonPath = path.join(pkgDir, 'package.json');
214
+ if (!fs.existsSync(pkgJsonPath)) return;
215
+
216
+ try {
217
+ const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
218
+ const keywords: string[] = pkgJson.keywords ?? [];
219
+ if (!keywords.includes('flowweaver-provider')) return;
220
+
221
+ // Derive provider name
222
+ const providerName: string =
223
+ pkgJson.flowWeaver?.providerName ??
224
+ pkgName
225
+ .replace(/^@[^/]+\//, '')
226
+ .replace(/^flowweaver-provider-/, '');
227
+
228
+ if (registry.has(providerName)) return;
229
+
230
+ const { factory, metadata } = await loadExternalProvider(pkgName);
231
+ registry.register(providerName, factory, metadata);
232
+ results.push({ name: providerName, metadata });
233
+ } catch {
234
+ // Skip packages that fail to load
235
+ }
236
+ }