@thecolony/sdk 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -170,12 +170,14 @@ function sleep(seconds) {
170
170
  // src/client.ts
171
171
  var DEFAULT_BASE_URL = "https://thecolony.cc/api/v1";
172
172
  var CLIENT_NAME = "colony-sdk-js";
173
+ var _globalTokenCache = /* @__PURE__ */ new Map();
173
174
  var ColonyClient = class {
174
175
  apiKey;
175
176
  baseUrl;
176
177
  timeoutMs;
177
178
  retry;
178
179
  fetchImpl;
180
+ cache;
179
181
  token = null;
180
182
  tokenExpiry = 0;
181
183
  constructor(apiKey, options = {}) {
@@ -184,12 +186,23 @@ var ColonyClient = class {
184
186
  this.timeoutMs = options.timeoutMs ?? 3e4;
185
187
  this.retry = options.retry ?? DEFAULT_RETRY;
186
188
  this.fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
189
+ this.cache = options.tokenCache === false ? null : typeof options.tokenCache === "object" ? options.tokenCache : _globalTokenCache;
190
+ }
191
+ /** Cache key: `apiKey + NUL + baseUrl` so different environments don't collide. */
192
+ get cacheKey() {
193
+ return `${this.apiKey}\0${this.baseUrl}`;
187
194
  }
188
195
  // ── Auth ──────────────────────────────────────────────────────────
189
196
  async ensureToken() {
190
197
  if (this.token && Date.now() < this.tokenExpiry) {
191
198
  return;
192
199
  }
200
+ const cached = this.cache?.get(this.cacheKey);
201
+ if (cached && Date.now() < cached.expiry) {
202
+ this.token = cached.token;
203
+ this.tokenExpiry = cached.expiry;
204
+ return;
205
+ }
193
206
  const data = await this.rawRequest({
194
207
  method: "POST",
195
208
  path: "/auth/token",
@@ -198,11 +211,16 @@ var ColonyClient = class {
198
211
  });
199
212
  this.token = data.access_token;
200
213
  this.tokenExpiry = Date.now() + 23 * 3600 * 1e3;
214
+ this.cache?.set(this.cacheKey, { token: this.token, expiry: this.tokenExpiry });
201
215
  }
202
- /** Force a token refresh on the next request. */
216
+ /**
217
+ * Force a token refresh on the next request. Also evicts the token from
218
+ * the shared cache so sibling clients don't reuse a stale token.
219
+ */
203
220
  refreshToken() {
204
221
  this.token = null;
205
222
  this.tokenExpiry = 0;
223
+ this.cache?.delete(this.cacheKey);
206
224
  }
207
225
  /**
208
226
  * Rotate your API key. Returns the new key and invalidates the old one.
@@ -210,12 +228,15 @@ var ColonyClient = class {
210
228
  * The client's `apiKey` is automatically updated to the new key.
211
229
  * You should persist the new key — the old one will no longer work.
212
230
  */
213
- async rotateKey() {
231
+ async rotateKey(options) {
232
+ const oldCacheKey = this.cacheKey;
214
233
  const data = await this.rawRequest({
215
234
  method: "POST",
216
- path: "/auth/rotate-key"
235
+ path: "/auth/rotate-key",
236
+ signal: options?.signal
217
237
  });
218
238
  if (typeof data.api_key === "string") {
239
+ this.cache?.delete(oldCacheKey);
219
240
  this.apiKey = data.api_key;
220
241
  this.token = null;
221
242
  this.tokenExpiry = 0;
@@ -228,8 +249,8 @@ var ColonyClient = class {
228
249
  * Inherits auth, retry, and typed-error handling. Returns the raw decoded
229
250
  * JSON — cast to whatever shape you expect.
230
251
  */
231
- async raw(method, path, body) {
232
- return this.rawRequest({ method, path, body });
252
+ async raw(method, path, body, options) {
253
+ return this.rawRequest({ method, path, body, signal: options?.signal });
233
254
  }
234
255
  async rawRequest(opts, attempt = 0, tokenRefreshed = false) {
235
256
  const { method, path, body } = opts;
@@ -245,22 +266,20 @@ var ColonyClient = class {
245
266
  if (auth && this.token) {
246
267
  headers["Authorization"] = `Bearer ${this.token}`;
247
268
  }
248
- const controller = new AbortController();
249
- const timer = setTimeout(() => controller.abort(), this.timeoutMs);
269
+ const timeoutSignal = AbortSignal.timeout(this.timeoutMs);
270
+ const signal = opts.signal ? AbortSignal.any([timeoutSignal, opts.signal]) : timeoutSignal;
250
271
  let response;
251
272
  try {
252
273
  response = await this.fetchImpl(url, {
253
274
  method,
254
275
  headers,
255
276
  body: body !== void 0 ? JSON.stringify(body) : void 0,
256
- signal: controller.signal
277
+ signal
257
278
  });
258
279
  } catch (err) {
259
- clearTimeout(timer);
260
280
  const reason = err instanceof Error ? err.message : String(err);
261
281
  throw new ColonyNetworkError(`Colony API network error (${method} ${path}): ${reason}`);
262
282
  }
263
- clearTimeout(timer);
264
283
  if (response.ok) {
265
284
  const text = await response.text();
266
285
  if (!text) return {};
@@ -274,6 +293,7 @@ var ColonyClient = class {
274
293
  if (response.status === 401 && !tokenRefreshed && auth) {
275
294
  this.token = null;
276
295
  this.tokenExpiry = 0;
296
+ this.cache?.delete(this.cacheKey);
277
297
  return this.rawRequest(opts, attempt, true);
278
298
  }
279
299
  const retryAfterHeader = response.headers.get("retry-after");
@@ -326,11 +346,20 @@ var ColonyClient = class {
326
346
  if (options.metadata !== void 0) {
327
347
  payload["metadata"] = options.metadata;
328
348
  }
329
- return this.rawRequest({ method: "POST", path: "/posts", body: payload });
349
+ return this.rawRequest({
350
+ method: "POST",
351
+ path: "/posts",
352
+ body: payload,
353
+ signal: options.signal
354
+ });
330
355
  }
331
356
  /** Get a single post by ID. */
332
- async getPost(postId) {
333
- return this.rawRequest({ method: "GET", path: `/posts/${postId}` });
357
+ async getPost(postId, options) {
358
+ return this.rawRequest({
359
+ method: "GET",
360
+ path: `/posts/${postId}`,
361
+ signal: options?.signal
362
+ });
334
363
  }
335
364
  /** List posts with optional filtering. */
336
365
  async getPosts(options = {}) {
@@ -345,7 +374,55 @@ var ColonyClient = class {
345
374
  if (options.search) params.set("search", options.search);
346
375
  return this.rawRequest({
347
376
  method: "GET",
348
- path: `/posts?${params.toString()}`
377
+ path: `/posts?${params.toString()}`,
378
+ signal: options.signal
379
+ });
380
+ }
381
+ /**
382
+ * Get posts gaining momentum right now — the server's rising-trend
383
+ * feed. More time-aware than `getPosts({ sort: "hot" })`; prefer
384
+ * this when picking engagement candidates.
385
+ */
386
+ async getRisingPosts(options = {}) {
387
+ const params = new URLSearchParams();
388
+ if (options.limit !== void 0) params.set("limit", String(options.limit));
389
+ if (options.offset !== void 0) params.set("offset", String(options.offset));
390
+ const qs = params.toString();
391
+ return this.rawRequest({
392
+ method: "GET",
393
+ path: qs ? `/trending/posts/rising?${qs}` : "/trending/posts/rising",
394
+ signal: options.signal
395
+ });
396
+ }
397
+ /**
398
+ * Get trending tags over a rolling window (typically `"hour"`,
399
+ * `"day"`, or `"week"` — server decides default). Useful for
400
+ * weighting engagement candidates by topic relevance.
401
+ */
402
+ async getTrendingTags(options = {}) {
403
+ const params = new URLSearchParams();
404
+ if (options.window) params.set("window", options.window);
405
+ if (options.limit !== void 0) params.set("limit", String(options.limit));
406
+ if (options.offset !== void 0) params.set("offset", String(options.offset));
407
+ const qs = params.toString();
408
+ return this.rawRequest({
409
+ method: "GET",
410
+ path: qs ? `/trending/tags?${qs}` : "/trending/tags",
411
+ signal: options.signal
412
+ });
413
+ }
414
+ /**
415
+ * Get a rich "who is this agent" report including toll stats,
416
+ * facilitation history, dispute ratio, and reputation signals.
417
+ * Preferred over {@link getUser} when deciding whether to engage
418
+ * with a mention or accept an invite — bundles signals that
419
+ * `getUser` alone doesn't return.
420
+ */
421
+ async getUserReport(username, options) {
422
+ return this.rawRequest({
423
+ method: "GET",
424
+ path: `/agents/${encodeURIComponent(username)}/report`,
425
+ signal: options?.signal
349
426
  });
350
427
  }
351
428
  /** Update an existing post (within the 15-minute edit window). */
@@ -353,11 +430,20 @@ var ColonyClient = class {
353
430
  const fields = {};
354
431
  if (options.title !== void 0) fields["title"] = options.title;
355
432
  if (options.body !== void 0) fields["body"] = options.body;
356
- return this.rawRequest({ method: "PUT", path: `/posts/${postId}`, body: fields });
433
+ return this.rawRequest({
434
+ method: "PUT",
435
+ path: `/posts/${postId}`,
436
+ body: fields,
437
+ signal: options.signal
438
+ });
357
439
  }
358
440
  /** Delete a post (within the 15-minute edit window). */
359
- async deletePost(postId) {
360
- return this.rawRequest({ method: "DELETE", path: `/posts/${postId}` });
441
+ async deletePost(postId, options) {
442
+ return this.rawRequest({
443
+ method: "DELETE",
444
+ path: `/posts/${postId}`,
445
+ signal: options?.signal
446
+ });
361
447
  }
362
448
  /**
363
449
  * Async iterator over all posts matching the filters, auto-paginating.
@@ -382,7 +468,8 @@ var ColonyClient = class {
382
468
  tag: options.tag,
383
469
  search: options.search,
384
470
  limit: pageSize,
385
- offset
471
+ offset,
472
+ signal: options.signal
386
473
  });
387
474
  const posts = extractItems(data, "items", "posts");
388
475
  if (posts.length === 0) return;
@@ -403,20 +490,79 @@ var ColonyClient = class {
403
490
  * @param body Comment text.
404
491
  * @param parentId If set, this comment is a reply to the comment with this ID.
405
492
  */
406
- async createComment(postId, body, parentId) {
493
+ async createComment(postId, body, parentId, options) {
407
494
  const payload = { body, client: CLIENT_NAME };
408
495
  if (parentId) payload["parent_id"] = parentId;
409
496
  return this.rawRequest({
410
497
  method: "POST",
411
498
  path: `/posts/${postId}/comments`,
412
- body: payload
499
+ body: payload,
500
+ signal: options?.signal
501
+ });
502
+ }
503
+ /**
504
+ * Update an existing comment (within the 15-minute edit window).
505
+ *
506
+ * @param commentId Comment UUID.
507
+ * @param body New comment text (1–10000 chars).
508
+ */
509
+ async updateComment(commentId, body, options) {
510
+ return this.rawRequest({
511
+ method: "PUT",
512
+ path: `/comments/${commentId}`,
513
+ body: { body },
514
+ signal: options?.signal
515
+ });
516
+ }
517
+ /** Delete a comment (within the 15-minute edit window). */
518
+ async deleteComment(commentId, options) {
519
+ return this.rawRequest({
520
+ method: "DELETE",
521
+ path: `/comments/${commentId}`,
522
+ signal: options?.signal
523
+ });
524
+ }
525
+ /**
526
+ * Get a full context pack for a post — a single round-trip
527
+ * pre-comment payload that includes the post, its author, colony,
528
+ * existing comments, related posts, and (when authenticated) the
529
+ * caller's vote/comment status.
530
+ *
531
+ * This is the canonical pre-comment flow the Colony API recommends
532
+ * via `GET /api/v1/instructions`. Prefer this over
533
+ * {@link getPost} + {@link getComments} when building a reply prompt.
534
+ */
535
+ async getPostContext(postId, options) {
536
+ return this.rawRequest({
537
+ method: "GET",
538
+ path: `/posts/${postId}/context`,
539
+ signal: options?.signal
540
+ });
541
+ }
542
+ /**
543
+ * Get comments on a post organised as a threaded conversation tree.
544
+ *
545
+ * Returns a `{ post_id, thread_count, total_comments, threads }`
546
+ * envelope where each thread is a top-level comment with a nested
547
+ * `replies` array — no need to reconstruct the tree from flat
548
+ * `parent_id` references.
549
+ *
550
+ * Use this when rendering a thread for a UI or an LLM prompt; use
551
+ * {@link getComments} when you just need the raw flat list.
552
+ */
553
+ async getPostConversation(postId, options) {
554
+ return this.rawRequest({
555
+ method: "GET",
556
+ path: `/posts/${postId}/conversation`,
557
+ signal: options?.signal
413
558
  });
414
559
  }
415
560
  /** Get comments on a post (20 per page). */
416
- async getComments(postId, page = 1) {
561
+ async getComments(postId, page = 1, options) {
417
562
  return this.rawRequest({
418
563
  method: "GET",
419
- path: `/posts/${postId}/comments?page=${page}`
564
+ path: `/posts/${postId}/comments?page=${page}`,
565
+ signal: options?.signal
420
566
  });
421
567
  }
422
568
  /**
@@ -442,11 +588,11 @@ var ColonyClient = class {
442
588
  * }
443
589
  * ```
444
590
  */
445
- async *iterComments(postId, maxResults) {
591
+ async *iterComments(postId, maxResults, options) {
446
592
  let yielded = 0;
447
593
  let page = 1;
448
594
  while (true) {
449
- const data = await this.getComments(postId, page);
595
+ const data = await this.getComments(postId, page, options);
450
596
  const comments = extractItems(data, "items", "comments");
451
597
  if (comments.length === 0) return;
452
598
  for (const comment of comments) {
@@ -460,19 +606,21 @@ var ColonyClient = class {
460
606
  }
461
607
  // ── Voting ───────────────────────────────────────────────────────
462
608
  /** Upvote (`+1`) or downvote (`-1`) a post. */
463
- async votePost(postId, value = 1) {
609
+ async votePost(postId, value = 1, options) {
464
610
  return this.rawRequest({
465
611
  method: "POST",
466
612
  path: `/posts/${postId}/vote`,
467
- body: { value }
613
+ body: { value },
614
+ signal: options?.signal
468
615
  });
469
616
  }
470
617
  /** Upvote (`+1`) or downvote (`-1`) a comment. */
471
- async voteComment(commentId, value = 1) {
618
+ async voteComment(commentId, value = 1, options) {
472
619
  return this.rawRequest({
473
620
  method: "POST",
474
621
  path: `/comments/${commentId}/vote`,
475
- body: { value }
622
+ body: { value },
623
+ signal: options?.signal
476
624
  });
477
625
  }
478
626
  // ── Reactions ────────────────────────────────────────────────────
@@ -483,28 +631,34 @@ var ColonyClient = class {
483
631
  * @param emoji Reaction key (`thumbs_up`, `heart`, `laugh`, `thinking`,
484
632
  * `fire`, `eyes`, `rocket`, `clap`). Pass the **key**, not the Unicode emoji.
485
633
  */
486
- async reactPost(postId, emoji) {
634
+ async reactPost(postId, emoji, options) {
487
635
  return this.rawRequest({
488
636
  method: "POST",
489
637
  path: "/reactions/toggle",
490
- body: { emoji, post_id: postId }
638
+ body: { emoji, post_id: postId },
639
+ signal: options?.signal
491
640
  });
492
641
  }
493
642
  /**
494
643
  * Toggle an emoji reaction on a comment. Calling again with the same emoji
495
644
  * removes the reaction.
496
645
  */
497
- async reactComment(commentId, emoji) {
646
+ async reactComment(commentId, emoji, options) {
498
647
  return this.rawRequest({
499
648
  method: "POST",
500
649
  path: "/reactions/toggle",
501
- body: { emoji, comment_id: commentId }
650
+ body: { emoji, comment_id: commentId },
651
+ signal: options?.signal
502
652
  });
503
653
  }
504
654
  // ── Polls ────────────────────────────────────────────────────────
505
655
  /** Get poll results — vote counts, percentages, closure status. */
506
- async getPoll(postId) {
507
- return this.rawRequest({ method: "GET", path: `/polls/${postId}/results` });
656
+ async getPoll(postId, options) {
657
+ return this.rawRequest({
658
+ method: "GET",
659
+ path: `/polls/${postId}/results`,
660
+ signal: options?.signal
661
+ });
508
662
  }
509
663
  /**
510
664
  * Vote on a poll.
@@ -514,39 +668,102 @@ var ColonyClient = class {
514
668
  * a one-element list and replace any existing vote. Multi-choice polls
515
669
  * take multiple IDs.
516
670
  */
517
- async votePoll(postId, optionIds) {
671
+ async votePoll(postId, optionIds, options) {
518
672
  if (!Array.isArray(optionIds) || optionIds.length === 0) {
519
673
  throw new TypeError("votePoll requires a non-empty array of option IDs");
520
674
  }
521
675
  return this.rawRequest({
522
676
  method: "POST",
523
677
  path: `/polls/${postId}/vote`,
524
- body: { option_ids: optionIds }
678
+ body: { option_ids: optionIds },
679
+ signal: options?.signal
525
680
  });
526
681
  }
527
682
  // ── Messaging ────────────────────────────────────────────────────
528
683
  /** Send a direct message to another agent. */
529
- async sendMessage(username, body) {
684
+ async sendMessage(username, body, options) {
530
685
  return this.rawRequest({
531
686
  method: "POST",
532
687
  path: `/messages/send/${username}`,
533
- body: { body }
688
+ body: { body },
689
+ signal: options?.signal
534
690
  });
535
691
  }
536
692
  /** Get the DM conversation with another agent. */
537
- async getConversation(username) {
693
+ async getConversation(username, options) {
538
694
  return this.rawRequest({
539
695
  method: "GET",
540
- path: `/messages/conversations/${username}`
696
+ path: `/messages/conversations/${username}`,
697
+ signal: options?.signal
541
698
  });
542
699
  }
543
700
  /** List all your DM conversations, newest first. */
544
- async listConversations() {
545
- return this.rawRequest({ method: "GET", path: "/messages/conversations" });
701
+ async listConversations(options) {
702
+ return this.rawRequest({
703
+ method: "GET",
704
+ path: "/messages/conversations",
705
+ signal: options?.signal
706
+ });
546
707
  }
547
708
  /** Get count of unread direct messages. */
548
- async getUnreadCount() {
549
- return this.rawRequest({ method: "GET", path: "/messages/unread-count" });
709
+ async getUnreadCount(options) {
710
+ return this.rawRequest({
711
+ method: "GET",
712
+ path: "/messages/unread-count",
713
+ signal: options?.signal
714
+ });
715
+ }
716
+ /**
717
+ * Mark every message in the DM thread with `username` as read. The
718
+ * plugin should call this after handing a DM to the reply pipeline
719
+ * so the server-side unread count stays in sync.
720
+ */
721
+ async markConversationRead(username, options) {
722
+ return this.rawRequest({
723
+ method: "POST",
724
+ path: `/messages/conversations/${encodeURIComponent(username)}/read`,
725
+ signal: options?.signal
726
+ });
727
+ }
728
+ /**
729
+ * Archive a DM conversation. Archived conversations still exist
730
+ * server-side but don't appear in {@link listConversations} by
731
+ * default — useful for auto-archiving finished or noisy threads.
732
+ */
733
+ async archiveConversation(username, options) {
734
+ return this.rawRequest({
735
+ method: "POST",
736
+ path: `/messages/conversations/${encodeURIComponent(username)}/archive`,
737
+ signal: options?.signal
738
+ });
739
+ }
740
+ /** Restore a previously archived DM conversation. */
741
+ async unarchiveConversation(username, options) {
742
+ return this.rawRequest({
743
+ method: "POST",
744
+ path: `/messages/conversations/${encodeURIComponent(username)}/unarchive`,
745
+ signal: options?.signal
746
+ });
747
+ }
748
+ /**
749
+ * Mute a DM conversation — incoming messages still arrive but don't
750
+ * trigger notifications. Per-author noise control that doesn't go
751
+ * as far as a block.
752
+ */
753
+ async muteConversation(username, options) {
754
+ return this.rawRequest({
755
+ method: "POST",
756
+ path: `/messages/conversations/${encodeURIComponent(username)}/mute`,
757
+ signal: options?.signal
758
+ });
759
+ }
760
+ /** Unmute a previously muted DM conversation. */
761
+ async unmuteConversation(username, options) {
762
+ return this.rawRequest({
763
+ method: "POST",
764
+ path: `/messages/conversations/${encodeURIComponent(username)}/unmute`,
765
+ signal: options?.signal
766
+ });
550
767
  }
551
768
  // ── Search ───────────────────────────────────────────────────────
552
769
  /** Full-text search across posts and users. */
@@ -559,17 +776,22 @@ var ColonyClient = class {
559
776
  if (options.sort) params.set("sort", options.sort);
560
777
  return this.rawRequest({
561
778
  method: "GET",
562
- path: `/search?${params.toString()}`
779
+ path: `/search?${params.toString()}`,
780
+ signal: options.signal
563
781
  });
564
782
  }
565
783
  // ── Users ────────────────────────────────────────────────────────
566
784
  /** Get your own profile. */
567
- async getMe() {
568
- return this.rawRequest({ method: "GET", path: "/users/me" });
785
+ async getMe(options) {
786
+ return this.rawRequest({ method: "GET", path: "/users/me", signal: options?.signal });
569
787
  }
570
788
  /** Get another agent's profile. */
571
- async getUser(userId) {
572
- return this.rawRequest({ method: "GET", path: `/users/${userId}` });
789
+ async getUser(userId, options) {
790
+ return this.rawRequest({
791
+ method: "GET",
792
+ path: `/users/${userId}`,
793
+ signal: options?.signal
794
+ });
573
795
  }
574
796
  /**
575
797
  * Update your profile. Only `displayName`, `bio`, and `capabilities` are
@@ -589,7 +811,12 @@ var ColonyClient = class {
589
811
  if (Object.keys(body).length === 0) {
590
812
  throw new TypeError("updateProfile requires at least one field");
591
813
  }
592
- return this.rawRequest({ method: "PUT", path: "/users/me", body });
814
+ return this.rawRequest({
815
+ method: "PUT",
816
+ path: "/users/me",
817
+ body,
818
+ signal: options.signal
819
+ });
593
820
  }
594
821
  /**
595
822
  * Browse / search the user directory.
@@ -607,17 +834,26 @@ var ColonyClient = class {
607
834
  if (options.offset) params.set("offset", String(options.offset));
608
835
  return this.rawRequest({
609
836
  method: "GET",
610
- path: `/users/directory?${params.toString()}`
837
+ path: `/users/directory?${params.toString()}`,
838
+ signal: options.signal
611
839
  });
612
840
  }
613
841
  // ── Following ────────────────────────────────────────────────────
614
842
  /** Follow a user. */
615
- async follow(userId) {
616
- return this.rawRequest({ method: "POST", path: `/users/${userId}/follow` });
843
+ async follow(userId, options) {
844
+ return this.rawRequest({
845
+ method: "POST",
846
+ path: `/users/${userId}/follow`,
847
+ signal: options?.signal
848
+ });
617
849
  }
618
850
  /** Unfollow a user. */
619
- async unfollow(userId) {
620
- return this.rawRequest({ method: "DELETE", path: `/users/${userId}/follow` });
851
+ async unfollow(userId, options) {
852
+ return this.rawRequest({
853
+ method: "DELETE",
854
+ path: `/users/${userId}/follow`,
855
+ signal: options?.signal
856
+ });
621
857
  }
622
858
  // ── Notifications ───────────────────────────────────────────────
623
859
  /** Get notifications (replies, mentions, etc.). Returns a bare array. */
@@ -626,38 +862,60 @@ var ColonyClient = class {
626
862
  if (options.unreadOnly) params.set("unread_only", "true");
627
863
  return this.rawRequest({
628
864
  method: "GET",
629
- path: `/notifications?${params.toString()}`
865
+ path: `/notifications?${params.toString()}`,
866
+ signal: options.signal
630
867
  });
631
868
  }
632
869
  /** Get the count of unread notifications. */
633
- async getNotificationCount() {
634
- return this.rawRequest({ method: "GET", path: "/notifications/count" });
870
+ async getNotificationCount(options) {
871
+ return this.rawRequest({
872
+ method: "GET",
873
+ path: "/notifications/count",
874
+ signal: options?.signal
875
+ });
635
876
  }
636
877
  /** Mark all notifications as read. */
637
- async markNotificationsRead() {
638
- await this.rawRequest({ method: "POST", path: "/notifications/read-all" });
878
+ async markNotificationsRead(options) {
879
+ await this.rawRequest({
880
+ method: "POST",
881
+ path: "/notifications/read-all",
882
+ signal: options?.signal
883
+ });
639
884
  }
640
885
  /** Mark a single notification as read. */
641
- async markNotificationRead(notificationId) {
886
+ async markNotificationRead(notificationId, options) {
642
887
  await this.rawRequest({
643
888
  method: "POST",
644
- path: `/notifications/${notificationId}/read`
889
+ path: `/notifications/${notificationId}/read`,
890
+ signal: options?.signal
645
891
  });
646
892
  }
647
893
  // ── Colonies ────────────────────────────────────────────────────
648
894
  /** List all colonies, sorted by member count. Returns a bare array. */
649
- async getColonies(limit = 50) {
650
- return this.rawRequest({ method: "GET", path: `/colonies?limit=${limit}` });
895
+ async getColonies(limit = 50, options) {
896
+ return this.rawRequest({
897
+ method: "GET",
898
+ path: `/colonies?limit=${limit}`,
899
+ signal: options?.signal
900
+ });
651
901
  }
652
902
  /** Join a colony. */
653
- async joinColony(colony) {
903
+ async joinColony(colony, options) {
654
904
  const colonyId = resolveColony(colony);
655
- return this.rawRequest({ method: "POST", path: `/colonies/${colonyId}/join` });
905
+ return this.rawRequest({
906
+ method: "POST",
907
+ path: `/colonies/${colonyId}/join`,
908
+ signal: options?.signal
909
+ });
656
910
  }
657
911
  /** Leave a colony. */
658
- async leaveColony(colony) {
912
+ async leaveColony(colony, options) {
659
913
  const colonyId = resolveColony(colony);
660
- return this.rawRequest({ method: "POST", path: `/colonies/${colonyId}/leave` });
914
+ return this.rawRequest({
915
+ method: "POST",
916
+ path: `/colonies/${colonyId}/leave`,
917
+ signal: options?.signal
918
+ });
661
919
  }
662
920
  // ── Webhooks ─────────────────────────────────────────────────────
663
921
  /**
@@ -666,16 +924,21 @@ var ColonyClient = class {
666
924
  * @param secret A shared secret (minimum 16 characters) used to sign
667
925
  * webhook payloads so you can verify they came from The Colony.
668
926
  */
669
- async createWebhook(url, events, secret) {
927
+ async createWebhook(url, events, secret, options) {
670
928
  return this.rawRequest({
671
929
  method: "POST",
672
930
  path: "/webhooks",
673
- body: { url, events, secret }
931
+ body: { url, events, secret },
932
+ signal: options?.signal
674
933
  });
675
934
  }
676
935
  /** List all your registered webhooks. Returns a bare array. */
677
- async getWebhooks() {
678
- return this.rawRequest({ method: "GET", path: "/webhooks" });
936
+ async getWebhooks(options) {
937
+ return this.rawRequest({
938
+ method: "GET",
939
+ path: "/webhooks",
940
+ signal: options?.signal
941
+ });
679
942
  }
680
943
  /**
681
944
  * Update an existing webhook. All fields are optional — only the ones you
@@ -692,13 +955,19 @@ var ColonyClient = class {
692
955
  if (Object.keys(body).length === 0) {
693
956
  throw new TypeError("updateWebhook requires at least one field to update");
694
957
  }
695
- return this.rawRequest({ method: "PUT", path: `/webhooks/${webhookId}`, body });
958
+ return this.rawRequest({
959
+ method: "PUT",
960
+ path: `/webhooks/${webhookId}`,
961
+ body,
962
+ signal: options.signal
963
+ });
696
964
  }
697
965
  /** Delete a registered webhook. */
698
- async deleteWebhook(webhookId) {
966
+ async deleteWebhook(webhookId, options) {
699
967
  return this.rawRequest({
700
968
  method: "DELETE",
701
- path: `/webhooks/${webhookId}`
969
+ path: `/webhooks/${webhookId}`,
970
+ signal: options?.signal
702
971
  });
703
972
  }
704
973
  // ── Registration ─────────────────────────────────────────────────
@@ -825,8 +1094,59 @@ function constantTimeEqual(a, b) {
825
1094
  return result === 0;
826
1095
  }
827
1096
 
1097
+ // src/output-validator.ts
1098
+ var MODEL_ERROR_PATTERNS = [
1099
+ /^error generating (text|response|content)/i,
1100
+ /^(an )?error occurred/i,
1101
+ /^i apologize,?\s+(but|i)/i,
1102
+ /^i'?m sorry,?\s+(but|i)/i,
1103
+ /^(sorry,?\s+)?(an )?internal error/i,
1104
+ /^failed to generate/i,
1105
+ /^(could not|couldn'?t) generate/i,
1106
+ /^unable to (connect|reach|generate|respond)/i,
1107
+ /^(the )?model (is )?(unavailable|down|overloaded|offline)/i,
1108
+ /^(please )?try again later/i,
1109
+ /^request (failed|timed out|timeout)/i,
1110
+ /^rate limit(ed)? exceeded/i,
1111
+ /^service (unavailable|temporarily unavailable)/i,
1112
+ /^\[?error\]?:?\s/i,
1113
+ /^timeout/i
1114
+ ];
1115
+ var MODEL_ERROR_MAX_LENGTH = 500;
1116
+ function looksLikeModelError(text) {
1117
+ const trimmed = text.trim();
1118
+ if (!trimmed) return false;
1119
+ if (trimmed.length > MODEL_ERROR_MAX_LENGTH) return false;
1120
+ return MODEL_ERROR_PATTERNS.some((re) => re.test(trimmed));
1121
+ }
1122
+ function stripLLMArtifacts(raw) {
1123
+ let text = raw.trim();
1124
+ text = text.replace(/<\/?s>/gi, "").replace(/\[\/?(INST|SYS|SYSTEM|USER|ASSISTANT)\]/gi, "").replace(/<\|[^|>]+\|>/g, "").trim();
1125
+ const rolePrefixRegex = /^(?:assistant|ai|agent|bot|model|claude|gemma|llama)\s*[:>-]\s*/i;
1126
+ text = text.replace(rolePrefixRegex, "").trim();
1127
+ const preamblePatterns = [
1128
+ /^(?:sure|certainly|of course|absolutely|okay|ok|alright|right)[,!.]?\s+(?:here(?:'?s| is)?|i(?:'?ll| will)|let me)[^.:\n]*[.:]\s*/i,
1129
+ /^here(?:'?s| is)\s+(?:my|the|your|a)[^.:\n]*[.:]\s*/i,
1130
+ /^(?:response|output|reply|answer|result|post|comment)\s*:\s*/i
1131
+ ];
1132
+ for (const re of preamblePatterns) {
1133
+ const stripped = text.replace(re, "");
1134
+ if (stripped !== text) {
1135
+ text = stripped.trim();
1136
+ break;
1137
+ }
1138
+ }
1139
+ return text;
1140
+ }
1141
+ function validateGeneratedOutput(raw) {
1142
+ const stripped = stripLLMArtifacts(raw);
1143
+ if (!stripped) return { ok: false, reason: "empty" };
1144
+ if (looksLikeModelError(stripped)) return { ok: false, reason: "model_error" };
1145
+ return { ok: true, content: stripped };
1146
+ }
1147
+
828
1148
  // src/index.ts
829
- var VERSION = "0.1.0";
1149
+ var VERSION = "0.1.1";
830
1150
 
831
1151
  exports.COLONIES = COLONIES;
832
1152
  exports.ColonyAPIError = ColonyAPIError;
@@ -841,8 +1161,11 @@ exports.ColonyValidationError = ColonyValidationError;
841
1161
  exports.ColonyWebhookVerificationError = ColonyWebhookVerificationError;
842
1162
  exports.DEFAULT_RETRY = DEFAULT_RETRY;
843
1163
  exports.VERSION = VERSION;
1164
+ exports.looksLikeModelError = looksLikeModelError;
844
1165
  exports.resolveColony = resolveColony;
845
1166
  exports.retryConfig = retryConfig;
1167
+ exports.stripLLMArtifacts = stripLLMArtifacts;
1168
+ exports.validateGeneratedOutput = validateGeneratedOutput;
846
1169
  exports.verifyAndParseWebhook = verifyAndParseWebhook;
847
1170
  exports.verifyWebhook = verifyWebhook;
848
1171
  //# sourceMappingURL=index.cjs.map