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