@synergenius/flow-weaver 0.23.1 → 0.23.3

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/README.md CHANGED
@@ -8,461 +8,112 @@
8
8
  [![License: Flow Weaver Library License](https://img.shields.io/badge/License-Flow%20Weaver%20Library-blue?style=flat)](./LICENSE)
9
9
  [![Node.js](https://img.shields.io/badge/Node.js-%3E%3D18-green?style=flat)](https://nodejs.org)
10
10
 
11
- **Build AI agent workflows visually. Ship them as your own code.**
11
+ **Design agent workflows in conversation. The compiled output is yours.**
12
12
 
13
- Design agent workflows in the Studio, in TypeScript, or let AI build them for you. The compiler validates everything with 20+ rule categories. The output is standalone TypeScript you deploy anywhere, zero runtime dependency on Flow Weaver.
14
-
15
- *Got an AGENTS.md or runbook? [Convert it to a typed workflow →](https://flowweaver.ai/services/md-converter)*
16
-
17
- [**flowweaver.ai**](https://flowweaver.ai) · [**Open the Studio**](https://flowweaver.ai/studio) · [**Docs**](https://flowweaver.ai/docs) · [**Discord**](https://discord.gg/6Byh3ur2bk) · [**npm**](https://www.npmjs.com/package/@synergenius/flow-weaver)
13
+ [**flowweaver.ai**](https://flowweaver.ai) · [**Studio**](https://flowweaver.ai/studio) · [**Docs**](https://flowweaver.ai/docs) · [**Discord**](https://discord.gg/6Byh3ur2bk) · [**npm**](https://www.npmjs.com/package/@synergenius/flow-weaver)
18
14
 
19
15
  ---
20
16
 
21
- ## Your AGENTS.md is a workflow waiting to happen
22
-
23
- You already wrote the steps, the branching logic, the edge cases. But it's a markdown file. It doesn't run, doesn't validate, and you can't see the full flow at a glance.
24
-
25
- **Paste this:**
26
-
27
- ```markdown
28
- # Code Review Agent
29
-
30
- ### 1. Analyze diff
31
- Send the PR diff to an LLM for security, quality, and style review.
32
-
33
- ### 2. Classify severity
34
- Critical (must fix) · Warning (should fix) · Suggestion (nice to have).
35
-
36
- ### 3. Route
37
- Critical issues → request changes. Otherwise → approve with comments.
38
- ```
39
-
40
- **Get this:**
41
-
42
- <a href="https://htmlpreview.github.io/?https://github.com/synergenius-fw/flow-weaver/blob/main/docs/images/code-review-agent.html">
43
- <picture>
44
- <source media="(prefers-color-scheme: dark)" srcset="docs/images/code-review-agent-dark.svg">
45
- <source media="(prefers-color-scheme: light)" srcset="docs/images/code-review-agent-light.svg">
46
- <img alt="Code Review Agent workflow diagram generated from AGENTS.md" src="docs/images/code-review-agent-dark.svg" width="100%">
47
- </picture>
48
- </a>
49
-
50
- <sup>Click the diagram to open the interactive version (zoom, pan, inspect nodes).</sup>
51
-
52
- The converter extracts the steps and decision points from your markdown and produces typed TypeScript plus a visual diagram. You can run it, test it, deploy it.
53
-
54
- [**Try the converter →**](https://flowweaver.ai/services/md-converter) &ensp; Works with AGENTS.md, CLAUDE.md, Cursor rules, runbooks, SOPs, and any process doc.
17
+ > **You:** Build a support agent that classifies messages and either auto-replies or escalates.
18
+
19
+ > **Weaver:** Created `support-agent.ts`. Four nodes, one workflow.
20
+ >
21
+ > ```
22
+ > Workflow: supportAgent
23
+ >
24
+ > Nodes (4):
25
+ > classify [classifyIntent] IN: message OUT: context
26
+ > route [routeAction] IN: context, execute OUT: context, onSuccess, onFailure
27
+ > reply [llmReply] IN: context OUT: reply
28
+ > esc [escalate] IN: context OUT: ticket, assignee
29
+ >
30
+ > Paths (2):
31
+ > Start -> classify -> route -> reply -> Exit
32
+ > Start -> classify -> route -> esc -> Exit
33
+ > ```
34
+
35
+ > **You:** Show me the graph.
36
+
37
+ > **Weaver:**
38
+ >
39
+ > ```
40
+ > ┌───────┐ ┌─────────────────┐ ┌──────────────┐ ┌───────────┐ ┌──────┐
41
+ > Start │━━━▶│ Classify Intent │━━━▶│ Route Action │━━━▶│ LLM Reply │━━━▶│ Exit │
42
+ > └───────┘ └─────────────────┘ └──────────────┘ └───────────┘ └──────┘
43
+ >
44
+ > Parallel: Escalate
45
+ > ```
46
+
47
+ > **You:** Compile it.
48
+
49
+ > **Weaver:** Done. Zero runtime deps. The output is a standalone TypeScript function you own.
50
+
51
+ > **You:** Validate.
52
+
53
+ > **Weaver:**
54
+ >
55
+ > ```
56
+ > ✓ 1 valid, 2 warnings in 312ms
57
+ > ```
58
+ >
59
+ > Structural, agent safety, and CI rules. All checked.
60
+
61
+ > **You:** Run it with "I need an urgent refund".
62
+
63
+ > **Weaver:**
64
+ >
65
+ > ```
66
+ > classify: → SUCCEEDED (0ms)
67
+ > route: → SUCCEEDED (0ms)
68
+ > reply: → CANCELLED
69
+ > esc: → SUCCEEDED (0ms)
70
+ > ✓ Workflow "supportAgent" completed in 2ms
71
+ >
72
+ > Result: { ticket: "ESC-1773950738529", assignee: "support-team" }
73
+ > ```
74
+
75
+ > **You:** Commit it.
76
+
77
+ > **Weaver:** Committed to `main`. It's a `.ts` file in your repo. Version-controlled like everything else.
78
+
79
+ > **You:** Now add sentiment analysis before routing.
80
+
81
+ > **Weaver:** Done. Here's what changed:
82
+ >
83
+ > ```
84
+ > [BREAKING] +1 types, ~1 types, +1 nodes, +5 conns, -2 conns
85
+ >
86
+ > + analyzeSentiment
87
+ > ~ routeAction (+inputs: sentiment)
88
+ > + sentiment (instance)
89
+ > + classify.context → sentiment.context
90
+ > + sentiment.sentiment → route.sentiment
91
+ > ```
92
+
93
+ > **You:** Where does this deploy?
94
+
95
+ > **Weaver:** Anywhere TypeScript runs. Or generate boilerplate:
96
+ >
97
+ > ```
98
+ > fw export support-agent.ts --target vercel
99
+ > fw export support-agent.ts --target lambda
100
+ > fw export support-agent.ts --target cloudflare
101
+ > fw export support-agent.ts --target inngest
102
+ > ```
55
103
 
56
104
  ---
57
105
 
58
- ## Try It
59
-
60
- **In the browser.** Open the [Cloud Studio](https://flowweaver.ai/studio), sign up, and start building. No install required.
61
-
62
- **From the terminal.**
106
+ ## Install
63
107
 
64
108
  ```bash
65
109
  npm install @synergenius/flow-weaver
66
- npx fw init my-project
67
- cd my-project
68
- npx fw run workflows/example.ts --params '{"input": "value"}'
69
- ```
70
-
71
- `fw` is the CLI command. `flow-weaver` also works as an alias.
72
-
73
- ## How It Works
74
-
75
- **Generate.** Let AI build it for you, design visually in the Studio, or write annotated TypeScript by hand. All three stay in sync.
76
-
77
- **Validate.** The compiler catches wiring errors, type mismatches, missing handlers, unreachable paths, and agent-specific mistakes before anything runs. Not at runtime, not in production.
78
-
79
- **Own.** Compiled output is plain TypeScript with zero imports from Flow Weaver. Clean git diffs, standard testing, no license obligations on the output. If Flow Weaver disappeared tomorrow, your agents would still run.
80
-
81
- ## Three Ways to Build
82
-
83
- **Studio.** A visual IDE that combines a full code editor with a graph builder. Write and refactor workflows in code or compose them on the canvas. Both representations stay synchronized: editing either one updates the other instantly.
84
-
85
- **TypeScript.** Define workflows in plain TypeScript by annotating functions with JSDoc. The compiler derives an executable workflow graph with static typing and compile-time validation. No YAML, no JSON configuration, no runtime layer. Remove the annotations and you keep a clean TypeScript file with zero dependencies.
86
-
87
- **AI Agents.** Connect Claude Code, Cursor, or OpenClaw and let agents scaffold, compile, fix validation errors, and iterate until the workflow works. Agents can also test with mock AI responses and generate diagrams for review.
88
-
89
- ## Quick Start
90
-
91
- Workflows are plain TypeScript. Annotations declare the graph structure:
92
-
93
- ```typescript
94
- // data-pipeline.ts
95
-
96
- /**
97
- * @flowWeaver nodeType
98
- * @input rawData - string
99
- * @output cleaned - string
100
- * @output wordCount - number
101
- */
102
- function processText(execute: boolean, rawData: string) {
103
- if (!execute) return { onSuccess: false, onFailure: false, cleaned: '', wordCount: 0 };
104
- const cleaned = rawData.trim().toLowerCase();
105
- return { onSuccess: true, onFailure: false, cleaned, wordCount: cleaned.split(/\s+/).length };
106
- }
107
-
108
- /**
109
- * @flowWeaver workflow
110
- * @node processor processText
111
- * @connect Start.rawData -> processor.rawData
112
- * @connect processor.cleaned -> Exit.cleaned
113
- * @connect processor.wordCount -> Exit.wordCount
114
- */
115
- export async function dataPipeline(
116
- execute: boolean,
117
- params: { rawData: string }
118
- ): Promise<{ onSuccess: boolean; onFailure: boolean; cleaned: string; wordCount: number }> {
119
- throw new Error('Not compiled');
120
- }
121
- ```
122
-
123
- ```bash
124
- npx fw compile data-pipeline.ts # generates executable code in-place
125
- npx fw run data-pipeline.ts --params '{"rawData": "Hello World"}'
110
+ npx flow-weaver init
126
111
  ```
127
112
 
128
- The compiler fills in the function body while preserving your code outside the generated markers.
129
-
130
- ## AI-Native Development with MCP
131
-
132
- Flow Weaver includes an MCP server with 48 tools for Claude Code, Cursor, OpenClaw, or any MCP-compatible agent:
133
-
134
- ```bash
135
- npx fw mcp-server # auto-registers with Claude Code
136
- ```
137
-
138
- | Capability | MCP Tools |
139
- |-----------|-----------|
140
- | **Build** | `fw_scaffold`, `fw_modify`, `fw_modify_batch`, `fw_add_node`, `fw_connect` |
141
- | **Model** | `fw_create_model`, `fw_workflow_status`, `fw_implement_node` |
142
- | **Validate** | `fw_validate` (with friendly error hints), `fw_doctor` |
143
- | **Understand** | `fw_describe` (json/text/mermaid), `fw_query` (10 query types), `fw_diff` |
144
- | **Test** | `fw_execute_workflow` (with trace), `fw_compile` |
145
- | **Visualize** | `fw_diagram` (SVG/HTML), `fw_get_state`, `fw_focus_node` |
146
- | **Deploy** | `fw_export` (Lambda, Vercel, Cloudflare, Inngest, GitHub Actions, GitLab CI — via packs), `fw_compile --target inngest` |
147
- | **Reuse** | `fw_list_patterns`, `fw_apply_pattern`, `fw_extract_pattern` |
148
- | **Extend** | `fw_market_search`, `fw_market_install` |
149
-
150
- ## Model-Driven Workflows
151
-
152
- Design first, implement later. Describe the workflow shape (nodes, ports, execution path) and the compiler generates a valid skeleton with `declare function` stubs:
153
-
154
- ```bash
155
- # Via MCP: fw_create_model with nodes, inputs/outputs, and execution path
156
- # Via CLI:
157
- npx fw status my-workflow.ts # shows stub vs implemented progress
158
- npx fw implement my-workflow.ts processData # scaffolds a node body
159
- ```
160
-
161
- The graph is valid before any node has a real implementation. Fill in node bodies incrementally, check `status` to track progress. The architect defines the shape, developers fill in the logic.
162
-
163
- ## Agent Workflow Templates
164
-
165
- Built-in templates for AI agent workflows:
166
-
167
- ```bash
168
- npx fw create workflow ai-agent my-agent.ts --provider openai --model gpt-4o
169
- npx fw create workflow ai-react react-agent.ts
170
- npx fw create workflow ai-rag rag-pipeline.ts
171
- npx fw create workflow ai-agent-durable durable-agent.ts
172
- ```
173
-
174
- **12 workflow templates:**
175
-
176
- | Template | What it builds |
177
- |----------|---------------|
178
- | `ai-agent` | Tool-calling agent with explicit loop and termination semantics |
179
- | `ai-react` | ReAct agent (Thought -> Action -> Observation) |
180
- | `ai-rag` | Retrieval-Augmented Generation pipeline |
181
- | `ai-chat` | Stateful conversational AI with memory |
182
- | `ai-agent-durable` | Durable agent pipeline with Inngest step-level retries |
183
- | `ai-pipeline-durable` | Multi-step AI pipeline with durability |
184
- | `sequential` | Linear data pipeline |
185
- | `foreach` | Iteration over collections |
186
- | `conditional` | Branching logic |
187
- | `aggregator` | Multi-source aggregation |
188
- | `webhook` | HTTP event handler |
189
- | `error-handler` | Error recovery pattern |
190
-
191
- Plus **12 node templates** for common building blocks: `llm-call`, `tool-executor`, `conversation-memory`, `prompt-template`, `json-extractor`, `human-approval`, `agent-router`, `rag-retriever`, `validator`, `transformer`, `http`, `aggregator`.
192
-
193
- ## Compile-Time Validation
194
-
195
- The validator understands AI agent patterns and enforces safety rules:
196
-
197
- ```
198
- AGENT_LLM_MISSING_ERROR_HANDLER LLM node's onFailure is unconnected, add error handling
199
- AGENT_UNGUARDED_TOOL_EXECUTOR Tool executor has no human-approval upstream, add a gate
200
- AGENT_MISSING_MEMORY_IN_LOOP Agent loop has LLM but no memory, conversations will be stateless
201
- AGENT_LLM_NO_FALLBACK LLM failure goes directly to Exit, add retry or fallback logic
202
- AGENT_TOOL_NO_OUTPUT_HANDLING Tool executor outputs are all unconnected, results are discarded
203
- ```
204
-
205
- These are not generic lint rules. The validator identifies LLM, tool-executor, human-approval, and memory nodes by port signatures, annotations, and naming patterns, then applies agent-specific checks.
206
-
207
- ## Deterministic Agent Testing
208
-
209
- Test LLM workflows without real API calls:
210
-
211
- ```typescript
212
- import { createMockLlmProvider, createRecordingProvider, loadRecording } from '@synergenius/flow-weaver/testing';
213
-
214
- // Mock: deterministic responses for CI
215
- const mock = createMockLlmProvider([
216
- { content: 'I need to search for that.', toolCalls: [{ name: 'search', args: { q: 'test' } }] },
217
- { content: 'Based on the results, the answer is 42.' },
218
- ]);
219
-
220
- // Record: capture real LLM calls, replay later
221
- const recorder = createRecordingProvider(realProvider);
222
- // ... run workflow ...
223
- saveRecording(recorder.getRecording(), 'fixtures/agent-session.json');
224
-
225
- // Replay: reproducible tests from recorded sessions
226
- const replay = loadRecording('fixtures/agent-session.json');
227
- ```
113
+ The CLI handles the rest.
228
114
 
229
- Mock human approvals, fast-forward delays, and simulate external events. Configured via `globalThis.__fw_mock_config__`.
230
-
231
- ## Scoped Ports: Agent Loops Without Graph Cycles
232
-
233
- Most workflow engines either ban loops entirely (DAG-only) or allow arbitrary cycles that are hard to reason about. Flow Weaver takes a third path: **scoped ports** express iteration while keeping the graph acyclic and statically analyzable.
234
-
235
- ```typescript
236
- /**
237
- * @flowWeaver workflow
238
- * @node agent llmCall
239
- * @node tools toolExecutor
240
- * @node memory conversationMemory
241
- * @scope agent, tools, memory // these nodes iterate together
242
- * @connect agent.toolCalls -> tools.calls
243
- * @connect tools.results -> memory.input
244
- * @connect memory.history -> agent.context
245
- */
246
- ```
247
-
248
- The scope's output ports become callback parameters, and input ports become return values. Agent reasoning loops, ForEach over collections, map/reduce patterns, and nested sub-workflows all work without introducing graph cycles.
249
-
250
- ## Deploy Anywhere
251
-
252
- Same workflow source, multiple deployment targets. Export targets are provided by marketplace packs — install the ones you need:
253
-
254
- ```bash
255
- npm install @synergenius/flow-weaver-pack-lambda @synergenius/flow-weaver-pack-vercel \
256
- @synergenius/flow-weaver-pack-cloudflare @synergenius/flow-weaver-pack-inngest \
257
- @synergenius/flow-weaver-pack-github-actions @synergenius/flow-weaver-pack-gitlab-ci
258
- ```
259
-
260
- ```bash
261
- # Plain TypeScript (default)
262
- fw compile workflow.ts
263
-
264
- # Inngest durable functions (per-node step.run, retries, cron)
265
- fw compile workflow.ts --target inngest --retries 3 --cron "0 9 * * *"
266
-
267
- # Serverless exports
268
- fw export workflow.ts --target lambda --output deploy/
269
- fw export workflow.ts --target vercel --output deploy/
270
- fw export workflow.ts --target cloudflare --output deploy/
271
-
272
- # CI/CD pipelines
273
- fw export workflow.ts --target github-actions --output .github/workflows/
274
- fw export workflow.ts --target gitlab-ci --output .
275
-
276
- # HTTP server with OpenAPI docs
277
- fw serve ./workflows --port 3000 --swagger
278
- ```
279
-
280
- Both the default TypeScript target and Inngest target parallelize independent nodes with `Promise.all()`. Inngest additionally wraps each node in `step.run()` for individual durability and generates typed Zod event schemas.
281
-
282
- ## Diagram Generation
283
-
284
- Generate SVG or interactive HTML diagrams from any workflow:
285
-
286
- ```bash
287
- fw diagram workflow.ts -o workflow.svg --theme dark
288
- fw diagram workflow.ts -o workflow.html --format html
289
- ```
290
-
291
- Customize node appearance with annotations:
292
-
293
- ```typescript
294
- /**
295
- * @flowWeaver nodeType
296
- * @color blue
297
- * @icon database
298
- */
299
- ```
300
-
301
- Named colors: `blue`, `purple`, `green`, `cyan`, `orange`, `pink`, `red`, `yellow`. Icons include `api`, `database`, `shield`, `brain`, `cloud`, `search`, `code`, and 60+ more from Material Design 3. The interactive HTML viewer supports zoom/pan, click-to-inspect nodes, and port-level hover with connection tracing.
302
-
303
- ## API
304
-
305
- ```typescript
306
- import {
307
- parseWorkflow, // Parse workflow file to AST
308
- compileWorkflow, // Parse + validate + generate in one step
309
- validateWorkflow, // Validate AST (returns errors and warnings)
310
- generateCode, // Generate code from AST
311
- generateInPlace, // Regenerate only the compiled markers in-place
312
- } from '@synergenius/flow-weaver';
313
-
314
- // Full compilation
315
- const { code, ast, errors } = await compileWorkflow('workflow.ts');
316
-
317
- // Step by step
318
- const { ast } = await parseWorkflow('workflow.ts');
319
- const { errors, warnings } = validateWorkflow(ast);
320
- const code = generateCode(ast);
321
- ```
322
-
323
- ### Package Exports
324
-
325
- | Import Path | Purpose |
326
- |-------------|---------|
327
- | `@synergenius/flow-weaver` | Parse, validate, compile, generate, AST types, builders, diff, patterns |
328
- | `@synergenius/flow-weaver/runtime` | Execution context, errors, function registry for generated code |
329
- | `@synergenius/flow-weaver/testing` | Mock LLM/approval providers, recording/replay, assertions, token tracking |
330
- | `@synergenius/flow-weaver/built-in-nodes` | delay, waitForEvent, waitForAgent, invokeWorkflow |
331
- | `@synergenius/flow-weaver/diagram` | SVG/HTML diagram layout and rendering |
332
- | `@synergenius/flow-weaver/ast` | AST types and utilities |
333
- | `@synergenius/flow-weaver/api` | Programmatic workflow manipulation API |
334
- | `@synergenius/flow-weaver/diff` | Semantic workflow diffing |
335
- | `@synergenius/flow-weaver/deployment` | Deployment base classes, registry, and CI/CD utilities |
336
- | `@synergenius/flow-weaver/marketplace` | Marketplace package utilities |
337
- | `@synergenius/flow-weaver/editor` | Editor completions and suggestions |
338
- | `@synergenius/flow-weaver/browser` | JSDoc port sync for browser environments |
339
- | `@synergenius/flow-weaver/describe` | Programmatic workflow description |
340
- | `@synergenius/flow-weaver/doc-metadata` | Documentation metadata extractors |
341
-
342
- ## CLI Reference
343
-
344
- ```bash
345
- # Core
346
- fw compile <file> # Compile to TypeScript or Inngest
347
- fw validate <file> # Validate without compiling
348
- fw run <file> # Execute a workflow
349
- fw dev <file> # Watch + compile + run
350
- fw strip <file> # Remove generated code sections
351
- fw describe <file> # Structure output (json/text/mermaid)
352
- fw diagram <file> # Generate SVG or interactive HTML diagram
353
- fw diff <f1> <f2> # Semantic workflow comparison
354
-
355
- # Model-driven
356
- fw status <file> # Show stub vs implemented progress
357
- fw implement <file> <node> # Scaffold a node body from its stub
358
-
359
- # Scaffolding
360
- fw init [directory] # Create new project
361
- fw create workflow <t> <f> # Scaffold from template
362
- fw create node <name> <f> # Scaffold node type
363
- fw templates # List available templates
364
- fw doctor # Check project compatibility
365
- fw grammar # Output annotation grammar (EBNF/railroad)
366
-
367
- # Deploy
368
- fw serve [directory] # HTTP server with Swagger UI
369
- fw export <file> # Export to Lambda/Vercel/Cloudflare/Inngest/GitHub Actions/GitLab CI (via packs)
370
- fw openapi <directory> # Generate OpenAPI spec
371
-
372
- # Patterns
373
- fw pattern list <path> # List reusable patterns
374
- fw pattern apply <p> <t> # Apply pattern to workflow
375
- fw pattern extract <src> # Extract pattern from nodes
376
-
377
- # Docs
378
- fw docs # List documentation topics
379
- fw docs read <topic> # Read a topic
380
- fw docs search <query> # Search documentation
381
-
382
- # Marketplace
383
- fw market search [query] # Search npm for packages
384
- fw market install <pkg> # Install a package
385
- fw market list # List installed packages
386
- fw market init <name> # Scaffold a marketplace package
387
- fw market pack # Validate and generate manifest
388
- fw market publish # Publish to npm
389
-
390
- # Editor / IDE
391
- fw mcp-server # Start MCP server for Claude Code
392
- fw listen # Stream editor events
393
- fw changelog # Generate changelog from git history
394
- fw migrate <glob> # Run migration transforms on workflow files
395
- fw plugin init <name> # Scaffold an external plugin
396
- ```
397
-
398
- ## Built-in Nodes
399
-
400
- | Node | Purpose |
401
- |------|---------|
402
- | `delay` | Sleep for a duration (ms, s, m, h, d). Mockable for fast testing. |
403
- | `waitForEvent` | Wait for an external event with optional field matching and timeout. Maps to Inngest `step.waitForEvent()` for zero-cost durable pauses. |
404
- | `waitForAgent` | Pause execution and wait for an external agent to provide a result. Supports multi-step human-in-the-loop and agent delegation patterns. |
405
- | `invokeWorkflow` | Invoke another workflow by ID with payload and timeout. Maps to Inngest `step.invoke()`. |
406
-
407
- ## STEP Port Architecture
408
-
409
- Every node follows a consistent contract:
410
-
411
- ```typescript
412
- function nodeName(
413
- execute: boolean, // Control: should this node run?
414
- ...inputs // Data inputs (typed)
415
- ): {
416
- onSuccess: boolean; // Success control output
417
- onFailure: boolean; // Failure control output
418
- ...outputs // Data outputs (typed)
419
- }
420
- ```
421
-
422
- Expression nodes (`@expression`) skip the control flow boilerplate. Inputs and outputs map directly to the TypeScript signature.
423
-
424
- ## Flow Weaver Cloud
425
-
426
- [flowweaver.ai](https://flowweaver.ai) is the hosted platform that adds collaboration, managed deployments, and team features on top of everything the CLI provides.
427
-
428
- What the cloud adds:
429
-
430
- - **Browser-based Studio** with real-time collaborative editing
431
- - **One-click deployment** of any workflow as an HTTP endpoint
432
- - **Execution logs** for every workflow run, with input/output history
433
- - **Visual debugger** for step-through node inspection (Pro)
434
- - **Version history** and workflow snapshots (Pro)
435
- - **AI Chat assistant** for building and debugging workflows (Pro)
436
- - **Organization workspaces** with team management (Business)
437
-
438
- Three tiers: **Free** (3 workflows, 1 deployment, 100 executions/month), **Pro** at 9 EUR/month (25 workflows, 10k executions), and **Business** at 29 EUR/month (unlimited workflows, 100k executions). See [flowweaver.ai/pricing](https://flowweaver.ai/pricing) for details.
439
-
440
- The CLI remains fully functional for local development. The cloud adds the parts that are hard to do alone: team collaboration, always-on endpoints, and operational visibility.
441
-
442
- ## Community
443
-
444
- - [Discord](https://discord.gg/6Byh3ur2bk)
445
- - [GitHub Discussions](https://github.com/synergenius-fw/flow-weaver/discussions)
446
- - [X / Twitter](https://x.com/flowweaver_ai)
447
- - [Website](https://flowweaver.ai)
448
-
449
- ## Development
450
-
451
- ```bash
452
- npm run build # Build
453
- npm run watch # Watch mode
454
- npm test # Run all tests
455
- npm run test:watch # Watch mode
456
- npm run typecheck # Type check
457
- npm run docs # Generate API docs
458
- ```
115
+ ---
459
116
 
460
117
  ## License
461
118
 
462
- Licensed under the Flow Weaver Library License. See [LICENSE](./LICENSE) for full terms.
463
-
464
- - **Free to use**: install, run, and compile workflows in any organization
465
- - **Free to host internally** for organizations with 15 or fewer people
466
- - **Commercial license required** to host internally for 16+ people (contact support@synergenius.pt)
467
- - **External hosting prohibited** without a commercial license
468
- - **Output is unrestricted**: compiled workflows, generated code, and deployment artifacts are yours
119
+ Licensed under the [Flow Weaver License](https://flowweaver.ai/license). See [LICENSE.md](./LICENSE.md).
@@ -14,6 +14,8 @@ export interface DeviceInfo {
14
14
  projectDir: string;
15
15
  platform: string;
16
16
  capabilities: string[];
17
+ /** Pack names that contributed device handlers (for auto-install on platform) */
18
+ packs?: string[];
17
19
  }
18
20
  export interface DeviceConnectionOptions {
19
21
  platformUrl: string;
@@ -46,6 +48,11 @@ export declare class DeviceConnection {
46
48
  * Add a capability to advertise to the platform.
47
49
  */
48
50
  addCapability(capability: string): void;
51
+ /**
52
+ * Set the list of packs that contributed device handlers.
53
+ * The platform uses this to auto-install packs in the user's workspace.
54
+ */
55
+ setPacks(packs: string[]): void;
49
56
  /**
50
57
  * Register a handler for incoming requests from the platform.
51
58
  */
@@ -38,6 +38,13 @@ export class DeviceConnection {
38
38
  this.deviceInfo.capabilities.push(capability);
39
39
  }
40
40
  }
41
+ /**
42
+ * Set the list of packs that contributed device handlers.
43
+ * The platform uses this to auto-install packs in the user's workspace.
44
+ */
45
+ setPacks(packs) {
46
+ this.deviceInfo.packs = packs;
47
+ }
41
48
  /**
42
49
  * Register a handler for incoming requests from the platform.
43
50
  */
@@ -3,8 +3,9 @@ import { loadCredentials, saveCredentials, clearCredentials, getPlatformUrl } fr
3
3
  import { PlatformClient } from '../config/platform-client.js';
4
4
  export async function loginCommand(options) {
5
5
  const platformUrl = options.platformUrl ?? getPlatformUrl();
6
+ const displayUrl = platformUrl.replace(/^https?:\/\//, '');
6
7
  console.log('');
7
- console.log(' \x1b[1mFlow Weaver Cloud\x1b[0m \x1b[2m(flowweaver.ai)\x1b[0m');
8
+ console.log(` \x1b[1mFlow Weaver\x1b[0m \x1b[2m(${displayUrl})\x1b[0m`);
8
9
  console.log('');
9
10
  // API key mode (for CI/headless)
10
11
  if (options.apiKey) {
@@ -42,8 +43,8 @@ async function loginWithBrowser(platformUrl) {
42
43
  interval = data.interval ?? 5;
43
44
  }
44
45
  catch {
45
- console.error(' \x1b[31m✗\x1b[0m Cannot connect to flowweaver.ai');
46
- console.error(' Check your internet connection or set FW_PLATFORM_URL');
46
+ console.error(` \x1b[31m✗\x1b[0m Cannot connect to ${platformUrl}`);
47
+ console.error(' Check the URL or set FW_PLATFORM_URL');
47
48
  process.exit(1);
48
49
  return;
49
50
  }
@@ -65,20 +66,24 @@ async function loginWithBrowser(platformUrl) {
65
66
  }
66
67
  console.log('');
67
68
  // Step 3: Poll for completion
68
- process.stdout.write(' Waiting for authentication...');
69
+ process.stderr.write(' Waiting for approval');
69
70
  let cancelled = false;
70
71
  const sigHandler = () => { cancelled = true; };
71
72
  process.on('SIGINT', sigHandler);
72
73
  const maxAttempts = 120; // 10 minutes at 5s intervals
74
+ let networkErrors = 0;
73
75
  for (let i = 0; i < maxAttempts && !cancelled; i++) {
74
76
  await new Promise(r => setTimeout(r, interval * 1000));
75
77
  try {
76
78
  const resp = await fetch(`${platformUrl}/auth/device/poll?deviceCode=${deviceCode}`);
77
- if (!resp.ok)
79
+ networkErrors = 0; // reset on successful connection
80
+ if (!resp.ok) {
81
+ process.stderr.write('.');
78
82
  continue;
83
+ }
79
84
  const data = await resp.json();
80
85
  if (data.status === 'approved' && data.token && data.user) {
81
- process.stdout.write(' \x1b[32m✓\x1b[0m\n\n');
86
+ process.stderr.write(' \x1b[32m✓\x1b[0m\n\n');
82
87
  saveCredentials({
83
88
  token: data.token,
84
89
  email: data.user.email,
@@ -95,7 +100,7 @@ async function loginWithBrowser(platformUrl) {
95
100
  return;
96
101
  }
97
102
  if (data.status === 'expired') {
98
- process.stdout.write(' \x1b[31mtimed out\x1b[0m\n\n');
103
+ process.stderr.write(' \x1b[31mtimed out\x1b[0m\n\n');
99
104
  console.log(' Code expired. Run \x1b[36mfw login\x1b[0m again.');
100
105
  console.log('');
101
106
  process.removeListener('SIGINT', sigHandler);
@@ -103,27 +108,30 @@ async function loginWithBrowser(platformUrl) {
103
108
  return;
104
109
  }
105
110
  if (data.status === 'denied') {
106
- process.stdout.write(' \x1b[31mdenied\x1b[0m\n\n');
111
+ process.stderr.write(' \x1b[31mdenied\x1b[0m\n\n');
107
112
  console.log(' Access denied.');
108
113
  console.log('');
109
114
  process.removeListener('SIGINT', sigHandler);
110
115
  process.exit(1);
111
116
  return;
112
117
  }
113
- // Still pending — show a dot for progress
114
- if (i % 4 === 3)
115
- process.stdout.write('.');
118
+ // Still pending — show a dot every poll
119
+ process.stderr.write('.');
116
120
  }
117
121
  catch {
118
- // Network error — keep trying
122
+ networkErrors++;
123
+ if (networkErrors >= 3) {
124
+ process.stderr.write(' \x1b[33m!\x1b[0m');
125
+ networkErrors = 0;
126
+ }
119
127
  }
120
128
  }
121
129
  process.removeListener('SIGINT', sigHandler);
122
130
  if (cancelled) {
123
- process.stdout.write(' \x1b[33mcancelled\x1b[0m\n\n');
131
+ process.stderr.write(' \x1b[33mcancelled\x1b[0m\n\n');
124
132
  }
125
133
  else {
126
- process.stdout.write(' \x1b[31mtimed out\x1b[0m\n\n');
134
+ process.stderr.write(' \x1b[31mtimed out\x1b[0m\n\n');
127
135
  console.log(' Authentication timed out. Run \x1b[36mfw login\x1b[0m again.');
128
136
  console.log('');
129
137
  }
@@ -209,23 +217,24 @@ export async function authStatusCommand() {
209
217
  console.log('');
210
218
  }
211
219
  function prompt(message, hidden = false) {
212
- return new Promise((resolve) => {
213
- const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
214
- if (hidden && process.stdin.isTTY) {
215
- // Hide password input
220
+ if (hidden && process.stdin.isTTY) {
221
+ // Raw-mode password input — no readline (it competes for stdin)
222
+ return new Promise((resolve) => {
216
223
  process.stderr.write(message);
217
224
  process.stdin.setRawMode(true);
225
+ process.stdin.resume();
218
226
  let input = '';
219
227
  const handler = (key) => {
220
228
  const ch = key.toString();
221
229
  if (ch === '\r' || ch === '\n') {
222
230
  process.stdin.setRawMode(false);
231
+ process.stdin.pause();
223
232
  process.stdin.removeListener('data', handler);
224
233
  process.stderr.write('\n');
225
- rl.close();
226
234
  resolve(input);
227
235
  }
228
236
  else if (ch === '\x03') { // Ctrl+C
237
+ process.stdin.setRawMode(false);
229
238
  process.exit(1);
230
239
  }
231
240
  else if (ch === '\x7f') { // Backspace
@@ -236,10 +245,11 @@ function prompt(message, hidden = false) {
236
245
  }
237
246
  };
238
247
  process.stdin.on('data', handler);
239
- }
240
- else {
241
- rl.question(message, (answer) => { rl.close(); resolve(answer); });
242
- }
248
+ });
249
+ }
250
+ return new Promise((resolve) => {
251
+ const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
252
+ rl.question(message, (answer) => { rl.close(); resolve(answer); });
243
253
  });
244
254
  }
245
255
  //# sourceMappingURL=auth.js.map
@@ -1,2 +1,4 @@
1
+ import { DeviceConnection } from '../../agent/device-connection.js';
2
+ export declare function loadPackDeviceHandlers(conn: DeviceConnection, projectDir: string): Promise<string[]>;
1
3
  export declare function handleConnect(projectDir: string): Promise<void>;
2
4
  //# sourceMappingURL=connect.d.ts.map
@@ -1,7 +1,42 @@
1
1
  import * as fs from 'node:fs';
2
2
  import * as path from 'node:path';
3
3
  import * as os from 'node:os';
4
+ import * as readline from 'node:readline';
4
5
  import { DeviceConnection } from '../../agent/device-connection.js';
6
+ import { discoverDeviceHandlers } from '../../marketplace/registry.js';
7
+ function promptYesNo(message) {
8
+ return new Promise((resolve) => {
9
+ const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
10
+ rl.question(message, (answer) => {
11
+ rl.close();
12
+ const normalized = answer.trim().toLowerCase();
13
+ resolve(normalized === '' || normalized === 'y' || normalized === 'yes');
14
+ });
15
+ });
16
+ }
17
+ export async function loadPackDeviceHandlers(conn, projectDir) {
18
+ const loadedPacks = [];
19
+ try {
20
+ const handlers = await discoverDeviceHandlers(projectDir);
21
+ for (const handler of handlers) {
22
+ try {
23
+ const mod = await import(handler.entrypoint);
24
+ if (typeof mod.register === 'function') {
25
+ await mod.register(conn, { projectDir });
26
+ loadedPacks.push(handler.packageName);
27
+ process.stderr.write(` \x1b[2m+ ${handler.packageName} handlers\x1b[0m\n`);
28
+ }
29
+ }
30
+ catch (err) {
31
+ process.stderr.write(` \x1b[33m⚠\x1b[0m Failed to load handlers from ${handler.packageName}: ${err instanceof Error ? err.message : err}\n`);
32
+ }
33
+ }
34
+ }
35
+ catch {
36
+ // Discovery failed — non-fatal
37
+ }
38
+ return loadedPacks;
39
+ }
5
40
  export async function handleConnect(projectDir) {
6
41
  // Load credentials
7
42
  const credPath = path.join(os.homedir(), '.fw', 'credentials.json');
@@ -46,13 +81,72 @@ export async function handleConnect(projectDir) {
46
81
  const entries = fs.readdirSync(dirPath, { withFileTypes: true });
47
82
  return entries
48
83
  .filter(e => !e.name.startsWith('.') && e.name !== 'node_modules' && e.name !== 'dist')
49
- .map(e => ({ name: e.name, type: e.isDirectory() ? 'directory' : 'file', path: path.relative(projectDir, path.join(dirPath, e.name)) }));
84
+ .map(e => ({ name: e.name, type: e.isDirectory() ? 'directory' : 'file', path: path.relative(projectDir, path.join(dirPath, e.name)), hasUnfetchedChildren: e.isDirectory() }));
50
85
  });
86
+ // Load pack device handlers (if any installed packs provide them)
87
+ const loadedPacks = await loadPackDeviceHandlers(conn, projectDir);
88
+ // Tell the platform which packs contributed handlers (for auto-install)
89
+ if (loadedPacks.length > 0) {
90
+ conn.setPacks(loadedPacks);
91
+ }
51
92
  console.log('');
52
93
  console.log(' \x1b[1mflow-weaver connect\x1b[0m');
53
94
  console.log(` \x1b[2mProject: ${path.basename(projectDir)}\x1b[0m`);
54
95
  console.log(` \x1b[2mPlatform: ${creds.platformUrl}\x1b[0m`);
55
96
  console.log('');
97
+ // Check if packs need to be installed in the Studio workspace
98
+ if (loadedPacks.length > 0) {
99
+ try {
100
+ const checkRes = await fetch(`${creds.platformUrl}/devices/check-packs`, {
101
+ method: 'POST',
102
+ headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${creds.token}` },
103
+ body: JSON.stringify({ packs: loadedPacks }),
104
+ });
105
+ if (checkRes.ok) {
106
+ const { missing } = await checkRes.json();
107
+ if (missing.length > 0) {
108
+ console.log(' The following packs need to be installed in Studio:');
109
+ for (const p of missing) {
110
+ console.log(` \x1b[36m${p}\x1b[0m`);
111
+ }
112
+ console.log('');
113
+ const answer = await promptYesNo(' Install now? (Y/n) ');
114
+ if (answer) {
115
+ process.stderr.write(' Installing...');
116
+ const installRes = await fetch(`${creds.platformUrl}/devices/install-packs`, {
117
+ method: 'POST',
118
+ headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${creds.token}` },
119
+ body: JSON.stringify({ packs: missing }),
120
+ });
121
+ if (installRes.ok) {
122
+ const { results } = await installRes.json();
123
+ const allOk = results.every(r => r.ok);
124
+ if (allOk) {
125
+ process.stderr.write(' \x1b[32m✓\x1b[0m\n\n');
126
+ }
127
+ else {
128
+ process.stderr.write(' \x1b[33mpartial\x1b[0m\n');
129
+ for (const r of results) {
130
+ if (!r.ok)
131
+ console.log(` \x1b[31m✗\x1b[0m ${r.pack}: ${r.error}`);
132
+ }
133
+ console.log('');
134
+ }
135
+ }
136
+ else {
137
+ process.stderr.write(' \x1b[31mfailed\x1b[0m\n\n');
138
+ }
139
+ }
140
+ else {
141
+ console.log(' \x1b[2mSkipped. Install manually via Studio marketplace.\x1b[0m\n');
142
+ }
143
+ }
144
+ }
145
+ }
146
+ catch {
147
+ // Check failed — non-fatal, continue connecting
148
+ }
149
+ }
56
150
  try {
57
151
  await conn.connect();
58
152
  console.log(' \x1b[2mPress Ctrl+C to disconnect.\x1b[0m\n');
@@ -9886,7 +9886,7 @@ var VERSION;
9886
9886
  var init_generated_version = __esm({
9887
9887
  "src/generated-version.ts"() {
9888
9888
  "use strict";
9889
- VERSION = "0.23.1";
9889
+ VERSION = "0.23.3";
9890
9890
  }
9891
9891
  });
9892
9892
 
@@ -35517,6 +35517,7 @@ var init_tag_registry = __esm({
35517
35517
  // src/marketplace/registry.ts
35518
35518
  var registry_exports = {};
35519
35519
  __export(registry_exports, {
35520
+ discoverDeviceHandlers: () => discoverDeviceHandlers,
35520
35521
  discoverDocTopics: () => discoverDocTopics,
35521
35522
  discoverInitContributions: () => discoverInitContributions,
35522
35523
  discoverTagHandlers: () => discoverTagHandlers,
@@ -35655,6 +35656,20 @@ async function discoverInitContributions(projectDir) {
35655
35656
  }
35656
35657
  return contributions;
35657
35658
  }
35659
+ async function discoverDeviceHandlers(projectDir) {
35660
+ const packages = await listInstalledPackages(projectDir);
35661
+ const handlers = [];
35662
+ for (const pkg of packages) {
35663
+ const manifest = pkg.manifest;
35664
+ if (!manifest.deviceHandlers) continue;
35665
+ handlers.push({
35666
+ packageName: pkg.name,
35667
+ packagePath: pkg.path,
35668
+ entrypoint: path5.join(pkg.path, manifest.deviceHandlers)
35669
+ });
35670
+ }
35671
+ return handlers;
35672
+ }
35658
35673
  var MARKETPLACE_KEYWORD, NPM_SEARCH_URL, PACK_NAME_RE;
35659
35674
  var init_registry = __esm({
35660
35675
  "src/marketplace/registry.ts"() {
@@ -95201,8 +95216,9 @@ __export(auth_exports, {
95201
95216
  import * as readline9 from "node:readline";
95202
95217
  async function loginCommand(options) {
95203
95218
  const platformUrl = options.platformUrl ?? getPlatformUrl();
95219
+ const displayUrl = platformUrl.replace(/^https?:\/\//, "");
95204
95220
  console.log("");
95205
- console.log(" \x1B[1mFlow Weaver Cloud\x1B[0m \x1B[2m(flowweaver.ai)\x1B[0m");
95221
+ console.log(` \x1B[1mFlow Weaver\x1B[0m \x1B[2m(${displayUrl})\x1B[0m`);
95206
95222
  console.log("");
95207
95223
  if (options.apiKey) {
95208
95224
  await loginWithApiKey(options.apiKey, platformUrl);
@@ -95234,8 +95250,8 @@ async function loginWithBrowser(platformUrl) {
95234
95250
  verificationUrl = data.verificationUrl;
95235
95251
  interval = data.interval ?? 5;
95236
95252
  } catch {
95237
- console.error(" \x1B[31m\u2717\x1B[0m Cannot connect to flowweaver.ai");
95238
- console.error(" Check your internet connection or set FW_PLATFORM_URL");
95253
+ console.error(` \x1B[31m\u2717\x1B[0m Cannot connect to ${platformUrl}`);
95254
+ console.error(" Check the URL or set FW_PLATFORM_URL");
95239
95255
  process.exit(1);
95240
95256
  return;
95241
95257
  }
@@ -95252,21 +95268,26 @@ async function loginWithBrowser(platformUrl) {
95252
95268
  console.log(` \x1B[36m${authUrl}\x1B[0m`);
95253
95269
  }
95254
95270
  console.log("");
95255
- process.stdout.write(" Waiting for authentication...");
95271
+ process.stderr.write(" Waiting for approval");
95256
95272
  let cancelled = false;
95257
95273
  const sigHandler = () => {
95258
95274
  cancelled = true;
95259
95275
  };
95260
95276
  process.on("SIGINT", sigHandler);
95261
95277
  const maxAttempts = 120;
95278
+ let networkErrors = 0;
95262
95279
  for (let i = 0; i < maxAttempts && !cancelled; i++) {
95263
95280
  await new Promise((r) => setTimeout(r, interval * 1e3));
95264
95281
  try {
95265
95282
  const resp = await fetch(`${platformUrl}/auth/device/poll?deviceCode=${deviceCode}`);
95266
- if (!resp.ok) continue;
95283
+ networkErrors = 0;
95284
+ if (!resp.ok) {
95285
+ process.stderr.write(".");
95286
+ continue;
95287
+ }
95267
95288
  const data = await resp.json();
95268
95289
  if (data.status === "approved" && data.token && data.user) {
95269
- process.stdout.write(" \x1B[32m\u2713\x1B[0m\n\n");
95290
+ process.stderr.write(" \x1B[32m\u2713\x1B[0m\n\n");
95270
95291
  saveCredentials({
95271
95292
  token: data.token,
95272
95293
  email: data.user.email,
@@ -95283,7 +95304,7 @@ async function loginWithBrowser(platformUrl) {
95283
95304
  return;
95284
95305
  }
95285
95306
  if (data.status === "expired") {
95286
- process.stdout.write(" \x1B[31mtimed out\x1B[0m\n\n");
95307
+ process.stderr.write(" \x1B[31mtimed out\x1B[0m\n\n");
95287
95308
  console.log(" Code expired. Run \x1B[36mfw login\x1B[0m again.");
95288
95309
  console.log("");
95289
95310
  process.removeListener("SIGINT", sigHandler);
@@ -95291,22 +95312,27 @@ async function loginWithBrowser(platformUrl) {
95291
95312
  return;
95292
95313
  }
95293
95314
  if (data.status === "denied") {
95294
- process.stdout.write(" \x1B[31mdenied\x1B[0m\n\n");
95315
+ process.stderr.write(" \x1B[31mdenied\x1B[0m\n\n");
95295
95316
  console.log(" Access denied.");
95296
95317
  console.log("");
95297
95318
  process.removeListener("SIGINT", sigHandler);
95298
95319
  process.exit(1);
95299
95320
  return;
95300
95321
  }
95301
- if (i % 4 === 3) process.stdout.write(".");
95322
+ process.stderr.write(".");
95302
95323
  } catch {
95324
+ networkErrors++;
95325
+ if (networkErrors >= 3) {
95326
+ process.stderr.write(" \x1B[33m!\x1B[0m");
95327
+ networkErrors = 0;
95328
+ }
95303
95329
  }
95304
95330
  }
95305
95331
  process.removeListener("SIGINT", sigHandler);
95306
95332
  if (cancelled) {
95307
- process.stdout.write(" \x1B[33mcancelled\x1B[0m\n\n");
95333
+ process.stderr.write(" \x1B[33mcancelled\x1B[0m\n\n");
95308
95334
  } else {
95309
- process.stdout.write(" \x1B[31mtimed out\x1B[0m\n\n");
95335
+ process.stderr.write(" \x1B[31mtimed out\x1B[0m\n\n");
95310
95336
  console.log(" Authentication timed out. Run \x1B[36mfw login\x1B[0m again.");
95311
95337
  console.log("");
95312
95338
  }
@@ -95392,21 +95418,22 @@ async function authStatusCommand() {
95392
95418
  console.log("");
95393
95419
  }
95394
95420
  function prompt(message, hidden = false) {
95395
- return new Promise((resolve38) => {
95396
- const rl = readline9.createInterface({ input: process.stdin, output: process.stderr });
95397
- if (hidden && process.stdin.isTTY) {
95421
+ if (hidden && process.stdin.isTTY) {
95422
+ return new Promise((resolve38) => {
95398
95423
  process.stderr.write(message);
95399
95424
  process.stdin.setRawMode(true);
95425
+ process.stdin.resume();
95400
95426
  let input = "";
95401
95427
  const handler = (key) => {
95402
95428
  const ch = key.toString();
95403
95429
  if (ch === "\r" || ch === "\n") {
95404
95430
  process.stdin.setRawMode(false);
95431
+ process.stdin.pause();
95405
95432
  process.stdin.removeListener("data", handler);
95406
95433
  process.stderr.write("\n");
95407
- rl.close();
95408
95434
  resolve38(input);
95409
95435
  } else if (ch === "") {
95436
+ process.stdin.setRawMode(false);
95410
95437
  process.exit(1);
95411
95438
  } else if (ch === "\x7F") {
95412
95439
  input = input.slice(0, -1);
@@ -95415,12 +95442,14 @@ function prompt(message, hidden = false) {
95415
95442
  }
95416
95443
  };
95417
95444
  process.stdin.on("data", handler);
95418
- } else {
95419
- rl.question(message, (answer) => {
95420
- rl.close();
95421
- resolve38(answer);
95422
- });
95423
- }
95445
+ });
95446
+ }
95447
+ return new Promise((resolve38) => {
95448
+ const rl = readline9.createInterface({ input: process.stdin, output: process.stderr });
95449
+ rl.question(message, (answer) => {
95450
+ rl.close();
95451
+ resolve38(answer);
95452
+ });
95424
95453
  });
95425
95454
  }
95426
95455
  var init_auth = __esm({
@@ -95572,6 +95601,13 @@ var init_device_connection = __esm({
95572
95601
  this.deviceInfo.capabilities.push(capability);
95573
95602
  }
95574
95603
  }
95604
+ /**
95605
+ * Set the list of packs that contributed device handlers.
95606
+ * The platform uses this to auto-install packs in the user's workspace.
95607
+ */
95608
+ setPacks(packs) {
95609
+ this.deviceInfo.packs = packs;
95610
+ }
95575
95611
  /**
95576
95612
  * Register a handler for incoming requests from the platform.
95577
95613
  */
@@ -95700,11 +95736,45 @@ var init_device_connection = __esm({
95700
95736
  // src/cli/commands/connect.ts
95701
95737
  var connect_exports = {};
95702
95738
  __export(connect_exports, {
95703
- handleConnect: () => handleConnect
95739
+ handleConnect: () => handleConnect,
95740
+ loadPackDeviceHandlers: () => loadPackDeviceHandlers
95704
95741
  });
95705
95742
  import * as fs49 from "node:fs";
95706
95743
  import * as path52 from "node:path";
95707
95744
  import * as os4 from "node:os";
95745
+ import * as readline10 from "node:readline";
95746
+ function promptYesNo(message) {
95747
+ return new Promise((resolve38) => {
95748
+ const rl = readline10.createInterface({ input: process.stdin, output: process.stderr });
95749
+ rl.question(message, (answer) => {
95750
+ rl.close();
95751
+ const normalized = answer.trim().toLowerCase();
95752
+ resolve38(normalized === "" || normalized === "y" || normalized === "yes");
95753
+ });
95754
+ });
95755
+ }
95756
+ async function loadPackDeviceHandlers(conn, projectDir) {
95757
+ const loadedPacks = [];
95758
+ try {
95759
+ const handlers = await discoverDeviceHandlers(projectDir);
95760
+ for (const handler of handlers) {
95761
+ try {
95762
+ const mod = await import(handler.entrypoint);
95763
+ if (typeof mod.register === "function") {
95764
+ await mod.register(conn, { projectDir });
95765
+ loadedPacks.push(handler.packageName);
95766
+ process.stderr.write(` \x1B[2m+ ${handler.packageName} handlers\x1B[0m
95767
+ `);
95768
+ }
95769
+ } catch (err) {
95770
+ process.stderr.write(` \x1B[33m\u26A0\x1B[0m Failed to load handlers from ${handler.packageName}: ${err instanceof Error ? err.message : err}
95771
+ `);
95772
+ }
95773
+ }
95774
+ } catch {
95775
+ }
95776
+ return loadedPacks;
95777
+ }
95708
95778
  async function handleConnect(projectDir) {
95709
95779
  const credPath = path52.join(os4.homedir(), ".fw", "credentials.json");
95710
95780
  if (!fs49.existsSync(credPath)) {
@@ -95740,13 +95810,63 @@ async function handleConnect(projectDir) {
95740
95810
  if (!dirPath.startsWith(projectDir)) throw new Error("Path outside project directory");
95741
95811
  if (!fs49.existsSync(dirPath)) throw new Error("Directory not found");
95742
95812
  const entries = fs49.readdirSync(dirPath, { withFileTypes: true });
95743
- return entries.filter((e) => !e.name.startsWith(".") && e.name !== "node_modules" && e.name !== "dist").map((e) => ({ name: e.name, type: e.isDirectory() ? "directory" : "file", path: path52.relative(projectDir, path52.join(dirPath, e.name)) }));
95813
+ return entries.filter((e) => !e.name.startsWith(".") && e.name !== "node_modules" && e.name !== "dist").map((e) => ({ name: e.name, type: e.isDirectory() ? "directory" : "file", path: path52.relative(projectDir, path52.join(dirPath, e.name)), hasUnfetchedChildren: e.isDirectory() }));
95744
95814
  });
95815
+ const loadedPacks = await loadPackDeviceHandlers(conn, projectDir);
95816
+ if (loadedPacks.length > 0) {
95817
+ conn.setPacks(loadedPacks);
95818
+ }
95745
95819
  console.log("");
95746
95820
  console.log(" \x1B[1mflow-weaver connect\x1B[0m");
95747
95821
  console.log(` \x1B[2mProject: ${path52.basename(projectDir)}\x1B[0m`);
95748
95822
  console.log(` \x1B[2mPlatform: ${creds.platformUrl}\x1B[0m`);
95749
95823
  console.log("");
95824
+ if (loadedPacks.length > 0) {
95825
+ try {
95826
+ const checkRes = await fetch(`${creds.platformUrl}/devices/check-packs`, {
95827
+ method: "POST",
95828
+ headers: { "Content-Type": "application/json", "Authorization": `Bearer ${creds.token}` },
95829
+ body: JSON.stringify({ packs: loadedPacks })
95830
+ });
95831
+ if (checkRes.ok) {
95832
+ const { missing } = await checkRes.json();
95833
+ if (missing.length > 0) {
95834
+ console.log(" The following packs need to be installed in Studio:");
95835
+ for (const p of missing) {
95836
+ console.log(` \x1B[36m${p}\x1B[0m`);
95837
+ }
95838
+ console.log("");
95839
+ const answer = await promptYesNo(" Install now? (Y/n) ");
95840
+ if (answer) {
95841
+ process.stderr.write(" Installing...");
95842
+ const installRes = await fetch(`${creds.platformUrl}/devices/install-packs`, {
95843
+ method: "POST",
95844
+ headers: { "Content-Type": "application/json", "Authorization": `Bearer ${creds.token}` },
95845
+ body: JSON.stringify({ packs: missing })
95846
+ });
95847
+ if (installRes.ok) {
95848
+ const { results } = await installRes.json();
95849
+ const allOk = results.every((r) => r.ok);
95850
+ if (allOk) {
95851
+ process.stderr.write(" \x1B[32m\u2713\x1B[0m\n\n");
95852
+ } else {
95853
+ process.stderr.write(" \x1B[33mpartial\x1B[0m\n");
95854
+ for (const r of results) {
95855
+ if (!r.ok) console.log(` \x1B[31m\u2717\x1B[0m ${r.pack}: ${r.error}`);
95856
+ }
95857
+ console.log("");
95858
+ }
95859
+ } else {
95860
+ process.stderr.write(" \x1B[31mfailed\x1B[0m\n\n");
95861
+ }
95862
+ } else {
95863
+ console.log(" \x1B[2mSkipped. Install manually via Studio marketplace.\x1B[0m\n");
95864
+ }
95865
+ }
95866
+ }
95867
+ } catch {
95868
+ }
95869
+ }
95750
95870
  try {
95751
95871
  await conn.connect();
95752
95872
  console.log(" \x1B[2mPress Ctrl+C to disconnect.\x1B[0m\n");
@@ -95770,6 +95890,7 @@ var init_connect = __esm({
95770
95890
  "src/cli/commands/connect.ts"() {
95771
95891
  "use strict";
95772
95892
  init_device_connection();
95893
+ init_registry();
95773
95894
  }
95774
95895
  });
95775
95896
 
@@ -95802,7 +95923,7 @@ var {
95802
95923
  // src/cli/index.ts
95803
95924
  init_logger();
95804
95925
  init_error_utils();
95805
- var version2 = true ? "0.23.1" : "0.0.0-dev";
95926
+ var version2 = true ? "0.23.3" : "0.0.0-dev";
95806
95927
  var program2 = new Command();
95807
95928
  program2.name("fw").description("Flow Weaver Annotations - Compile and validate workflow files").option("-v, --version", "Output the current version").option("--no-color", "Disable colors").option("--color", "Force colors").on("option:version", () => {
95808
95929
  logger.banner(version2);
@@ -1,2 +1,2 @@
1
- export declare const VERSION = "0.23.1";
1
+ export declare const VERSION = "0.23.3";
2
2
  //# sourceMappingURL=generated-version.d.ts.map
@@ -1,3 +1,3 @@
1
1
  // Auto-generated by scripts/generate-version.ts — do not edit manually
2
- export const VERSION = '0.23.1';
2
+ export const VERSION = '0.23.3';
3
3
  //# sourceMappingURL=generated-version.js.map
@@ -7,5 +7,5 @@
7
7
  export type { TMarketplaceManifest, TManifestNodeType, TManifestWorkflow, TManifestPattern, TManifestExportTarget, TManifestPort, TValidationIssue, TValidationSeverity, TPackageValidationResult, TMarketplacePackageInfo, TInstalledPackage, TMarketInitConfig, TManifestTagHandler, TManifestValidationRuleSet, TManifestDocTopic, TManifestInitContribution, TManifestCliCommand, TManifestMcpTool, } from './types.js';
8
8
  export { generateManifest, writeManifest, readManifest, type GenerateManifestOptions, type GenerateManifestResult, } from './manifest.js';
9
9
  export { validatePackage } from './validator.js';
10
- export { searchPackages, listInstalledPackages, getInstalledPackageManifest, discoverTagHandlers, discoverValidationRuleSets, discoverDocTopics, discoverInitContributions, type SearchOptions, type TDiscoveredTagHandler, type TDiscoveredValidationRuleSet, type TDiscoveredDocTopic, type TDiscoveredInitContribution, } from './registry.js';
10
+ export { searchPackages, listInstalledPackages, getInstalledPackageManifest, discoverTagHandlers, discoverValidationRuleSets, discoverDocTopics, discoverInitContributions, discoverDeviceHandlers, type SearchOptions, type TDiscoveredTagHandler, type TDiscoveredValidationRuleSet, type TDiscoveredDocTopic, type TDiscoveredInitContribution, type TDiscoveredDeviceHandler, } from './registry.js';
11
11
  //# sourceMappingURL=index.d.ts.map
@@ -6,5 +6,5 @@
6
6
  */
7
7
  export { generateManifest, writeManifest, readManifest, } from './manifest.js';
8
8
  export { validatePackage } from './validator.js';
9
- export { searchPackages, listInstalledPackages, getInstalledPackageManifest, discoverTagHandlers, discoverValidationRuleSets, discoverDocTopics, discoverInitContributions, } from './registry.js';
9
+ export { searchPackages, listInstalledPackages, getInstalledPackageManifest, discoverTagHandlers, discoverValidationRuleSets, discoverDocTopics, discoverInitContributions, discoverDeviceHandlers, } from './registry.js';
10
10
  //# sourceMappingURL=index.js.map
@@ -49,6 +49,15 @@ export type TDiscoveredInitContribution = TManifestInitContribution & {
49
49
  /** Package name this contribution belongs to */
50
50
  packageName: string;
51
51
  };
52
+ /** A device handler entry point discovered from an installed pack manifest. */
53
+ export type TDiscoveredDeviceHandler = {
54
+ /** npm package name */
55
+ packageName: string;
56
+ /** Absolute path to the installed package */
57
+ packagePath: string;
58
+ /** Absolute path to the device handler entrypoint module */
59
+ entrypoint: string;
60
+ };
52
61
  /**
53
62
  * Discover all tag handlers from installed pack manifests.
54
63
  */
@@ -65,4 +74,11 @@ export declare function discoverDocTopics(projectDir: string): Promise<TDiscover
65
74
  * Discover all init contributions from installed pack manifests.
66
75
  */
67
76
  export declare function discoverInitContributions(projectDir: string): Promise<TDiscoveredInitContribution[]>;
77
+ /**
78
+ * Discover all device handler entry points from installed pack manifests.
79
+ *
80
+ * Returns packs that declare a `deviceHandlers` field in their manifest,
81
+ * resolved to an absolute entrypoint path.
82
+ */
83
+ export declare function discoverDeviceHandlers(projectDir: string): Promise<TDiscoveredDeviceHandler[]>;
68
84
  //# sourceMappingURL=registry.d.ts.map
@@ -174,4 +174,25 @@ export async function discoverInitContributions(projectDir) {
174
174
  }
175
175
  return contributions;
176
176
  }
177
+ /**
178
+ * Discover all device handler entry points from installed pack manifests.
179
+ *
180
+ * Returns packs that declare a `deviceHandlers` field in their manifest,
181
+ * resolved to an absolute entrypoint path.
182
+ */
183
+ export async function discoverDeviceHandlers(projectDir) {
184
+ const packages = await listInstalledPackages(projectDir);
185
+ const handlers = [];
186
+ for (const pkg of packages) {
187
+ const manifest = pkg.manifest;
188
+ if (!manifest.deviceHandlers)
189
+ continue;
190
+ handlers.push({
191
+ packageName: pkg.name,
192
+ packagePath: pkg.path,
193
+ entrypoint: path.join(pkg.path, manifest.deviceHandlers),
194
+ });
195
+ }
196
+ return handlers;
197
+ }
177
198
  //# sourceMappingURL=registry.js.map
@@ -44,6 +44,8 @@ export type TMarketplaceManifest = {
44
44
  mcpEntrypoint?: string;
45
45
  /** MCP tools contributed by this pack */
46
46
  mcpTools?: TManifestMcpTool[];
47
+ /** Entry point for device connection handler registration */
48
+ deviceHandlers?: string;
47
49
  /** External dependency information */
48
50
  dependencies?: {
49
51
  /** Flow Weaver peer dependency constraints */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@synergenius/flow-weaver",
3
- "version": "0.23.1",
3
+ "version": "0.23.3",
4
4
  "description": "Deterministic workflow compiler for AI agents. Compiles to standalone TypeScript, no runtime dependencies.",
5
5
  "private": false,
6
6
  "type": "module",
@@ -129,7 +129,7 @@
129
129
  "typecheck": "tsc --noEmit -p tsconfig.build.json",
130
130
  "docs": "typedoc && tsx scripts/generate-grammar-docs.ts",
131
131
  "docs:serve": "npm run docs && npx http-server docs/api -c-1 -o",
132
- "prepare": "npm run build",
132
+ "prepare": "npm run build && husky || true",
133
133
  "cli": "tsx src/cli/index.ts",
134
134
  "diagram": "tsx scripts/generate-diagram.ts"
135
135
  },
@@ -179,6 +179,7 @@
179
179
  "@types/react": "^19.0.0",
180
180
  "@types/ws": "^8.18.1",
181
181
  "@vitest/coverage-v8": "^4.0.18",
182
+ "husky": "^9.1.7",
182
183
  "prettier": "^3.1.1",
183
184
  "rimraf": "6.1.2",
184
185
  "ts-node": "^10.9.2",