@synergenius/flow-weaver-pack-weaver 0.9.0 → 0.9.4
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/bot/ai-client.d.ts +22 -2
- package/dist/bot/ai-client.d.ts.map +1 -1
- package/dist/bot/ai-client.js +168 -20
- package/dist/bot/ai-client.js.map +1 -1
- package/dist/bot/ansi.d.ts +13 -0
- package/dist/bot/ansi.d.ts.map +1 -0
- package/dist/bot/ansi.js +13 -0
- package/dist/bot/ansi.js.map +1 -0
- package/dist/bot/assistant-core.d.ts +25 -0
- package/dist/bot/assistant-core.d.ts.map +1 -0
- package/dist/bot/assistant-core.js +272 -0
- package/dist/bot/assistant-core.js.map +1 -0
- package/dist/bot/assistant-tools.d.ts +10 -0
- package/dist/bot/assistant-tools.d.ts.map +1 -0
- package/dist/bot/assistant-tools.js +324 -0
- package/dist/bot/assistant-tools.js.map +1 -0
- package/dist/bot/audit-logger.d.ts.map +1 -1
- package/dist/bot/audit-logger.js +9 -5
- package/dist/bot/audit-logger.js.map +1 -1
- package/dist/bot/bot-manager.d.ts +49 -0
- package/dist/bot/bot-manager.d.ts.map +1 -0
- package/dist/bot/bot-manager.js +279 -0
- package/dist/bot/bot-manager.js.map +1 -0
- package/dist/bot/child-process-tracker.d.ts +6 -0
- package/dist/bot/child-process-tracker.d.ts.map +1 -0
- package/dist/bot/child-process-tracker.js +35 -0
- package/dist/bot/child-process-tracker.js.map +1 -0
- package/dist/bot/cli-provider.d.ts.map +1 -1
- package/dist/bot/cli-provider.js +13 -8
- package/dist/bot/cli-provider.js.map +1 -1
- package/dist/bot/conversation-store.d.ts +40 -0
- package/dist/bot/conversation-store.d.ts.map +1 -0
- package/dist/bot/conversation-store.js +182 -0
- package/dist/bot/conversation-store.js.map +1 -0
- package/dist/bot/error-classifier.d.ts +27 -0
- package/dist/bot/error-classifier.d.ts.map +1 -0
- package/dist/bot/error-classifier.js +71 -0
- package/dist/bot/error-classifier.js.map +1 -0
- package/dist/bot/error-guide.d.ts +5 -0
- package/dist/bot/error-guide.d.ts.map +1 -0
- package/dist/bot/error-guide.js +5 -0
- package/dist/bot/error-guide.js.map +1 -0
- package/dist/bot/knowledge-store.d.ts +17 -0
- package/dist/bot/knowledge-store.d.ts.map +1 -0
- package/dist/bot/knowledge-store.js +53 -0
- package/dist/bot/knowledge-store.js.map +1 -0
- package/dist/bot/paths.d.ts +11 -0
- package/dist/bot/paths.d.ts.map +1 -0
- package/dist/bot/paths.js +26 -0
- package/dist/bot/paths.js.map +1 -0
- package/dist/bot/retry-utils.d.ts +5 -0
- package/dist/bot/retry-utils.d.ts.map +1 -0
- package/dist/bot/retry-utils.js +5 -0
- package/dist/bot/retry-utils.js.map +1 -0
- package/dist/bot/runner.d.ts.map +1 -1
- package/dist/bot/runner.js +12 -1
- package/dist/bot/runner.js.map +1 -1
- package/dist/bot/safety.d.ts +10 -0
- package/dist/bot/safety.d.ts.map +1 -0
- package/dist/bot/safety.js +14 -0
- package/dist/bot/safety.js.map +1 -0
- package/dist/bot/session-state.d.ts.map +1 -1
- package/dist/bot/session-state.js +3 -1
- package/dist/bot/session-state.js.map +1 -1
- package/dist/bot/steering.js +2 -2
- package/dist/bot/steering.js.map +1 -1
- package/dist/bot/step-executor.d.ts +10 -5
- package/dist/bot/step-executor.d.ts.map +1 -1
- package/dist/bot/step-executor.js +252 -3
- package/dist/bot/step-executor.js.map +1 -1
- package/dist/bot/system-prompt.d.ts +1 -1
- package/dist/bot/system-prompt.d.ts.map +1 -1
- package/dist/bot/system-prompt.js +69 -43
- package/dist/bot/system-prompt.js.map +1 -1
- package/dist/bot/task-decomposer.d.ts +24 -0
- package/dist/bot/task-decomposer.d.ts.map +1 -0
- package/dist/bot/task-decomposer.js +75 -0
- package/dist/bot/task-decomposer.js.map +1 -0
- package/dist/bot/task-queue.d.ts +17 -4
- package/dist/bot/task-queue.d.ts.map +1 -1
- package/dist/bot/task-queue.js +83 -5
- package/dist/bot/task-queue.js.map +1 -1
- package/dist/bot/terminal-renderer.d.ts +60 -0
- package/dist/bot/terminal-renderer.d.ts.map +1 -0
- package/dist/bot/terminal-renderer.js +204 -0
- package/dist/bot/terminal-renderer.js.map +1 -0
- package/dist/bot/tool-registry.d.ts +24 -0
- package/dist/bot/tool-registry.d.ts.map +1 -0
- package/dist/bot/tool-registry.js +458 -0
- package/dist/bot/tool-registry.js.map +1 -0
- package/dist/bot/types.d.ts +7 -0
- package/dist/bot/types.d.ts.map +1 -1
- package/dist/bot/weaver-tools.d.ts +18 -0
- package/dist/bot/weaver-tools.d.ts.map +1 -0
- package/dist/bot/weaver-tools.js +124 -0
- package/dist/bot/weaver-tools.js.map +1 -0
- package/dist/cli-bridge.d.ts.map +1 -1
- package/dist/cli-bridge.js +5 -1
- package/dist/cli-bridge.js.map +1 -1
- package/dist/cli-handlers.d.ts +13 -1
- package/dist/cli-handlers.d.ts.map +1 -1
- package/dist/cli-handlers.js +615 -48
- package/dist/cli-handlers.js.map +1 -1
- package/dist/mcp-tools.js +2 -2
- package/dist/mcp-tools.js.map +1 -1
- package/dist/node-types/abort-task.d.ts.map +1 -1
- package/dist/node-types/abort-task.js +4 -3
- package/dist/node-types/abort-task.js.map +1 -1
- package/dist/node-types/agent-execute.d.ts +38 -0
- package/dist/node-types/agent-execute.d.ts.map +1 -0
- package/dist/node-types/agent-execute.js +252 -0
- package/dist/node-types/agent-execute.js.map +1 -0
- package/dist/node-types/bot-report.d.ts +5 -3
- package/dist/node-types/bot-report.d.ts.map +1 -1
- package/dist/node-types/bot-report.js +39 -7
- package/dist/node-types/bot-report.js.map +1 -1
- package/dist/node-types/build-context.d.ts +3 -3
- package/dist/node-types/build-context.d.ts.map +1 -1
- package/dist/node-types/build-context.js +108 -24
- package/dist/node-types/build-context.js.map +1 -1
- package/dist/node-types/detect-provider.d.ts +2 -2
- package/dist/node-types/detect-provider.d.ts.map +1 -1
- package/dist/node-types/detect-provider.js +3 -1
- package/dist/node-types/detect-provider.js.map +1 -1
- package/dist/node-types/exec-validate-retry.d.ts.map +1 -1
- package/dist/node-types/exec-validate-retry.js +43 -6
- package/dist/node-types/exec-validate-retry.js.map +1 -1
- package/dist/node-types/execute-plan.d.ts.map +1 -1
- package/dist/node-types/execute-plan.js +31 -8
- package/dist/node-types/execute-plan.js.map +1 -1
- package/dist/node-types/execute-target.d.ts.map +1 -1
- package/dist/node-types/execute-target.js +3 -1
- package/dist/node-types/execute-target.js.map +1 -1
- package/dist/node-types/fix-errors.d.ts.map +1 -1
- package/dist/node-types/fix-errors.js +21 -5
- package/dist/node-types/fix-errors.js.map +1 -1
- package/dist/node-types/genesis-observe.d.ts.map +1 -1
- package/dist/node-types/genesis-observe.js +3 -1
- package/dist/node-types/genesis-observe.js.map +1 -1
- package/dist/node-types/genesis-report.js +4 -1
- package/dist/node-types/genesis-report.js.map +1 -1
- package/dist/node-types/git-ops.d.ts.map +1 -1
- package/dist/node-types/git-ops.js +98 -4
- package/dist/node-types/git-ops.js.map +1 -1
- package/dist/node-types/index.d.ts +2 -0
- package/dist/node-types/index.d.ts.map +1 -1
- package/dist/node-types/index.js +2 -0
- package/dist/node-types/index.js.map +1 -1
- package/dist/node-types/load-config.d.ts +2 -2
- package/dist/node-types/load-config.d.ts.map +1 -1
- package/dist/node-types/load-config.js.map +1 -1
- package/dist/node-types/plan-task.d.ts.map +1 -1
- package/dist/node-types/plan-task.js +14 -2
- package/dist/node-types/plan-task.js.map +1 -1
- package/dist/node-types/read-workflow.js +8 -2
- package/dist/node-types/read-workflow.js.map +1 -1
- package/dist/node-types/receive-task.d.ts.map +1 -1
- package/dist/node-types/receive-task.js +35 -26
- package/dist/node-types/receive-task.js.map +1 -1
- package/dist/node-types/send-notify.js +2 -1
- package/dist/node-types/send-notify.js.map +1 -1
- package/dist/node-types/validate-gate.d.ts +18 -0
- package/dist/node-types/validate-gate.d.ts.map +1 -0
- package/dist/node-types/validate-gate.js +96 -0
- package/dist/node-types/validate-gate.js.map +1 -0
- package/dist/workflows/genesis-task.d.ts +20 -12
- package/dist/workflows/genesis-task.d.ts.map +1 -1
- package/dist/workflows/genesis-task.js +20 -12
- package/dist/workflows/genesis-task.js.map +1 -1
- package/dist/workflows/weaver-agent.d.ts +35 -0
- package/dist/workflows/weaver-agent.d.ts.map +1 -0
- package/dist/workflows/weaver-agent.js +777 -0
- package/dist/workflows/weaver-agent.js.map +1 -0
- package/dist/workflows/weaver-bot-batch.d.ts +19 -26
- package/dist/workflows/weaver-bot-batch.d.ts.map +1 -1
- package/dist/workflows/weaver-bot-batch.js +1043 -27
- package/dist/workflows/weaver-bot-batch.js.map +1 -1
- package/dist/workflows/weaver-bot.d.ts +21 -35
- package/dist/workflows/weaver-bot.d.ts.map +1 -1
- package/dist/workflows/weaver-bot.js +1119 -36
- package/dist/workflows/weaver-bot.js.map +1 -1
- package/flowweaver.manifest.json +21 -1
- package/package.json +5 -2
- package/src/bot/ai-client.ts +180 -19
- package/src/bot/ansi.ts +12 -0
- package/src/bot/assistant-core.ts +312 -0
- package/src/bot/assistant-tools.ts +318 -0
- package/src/bot/audit-logger.ts +6 -5
- package/src/bot/bot-manager.ts +293 -0
- package/src/bot/child-process-tracker.ts +40 -0
- package/src/bot/cli-provider.ts +13 -8
- package/src/bot/conversation-store.ts +222 -0
- package/src/bot/error-classifier.ts +90 -0
- package/src/bot/error-guide.ts +4 -0
- package/src/bot/knowledge-store.ts +59 -0
- package/src/bot/paths.ts +27 -0
- package/src/bot/retry-utils.ts +4 -0
- package/src/bot/runner.ts +12 -1
- package/src/bot/safety.ts +16 -0
- package/src/bot/session-state.ts +2 -1
- package/src/bot/steering.ts +2 -2
- package/src/bot/step-executor.ts +313 -5
- package/src/bot/system-prompt.ts +70 -47
- package/src/bot/task-decomposer.ts +100 -0
- package/src/bot/task-queue.ts +100 -8
- package/src/bot/terminal-renderer.ts +238 -0
- package/src/bot/tool-registry.ts +477 -0
- package/src/bot/types.ts +8 -0
- package/src/bot/weaver-tools.ts +134 -0
- package/src/cli-bridge.ts +7 -1
- package/src/cli-handlers.ts +624 -48
- package/src/mcp-tools.ts +2 -2
- package/src/node-types/abort-task.ts +5 -4
- package/src/node-types/agent-execute.ts +303 -0
- package/src/node-types/bot-report.ts +40 -9
- package/src/node-types/build-context.ts +112 -25
- package/src/node-types/detect-provider.ts +4 -3
- package/src/node-types/exec-validate-retry.ts +47 -8
- package/src/node-types/execute-plan.ts +32 -8
- package/src/node-types/execute-target.ts +2 -1
- package/src/node-types/fix-errors.ts +20 -5
- package/src/node-types/genesis-observe.ts +2 -1
- package/src/node-types/genesis-report.ts +1 -1
- package/src/node-types/git-ops.ts +93 -4
- package/src/node-types/index.ts +2 -0
- package/src/node-types/load-config.ts +3 -3
- package/src/node-types/plan-task.ts +15 -3
- package/src/node-types/read-workflow.ts +2 -2
- package/src/node-types/receive-task.ts +31 -26
- package/src/node-types/send-notify.ts +1 -1
- package/src/node-types/validate-gate.ts +112 -0
- package/src/workflows/genesis-task.ts +20 -12
- package/src/workflows/weaver-agent.ts +799 -0
- package/src/workflows/weaver-bot-batch.ts +1049 -27
- package/src/workflows/weaver-bot.ts +1123 -36
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified error classification — merges error-guide.ts and retry-utils.ts
|
|
3
|
+
* into a single source of truth for error handling.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface ErrorClassification {
|
|
7
|
+
isTransient: boolean;
|
|
8
|
+
guidance: string | null;
|
|
9
|
+
category: 'auth' | 'network' | 'rate-limit' | 'timeout' | 'parse' | 'system' | 'unknown';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const PATTERNS: Array<{ pattern: RegExp; isTransient: boolean; guidance: string; category: ErrorClassification['category'] }> = [
|
|
13
|
+
{ pattern: /ETIMEDOUT/i, isTransient: true, guidance: 'Network timeout. Check internet or increase timeout.', category: 'timeout' },
|
|
14
|
+
{ pattern: /ECONNRESET/i, isTransient: true, guidance: 'Connection reset. Retry in a few seconds.', category: 'network' },
|
|
15
|
+
{ pattern: /ECONNREFUSED/i, isTransient: true, guidance: 'Connection refused. Is the service running?', category: 'network' },
|
|
16
|
+
{ pattern: /EPIPE|ENOTFOUND/i, isTransient: true, guidance: 'Network error.', category: 'network' },
|
|
17
|
+
{ pattern: /401|authentication|invalid.*key/i, isTransient: false, guidance: 'Authentication failed. Check API key or run "weaver init".', category: 'auth' },
|
|
18
|
+
{ pattern: /403|forbidden/i, isTransient: false, guidance: 'Access denied. API key may lack permissions.', category: 'auth' },
|
|
19
|
+
{ pattern: /429|rate.?limit|too many requests/i, isTransient: true, guidance: 'Rate limited. Wait a few minutes or reduce --parallel.', category: 'rate-limit' },
|
|
20
|
+
{ pattern: /502|bad gateway/i, isTransient: true, guidance: 'Server error (502). Will auto-retry.', category: 'network' },
|
|
21
|
+
{ pattern: /503|service unavailable|overloaded/i, isTransient: true, guidance: 'Service overloaded. Will auto-retry.', category: 'network' },
|
|
22
|
+
{ pattern: /504|gateway timeout/i, isTransient: true, guidance: 'Gateway timeout (504). Will auto-retry.', category: 'timeout' },
|
|
23
|
+
{ pattern: /exit(?:ed with)? code 143/i, isTransient: true, guidance: 'Process killed (SIGTERM). Likely timeout or Ctrl+C.', category: 'timeout' },
|
|
24
|
+
{ pattern: /exit code 137/i, isTransient: false, guidance: 'Process killed (OOM). System may be low on memory.', category: 'system' },
|
|
25
|
+
{ pattern: /ENOMEM/i, isTransient: false, guidance: 'Out of memory.', category: 'system' },
|
|
26
|
+
{ pattern: /ENOSPC/i, isTransient: false, guidance: 'Disk full.', category: 'system' },
|
|
27
|
+
{ pattern: /lock.*retries|failed to acquire.*lock/i, isTransient: true, guidance: 'File lock contention. Another weaver process may be running.', category: 'system' },
|
|
28
|
+
{ pattern: /not a workflow|No @flowWeaver/i, isTransient: false, guidance: 'Not a Flow Weaver workflow. Ensure @flowWeaver annotations exist.', category: 'parse' },
|
|
29
|
+
{ pattern: /parse.*json|unexpected token/i, isTransient: false, guidance: 'JSON parse error. AI may have returned malformed output.', category: 'parse' },
|
|
30
|
+
{ pattern: /Queue full/i, isTransient: false, guidance: 'Too many pending tasks (200 max). Process or clear first.', category: 'system' },
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Classify an error into transient/permanent with guidance and category.
|
|
35
|
+
*/
|
|
36
|
+
export function classifyError(err: unknown): ErrorClassification {
|
|
37
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
38
|
+
// Also check Node.js error codes
|
|
39
|
+
const code = err instanceof Error && 'code' in err ? (err as NodeJS.ErrnoException).code : undefined;
|
|
40
|
+
const fullMsg = code ? `${msg} ${code}` : msg;
|
|
41
|
+
|
|
42
|
+
for (const p of PATTERNS) {
|
|
43
|
+
if (p.pattern.test(fullMsg)) {
|
|
44
|
+
return { isTransient: p.isTransient, guidance: p.guidance, category: p.category };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return { isTransient: false, guidance: null, category: 'unknown' };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Convenience: check if an error is transient (retriable). */
|
|
51
|
+
export function isTransientError(err: unknown): boolean {
|
|
52
|
+
return classifyError(err).isTransient;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Convenience: get actionable guidance for an error message. */
|
|
56
|
+
export function getErrorGuidance(msg: string): string | null {
|
|
57
|
+
return classifyError(new Error(msg)).guidance;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Run a function with exponential backoff retry on transient errors.
|
|
62
|
+
*/
|
|
63
|
+
export async function withRetry<T>(
|
|
64
|
+
fn: () => Promise<T>,
|
|
65
|
+
options?: {
|
|
66
|
+
maxRetries?: number;
|
|
67
|
+
baseDelayMs?: number;
|
|
68
|
+
multiplier?: number;
|
|
69
|
+
onRetry?: (attempt: number, delay: number, err: Error) => void;
|
|
70
|
+
},
|
|
71
|
+
): Promise<T> {
|
|
72
|
+
const maxRetries = options?.maxRetries ?? 3;
|
|
73
|
+
const baseDelay = options?.baseDelayMs ?? 5_000;
|
|
74
|
+
const multiplier = options?.multiplier ?? 3;
|
|
75
|
+
|
|
76
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
77
|
+
try {
|
|
78
|
+
return await fn();
|
|
79
|
+
} catch (err: unknown) {
|
|
80
|
+
const isLast = attempt >= maxRetries;
|
|
81
|
+
if (isLast || !isTransientError(err)) throw err;
|
|
82
|
+
const delay = baseDelay * Math.pow(multiplier, attempt);
|
|
83
|
+
options?.onRetry?.(attempt + 1, delay, err instanceof Error ? err : new Error(String(err)));
|
|
84
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Unreachable, but TypeScript needs it
|
|
89
|
+
throw new Error('withRetry: exhausted retries');
|
|
90
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import * as os from 'node:os';
|
|
4
|
+
import * as crypto from 'node:crypto';
|
|
5
|
+
|
|
6
|
+
export interface KnowledgeEntry {
|
|
7
|
+
key: string;
|
|
8
|
+
value: string;
|
|
9
|
+
source: string;
|
|
10
|
+
createdAt: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class KnowledgeStore {
|
|
14
|
+
private filePath: string;
|
|
15
|
+
|
|
16
|
+
constructor(projectDir?: string) {
|
|
17
|
+
const dir = projectDir
|
|
18
|
+
? path.join(os.homedir(), '.weaver', 'projects', crypto.createHash('sha256').update(projectDir).digest('hex').slice(0, 8))
|
|
19
|
+
: path.join(os.homedir(), '.weaver');
|
|
20
|
+
this.filePath = path.join(dir, 'knowledge.ndjson');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
learn(key: string, value: string, source: string): void {
|
|
24
|
+
// Append entry to NDJSON file. If key exists, update it.
|
|
25
|
+
const entries = this.readAll().filter(e => e.key !== key);
|
|
26
|
+
entries.push({ key, value, source, createdAt: Date.now() });
|
|
27
|
+
this.writeAll(entries);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
recall(query: string): KnowledgeEntry[] {
|
|
31
|
+
// Fuzzy match: return entries whose key contains the query (case-insensitive)
|
|
32
|
+
const lower = query.toLowerCase();
|
|
33
|
+
return this.readAll().filter(e => e.key.toLowerCase().includes(lower) || e.value.toLowerCase().includes(lower));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
forget(key: string): void {
|
|
37
|
+
const entries = this.readAll().filter(e => e.key !== key);
|
|
38
|
+
this.writeAll(entries);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
list(): KnowledgeEntry[] {
|
|
42
|
+
return this.readAll();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private readAll(): KnowledgeEntry[] {
|
|
46
|
+
if (!fs.existsSync(this.filePath)) return [];
|
|
47
|
+
const content = fs.readFileSync(this.filePath, 'utf-8').trim();
|
|
48
|
+
if (!content) return [];
|
|
49
|
+
return content.split('\n').filter(Boolean).map(line => {
|
|
50
|
+
try { return JSON.parse(line); } catch { return null; }
|
|
51
|
+
}).filter(Boolean) as KnowledgeEntry[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private writeAll(entries: KnowledgeEntry[]): void {
|
|
55
|
+
const dir = path.dirname(this.filePath);
|
|
56
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
57
|
+
fs.writeFileSync(this.filePath, entries.map(e => JSON.stringify(e)).join('\n') + (entries.length > 0 ? '\n' : ''));
|
|
58
|
+
}
|
|
59
|
+
}
|
package/src/bot/paths.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared path resolution — single source of truth for .weaver directory layout.
|
|
3
|
+
*/
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import * as os from 'node:os';
|
|
6
|
+
import * as crypto from 'node:crypto';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Hash a directory path into a short filesystem-safe string.
|
|
10
|
+
* Used for per-project isolation under ~/.weaver/projects/.
|
|
11
|
+
*/
|
|
12
|
+
export function hashDir(dir: string): string {
|
|
13
|
+
return crypto.createHash('sha256').update(dir).digest('hex').slice(0, 8);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Resolve the weaver working directory.
|
|
18
|
+
* Priority: explicit > WEAVER_QUEUE_DIR > WEAVER_STEERING_DIR > project-scoped > global fallback.
|
|
19
|
+
*/
|
|
20
|
+
export function resolveWeaverDir(explicit?: string): string {
|
|
21
|
+
return explicit
|
|
22
|
+
?? process.env.WEAVER_QUEUE_DIR
|
|
23
|
+
?? process.env.WEAVER_STEERING_DIR
|
|
24
|
+
?? (process.env.WEAVER_PROJECT_DIR
|
|
25
|
+
? path.join(os.homedir(), '.weaver', 'projects', hashDir(process.env.WEAVER_PROJECT_DIR))
|
|
26
|
+
: path.join(os.homedir(), '.weaver'));
|
|
27
|
+
}
|
package/src/bot/runner.ts
CHANGED
|
@@ -224,6 +224,16 @@ export async function runWorkflow(
|
|
|
224
224
|
const summary = buildSummary(result);
|
|
225
225
|
const outcome = success ? 'completed' : 'failed';
|
|
226
226
|
|
|
227
|
+
// Extract stepLog from WeaverContext if available
|
|
228
|
+
let stepLog: import('./types.js').StepLogEntry[] | undefined;
|
|
229
|
+
try {
|
|
230
|
+
const ctxStr = result?.ctx as string | undefined;
|
|
231
|
+
if (ctxStr) {
|
|
232
|
+
const ctx = JSON.parse(ctxStr);
|
|
233
|
+
if (ctx.stepLogJson) stepLog = JSON.parse(ctx.stepLogJson);
|
|
234
|
+
}
|
|
235
|
+
} catch { /* stepLog extraction is best-effort */ }
|
|
236
|
+
|
|
227
237
|
await notifier({
|
|
228
238
|
type: 'workflow-complete',
|
|
229
239
|
workflowFile: absPath,
|
|
@@ -237,7 +247,7 @@ export async function runWorkflow(
|
|
|
237
247
|
recordRun(store, {
|
|
238
248
|
id: runId, workflowFile: absPath, startedAt, success, outcome: outcome as RunOutcome, summary,
|
|
239
249
|
functionName: execResult.functionName, executionTime: execResult.executionTime,
|
|
240
|
-
dryRun: false, provider: providerConfig.name, params: options?.params,
|
|
250
|
+
dryRun: false, provider: providerConfig.name, params: options?.params, stepLog,
|
|
241
251
|
}, verbose);
|
|
242
252
|
|
|
243
253
|
auditEmit('run-complete', { success, outcome, summary });
|
|
@@ -282,6 +292,7 @@ function recordRun(
|
|
|
282
292
|
outcome: RunOutcome; summary: string; functionName?: string;
|
|
283
293
|
executionTime?: number; dryRun: boolean; provider?: string;
|
|
284
294
|
params?: Record<string, unknown>;
|
|
295
|
+
stepLog?: import('./types.js').StepLogEntry[];
|
|
285
296
|
},
|
|
286
297
|
verbose: boolean,
|
|
287
298
|
): void {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared safety constants and checks — single source of truth.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const BLOCKED_COMMANDS = ['rm -rf', 'git push', 'npm publish', 'sudo', 'curl|sh', 'wget|sh'];
|
|
6
|
+
export const BLOCKED_URL_PATTERN = /localhost|127\.0\.0\.1|0\.0\.0\.0|10\.\d|172\.(1[6-9]|2\d|3[01])\.|192\.168\./i;
|
|
7
|
+
export const MAX_READ_SIZE = 1_048_576;
|
|
8
|
+
export const CHARS_PER_TOKEN = 4;
|
|
9
|
+
|
|
10
|
+
export function isBlockedCommand(cmd: string): boolean {
|
|
11
|
+
return BLOCKED_COMMANDS.some(b => cmd.includes(b));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function isBlockedUrl(url: string): boolean {
|
|
15
|
+
return BLOCKED_URL_PATTERN.test(url);
|
|
16
|
+
}
|
package/src/bot/session-state.ts
CHANGED
|
@@ -40,7 +40,8 @@ export class SessionStore {
|
|
|
40
40
|
try {
|
|
41
41
|
if (!fs.existsSync(this.filePath)) return null;
|
|
42
42
|
return JSON.parse(fs.readFileSync(this.filePath, 'utf-8')) as SessionState;
|
|
43
|
-
} catch {
|
|
43
|
+
} catch (err) {
|
|
44
|
+
if (process.env.WEAVER_VERBOSE) process.stderr.write(`[weaver] session state load failed: ${err}\n`);
|
|
44
45
|
return null;
|
|
45
46
|
}
|
|
46
47
|
}
|
package/src/bot/steering.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
|
-
import * as os from 'node:os';
|
|
4
3
|
import { withFileLock } from './file-lock.js';
|
|
4
|
+
import { resolveWeaverDir } from './paths.js';
|
|
5
5
|
|
|
6
6
|
export interface SteeringCommand {
|
|
7
7
|
command: 'pause' | 'resume' | 'cancel' | 'redirect' | 'queue';
|
|
@@ -13,7 +13,7 @@ export class SteeringController {
|
|
|
13
13
|
private controlPath: string;
|
|
14
14
|
|
|
15
15
|
constructor(controlDir?: string) {
|
|
16
|
-
const dir = controlDir ??
|
|
16
|
+
const dir = controlDir ?? resolveWeaverDir();
|
|
17
17
|
this.controlPath = path.join(dir, 'control.json');
|
|
18
18
|
}
|
|
19
19
|
|
package/src/bot/step-executor.ts
CHANGED
|
@@ -1,27 +1,335 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
|
+
import { execSync } from 'node:child_process';
|
|
3
4
|
import { runCommand } from '@synergenius/flow-weaver';
|
|
4
5
|
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Safety thresholds
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
/** Refuse writes that shrink an existing file by more than this ratio (0-1). */
|
|
11
|
+
const MAX_SHRINK_RATIO = 0.5;
|
|
12
|
+
|
|
13
|
+
/** Minimum file size (bytes) before shrink guard kicks in. */
|
|
14
|
+
const SHRINK_GUARD_MIN_SIZE = 100;
|
|
15
|
+
|
|
16
|
+
/** Maximum number of files that can be written in a single plan. */
|
|
17
|
+
const MAX_FILES_PER_PLAN = 50;
|
|
18
|
+
|
|
19
|
+
/** Maximum shell command execution time (ms). */
|
|
20
|
+
const SHELL_TIMEOUT = 60_000;
|
|
21
|
+
|
|
22
|
+
/** Maximum file size for read/patch operations (1MB). */
|
|
23
|
+
const MAX_READ_SIZE = 1_048_576;
|
|
24
|
+
|
|
25
|
+
/** Maximum files returned by list-files. */
|
|
26
|
+
const MAX_LIST_FILES = 1000;
|
|
27
|
+
|
|
28
|
+
/** Shell commands that are NEVER allowed (destructive operations). */
|
|
29
|
+
const BLOCKED_SHELL_PATTERNS = [
|
|
30
|
+
/\brm\s+(-[a-z]*r|-[a-z]*f)[a-z]*\s/i, // rm with -r or -f flags
|
|
31
|
+
/\bgit\s+push\b/i, // git push (no remote ops)
|
|
32
|
+
/\bgit\s+reset\s+--hard\b/i, // git reset --hard
|
|
33
|
+
/\bnpm\s+publish\b/i, // npm publish
|
|
34
|
+
/\bcurl\b.*\|\s*(sh|bash)\b/i, // curl | sh/bash
|
|
35
|
+
/\bwget\b.*\|\s*(sh|bash)\b/i, // wget | sh/bash
|
|
36
|
+
/\bsudo\b/i, // sudo
|
|
37
|
+
/\bchmod\s+777\b/i, // chmod 777
|
|
38
|
+
/\bkill\s+-9\b/i, // kill -9
|
|
39
|
+
/\bmkfs\b/i, // format disk
|
|
40
|
+
/\bdd\s+if=/i, // dd (disk destroyer)
|
|
41
|
+
/>\s*\/dev\/sd/i, // write to raw disk
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
/** Track files written in this process to enforce the per-plan cap. */
|
|
45
|
+
let filesWrittenThisPlan = 0;
|
|
46
|
+
|
|
47
|
+
/** Reset the per-plan counter between plans. */
|
|
48
|
+
export function resetPlanFileCounter(): void {
|
|
49
|
+
filesWrittenThisPlan = 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// Path safety
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
function assertSafePath(filePath: string, projectDir: string): void {
|
|
57
|
+
const resolved = path.resolve(projectDir, filePath);
|
|
58
|
+
if (!resolved.startsWith(path.resolve(projectDir))) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
`Path traversal blocked: "${filePath}" resolves outside project directory.`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// Write safety
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
interface WriteGuardResult {
|
|
70
|
+
allowed: boolean;
|
|
71
|
+
reason?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function checkWriteSafety(filePath: string, content: string): WriteGuardResult {
|
|
75
|
+
// Guard 1: Empty content
|
|
76
|
+
if (!content || content.trim().length === 0) {
|
|
77
|
+
return {
|
|
78
|
+
allowed: false,
|
|
79
|
+
reason:
|
|
80
|
+
`Refusing to write empty content to ${path.basename(filePath)}. ` +
|
|
81
|
+
`Use read-file first, then write the complete modified file back.`,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Guard 2: Per-plan file cap
|
|
86
|
+
if (filesWrittenThisPlan >= MAX_FILES_PER_PLAN) {
|
|
87
|
+
return {
|
|
88
|
+
allowed: false,
|
|
89
|
+
reason: `File write limit reached (${MAX_FILES_PER_PLAN} files per plan).`,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Guard 3: Shrink detection
|
|
94
|
+
if (fs.existsSync(filePath)) {
|
|
95
|
+
const existingSize = fs.statSync(filePath).size;
|
|
96
|
+
const newSize = Buffer.byteLength(content, 'utf-8');
|
|
97
|
+
if (existingSize > SHRINK_GUARD_MIN_SIZE && newSize < existingSize * MAX_SHRINK_RATIO) {
|
|
98
|
+
const shrinkPct = Math.round((1 - newSize / existingSize) * 100);
|
|
99
|
+
return {
|
|
100
|
+
allowed: false,
|
|
101
|
+
reason:
|
|
102
|
+
`Refusing to write ${path.basename(filePath)}: new content (${newSize}B) ` +
|
|
103
|
+
`is ${shrinkPct}% smaller than existing (${existingSize}B). ` +
|
|
104
|
+
`Use read-file first, make targeted changes, write complete file back.`,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return { allowed: true };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
// Shell safety
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
function checkShellSafety(command: string): WriteGuardResult {
|
|
117
|
+
for (const pattern of BLOCKED_SHELL_PATTERNS) {
|
|
118
|
+
if (pattern.test(command)) {
|
|
119
|
+
return {
|
|
120
|
+
allowed: false,
|
|
121
|
+
reason: `Shell command blocked by safety policy: matches "${pattern.source}"`,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return { allowed: true };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// Main executor
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
export interface StepResult {
|
|
133
|
+
file?: string;
|
|
134
|
+
files?: string[];
|
|
135
|
+
created?: boolean;
|
|
136
|
+
output?: string;
|
|
137
|
+
blocked?: boolean;
|
|
138
|
+
blockReason?: string;
|
|
139
|
+
}
|
|
140
|
+
|
|
5
141
|
export async function executeStep(
|
|
6
142
|
step: { operation: string; args: Record<string, unknown> },
|
|
7
143
|
projectDir: string,
|
|
8
|
-
): Promise<
|
|
144
|
+
): Promise<StepResult> {
|
|
9
145
|
const args = step.args;
|
|
10
146
|
const file = args.file as string | undefined;
|
|
11
147
|
|
|
12
148
|
switch (step.operation) {
|
|
149
|
+
// -----------------------------------------------------------------
|
|
150
|
+
// File write operations (with safety guards)
|
|
151
|
+
// -----------------------------------------------------------------
|
|
13
152
|
case 'write-file':
|
|
14
153
|
case 'create-workflow':
|
|
15
154
|
case 'modify-source':
|
|
16
155
|
case 'implement-node': {
|
|
17
|
-
|
|
156
|
+
if (!file) {
|
|
157
|
+
return { blocked: true, blockReason: `${step.operation} requires a "file" argument.` };
|
|
158
|
+
}
|
|
159
|
+
assertSafePath(file, projectDir);
|
|
160
|
+
const filePath = path.resolve(projectDir, file);
|
|
161
|
+
const content = (args.content as string) ?? (args.body as string) ?? '';
|
|
162
|
+
|
|
163
|
+
const guard = checkWriteSafety(filePath, content);
|
|
164
|
+
if (!guard.allowed) {
|
|
165
|
+
return { file: filePath, blocked: true, blockReason: guard.reason };
|
|
166
|
+
}
|
|
167
|
+
|
|
18
168
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
19
169
|
const existed = fs.existsSync(filePath);
|
|
20
|
-
fs.writeFileSync(filePath,
|
|
170
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
171
|
+
filesWrittenThisPlan++;
|
|
21
172
|
return { file: filePath, created: !existed };
|
|
22
173
|
}
|
|
23
|
-
|
|
24
|
-
|
|
174
|
+
|
|
175
|
+
// -----------------------------------------------------------------
|
|
176
|
+
// Patch file: surgical find-and-replace (no full rewrite needed)
|
|
177
|
+
// -----------------------------------------------------------------
|
|
178
|
+
case 'patch-file': {
|
|
179
|
+
if (!file) {
|
|
180
|
+
return { blocked: true, blockReason: 'patch-file requires a "file" argument.' };
|
|
181
|
+
}
|
|
182
|
+
assertSafePath(file, projectDir);
|
|
183
|
+
const filePath = path.resolve(projectDir, file);
|
|
184
|
+
|
|
185
|
+
if (!fs.existsSync(filePath)) {
|
|
186
|
+
return { blocked: true, blockReason: `File not found: ${file}` };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// File size guard
|
|
190
|
+
const fileSize = fs.statSync(filePath).size;
|
|
191
|
+
if (fileSize > MAX_READ_SIZE) {
|
|
192
|
+
return { blocked: true, blockReason: `File too large for patch-file (${fileSize} bytes, max ${MAX_READ_SIZE}).` };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
let content = fs.readFileSync(filePath, 'utf-8');
|
|
196
|
+
const patches = (args.patches as Array<{ find: string; replace: string }>) ?? [];
|
|
197
|
+
|
|
198
|
+
if (!patches.length && args.find && args.replace !== undefined) {
|
|
199
|
+
patches.push({ find: args.find as string, replace: args.replace as string });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (!patches.length) {
|
|
203
|
+
return { blocked: true, blockReason: 'patch-file requires "patches" array or "find"+"replace" args.' };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
let applied = 0;
|
|
207
|
+
const notFound: string[] = [];
|
|
208
|
+
|
|
209
|
+
for (const patch of patches) {
|
|
210
|
+
if (content.includes(patch.find)) {
|
|
211
|
+
content = content.split(patch.find).join(patch.replace); // replaceAll without regex
|
|
212
|
+
applied++;
|
|
213
|
+
} else {
|
|
214
|
+
notFound.push(patch.find.substring(0, 60));
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (applied === 0) {
|
|
219
|
+
return {
|
|
220
|
+
file: filePath,
|
|
221
|
+
output: `No patches applied. Search strings not found: ${notFound.join('; ')}`,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
226
|
+
filesWrittenThisPlan++;
|
|
227
|
+
|
|
228
|
+
const summary = `Applied ${applied}/${patches.length} patches` +
|
|
229
|
+
(notFound.length ? `. Not found: ${notFound.join('; ')}` : '');
|
|
230
|
+
return { file: filePath, output: summary };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// -----------------------------------------------------------------
|
|
234
|
+
// Read file: return content for AI context
|
|
235
|
+
// -----------------------------------------------------------------
|
|
236
|
+
case 'read-file': {
|
|
237
|
+
if (!file) {
|
|
238
|
+
return { output: '' };
|
|
239
|
+
}
|
|
240
|
+
assertSafePath(file, projectDir);
|
|
241
|
+
const filePath = path.resolve(projectDir, file);
|
|
242
|
+
if (!fs.existsSync(filePath)) {
|
|
243
|
+
return { output: `File not found: ${file}` };
|
|
244
|
+
}
|
|
245
|
+
if (fs.statSync(filePath).isDirectory()) {
|
|
246
|
+
const entries = fs.readdirSync(filePath, { encoding: 'utf-8' });
|
|
247
|
+
return { output: `"${file}" is a directory. Contents:\n${entries.join('\n')}` };
|
|
248
|
+
}
|
|
249
|
+
const fileSize = fs.statSync(filePath).size;
|
|
250
|
+
if (fileSize > MAX_READ_SIZE) {
|
|
251
|
+
return { output: `File too large to read (${fileSize} bytes, max ${MAX_READ_SIZE}). Use run-shell with head/tail instead.` };
|
|
252
|
+
}
|
|
253
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
254
|
+
return { file: filePath, output: content };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// -----------------------------------------------------------------
|
|
258
|
+
// Shell command: run arbitrary command with safety guards
|
|
259
|
+
// -----------------------------------------------------------------
|
|
260
|
+
case 'run-shell': {
|
|
261
|
+
const command = (args.command as string) ?? '';
|
|
262
|
+
if (!command.trim()) {
|
|
263
|
+
return { blocked: true, blockReason: 'run-shell requires a "command" argument.' };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const shellGuard = checkShellSafety(command);
|
|
267
|
+
if (!shellGuard.allowed) {
|
|
268
|
+
return { blocked: true, blockReason: shellGuard.reason };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
try {
|
|
272
|
+
const output = execSync(command, {
|
|
273
|
+
cwd: projectDir,
|
|
274
|
+
encoding: 'utf-8',
|
|
275
|
+
timeout: SHELL_TIMEOUT,
|
|
276
|
+
maxBuffer: 1024 * 1024, // 1MB output cap
|
|
277
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
278
|
+
});
|
|
279
|
+
return { output: output.trim() };
|
|
280
|
+
} catch (err: unknown) {
|
|
281
|
+
// Shell commands that exit non-zero still return useful output
|
|
282
|
+
const execErr = err as { stdout?: string; stderr?: string; status?: number };
|
|
283
|
+
const stdout = (execErr.stdout ?? '').trim();
|
|
284
|
+
const stderr = (execErr.stderr ?? '').trim();
|
|
285
|
+
const combined = [stdout, stderr].filter(Boolean).join('\n');
|
|
286
|
+
return {
|
|
287
|
+
output: combined || (err instanceof Error ? err.message : String(err)),
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// -----------------------------------------------------------------
|
|
293
|
+
// List files: glob-like directory listing
|
|
294
|
+
// -----------------------------------------------------------------
|
|
295
|
+
case 'list-files': {
|
|
296
|
+
const dir = (args.directory as string) ?? (args.dir as string) ?? '.';
|
|
297
|
+
const pattern = (args.pattern as string) ?? '';
|
|
298
|
+
assertSafePath(dir, projectDir);
|
|
299
|
+
const targetDir = path.resolve(projectDir, dir);
|
|
300
|
+
|
|
301
|
+
if (!fs.existsSync(targetDir)) {
|
|
302
|
+
return { output: `Directory not found: ${dir}` };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const entries = fs.readdirSync(targetDir, { recursive: true, encoding: 'utf-8' }) as string[];
|
|
306
|
+
let files = entries
|
|
307
|
+
.filter(e => {
|
|
308
|
+
if (e.includes('node_modules') || e.includes('.git')) return false;
|
|
309
|
+
const full = path.join(targetDir, e);
|
|
310
|
+
try { return fs.statSync(full).isFile(); } catch { return false; }
|
|
311
|
+
})
|
|
312
|
+
.sort();
|
|
313
|
+
|
|
314
|
+
if (pattern) {
|
|
315
|
+
try {
|
|
316
|
+
const regex = new RegExp(pattern);
|
|
317
|
+
files = files.filter(f => regex.test(f));
|
|
318
|
+
} catch {
|
|
319
|
+
return { output: `Invalid regex pattern: ${pattern}` };
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (files.length > MAX_LIST_FILES) {
|
|
324
|
+
files = files.slice(0, MAX_LIST_FILES);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return { files: files.map(f => path.join(dir, f)), output: files.join('\n') };
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// -----------------------------------------------------------------
|
|
331
|
+
// Flow-weaver CLI commands (via programmatic API)
|
|
332
|
+
// -----------------------------------------------------------------
|
|
25
333
|
default: {
|
|
26
334
|
const result = await runCommand(step.operation, { ...args, cwd: projectDir });
|
|
27
335
|
return {
|