@ziggy-ai/client-sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,738 @@
1
+ // src/types.ts
2
+ var ZiggyError = class extends Error {
3
+ constructor(message, status, code, details) {
4
+ super(message);
5
+ this.status = status;
6
+ this.code = code;
7
+ this.details = details;
8
+ this.name = "ZiggyError";
9
+ }
10
+ };
11
+ var ZiggyConnectionError = class extends ZiggyError {
12
+ constructor(message, retryable = true) {
13
+ super(message, 0, "CONNECTION_ERROR");
14
+ this.retryable = retryable;
15
+ this.name = "ZiggyConnectionError";
16
+ }
17
+ };
18
+
19
+ // src/auth.ts
20
+ function buildHeaders(config, extra) {
21
+ return {
22
+ "Content-Type": "application/json",
23
+ "X-API-Key": config.apiKey,
24
+ ...extra
25
+ };
26
+ }
27
+
28
+ // src/connection.ts
29
+ var Connection = class {
30
+ config;
31
+ constructor(config) {
32
+ this.config = config;
33
+ }
34
+ /**
35
+ * Make an authenticated JSON request.
36
+ */
37
+ async request(method, path, body, extraHeaders) {
38
+ const url = `${this.config.baseUrl}/v1${path}`;
39
+ const headers = buildHeaders(this.config, extraHeaders);
40
+ const timeout = this.config.timeout ?? 3e4;
41
+ const controller = new AbortController();
42
+ const timer = setTimeout(() => controller.abort(), timeout);
43
+ try {
44
+ const response = await fetch(url, {
45
+ method,
46
+ headers,
47
+ body: body ? JSON.stringify(body) : void 0,
48
+ signal: controller.signal
49
+ });
50
+ if (!response.ok) {
51
+ let errorBody;
52
+ try {
53
+ errorBody = await response.json();
54
+ } catch {
55
+ errorBody = await response.text();
56
+ }
57
+ const message = typeof errorBody === "object" && errorBody !== null && "error" in errorBody ? errorBody.error : `HTTP ${response.status}`;
58
+ throw new ZiggyError(message, response.status, void 0, errorBody);
59
+ }
60
+ return await response.json();
61
+ } catch (error) {
62
+ if (error instanceof ZiggyError) throw error;
63
+ if (error instanceof DOMException && error.name === "AbortError") {
64
+ throw new ZiggyConnectionError("Request timed out", true);
65
+ }
66
+ throw new ZiggyConnectionError(
67
+ error instanceof Error ? error.message : "Connection failed",
68
+ true
69
+ );
70
+ } finally {
71
+ clearTimeout(timer);
72
+ }
73
+ }
74
+ /**
75
+ * Make an authenticated request that returns a ReadableStream (for ndjson).
76
+ */
77
+ async streamRequest(method, path, body) {
78
+ const url = `${this.config.baseUrl}/v1${path}`;
79
+ const headers = buildHeaders(this.config);
80
+ try {
81
+ const response = await fetch(url, {
82
+ method,
83
+ headers,
84
+ body: body ? JSON.stringify(body) : void 0
85
+ });
86
+ if (!response.ok) {
87
+ let errorBody;
88
+ try {
89
+ errorBody = await response.json();
90
+ } catch {
91
+ errorBody = await response.text();
92
+ }
93
+ const message = typeof errorBody === "object" && errorBody !== null && "error" in errorBody ? errorBody.error : `HTTP ${response.status}`;
94
+ throw new ZiggyError(message, response.status, void 0, errorBody);
95
+ }
96
+ return response;
97
+ } catch (error) {
98
+ if (error instanceof ZiggyError) throw error;
99
+ throw new ZiggyConnectionError(
100
+ error instanceof Error ? error.message : "Connection failed",
101
+ true
102
+ );
103
+ }
104
+ }
105
+ };
106
+
107
+ // src/session.ts
108
+ var SessionManager = class {
109
+ connection;
110
+ appId;
111
+ _sessionId = null;
112
+ _conversationId = null;
113
+ constructor(connection, config) {
114
+ this.connection = connection;
115
+ this.appId = config.appId;
116
+ }
117
+ get sessionId() {
118
+ return this._sessionId;
119
+ }
120
+ get conversationId() {
121
+ return this._conversationId;
122
+ }
123
+ /**
124
+ * Create or resume a session.
125
+ * Replaces 4 separate calls (brand + context + triggers + actions).
126
+ */
127
+ async create(request) {
128
+ const response = await this.connection.request("POST", "/sessions", {
129
+ appId: this.appId,
130
+ ...request,
131
+ capabilities: request.capabilities ?? ["streaming", "actions"]
132
+ });
133
+ this._sessionId = response.sessionId;
134
+ this._conversationId = response.conversationId;
135
+ return response;
136
+ }
137
+ /**
138
+ * Push context update (delta merge) with piggybacked trigger evaluation.
139
+ */
140
+ async pushContext(userId, context, events) {
141
+ if (!this._sessionId) {
142
+ throw new Error("No active session \u2014 call create() first");
143
+ }
144
+ return this.connection.request(
145
+ "POST",
146
+ `/sessions/${this._sessionId}/context`,
147
+ { userId, context, events }
148
+ );
149
+ }
150
+ /**
151
+ * End the current session.
152
+ */
153
+ async destroy() {
154
+ if (!this._sessionId) return;
155
+ await this.connection.request("DELETE", `/sessions/${this._sessionId}`);
156
+ this._sessionId = null;
157
+ this._conversationId = null;
158
+ }
159
+ };
160
+
161
+ // src/stream-parser.ts
162
+ async function* parseNdjsonStream(response) {
163
+ const reader = response.body?.getReader();
164
+ if (!reader) throw new Error("Response has no body");
165
+ const decoder = new TextDecoder();
166
+ let buffer = "";
167
+ try {
168
+ while (true) {
169
+ const { done, value } = await reader.read();
170
+ if (done) break;
171
+ buffer += decoder.decode(value, { stream: true });
172
+ const lines = buffer.split("\n");
173
+ buffer = lines.pop() || "";
174
+ for (const line of lines) {
175
+ const trimmed = line.trim();
176
+ if (!trimmed) continue;
177
+ try {
178
+ const event = JSON.parse(trimmed);
179
+ yield event;
180
+ } catch {
181
+ console.warn("[ZiggySDK] Malformed ndjson line:", trimmed);
182
+ }
183
+ }
184
+ }
185
+ if (buffer.trim()) {
186
+ try {
187
+ const event = JSON.parse(buffer.trim());
188
+ yield event;
189
+ } catch {
190
+ }
191
+ }
192
+ } finally {
193
+ reader.releaseLock();
194
+ }
195
+ }
196
+ var SequenceTracker = class {
197
+ _lastSeq = -1;
198
+ _streamId = null;
199
+ get lastSeq() {
200
+ return this._lastSeq;
201
+ }
202
+ get streamId() {
203
+ return this._streamId;
204
+ }
205
+ update(event) {
206
+ if ("seq" in event && typeof event.seq === "number") {
207
+ this._lastSeq = event.seq;
208
+ }
209
+ if (event.type === "stream.start") {
210
+ this._streamId = event.streamId;
211
+ }
212
+ }
213
+ reset() {
214
+ this._lastSeq = -1;
215
+ this._streamId = null;
216
+ }
217
+ };
218
+
219
+ // src/chat.ts
220
+ var ChatClient = class {
221
+ connection;
222
+ sessionManager;
223
+ userId = null;
224
+ constructor(connection, sessionManager) {
225
+ this.connection = connection;
226
+ this.sessionManager = sessionManager;
227
+ }
228
+ /** Set the current user ID (called after session creation) */
229
+ setUserId(userId) {
230
+ this.userId = userId;
231
+ }
232
+ /**
233
+ * Send a non-streaming chat message.
234
+ */
235
+ async send(request) {
236
+ const sessionId = this.sessionManager.sessionId;
237
+ if (!sessionId) throw new Error("No active session");
238
+ if (!this.userId) throw new Error("No user ID set");
239
+ return this.connection.request("POST", "/chat", {
240
+ sessionId,
241
+ userId: this.userId,
242
+ ...request
243
+ });
244
+ }
245
+ /**
246
+ * Send a streaming chat message.
247
+ * Returns an async iterable of structured stream events.
248
+ */
249
+ async *stream(request) {
250
+ const sessionId = this.sessionManager.sessionId;
251
+ if (!sessionId) throw new Error("No active session");
252
+ if (!this.userId) throw new Error("No user ID set");
253
+ const response = await this.connection.streamRequest("POST", "/chat/stream", {
254
+ sessionId,
255
+ userId: this.userId,
256
+ ...request
257
+ });
258
+ const tracker = new SequenceTracker();
259
+ for await (const event of parseNdjsonStream(response)) {
260
+ tracker.update(event);
261
+ yield event;
262
+ }
263
+ }
264
+ /**
265
+ * Resume an interrupted stream from a given sequence number.
266
+ */
267
+ async *resume(streamId, afterSeq) {
268
+ const response = await this.connection.streamRequest(
269
+ "GET",
270
+ `/chat/stream/${streamId}?after_seq=${afterSeq}`
271
+ );
272
+ for await (const event of parseNdjsonStream(response)) {
273
+ yield event;
274
+ }
275
+ }
276
+ };
277
+
278
+ // src/context.ts
279
+ var FLUSH_INTERVAL_MS = 1e4;
280
+ var IMPORTANT_FIELDS = ["page", "errors", "process"];
281
+ var ContextManager = class {
282
+ sessionManager;
283
+ userId = null;
284
+ pendingContext = {};
285
+ pendingEvents = [];
286
+ flushTimer = null;
287
+ onTrigger;
288
+ constructor(sessionManager) {
289
+ this.sessionManager = sessionManager;
290
+ }
291
+ /** Set user ID and start batching */
292
+ start(userId, onTrigger) {
293
+ this.userId = userId;
294
+ this.onTrigger = onTrigger;
295
+ this.flushTimer = setInterval(() => {
296
+ this.flush().catch(() => {
297
+ });
298
+ }, FLUSH_INTERVAL_MS);
299
+ }
300
+ /** Stop batching */
301
+ stop() {
302
+ if (this.flushTimer) {
303
+ clearInterval(this.flushTimer);
304
+ this.flushTimer = null;
305
+ }
306
+ }
307
+ /**
308
+ * Queue a context update. Merges with pending batch.
309
+ * Flushes immediately for important changes.
310
+ */
311
+ update(context) {
312
+ Object.assign(this.pendingContext, context);
313
+ const hasImportant = Object.keys(context).some((k) => IMPORTANT_FIELDS.includes(k));
314
+ if (hasImportant) {
315
+ this.flush().catch(() => {
316
+ });
317
+ }
318
+ }
319
+ /**
320
+ * Queue an event for the next flush.
321
+ */
322
+ addEvent(event) {
323
+ this.pendingEvents.push(event);
324
+ }
325
+ /**
326
+ * Flush pending context and events to the server.
327
+ */
328
+ async flush() {
329
+ if (!this.userId) return;
330
+ if (Object.keys(this.pendingContext).length === 0 && this.pendingEvents.length === 0) return;
331
+ const context = { ...this.pendingContext };
332
+ const events = [...this.pendingEvents];
333
+ this.pendingContext = {};
334
+ this.pendingEvents = [];
335
+ try {
336
+ const response = await this.sessionManager.pushContext(this.userId, context, events.length > 0 ? events : void 0);
337
+ if (response.trigger && this.onTrigger) {
338
+ this.onTrigger(response);
339
+ }
340
+ } catch {
341
+ this.pendingContext = { ...context, ...this.pendingContext };
342
+ this.pendingEvents = [...events, ...this.pendingEvents];
343
+ }
344
+ }
345
+ };
346
+
347
+ // src/tools.ts
348
+ var ToolClient = class {
349
+ connection;
350
+ sessionManager;
351
+ userId = null;
352
+ constructor(connection, sessionManager) {
353
+ this.connection = connection;
354
+ this.sessionManager = sessionManager;
355
+ }
356
+ setUserId(userId) {
357
+ this.userId = userId;
358
+ }
359
+ /**
360
+ * Invoke a tool (non-streaming).
361
+ */
362
+ async invoke(toolId, input) {
363
+ const sessionId = this.sessionManager.sessionId;
364
+ if (!sessionId) throw new Error("No active session");
365
+ if (!this.userId) throw new Error("No user ID set");
366
+ return this.connection.request("POST", "/tools/invoke", {
367
+ sessionId,
368
+ userId: this.userId,
369
+ toolId,
370
+ input: input ?? {}
371
+ });
372
+ }
373
+ /**
374
+ * Invoke a tool with streaming (for long-running tools).
375
+ */
376
+ async *invokeStream(toolId, input) {
377
+ const sessionId = this.sessionManager.sessionId;
378
+ if (!sessionId) throw new Error("No active session");
379
+ if (!this.userId) throw new Error("No user ID set");
380
+ const response = await this.connection.streamRequest("POST", "/tools/invoke/stream", {
381
+ sessionId,
382
+ userId: this.userId,
383
+ toolId,
384
+ input: input ?? {}
385
+ });
386
+ for await (const event of parseNdjsonStream(response)) {
387
+ yield event;
388
+ }
389
+ }
390
+ };
391
+
392
+ // src/feedback.ts
393
+ var FeedbackClient = class {
394
+ connection;
395
+ constructor(connection) {
396
+ this.connection = connection;
397
+ }
398
+ /**
399
+ * Quick reaction on a message.
400
+ */
401
+ async react(messageId, reaction) {
402
+ await this.connection.request("POST", "/feedback/reaction", {
403
+ messageId,
404
+ reaction
405
+ });
406
+ }
407
+ /**
408
+ * Submit detailed feedback.
409
+ */
410
+ async submit(feedback) {
411
+ await this.connection.request("POST", "/feedback", feedback);
412
+ }
413
+ /**
414
+ * Record an outcome.
415
+ */
416
+ async recordOutcome(outcome) {
417
+ await this.connection.request("POST", "/feedback/outcome", {
418
+ ...outcome,
419
+ agentIds: outcome.agentIds ?? []
420
+ });
421
+ }
422
+ };
423
+
424
+ // src/quests.ts
425
+ var QuestClient = class {
426
+ connection;
427
+ constructor(connection) {
428
+ this.connection = connection;
429
+ }
430
+ /**
431
+ * Create a new quest.
432
+ */
433
+ async create(request) {
434
+ const result = await this.connection.request(
435
+ "POST",
436
+ "/quests",
437
+ request
438
+ );
439
+ return result.quest;
440
+ }
441
+ /**
442
+ * Get quest status.
443
+ */
444
+ async get(questId) {
445
+ const result = await this.connection.request("GET", `/quests/${questId}`);
446
+ return result.quest;
447
+ }
448
+ /**
449
+ * Update quest status, context, or result.
450
+ */
451
+ async update(questId, updates) {
452
+ const result = await this.connection.request(
453
+ "PATCH",
454
+ `/quests/${questId}`,
455
+ updates
456
+ );
457
+ return result.quest;
458
+ }
459
+ /**
460
+ * Update a specific quest step.
461
+ */
462
+ async updateStep(questId, stepId, updates) {
463
+ const result = await this.connection.request(
464
+ "PATCH",
465
+ `/quests/${questId}/steps/${stepId}`,
466
+ updates
467
+ );
468
+ return result.quest;
469
+ }
470
+ /**
471
+ * Cancel a quest.
472
+ */
473
+ async cancel(questId) {
474
+ return this.update(questId, { status: "cancelled" });
475
+ }
476
+ /**
477
+ * List quests for a session.
478
+ */
479
+ async list(sessionId) {
480
+ const result = await this.connection.request(
481
+ "GET",
482
+ `/quests/session/${sessionId}`
483
+ );
484
+ return result.quests;
485
+ }
486
+ /**
487
+ * Stream quest updates with optional resumption.
488
+ */
489
+ async *stream(questId, afterSeq) {
490
+ const path = afterSeq !== void 0 ? `/quests/${questId}/stream?afterSeq=${afterSeq}` : `/quests/${questId}/stream`;
491
+ const response = await this.connection.streamRequest("GET", path);
492
+ for await (const event of parseNdjsonStream(response)) {
493
+ yield event;
494
+ }
495
+ }
496
+ };
497
+
498
+ // src/analytics.ts
499
+ var FLUSH_INTERVAL_MS2 = 3e4;
500
+ var AnalyticsClient = class {
501
+ connection;
502
+ sessionId = null;
503
+ pending = [];
504
+ flushTimer = null;
505
+ constructor(connection) {
506
+ this.connection = connection;
507
+ }
508
+ /** Start batching for a session */
509
+ start(sessionId) {
510
+ this.sessionId = sessionId;
511
+ this.flushTimer = setInterval(() => {
512
+ this.flush().catch(() => {
513
+ });
514
+ }, FLUSH_INTERVAL_MS2);
515
+ }
516
+ /** Stop batching and flush remaining events */
517
+ async stop() {
518
+ if (this.flushTimer) {
519
+ clearInterval(this.flushTimer);
520
+ this.flushTimer = null;
521
+ }
522
+ await this.flush();
523
+ }
524
+ /** Track an event */
525
+ track(type, data) {
526
+ this.pending.push({
527
+ type,
528
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
529
+ data
530
+ });
531
+ }
532
+ /** Flush pending events */
533
+ async flush() {
534
+ if (!this.sessionId || this.pending.length === 0) return;
535
+ const events = [...this.pending];
536
+ this.pending = [];
537
+ try {
538
+ await this.connection.request("POST", "/feedback/analytics/events", {
539
+ sessionId: this.sessionId,
540
+ events
541
+ });
542
+ } catch {
543
+ this.pending = [...events, ...this.pending];
544
+ }
545
+ }
546
+ };
547
+
548
+ // src/app.ts
549
+ var AppClient = class {
550
+ connection;
551
+ constructor(connection) {
552
+ this.connection = connection;
553
+ }
554
+ // ─── Triggers ──────────────────────────────────────────────────────────────
555
+ /**
556
+ * Register triggers (full sync — replaces all existing triggers).
557
+ */
558
+ async registerTriggers(request) {
559
+ return this.connection.request("POST", "/apps/triggers", request);
560
+ }
561
+ /**
562
+ * List all registered triggers.
563
+ */
564
+ async listTriggers() {
565
+ return this.connection.request("GET", "/apps/triggers");
566
+ }
567
+ /**
568
+ * Delete a specific trigger by ID.
569
+ */
570
+ async deleteTrigger(triggerId) {
571
+ return this.connection.request("DELETE", `/apps/triggers/${triggerId}`);
572
+ }
573
+ // ─── Quick Actions ─────────────────────────────────────────────────────────
574
+ /**
575
+ * Register quick action sets (full sync — replaces all existing sets).
576
+ */
577
+ async registerQuickActions(request) {
578
+ return this.connection.request("POST", "/apps/quick-actions", request);
579
+ }
580
+ /**
581
+ * List all registered quick action sets.
582
+ */
583
+ async listQuickActions() {
584
+ return this.connection.request("GET", "/apps/quick-actions");
585
+ }
586
+ /**
587
+ * Delete a specific quick action set by ID.
588
+ */
589
+ async deleteQuickActionSet(setId) {
590
+ return this.connection.request("DELETE", `/apps/quick-actions/${setId}`);
591
+ }
592
+ // ─── Brand ─────────────────────────────────────────────────────────────────
593
+ /**
594
+ * Set or update brand configuration.
595
+ */
596
+ async setBrand(brand) {
597
+ return this.connection.request("PUT", "/apps/brand", brand);
598
+ }
599
+ /**
600
+ * Get brand configuration.
601
+ */
602
+ async getBrand() {
603
+ return this.connection.request("GET", "/apps/brand");
604
+ }
605
+ // ─── Credentials ────────────────────────────────────────────────────────────
606
+ /**
607
+ * Set or update tool credentials (encrypted at rest).
608
+ * Merges with existing credentials — pass only the ones to update.
609
+ */
610
+ async setCredentials(request) {
611
+ return this.connection.request("PUT", "/apps/credentials", request);
612
+ }
613
+ /**
614
+ * List which credential keys are stored (values are never returned).
615
+ */
616
+ async getCredentials() {
617
+ return this.connection.request("GET", "/apps/credentials");
618
+ }
619
+ /**
620
+ * Delete all stored credentials.
621
+ */
622
+ async deleteCredentials() {
623
+ return this.connection.request("DELETE", "/apps/credentials");
624
+ }
625
+ };
626
+
627
+ // src/callback-handler.ts
628
+ var CallbackHandler = class {
629
+ handlers = /* @__PURE__ */ new Map();
630
+ /**
631
+ * Register a handler for a specific tool ID.
632
+ */
633
+ on(toolId, handler) {
634
+ this.handlers.set(toolId, handler);
635
+ return this;
636
+ }
637
+ /**
638
+ * Remove a handler for a specific tool ID.
639
+ */
640
+ off(toolId) {
641
+ this.handlers.delete(toolId);
642
+ return this;
643
+ }
644
+ /**
645
+ * Handle an incoming callback request from Ziggy.
646
+ * Returns a response object suitable for sending back as JSON.
647
+ */
648
+ async handle(request) {
649
+ const { toolId, params, requestId, sessionId } = request;
650
+ const handler = this.handlers.get(toolId);
651
+ if (!handler) {
652
+ return {
653
+ requestId,
654
+ success: false,
655
+ error: `No handler registered for tool: ${toolId}`
656
+ };
657
+ }
658
+ try {
659
+ const result = await handler(params, sessionId);
660
+ return { requestId, success: true, result };
661
+ } catch (error) {
662
+ return {
663
+ requestId,
664
+ success: false,
665
+ error: error instanceof Error ? error.message : "Handler failed"
666
+ };
667
+ }
668
+ }
669
+ /**
670
+ * List all registered tool IDs.
671
+ */
672
+ get registeredTools() {
673
+ return Array.from(this.handlers.keys());
674
+ }
675
+ };
676
+
677
+ // src/index.ts
678
+ var ZiggyClient = class {
679
+ /** Session lifecycle */
680
+ sessions;
681
+ /** Chat (non-streaming and streaming) */
682
+ chat;
683
+ /** Context updates (auto-batched) */
684
+ context;
685
+ /** Tool invocation */
686
+ tools;
687
+ /** Feedback and reactions */
688
+ feedback;
689
+ /** Quest tracking */
690
+ quests;
691
+ /** Analytics (batched) */
692
+ analytics;
693
+ /** App configuration (triggers, quick actions, brand) */
694
+ app;
695
+ /** Callback handler for tool execution requests from Ziggy */
696
+ callbacks;
697
+ connection;
698
+ constructor(config) {
699
+ this.connection = new Connection(config);
700
+ this.sessions = new SessionManager(this.connection, config);
701
+ this.chat = new ChatClient(this.connection, this.sessions);
702
+ this.context = new ContextManager(this.sessions);
703
+ this.tools = new ToolClient(this.connection, this.sessions);
704
+ this.feedback = new FeedbackClient(this.connection);
705
+ this.quests = new QuestClient(this.connection);
706
+ this.analytics = new AnalyticsClient(this.connection);
707
+ this.app = new AppClient(this.connection);
708
+ this.callbacks = new CallbackHandler();
709
+ }
710
+ /**
711
+ * Initialize the client for a user.
712
+ * Creates a session and starts context batching + analytics.
713
+ */
714
+ async connect(request) {
715
+ const session = await this.sessions.create(request);
716
+ this.chat.setUserId(request.userId);
717
+ this.tools.setUserId(request.userId);
718
+ this.context.start(request.userId, request.onTrigger);
719
+ this.analytics.start(session.sessionId);
720
+ return session;
721
+ }
722
+ /**
723
+ * Disconnect — flush analytics, stop batching, end session.
724
+ */
725
+ async disconnect() {
726
+ this.context.stop();
727
+ await this.analytics.stop();
728
+ await this.sessions.destroy();
729
+ }
730
+ };
731
+ export {
732
+ CallbackHandler,
733
+ SequenceTracker,
734
+ ZiggyClient,
735
+ ZiggyConnectionError,
736
+ ZiggyError,
737
+ parseNdjsonStream
738
+ };