@lunnoa/client 2.4.0 → 3.0.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/react.js CHANGED
@@ -76,16 +76,21 @@ var LunnoaApiError = class extends Error {
76
76
  method;
77
77
  /** Path of the failed request (without the base URL). */
78
78
  path;
79
+ /** Present on definition-sync 409s when the server sends the current revision. */
80
+ currentRevision;
79
81
  constructor(args) {
80
82
  const detail = extractMessage(args.body);
83
+ const currentRevision = extractCurrentRevision(args.body);
84
+ const shown = [detail, currentRevision ? `currentRevision: ${currentRevision}` : void 0].filter(Boolean).join(" ");
81
85
  super(
82
- `Lunnoa API request failed: ${args.method} ${args.path} \u2192 ${args.status}${detail ? ` (${detail})` : ""}`
86
+ `Lunnoa API request failed: ${args.method} ${args.path} \u2192 ${args.status}${shown ? ` (${shown})` : ""}`
83
87
  );
84
88
  this.name = "LunnoaApiError";
85
89
  this.status = args.status;
86
90
  this.body = args.body;
87
91
  this.method = args.method;
88
92
  this.path = args.path;
93
+ this.currentRevision = currentRevision;
89
94
  }
90
95
  /** True for 401 responses (missing, revoked, or expired credential). */
91
96
  get isUnauthorized() {
@@ -104,6 +109,11 @@ var LunnoaApiError = class extends Error {
104
109
  return this.status === 429;
105
110
  }
106
111
  };
112
+ function extractCurrentRevision(body) {
113
+ if (!body || typeof body !== "object") return void 0;
114
+ const revision = body.currentRevision;
115
+ return typeof revision === "string" ? revision : void 0;
116
+ }
107
117
  function extractMessage(body) {
108
118
  if (body && typeof body === "object") {
109
119
  const record = body;
@@ -580,10 +590,55 @@ var ActionsResource = class {
580
590
  }
581
591
  };
582
592
 
593
+ // src/define/files.ts
594
+ function hasFileAgentSignals(record) {
595
+ if (record.instructions && typeof record.instructions === "object") return true;
596
+ if (record.settings && typeof record.settings === "object") return true;
597
+ if ("workflows" in record || "agents" in record || "knowledge" in record || "variables" in record || "objects" in record || "objectTypes" in record || "mcpServers" in record || "a2aAgents" in record) {
598
+ return true;
599
+ }
600
+ return Array.isArray(record.tools) && record.tools.some(
601
+ (tool) => tool && typeof tool === "object" && !("kind" in tool) && !("use" in tool)
602
+ );
603
+ }
604
+ function isAgentFileShape(config) {
605
+ if (!config || typeof config !== "object") return false;
606
+ return hasFileAgentSignals(config);
607
+ }
608
+
583
609
  // src/define/index.ts
584
610
  var DEFINE_KINDS = {
611
+ connection: "connection",
585
612
  action: "action",
613
+ trigger: "trigger",
586
614
  workflow: "workflow"};
615
+ var PATH_CONDITIONS = [
616
+ "contains",
617
+ "does_not_contain",
618
+ "equals",
619
+ "does_not_equal",
620
+ "exists",
621
+ "does_not_exist",
622
+ "starts_with",
623
+ "ends_with",
624
+ "is_empty",
625
+ "is_not_empty",
626
+ "comes_before",
627
+ "comes_after",
628
+ "is_in_the_past",
629
+ "is_in_the_future",
630
+ "is_true",
631
+ "is_false",
632
+ "is_truthy",
633
+ "is_falsey",
634
+ "is_greater_than",
635
+ "is_less_than",
636
+ "is_greater_than_or_equal_to",
637
+ "is_less_than_or_equal_to",
638
+ "matches_regex",
639
+ "does_not_match_regex"
640
+ ];
641
+ new Set(PATH_CONDITIONS);
587
642
  function resolveActionAppId(action) {
588
643
  if (action.appId?.trim()) {
589
644
  return action.appId.trim();
@@ -684,10 +739,24 @@ function unwrapTool(tool) {
684
739
  }
685
740
  return tool;
686
741
  }
742
+ function syncFields(options) {
743
+ return {
744
+ ...options?.baseRevision ? { baseRevision: options.baseRevision } : {},
745
+ ...options?.force ? { force: true } : {}
746
+ };
747
+ }
748
+ function idsOf(links) {
749
+ if (!links) return void 0;
750
+ return links.map((link) => link.id);
751
+ }
687
752
  function agentDefinitionToUpsert(def, options) {
753
+ if (isAgentFileShape(def)) {
754
+ return fileAgentToUpsert(def, options);
755
+ }
688
756
  const tools = [];
689
757
  const workflowIds = [...options?.workflowIds ?? []];
690
- for (const tool of def.tools ?? []) {
758
+ const authored = def;
759
+ for (const tool of authored.tools ?? []) {
691
760
  const unwrapped = unwrapTool(tool);
692
761
  if (unwrapped.kind === DEFINE_KINDS.workflow) {
693
762
  continue;
@@ -703,17 +772,64 @@ function agentDefinitionToUpsert(def, options) {
703
772
  });
704
773
  }
705
774
  }
775
+ return {
776
+ slug: authored.slug,
777
+ name: authored.name,
778
+ description: authored.description,
779
+ instructions: authored.instructions,
780
+ model: authored.model,
781
+ aiConnectionId: authored.aiConnection?.id,
782
+ connectionIds: authored.connections?.map((c) => c.id).filter((id) => Boolean(id)),
783
+ workflowIds: workflowIds.length ? workflowIds : void 0,
784
+ tools,
785
+ codeSource: options?.codeSource ?? "generated",
786
+ ...syncFields(options)
787
+ };
788
+ }
789
+ function fileAgentToUpsert(def, options) {
790
+ if (typeof def.instructions !== "string") {
791
+ throw new Error(
792
+ "Agent instructions are still a file reference. The CLI resolves these before push."
793
+ );
794
+ }
795
+ const tools = [];
796
+ for (const tool of def.tools ?? []) {
797
+ const action = {
798
+ id: tool.id,
799
+ appId: tool.appId,
800
+ connection: tool.connection?.id ? { id: tool.connection.id } : void 0
801
+ };
802
+ tools.push({
803
+ key: tool.key,
804
+ appId: resolveActionAppId(action),
805
+ actionId: resolveActionId(action),
806
+ connectionId: tool.connection?.id,
807
+ name: tool.name,
808
+ description: tool.description,
809
+ input: tool.input
810
+ });
811
+ }
706
812
  return {
707
813
  slug: def.slug,
708
814
  name: def.name,
709
- description: def.description,
815
+ // A file is the whole definition. Omitted description and model are empty.
816
+ description: def.description ?? null,
710
817
  instructions: def.instructions,
711
- model: def.model,
818
+ model: def.model ?? null,
712
819
  aiConnectionId: def.aiConnection?.id,
713
- connectionIds: def.connections?.map((c) => c.id).filter((id) => Boolean(id)),
714
- workflowIds: workflowIds.length ? workflowIds : void 0,
715
- tools,
716
- managedByCode: true
820
+ ...def.settings ? { settings: def.settings } : {},
821
+ ...def.tools ? { tools } : {},
822
+ ...def.workflows ? { workflows: def.workflows } : {},
823
+ ...def.agents ? { agents: def.agents } : {},
824
+ ...def.connections ? { connectionIds: idsOf(def.connections) } : {},
825
+ ...def.knowledge ? { knowledgeIds: idsOf(def.knowledge) } : {},
826
+ ...def.variables ? { variableIds: idsOf(def.variables) } : {},
827
+ ...def.objects ? { objectIds: idsOf(def.objects) } : {},
828
+ ...def.objectTypes ? { objectTypeIds: idsOf(def.objectTypes) } : {},
829
+ ...def.mcpServers ? { mcpServers: def.mcpServers } : {},
830
+ ...def.a2aAgents ? { a2aAgentIds: idsOf(def.a2aAgents) } : {},
831
+ codeSource: "synced",
832
+ ...syncFields(options)
717
833
  };
718
834
  }
719
835
  var AgentsResource = class {
@@ -733,15 +849,22 @@ var AgentsResource = class {
733
849
  query: serializeListQuery(options)
734
850
  });
735
851
  }
852
+ /** Exported definition for `lunnoa/agents/<slug>.json`. */
853
+ getDefinition(slug) {
854
+ return this.http.get(
855
+ `/api/agents/slug/${encodeURIComponent(slug)}/definition`
856
+ );
857
+ }
736
858
  /**
737
859
  * Upsert an agent by slug inside the workspace on the API key.
738
860
  * Prefer the CLI (`npx @lunnoa/client agents deploy`) for happy-path deploy.
739
861
  */
740
862
  upsertBySlug(body, options) {
741
863
  const payload = "kind" in body && body.kind === "agent" ? agentDefinitionToUpsert(body, options) : body;
742
- return this.http.post("/api/agents/upsert", {
743
- body: payload
744
- });
864
+ return this.http.post(
865
+ "/api/agents/upsert",
866
+ { body: payload }
867
+ );
745
868
  }
746
869
  };
747
870
 
@@ -1843,6 +1966,11 @@ function stepFromDefinition(step) {
1843
1966
  ...step.name ? { name: step.name } : {}
1844
1967
  };
1845
1968
  }
1969
+ if (step.paths?.length) {
1970
+ throw new Error(
1971
+ "runs.start runs straight lines only. Deploy workflows with paths, then execute them."
1972
+ );
1973
+ }
1846
1974
  const use = step.use;
1847
1975
  const input = {
1848
1976
  ...use.input ?? {},
@@ -1860,6 +1988,11 @@ function stepFromDefinition(step) {
1860
1988
  }
1861
1989
  function normalizeStartRunInput(input) {
1862
1990
  if (isWorkflowDefinition(input)) {
1991
+ if ("format" in input && input.format === "graph") {
1992
+ throw new Error(
1993
+ "runs.start runs straight lines only. Push the graph workflow, then execute it."
1994
+ );
1995
+ }
1863
1996
  return {
1864
1997
  steps: input.steps.map(stepFromDefinition),
1865
1998
  name: input.name ?? input.slug
@@ -2051,6 +2184,18 @@ var WorkflowAppsResource = class {
2051
2184
  }
2052
2185
  };
2053
2186
 
2187
+ // src/resources/code-sync.ts
2188
+ var CodeSyncResource = class {
2189
+ constructor(http) {
2190
+ this.http = http;
2191
+ }
2192
+ http;
2193
+ /** Workspace id and whether synced resources are locked in the UI. */
2194
+ context() {
2195
+ return this.http.get("/api/code-sync/context");
2196
+ }
2197
+ };
2198
+
2054
2199
  // src/resources/workflows.ts
2055
2200
  function triggerToUpsertInput(trigger) {
2056
2201
  return {
@@ -2063,8 +2208,38 @@ function triggerToUpsertInput(trigger) {
2063
2208
  ...trigger.description ? { description: trigger.description } : {}
2064
2209
  };
2065
2210
  }
2211
+ function asAction(use) {
2212
+ if (use && typeof use === "object" && "kind" in use && use.kind === DEFINE_KINDS.action) {
2213
+ return use;
2214
+ }
2215
+ const fileUse = use;
2216
+ return {
2217
+ kind: DEFINE_KINDS.action,
2218
+ id: fileUse.id,
2219
+ appId: fileUse.appId,
2220
+ connection: fileUse.connection?.id ? { kind: DEFINE_KINDS.connection, id: fileUse.connection.id } : void 0
2221
+ };
2222
+ }
2223
+ function asTrigger(trigger) {
2224
+ if ("kind" in trigger && trigger.kind === DEFINE_KINDS.trigger) {
2225
+ return trigger;
2226
+ }
2227
+ const fileTrigger = trigger;
2228
+ return {
2229
+ kind: DEFINE_KINDS.trigger,
2230
+ id: fileTrigger.id,
2231
+ appId: fileTrigger.appId,
2232
+ connection: fileTrigger.connection?.id ? { kind: DEFINE_KINDS.connection, id: fileTrigger.connection.id } : void 0,
2233
+ input: fileTrigger.input,
2234
+ inputs: fileTrigger.inputs,
2235
+ name: fileTrigger.name,
2236
+ description: fileTrigger.description
2237
+ };
2238
+ }
2066
2239
  function stepToUpsertInput(step) {
2067
- const use = step.use;
2240
+ const use = asAction(step.use);
2241
+ const retry = "retrySettings" in step ? step.retrySettings : void 0;
2242
+ const continueOnFailure = "continueOnFailure" in step ? step.continueOnFailure : void 0;
2068
2243
  return {
2069
2244
  ...step.key ? { key: step.key } : {},
2070
2245
  appId: resolveActionAppId(use),
@@ -2075,18 +2250,60 @@ function stepToUpsertInput(step) {
2075
2250
  ...step.input ?? {}
2076
2251
  },
2077
2252
  name: step.name ?? use.name,
2078
- description: use.description
2253
+ description: step.description ?? use.description,
2254
+ ...retry ? { retrySettings: retry } : {},
2255
+ ...continueOnFailure ? { continueOnFailure } : {},
2256
+ ...step.paths?.length ? {
2257
+ paths: step.paths.map((path) => ({
2258
+ label: path.label,
2259
+ ...path.when ? { when: path.when } : {},
2260
+ steps: path.steps.map(
2261
+ (nested) => stepToUpsertInput(nested)
2262
+ )
2263
+ }))
2264
+ } : {}
2079
2265
  };
2080
2266
  }
2081
- function workflowDefinitionToUpsert(def) {
2267
+ function syncFields2(options) {
2082
2268
  return {
2083
- slug: def.slug,
2084
- name: def.name,
2085
- description: def.description,
2086
- isActive: def.isActive,
2087
- ...def.trigger ? { trigger: triggerToUpsertInput(def.trigger) } : {},
2088
- managedByCode: true,
2089
- steps: def.steps.map(stepToUpsertInput)
2269
+ ...options?.baseRevision ? { baseRevision: options.baseRevision } : {},
2270
+ ...options?.force ? { force: true } : {}
2271
+ };
2272
+ }
2273
+ function workflowDefinitionToUpsert(def, options) {
2274
+ if ("format" in def && def.format === "graph") {
2275
+ return {
2276
+ slug: def.slug,
2277
+ name: def.name,
2278
+ // A file is the whole definition: an omitted description is empty, not "leave it".
2279
+ description: def.description ?? null,
2280
+ codeSource: "synced",
2281
+ graph: { nodes: def.nodes, edges: def.edges ?? [] },
2282
+ ...syncFields2(options)
2283
+ };
2284
+ }
2285
+ if ("format" in def && def.format === "steps") {
2286
+ const file = def;
2287
+ return {
2288
+ slug: file.slug,
2289
+ name: file.name,
2290
+ description: file.description ?? null,
2291
+ ...file.trigger ? { trigger: triggerToUpsertInput(asTrigger(file.trigger)) } : {},
2292
+ codeSource: "synced",
2293
+ steps: file.steps.map((step) => stepToUpsertInput(step)),
2294
+ ...syncFields2(options)
2295
+ };
2296
+ }
2297
+ const authored = def;
2298
+ return {
2299
+ slug: authored.slug,
2300
+ name: authored.name,
2301
+ description: authored.description,
2302
+ isActive: authored.isActive,
2303
+ ...authored.trigger ? { trigger: triggerToUpsertInput(authored.trigger) } : {},
2304
+ codeSource: options?.codeSource ?? "generated",
2305
+ ...syncFields2(options),
2306
+ steps: authored.steps.map((step) => stepToUpsertInput(step))
2090
2307
  };
2091
2308
  }
2092
2309
  var WorkflowsResource = class {
@@ -2111,15 +2328,22 @@ var WorkflowsResource = class {
2111
2328
  query: serializeListQuery(options)
2112
2329
  });
2113
2330
  }
2331
+ /** Exported definition for `lunnoa/workflows/<slug>.json`. */
2332
+ getDefinition(slug) {
2333
+ return this.http.get(
2334
+ `/api/workflows/slug/${encodeURIComponent(slug)}/definition`
2335
+ );
2336
+ }
2114
2337
  /**
2115
2338
  * Upsert a workflow by slug inside the workspace on the API key.
2116
2339
  * Prefer the CLI (`npx @lunnoa/client workflows deploy`) for happy-path deploy.
2117
2340
  */
2118
- upsertBySlug(body) {
2119
- const payload = "kind" in body && body.kind === "workflow" ? workflowDefinitionToUpsert(body) : body;
2120
- return this.http.post("/api/workflows/upsert", {
2121
- body: payload
2122
- });
2341
+ upsertBySlug(body, options) {
2342
+ const payload = "kind" in body && body.kind === "workflow" ? workflowDefinitionToUpsert(body, options) : body;
2343
+ return this.http.post(
2344
+ "/api/workflows/upsert",
2345
+ { body: payload }
2346
+ );
2123
2347
  }
2124
2348
  /**
2125
2349
  * Starts a new execution of the workflow with the given trigger inputs.
@@ -2185,6 +2409,8 @@ var LunnoaClient = class {
2185
2409
  aiConnections;
2186
2410
  workflowApps;
2187
2411
  discovery;
2412
+ /** Workspace id for the definitions lock, and the code-sync lock flag. */
2413
+ codeSync;
2188
2414
  constructor(options) {
2189
2415
  this.http = new HttpClient(options);
2190
2416
  this.auth = new AuthResource(this.http, this.http.tokenStore);
@@ -2208,6 +2434,7 @@ var LunnoaClient = class {
2208
2434
  this.connections = new ConnectionsResource(this.http);
2209
2435
  this.aiConnections = new AiConnectionsResource(this.http);
2210
2436
  this.discovery = new DiscoveryResource(this.http);
2437
+ this.codeSync = new CodeSyncResource(this.http);
2211
2438
  }
2212
2439
  };
2213
2440