@synergenius/flow-weaver-pack-weaver 0.9.5 → 0.9.6

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.
@@ -31,6 +31,8 @@ export interface AssistantOptions {
31
31
  systemPrompt?: string;
32
32
  /** Override for testing — provide messages instead of reading stdin */
33
33
  inputMessages?: string[];
34
+ /** Watch a directory for file changes and auto-suggest fixes */
35
+ watchDir?: string;
34
36
  /** Resume a specific conversation by ID */
35
37
  resumeId?: string;
36
38
  /** Always start a fresh conversation */
@@ -71,6 +73,41 @@ For those: you may summarize or explain the result as needed.`;
71
73
 
72
74
  export async function runAssistant(opts: AssistantOptions): Promise<void> {
73
75
  const { provider, tools, executor, projectDir } = opts;
76
+ const out = (s: string) => process.stderr.write(s);
77
+
78
+ // Pipe mode: if stdin is not a TTY, read all input as one message
79
+ if (!process.stdin.isTTY && !opts.inputMessages) {
80
+ const chunks: string[] = [];
81
+ for await (const chunk of process.stdin) {
82
+ chunks.push(typeof chunk === 'string' ? chunk : chunk.toString());
83
+ }
84
+ const pipeInput = chunks.join('').trim();
85
+ if (!pipeInput) return;
86
+
87
+ // Build system prompt for pipe mode
88
+ let systemPrompt = opts.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
89
+ try {
90
+ const fsMod = await import('node:fs');
91
+ const pathMod = await import('node:path');
92
+ const planPath = pathMod.resolve(projectDir, '.weaver-plan.md');
93
+ if (fsMod.existsSync(planPath)) {
94
+ const plan = fsMod.readFileSync(planPath, 'utf-8').trim();
95
+ systemPrompt += '\n\n## Project Plan & Vision\n\nAll bots you spawn and tasks you queue MUST align with this plan.\n\n' + plan;
96
+ }
97
+ } catch { /* plan not available */ }
98
+
99
+ // Run single message, print result, exit
100
+ await runAgentLoop(provider, tools, executor, [{ role: 'user', content: pipeInput }], {
101
+ systemPrompt, maxIterations: 20,
102
+ onStreamEvent: (e) => { if (e.type === 'text_delta') out(e.text); },
103
+ onToolEvent: (e) => {
104
+ if (e.type === 'tool_call_start') out(`\n ${c.cyan('◆')} ${e.name}\n`);
105
+ if (e.type === 'tool_call_result') out(` ${c.dim('→')} ${(e.result ?? '').slice(0, 200)}\n`);
106
+ },
107
+ });
108
+ out('\n');
109
+ return;
110
+ }
74
111
 
75
112
  // Build system prompt — include project plan if it exists
76
113
  let systemPrompt = opts.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
@@ -84,8 +121,6 @@ export async function runAssistant(opts: AssistantOptions): Promise<void> {
84
121
  }
85
122
  } catch { /* plan not available */ }
86
123
 
87
- const out = (s: string) => process.stderr.write(s);
88
-
89
124
  // Persistent conversation store
90
125
  const { ConversationStore } = await import('./conversation-store.js');
91
126
  const store = new ConversationStore();
@@ -117,13 +152,30 @@ export async function runAssistant(opts: AssistantOptions): Promise<void> {
117
152
  }
118
153
  }
119
154
 
155
+ // Resolve versions
156
+ let fwVersion = '?';
157
+ let weaverVersion = '?';
158
+ try {
159
+ const { execFileSync: vExec } = await import('node:child_process');
160
+ fwVersion = vExec('npx', ['flow-weaver', '--version'], { encoding: 'utf-8', cwd: projectDir, timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] }).trim().replace(/^flow-weaver\s+v?/i, '');
161
+ } catch { /* not available */ }
162
+ try {
163
+ const fsMod = await import('node:fs');
164
+ const url = await import('node:url');
165
+ const packPkg = JSON.parse(fsMod.readFileSync(new url.URL('../package.json', import.meta.url), 'utf-8'));
166
+ weaverVersion = packPkg.version;
167
+ } catch { /* not available */ }
168
+
120
169
  // Welcome
121
- out(`\n ${c.bold('weaver assistant')}\n`);
122
- out(` ${c.dim(`Project: ${projectDir}`)}\n`);
170
+ out(`\n ${c.bold('weaver assistant')} ${c.dim(`v${weaverVersion}`)} ${c.dim(`· flow-weaver v${fwVersion}`)}\n`);
171
+ if (process.env.FW_PLATFORM_TOKEN) {
172
+ out(` ${c.dim('AI: Platform credits (no API key needed)')}\n`);
173
+ }
174
+ out(` ${c.dim(`Project: ${path.basename(projectDir)}`)}\n`);
123
175
  if (conversation.title) {
124
176
  out(` ${c.dim(`Resuming: "${conversation.title}" (${conversation.messageCount} messages)`)}\n`);
125
177
  } else {
126
- out(` ${c.dim(`Conversation: ${conversation.id}`)}\n`);
178
+ out(` ${c.dim(`New conversation`)}\n`);
127
179
  }
128
180
  out(` ${c.dim('Type your request. Ctrl+C to exit. /help for commands.')}\n\n`);
129
181
 
@@ -188,6 +240,31 @@ export async function runAssistant(opts: AssistantOptions): Promise<void> {
188
240
  onVerbose: () => { out(` ${c.dim('Verbose toggling not yet wired to streaming.')}\n`); },
189
241
  };
190
242
 
243
+ // Watch mode: monitor directory for file changes, auto-validate
244
+ let watcher: import('node:fs').FSWatcher | null = null;
245
+ if (opts.watchDir) {
246
+ try {
247
+ const fsMod = await import('node:fs');
248
+ const { execFileSync } = await import('node:child_process');
249
+ watcher = fsMod.watch(opts.watchDir, { recursive: true }, (_event, filename) => {
250
+ if (!filename || !filename.endsWith('.ts')) return;
251
+ const filePath = `${opts.watchDir}/${filename}`;
252
+ try {
253
+ const result = execFileSync('npx', ['flow-weaver', 'validate', filePath, '--json'], {
254
+ encoding: 'utf-8', cwd: projectDir, timeout: 15_000, stdio: ['pipe', 'pipe', 'pipe'],
255
+ });
256
+ const parsed = JSON.parse(result);
257
+ const errorCount = parsed.errorCount ?? parsed.errors?.length ?? 0;
258
+ if (errorCount > 0) {
259
+ out(`\n ${c.yellow('⚠')} ${filename} changed: ${errorCount} validation error(s)\n`);
260
+ out(` ${c.dim('Type a message to fix, or ignore.')}\n`);
261
+ }
262
+ } catch { /* validation failed or not a workflow — ignore */ }
263
+ });
264
+ out(` ${c.dim(`Watching: ${opts.watchDir}`)}\n\n`);
265
+ } catch { /* watch not available */ }
266
+ }
267
+
191
268
  // Main conversation loop
192
269
  while (!shouldExit) {
193
270
  const input = await getNextInput();
@@ -239,6 +316,9 @@ export async function runAssistant(opts: AssistantOptions): Promise<void> {
239
316
  store.appendMessages(conversation.id, [{ role: 'user', content: input }, ...newMessages]);
240
317
  store.updateAfterTurn(conversation.id, [{ role: 'user', content: input }, ...newMessages], tokensUsed);
241
318
 
319
+ // Sync to cloud if logged in (fire-and-forget)
320
+ store.syncToCloud(conversation.id, [{ role: 'user', content: input }, ...newMessages]).catch(() => {});
321
+
242
322
  // Auto-title from first assistant response
243
323
  if (!conversation.title) {
244
324
  const firstAssistant = newMessages.find(m => m.role === 'assistant');
@@ -265,6 +345,7 @@ export async function runAssistant(opts: AssistantOptions): Promise<void> {
265
345
  out('\n');
266
346
  }
267
347
 
348
+ watcher?.close();
268
349
  richInput?.destroy();
269
350
  out(`\n ${c.dim('Goodbye.')}\n\n`);
270
351
  }
@@ -176,6 +176,38 @@ export class ConversationStore {
176
176
  });
177
177
  }
178
178
 
179
+ async syncToCloud(id: string, newMessages: AgentMessage[]): Promise<void> {
180
+ try {
181
+ const credPath = path.join(os.homedir(), '.fw', 'credentials.json');
182
+ if (!fs.existsSync(credPath)) return;
183
+ const creds = JSON.parse(fs.readFileSync(credPath, 'utf-8'));
184
+ if (!creds.token || !creds.platformUrl || creds.expiresAt <= Date.now()) return;
185
+
186
+ const conversation = this.get(id);
187
+ if (!conversation) return;
188
+
189
+ // Fire-and-forget sync — don't block the conversation
190
+ const lastMessage = newMessages.find(m => m.role === 'user');
191
+ if (!lastMessage) return;
192
+
193
+ const message = typeof lastMessage.content === 'string' ? lastMessage.content : JSON.stringify(lastMessage.content);
194
+
195
+ fetch(`${creds.platformUrl}/ai-chat/stream`, {
196
+ method: 'POST',
197
+ headers: {
198
+ 'Content-Type': 'application/json',
199
+ ...(creds.token.startsWith('fw_')
200
+ ? { 'X-API-Key': creds.token }
201
+ : { Authorization: `Bearer ${creds.token}` }),
202
+ },
203
+ body: JSON.stringify({
204
+ message: `[Synced from CLI] ${message}`,
205
+ conversationId: (conversation as any).cloudConversationId,
206
+ }),
207
+ }).catch(() => {}); // fire-and-forget
208
+ } catch { /* sync not available */ }
209
+ }
210
+
179
211
  async setTitle(id: string, title: string): Promise<void> {
180
212
  await withFileLock(this.indexPath, () => {
181
213
  const index = this.readIndex();
@@ -1,5 +1,6 @@
1
1
  import * as path from 'node:path';
2
2
  import * as fs from 'node:fs';
3
+ import * as os from 'node:os';
3
4
  import { fileURLToPath } from 'node:url';
4
5
  import { runWorkflow } from './bot/runner.js';
5
6
  import { RunStore } from './bot/run-store.js';
@@ -67,6 +68,7 @@ export interface ParsedArgs {
67
68
  assistantResume?: string;
68
69
  assistantList?: boolean;
69
70
  assistantDelete?: string;
71
+ assistantWatch?: string;
70
72
  }
71
73
 
72
74
  export function parseArgs(argv: string[]): ParsedArgs {
@@ -261,6 +263,9 @@ export function parseArgs(argv: string[]): ParsedArgs {
261
263
  result.genesisInit = true;
262
264
  } else if (arg === '--watch') {
263
265
  result.genesisWatch = true;
266
+ } else if (arg === '--watch-dir' && i + 1 < args.length) {
267
+ i++;
268
+ result.assistantWatch = args[i];
264
269
  } else if (arg === '--new') {
265
270
  result.assistantNew = true;
266
271
  } else if (arg === '--resume' && i + 1 < args.length) {
@@ -1093,6 +1098,43 @@ export async function handleEject(opts: ParsedArgs): Promise<void> {
1093
1098
  console.log(`[weaver] Metadata written to ${metaPath}`);
1094
1099
  console.log('[weaver] You can now customize the ejected workflow(s) freely.');
1095
1100
  console.log('[weaver] The bot will use local files when available.');
1101
+
1102
+ // Generate CUSTOMIZATION.md guide
1103
+ const customizationGuide = `# Customizing Your Weaver Bot
1104
+
1105
+ ## What You Got
1106
+ ${results.map(r => `- ${r.file} — ${r.workflow} workflow (${r.nodeTypes.length} node types)`).join('\n')}
1107
+
1108
+ You can edit these files freely. Weaver will use your local versions.
1109
+
1110
+ ## Safe Changes
1111
+ - Add a notification node (Slack, Discord, email) after git-ops
1112
+ - Change approval mode: edit the @node approve line
1113
+ - Add custom validation: insert a node between agent and gitOps
1114
+ - Modify the system prompt: edit system-prompt.ts
1115
+ - Add new tools: create a node type and wire it in
1116
+
1117
+ ## How to Add a Node
1118
+ 1. Create a new file: my-node.ts with @flowWeaver nodeType annotation
1119
+ 2. Import it in the workflow file
1120
+ 3. Add: @node myNode myNodeType [color: "blue"] [icon: "star"]
1121
+ 4. Wire it: @connect existingNode.output -> myNode.input
1122
+ 5. Compile: flow-weaver compile <workflow-file>
1123
+
1124
+ ## How to Test
1125
+ flow-weaver validate <workflow-file> # check for errors
1126
+ flow-weaver diagram <workflow-file> # visualize the DAG
1127
+ weaver bot "test task" --auto-approve # run a test task
1128
+
1129
+ ## Learn More
1130
+ weaver examples # see what's possible
1131
+ weaver doctor # check your setup
1132
+ flow-weaver docs concepts # Flow Weaver documentation
1133
+ `;
1134
+
1135
+ const customPath = path.resolve(destDir, 'CUSTOMIZATION.md');
1136
+ fs.writeFileSync(customPath, customizationGuide, 'utf-8');
1137
+ console.log(`[weaver] Generated ${customPath}`);
1096
1138
  }
1097
1139
 
1098
1140
  export async function handleRun(opts: ParsedArgs): Promise<void> {
@@ -1525,6 +1567,32 @@ export async function handleSession(opts: ParsedArgs): Promise<void> {
1525
1567
  sendDesktopNotification('Weaver Session Complete', `${sessionCompleted} done, ${sessionFailed} failed, ${sessionNoOp} no-op`);
1526
1568
  } catch { /* non-fatal */ }
1527
1569
  }
1570
+
1571
+ // Webhook notification if configured
1572
+ if (config?.notify) {
1573
+ try {
1574
+ const webhookUrl = typeof config.notify === 'string' ? config.notify
1575
+ : typeof config.notify === 'object' && 'webhook' in config.notify ? (config.notify as { webhook: string }).webhook
1576
+ : null;
1577
+ if (webhookUrl) {
1578
+ fetch(webhookUrl, {
1579
+ method: 'POST',
1580
+ headers: { 'Content-Type': 'application/json' },
1581
+ body: JSON.stringify({
1582
+ event: 'session.completed',
1583
+ timestamp: new Date().toISOString(),
1584
+ tasks: taskCount,
1585
+ completed: sessionCompleted,
1586
+ failed: sessionFailed,
1587
+ noOp: sessionNoOp,
1588
+ tokens: sessionInputTokens + sessionOutputTokens,
1589
+ cost: sessionCost,
1590
+ elapsed: Date.now() - sessionStartTime,
1591
+ }),
1592
+ }).catch(() => {}); // fire-and-forget
1593
+ }
1594
+ } catch { /* non-fatal */ }
1595
+ }
1528
1596
  }
1529
1597
 
1530
1598
  export async function handleAssistant(opts: ParsedArgs): Promise<void> {
@@ -1559,13 +1627,37 @@ export async function handleAssistant(opts: ParsedArgs): Promise<void> {
1559
1627
 
1560
1628
  const config = await loadConfig(opts.configPath);
1561
1629
 
1630
+ // Check platform login — override provider if logged in
1631
+ let providerOverride: string | undefined;
1632
+ try {
1633
+ const credPath = path.join(os.homedir(), '.fw', 'credentials.json');
1634
+ if (fs.existsSync(credPath)) {
1635
+ const creds = JSON.parse(fs.readFileSync(credPath, 'utf-8'));
1636
+ if (creds.token && creds.platformUrl && creds.expiresAt > Date.now()) {
1637
+ providerOverride = 'platform';
1638
+ // Make credentials available to the provider
1639
+ process.env.FW_PLATFORM_TOKEN = creds.token;
1640
+ process.env.FW_PLATFORM_URL = creds.platformUrl;
1641
+ }
1642
+ }
1643
+ } catch { /* not available */ }
1644
+
1562
1645
  // Create provider
1563
- const { createAnthropicProvider, createClaudeCliProvider } = await import('@synergenius/flow-weaver/agent');
1646
+ const agentMod = await import('@synergenius/flow-weaver/agent');
1647
+ const { createAnthropicProvider, createClaudeCliProvider } = agentMod;
1648
+ const createPlatformProvider = 'createPlatformProvider' in agentMod
1649
+ ? (agentMod as Record<string, unknown>).createPlatformProvider as (opts: { token: string; platformUrl: string }) => unknown
1650
+ : null;
1564
1651
  const providerSetting = config?.provider ?? 'auto';
1565
1652
  const providerType = typeof providerSetting === 'object' ? providerSetting.name : String(providerSetting);
1566
1653
 
1567
- let provider;
1568
- if (providerType === 'anthropic' || (providerType === 'auto' && process.env.ANTHROPIC_API_KEY)) {
1654
+ let provider: import('@synergenius/flow-weaver/agent').AgentProvider;
1655
+ if ((providerOverride === 'platform' || providerType === 'platform') && createPlatformProvider) {
1656
+ provider = createPlatformProvider({
1657
+ token: process.env.FW_PLATFORM_TOKEN!,
1658
+ platformUrl: process.env.FW_PLATFORM_URL!,
1659
+ }) as import('@synergenius/flow-weaver/agent').AgentProvider;
1660
+ } else if (providerType === 'anthropic' || (providerType === 'auto' && process.env.ANTHROPIC_API_KEY)) {
1569
1661
  const apiKey = process.env.ANTHROPIC_API_KEY ?? (typeof providerSetting === 'object' ? (providerSetting as { apiKey?: string }).apiKey : undefined);
1570
1662
  if (!apiKey) { console.error('ANTHROPIC_API_KEY required for anthropic provider'); process.exit(1); }
1571
1663
  provider = createAnthropicProvider({ apiKey, model: typeof providerSetting === 'object' ? providerSetting.model : undefined });
@@ -1584,6 +1676,7 @@ export async function handleAssistant(opts: ParsedArgs): Promise<void> {
1584
1676
  projectDir,
1585
1677
  resumeId: opts.assistantResume,
1586
1678
  newConversation: opts.assistantNew,
1679
+ watchDir: opts.assistantWatch,
1587
1680
  });
1588
1681
  }
1589
1682
 
@@ -2125,6 +2218,15 @@ export async function handleDoctor(opts: ParsedArgs): Promise<void> {
2125
2218
  checks.push({ label: 'Connection', status: 'warn', detail: 'Not tested' });
2126
2219
  }
2127
2220
 
2221
+ // Weaver version (this pack)
2222
+ try {
2223
+ const url = await import('node:url');
2224
+ const packPkg = JSON.parse(fs.readFileSync(new url.URL('../package.json', import.meta.url) as unknown as string, 'utf-8'));
2225
+ checks.push({ label: 'Weaver', status: 'ok', detail: `v${packPkg.version}` });
2226
+ } catch {
2227
+ checks.push({ label: 'Weaver', status: 'ok', detail: 'unknown version' });
2228
+ }
2229
+
2128
2230
  // Flow Weaver version
2129
2231
  try {
2130
2232
  const { execFileSync: fwCheck } = await import('node:child_process');
@@ -140,7 +140,7 @@ export async function weaverAgentExecute(
140
140
  auditEmit('run-start', { task: task.instruction });
141
141
 
142
142
  try {
143
- const provider = createProvider(pInfo, projectDir);
143
+ const provider = await createProvider(pInfo, projectDir);
144
144
  const executor = createWeaverExecutor(projectDir);
145
145
 
146
146
  const filesModified: string[] = [];
@@ -253,10 +253,10 @@ export async function weaverAgentExecute(
253
253
  /**
254
254
  * Create an AgentProvider from pack-weaver's ProviderInfo.
255
255
  */
256
- function createProvider(
256
+ async function createProvider(
257
257
  pInfo: { type: string; apiKey?: string; model?: string; maxTokens?: number },
258
258
  projectDir?: string,
259
- ): AgentProvider {
259
+ ): Promise<AgentProvider> {
260
260
  const type = pInfo.type ?? 'auto';
261
261
 
262
262
  // Explicit Anthropic API
@@ -273,8 +273,21 @@ function createProvider(
273
273
  return createSessionProvider(pInfo.model, projectDir);
274
274
  }
275
275
 
276
- // Auto mode: try Anthropic API key from env, then fall back to Claude CLI
276
+ // Auto mode: try platform login, then Anthropic API key, then Claude CLI
277
277
  if (type === 'auto') {
278
+ // Check platform login first (no local key needed)
279
+ // Uses env vars set by handleSession/handleAssistant, or reads credentials directly
280
+ if (process.env.FW_PLATFORM_TOKEN && process.env.FW_PLATFORM_URL) {
281
+ try {
282
+ const agentMod = await import('@synergenius/flow-weaver/agent');
283
+ if ('createPlatformProvider' in agentMod) {
284
+ const factory = (agentMod as Record<string, unknown>).createPlatformProvider as
285
+ (opts: { token: string; platformUrl: string }) => AgentProvider;
286
+ return factory({ token: process.env.FW_PLATFORM_TOKEN, platformUrl: process.env.FW_PLATFORM_URL });
287
+ }
288
+ } catch { /* platform provider not available in this fw version */ }
289
+ }
290
+
278
291
  if (process.env.ANTHROPIC_API_KEY) {
279
292
  return createAnthropicProvider({
280
293
  apiKey: process.env.ANTHROPIC_API_KEY,