@lunnoa/client 0.0.0-development

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/cli.js ADDED
@@ -0,0 +1,2635 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/index.ts
4
+ import { mkdir, writeFile } from "fs/promises";
5
+ import { resolve as resolve2 } from "path";
6
+
7
+ // src/core/auth.ts
8
+ var API_KEY_PREFIX = "lna_";
9
+ function isBrowserEnvironment() {
10
+ return typeof window !== "undefined";
11
+ }
12
+ function assertApiKeyAllowedInEnvironment(apiKey) {
13
+ if (isBrowserEnvironment()) {
14
+ throw new Error(
15
+ "Refusing to use a Lunnoa API key (lna_...) in a browser environment. API keys are server-side credentials: anyone opening dev tools could read the key and act as the service account across its whole workspace. Either call the Lunnoa API from your own backend which holds the key (backend-for-frontend pattern), or authenticate the end user with a Lunnoa user JWT and pass it as `accessToken`."
16
+ );
17
+ }
18
+ if (!apiKey.startsWith(API_KEY_PREFIX)) {
19
+ throw new Error(
20
+ `Invalid Lunnoa API key: expected it to start with "${API_KEY_PREFIX}". If you are passing a user JWT, use the \`accessToken\` option instead of \`apiKey\`.`
21
+ );
22
+ }
23
+ }
24
+
25
+ // src/core/errors.ts
26
+ var LunnoaApiError = class extends Error {
27
+ /** HTTP status code of the failed response. */
28
+ status;
29
+ /** Parsed JSON error body when the server returned one, otherwise the raw text. */
30
+ body;
31
+ /** HTTP method of the failed request. */
32
+ method;
33
+ /** Path of the failed request (without the base URL). */
34
+ path;
35
+ constructor(args) {
36
+ const detail = extractMessage(args.body);
37
+ super(
38
+ `Lunnoa API request failed: ${args.method} ${args.path} \u2192 ${args.status}${detail ? ` (${detail})` : ""}`
39
+ );
40
+ this.name = "LunnoaApiError";
41
+ this.status = args.status;
42
+ this.body = args.body;
43
+ this.method = args.method;
44
+ this.path = args.path;
45
+ }
46
+ /** True for 401 responses (missing, revoked, or expired credential). */
47
+ get isUnauthorized() {
48
+ return this.status === 401;
49
+ }
50
+ /** True for 403 responses (missing permission or feature not licensed). */
51
+ get isForbidden() {
52
+ return this.status === 403;
53
+ }
54
+ /** True for 404 responses. */
55
+ get isNotFound() {
56
+ return this.status === 404;
57
+ }
58
+ /** True for 429 responses (rate limited). */
59
+ get isRateLimited() {
60
+ return this.status === 429;
61
+ }
62
+ };
63
+ function extractMessage(body) {
64
+ if (body && typeof body === "object") {
65
+ const record = body;
66
+ const candidate = record.message ?? record.error;
67
+ if (typeof candidate === "string") {
68
+ return candidate;
69
+ }
70
+ if (Array.isArray(candidate)) {
71
+ return candidate.filter((m) => typeof m === "string").join("; ");
72
+ }
73
+ }
74
+ if (typeof body === "string" && body.length > 0 && body.length < 300) {
75
+ return body;
76
+ }
77
+ return void 0;
78
+ }
79
+
80
+ // src/core/http.ts
81
+ var HttpClient = class {
82
+ baseUrl;
83
+ tokenStore;
84
+ /** Underlying fetch (shared with AI SDK transports when needed). */
85
+ fetch;
86
+ apiKey;
87
+ accessToken;
88
+ autoRefresh;
89
+ defaultHeaders;
90
+ refreshInFlight = null;
91
+ constructor(options) {
92
+ if (!options.baseUrl) {
93
+ throw new Error("LunnoaClient requires a baseUrl.");
94
+ }
95
+ if (!options.apiKey && !options.accessToken && !options.tokenStore) {
96
+ throw new Error(
97
+ "LunnoaClient requires an apiKey (server-side), an accessToken (user JWT), or a tokenStore (Pattern B)."
98
+ );
99
+ }
100
+ if (options.apiKey) {
101
+ assertApiKeyAllowedInEnvironment(options.apiKey);
102
+ }
103
+ if (typeof options.accessToken === "string" && options.accessToken.startsWith(API_KEY_PREFIX)) {
104
+ throw new Error(
105
+ "The accessToken option received an lna_ API key. API keys must be passed as `apiKey` (and never in a browser); accessToken is for user JWTs only."
106
+ );
107
+ }
108
+ this.baseUrl = options.baseUrl.replace(/\/+$/, "");
109
+ this.apiKey = options.apiKey;
110
+ this.accessToken = options.accessToken;
111
+ this.tokenStore = options.tokenStore;
112
+ this.autoRefresh = options.autoRefresh ?? Boolean(options.tokenStore && !options.apiKey);
113
+ this.fetch = options.fetch ?? globalThis.fetch.bind(globalThis);
114
+ this.defaultHeaders = options.headers ?? {};
115
+ }
116
+ async resolveAuthHeader() {
117
+ if (this.apiKey) {
118
+ return `Bearer ${this.apiKey}`;
119
+ }
120
+ if (this.tokenStore) {
121
+ const fromStore = await this.tokenStore.getAccessToken();
122
+ if (!fromStore) {
123
+ throw new Error(
124
+ "No access token in the token store. Call lunnoa.auth.login(...) first, or seed the store with setTokens / createMemoryTokenStore({ accessToken })."
125
+ );
126
+ }
127
+ return `Bearer ${fromStore}`;
128
+ }
129
+ const token = typeof this.accessToken === "function" ? await this.accessToken() : this.accessToken;
130
+ if (!token) {
131
+ throw new Error("accessToken resolved to an empty value.");
132
+ }
133
+ return `Bearer ${token}`;
134
+ }
135
+ buildUrl(path, query) {
136
+ const url = new URL(this.baseUrl + path);
137
+ if (query) {
138
+ for (const [key, value] of Object.entries(query)) {
139
+ if (value !== void 0 && value !== null) {
140
+ url.searchParams.set(key, String(value));
141
+ }
142
+ }
143
+ }
144
+ return url.toString();
145
+ }
146
+ async request(method, path, options = {}) {
147
+ const url = this.buildUrl(path, options.query);
148
+ const headers = {
149
+ ...this.defaultHeaders,
150
+ ...options.headers
151
+ };
152
+ if (!options.skipAuth) {
153
+ headers.Authorization = await this.resolveAuthHeader();
154
+ }
155
+ const init = { method, headers, signal: options.signal };
156
+ if (options.body !== void 0) {
157
+ headers["Content-Type"] = "application/json";
158
+ init.body = JSON.stringify(options.body);
159
+ }
160
+ const response = await this.fetch(url, init);
161
+ if (!response.ok && response.status === 401 && !options.skipAuth && !options._retriedAfterRefresh && this.autoRefresh && this.tokenStore && !this.apiKey) {
162
+ const refreshed = await this.tryRefreshAccessToken();
163
+ if (refreshed) {
164
+ return this.request(method, path, {
165
+ ...options,
166
+ _retriedAfterRefresh: true
167
+ });
168
+ }
169
+ }
170
+ if (!response.ok) {
171
+ const body = await parseBodySafely(response);
172
+ throw new LunnoaApiError({
173
+ status: response.status,
174
+ body,
175
+ method,
176
+ path
177
+ });
178
+ }
179
+ if (options.raw) {
180
+ return response;
181
+ }
182
+ if (response.status === 204) {
183
+ return void 0;
184
+ }
185
+ return await parseBodySafely(response);
186
+ }
187
+ get(path, options) {
188
+ return this.request("GET", path, options);
189
+ }
190
+ post(path, options) {
191
+ return this.request("POST", path, options);
192
+ }
193
+ patch(path, options) {
194
+ return this.request("PATCH", path, options);
195
+ }
196
+ put(path, options) {
197
+ return this.request("PUT", path, options);
198
+ }
199
+ delete(path, options) {
200
+ return this.request("DELETE", path, options);
201
+ }
202
+ /**
203
+ * Single-flight refresh using the token store. Returns false when refresh
204
+ * is impossible or fails (store is cleared on failure).
205
+ */
206
+ async tryRefreshAccessToken() {
207
+ if (!this.tokenStore) {
208
+ return false;
209
+ }
210
+ if (!this.refreshInFlight) {
211
+ this.refreshInFlight = this.performRefresh().finally(() => {
212
+ this.refreshInFlight = null;
213
+ });
214
+ }
215
+ return this.refreshInFlight;
216
+ }
217
+ async performRefresh() {
218
+ const refreshToken = await this.tokenStore.getRefreshToken();
219
+ if (!refreshToken) {
220
+ return false;
221
+ }
222
+ try {
223
+ const url = this.buildUrl("/api/auth/refresh-token");
224
+ const response = await this.fetch(url, {
225
+ method: "POST",
226
+ headers: { "Content-Type": "application/json" },
227
+ body: JSON.stringify({ refreshToken })
228
+ });
229
+ if (!response.ok) {
230
+ await this.tokenStore.clear();
231
+ return false;
232
+ }
233
+ const body = await parseBodySafely(response);
234
+ if (!body?.access_token) {
235
+ await this.tokenStore.clear();
236
+ return false;
237
+ }
238
+ await this.tokenStore.setTokens({ accessToken: body.access_token });
239
+ return true;
240
+ } catch {
241
+ await this.tokenStore.clear();
242
+ return false;
243
+ }
244
+ }
245
+ };
246
+ async function parseBodySafely(response) {
247
+ const text = await response.text();
248
+ if (!text) {
249
+ return void 0;
250
+ }
251
+ try {
252
+ return JSON.parse(text);
253
+ } catch {
254
+ return text;
255
+ }
256
+ }
257
+
258
+ // src/streaming/sse.ts
259
+ async function* parseSseStream(stream) {
260
+ for await (const event of parseSseEvents(stream)) {
261
+ if (event.data !== null) {
262
+ yield event.data;
263
+ }
264
+ }
265
+ }
266
+ async function* parseSseEvents(stream) {
267
+ const reader = stream.getReader();
268
+ const decoder = new TextDecoder("utf-8", { fatal: false });
269
+ let buffer = "";
270
+ try {
271
+ while (true) {
272
+ const { done, value } = await reader.read();
273
+ if (done) {
274
+ break;
275
+ }
276
+ buffer += decoder.decode(value, { stream: true });
277
+ buffer = buffer.replace(/\r\n/g, "\n");
278
+ let boundary = buffer.indexOf("\n\n");
279
+ while (boundary !== -1) {
280
+ const rawEvent = buffer.slice(0, boundary);
281
+ buffer = buffer.slice(boundary + 2);
282
+ boundary = buffer.indexOf("\n\n");
283
+ const parsed = extractEvent(rawEvent);
284
+ if (parsed.data !== null || parsed.event !== "message") {
285
+ yield parsed;
286
+ }
287
+ }
288
+ }
289
+ const remaining = buffer.trim();
290
+ if (remaining) {
291
+ const parsed = extractEvent(remaining);
292
+ if (parsed.data !== null || parsed.event !== "message") {
293
+ yield parsed;
294
+ }
295
+ }
296
+ } finally {
297
+ reader.releaseLock();
298
+ }
299
+ }
300
+ function extractEvent(rawEvent) {
301
+ const dataLines = [];
302
+ let eventName = "message";
303
+ for (const line of rawEvent.split("\n")) {
304
+ if (line.startsWith(":")) {
305
+ continue;
306
+ }
307
+ if (line.startsWith("event:")) {
308
+ let value = line.slice("event:".length);
309
+ if (value.startsWith(" ")) {
310
+ value = value.slice(1);
311
+ }
312
+ if (value) {
313
+ eventName = value;
314
+ }
315
+ continue;
316
+ }
317
+ if (line.startsWith("data:")) {
318
+ let value = line.slice("data:".length);
319
+ if (value.startsWith(" ")) {
320
+ value = value.slice(1);
321
+ }
322
+ dataLines.push(value);
323
+ }
324
+ }
325
+ return {
326
+ event: eventName,
327
+ data: dataLines.length === 0 ? null : dataLines.join("\n")
328
+ };
329
+ }
330
+
331
+ // src/streaming/agent-chat.ts
332
+ var AgentChatStream = class {
333
+ constructor(response, options) {
334
+ this.response = response;
335
+ this.options = options;
336
+ this.runId = response.headers.get("X-Task-Run-Id");
337
+ }
338
+ response;
339
+ options;
340
+ /**
341
+ * Server run id of this turn (`X-Task-Run-Id` response header), when
342
+ * provided. Useful for diagnostics.
343
+ */
344
+ runId;
345
+ consumed = false;
346
+ [Symbol.asyncIterator]() {
347
+ return this.chunks();
348
+ }
349
+ async *chunks() {
350
+ if (this.consumed) {
351
+ throw new Error("This AgentChatStream has already been consumed.");
352
+ }
353
+ this.consumed = true;
354
+ const body = this.response.body;
355
+ if (!body) {
356
+ return;
357
+ }
358
+ for await (const data of parseSseStream(body)) {
359
+ if (this.options?.signal?.aborted) {
360
+ return;
361
+ }
362
+ if (data === "[DONE]") {
363
+ return;
364
+ }
365
+ let parsed;
366
+ try {
367
+ parsed = JSON.parse(data);
368
+ } catch {
369
+ continue;
370
+ }
371
+ if (parsed && typeof parsed === "object" && "type" in parsed) {
372
+ yield parsed;
373
+ }
374
+ }
375
+ }
376
+ /**
377
+ * The same chunks as a web `ReadableStream`, ready for the AI SDK's
378
+ * `readUIMessageStream({ stream })` to assemble into `UIMessage` snapshots.
379
+ */
380
+ toUIMessageStream() {
381
+ const iterator = this.chunks();
382
+ return new ReadableStream({
383
+ async pull(controller) {
384
+ const { done, value } = await iterator.next();
385
+ if (done) {
386
+ controller.close();
387
+ } else {
388
+ controller.enqueue(value);
389
+ }
390
+ },
391
+ async cancel() {
392
+ await iterator.return();
393
+ }
394
+ });
395
+ }
396
+ /** Collects the full text of the turn (concatenated `text-delta` chunks). */
397
+ async text() {
398
+ let text = "";
399
+ for await (const chunk of this.chunks()) {
400
+ if (chunk.type === "text-delta" && typeof chunk.delta === "string") {
401
+ text += chunk.delta;
402
+ }
403
+ }
404
+ return text;
405
+ }
406
+ };
407
+ var AgentChatClient = class {
408
+ constructor(http) {
409
+ this.http = http;
410
+ }
411
+ http;
412
+ /**
413
+ * Sends a user message to an agent task and streams the assistant turn.
414
+ * Creates the task row when `taskId` does not exist yet (client-generated
415
+ * UUIDs are supported, mirroring the bundled UI).
416
+ *
417
+ * `message` may be a plain string (wrapped into a UIMessage) or a full AI
418
+ * SDK `UIMessage`.
419
+ */
420
+ async streamMessage(agentId, taskId, message, options = {}) {
421
+ const uiMessage = typeof message === "string" ? {
422
+ id: generateMessageId(),
423
+ role: "user",
424
+ parts: [{ type: "text", text: message }]
425
+ } : message;
426
+ const response = await this.http.request(
427
+ "POST",
428
+ `/api/agents/${agentId}/tasks/${taskId}/stream-message`,
429
+ {
430
+ raw: true,
431
+ signal: options.signal,
432
+ headers: { Accept: "text/event-stream" },
433
+ body: {
434
+ messages: [uiMessage],
435
+ ...options.chatMode ? { chatMode: options.chatMode } : {}
436
+ }
437
+ }
438
+ );
439
+ return new AgentChatStream(response, { signal: options.signal });
440
+ }
441
+ /**
442
+ * Reattaches to an in-flight agent turn after a disconnect (the resumable
443
+ * stream replays from the beginning of the turn). Returns `null` when
444
+ * there is no active stream to resume (server responds 204).
445
+ */
446
+ async resumeStream(agentId, taskId, options = {}) {
447
+ const response = await this.http.request(
448
+ "GET",
449
+ `/api/agents/${agentId}/tasks/${taskId}/stream-message`,
450
+ {
451
+ raw: true,
452
+ signal: options.signal,
453
+ headers: { Accept: "text/event-stream" }
454
+ }
455
+ );
456
+ if (response.status === 204) {
457
+ return null;
458
+ }
459
+ return new AgentChatStream(response, { signal: options.signal });
460
+ }
461
+ /**
462
+ * Requests cancellation of the active agent turn for a task. Idempotent:
463
+ * succeeds even when no stream is active. Optionally persists the partial
464
+ * assistant message received so far.
465
+ */
466
+ async stop(agentId, taskId, options = {}) {
467
+ return this.http.post(
468
+ `/api/agents/${agentId}/tasks/${taskId}/stream-message/stop`,
469
+ {
470
+ body: options.assistantMessage ? { assistantMessage: options.assistantMessage } : {}
471
+ }
472
+ );
473
+ }
474
+ };
475
+ function generateMessageId() {
476
+ const cryptoObj = globalThis.crypto;
477
+ if (cryptoObj?.randomUUID) {
478
+ return cryptoObj.randomUUID();
479
+ }
480
+ return `msg_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
481
+ }
482
+
483
+ // src/resources/actions.ts
484
+ var ActionsResource = class {
485
+ constructor(http, executions) {
486
+ this.http = http;
487
+ this.executions = executions;
488
+ }
489
+ http;
490
+ executions;
491
+ /**
492
+ * Run a single catalogue action as a one-step ad-hoc Execution (`source: SDK`).
493
+ *
494
+ * The platform runs the action synchronously and returns the execution id
495
+ * plus status/output/executionPath. You can also poll
496
+ * {@link ExecutionsResource.get} / {@link ExecutionsResource.waitUntilFinished}
497
+ * with the returned id.
498
+ *
499
+ * Discover `appId` / `actionId` / `inputConfig` via `workflowApps.list()`.
500
+ */
501
+ run(options) {
502
+ return this.http.post("/api/actions/run", {
503
+ body: options
504
+ });
505
+ }
506
+ /**
507
+ * Convenience: run the action and wait until the Execution reaches a
508
+ * terminal status (or pauses in `NEEDS_INPUT`). Prefer {@link run} when the
509
+ * synchronous response is enough.
510
+ */
511
+ async runAndWait(options, waitOptions) {
512
+ const { id } = await this.run(options);
513
+ return this.executions.waitUntilFinished(id, {
514
+ ...waitOptions,
515
+ expansion: [
516
+ "executionPath",
517
+ "source",
518
+ ...waitOptions?.expansion ?? []
519
+ ]
520
+ });
521
+ }
522
+ /**
523
+ * Thin discovery helper: find one action definition from `workflowApps.list()`.
524
+ * Prefer calling `workflowApps.list()` once and caching when looking up many
525
+ * actions.
526
+ */
527
+ async get(appId, actionId) {
528
+ const apps = await this.http.get("/api/workflow-apps");
529
+ const app = apps.find((a) => a.id === appId);
530
+ return app?.actions?.find((action) => action.id === actionId);
531
+ }
532
+ };
533
+
534
+ // src/define/index.ts
535
+ var DEFINE_KINDS = {
536
+ connection: "connection",
537
+ aiConnection: "aiConnection",
538
+ action: "action",
539
+ workflow: "workflow",
540
+ agent: "agent"
541
+ };
542
+ function resolveActionAppId(action) {
543
+ if (action.appId?.trim()) {
544
+ return action.appId.trim();
545
+ }
546
+ const id = action.id.trim();
547
+ const actionMarker = "_action_";
548
+ const markerIndex = id.indexOf(actionMarker);
549
+ if (markerIndex > 0) {
550
+ return id.slice(0, markerIndex);
551
+ }
552
+ const dot = id.indexOf(".");
553
+ if (dot > 0) {
554
+ return id.slice(0, dot);
555
+ }
556
+ throw new Error(
557
+ `Cannot infer appId for action "${id}". Pass appId explicitly.`
558
+ );
559
+ }
560
+ function resolveActionId(action) {
561
+ const id = action.id.trim();
562
+ const appId = action.appId?.trim();
563
+ if (appId && id.startsWith(`${appId}.`)) {
564
+ return id.slice(appId.length + 1);
565
+ }
566
+ const marker = "_action_";
567
+ if (id.includes(marker)) {
568
+ return id;
569
+ }
570
+ const dot = id.indexOf(".");
571
+ if (dot > 0) {
572
+ return id.slice(dot + 1);
573
+ }
574
+ return id;
575
+ }
576
+
577
+ // src/resources/common.ts
578
+ function serializeListQuery(options = {}) {
579
+ return {
580
+ expansion: serializeCsv(options.expansion),
581
+ filterBy: serializeFilterBy(options.filterBy),
582
+ includeType: serializeCsv(options.includeType)
583
+ };
584
+ }
585
+ function serializeCsv(value) {
586
+ if (value === void 0) {
587
+ return void 0;
588
+ }
589
+ return Array.isArray(value) ? value.join(",") : value;
590
+ }
591
+ function serializeFilterBy(value) {
592
+ if (value === void 0) {
593
+ return void 0;
594
+ }
595
+ if (typeof value === "string") {
596
+ return value;
597
+ }
598
+ return Object.entries(value).map(([key, filterValue]) => `${key}:${filterValue}`).join(",");
599
+ }
600
+
601
+ // src/resources/agents.ts
602
+ function unwrapTool(tool) {
603
+ if ("use" in tool && tool.use) {
604
+ return tool.use;
605
+ }
606
+ return tool;
607
+ }
608
+ function agentDefinitionToUpsert(def, options) {
609
+ const tools = [];
610
+ const workflowIds = [...options?.workflowIds ?? []];
611
+ for (const tool of def.tools ?? []) {
612
+ const unwrapped = unwrapTool(tool);
613
+ if (unwrapped.kind === DEFINE_KINDS.workflow) {
614
+ continue;
615
+ }
616
+ if (unwrapped.kind === DEFINE_KINDS.action) {
617
+ tools.push({
618
+ appId: resolveActionAppId(unwrapped),
619
+ actionId: resolveActionId(unwrapped),
620
+ connectionId: unwrapped.connection?.id,
621
+ name: unwrapped.name,
622
+ description: unwrapped.description,
623
+ input: unwrapped.input
624
+ });
625
+ }
626
+ }
627
+ return {
628
+ slug: def.slug,
629
+ name: def.name,
630
+ description: def.description,
631
+ instructions: def.instructions,
632
+ model: def.model,
633
+ aiConnectionId: def.aiConnection?.id,
634
+ connectionIds: def.connections?.map((c) => c.id).filter((id) => Boolean(id)),
635
+ workflowIds: workflowIds.length ? workflowIds : void 0,
636
+ tools,
637
+ managedByCode: true
638
+ };
639
+ }
640
+ var AgentsResource = class {
641
+ constructor(http) {
642
+ this.http = http;
643
+ }
644
+ http;
645
+ /** Lists agents visible to the caller in the current workspace. */
646
+ list(options) {
647
+ return this.http.get("/api/agents", {
648
+ query: serializeListQuery(options)
649
+ });
650
+ }
651
+ /** Returns a single agent. */
652
+ get(agentId, options) {
653
+ return this.http.get(`/api/agents/${agentId}`, {
654
+ query: serializeListQuery(options)
655
+ });
656
+ }
657
+ /**
658
+ * Upsert an agent by project-scoped slug.
659
+ * Prefer the CLI (`npx @lunnoa/client agents deploy`) for happy-path deploy.
660
+ */
661
+ upsertBySlug(projectId, body, options) {
662
+ const payload = "kind" in body && body.kind === "agent" ? agentDefinitionToUpsert(body, options) : body;
663
+ return this.http.post(`/api/projects/${projectId}/agents/upsert`, {
664
+ body: payload
665
+ });
666
+ }
667
+ };
668
+
669
+ // src/auth/types.ts
670
+ function isLoginRequires2FA(result) {
671
+ return "requires2FA" in result && result.requires2FA === true;
672
+ }
673
+ function mapWireTokens(wire) {
674
+ return {
675
+ accessToken: wire.access_token,
676
+ ...wire.refresh_token !== void 0 ? { refreshToken: wire.refresh_token } : {}
677
+ };
678
+ }
679
+ function mapLoginWire(wire) {
680
+ if ("requires2FA" in wire && wire.requires2FA) {
681
+ return {
682
+ requires2FA: true,
683
+ sessionToken: wire.sessionToken
684
+ };
685
+ }
686
+ const tokens = wire;
687
+ return {
688
+ accessToken: tokens.access_token,
689
+ refreshToken: tokens.refresh_token
690
+ };
691
+ }
692
+
693
+ // src/resources/auth.ts
694
+ var AuthResource = class {
695
+ constructor(http, tokenStore) {
696
+ this.http = http;
697
+ this.tokenStore = tokenStore;
698
+ this.sso = new SsoAuthResource(http);
699
+ }
700
+ http;
701
+ tokenStore;
702
+ sso;
703
+ /**
704
+ * `POST /api/auth/login`
705
+ *
706
+ * On success without 2FA, tokens are written to the token store (when set).
707
+ * When 2FA is required, returns `{ requires2FA, sessionToken }` and does not
708
+ * store tokens until {@link verify2faLogin} succeeds.
709
+ */
710
+ async login(input) {
711
+ const wire = await this.http.post("/api/auth/login", {
712
+ body: { email: input.email, password: input.password },
713
+ skipAuth: true
714
+ });
715
+ const result = mapLoginWire(wire);
716
+ if (!isLoginRequires2FA(result)) {
717
+ await this.persistTokens(result);
718
+ }
719
+ return result;
720
+ }
721
+ /**
722
+ * `POST /api/auth/2fa/verify-login`
723
+ *
724
+ * Completes password login after TOTP or backup-code verification.
725
+ */
726
+ async verify2faLogin(input) {
727
+ const wire = await this.http.post(
728
+ "/api/auth/2fa/verify-login",
729
+ {
730
+ body: {
731
+ sessionToken: input.sessionToken,
732
+ token: input.token
733
+ },
734
+ skipAuth: true
735
+ }
736
+ );
737
+ const result = mapLoginWire(wire);
738
+ if (isLoginRequires2FA(result)) {
739
+ throw new Error(
740
+ "Unexpected 2FA challenge after verify2faLogin; expected access and refresh tokens."
741
+ );
742
+ }
743
+ await this.persistTokens(result);
744
+ return result;
745
+ }
746
+ /**
747
+ * `POST /api/auth/login-with-token`
748
+ *
749
+ * Exchanges a short-lived hidden JWT (SSO callback or email verification)
750
+ * for access and refresh tokens.
751
+ */
752
+ async loginWithToken(input) {
753
+ const wire = await this.http.post("/api/auth/login-with-token", {
754
+ body: { token: input.token },
755
+ skipAuth: true
756
+ });
757
+ const tokens = {
758
+ accessToken: wire.access_token,
759
+ refreshToken: wire.refresh_token
760
+ };
761
+ await this.persistTokens(tokens);
762
+ return tokens;
763
+ }
764
+ /**
765
+ * `POST /api/auth/refresh-token`
766
+ *
767
+ * Returns a new access token. The refresh token itself is not rotated.
768
+ * When a token store is configured and `refreshToken` is omitted, the
769
+ * stored refresh token is used.
770
+ */
771
+ async refresh(input) {
772
+ const refreshToken = input?.refreshToken ?? await this.tokenStore?.getRefreshToken() ?? null;
773
+ if (!refreshToken) {
774
+ throw new Error(
775
+ "auth.refresh requires a refreshToken argument or a token store that holds one."
776
+ );
777
+ }
778
+ const wire = await this.http.post(
779
+ "/api/auth/refresh-token",
780
+ {
781
+ body: { refreshToken },
782
+ skipAuth: true
783
+ }
784
+ );
785
+ const mapped = mapWireTokens(wire);
786
+ if (this.tokenStore) {
787
+ await this.tokenStore.setTokens({
788
+ accessToken: mapped.accessToken,
789
+ // Keep existing refresh token; refresh responses omit it.
790
+ refreshToken: mapped.refreshToken
791
+ });
792
+ }
793
+ return { accessToken: mapped.accessToken };
794
+ }
795
+ /**
796
+ * `GET /api/users/me` — current user profile for the active JWT.
797
+ */
798
+ me() {
799
+ return this.http.get("/api/users/me");
800
+ }
801
+ /**
802
+ * Clears tokens from the token store.
803
+ * There is no server-side logout endpoint; access tokens remain valid until expiry
804
+ * (or until the user's `tokensValidFrom` watermark invalidates them).
805
+ */
806
+ async logout() {
807
+ await this.tokenStore?.clear();
808
+ }
809
+ /** True when the token store has an access token (sync stores only wait on Promise). */
810
+ async hasSession() {
811
+ const token = await this.tokenStore?.getAccessToken();
812
+ return Boolean(token);
813
+ }
814
+ async persistTokens(tokens) {
815
+ if (!this.tokenStore) {
816
+ return;
817
+ }
818
+ await this.tokenStore.setTokens({
819
+ accessToken: tokens.accessToken,
820
+ refreshToken: tokens.refreshToken
821
+ });
822
+ }
823
+ };
824
+ var SsoAuthResource = class {
825
+ constructor(http) {
826
+ this.http = http;
827
+ }
828
+ http;
829
+ /** `GET /api/auth/sso/providers` (public). */
830
+ listProviders() {
831
+ return this.http.get("/api/auth/sso/providers", {
832
+ skipAuth: true
833
+ });
834
+ }
835
+ /**
836
+ * Absolute URL for `GET /api/auth/sso/:providerId/login`.
837
+ * Navigate the browser here to start the OIDC redirect.
838
+ */
839
+ getLoginUrl(providerId) {
840
+ if (!providerId) {
841
+ throw new Error("sso.getLoginUrl requires a providerId.");
842
+ }
843
+ return this.http.buildUrl(
844
+ `/api/auth/sso/${encodeURIComponent(providerId)}/login`
845
+ );
846
+ }
847
+ };
848
+
849
+ // src/resources/connections.ts
850
+ var ConnectionsResource = class {
851
+ constructor(http) {
852
+ this.http = http;
853
+ }
854
+ http;
855
+ /** Lists connections in the workspace. */
856
+ list(options) {
857
+ return this.http.get("/api/connections", {
858
+ query: serializeListQuery(options)
859
+ });
860
+ }
861
+ /** Returns a single connection (secrets are never included). */
862
+ get(connectionId, options) {
863
+ return this.http.get(`/api/connections/${connectionId}`, {
864
+ query: serializeListQuery(options)
865
+ });
866
+ }
867
+ };
868
+
869
+ // src/resources/discovery.ts
870
+ var DiscoveryResource = class {
871
+ constructor(http) {
872
+ this.http = http;
873
+ }
874
+ http;
875
+ /**
876
+ * Returns the deployment's feature flags (storage, vector database, email,
877
+ * SSO, edition gating), letting a custom UI adapt to what is enabled.
878
+ */
879
+ enabledFeatures() {
880
+ return this.http.get("/api/discovery/enabled-features");
881
+ }
882
+ };
883
+
884
+ // src/core/pagination.ts
885
+ async function* iteratePages(fetchPage) {
886
+ let page = 1;
887
+ while (true) {
888
+ const { data, pagination } = await fetchPage(page);
889
+ for (const item of data) {
890
+ yield item;
891
+ }
892
+ if (page >= pagination.totalPages || data.length === 0) {
893
+ return;
894
+ }
895
+ page += 1;
896
+ }
897
+ }
898
+ async function* iterateOffset(fetchBatch) {
899
+ let offset = 0;
900
+ while (true) {
901
+ const { items, pagination } = await fetchBatch(offset);
902
+ for (const item of items) {
903
+ yield item;
904
+ }
905
+ offset += items.length;
906
+ const exhausted = items.length === 0 || (pagination.hasMore !== void 0 ? !pagination.hasMore : offset >= pagination.total);
907
+ if (exhausted) {
908
+ return;
909
+ }
910
+ }
911
+ }
912
+
913
+ // src/resources/entities.ts
914
+ var EntitiesResource = class {
915
+ constructor(http) {
916
+ this.http = http;
917
+ }
918
+ http;
919
+ /** Returns one page of entities matching the filter. */
920
+ list(options = {}) {
921
+ return this.http.get("/api/objects", {
922
+ query: this.serializeListOptions(options)
923
+ });
924
+ }
925
+ /**
926
+ * Async-iterates every entity matching the filter, fetching pages lazily.
927
+ *
928
+ * ```ts
929
+ * for await (const invoice of client.entities.iterate({ objectTypeSlug: 'invoice' })) { ... }
930
+ * ```
931
+ */
932
+ iterate(options = {}) {
933
+ return iteratePages(async (page) => {
934
+ const result = await this.list({ ...options, page });
935
+ return { data: result.data ?? [], pagination: result.pagination };
936
+ });
937
+ }
938
+ /** Returns a single entity. Request `expansion: ['attributes']` for field values. */
939
+ get(entityId, options = {}) {
940
+ return this.http.get(`/api/objects/${entityId}`, {
941
+ query: { expansion: serializeCsv(options.expansion) }
942
+ });
943
+ }
944
+ /** Creates an entity. `attributes` are keyed by field ID. */
945
+ create(data, options = {}) {
946
+ return this.http.post("/api/objects", {
947
+ body: data,
948
+ query: { expansion: serializeCsv(options.expansion) }
949
+ });
950
+ }
951
+ /** Partially updates an entity; supplied attributes are merged. */
952
+ update(entityId, data, options = {}) {
953
+ return this.http.patch(`/api/objects/${entityId}`, {
954
+ body: data,
955
+ query: { expansion: serializeCsv(options.expansion) }
956
+ });
957
+ }
958
+ /** Soft-deletes an entity. */
959
+ delete(entityId) {
960
+ return this.http.delete(`/api/objects/${entityId}`);
961
+ }
962
+ /** Moves an entity to a new state (validated against the type's state machine). */
963
+ changeState(entityId, data) {
964
+ return this.http.post(
965
+ `/api/objects/${entityId}/state`,
966
+ { body: data }
967
+ );
968
+ }
969
+ /** Returns the entity's state changes, newest first. */
970
+ getStateHistory(entityId, options = {}) {
971
+ return this.http.get(
972
+ `/api/objects/${entityId}/state/history`,
973
+ { query: { limit: options.limit } }
974
+ );
975
+ }
976
+ /** Returns the current state and the transitions allowed from it. */
977
+ getStateTransitions(entityId) {
978
+ return this.http.get(
979
+ `/api/objects/${entityId}/state/transitions`
980
+ );
981
+ }
982
+ /** Imports pre-parsed rows into an entity type (SuperAdmin only). */
983
+ import(data) {
984
+ return this.http.post("/api/objects/import", {
985
+ body: data
986
+ });
987
+ }
988
+ /** Exports entities of a type as flat JSON rows. */
989
+ export(data) {
990
+ return this.http.post(
991
+ "/api/objects/export",
992
+ { body: data }
993
+ );
994
+ }
995
+ /** Returns the column headers and a sample row for importing (SuperAdmin only). */
996
+ getImportTemplate(entityTypeId) {
997
+ return this.http.get(
998
+ `/api/objects/import/template/${entityTypeId}`
999
+ );
1000
+ }
1001
+ serializeListOptions(options) {
1002
+ return {
1003
+ objectTypeId: options.objectTypeId,
1004
+ objectTypeSlug: options.objectTypeSlug,
1005
+ state: options.state,
1006
+ states: options.states?.join(","),
1007
+ ownerId: options.ownerId,
1008
+ search: options.search,
1009
+ sortBy: options.sortBy,
1010
+ sortOrder: options.sortOrder,
1011
+ page: options.page,
1012
+ pageSize: options.pageSize,
1013
+ expansion: serializeCsv(options.expansion)
1014
+ };
1015
+ }
1016
+ };
1017
+
1018
+ // src/resources/entity-types.ts
1019
+ var EntityTypesResource = class {
1020
+ constructor(http) {
1021
+ this.http = http;
1022
+ }
1023
+ http;
1024
+ /**
1025
+ * Lists entity types in the workspace. Request
1026
+ * `expansion: ['attributeSchema', 'stateSchema']` for the field and state
1027
+ * machine definitions the codegen CLI builds types from.
1028
+ */
1029
+ list(options = {}) {
1030
+ return this.http.get("/api/object-types", {
1031
+ query: {
1032
+ includeDisabled: options.includeDisabled,
1033
+ expansion: serializeCsv(options.expansion)
1034
+ }
1035
+ });
1036
+ }
1037
+ /** Returns a single entity type by ID. */
1038
+ get(id, options = {}) {
1039
+ return this.http.get(`/api/object-types/${id}`, {
1040
+ query: { expansion: serializeCsv(options.expansion) }
1041
+ });
1042
+ }
1043
+ /** Returns a single entity type by its URL-safe slug. */
1044
+ getBySlug(slug, options = {}) {
1045
+ return this.http.get(`/api/object-types/slug/${slug}`, {
1046
+ query: { expansion: serializeCsv(options.expansion) }
1047
+ });
1048
+ }
1049
+ };
1050
+
1051
+ // src/resources/approvals.ts
1052
+ var ApprovalsResource = class {
1053
+ constructor(http) {
1054
+ this.http = http;
1055
+ }
1056
+ http;
1057
+ /**
1058
+ * @param options.status `pending` (default) for actionable items, or `done` for history.
1059
+ */
1060
+ async inbox(options) {
1061
+ return this.http.get("/api/approvals/inbox", {
1062
+ query: { status: options?.status ?? "pending" }
1063
+ });
1064
+ }
1065
+ async decide(approvalId, body) {
1066
+ return this.http.post(`/api/approvals/${approvalId}/decide`, { body });
1067
+ }
1068
+ };
1069
+
1070
+ // src/streaming/execution-progress.ts
1071
+ var ExecutionProgressStream = class {
1072
+ constructor(response, options) {
1073
+ this.response = response;
1074
+ this.options = options;
1075
+ }
1076
+ response;
1077
+ options;
1078
+ consumed = false;
1079
+ [Symbol.asyncIterator]() {
1080
+ return this.events();
1081
+ }
1082
+ async *events() {
1083
+ if (this.consumed) {
1084
+ throw new Error("This ExecutionProgressStream has already been consumed.");
1085
+ }
1086
+ this.consumed = true;
1087
+ const body = this.response.body;
1088
+ if (!body) {
1089
+ return;
1090
+ }
1091
+ for await (const frame of parseSseEvents(body)) {
1092
+ if (this.options?.signal?.aborted) {
1093
+ return;
1094
+ }
1095
+ if (frame.data === null) {
1096
+ continue;
1097
+ }
1098
+ let parsed;
1099
+ try {
1100
+ parsed = JSON.parse(frame.data);
1101
+ } catch {
1102
+ continue;
1103
+ }
1104
+ if (!parsed || typeof parsed !== "object") {
1105
+ continue;
1106
+ }
1107
+ yield {
1108
+ event: frame.event,
1109
+ data: parsed
1110
+ };
1111
+ if (frame.event === "execution.finished" || frame.event === "execution.error") {
1112
+ return;
1113
+ }
1114
+ }
1115
+ }
1116
+ };
1117
+ async function openExecutionStream(http, executionId, options) {
1118
+ const response = await http.request("GET", `/api/executions/${executionId}/stream`, {
1119
+ raw: true,
1120
+ signal: options?.signal
1121
+ });
1122
+ if (!response.ok) {
1123
+ const body = await response.text().catch(() => void 0);
1124
+ let parsed = body;
1125
+ try {
1126
+ parsed = body ? JSON.parse(body) : void 0;
1127
+ } catch {
1128
+ }
1129
+ throw new LunnoaApiError({
1130
+ status: response.status,
1131
+ body: parsed,
1132
+ method: "GET",
1133
+ path: `/api/executions/${executionId}/stream`
1134
+ });
1135
+ }
1136
+ return new ExecutionProgressStream(response, options);
1137
+ }
1138
+
1139
+ // src/resources/executions.ts
1140
+ var TERMINAL_STATUSES = /* @__PURE__ */ new Set([
1141
+ "SUCCESS",
1142
+ "FAILED",
1143
+ "CANCELLED"
1144
+ ]);
1145
+ var ExecutionTimeoutError = class extends Error {
1146
+ constructor(executionId, timeoutMs) {
1147
+ super(
1148
+ `Execution ${executionId} did not finish within ${timeoutMs}ms. It is still running server-side; keep polling executions.get() or increase timeoutMs.`
1149
+ );
1150
+ this.executionId = executionId;
1151
+ this.timeoutMs = timeoutMs;
1152
+ this.name = "ExecutionTimeoutError";
1153
+ }
1154
+ executionId;
1155
+ timeoutMs;
1156
+ };
1157
+ var ExecutionsResource = class {
1158
+ constructor(http, workflowApps) {
1159
+ this.http = http;
1160
+ this.workflowApps = workflowApps;
1161
+ }
1162
+ http;
1163
+ workflowApps;
1164
+ catalogCache = null;
1165
+ /**
1166
+ * Starts a new execution of a workflow. Only the execution ID is returned;
1167
+ * use {@link waitUntilFinished} or `get` to track progress.
1168
+ */
1169
+ execute(workflowId, inputData) {
1170
+ return this.http.post(
1171
+ `/api/executions/workflows/${workflowId}/execute`,
1172
+ { body: { inputData: inputData ?? {} } }
1173
+ );
1174
+ }
1175
+ /** Lists executions in the current workspace, most recently started first. */
1176
+ list(options) {
1177
+ return this.http.get("/api/executions", {
1178
+ query: serializeListQuery(options)
1179
+ });
1180
+ }
1181
+ /**
1182
+ * Returns a single execution. Request `expansion: ['status', 'executionPath']`
1183
+ * for the business-readable step list custom UIs render progress from.
1184
+ */
1185
+ get(executionId, options) {
1186
+ return this.http.get(`/api/executions/${executionId}`, {
1187
+ query: serializeListQuery(options)
1188
+ });
1189
+ }
1190
+ /**
1191
+ * Timeline-ready progress: `executionPath` plus catalogue icons/logos.
1192
+ * Use this for a custom status bar / step list (not raw nodes/edges).
1193
+ */
1194
+ async getProgress(executionId) {
1195
+ const execution = await this.get(executionId, {
1196
+ expansion: [
1197
+ "status",
1198
+ "statusMessage",
1199
+ "executionPath",
1200
+ "pendingInput",
1201
+ "source",
1202
+ "name",
1203
+ "startedAt",
1204
+ "stoppedAt",
1205
+ "output"
1206
+ ]
1207
+ });
1208
+ return this.#toProgress(execution);
1209
+ }
1210
+ /**
1211
+ * Opens the Public API SSE stream (`GET /executions/:id/stream`).
1212
+ * Yields `execution.progress`, `loop.progress`, `execution.finished`, etc.
1213
+ * For a simple timeline, prefer {@link watchProgress}.
1214
+ */
1215
+ stream(executionId, options) {
1216
+ return openExecutionStream(this.http, executionId, options);
1217
+ }
1218
+ /**
1219
+ * Live progress snapshots for a custom timeline UI.
1220
+ * Subscribes to SSE `execution.progress` events (and enriches steps with icons).
1221
+ * Completes when the execution reaches a terminal status or the signal aborts.
1222
+ */
1223
+ async *watchProgress(executionId, options) {
1224
+ const stream = await this.stream(executionId, options);
1225
+ for await (const frame of stream) {
1226
+ if (frame.event === "execution.progress") {
1227
+ const payload = frame.data;
1228
+ yield await this.#progressFromPayload(payload);
1229
+ } else if (frame.event === "execution.finished") {
1230
+ yield await this.getProgress(executionId);
1231
+ return;
1232
+ } else if (frame.event === "execution.error") {
1233
+ throw new Error(
1234
+ typeof frame.data.message === "string" ? frame.data.message : `Execution stream error for ${executionId}`
1235
+ );
1236
+ }
1237
+ }
1238
+ }
1239
+ /**
1240
+ * Polls an execution with exponential backoff until it reaches a terminal
1241
+ * status (`SUCCESS`, `FAILED`, `CANCELLED`) — or pauses in `NEEDS_INPUT`
1242
+ * (unless `stopOnNeedsInput: false`). Resolves with the final execution
1243
+ * including `status`, `statusMessage`, and `output`.
1244
+ */
1245
+ async waitUntilFinished(executionId, options = {}) {
1246
+ const {
1247
+ timeoutMs = 10 * 60 * 1e3,
1248
+ initialDelayMs = 500,
1249
+ maxDelayMs = 5e3,
1250
+ backoffFactor = 1.5,
1251
+ stopOnNeedsInput = true,
1252
+ expansion = [],
1253
+ signal,
1254
+ sleep = defaultSleep
1255
+ } = options;
1256
+ const pollExpansion = Array.from(
1257
+ /* @__PURE__ */ new Set(["status", "statusMessage", "output", ...expansion])
1258
+ );
1259
+ const startedAt = Date.now();
1260
+ let waitedMs = 0;
1261
+ let delay = initialDelayMs;
1262
+ while (true) {
1263
+ signal?.throwIfAborted();
1264
+ const execution = await this.get(executionId, {
1265
+ expansion: pollExpansion
1266
+ });
1267
+ const status = execution.status;
1268
+ if (status && TERMINAL_STATUSES.has(status)) {
1269
+ return execution;
1270
+ }
1271
+ if (stopOnNeedsInput && status === "NEEDS_INPUT") {
1272
+ return execution;
1273
+ }
1274
+ const elapsed = Math.max(Date.now() - startedAt, waitedMs);
1275
+ if (elapsed + delay > timeoutMs) {
1276
+ throw new ExecutionTimeoutError(executionId, timeoutMs);
1277
+ }
1278
+ await sleep(delay);
1279
+ waitedMs += delay;
1280
+ delay = Math.min(delay * backoffFactor, maxDelayMs);
1281
+ }
1282
+ }
1283
+ /**
1284
+ * Resumes an execution paused in `NEEDS_INPUT` by submitting values for the
1285
+ * waiting node. Identify the waiting node and its field schema via
1286
+ * `expansion: ['pendingInput', 'executionPath']` (prefer `pendingInput` for
1287
+ * the form contract; `executionPath` steps with status `NEEDS_INPUT` also
1288
+ * carry the `nodeId`). Required fields from the schema must be present.
1289
+ * The server acknowledges immediately and resumes asynchronously — follow
1290
+ * up with {@link waitUntilFinished}.
1291
+ */
1292
+ async submitInput(executionId, nodeId, values) {
1293
+ await this.http.post(
1294
+ `/api/webhooks/executions/${executionId}/nodes/${nodeId}/input`,
1295
+ { body: values }
1296
+ );
1297
+ }
1298
+ /**
1299
+ * Submit an approve/reject decision for a Request Approval node.
1300
+ * Uses the authenticated Approvals API (not the public input webhook).
1301
+ * Partial approvals keep the execution paused; terminal decisions resume it.
1302
+ */
1303
+ async submitApproval(executionId, nodeId, body) {
1304
+ return this.http.post(
1305
+ `/api/executions/${executionId}/nodes/${nodeId}/approvals`,
1306
+ { body }
1307
+ );
1308
+ }
1309
+ async #toProgress(execution) {
1310
+ const path = execution.executionPath ?? [];
1311
+ const steps = await this.#enrichSteps(path);
1312
+ return {
1313
+ executionId: execution.id,
1314
+ status: execution.status,
1315
+ statusMessage: execution.statusMessage ?? null,
1316
+ name: execution.name ?? null,
1317
+ source: execution.source ?? null,
1318
+ startedAt: execution.startedAt,
1319
+ stoppedAt: execution.stoppedAt ?? null,
1320
+ steps,
1321
+ pendingInput: execution.pendingInput,
1322
+ output: execution.output,
1323
+ activeStepIndex: findActiveStepIndex(steps)
1324
+ };
1325
+ }
1326
+ async #progressFromPayload(payload) {
1327
+ const steps = await this.#enrichSteps(payload.executionPath ?? []);
1328
+ return {
1329
+ executionId: payload.executionId,
1330
+ status: payload.status ?? void 0,
1331
+ statusMessage: payload.statusMessage,
1332
+ name: payload.name,
1333
+ source: payload.source,
1334
+ startedAt: payload.startedAt,
1335
+ stoppedAt: payload.stoppedAt,
1336
+ steps,
1337
+ pendingInput: payload.pendingInput,
1338
+ output: payload.output,
1339
+ activeStepIndex: findActiveStepIndex(steps)
1340
+ };
1341
+ }
1342
+ async #enrichSteps(path) {
1343
+ const catalog = await this.#loadCatalog();
1344
+ return path.map((step) => {
1345
+ const app = catalog?.find((a) => a.id === step.appId);
1346
+ const action = step.actionId && Array.isArray(app?.actions) ? app.actions.find(
1347
+ (a) => a && typeof a === "object" && a.id === step.actionId
1348
+ ) : void 0;
1349
+ const trigger = step.triggerId && Array.isArray(app?.triggers) ? app.triggers.find(
1350
+ (t) => t && typeof t === "object" && t.id === step.triggerId
1351
+ ) : void 0;
1352
+ const iconUrl = typeof action?.iconUrl === "string" && action.iconUrl || typeof trigger?.iconUrl === "string" && trigger.iconUrl || null;
1353
+ const durationMs = computeDurationMs(step.startTime, step.endTime);
1354
+ return {
1355
+ ...step,
1356
+ appLogoUrl: app?.logoUrl ?? null,
1357
+ iconUrl,
1358
+ durationMs
1359
+ };
1360
+ });
1361
+ }
1362
+ async #loadCatalog() {
1363
+ if (!this.workflowApps) {
1364
+ return null;
1365
+ }
1366
+ if (!this.catalogCache) {
1367
+ try {
1368
+ this.catalogCache = await this.workflowApps.list();
1369
+ } catch {
1370
+ return null;
1371
+ }
1372
+ }
1373
+ return this.catalogCache;
1374
+ }
1375
+ };
1376
+ function findActiveStepIndex(steps) {
1377
+ const idx = steps.findIndex(
1378
+ (s) => s.status === "RUNNING" || s.status === "NEEDS_INPUT" || s.status === "RETRYING" || s.status === "WAITING" || s.status === "SCHEDULED"
1379
+ );
1380
+ return idx;
1381
+ }
1382
+ function computeDurationMs(start, end) {
1383
+ if (!start || !end) return null;
1384
+ const a = Date.parse(start);
1385
+ const b = Date.parse(end);
1386
+ if (Number.isNaN(a) || Number.isNaN(b) || b < a) return null;
1387
+ return b - a;
1388
+ }
1389
+ function defaultSleep(ms) {
1390
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
1391
+ }
1392
+
1393
+ // src/resources/knowledge.ts
1394
+ var KnowledgeResource = class {
1395
+ constructor(http) {
1396
+ this.http = http;
1397
+ }
1398
+ http;
1399
+ /** Lists knowledge notebooks in the workspace. */
1400
+ list(options = {}) {
1401
+ return this.http.get("/api/knowledge", {
1402
+ query: { expansion: serializeCsv(options.expansion) }
1403
+ });
1404
+ }
1405
+ /** Returns a single knowledge notebook. */
1406
+ get(knowledgeId, options = {}) {
1407
+ return this.http.get(`/api/knowledge/${knowledgeId}`, {
1408
+ query: { expansion: serializeCsv(options.expansion) }
1409
+ });
1410
+ }
1411
+ /** Creates a knowledge notebook. Subject to the deployment's storage/vector infrastructure. */
1412
+ create(data) {
1413
+ return this.http.post("/api/knowledge", { body: data });
1414
+ }
1415
+ /** Updates a knowledge notebook. */
1416
+ update(knowledgeId, data) {
1417
+ return this.http.patch(`/api/knowledge/${knowledgeId}`, {
1418
+ body: data
1419
+ });
1420
+ }
1421
+ /** Deletes a knowledge notebook. */
1422
+ delete(knowledgeId) {
1423
+ return this.http.delete(`/api/knowledge/${knowledgeId}`);
1424
+ }
1425
+ /** Saves raw text into the notebook (chunked, embedded, and indexed server-side). */
1426
+ saveUploadedText(knowledgeId, data) {
1427
+ return this.http.post(
1428
+ `/api/knowledge/${knowledgeId}/saveUploadedText`,
1429
+ { body: data }
1430
+ );
1431
+ }
1432
+ /** Lists the documents stored in a notebook. */
1433
+ listDocuments(knowledgeId) {
1434
+ return this.http.get(
1435
+ `/api/knowledge/${knowledgeId}/documents`
1436
+ );
1437
+ }
1438
+ /** Returns one document group (an uploaded document and its chunks). */
1439
+ getDocumentGroup(knowledgeId, groupId) {
1440
+ return this.http.get(
1441
+ `/api/knowledge/${knowledgeId}/documents/${groupId}`
1442
+ );
1443
+ }
1444
+ /** Deletes one document group from the notebook. */
1445
+ deleteDocumentGroup(knowledgeId, groupId) {
1446
+ return this.http.delete(
1447
+ `/api/knowledge/${knowledgeId}/documents/${groupId}`
1448
+ );
1449
+ }
1450
+ };
1451
+
1452
+ // src/resources/projects.ts
1453
+ var ProjectsResource = class {
1454
+ constructor(http) {
1455
+ this.http = http;
1456
+ }
1457
+ http;
1458
+ /** Lists projects in the workspace, most recently updated first. */
1459
+ list(options) {
1460
+ return this.http.get("/api/projects", {
1461
+ query: serializeListQuery(options)
1462
+ });
1463
+ }
1464
+ /** Returns a single project. */
1465
+ get(projectId, options) {
1466
+ return this.http.get(`/api/projects/${projectId}`, {
1467
+ query: serializeListQuery(options)
1468
+ });
1469
+ }
1470
+ /** Creates a project. */
1471
+ create(data) {
1472
+ return this.http.post("/api/projects", { body: data });
1473
+ }
1474
+ };
1475
+
1476
+ // src/resources/queue-items.ts
1477
+ var QueueItemsResource = class {
1478
+ constructor(http) {
1479
+ this.http = http;
1480
+ }
1481
+ http;
1482
+ /** Returns one page of items in a queue (offset-based pagination). */
1483
+ list(queueId, options = {}) {
1484
+ return this.http.get(`/api/queues/${queueId}/items`, {
1485
+ query: {
1486
+ limit: options.limit,
1487
+ offset: options.offset,
1488
+ expansion: serializeCsv(options.expansion),
1489
+ filterBy: serializeFilterBy(options.filterBy)
1490
+ }
1491
+ });
1492
+ }
1493
+ /** Async-iterates every item in a queue, fetching batches lazily. */
1494
+ iterate(queueId, options = {}) {
1495
+ return iterateOffset(async (offset) => {
1496
+ const result = await this.list(queueId, { ...options, offset });
1497
+ return {
1498
+ items: result.items ?? [],
1499
+ pagination: result.pagination
1500
+ };
1501
+ });
1502
+ }
1503
+ /** Returns a single queue item. */
1504
+ get(queueId, itemId) {
1505
+ return this.http.get(
1506
+ `/api/queues/${queueId}/items/${itemId}`
1507
+ );
1508
+ }
1509
+ /** Adds an item to a queue. */
1510
+ create(queueId, data) {
1511
+ return this.http.post(`/api/queues/${queueId}/items`, {
1512
+ body: data
1513
+ });
1514
+ }
1515
+ /** Adds many items to a queue in one call. */
1516
+ bulkCreate(queueId, data) {
1517
+ return this.http.post(
1518
+ `/api/queues/${queueId}/items/bulk`,
1519
+ { body: data }
1520
+ );
1521
+ }
1522
+ /** Updates a queue item. */
1523
+ update(queueId, itemId, data) {
1524
+ return this.http.patch(
1525
+ `/api/queues/${queueId}/items/${itemId}`,
1526
+ { body: data }
1527
+ );
1528
+ }
1529
+ /** Deletes a queue item. */
1530
+ delete(queueId, itemId) {
1531
+ return this.http.delete(
1532
+ `/api/queues/${queueId}/items/${itemId}`
1533
+ );
1534
+ }
1535
+ /** Returns items that are ready to be processed. */
1536
+ listReady(queueId, options = {}) {
1537
+ return this.http.get(`/api/queues/${queueId}/items/ready`, {
1538
+ query: { limit: options.limit }
1539
+ });
1540
+ }
1541
+ /** Returns items eligible for retry. */
1542
+ listRetryable(queueId) {
1543
+ return this.http.get(`/api/queues/${queueId}/items/retry`);
1544
+ }
1545
+ /** Returns items with the given status. */
1546
+ listByStatus(queueId, status) {
1547
+ return this.http.get(
1548
+ `/api/queues/${queueId}/items/status/${status}`
1549
+ );
1550
+ }
1551
+ /** Looks an item up by the external ID your system supplied. */
1552
+ getByExternalId(queueId, externalId) {
1553
+ return this.http.get(
1554
+ `/api/queues/${queueId}/items/external/${externalId}`
1555
+ );
1556
+ }
1557
+ /** Searches items in the queue. */
1558
+ search(queueId, query) {
1559
+ return this.http.get(`/api/queues/${queueId}/items/search`, {
1560
+ query: { q: query }
1561
+ });
1562
+ }
1563
+ /** Returns per-status and per-priority counts for the queue. */
1564
+ stats(queueId) {
1565
+ return this.http.get(`/api/queues/${queueId}/items/stats`);
1566
+ }
1567
+ /** Returns the recorded processing errors of an item. */
1568
+ listErrors(queueId, itemId) {
1569
+ return this.http.get(
1570
+ `/api/queues/${queueId}/items/${itemId}/errors`
1571
+ );
1572
+ }
1573
+ /** Updates the processing status of an item. */
1574
+ updateStatus(queueId, itemId, status) {
1575
+ return this.http.patch(
1576
+ `/api/queues/${queueId}/items/${itemId}/status`,
1577
+ { body: { status } }
1578
+ );
1579
+ }
1580
+ /** Links an item to the workflow execution that processed it. */
1581
+ linkExecution(queueId, itemId, executionId) {
1582
+ return this.http.patch(
1583
+ `/api/queues/${queueId}/items/${itemId}/link-execution`,
1584
+ { body: { executionId } }
1585
+ );
1586
+ }
1587
+ /** Queues the item for another processing attempt. */
1588
+ retry(queueId, itemId) {
1589
+ return this.http.post(
1590
+ `/api/queues/${queueId}/items/${itemId}/retry`
1591
+ );
1592
+ }
1593
+ /** Records a processing error on the item. */
1594
+ addError(queueId, itemId, error) {
1595
+ return this.http.post(
1596
+ `/api/queues/${queueId}/items/${itemId}/error`,
1597
+ { body: error }
1598
+ );
1599
+ }
1600
+ };
1601
+
1602
+ // src/resources/queues.ts
1603
+ var QueuesResource = class {
1604
+ constructor(http) {
1605
+ this.http = http;
1606
+ }
1607
+ http;
1608
+ /** Lists queues in the workspace, ordered by priority then creation date. */
1609
+ list(options) {
1610
+ return this.http.get("/api/queues", {
1611
+ query: serializeListQuery(options)
1612
+ });
1613
+ }
1614
+ /** Returns a single queue. */
1615
+ get(queueId, options) {
1616
+ return this.http.get(`/api/queues/${queueId}`, {
1617
+ query: serializeListQuery(options)
1618
+ });
1619
+ }
1620
+ /** Creates a queue. */
1621
+ create(data) {
1622
+ return this.http.post("/api/queues", { body: data });
1623
+ }
1624
+ /** Updates a queue. */
1625
+ update(queueId, data) {
1626
+ return this.http.patch(`/api/queues/${queueId}`, { body: data });
1627
+ }
1628
+ /** Deletes a queue. */
1629
+ delete(queueId) {
1630
+ return this.http.delete(`/api/queues/${queueId}`);
1631
+ }
1632
+ /** Returns aggregate statistics for a queue. */
1633
+ stats(queueId) {
1634
+ return this.http.get(`/api/queues/${queueId}/stats`);
1635
+ }
1636
+ /** Activates a queue. */
1637
+ activate(queueId) {
1638
+ return this.http.patch(`/api/queues/${queueId}/activate`);
1639
+ }
1640
+ /** Deactivates a queue. */
1641
+ deactivate(queueId) {
1642
+ return this.http.patch(`/api/queues/${queueId}/deactivate`);
1643
+ }
1644
+ };
1645
+
1646
+ // src/resources/runs.ts
1647
+ function isWorkflowDefinition(value) {
1648
+ return typeof value === "object" && value !== null && value.kind === "workflow";
1649
+ }
1650
+ function isActionDefinition(value) {
1651
+ return typeof value === "object" && value !== null && value.kind === "action";
1652
+ }
1653
+ function stepFromDefinition(step) {
1654
+ if ("appId" in step && "actionId" in step && !("use" in step) && !("kind" in step)) {
1655
+ return step;
1656
+ }
1657
+ if (isActionDefinition(step)) {
1658
+ return {
1659
+ appId: resolveActionAppId(step),
1660
+ actionId: resolveActionId(step),
1661
+ ...step.connection?.id ? { connectionId: step.connection.id } : {},
1662
+ ...step.input ? { input: step.input } : {},
1663
+ ...step.name ? { name: step.name } : {}
1664
+ };
1665
+ }
1666
+ const use = step.use;
1667
+ const input = {
1668
+ ...use.input ?? {},
1669
+ ...step.input ?? {}
1670
+ };
1671
+ return {
1672
+ appId: resolveActionAppId(use),
1673
+ actionId: resolveActionId(use),
1674
+ ...use.connection?.id ? { connectionId: use.connection.id } : {},
1675
+ ...Object.keys(input).length ? { input } : {},
1676
+ ...step.name ?? use.name ? { name: step.name ?? use.name } : {}
1677
+ };
1678
+ }
1679
+ function normalizeStartRunInput(input) {
1680
+ if (isWorkflowDefinition(input)) {
1681
+ return {
1682
+ steps: input.steps.map(stepFromDefinition),
1683
+ name: input.name ?? input.slug
1684
+ };
1685
+ }
1686
+ if (Array.isArray(input)) {
1687
+ return { steps: input.map((s) => stepFromDefinition(s)) };
1688
+ }
1689
+ return input;
1690
+ }
1691
+ var RunsResource = class {
1692
+ constructor(http, executions) {
1693
+ this.http = http;
1694
+ this.executions = executions;
1695
+ }
1696
+ http;
1697
+ executions;
1698
+ /**
1699
+ * Start a linear multi-step ad-hoc Execution.
1700
+ *
1701
+ * Accepts a `defineWorkflow` result, a step array, or an explicit
1702
+ * `{ steps, projectId?, name? }` payload.
1703
+ */
1704
+ start(input) {
1705
+ const options = normalizeStartRunInput(input);
1706
+ if (!options.steps?.length) {
1707
+ throw new Error("runs.start requires at least one step");
1708
+ }
1709
+ return this.http.post("/api/runs", {
1710
+ body: options
1711
+ });
1712
+ }
1713
+ /** Convenience: start and wait until terminal / NEEDS_INPUT. */
1714
+ async startAndWait(input, waitOptions) {
1715
+ const { id } = await this.start(input);
1716
+ return this.executions.waitUntilFinished(id, {
1717
+ ...waitOptions,
1718
+ expansion: [
1719
+ "executionPath",
1720
+ "source",
1721
+ ...waitOptions?.expansion ?? []
1722
+ ]
1723
+ });
1724
+ }
1725
+ };
1726
+
1727
+ // src/resources/tasks.ts
1728
+ var TasksResource = class {
1729
+ constructor(http) {
1730
+ this.http = http;
1731
+ }
1732
+ http;
1733
+ /** Lists tasks (conversation threads) visible to the caller, most recently updated first. */
1734
+ list(options) {
1735
+ return this.http.get("/api/tasks", {
1736
+ query: serializeListQuery(options)
1737
+ });
1738
+ }
1739
+ /** Returns a single task. Request `expansion: ['messages']` for the conversation history. */
1740
+ get(taskId, options) {
1741
+ return this.http.get(`/api/tasks/${taskId}`, {
1742
+ query: serializeListQuery(options)
1743
+ });
1744
+ }
1745
+ /** Creates a task (conversation thread) for an agent. */
1746
+ create(agentId, data, options) {
1747
+ return this.http.post(`/api/agents/${agentId}/tasks`, {
1748
+ body: data,
1749
+ query: serializeListQuery(options)
1750
+ });
1751
+ }
1752
+ /** Updates the task name or description. */
1753
+ update(taskId, data, options) {
1754
+ return this.http.patch(`/api/tasks/${taskId}`, {
1755
+ body: data,
1756
+ query: serializeListQuery(options)
1757
+ });
1758
+ }
1759
+ /** Permanently deletes a task and its messages. */
1760
+ delete(taskId) {
1761
+ return this.http.delete(`/api/tasks/${taskId}`);
1762
+ }
1763
+ /**
1764
+ * Sends a message to an agent task and waits for the full agent turn to
1765
+ * finish (non-streaming). Returns either a plain string or the assistant
1766
+ * messages in AI SDK UIMessage format. For token-by-token output use
1767
+ * `client.agentChat.streamMessage` instead.
1768
+ */
1769
+ message(agentId, taskId, message, options = {}) {
1770
+ const uiMessage = typeof message === "string" ? {
1771
+ id: generateMessageId2(),
1772
+ role: "user",
1773
+ parts: [{ type: "text", text: message }]
1774
+ } : message;
1775
+ return this.http.post(
1776
+ `/api/agents/${agentId}/tasks/${taskId}/message`,
1777
+ {
1778
+ body: {
1779
+ messages: [uiMessage],
1780
+ ...options.chatMode ? { chatMode: options.chatMode } : {}
1781
+ }
1782
+ }
1783
+ );
1784
+ }
1785
+ };
1786
+ function generateMessageId2() {
1787
+ const cryptoObj = globalThis.crypto;
1788
+ if (cryptoObj?.randomUUID) {
1789
+ return cryptoObj.randomUUID();
1790
+ }
1791
+ return `msg_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
1792
+ }
1793
+
1794
+ // src/resources/variables.ts
1795
+ var VariablesResource = class {
1796
+ constructor(http) {
1797
+ this.http = http;
1798
+ }
1799
+ http;
1800
+ /** Lists variables in the workspace. */
1801
+ list(options) {
1802
+ return this.http.get("/api/variables", {
1803
+ query: serializeListQuery(options)
1804
+ });
1805
+ }
1806
+ /** Returns a single variable. */
1807
+ get(variableId, options) {
1808
+ return this.http.get(`/api/variables/${variableId}`, {
1809
+ query: serializeListQuery(options)
1810
+ });
1811
+ }
1812
+ /** Creates a variable. */
1813
+ create(data) {
1814
+ return this.http.post("/api/variables", { body: data });
1815
+ }
1816
+ /** Updates a variable. */
1817
+ update(variableId, data) {
1818
+ return this.http.patch(`/api/variables/${variableId}`, {
1819
+ body: data
1820
+ });
1821
+ }
1822
+ /** Deletes a variable. */
1823
+ delete(variableId) {
1824
+ return this.http.delete(`/api/variables/${variableId}`);
1825
+ }
1826
+ };
1827
+
1828
+ // src/resources/workflow-apps.ts
1829
+ var WorkflowAppsResource = class {
1830
+ constructor(http) {
1831
+ this.http = http;
1832
+ }
1833
+ http;
1834
+ /** Lists all workflow apps installed on the deployment. */
1835
+ list() {
1836
+ return this.http.get("/api/workflow-apps");
1837
+ }
1838
+ /**
1839
+ * Creates a connection for an app where the connection type is
1840
+ * non-interactive (API key, basic auth). OAuth connections require the
1841
+ * browser flow inside the bundled UI and cannot be created here.
1842
+ */
1843
+ connect(appId, connectionId, value) {
1844
+ return this.http.post(
1845
+ `/api/workflow-apps/${appId}/connections/${connectionId}/connect`,
1846
+ { body: value }
1847
+ );
1848
+ }
1849
+ };
1850
+
1851
+ // src/resources/workflows.ts
1852
+ function stepToUpsertInput(step) {
1853
+ const use = step.use;
1854
+ return {
1855
+ appId: resolveActionAppId(use),
1856
+ actionId: resolveActionId(use),
1857
+ connectionId: use.connection?.id,
1858
+ input: {
1859
+ ...use.input ?? {},
1860
+ ...step.input ?? {}
1861
+ },
1862
+ name: step.name ?? use.name,
1863
+ description: use.description
1864
+ };
1865
+ }
1866
+ function workflowDefinitionToUpsert(def) {
1867
+ return {
1868
+ slug: def.slug,
1869
+ name: def.name,
1870
+ description: def.description,
1871
+ isActive: def.isActive,
1872
+ managedByCode: true,
1873
+ steps: def.steps.map(stepToUpsertInput)
1874
+ };
1875
+ }
1876
+ var WorkflowsResource = class {
1877
+ constructor(http, executions) {
1878
+ this.http = http;
1879
+ this.executions = executions;
1880
+ }
1881
+ http;
1882
+ executions;
1883
+ /** Lists workflows in the current workspace. */
1884
+ list(options) {
1885
+ return this.http.get("/api/workflows", {
1886
+ query: serializeListQuery(options)
1887
+ });
1888
+ }
1889
+ /**
1890
+ * Returns a single workflow. Request `expansion: ['triggerNode']` to read
1891
+ * the manual trigger's custom input field definitions.
1892
+ */
1893
+ get(workflowId, options) {
1894
+ return this.http.get(`/api/workflows/${workflowId}`, {
1895
+ query: serializeListQuery(options)
1896
+ });
1897
+ }
1898
+ /**
1899
+ * Upsert a workflow by project-scoped slug from a linear step list.
1900
+ * Prefer the CLI (`npx @lunnoa/client workflows deploy`) for happy-path deploy.
1901
+ */
1902
+ upsertBySlug(projectId, body) {
1903
+ const payload = "kind" in body && body.kind === "workflow" ? workflowDefinitionToUpsert(body) : body;
1904
+ return this.http.post(
1905
+ `/api/projects/${projectId}/workflows/upsert`,
1906
+ { body: payload }
1907
+ );
1908
+ }
1909
+ /**
1910
+ * Starts a new execution of the workflow with the given trigger inputs.
1911
+ * Returns immediately with the execution ID.
1912
+ */
1913
+ execute(workflowId, inputs) {
1914
+ return this.executions.execute(workflowId, inputs);
1915
+ }
1916
+ /**
1917
+ * Convenience: execute the workflow and wait (poll with backoff) until it
1918
+ * reaches a terminal status or pauses in `NEEDS_INPUT`.
1919
+ */
1920
+ async executeAndWait(workflowId, inputs, waitOptions) {
1921
+ const { id } = await this.executions.execute(workflowId, inputs);
1922
+ return this.executions.waitUntilFinished(id, waitOptions);
1923
+ }
1924
+ /**
1925
+ * Triggers a workflow through its unauthenticated custom webhook endpoint
1926
+ * (`POST /api/webhooks/workflows/:workflowId`). Only works for workflows
1927
+ * with a webhook trigger. Returns the synchronous trigger output when the
1928
+ * workflow responds synchronously.
1929
+ */
1930
+ triggerWebhook(workflowId, payload) {
1931
+ return this.http.post(
1932
+ `/api/webhooks/workflows/${workflowId}`,
1933
+ { body: payload ?? {} }
1934
+ );
1935
+ }
1936
+ };
1937
+
1938
+ // src/client.ts
1939
+ var LunnoaClient = class {
1940
+ /** The underlying HTTP layer, exposed for advanced/escape-hatch requests. */
1941
+ http;
1942
+ /** Login, refresh, SSO discovery, and session helpers (Pattern B). */
1943
+ auth;
1944
+ agents;
1945
+ tasks;
1946
+ /** Hand-written SSE streaming client for agent chat (AI SDK UIMessage chunks). */
1947
+ agentChat;
1948
+ workflows;
1949
+ executions;
1950
+ /** Run catalogue actions as one-step ad-hoc Executions (`source: SDK`). */
1951
+ actions;
1952
+ /**
1953
+ * Multi-step ad-hoc Executions (`source: SDK`) via `POST /api/runs`.
1954
+ * Prefer `defineWorkflow` + `runs.start` for local linear chains.
1955
+ */
1956
+ runs;
1957
+ /** Authenticated Approvals inbox (workspace API). */
1958
+ approvals;
1959
+ entities;
1960
+ entityTypes;
1961
+ knowledge;
1962
+ queues;
1963
+ queueItems;
1964
+ variables;
1965
+ connections;
1966
+ projects;
1967
+ workflowApps;
1968
+ discovery;
1969
+ constructor(options) {
1970
+ this.http = new HttpClient(options);
1971
+ this.auth = new AuthResource(this.http, this.http.tokenStore);
1972
+ this.agents = new AgentsResource(this.http);
1973
+ this.tasks = new TasksResource(this.http);
1974
+ this.agentChat = new AgentChatClient(this.http);
1975
+ this.workflowApps = new WorkflowAppsResource(this.http);
1976
+ this.executions = new ExecutionsResource(this.http, this.workflowApps);
1977
+ this.actions = new ActionsResource(this.http, this.executions);
1978
+ this.runs = new RunsResource(this.http, this.executions);
1979
+ this.approvals = new ApprovalsResource(this.http);
1980
+ this.workflows = new WorkflowsResource(this.http, this.executions);
1981
+ this.entities = new EntitiesResource(this.http);
1982
+ this.entityTypes = new EntityTypesResource(this.http);
1983
+ this.knowledge = new KnowledgeResource(this.http);
1984
+ this.queues = new QueuesResource(this.http);
1985
+ this.queueItems = new QueueItemsResource(this.http);
1986
+ this.variables = new VariablesResource(this.http);
1987
+ this.connections = new ConnectionsResource(this.http);
1988
+ this.projects = new ProjectsResource(this.http);
1989
+ this.discovery = new DiscoveryResource(this.http);
1990
+ }
1991
+ };
1992
+
1993
+ // src/cli/generate.ts
1994
+ function toCamelCase(input) {
1995
+ const parts = input.replace(/[^a-zA-Z0-9]+/g, " ").trim().split(/\s+/);
1996
+ if (parts.length === 0 || parts[0] === "") {
1997
+ return "_";
1998
+ }
1999
+ const [first, ...rest] = parts;
2000
+ const camel = first.charAt(0).toLowerCase() + first.slice(1) + rest.map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join("");
2001
+ return /^[0-9]/.test(camel) ? `_${camel}` : camel;
2002
+ }
2003
+ function toPascalCase(input) {
2004
+ const camel = toCamelCase(input);
2005
+ return camel.charAt(0).toUpperCase() + camel.slice(1);
2006
+ }
2007
+ function uniqueName(base, used) {
2008
+ let name = base;
2009
+ let counter = 2;
2010
+ while (used.has(name)) {
2011
+ name = `${base}${counter}`;
2012
+ counter += 1;
2013
+ }
2014
+ used.add(name);
2015
+ return name;
2016
+ }
2017
+ function quote(value) {
2018
+ return JSON.stringify(value);
2019
+ }
2020
+ function docComment(lines, indent = "") {
2021
+ const filtered = lines.filter(
2022
+ (line) => !!line && line.trim().length > 0
2023
+ );
2024
+ if (filtered.length === 0) {
2025
+ return "";
2026
+ }
2027
+ return `${indent}/**
2028
+ ` + filtered.map((line) => `${indent} * ${line.replace(/\*\//g, "*\\/")}`).join("\n") + `
2029
+ ${indent} */
2030
+ `;
2031
+ }
2032
+ function attributeFieldToTsType(field) {
2033
+ const options = field.config?.options;
2034
+ const optionUnion = options && options.length > 0 ? options.map((o) => quote(o.value)).join(" | ") : null;
2035
+ switch (field.type) {
2036
+ case "text":
2037
+ case "longText":
2038
+ case "richText":
2039
+ case "url":
2040
+ case "email":
2041
+ case "phone":
2042
+ return "string";
2043
+ case "number":
2044
+ case "currency":
2045
+ return "number";
2046
+ case "date":
2047
+ case "datetime":
2048
+ return "string";
2049
+ case "boolean":
2050
+ return "boolean";
2051
+ case "dropdown":
2052
+ return optionUnion ?? "string";
2053
+ case "multiSelect":
2054
+ return optionUnion ? `Array<${optionUnion}>` : "string[]";
2055
+ case "user":
2056
+ case "team":
2057
+ case "entityReference":
2058
+ return field.config?.multiple ? "string[]" : "string";
2059
+ case "file":
2060
+ return "unknown";
2061
+ default:
2062
+ return "unknown";
2063
+ }
2064
+ }
2065
+ function workflowInputFieldToTsType(field) {
2066
+ const selectUnion = field.selectOptions && field.selectOptions.length > 0 ? field.selectOptions.filter((o) => typeof o.value === "string").map((o) => quote(o.value)).join(" | ") : null;
2067
+ let base;
2068
+ switch (field.inputType) {
2069
+ case "number":
2070
+ base = "number";
2071
+ break;
2072
+ case "select":
2073
+ case "dynamic-select":
2074
+ base = selectUnion ?? "string";
2075
+ break;
2076
+ case "multi-select":
2077
+ case "dynamic-multi-select":
2078
+ base = selectUnion ? `Array<${selectUnion}>` : "string[]";
2079
+ break;
2080
+ case "switch": {
2081
+ const on = field.switchOptions?.checked;
2082
+ const off = field.switchOptions?.unchecked;
2083
+ base = on && off ? `${quote(on)} | ${quote(off)}` : "string";
2084
+ break;
2085
+ }
2086
+ case "json":
2087
+ base = "unknown";
2088
+ break;
2089
+ case "map":
2090
+ base = "Record<string, string>";
2091
+ break;
2092
+ case "text":
2093
+ case "raw-text":
2094
+ case "date":
2095
+ case "date-time":
2096
+ case "code":
2097
+ case "file":
2098
+ base = "string";
2099
+ break;
2100
+ default:
2101
+ base = "unknown";
2102
+ }
2103
+ if (field.occurenceType === "multiple" && !base.endsWith("[]")) {
2104
+ base = `Array<${base}>`;
2105
+ }
2106
+ return base;
2107
+ }
2108
+ function isFieldRequired(field) {
2109
+ return Boolean(field.required);
2110
+ }
2111
+ function generateDeploymentModule(definitions) {
2112
+ const chunks = [];
2113
+ chunks.push(
2114
+ `/* eslint-disable */
2115
+ /**
2116
+ * Deployment-specific typed accessors for ${definitions.baseUrl}
2117
+ *
2118
+ * Generated by \`npx @lunnoa/client codegen\` \u2014 do not edit by hand.
2119
+ * Re-run codegen whenever entity types, workflows, or agents change on
2120
+ * the deployment.
2121
+ */
2122
+ import type { LunnoaClient, Entity, Execution, ExecuteWorkflowResult, PaginatedEntities, Task, AgentChatStream, WaitUntilFinishedOptions, ListEntitiesOptions, UIMessage } from '@lunnoa/client';
2123
+ `
2124
+ );
2125
+ chunks.push(GENERATED_RUNTIME_HELPERS);
2126
+ const usedTypeNames = /* @__PURE__ */ new Set();
2127
+ const usedPropNames = /* @__PURE__ */ new Set();
2128
+ const entityEntries = [];
2129
+ for (const entityType of definitions.entityTypes) {
2130
+ const typeName = uniqueName(toPascalCase(entityType.slug || entityType.name), usedTypeNames);
2131
+ const prop = uniqueName(toCamelCase(entityType.slug || entityType.name), usedPropNames);
2132
+ entityEntries.push({ prop, typeName, def: entityType });
2133
+ chunks.push(generateEntityTypeBlock(entityType, typeName));
2134
+ }
2135
+ const usedWorkflowProps = /* @__PURE__ */ new Set();
2136
+ const workflowEntries = [];
2137
+ for (const workflow of definitions.workflows) {
2138
+ const fields = workflow.triggerNode?.value?.customInputConfig ?? [];
2139
+ const prop = uniqueName(toCamelCase(workflow.name), usedWorkflowProps);
2140
+ const typeName = uniqueName(`${toPascalCase(workflow.name)}Inputs`, usedTypeNames);
2141
+ workflowEntries.push({ prop, typeName, def: workflow, fields });
2142
+ chunks.push(generateWorkflowInputsBlock(workflow, typeName, fields));
2143
+ }
2144
+ const usedAgentProps = /* @__PURE__ */ new Set();
2145
+ const agentEntries = [];
2146
+ for (const agent of definitions.agents) {
2147
+ agentEntries.push({
2148
+ prop: uniqueName(toCamelCase(agent.name), usedAgentProps),
2149
+ def: agent
2150
+ });
2151
+ }
2152
+ chunks.push(generateFactory(entityEntries, workflowEntries, agentEntries));
2153
+ return chunks.join("\n");
2154
+ }
2155
+ function generateEntityTypeBlock(entityType, typeName) {
2156
+ const fields = (entityType.attributeSchema?.sections ?? []).flatMap(
2157
+ (section) => section.fields ?? []
2158
+ );
2159
+ const states = entityType.stateSchema?.states ?? [];
2160
+ const fieldLines = fields.map((field) => {
2161
+ const doc2 = docComment(
2162
+ [field.label ?? field.name, field.helpText],
2163
+ " "
2164
+ );
2165
+ const optional = field.required ? "" : "?";
2166
+ const key = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(field.id) ? field.id : quote(field.id);
2167
+ return `${doc2} ${key}${optional}: ${attributeFieldToTsType(field)};`;
2168
+ }).join("\n");
2169
+ const stateType = states.length > 0 ? states.map((s) => quote(s.id)).join(" | ") : "string";
2170
+ const doc = docComment([
2171
+ `${entityType.name} (\`${entityType.slug}\`)`,
2172
+ entityType.description ?? void 0
2173
+ ]);
2174
+ return `${doc}export interface ${typeName}Attributes {
2175
+ ${fieldLines || " [fieldId: string]: unknown;"}
2176
+ }
2177
+
2178
+ export type ${typeName}State = ${stateType};
2179
+
2180
+ export type ${typeName} = Entity & { attributes?: ${typeName}Attributes; state?: ${typeName}State };
2181
+ `;
2182
+ }
2183
+ function generateWorkflowInputsBlock(workflow, typeName, fields) {
2184
+ const doc = docComment([
2185
+ `Trigger inputs for workflow "${workflow.name}"`,
2186
+ workflow.description ?? void 0
2187
+ ]);
2188
+ if (fields.length === 0) {
2189
+ return `${doc}export type ${typeName} = Record<string, unknown>;
2190
+ `;
2191
+ }
2192
+ const fieldLines = fields.map((field) => {
2193
+ const fieldDoc = docComment([field.label, field.description], " ");
2194
+ const optional = isFieldRequired(field) ? "" : "?";
2195
+ const key = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(field.id) ? field.id : quote(field.id);
2196
+ return `${fieldDoc} ${key}${optional}: ${workflowInputFieldToTsType(field)};`;
2197
+ }).join("\n");
2198
+ return `${doc}export interface ${typeName} {
2199
+ ${fieldLines}
2200
+ }
2201
+ `;
2202
+ }
2203
+ function generateFactory(entityEntries, workflowEntries, agentEntries) {
2204
+ const entityProps = entityEntries.map(({ prop, typeName, def }) => {
2205
+ const doc = docComment([`${def.name} entities (\`${def.slug}\`)`], " ");
2206
+ return `${doc} ${prop}: typedEntityAccessor<${typeName}Attributes, ${typeName}State>(client, ${quote(def.id)}),`;
2207
+ }).join("\n");
2208
+ const workflowProps = workflowEntries.map(({ prop, typeName, def }) => {
2209
+ const doc = docComment(
2210
+ [
2211
+ `Workflow "${def.name}"`,
2212
+ def.description ?? void 0,
2213
+ def.isActive === false ? "NOTE: currently inactive on the deployment." : void 0
2214
+ ],
2215
+ " "
2216
+ );
2217
+ return `${doc} ${prop}: typedWorkflowAccessor<${typeName}>(client, ${quote(def.id)}),`;
2218
+ }).join("\n");
2219
+ const agentProps = agentEntries.map(({ prop, def }) => {
2220
+ const doc = docComment(
2221
+ [`Agent "${def.name}"`, def.description ?? void 0],
2222
+ " "
2223
+ );
2224
+ return `${doc} ${prop}: typedAgentAccessor(client, ${quote(def.id)}),`;
2225
+ }).join("\n");
2226
+ return `/**
2227
+ * Wraps a \`LunnoaClient\` with accessors typed from this deployment's own
2228
+ * entity types, workflows, and agents.
2229
+ *
2230
+ * \`\`\`ts
2231
+ * const lunnoa = createDeploymentClient(new LunnoaClient({ baseUrl, apiKey }));
2232
+ * const open = await lunnoa.entities.invoice.list({ state: 'pending' });
2233
+ * \`\`\`
2234
+ */
2235
+ export function createDeploymentClient(client: LunnoaClient) {
2236
+ return {
2237
+ client,
2238
+ entities: {
2239
+ ${entityProps}
2240
+ },
2241
+ workflows: {
2242
+ ${workflowProps}
2243
+ },
2244
+ agents: {
2245
+ ${agentProps}
2246
+ },
2247
+ };
2248
+ }
2249
+ `;
2250
+ }
2251
+ var GENERATED_RUNTIME_HELPERS = `
2252
+ // ---------------------------------------------------------------------------
2253
+ // Small runtime helpers the generated accessors are built from.
2254
+ // ---------------------------------------------------------------------------
2255
+
2256
+ function typedEntityAccessor<A, S extends string>(
2257
+ client: LunnoaClient,
2258
+ objectTypeId: string,
2259
+ ) {
2260
+ type Typed = Entity & { attributes?: A; state?: S };
2261
+ return {
2262
+ objectTypeId,
2263
+ /** Lists entities of this type (attributes expanded by default). */
2264
+ async list(
2265
+ options: Omit<ListEntitiesOptions, 'objectTypeId' | 'objectTypeSlug' | 'state' | 'states'> & {
2266
+ state?: S;
2267
+ states?: S[];
2268
+ } = {},
2269
+ ): Promise<Omit<PaginatedEntities, 'data'> & { data: Typed[] }> {
2270
+ const result = await client.entities.list({
2271
+ expansion: ['attributes'],
2272
+ ...options,
2273
+ objectTypeId,
2274
+ });
2275
+ return result as Omit<PaginatedEntities, 'data'> & { data: Typed[] };
2276
+ },
2277
+ /** Async-iterates every entity of this type. */
2278
+ iterate(
2279
+ options: Omit<ListEntitiesOptions, 'objectTypeId' | 'objectTypeSlug' | 'page' | 'state' | 'states'> & {
2280
+ state?: S;
2281
+ states?: S[];
2282
+ } = {},
2283
+ ): AsyncGenerator<Typed, void, undefined> {
2284
+ return client.entities.iterate({
2285
+ expansion: ['attributes'],
2286
+ ...options,
2287
+ objectTypeId,
2288
+ }) as AsyncGenerator<Typed, void, undefined>;
2289
+ },
2290
+ /** Returns a single entity of this type. */
2291
+ async get(entityId: string): Promise<Typed> {
2292
+ return (await client.entities.get(entityId, {
2293
+ expansion: ['attributes'],
2294
+ })) as Typed;
2295
+ },
2296
+ /** Creates an entity of this type with compile-time checked attributes. */
2297
+ async create(data: { name: string; attributes?: A; state?: S; ownerId?: string }): Promise<Typed> {
2298
+ return (await client.entities.create(
2299
+ { ...data, objectTypeId } as never,
2300
+ { expansion: ['attributes'] },
2301
+ )) as Typed;
2302
+ },
2303
+ /** Partially updates an entity of this type. */
2304
+ async update(
2305
+ entityId: string,
2306
+ data: { name?: string; attributes?: Partial<A>; ownerId?: string },
2307
+ ): Promise<Typed> {
2308
+ return (await client.entities.update(entityId, data as never, {
2309
+ expansion: ['attributes'],
2310
+ })) as Typed;
2311
+ },
2312
+ /** Soft-deletes an entity of this type. */
2313
+ delete(entityId: string): Promise<boolean> {
2314
+ return client.entities.delete(entityId);
2315
+ },
2316
+ /** Moves an entity of this type to a new state. */
2317
+ changeState(entityId: string, newState: S, reason?: string) {
2318
+ return client.entities.changeState(entityId, { newState, reason } as never);
2319
+ },
2320
+ };
2321
+ }
2322
+
2323
+ function typedWorkflowAccessor<I>(client: LunnoaClient, workflowId: string) {
2324
+ return {
2325
+ workflowId,
2326
+ /** Starts an execution with compile-time checked trigger inputs. */
2327
+ execute(inputs: I): Promise<ExecuteWorkflowResult> {
2328
+ return client.workflows.execute(workflowId, inputs as Record<string, unknown>);
2329
+ },
2330
+ /** Starts an execution and polls until it finishes (or pauses in NEEDS_INPUT). */
2331
+ executeAndWait(inputs: I, waitOptions?: WaitUntilFinishedOptions): Promise<Execution> {
2332
+ return client.workflows.executeAndWait(
2333
+ workflowId,
2334
+ inputs as Record<string, unknown>,
2335
+ waitOptions,
2336
+ );
2337
+ },
2338
+ };
2339
+ }
2340
+
2341
+ function typedAgentAccessor(client: LunnoaClient, agentId: string) {
2342
+ return {
2343
+ agentId,
2344
+ /** Creates a task (conversation thread) for this agent. */
2345
+ createTask(data: { name?: string; id?: string; customIdentifier?: string } = {}): Promise<Task> {
2346
+ return client.tasks.create(agentId, data as never);
2347
+ },
2348
+ /** Sends a message and streams the assistant turn (SSE, AI SDK chunks). */
2349
+ streamMessage(
2350
+ taskId: string,
2351
+ message: string | UIMessage,
2352
+ options?: { chatMode?: 'builder' | 'preview'; signal?: AbortSignal },
2353
+ ): Promise<AgentChatStream> {
2354
+ return client.agentChat.streamMessage(agentId, taskId, message, options);
2355
+ },
2356
+ /** Sends a message and waits for the full turn (non-streaming). */
2357
+ message(taskId: string, message: string | UIMessage) {
2358
+ return client.tasks.message(agentId, taskId, message);
2359
+ },
2360
+ };
2361
+ }
2362
+ `;
2363
+
2364
+ // src/cli/load-definition.ts
2365
+ import { pathToFileURL } from "url";
2366
+ import { resolve } from "path";
2367
+ import { access } from "fs/promises";
2368
+ import { constants as fsConstants } from "fs";
2369
+ async function fileExists(path) {
2370
+ try {
2371
+ await access(path, fsConstants.R_OK);
2372
+ return true;
2373
+ } catch {
2374
+ return false;
2375
+ }
2376
+ }
2377
+ async function loadDefinitionModule(pathArg) {
2378
+ const absolute = resolve(process.cwd(), pathArg);
2379
+ if (!await fileExists(absolute)) {
2380
+ throw new Error(`Definition file not found: ${absolute}`);
2381
+ }
2382
+ const mod = await import(pathToFileURL(absolute).href);
2383
+ return mod;
2384
+ }
2385
+ function pickWorkflowDefinition(mod) {
2386
+ const candidates = [
2387
+ mod.default,
2388
+ mod.workflow,
2389
+ ...Object.values(mod)
2390
+ ];
2391
+ for (const value of candidates) {
2392
+ if (value && typeof value === "object" && value.kind === DEFINE_KINDS.workflow) {
2393
+ return value;
2394
+ }
2395
+ }
2396
+ throw new Error(
2397
+ "No defineWorkflow() export found. Export the definition as default or `workflow`."
2398
+ );
2399
+ }
2400
+ function pickAgentDefinition(mod) {
2401
+ const candidates = [mod.default, mod.agent, ...Object.values(mod)];
2402
+ for (const value of candidates) {
2403
+ if (value && typeof value === "object" && value.kind === DEFINE_KINDS.agent) {
2404
+ return value;
2405
+ }
2406
+ }
2407
+ throw new Error(
2408
+ "No defineAgent() export found. Export the definition as default or `agent`."
2409
+ );
2410
+ }
2411
+ async function loadOptionalConfig() {
2412
+ const candidates = [
2413
+ "lunnoa.config.ts",
2414
+ "lunnoa.config.mjs",
2415
+ "lunnoa.config.js",
2416
+ "lunnoa.config.json"
2417
+ ];
2418
+ for (const name of candidates) {
2419
+ const absolute = resolve(process.cwd(), name);
2420
+ if (!await fileExists(absolute)) {
2421
+ continue;
2422
+ }
2423
+ if (name.endsWith(".json")) {
2424
+ const { readFile } = await import("fs/promises");
2425
+ const raw = await readFile(absolute, "utf8");
2426
+ return JSON.parse(raw);
2427
+ }
2428
+ const mod = await import(pathToFileURL(absolute).href);
2429
+ const cfg = mod.default ?? mod;
2430
+ return {
2431
+ url: cfg.url,
2432
+ apiKey: cfg.apiKey,
2433
+ projectId: cfg.projectId
2434
+ };
2435
+ }
2436
+ return {};
2437
+ }
2438
+
2439
+ // src/cli/index.ts
2440
+ function parseArgs(argv) {
2441
+ const args = { command: argv[0], out: "./src/lunnoa" };
2442
+ let i = 1;
2443
+ if (args.command === "workflows" || args.command === "agents") {
2444
+ args.subcommand = argv[1];
2445
+ i = 2;
2446
+ if (argv[2] && !argv[2].startsWith("--")) {
2447
+ args.path = argv[2];
2448
+ i = 3;
2449
+ }
2450
+ }
2451
+ for (; i < argv.length; i += 1) {
2452
+ const flag = argv[i];
2453
+ const value = argv[i + 1];
2454
+ switch (flag) {
2455
+ case "--url":
2456
+ args.url = value;
2457
+ i += 1;
2458
+ break;
2459
+ case "--api-key":
2460
+ args.apiKey = value;
2461
+ i += 1;
2462
+ break;
2463
+ case "--access-token":
2464
+ args.accessToken = value;
2465
+ i += 1;
2466
+ break;
2467
+ case "--project-id":
2468
+ args.projectId = value;
2469
+ i += 1;
2470
+ break;
2471
+ case "--out":
2472
+ args.out = value;
2473
+ i += 1;
2474
+ break;
2475
+ default:
2476
+ if (!args.path && !flag.startsWith("--")) {
2477
+ args.path = flag;
2478
+ break;
2479
+ }
2480
+ throw new Error(`Unknown option: ${flag}`);
2481
+ }
2482
+ }
2483
+ return args;
2484
+ }
2485
+ var USAGE = `Usage:
2486
+ npx @lunnoa/client codegen --url <deployment-url> --api-key <lna_key> [--out <dir>]
2487
+ npx @lunnoa/client workflows deploy <path> --url <url> --api-key <key> --project-id <uuid>
2488
+ npx @lunnoa/client agents deploy <path> --url <url> --api-key <key> --project-id <uuid>
2489
+
2490
+ Options:
2491
+ --url Base URL of the Lunnoa Automate deployment
2492
+ --api-key Machine API key (lna_...)
2493
+ --access-token User JWT as an alternative to --api-key
2494
+ --project-id Project UUID (required for deploy)
2495
+ --out Output directory for lunnoa.generated.ts (codegen; default: ./src/lunnoa)
2496
+
2497
+ Environment: LUNNOA_URL, LUNNOA_API_KEY, LUNNOA_PROJECT_ID
2498
+ Optional config file: lunnoa.config.ts | .mjs | .js | .json
2499
+ `;
2500
+ async function bindClient(args) {
2501
+ const fileConfig = await loadOptionalConfig();
2502
+ const url = args.url ?? process.env.LUNNOA_URL ?? fileConfig.url;
2503
+ const apiKey = args.apiKey ?? process.env.LUNNOA_API_KEY ?? fileConfig.apiKey;
2504
+ const accessToken = args.accessToken;
2505
+ const projectId = args.projectId ?? process.env.LUNNOA_PROJECT_ID ?? fileConfig.projectId;
2506
+ if (!url) {
2507
+ throw new Error("Missing --url (or LUNNOA_URL / lunnoa.config).");
2508
+ }
2509
+ if (!apiKey && !accessToken) {
2510
+ throw new Error(
2511
+ "Missing --api-key (or LUNNOA_API_KEY) or --access-token."
2512
+ );
2513
+ }
2514
+ return {
2515
+ client: new LunnoaClient({ baseUrl: url, apiKey, accessToken }),
2516
+ projectId,
2517
+ url
2518
+ };
2519
+ }
2520
+ async function fetchDefinitions(client, baseUrl) {
2521
+ const entityTypes = await client.entityTypes.list({
2522
+ expansion: ["attributeSchema", "stateSchema", "description"]
2523
+ });
2524
+ const workflowSummaries = await client.workflows.list({
2525
+ expansion: ["description", "isActive"]
2526
+ });
2527
+ const workflows = [];
2528
+ for (const summary of workflowSummaries) {
2529
+ if (!summary.id) {
2530
+ continue;
2531
+ }
2532
+ try {
2533
+ const full = await client.workflows.get(summary.id, {
2534
+ expansion: ["description", "isActive", "triggerNode"]
2535
+ });
2536
+ workflows.push(full);
2537
+ } catch {
2538
+ workflows.push(summary);
2539
+ }
2540
+ }
2541
+ const agents = await client.agents.list({
2542
+ expansion: ["description"]
2543
+ });
2544
+ return { baseUrl, entityTypes, workflows, agents };
2545
+ }
2546
+ async function runCodegen(args) {
2547
+ const { client, url } = await bindClient(args);
2548
+ console.log(`Querying ${url} ...`);
2549
+ const definitions = await fetchDefinitions(client, url);
2550
+ console.log(
2551
+ `Found ${definitions.entityTypes.length} entity type(s), ${definitions.workflows.length} workflow(s), ${definitions.agents.length} agent(s).`
2552
+ );
2553
+ const moduleSource = generateDeploymentModule(definitions);
2554
+ const outDir = resolve2(process.cwd(), args.out);
2555
+ await mkdir(outDir, { recursive: true });
2556
+ const outFile = resolve2(outDir, "lunnoa.generated.ts");
2557
+ await writeFile(outFile, moduleSource, "utf8");
2558
+ console.log(`Wrote ${outFile}`);
2559
+ }
2560
+ async function deployWorkflow(args) {
2561
+ if (!args.path) {
2562
+ throw new Error("Missing definition path. Example: workflows deploy ./workflow.ts");
2563
+ }
2564
+ const { client, projectId } = await bindClient(args);
2565
+ if (!projectId) {
2566
+ throw new Error(
2567
+ "Missing --project-id (or LUNNOA_PROJECT_ID / lunnoa.config)."
2568
+ );
2569
+ }
2570
+ const mod = await loadDefinitionModule(args.path);
2571
+ const def = pickWorkflowDefinition(mod);
2572
+ const payload = workflowDefinitionToUpsert(def);
2573
+ console.log(`Deploying workflow slug="${payload.slug}" to project ${projectId} ...`);
2574
+ const result = await client.workflows.upsertBySlug(projectId, payload);
2575
+ console.log(`Upserted workflow ${result.id} (${payload.slug})`);
2576
+ }
2577
+ async function deployAgent(args) {
2578
+ if (!args.path) {
2579
+ throw new Error("Missing definition path. Example: agents deploy ./agent.ts");
2580
+ }
2581
+ const { client, projectId } = await bindClient(args);
2582
+ if (!projectId) {
2583
+ throw new Error(
2584
+ "Missing --project-id (or LUNNOA_PROJECT_ID / lunnoa.config)."
2585
+ );
2586
+ }
2587
+ const mod = await loadDefinitionModule(args.path);
2588
+ const def = pickAgentDefinition(mod);
2589
+ const workflowIds = [];
2590
+ for (const tool of def.tools ?? []) {
2591
+ const unwrapped = "use" in tool && tool.use ? tool.use : tool;
2592
+ if (unwrapped && typeof unwrapped === "object" && unwrapped.kind === DEFINE_KINDS.workflow) {
2593
+ const wf = unwrapped;
2594
+ const upserted = await client.workflows.upsertBySlug(
2595
+ projectId,
2596
+ workflowDefinitionToUpsert(wf)
2597
+ );
2598
+ if (upserted.id) {
2599
+ workflowIds.push(upserted.id);
2600
+ console.log(`Linked workflow tool ${upserted.id} (${wf.slug})`);
2601
+ }
2602
+ }
2603
+ }
2604
+ const payload = agentDefinitionToUpsert(def, { workflowIds });
2605
+ console.log(`Deploying agent slug="${payload.slug}" to project ${projectId} ...`);
2606
+ const result = await client.agents.upsertBySlug(projectId, payload);
2607
+ console.log(`Upserted agent ${result.id} (${payload.slug})`);
2608
+ }
2609
+ async function main() {
2610
+ const args = parseArgs(process.argv.slice(2));
2611
+ if (!args.command || args.command === "--help" || args.command === "help") {
2612
+ console.log(USAGE);
2613
+ return;
2614
+ }
2615
+ if (args.command === "codegen") {
2616
+ await runCodegen(args);
2617
+ return;
2618
+ }
2619
+ if (args.command === "workflows" && args.subcommand === "deploy") {
2620
+ await deployWorkflow(args);
2621
+ return;
2622
+ }
2623
+ if (args.command === "agents" && args.subcommand === "deploy") {
2624
+ await deployAgent(args);
2625
+ return;
2626
+ }
2627
+ console.error(`Unknown command: ${args.command} ${args.subcommand ?? ""}
2628
+ `);
2629
+ console.log(USAGE);
2630
+ process.exitCode = 1;
2631
+ }
2632
+ main().catch((error) => {
2633
+ console.error(error instanceof Error ? error.message : error);
2634
+ process.exitCode = 1;
2635
+ });