@synergenius/flow-weaver 0.10.3 → 0.10.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.
@@ -53974,7 +53974,7 @@ function buildCoerceSuggestion(quoted, targetType) {
53974
53974
  if (portRefs.length < 2) return null;
53975
53975
  const coerceType = COERCE_TARGET_TYPES[targetType.toUpperCase()] || targetType.toLowerCase();
53976
53976
  if (!["string", "number", "boolean", "json", "object"].includes(coerceType)) return null;
53977
- return `@coerce c1 ${portRefs[0]} -> ${portRefs[1]} as ${coerceType}`;
53977
+ return `@connect ${portRefs[0]} -> ${portRefs[1]} as ${coerceType}`;
53978
53978
  }
53979
53979
  function extractCyclePath(message) {
53980
53980
  const match2 = message.match(/:\s*(.+ -> .+)/);
@@ -54083,7 +54083,7 @@ var errorMappers = {
54083
54083
  return {
54084
54084
  title: "Type Mismatch",
54085
54085
  explanation: `Type mismatch: you're connecting a ${source} to a ${target}. The value will be automatically converted, but this might cause unexpected behavior.`,
54086
- fix: coerceSuggestion ? `Add an explicit coercion: \`${coerceSuggestion}\`, change one of the port types, or use @strictTypes to turn this into an error.` : `Add a @coerce annotation between the two ports, change one of the port types, or use @strictTypes to turn this into an error.`,
54086
+ fix: coerceSuggestion ? `Use \`${coerceSuggestion}\` for explicit coercion, change one of the port types, or use @strictTypes to turn this into an error.` : `Add \`as <type>\` to the @connect annotation (e.g. \`as string\`), change one of the port types, or use @strictTypes to turn this into an error.`,
54087
54087
  code: error2.code
54088
54088
  };
54089
54089
  },
@@ -54182,7 +54182,7 @@ var errorMappers = {
54182
54182
  return {
54183
54183
  title: "Type Incompatible",
54184
54184
  explanation: `Type mismatch: ${source} to ${target}. With @strictTypes enabled, this is an error instead of a warning.`,
54185
- fix: coerceSuggestion ? `Add an explicit coercion: \`${coerceSuggestion}\`, change one of the port types, or remove @strictTypes to allow implicit coercions.` : `Add a @coerce annotation between the ports, change one of the port types, or remove @strictTypes to allow implicit coercions.`,
54185
+ fix: coerceSuggestion ? `Use \`${coerceSuggestion}\` for explicit coercion, change one of the port types, or remove @strictTypes to allow implicit coercions.` : `Add \`as <type>\` to the @connect annotation (e.g. \`as number\`), change one of the port types, or remove @strictTypes to allow implicit coercions.`,
54186
54186
  code: error2.code
54187
54187
  };
54188
54188
  },
@@ -54195,7 +54195,7 @@ var errorMappers = {
54195
54195
  return {
54196
54196
  title: "Unusual Type Coercion",
54197
54197
  explanation: `Converting ${source} to ${target} is technically valid but semantically unusual and may produce unexpected behavior.`,
54198
- fix: coerceSuggestion ? `If intentional, add an explicit coercion: \`${coerceSuggestion}\`, or use @strictTypes to enforce type safety.` : `If intentional, add an explicit @coerce annotation, or use @strictTypes to enforce type safety.`,
54198
+ fix: coerceSuggestion ? `If intentional, use \`${coerceSuggestion}\` for explicit coercion, or use @strictTypes to enforce type safety.` : `If intentional, add \`as <type>\` to the @connect annotation, or use @strictTypes to enforce type safety.`,
54199
54199
  code: error2.code
54200
54200
  };
54201
54201
  },
@@ -54464,7 +54464,7 @@ var errorMappers = {
54464
54464
  return {
54465
54465
  title: "Lossy Type Conversion",
54466
54466
  explanation: `Converting ${source} to ${target} may lose data or produce unexpected results (e.g., NaN, truncation).`,
54467
- fix: coerceSuggestion ? `Add an explicit coercion: \`${coerceSuggestion}\`, or use @strictTypes to enforce type safety.` : `Add an explicit conversion with @coerce, or use @strictTypes to enforce type safety.`,
54467
+ fix: coerceSuggestion ? `Use \`${coerceSuggestion}\` for explicit coercion, or use @strictTypes to enforce type safety.` : `Add \`as <type>\` to the @connect annotation for explicit conversion, or use @strictTypes to enforce type safety.`,
54468
54468
  code: error2.code
54469
54469
  };
54470
54470
  },
@@ -61588,24 +61588,46 @@ path[data-source].port-hover { opacity: 1; }
61588
61588
  /* Info panel */
61589
61589
  #info-panel {
61590
61590
  position: fixed; bottom: 52px; left: 16px;
61591
- max-width: 320px; min-width: 200px;
61591
+ max-width: 480px; min-width: 260px;
61592
61592
  background: ${surfaceMain}; border: 1px solid ${borderSubtle};
61593
61593
  border-radius: 8px; padding: 12px 16px;
61594
61594
  font-size: 13px; line-height: 1.5;
61595
61595
  box-shadow: 0 2px 8px rgba(0,0,0,0.2);
61596
61596
  z-index: 10; display: none;
61597
+ max-height: calc(100vh - 120px); overflow-y: auto;
61597
61598
  }
61598
61599
  #info-panel.visible { display: block; }
61599
61600
  #info-panel h3 {
61600
61601
  font-size: 14px; font-weight: 700; margin-bottom: 6px;
61601
61602
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
61602
61603
  }
61604
+ #info-panel .node-desc { color: ${textMed}; font-size: 12px; margin-bottom: 8px; font-style: italic; }
61603
61605
  #info-panel .info-section { margin-bottom: 6px; }
61604
61606
  #info-panel .info-label { font-size: 11px; font-weight: 600; color: ${textLow}; text-transform: uppercase; letter-spacing: 0.5px; }
61605
61607
  #info-panel .info-value { color: ${textMed}; }
61606
61608
  #info-panel .port-list { list-style: none; padding: 0; }
61607
61609
  #info-panel .port-list li { padding: 1px 0; }
61608
61610
  #info-panel .port-list li::before { content: '\\2022'; margin-right: 6px; color: ${textLow}; }
61611
+ #info-panel .port-type { color: ${textLow}; font-size: 11px; margin-left: 4px; font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace; }
61612
+ #info-panel pre {
61613
+ background: ${isDark ? "#161625" : "#f0f1fa"}; border: 1px solid ${borderSubtle};
61614
+ border-radius: 6px; padding: 10px; overflow-x: auto;
61615
+ font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
61616
+ font-size: 12px; line-height: 1.6; white-space: pre;
61617
+ max-height: 300px; overflow-y: auto; margin: 6px 0 0;
61618
+ color: ${isDark ? "#e6edf3" : "#1a2340"};
61619
+ }
61620
+ .hl-kw { color: ${isDark ? "#8e9eff" : "#4040bf"}; }
61621
+ .hl-str { color: ${isDark ? "#ff7b72" : "#c4432b"}; }
61622
+ .hl-num { color: ${isDark ? "#f0a050" : "#b35e14"}; }
61623
+ .hl-cm { color: #6a737d; font-style: italic; }
61624
+ .hl-fn { color: ${isDark ? "#d2a8ff" : "#7c3aed"}; }
61625
+ .hl-ty { color: ${isDark ? "#d2a8ff" : "#7c3aed"}; }
61626
+ .hl-pn { color: ${isDark ? "#b8bdd0" : "#4a5578"}; }
61627
+ .hl-ann { color: ${isDark ? "#8e9eff" : "#4040bf"}; font-weight: 600; font-style: normal; }
61628
+ .hl-arr { color: ${isDark ? "#79c0ff" : "#0969da"}; font-weight: 600; font-style: normal; }
61629
+ .hl-id { color: ${isDark ? "#e6edf3" : "#1a2340"}; font-style: normal; }
61630
+ .hl-sc { color: ${isDark ? "#d2a8ff" : "#7c3aed"}; font-style: italic; }
61609
61631
 
61610
61632
  /* Branding badge */
61611
61633
  #branding {
@@ -61660,8 +61682,8 @@ path[data-source].port-hover { opacity: 1; }
61660
61682
  <circle cx="10" cy="10" r="1.5" fill="${dotColor}" opacity="0.6"/>
61661
61683
  </pattern>
61662
61684
  </defs>
61663
- <rect x="-100000" y="-100000" width="200000" height="200000" fill="${bg}"/>
61664
- <rect x="-100000" y="-100000" width="200000" height="200000" fill="url(#viewer-dots)"/>
61685
+ <rect x="-100000" y="-100000" width="200000" height="200000" fill="${bg}" pointer-events="none"/>
61686
+ <rect x="-100000" y="-100000" width="200000" height="200000" fill="url(#viewer-dots)" pointer-events="none"/>
61665
61687
  <g id="diagram">${inner}</g>
61666
61688
  </svg>
61667
61689
  <div id="controls">
@@ -61691,6 +61713,7 @@ path[data-source].port-hover { opacity: 1; }
61691
61713
  </a>
61692
61714
  <div id="scroll-hint">Use <kbd id="mod-key">Ctrl</kbd> + scroll to zoom</div>
61693
61715
  <div id="studio-hint">Like rearranging? <a href="https://flowweaver.ai" target="_blank" rel="noopener">Flow Weaver Studio</a> saves your layouts.</div>
61716
+ <script>var nodeSources = ${JSON.stringify(options.nodeSources ?? {})};</script>
61694
61717
  <script>
61695
61718
  (function() {
61696
61719
  'use strict';
@@ -61775,6 +61798,7 @@ path[data-source].port-hover { opacity: 1; }
61775
61798
 
61776
61799
  // ---- Pan (drag) + Node drag ----
61777
61800
  var draggedNodeId = null, dragNodeStart = null, didDragNode = false;
61801
+ var clickTarget = null; // stash the real target before setPointerCapture steals it
61778
61802
  var dragCount = 0, nudgeIndex = 0, nudgeTimer = null;
61779
61803
  var nudgeMessages = [
61780
61804
  'Like rearranging? <a href="https://flowweaver.ai" target="_blank" rel="noopener">Flow Weaver Studio</a> saves your layouts.',
@@ -61789,6 +61813,8 @@ path[data-source].port-hover { opacity: 1; }
61789
61813
 
61790
61814
  canvas.addEventListener('pointerdown', function(e) {
61791
61815
  if (e.button !== 0) return;
61816
+ clickTarget = e.target; // stash before setPointerCapture redirects events
61817
+ didDrag = false;
61792
61818
  // Check if clicking on a node body (walk up to detect data-node-id)
61793
61819
  var t = e.target;
61794
61820
  while (t && t !== canvas) {
@@ -61805,7 +61831,6 @@ path[data-source].port-hover { opacity: 1; }
61805
61831
  }
61806
61832
  // Canvas pan
61807
61833
  pointerDown = true;
61808
- didDrag = false;
61809
61834
  dragLast = { x: e.clientX, y: e.clientY };
61810
61835
  canvas.setPointerCapture(e.pointerId);
61811
61836
  });
@@ -62258,14 +62283,28 @@ path[data-source].port-hover { opacity: 1; }
62258
62283
  // Build info panel
62259
62284
  infoTitle.textContent = labelText;
62260
62285
  var html = '';
62286
+ var src = nodeSources[nodeId];
62287
+ if (src && src.description) {
62288
+ html += '<div class="node-desc">' + escapeH(src.description) + '</div>';
62289
+ }
62290
+ var portInfo = (src && src.ports) ? src.ports : {};
62291
+ function portLabel(name) {
62292
+ var p = portInfo[name];
62293
+ var label = escapeH(name);
62294
+ if (p) {
62295
+ var typeStr = p.tsType || p.type;
62296
+ if (typeStr) label += ' <span class="port-type">' + escapeH(typeStr) + '</span>';
62297
+ }
62298
+ return label;
62299
+ }
62261
62300
  if (inputs.length) {
62262
62301
  html += '<div class="info-section"><div class="info-label">Inputs</div><ul class="port-list">';
62263
- inputs.forEach(function(n) { html += '<li>' + escapeH(n) + '</li>'; });
62302
+ inputs.forEach(function(n) { html += '<li>' + portLabel(n) + '</li>'; });
62264
62303
  html += '</ul></div>';
62265
62304
  }
62266
62305
  if (outputs.length) {
62267
62306
  html += '<div class="info-section"><div class="info-label">Outputs</div><ul class="port-list">';
62268
- outputs.forEach(function(n) { html += '<li>' + escapeH(n) + '</li>'; });
62307
+ outputs.forEach(function(n) { html += '<li>' + portLabel(n) + '</li>'; });
62269
62308
  html += '</ul></div>';
62270
62309
  }
62271
62310
  if (connectedNodes.size) {
@@ -62273,6 +62312,10 @@ path[data-source].port-hover { opacity: 1; }
62273
62312
  html += Array.from(connectedNodes).map(escapeH).join(', ');
62274
62313
  html += '</div></div>';
62275
62314
  }
62315
+ if (src && src.source) {
62316
+ html += '<div class="info-section"><div class="info-label">Source</div>';
62317
+ html += '<pre><code>' + highlightTS(src.source) + '</code></pre></div>';
62318
+ }
62276
62319
  infoBody.innerHTML = html;
62277
62320
  infoPanel.classList.add('visible');
62278
62321
  }
@@ -62281,10 +62324,130 @@ path[data-source].port-hover { opacity: 1; }
62281
62324
  return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
62282
62325
  }
62283
62326
 
62327
+ var fwAnnotations = 'flowWeaver,input,output,step,node,connect,param,returns,fwImport,label,scope,position,color,icon,tag,map,path,name,description,expression,executeWhen,pullExecution,strictTypes,autoConnect,port,trigger,cancelOn,retries,timeout,throttle';
62328
+
62329
+ function highlightJSDoc(block) {
62330
+ var annSet = fwAnnotations.split(',');
62331
+ var out = '';
62332
+ var re = /(@[a-zA-Z]+)|(-&gt;)|(\\.[a-zA-Z_][a-zA-Z0-9_]*)|("[^"]*")|('\\''[^'\\'']*'\\'')|(-?[0-9]+(?:\\.[0-9]+)?)|([a-zA-Z_][a-zA-Z0-9_]*)|([^@a-zA-Z0-9"'\\-.]+)/g;
62333
+ var m;
62334
+ while ((m = re.exec(block)) !== null) {
62335
+ if (m[1]) {
62336
+ var tag = m[1].slice(1);
62337
+ if (annSet.indexOf(tag) >= 0) {
62338
+ out += '<span class="hl-ann">' + m[0] + '</span>';
62339
+ } else {
62340
+ out += '<span class="hl-ann">' + m[0] + '</span>';
62341
+ }
62342
+ } else if (m[2]) {
62343
+ out += '<span class="hl-arr">' + m[2] + '</span>';
62344
+ } else if (m[3]) {
62345
+ // .portName scope reference
62346
+ out += '<span class="hl-sc">' + m[3] + '</span>';
62347
+ } else if (m[4] || m[5]) {
62348
+ out += '<span class="hl-str">' + (m[4] || m[5]) + '</span>';
62349
+ } else if (m[6]) {
62350
+ out += '<span class="hl-num">' + m[6] + '</span>';
62351
+ } else if (m[7]) {
62352
+ var tys = 'string,number,boolean,any,void,never,unknown,STEP,STRING,NUMBER,BOOLEAN,ARRAY,OBJECT,FUNCTION,ANY';
62353
+ if (tys.split(',').indexOf(m[7]) >= 0) {
62354
+ out += '<span class="hl-ty">' + m[7] + '</span>';
62355
+ } else {
62356
+ out += '<span class="hl-id">' + m[7] + '</span>';
62357
+ }
62358
+ } else {
62359
+ out += m[0];
62360
+ }
62361
+ }
62362
+ return out;
62363
+ }
62364
+
62365
+ function highlightTS(code) {
62366
+ var tokens = [];
62367
+ var i = 0;
62368
+ while (i < code.length) {
62369
+ // Line comments
62370
+ if (code[i] === '/' && code[i+1] === '/') {
62371
+ var end = code.indexOf('\\n', i);
62372
+ if (end === -1) end = code.length;
62373
+ tokens.push({ t: 'cm', v: code.slice(i, end) });
62374
+ i = end;
62375
+ continue;
62376
+ }
62377
+ // Block comments (detect JSDoc for annotation highlighting)
62378
+ if (code[i] === '/' && code[i+1] === '*') {
62379
+ var end = code.indexOf('*/', i + 2);
62380
+ if (end === -1) end = code.length; else end += 2;
62381
+ var block = code.slice(i, end);
62382
+ var hasFW = /@(flowWeaver|input|output|step|node|connect|param|returns)/.test(block);
62383
+ if (hasFW) {
62384
+ tokens.push({ t: 'jsdoc', v: block });
62385
+ } else {
62386
+ tokens.push({ t: 'cm', v: block });
62387
+ }
62388
+ i = end;
62389
+ continue;
62390
+ }
62391
+ // Strings
62392
+ if (code[i] === "'" || code[i] === '"' || code[i] === '\`') {
62393
+ var q = code[i], j = i + 1;
62394
+ while (j < code.length && code[j] !== q) { if (code[j] === '\\\\') j++; j++; }
62395
+ tokens.push({ t: 'str', v: code.slice(i, j + 1) });
62396
+ i = j + 1;
62397
+ continue;
62398
+ }
62399
+ // Numbers
62400
+ if (/[0-9]/.test(code[i]) && (i === 0 || /[^a-zA-Z_$]/.test(code[i-1]))) {
62401
+ var j = i;
62402
+ while (j < code.length && /[0-9a-fA-FxX._]/.test(code[j])) j++;
62403
+ tokens.push({ t: 'num', v: code.slice(i, j) });
62404
+ i = j;
62405
+ continue;
62406
+ }
62407
+ // Words
62408
+ if (/[a-zA-Z_$]/.test(code[i])) {
62409
+ var j = i;
62410
+ while (j < code.length && /[a-zA-Z0-9_$]/.test(code[j])) j++;
62411
+ var w = code.slice(i, j);
62412
+ var kws = 'async,await,break,case,catch,class,const,continue,default,delete,do,else,export,extends,finally,for,from,function,if,import,in,instanceof,let,new,of,return,switch,throw,try,typeof,var,void,while,yield';
62413
+ var tys = 'string,number,boolean,any,void,never,unknown,null,undefined,true,false,Promise,Record,Map,Set,Array,Partial,Required,Omit,Pick';
62414
+ if (kws.split(',').indexOf(w) >= 0) {
62415
+ tokens.push({ t: 'kw', v: w });
62416
+ } else if (tys.split(',').indexOf(w) >= 0) {
62417
+ tokens.push({ t: 'ty', v: w });
62418
+ } else if (j < code.length && code[j] === '(') {
62419
+ tokens.push({ t: 'fn', v: w });
62420
+ } else {
62421
+ tokens.push({ t: '', v: w });
62422
+ }
62423
+ i = j;
62424
+ continue;
62425
+ }
62426
+ // Punctuation
62427
+ if (/[{}()\\[\\];:.,<>=!&|?+\\-*/%^~@]/.test(code[i])) {
62428
+ tokens.push({ t: 'pn', v: code[i] });
62429
+ i++;
62430
+ continue;
62431
+ }
62432
+ // Whitespace and other
62433
+ tokens.push({ t: '', v: code[i] });
62434
+ i++;
62435
+ }
62436
+ return tokens.map(function(tk) {
62437
+ if (tk.t === 'jsdoc') {
62438
+ return '<span class="hl-cm">' + highlightJSDoc(escapeH(tk.v)) + '</span>';
62439
+ }
62440
+ var v = escapeH(tk.v);
62441
+ return tk.t ? '<span class="hl-' + tk.t + '">' + v + '</span>' : v;
62442
+ }).join('');
62443
+ }
62444
+
62284
62445
  // Delegate click: port click > node click > background
62446
+ // Use clickTarget (stashed from pointerdown) because setPointerCapture redirects click to canvas
62285
62447
  canvas.addEventListener('click', function(e) {
62286
62448
  if (didDrag || didDragNode) { didDragNode = false; return; }
62287
- var target = e.target;
62449
+ var target = clickTarget || e.target;
62450
+ clickTarget = null;
62288
62451
  while (target && target !== canvas) {
62289
62452
  if (target.hasAttribute && target.hasAttribute('data-port-id')) {
62290
62453
  e.stopPropagation();
@@ -62325,24 +62488,57 @@ function fileToSVG(filePath, options = {}) {
62325
62488
  return pickAndRender(result.workflows, options);
62326
62489
  }
62327
62490
  function fileToHTML(filePath, options = {}) {
62328
- const svg = fileToSVG(filePath, options);
62329
- return wrapSVGInHTML(svg, { title: options.workflowName, theme: options.theme });
62491
+ const result = parser.parse(filePath);
62492
+ const ast = pickWorkflow(result.workflows, options);
62493
+ const svg = workflowToSVG(ast, options);
62494
+ return wrapSVGInHTML(svg, { title: options.workflowName ?? ast.name, theme: options.theme, nodeSources: buildNodeSourceMap(ast) });
62330
62495
  }
62331
- function pickAndRender(workflows, options) {
62496
+ function buildNodeSourceMap(ast) {
62497
+ const typeMap = new Map(ast.nodeTypes.map((nt) => [nt.functionName, nt]));
62498
+ const map3 = {};
62499
+ for (const inst of ast.instances) {
62500
+ const nt = typeMap.get(inst.nodeType);
62501
+ if (!nt) continue;
62502
+ const ports = {};
62503
+ for (const [name, def] of Object.entries(nt.inputs ?? {})) {
62504
+ ports[name] = { type: def.dataType, tsType: def.tsType };
62505
+ }
62506
+ for (const [name, def] of Object.entries(nt.outputs ?? {})) {
62507
+ ports[name] = { type: def.dataType, tsType: def.tsType };
62508
+ }
62509
+ map3[inst.id] = { description: nt.description, source: nt.functionText, ports };
62510
+ }
62511
+ const startPorts = {};
62512
+ for (const [name, def] of Object.entries(ast.startPorts ?? {})) {
62513
+ startPorts[name] = { type: def.dataType, tsType: def.tsType };
62514
+ }
62515
+ if (Object.keys(startPorts).length) {
62516
+ map3["Start"] = { description: ast.description, ports: startPorts };
62517
+ }
62518
+ const exitPorts = {};
62519
+ for (const [name, def] of Object.entries(ast.exitPorts ?? {})) {
62520
+ exitPorts[name] = { type: def.dataType, tsType: def.tsType };
62521
+ }
62522
+ if (Object.keys(exitPorts).length) {
62523
+ map3["Exit"] = { ports: exitPorts };
62524
+ }
62525
+ return map3;
62526
+ }
62527
+ function pickWorkflow(workflows, options) {
62332
62528
  if (workflows.length === 0) {
62333
62529
  throw new Error("No workflows found in source code");
62334
62530
  }
62335
- let workflow;
62336
62531
  if (options.workflowName) {
62337
62532
  const found = workflows.find((w) => w.name === options.workflowName);
62338
62533
  if (!found) {
62339
62534
  throw new Error(`Workflow "${options.workflowName}" not found. Available: ${workflows.map((w) => w.name).join(", ")}`);
62340
62535
  }
62341
- workflow = found;
62342
- } else {
62343
- workflow = workflows[0];
62536
+ return found;
62344
62537
  }
62345
- return workflowToSVG(workflow, options);
62538
+ return workflows[0];
62539
+ }
62540
+ function pickAndRender(workflows, options) {
62541
+ return workflowToSVG(pickWorkflow(workflows, options), options);
62346
62542
  }
62347
62543
 
62348
62544
  // src/cli/commands/diagram.ts
@@ -96900,7 +97096,7 @@ function displayInstalledPackage(pkg) {
96900
97096
  }
96901
97097
 
96902
97098
  // src/cli/index.ts
96903
- var version2 = true ? "0.10.3" : "0.0.0-dev";
97099
+ var version2 = true ? "0.10.4" : "0.0.0-dev";
96904
97100
  var program2 = new Command();
96905
97101
  program2.name("flow-weaver").description("Flow Weaver Annotations - Compile and validate workflow files").version(version2, "-v, --version", "Output the current version");
96906
97102
  program2.configureOutput({
@@ -8,6 +8,14 @@
8
8
  export interface HtmlViewerOptions {
9
9
  title?: string;
10
10
  theme?: 'dark' | 'light';
11
+ nodeSources?: Record<string, {
12
+ description?: string;
13
+ source?: string;
14
+ ports?: Record<string, {
15
+ type: string;
16
+ tsType?: string;
17
+ }>;
18
+ }>;
11
19
  }
12
20
  export declare function wrapSVGInHTML(svgContent: string, options?: HtmlViewerOptions): string;
13
21
  //# sourceMappingURL=html-viewer.d.ts.map
@@ -122,24 +122,46 @@ path[data-source].port-hover { opacity: 1; }
122
122
  /* Info panel */
123
123
  #info-panel {
124
124
  position: fixed; bottom: 52px; left: 16px;
125
- max-width: 320px; min-width: 200px;
125
+ max-width: 480px; min-width: 260px;
126
126
  background: ${surfaceMain}; border: 1px solid ${borderSubtle};
127
127
  border-radius: 8px; padding: 12px 16px;
128
128
  font-size: 13px; line-height: 1.5;
129
129
  box-shadow: 0 2px 8px rgba(0,0,0,0.2);
130
130
  z-index: 10; display: none;
131
+ max-height: calc(100vh - 120px); overflow-y: auto;
131
132
  }
132
133
  #info-panel.visible { display: block; }
133
134
  #info-panel h3 {
134
135
  font-size: 14px; font-weight: 700; margin-bottom: 6px;
135
136
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
136
137
  }
138
+ #info-panel .node-desc { color: ${textMed}; font-size: 12px; margin-bottom: 8px; font-style: italic; }
137
139
  #info-panel .info-section { margin-bottom: 6px; }
138
140
  #info-panel .info-label { font-size: 11px; font-weight: 600; color: ${textLow}; text-transform: uppercase; letter-spacing: 0.5px; }
139
141
  #info-panel .info-value { color: ${textMed}; }
140
142
  #info-panel .port-list { list-style: none; padding: 0; }
141
143
  #info-panel .port-list li { padding: 1px 0; }
142
144
  #info-panel .port-list li::before { content: '\\2022'; margin-right: 6px; color: ${textLow}; }
145
+ #info-panel .port-type { color: ${textLow}; font-size: 11px; margin-left: 4px; font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace; }
146
+ #info-panel pre {
147
+ background: ${isDark ? '#161625' : '#f0f1fa'}; border: 1px solid ${borderSubtle};
148
+ border-radius: 6px; padding: 10px; overflow-x: auto;
149
+ font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
150
+ font-size: 12px; line-height: 1.6; white-space: pre;
151
+ max-height: 300px; overflow-y: auto; margin: 6px 0 0;
152
+ color: ${isDark ? '#e6edf3' : '#1a2340'};
153
+ }
154
+ .hl-kw { color: ${isDark ? '#8e9eff' : '#4040bf'}; }
155
+ .hl-str { color: ${isDark ? '#ff7b72' : '#c4432b'}; }
156
+ .hl-num { color: ${isDark ? '#f0a050' : '#b35e14'}; }
157
+ .hl-cm { color: #6a737d; font-style: italic; }
158
+ .hl-fn { color: ${isDark ? '#d2a8ff' : '#7c3aed'}; }
159
+ .hl-ty { color: ${isDark ? '#d2a8ff' : '#7c3aed'}; }
160
+ .hl-pn { color: ${isDark ? '#b8bdd0' : '#4a5578'}; }
161
+ .hl-ann { color: ${isDark ? '#8e9eff' : '#4040bf'}; font-weight: 600; font-style: normal; }
162
+ .hl-arr { color: ${isDark ? '#79c0ff' : '#0969da'}; font-weight: 600; font-style: normal; }
163
+ .hl-id { color: ${isDark ? '#e6edf3' : '#1a2340'}; font-style: normal; }
164
+ .hl-sc { color: ${isDark ? '#d2a8ff' : '#7c3aed'}; font-style: italic; }
143
165
 
144
166
  /* Branding badge */
145
167
  #branding {
@@ -194,8 +216,8 @@ path[data-source].port-hover { opacity: 1; }
194
216
  <circle cx="10" cy="10" r="1.5" fill="${dotColor}" opacity="0.6"/>
195
217
  </pattern>
196
218
  </defs>
197
- <rect x="-100000" y="-100000" width="200000" height="200000" fill="${bg}"/>
198
- <rect x="-100000" y="-100000" width="200000" height="200000" fill="url(#viewer-dots)"/>
219
+ <rect x="-100000" y="-100000" width="200000" height="200000" fill="${bg}" pointer-events="none"/>
220
+ <rect x="-100000" y="-100000" width="200000" height="200000" fill="url(#viewer-dots)" pointer-events="none"/>
199
221
  <g id="diagram">${inner}</g>
200
222
  </svg>
201
223
  <div id="controls">
@@ -225,6 +247,7 @@ path[data-source].port-hover { opacity: 1; }
225
247
  </a>
226
248
  <div id="scroll-hint">Use <kbd id="mod-key">Ctrl</kbd> + scroll to zoom</div>
227
249
  <div id="studio-hint">Like rearranging? <a href="https://flowweaver.ai" target="_blank" rel="noopener">Flow Weaver Studio</a> saves your layouts.</div>
250
+ <script>var nodeSources = ${JSON.stringify(options.nodeSources ?? {})};</script>
228
251
  <script>
229
252
  (function() {
230
253
  'use strict';
@@ -309,6 +332,7 @@ path[data-source].port-hover { opacity: 1; }
309
332
 
310
333
  // ---- Pan (drag) + Node drag ----
311
334
  var draggedNodeId = null, dragNodeStart = null, didDragNode = false;
335
+ var clickTarget = null; // stash the real target before setPointerCapture steals it
312
336
  var dragCount = 0, nudgeIndex = 0, nudgeTimer = null;
313
337
  var nudgeMessages = [
314
338
  'Like rearranging? <a href="https://flowweaver.ai" target="_blank" rel="noopener">Flow Weaver Studio</a> saves your layouts.',
@@ -323,6 +347,8 @@ path[data-source].port-hover { opacity: 1; }
323
347
 
324
348
  canvas.addEventListener('pointerdown', function(e) {
325
349
  if (e.button !== 0) return;
350
+ clickTarget = e.target; // stash before setPointerCapture redirects events
351
+ didDrag = false;
326
352
  // Check if clicking on a node body (walk up to detect data-node-id)
327
353
  var t = e.target;
328
354
  while (t && t !== canvas) {
@@ -339,7 +365,6 @@ path[data-source].port-hover { opacity: 1; }
339
365
  }
340
366
  // Canvas pan
341
367
  pointerDown = true;
342
- didDrag = false;
343
368
  dragLast = { x: e.clientX, y: e.clientY };
344
369
  canvas.setPointerCapture(e.pointerId);
345
370
  });
@@ -792,14 +817,28 @@ path[data-source].port-hover { opacity: 1; }
792
817
  // Build info panel
793
818
  infoTitle.textContent = labelText;
794
819
  var html = '';
820
+ var src = nodeSources[nodeId];
821
+ if (src && src.description) {
822
+ html += '<div class="node-desc">' + escapeH(src.description) + '</div>';
823
+ }
824
+ var portInfo = (src && src.ports) ? src.ports : {};
825
+ function portLabel(name) {
826
+ var p = portInfo[name];
827
+ var label = escapeH(name);
828
+ if (p) {
829
+ var typeStr = p.tsType || p.type;
830
+ if (typeStr) label += ' <span class="port-type">' + escapeH(typeStr) + '</span>';
831
+ }
832
+ return label;
833
+ }
795
834
  if (inputs.length) {
796
835
  html += '<div class="info-section"><div class="info-label">Inputs</div><ul class="port-list">';
797
- inputs.forEach(function(n) { html += '<li>' + escapeH(n) + '</li>'; });
836
+ inputs.forEach(function(n) { html += '<li>' + portLabel(n) + '</li>'; });
798
837
  html += '</ul></div>';
799
838
  }
800
839
  if (outputs.length) {
801
840
  html += '<div class="info-section"><div class="info-label">Outputs</div><ul class="port-list">';
802
- outputs.forEach(function(n) { html += '<li>' + escapeH(n) + '</li>'; });
841
+ outputs.forEach(function(n) { html += '<li>' + portLabel(n) + '</li>'; });
803
842
  html += '</ul></div>';
804
843
  }
805
844
  if (connectedNodes.size) {
@@ -807,6 +846,10 @@ path[data-source].port-hover { opacity: 1; }
807
846
  html += Array.from(connectedNodes).map(escapeH).join(', ');
808
847
  html += '</div></div>';
809
848
  }
849
+ if (src && src.source) {
850
+ html += '<div class="info-section"><div class="info-label">Source</div>';
851
+ html += '<pre><code>' + highlightTS(src.source) + '</code></pre></div>';
852
+ }
810
853
  infoBody.innerHTML = html;
811
854
  infoPanel.classList.add('visible');
812
855
  }
@@ -815,10 +858,130 @@ path[data-source].port-hover { opacity: 1; }
815
858
  return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
816
859
  }
817
860
 
861
+ var fwAnnotations = 'flowWeaver,input,output,step,node,connect,param,returns,fwImport,label,scope,position,color,icon,tag,map,path,name,description,expression,executeWhen,pullExecution,strictTypes,autoConnect,port,trigger,cancelOn,retries,timeout,throttle';
862
+
863
+ function highlightJSDoc(block) {
864
+ var annSet = fwAnnotations.split(',');
865
+ var out = '';
866
+ var re = /(@[a-zA-Z]+)|(-&gt;)|(\\.[a-zA-Z_][a-zA-Z0-9_]*)|("[^"]*")|('\\''[^'\\'']*'\\'')|(-?[0-9]+(?:\\.[0-9]+)?)|([a-zA-Z_][a-zA-Z0-9_]*)|([^@a-zA-Z0-9"'\\-.]+)/g;
867
+ var m;
868
+ while ((m = re.exec(block)) !== null) {
869
+ if (m[1]) {
870
+ var tag = m[1].slice(1);
871
+ if (annSet.indexOf(tag) >= 0) {
872
+ out += '<span class="hl-ann">' + m[0] + '</span>';
873
+ } else {
874
+ out += '<span class="hl-ann">' + m[0] + '</span>';
875
+ }
876
+ } else if (m[2]) {
877
+ out += '<span class="hl-arr">' + m[2] + '</span>';
878
+ } else if (m[3]) {
879
+ // .portName scope reference
880
+ out += '<span class="hl-sc">' + m[3] + '</span>';
881
+ } else if (m[4] || m[5]) {
882
+ out += '<span class="hl-str">' + (m[4] || m[5]) + '</span>';
883
+ } else if (m[6]) {
884
+ out += '<span class="hl-num">' + m[6] + '</span>';
885
+ } else if (m[7]) {
886
+ var tys = 'string,number,boolean,any,void,never,unknown,STEP,STRING,NUMBER,BOOLEAN,ARRAY,OBJECT,FUNCTION,ANY';
887
+ if (tys.split(',').indexOf(m[7]) >= 0) {
888
+ out += '<span class="hl-ty">' + m[7] + '</span>';
889
+ } else {
890
+ out += '<span class="hl-id">' + m[7] + '</span>';
891
+ }
892
+ } else {
893
+ out += m[0];
894
+ }
895
+ }
896
+ return out;
897
+ }
898
+
899
+ function highlightTS(code) {
900
+ var tokens = [];
901
+ var i = 0;
902
+ while (i < code.length) {
903
+ // Line comments
904
+ if (code[i] === '/' && code[i+1] === '/') {
905
+ var end = code.indexOf('\\n', i);
906
+ if (end === -1) end = code.length;
907
+ tokens.push({ t: 'cm', v: code.slice(i, end) });
908
+ i = end;
909
+ continue;
910
+ }
911
+ // Block comments (detect JSDoc for annotation highlighting)
912
+ if (code[i] === '/' && code[i+1] === '*') {
913
+ var end = code.indexOf('*/', i + 2);
914
+ if (end === -1) end = code.length; else end += 2;
915
+ var block = code.slice(i, end);
916
+ var hasFW = /@(flowWeaver|input|output|step|node|connect|param|returns)/.test(block);
917
+ if (hasFW) {
918
+ tokens.push({ t: 'jsdoc', v: block });
919
+ } else {
920
+ tokens.push({ t: 'cm', v: block });
921
+ }
922
+ i = end;
923
+ continue;
924
+ }
925
+ // Strings
926
+ if (code[i] === "'" || code[i] === '"' || code[i] === '\`') {
927
+ var q = code[i], j = i + 1;
928
+ while (j < code.length && code[j] !== q) { if (code[j] === '\\\\') j++; j++; }
929
+ tokens.push({ t: 'str', v: code.slice(i, j + 1) });
930
+ i = j + 1;
931
+ continue;
932
+ }
933
+ // Numbers
934
+ if (/[0-9]/.test(code[i]) && (i === 0 || /[^a-zA-Z_$]/.test(code[i-1]))) {
935
+ var j = i;
936
+ while (j < code.length && /[0-9a-fA-FxX._]/.test(code[j])) j++;
937
+ tokens.push({ t: 'num', v: code.slice(i, j) });
938
+ i = j;
939
+ continue;
940
+ }
941
+ // Words
942
+ if (/[a-zA-Z_$]/.test(code[i])) {
943
+ var j = i;
944
+ while (j < code.length && /[a-zA-Z0-9_$]/.test(code[j])) j++;
945
+ var w = code.slice(i, j);
946
+ var kws = 'async,await,break,case,catch,class,const,continue,default,delete,do,else,export,extends,finally,for,from,function,if,import,in,instanceof,let,new,of,return,switch,throw,try,typeof,var,void,while,yield';
947
+ var tys = 'string,number,boolean,any,void,never,unknown,null,undefined,true,false,Promise,Record,Map,Set,Array,Partial,Required,Omit,Pick';
948
+ if (kws.split(',').indexOf(w) >= 0) {
949
+ tokens.push({ t: 'kw', v: w });
950
+ } else if (tys.split(',').indexOf(w) >= 0) {
951
+ tokens.push({ t: 'ty', v: w });
952
+ } else if (j < code.length && code[j] === '(') {
953
+ tokens.push({ t: 'fn', v: w });
954
+ } else {
955
+ tokens.push({ t: '', v: w });
956
+ }
957
+ i = j;
958
+ continue;
959
+ }
960
+ // Punctuation
961
+ if (/[{}()\\[\\];:.,<>=!&|?+\\-*/%^~@]/.test(code[i])) {
962
+ tokens.push({ t: 'pn', v: code[i] });
963
+ i++;
964
+ continue;
965
+ }
966
+ // Whitespace and other
967
+ tokens.push({ t: '', v: code[i] });
968
+ i++;
969
+ }
970
+ return tokens.map(function(tk) {
971
+ if (tk.t === 'jsdoc') {
972
+ return '<span class="hl-cm">' + highlightJSDoc(escapeH(tk.v)) + '</span>';
973
+ }
974
+ var v = escapeH(tk.v);
975
+ return tk.t ? '<span class="hl-' + tk.t + '">' + v + '</span>' : v;
976
+ }).join('');
977
+ }
978
+
818
979
  // Delegate click: port click > node click > background
980
+ // Use clickTarget (stashed from pointerdown) because setPointerCapture redirects click to canvas
819
981
  canvas.addEventListener('click', function(e) {
820
982
  if (didDrag || didDragNode) { didDragNode = false; return; }
821
- var target = e.target;
983
+ var target = clickTarget || e.target;
984
+ clickTarget = null;
822
985
  while (target && target !== canvas) {
823
986
  if (target.hasAttribute && target.hasAttribute('data-port-id')) {
824
987
  e.stopPropagation();
@@ -28,37 +28,73 @@ export function fileToSVG(filePath, options = {}) {
28
28
  */
29
29
  export function workflowToHTML(ast, options = {}) {
30
30
  const svg = workflowToSVG(ast, options);
31
- return wrapSVGInHTML(svg, { title: options.workflowName ?? ast.name, theme: options.theme });
31
+ return wrapSVGInHTML(svg, { title: options.workflowName ?? ast.name, theme: options.theme, nodeSources: buildNodeSourceMap(ast) });
32
32
  }
33
33
  /**
34
34
  * Parse TypeScript source code and render the first (or named) workflow to interactive HTML.
35
35
  */
36
36
  export function sourceToHTML(code, options = {}) {
37
- const svg = sourceToSVG(code, options);
38
- return wrapSVGInHTML(svg, { title: options.workflowName, theme: options.theme });
37
+ const result = parser.parseFromString(code);
38
+ const ast = pickWorkflow(result.workflows, options);
39
+ const svg = workflowToSVG(ast, options);
40
+ return wrapSVGInHTML(svg, { title: options.workflowName ?? ast.name, theme: options.theme, nodeSources: buildNodeSourceMap(ast) });
39
41
  }
40
42
  /**
41
43
  * Parse a workflow file and render the first (or named) workflow to interactive HTML.
42
44
  */
43
45
  export function fileToHTML(filePath, options = {}) {
44
- const svg = fileToSVG(filePath, options);
45
- return wrapSVGInHTML(svg, { title: options.workflowName, theme: options.theme });
46
+ const result = parser.parse(filePath);
47
+ const ast = pickWorkflow(result.workflows, options);
48
+ const svg = workflowToSVG(ast, options);
49
+ return wrapSVGInHTML(svg, { title: options.workflowName ?? ast.name, theme: options.theme, nodeSources: buildNodeSourceMap(ast) });
46
50
  }
47
- function pickAndRender(workflows, options) {
51
+ function buildNodeSourceMap(ast) {
52
+ const typeMap = new Map(ast.nodeTypes.map(nt => [nt.functionName, nt]));
53
+ const map = {};
54
+ for (const inst of ast.instances) {
55
+ const nt = typeMap.get(inst.nodeType);
56
+ if (!nt)
57
+ continue;
58
+ const ports = {};
59
+ for (const [name, def] of Object.entries(nt.inputs ?? {})) {
60
+ ports[name] = { type: def.dataType, tsType: def.tsType };
61
+ }
62
+ for (const [name, def] of Object.entries(nt.outputs ?? {})) {
63
+ ports[name] = { type: def.dataType, tsType: def.tsType };
64
+ }
65
+ map[inst.id] = { description: nt.description, source: nt.functionText, ports };
66
+ }
67
+ // Virtual Start/Exit nodes get their port types from the workflow definition
68
+ const startPorts = {};
69
+ for (const [name, def] of Object.entries(ast.startPorts ?? {})) {
70
+ startPorts[name] = { type: def.dataType, tsType: def.tsType };
71
+ }
72
+ if (Object.keys(startPorts).length) {
73
+ map['Start'] = { description: ast.description, ports: startPorts };
74
+ }
75
+ const exitPorts = {};
76
+ for (const [name, def] of Object.entries(ast.exitPorts ?? {})) {
77
+ exitPorts[name] = { type: def.dataType, tsType: def.tsType };
78
+ }
79
+ if (Object.keys(exitPorts).length) {
80
+ map['Exit'] = { ports: exitPorts };
81
+ }
82
+ return map;
83
+ }
84
+ function pickWorkflow(workflows, options) {
48
85
  if (workflows.length === 0) {
49
86
  throw new Error('No workflows found in source code');
50
87
  }
51
- let workflow;
52
88
  if (options.workflowName) {
53
89
  const found = workflows.find(w => w.name === options.workflowName);
54
90
  if (!found) {
55
91
  throw new Error(`Workflow "${options.workflowName}" not found. Available: ${workflows.map(w => w.name).join(', ')}`);
56
92
  }
57
- workflow = found;
58
- }
59
- else {
60
- workflow = workflows[0];
93
+ return found;
61
94
  }
62
- return workflowToSVG(workflow, options);
95
+ return workflows[0];
96
+ }
97
+ function pickAndRender(workflows, options) {
98
+ return workflowToSVG(pickWorkflow(workflows, options), options);
63
99
  }
64
100
  //# sourceMappingURL=index.js.map
@@ -35,7 +35,7 @@ const COERCE_TARGET_TYPES = {
35
35
  OBJECT: 'object',
36
36
  };
37
37
  /**
38
- * Build a concrete @coerce suggestion from error message context.
38
+ * Build a concrete `@connect ... as <type>` coercion suggestion from error context.
39
39
  * Returns null if not enough info is available.
40
40
  */
41
41
  function buildCoerceSuggestion(quoted, targetType) {
@@ -51,7 +51,7 @@ function buildCoerceSuggestion(quoted, targetType) {
51
51
  const coerceType = COERCE_TARGET_TYPES[targetType.toUpperCase()] || targetType.toLowerCase();
52
52
  if (!['string', 'number', 'boolean', 'json', 'object'].includes(coerceType))
53
53
  return null;
54
- return `@coerce c1 ${portRefs[0]} -> ${portRefs[1]} as ${coerceType}`;
54
+ return `@connect ${portRefs[0]} -> ${portRefs[1]} as ${coerceType}`;
55
55
  }
56
56
  function extractCyclePath(message) {
57
57
  const match = message.match(/:\s*(.+ -> .+)/);
@@ -163,8 +163,8 @@ const errorMappers = {
163
163
  title: 'Type Mismatch',
164
164
  explanation: `Type mismatch: you're connecting a ${source} to a ${target}. The value will be automatically converted, but this might cause unexpected behavior.`,
165
165
  fix: coerceSuggestion
166
- ? `Add an explicit coercion: \`${coerceSuggestion}\`, change one of the port types, or use @strictTypes to turn this into an error.`
167
- : `Add a @coerce annotation between the two ports, change one of the port types, or use @strictTypes to turn this into an error.`,
166
+ ? `Use \`${coerceSuggestion}\` for explicit coercion, change one of the port types, or use @strictTypes to turn this into an error.`
167
+ : `Add \`as <type>\` to the @connect annotation (e.g. \`as string\`), change one of the port types, or use @strictTypes to turn this into an error.`,
168
168
  code: error.code,
169
169
  };
170
170
  },
@@ -264,8 +264,8 @@ const errorMappers = {
264
264
  title: 'Type Incompatible',
265
265
  explanation: `Type mismatch: ${source} to ${target}. With @strictTypes enabled, this is an error instead of a warning.`,
266
266
  fix: coerceSuggestion
267
- ? `Add an explicit coercion: \`${coerceSuggestion}\`, change one of the port types, or remove @strictTypes to allow implicit coercions.`
268
- : `Add a @coerce annotation between the ports, change one of the port types, or remove @strictTypes to allow implicit coercions.`,
267
+ ? `Use \`${coerceSuggestion}\` for explicit coercion, change one of the port types, or remove @strictTypes to allow implicit coercions.`
268
+ : `Add \`as <type>\` to the @connect annotation (e.g. \`as number\`), change one of the port types, or remove @strictTypes to allow implicit coercions.`,
269
269
  code: error.code,
270
270
  };
271
271
  },
@@ -279,8 +279,8 @@ const errorMappers = {
279
279
  title: 'Unusual Type Coercion',
280
280
  explanation: `Converting ${source} to ${target} is technically valid but semantically unusual and may produce unexpected behavior.`,
281
281
  fix: coerceSuggestion
282
- ? `If intentional, add an explicit coercion: \`${coerceSuggestion}\`, or use @strictTypes to enforce type safety.`
283
- : `If intentional, add an explicit @coerce annotation, or use @strictTypes to enforce type safety.`,
282
+ ? `If intentional, use \`${coerceSuggestion}\` for explicit coercion, or use @strictTypes to enforce type safety.`
283
+ : `If intentional, add \`as <type>\` to the @connect annotation, or use @strictTypes to enforce type safety.`,
284
284
  code: error.code,
285
285
  };
286
286
  },
@@ -550,8 +550,8 @@ const errorMappers = {
550
550
  title: 'Lossy Type Conversion',
551
551
  explanation: `Converting ${source} to ${target} may lose data or produce unexpected results (e.g., NaN, truncation).`,
552
552
  fix: coerceSuggestion
553
- ? `Add an explicit coercion: \`${coerceSuggestion}\`, or use @strictTypes to enforce type safety.`
554
- : `Add an explicit conversion with @coerce, or use @strictTypes to enforce type safety.`,
553
+ ? `Use \`${coerceSuggestion}\` for explicit coercion, or use @strictTypes to enforce type safety.`
554
+ : `Add \`as <type>\` to the @connect annotation for explicit conversion, or use @strictTypes to enforce type safety.`,
555
555
  code: error.code,
556
556
  };
557
557
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@synergenius/flow-weaver",
3
- "version": "0.10.3",
3
+ "version": "0.10.4",
4
4
  "description": "Deterministic workflow compiler for AI agents. Compiles to standalone TypeScript, no runtime dependencies.",
5
5
  "private": false,
6
6
  "type": "module",