@particle-academy/fancy-flow 0.41.0 → 0.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -11033,11 +11033,11 @@ function NoteNodeInner(props) {
11033
11033
  const config = data.config ?? {};
11034
11034
  const api = useFlowEditorOptional();
11035
11035
  const hasConfigText = typeof config.text === "string" && config.text.length > 0;
11036
- const text = hasConfigText ? String(config.text) : typeof data.body === "string" ? data.body : typeof data.text === "string" ? data.text : "";
11036
+ const text2 = hasConfigText ? String(config.text) : typeof data.body === "string" ? data.body : typeof data.text === "string" ? data.text : "";
11037
11037
  const title = typeof config.title === "string" && config.title || (!hasConfigText && typeof data.body === "string" && typeof data.label === "string" ? data.label : "");
11038
11038
  const skin = NOTE_COLORS[coerceColor(config.color)];
11039
11039
  const [editing, setEditing] = ReactExports.useState(false);
11040
- const [draft, setDraft] = ReactExports.useState(text);
11040
+ const [draft, setDraft] = ReactExports.useState(text2);
11041
11041
  const areaRef = ReactExports.useRef(null);
11042
11042
  ReactExports.useEffect(() => {
11043
11043
  if (editing && areaRef.current) {
@@ -11048,7 +11048,7 @@ function NoteNodeInner(props) {
11048
11048
  }, [editing]);
11049
11049
  const commit = () => {
11050
11050
  setEditing(false);
11051
- if (!api || draft === text) return;
11051
+ if (!api || draft === text2) return;
11052
11052
  const current = api.nodes.find((n) => n.id === props.id);
11053
11053
  if (!current) return;
11054
11054
  api.updateNode({
@@ -11062,7 +11062,7 @@ function NoteNodeInner(props) {
11062
11062
  className: ["ff-note", props.selected ? "ff-note--selected" : ""].filter(Boolean).join(" "),
11063
11063
  style: { background: skin.bg, borderColor: skin.border },
11064
11064
  onDoubleClick: api ? () => {
11065
- setDraft(text);
11065
+ setDraft(text2);
11066
11066
  setEditing(true);
11067
11067
  } : void 0,
11068
11068
  children: [
@@ -11081,7 +11081,7 @@ function NoteNodeInner(props) {
11081
11081
  if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) commit();
11082
11082
  }
11083
11083
  }
11084
- ) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ff-note__body", children: text || /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ff-note__placeholder", children: api ? "Double-click to write a note\u2026" : "Note" }) })
11084
+ ) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ff-note__body", children: text2 || /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ff-note__placeholder", children: api ? "Double-click to write a note\u2026" : "Note" }) })
11085
11085
  ]
11086
11086
  }
11087
11087
  );
@@ -11496,6 +11496,9 @@ var KINDS = [
11496
11496
  },
11497
11497
  {
11498
11498
  name: "@particle-academy/user_input",
11499
+ // The keys an author declared on THIS node — the case a static list cannot
11500
+ // express, and the one issue #5 named.
11501
+ outputShape: (config) => (config.fields ?? []).filter((f) => typeof f.key === "string" && f.key !== "").map((f) => ({ path: f.key, type: "string", description: f.label })),
11499
11502
  aliases: ["user_input", "@fancy/user_input"],
11500
11503
  pausesForHuman: "input",
11501
11504
  category: "human",
@@ -11716,6 +11719,10 @@ var KINDS = [
11716
11719
  },
11717
11720
  {
11718
11721
  name: "@particle-academy/for_each",
11722
+ outputShape: [
11723
+ { path: "items", type: "array" },
11724
+ { path: "count", type: "number" }
11725
+ ],
11719
11726
  aliases: ["for_each", "@fancy/for_each"],
11720
11727
  category: "logic",
11721
11728
  label: "For Each",
@@ -11879,6 +11886,12 @@ var KINDS = [
11879
11886
  // ───────────── AI ─────────────
11880
11887
  {
11881
11888
  name: "@particle-academy/llm_call",
11889
+ // LlmClient::complete() -> array{text:string,usage?:array,raw?:mixed}
11890
+ outputShape: [
11891
+ { path: "text", type: "string", description: "The model's completion." },
11892
+ { path: "usage", type: "object", description: "Token counts, when the provider reports them." },
11893
+ { path: "raw", type: "unknown", description: "The provider's untouched response." }
11894
+ ],
11882
11895
  aliases: ["llm_call", "@fancy/llm_call"],
11883
11896
  category: "ai",
11884
11897
  label: "LLM Call",
@@ -12013,6 +12026,10 @@ var KINDS = [
12013
12026
  },
12014
12027
  {
12015
12028
  name: "@particle-academy/embed_search",
12029
+ outputShape: [
12030
+ { path: "query", type: "string" },
12031
+ { path: "matches", type: "array", description: "Ranked results from the vector store." }
12032
+ ],
12016
12033
  aliases: ["embed_search", "@fancy/embed_search"],
12017
12034
  category: "ai",
12018
12035
  label: "Embed & Search",
@@ -12027,6 +12044,12 @@ var KINDS = [
12027
12044
  // ───────────── IO ─────────────
12028
12045
  {
12029
12046
  name: "@particle-academy/api_request",
12047
+ // HttpClient::send() -> array{status:int,headers:array,body:mixed}
12048
+ outputShape: [
12049
+ { path: "status", type: "number", description: "HTTP status code." },
12050
+ { path: "headers", type: "object" },
12051
+ { path: "body", type: "unknown", description: "Decoded JSON when the response is JSON, otherwise the raw body." }
12052
+ ],
12030
12053
  aliases: ["api_request", "@fancy/api_request"],
12031
12054
  category: "io",
12032
12055
  label: "API Request",
@@ -12811,6 +12834,230 @@ function paletteDropHandlers(onDrop) {
12811
12834
  }
12812
12835
  };
12813
12836
  }
12837
+
12838
+ // src/expressions/variables.ts
12839
+ var ROOT = "$json";
12840
+ function expr(path) {
12841
+ return `{{ ${path} }}`;
12842
+ }
12843
+ function outputFieldsFor(node) {
12844
+ const kindName = node.data?.kind ?? node.type;
12845
+ if (!kindName) return [];
12846
+ const kind = getNodeKind(kindName);
12847
+ const shape = kind?.outputShape;
12848
+ if (!shape) return [];
12849
+ if (typeof shape === "function") {
12850
+ try {
12851
+ const config = node.data?.config ?? {};
12852
+ return shape(config) ?? [];
12853
+ } catch {
12854
+ return [];
12855
+ }
12856
+ }
12857
+ return shape;
12858
+ }
12859
+ function baseVariables() {
12860
+ return [
12861
+ { expression: expr(ROOT), path: ROOT, type: "unknown", description: "The whole incoming value." }
12862
+ ];
12863
+ }
12864
+ function availableVariables(graph, nodeId) {
12865
+ const out = baseVariables();
12866
+ const seen = /* @__PURE__ */ new Set([ROOT]);
12867
+ const byId = new Map(graph.nodes.map((n) => [n.id, n]));
12868
+ const upstreamIds = graph.edges.filter((e) => e.target === nodeId).map((e) => e.source);
12869
+ for (const id2 of upstreamIds) {
12870
+ if (id2 === nodeId) continue;
12871
+ const upstream = byId.get(id2);
12872
+ if (!upstream) continue;
12873
+ const kindName = upstream.data?.kind ?? upstream.type;
12874
+ const label = upstream.data?.label ?? (kindName ? getNodeKind(kindName)?.label : void 0);
12875
+ for (const field of outputFieldsFor(upstream)) {
12876
+ const path = `${ROOT}.${field.path}`;
12877
+ if (seen.has(path)) continue;
12878
+ seen.add(path);
12879
+ out.push({
12880
+ expression: expr(path),
12881
+ path,
12882
+ type: field.type,
12883
+ description: field.description,
12884
+ source: label
12885
+ });
12886
+ }
12887
+ }
12888
+ return out;
12889
+ }
12890
+ function describeExpressionGrammar() {
12891
+ return {
12892
+ forms: [
12893
+ { syntax: "{{ $json }}", meaning: "The whole value arriving on this node's input." },
12894
+ { syntax: "{{ $json.field }}", meaning: "One key of that value. Dot-paths nest: $json.user.email." },
12895
+ { syntax: "{{ $input.field }}", meaning: "An alias for $json \u2014 the same value, either spelling." },
12896
+ { syntax: "{{ in }}", meaning: "The input by its context key, rather than through the alias." }
12897
+ ],
12898
+ note: "Dot-paths only \u2014 no arithmetic, comparisons or function calls; anything else resolves to nothing. A field that is exactly one expression keeps the resolved value's type; mixed text interpolates as a string. fancy-flow's own runFlow does not interpolate: it hands config to your executor verbatim, and the fancy-flow-php runtime is what resolves these."
12899
+ };
12900
+ }
12901
+ function findTrigger(text2, caret) {
12902
+ const before = text2.slice(0, caret);
12903
+ const open = before.lastIndexOf("{{");
12904
+ if (open === -1) return null;
12905
+ if (before.indexOf("}}", open) !== -1) return null;
12906
+ return { open, query: before.slice(open + 2).trim() };
12907
+ }
12908
+ function filterVariables(vars, query) {
12909
+ const q2 = query.trim().toLowerCase();
12910
+ if (q2 === "") return vars;
12911
+ return vars.filter((v2) => v2.path.toLowerCase().includes(q2));
12912
+ }
12913
+ function applyCompletion(text2, caret, trigger, variable) {
12914
+ const head = text2.slice(0, trigger.open);
12915
+ const tail = text2.slice(caret);
12916
+ return { value: `${head}${variable.expression}${tail}`, caret: head.length + variable.expression.length };
12917
+ }
12918
+ function ExpressionField({
12919
+ value,
12920
+ onChange,
12921
+ placeholder,
12922
+ rows = 2,
12923
+ handle,
12924
+ graph,
12925
+ nodeId
12926
+ }) {
12927
+ const ref = ReactExports.useRef(null);
12928
+ const [trigger, setTrigger] = ReactExports.useState(null);
12929
+ const [active, setActive] = ReactExports.useState(0);
12930
+ const [showHelp, setShowHelp] = ReactExports.useState(false);
12931
+ const variables = ReactExports.useMemo(
12932
+ () => graph && nodeId ? availableVariables(graph, nodeId) : baseVariables(),
12933
+ [graph, nodeId]
12934
+ );
12935
+ const matches = ReactExports.useMemo(
12936
+ () => trigger ? filterVariables(variables, trigger.query) : [],
12937
+ [trigger, variables]
12938
+ );
12939
+ const help = ReactExports.useMemo(() => describeExpressionGrammar(), []);
12940
+ const open = trigger !== null && matches.length > 0;
12941
+ const sync = (el) => {
12942
+ const next = findTrigger(el.value, el.selectionStart ?? el.value.length);
12943
+ setTrigger(next);
12944
+ setActive(0);
12945
+ };
12946
+ const choose = (variable) => {
12947
+ const el = ref.current;
12948
+ if (!el || !trigger) return;
12949
+ const { value: next, caret } = applyCompletion(
12950
+ el.value,
12951
+ el.selectionStart ?? el.value.length,
12952
+ trigger,
12953
+ variable
12954
+ );
12955
+ onChange(next);
12956
+ setTrigger(null);
12957
+ requestAnimationFrame(() => {
12958
+ el.focus();
12959
+ el.setSelectionRange(caret, caret);
12960
+ });
12961
+ };
12962
+ const onKeyDown = (e) => {
12963
+ if (!open) return;
12964
+ if (e.key === "ArrowDown") {
12965
+ e.preventDefault();
12966
+ setActive((i) => (i + 1) % matches.length);
12967
+ } else if (e.key === "ArrowUp") {
12968
+ e.preventDefault();
12969
+ setActive((i) => (i - 1 + matches.length) % matches.length);
12970
+ } else if (e.key === "Enter" || e.key === "Tab") {
12971
+ e.preventDefault();
12972
+ choose(matches[active] ?? matches[0]);
12973
+ } else if (e.key === "Escape") {
12974
+ e.preventDefault();
12975
+ setTrigger(null);
12976
+ }
12977
+ };
12978
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ff-expression", "data-ff-expression": handle["data-ff-field"], children: [
12979
+ /* @__PURE__ */ jsxRuntime.jsx(
12980
+ "textarea",
12981
+ {
12982
+ ...handle,
12983
+ ref,
12984
+ className: "ff-panel__input ff-panel__input--expression",
12985
+ rows,
12986
+ value: value ?? "",
12987
+ placeholder: placeholder ?? "{{ $json.field }}",
12988
+ spellCheck: false,
12989
+ role: "combobox",
12990
+ "aria-expanded": open,
12991
+ "aria-autocomplete": "list",
12992
+ "aria-controls": open ? `${handle.id ?? handle["data-ff-field"]}-vars` : void 0,
12993
+ onChange: (e) => {
12994
+ onChange(e.target.value);
12995
+ sync(e.target);
12996
+ },
12997
+ onKeyDown,
12998
+ onClick: (e) => sync(e.currentTarget),
12999
+ onBlur: () => {
13000
+ window.setTimeout(() => setTrigger(null), 120);
13001
+ }
13002
+ }
13003
+ ),
13004
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ff-expression__bar", children: [
13005
+ /* @__PURE__ */ jsxRuntime.jsxs(
13006
+ "button",
13007
+ {
13008
+ type: "button",
13009
+ className: "ff-expression__helpbtn",
13010
+ "aria-expanded": showHelp,
13011
+ onClick: () => setShowHelp((s) => !s),
13012
+ children: [
13013
+ "{{ }}",
13014
+ " reference"
13015
+ ]
13016
+ }
13017
+ ),
13018
+ variables.length > 1 && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "ff-expression__hint", children: [
13019
+ "type ",
13020
+ /* @__PURE__ */ jsxRuntime.jsx("code", { children: "{{" }),
13021
+ " for ",
13022
+ variables.length,
13023
+ " variables"
13024
+ ] })
13025
+ ] }),
13026
+ open && /* @__PURE__ */ jsxRuntime.jsx(
13027
+ "ul",
13028
+ {
13029
+ className: "ff-expression__menu",
13030
+ id: `${handle.id ?? handle["data-ff-field"]}-vars`,
13031
+ role: "listbox",
13032
+ "data-ff-expression-menu": handle["data-ff-field"],
13033
+ children: matches.map((v2, i) => /* @__PURE__ */ jsxRuntime.jsx("li", { role: "option", "aria-selected": i === active, children: /* @__PURE__ */ jsxRuntime.jsxs(
13034
+ "button",
13035
+ {
13036
+ type: "button",
13037
+ className: `ff-expression__option${i === active ? " is-active" : ""}`,
13038
+ "data-ff-expression-option": v2.path,
13039
+ onMouseDown: (e) => {
13040
+ e.preventDefault();
13041
+ choose(v2);
13042
+ },
13043
+ children: [
13044
+ /* @__PURE__ */ jsxRuntime.jsx("code", { className: "ff-expression__path", children: v2.path }),
13045
+ v2.source && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ff-expression__source", children: v2.source }),
13046
+ v2.description && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ff-expression__desc", children: v2.description })
13047
+ ]
13048
+ }
13049
+ ) }, v2.expression))
13050
+ }
13051
+ ),
13052
+ showHelp && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ff-expression__help", role: "note", "data-ff-expression-help": true, children: [
13053
+ /* @__PURE__ */ jsxRuntime.jsx("dl", { children: help.forms.map((f) => /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
13054
+ /* @__PURE__ */ jsxRuntime.jsx("dt", { children: /* @__PURE__ */ jsxRuntime.jsx("code", { children: f.syntax }) }),
13055
+ /* @__PURE__ */ jsxRuntime.jsx("dd", { children: f.meaning })
13056
+ ] }, f.syntax)) }),
13057
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "ff-expression__note", children: help.note })
13058
+ ] })
13059
+ ] });
13060
+ }
12814
13061
  function ConfigFieldRenderer({
12815
13062
  field,
12816
13063
  value,
@@ -12818,7 +13065,9 @@ function ConfigFieldRenderer({
12818
13065
  id: id2,
12819
13066
  renderCredentialField,
12820
13067
  renderDocumentField,
12821
- fieldRenderers
13068
+ fieldRenderers,
13069
+ graph,
13070
+ nodeId
12822
13071
  }) {
12823
13072
  const handle = { id: id2, "data-ff-field": field.key };
12824
13073
  const custom = fieldRenderers?.[field.type];
@@ -12900,15 +13149,14 @@ function ConfigFieldRenderer({
12900
13149
  return /* @__PURE__ */ jsxRuntime.jsx(JsonField, { value, onChange, rows: field.rows, handle });
12901
13150
  case "expression":
12902
13151
  return /* @__PURE__ */ jsxRuntime.jsx(
12903
- "textarea",
13152
+ ExpressionField,
12904
13153
  {
12905
- ...handle,
12906
- className: "ff-panel__input ff-panel__input--expression",
12907
- rows: 2,
13154
+ handle,
12908
13155
  value: value ?? "",
13156
+ onChange,
12909
13157
  placeholder: field.example ?? "{{ $json.field }}",
12910
- spellCheck: false,
12911
- onChange: (e) => onChange(e.target.value)
13158
+ graph,
13159
+ nodeId
12912
13160
  }
12913
13161
  );
12914
13162
  case "credential":
@@ -12935,7 +13183,9 @@ function ConfigFieldRenderer({
12935
13183
  onChange,
12936
13184
  renderCredentialField,
12937
13185
  renderDocumentField,
12938
- fieldRenderers
13186
+ fieldRenderers,
13187
+ graph,
13188
+ nodeId
12939
13189
  }
12940
13190
  );
12941
13191
  case "keyvalue":
@@ -12988,6 +13238,8 @@ function RepeaterField({
12988
13238
  field,
12989
13239
  value,
12990
13240
  onChange,
13241
+ graph,
13242
+ nodeId,
12991
13243
  renderCredentialField,
12992
13244
  renderDocumentField,
12993
13245
  fieldRenderers
@@ -13068,6 +13320,8 @@ function RepeaterField({
13068
13320
  /* @__PURE__ */ jsxRuntime.jsx(
13069
13321
  ConfigFieldRenderer,
13070
13322
  {
13323
+ graph,
13324
+ nodeId,
13071
13325
  field: sub,
13072
13326
  value: row[sub.key],
13073
13327
  onChange: (cell) => setCell(i, sub.key, cell),
@@ -13182,7 +13436,7 @@ function JsonField({
13182
13436
  return "";
13183
13437
  }
13184
13438
  }, [value]);
13185
- const [text, setText] = ReactExports.useState(serialized);
13439
+ const [text2, setText] = ReactExports.useState(serialized);
13186
13440
  const [error, setError] = ReactExports.useState(null);
13187
13441
  const [touched, setTouched] = ReactExports.useState(false);
13188
13442
  const lastProps = ReactExports.useRef(serialized);
@@ -13220,7 +13474,7 @@ function JsonField({
13220
13474
  ...handle,
13221
13475
  className: "ff-panel__input ff-panel__input--json",
13222
13476
  rows: rows ?? 6,
13223
- value: text,
13477
+ value: text2,
13224
13478
  spellCheck: false,
13225
13479
  "aria-invalid": showError || void 0,
13226
13480
  "aria-describedby": showError ? errorId : void 0,
@@ -13249,6 +13503,7 @@ function NodeConfigPanel({
13249
13503
  renderCredentialField,
13250
13504
  renderDocumentField,
13251
13505
  fieldRenderers,
13506
+ graph,
13252
13507
  className,
13253
13508
  style: style2
13254
13509
  }) {
@@ -13354,7 +13609,9 @@ function NodeConfigPanel({
13354
13609
  onChange: (v2) => setConfigValue(field.key, v2),
13355
13610
  renderCredentialField,
13356
13611
  renderDocumentField: documentField,
13357
- fieldRenderers
13612
+ fieldRenderers,
13613
+ graph,
13614
+ nodeId: node.id
13358
13615
  }
13359
13616
  )
13360
13617
  ] }, field.key))
@@ -14646,8 +14903,8 @@ function FlowEditorInner({
14646
14903
  x: labelEdit.x,
14647
14904
  y: labelEdit.y,
14648
14905
  initial: flow.edges.find((e) => e.id === labelEdit.edgeId)?.label ?? "",
14649
- onCommit: (text) => {
14650
- api.setEdgeLabel(labelEdit.edgeId, text);
14906
+ onCommit: (text2) => {
14907
+ api.setEdgeLabel(labelEdit.edgeId, text2);
14651
14908
  setLabelEdit(null);
14652
14909
  },
14653
14910
  onCancel: () => setLabelEdit(null)
@@ -14667,6 +14924,7 @@ function FlowEditorInner({
14667
14924
  node: api.selected,
14668
14925
  onChange: api.updateNode,
14669
14926
  fieldRenderers,
14927
+ graph: flow,
14670
14928
  onDelete: builtins.delete === false ? void 0 : (n) => api.deleteNodes([n.id])
14671
14929
  }
14672
14930
  ),
@@ -14751,7 +15009,7 @@ function EdgeLabelEditor({
14751
15009
  onCommit,
14752
15010
  onCancel
14753
15011
  }) {
14754
- const [text, setText] = ReactExports.useState(initial);
15012
+ const [text2, setText] = ReactExports.useState(initial);
14755
15013
  const ref = ReactExports.useRef(null);
14756
15014
  ReactExports.useEffect(() => {
14757
15015
  ref.current?.focus();
@@ -14768,17 +15026,17 @@ function EdgeLabelEditor({
14768
15026
  {
14769
15027
  ref,
14770
15028
  className: "ff-panel__input",
14771
- value: text,
15029
+ value: text2,
14772
15030
  placeholder: "Label this connection",
14773
15031
  "aria-label": "Connection label",
14774
15032
  "data-action": "edge-label-input",
14775
15033
  onChange: (e) => setText(e.target.value),
14776
15034
  onKeyDown: (e) => {
14777
15035
  e.stopPropagation();
14778
- if (e.key === "Enter") onCommit(text);
15036
+ if (e.key === "Enter") onCommit(text2);
14779
15037
  if (e.key === "Escape") onCancel();
14780
15038
  },
14781
- onBlur: () => onCommit(text)
15039
+ onBlur: () => onCommit(text2)
14782
15040
  }
14783
15041
  )
14784
15042
  }
@@ -15013,6 +15271,67 @@ var flowKeys = {
15013
15271
  run: (runId) => ["flow", "runs", runId]
15014
15272
  };
15015
15273
 
15274
+ // src/expressions/expr.ts
15275
+ var WHOLE = /^\{\{\s*([\s\S]*?)\s*\}\}$/;
15276
+ var EACH = /\{\{\s*([\s\S]*?)\s*\}\}/g;
15277
+ var FALSY_STRINGS = /* @__PURE__ */ new Set(["", "0", "false", "no", "off", "null"]);
15278
+ function resolvePath(path, context) {
15279
+ const trimmed = path.trim();
15280
+ if (trimmed === "") return null;
15281
+ const segments = trimmed.split(".");
15282
+ let cursor;
15283
+ const head = segments[0];
15284
+ if (head === "$json" || head === "$input") {
15285
+ cursor = context !== null && typeof context === "object" && "in" in context ? context.in : context;
15286
+ segments.shift();
15287
+ } else {
15288
+ cursor = context;
15289
+ }
15290
+ for (const segment of segments) {
15291
+ if (cursor === null || cursor === void 0) return null;
15292
+ if (typeof cursor !== "object") return null;
15293
+ const next = cursor[segment];
15294
+ if (next === void 0) return null;
15295
+ cursor = next;
15296
+ }
15297
+ return cursor === void 0 ? null : cursor;
15298
+ }
15299
+ function evaluateExpression(template, context) {
15300
+ if (typeof template !== "string") return template;
15301
+ const whole = WHOLE.exec(template.trim());
15302
+ if (whole) return resolvePath(whole[1] ?? "", context);
15303
+ return template.replace(EACH, (_m, path) => stringify(resolvePath(path, context)));
15304
+ }
15305
+ function truthy(value) {
15306
+ if (typeof value === "boolean") return value;
15307
+ if (value === null || value === void 0) return false;
15308
+ if (typeof value === "string") return !FALSY_STRINGS.has(value.trim().toLowerCase());
15309
+ if (Array.isArray(value)) return value.length > 0;
15310
+ if (typeof value === "number") return value !== 0;
15311
+ return Boolean(value);
15312
+ }
15313
+ function text(value) {
15314
+ return stringify(value);
15315
+ }
15316
+ function stringify(value) {
15317
+ if (typeof value === "string") return value;
15318
+ if (typeof value === "boolean") return value ? "true" : "false";
15319
+ if (value === null || value === void 0) return "";
15320
+ if (typeof value === "number" || typeof value === "bigint") return String(value);
15321
+ try {
15322
+ return JSON.stringify(value) ?? "";
15323
+ } catch {
15324
+ return "";
15325
+ }
15326
+ }
15327
+ function evaluateConfig(config, context) {
15328
+ const out = {};
15329
+ for (const [key, value] of Object.entries(config)) {
15330
+ out[key] = Array.isArray(value) ? value.map((v2) => evaluateExpression(v2, context)) : value !== null && typeof value === "object" ? evaluateConfig(value, context) : evaluateExpression(value, context);
15331
+ }
15332
+ return out;
15333
+ }
15334
+
15016
15335
  // src/index.ts
15017
15336
  registerBuiltinKinds();
15018
15337
  /*! Bundled license information:
@@ -15026,6 +15345,7 @@ exports.ActionNode = ActionNode;
15026
15345
  exports.BUILTIN_KINDS = BUILTIN_KINDS;
15027
15346
  exports.ConfigFieldRenderer = ConfigFieldRenderer;
15028
15347
  exports.DecisionNode = DecisionNode;
15348
+ exports.ExpressionField = ExpressionField;
15029
15349
  exports.FlowCanvas = FlowCanvas;
15030
15350
  exports.FlowEditor = FlowEditor;
15031
15351
  exports.FlowRunControls = FlowRunControls;
@@ -15049,6 +15369,8 @@ exports.WORKFLOW_SCHEMA_VERSION = WORKFLOW_SCHEMA_VERSION;
15049
15369
  exports.alignNodes = alignNodes;
15050
15370
  exports.applyOutputsToNodes = applyOutputsToNodes;
15051
15371
  exports.applyStatusesToNodes = applyStatusesToNodes;
15372
+ exports.availableVariables = availableVariables;
15373
+ exports.baseVariables = baseVariables;
15052
15374
  exports.buildNodeTypes = buildNodeTypes;
15053
15375
  exports.categoryAccent = categoryAccent;
15054
15376
  exports.clearNodeKindOverrides = clearNodeKindOverrides;
@@ -15060,8 +15382,11 @@ exports.defaultConfigFor = defaultConfigFor;
15060
15382
  exports.defaultNodeTypes = defaultNodeTypes;
15061
15383
  exports.defaultPortCompatibility = defaultPortCompatibility;
15062
15384
  exports.defineNode = defineNode;
15385
+ exports.describeExpressionGrammar = describeExpressionGrammar;
15063
15386
  exports.distributeNodes = distributeNodes;
15064
15387
  exports.encodePause = encodePause;
15388
+ exports.evaluateConfig = evaluateConfig;
15389
+ exports.evaluateExpression = evaluateExpression;
15065
15390
  exports.exportWorkflow = exportWorkflow;
15066
15391
  exports.flowKeys = flowKeys;
15067
15392
  exports.flowLive = flowLive;
@@ -15074,6 +15399,7 @@ exports.listNodeKinds = listNodeKinds;
15074
15399
  exports.migrateSchema = migrateSchema;
15075
15400
  exports.onNodeKindsChanged = onNodeKindsChanged;
15076
15401
  exports.onRichInputAdapterChanged = onRichInputAdapterChanged;
15402
+ exports.outputFieldsFor = outputFieldsFor;
15077
15403
  exports.overrideNodeKind = overrideNodeKind;
15078
15404
  exports.paletteDropHandlers = paletteDropHandlers;
15079
15405
  exports.pauseForHuman = pauseForHuman;
@@ -15082,9 +15408,12 @@ exports.registerBuiltinKinds = registerBuiltinKinds;
15082
15408
  exports.registerNodeKind = registerNodeKind;
15083
15409
  exports.registerRichInputAdapter = registerRichInputAdapter;
15084
15410
  exports.resolveNodePorts = resolveNodePorts;
15411
+ exports.resolvePath = resolvePath;
15085
15412
  exports.resolvePortSpec = resolvePortSpec;
15086
15413
  exports.runCohort = runCohort;
15087
15414
  exports.runFlow = runFlow;
15415
+ exports.text = text;
15416
+ exports.truthy = truthy;
15088
15417
  exports.useFlowEditor = useFlowEditor;
15089
15418
  exports.useFlowEditorOptional = useFlowEditorOptional;
15090
15419
  exports.useFlowHistory = useFlowHistory;