@ziggs-ai/api-client 0.9.0 → 0.9.2

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.
Files changed (60) hide show
  1. package/dist/ConnectionManager.d.ts +23 -59
  2. package/dist/ConnectionManager.js +37 -166
  3. package/dist/capabilities/agreements.js +2 -2
  4. package/dist/capabilities/chat.js +13 -3
  5. package/dist/capabilities/connections.js +1 -1
  6. package/dist/capabilities/index.d.ts +1 -1
  7. package/dist/capabilities/index.js +1 -1
  8. package/dist/capabilities/links.d.ts +0 -8
  9. package/dist/capabilities/links.js +3 -25
  10. package/dist/capabilities/payments.js +5 -0
  11. package/dist/capabilities/types.js +3 -0
  12. package/dist/config.d.ts +33 -0
  13. package/dist/config.js +42 -0
  14. package/dist/http/AgentSearchClient.d.ts +0 -1
  15. package/dist/http/AgentSearchClient.js +0 -1
  16. package/dist/http/AgreementClient.d.ts +32 -1
  17. package/dist/http/AgreementClient.js +96 -23
  18. package/dist/http/ArtifactsClient.d.ts +0 -1
  19. package/dist/http/ArtifactsClient.js +0 -1
  20. package/dist/http/ChatClient.d.ts +6 -3
  21. package/dist/http/ChatClient.js +7 -6
  22. package/dist/http/ConnectionsClient.d.ts +0 -1
  23. package/dist/http/ConnectionsClient.js +4 -5
  24. package/dist/http/ContextDiscoveryClient.d.ts +0 -1
  25. package/dist/http/ContextDiscoveryClient.js +2 -2
  26. package/dist/http/ContextGrantsClient.d.ts +0 -1
  27. package/dist/http/ContextGrantsClient.js +0 -1
  28. package/dist/http/ContextReadClient.d.ts +0 -1
  29. package/dist/http/ContextReadClient.js +5 -17
  30. package/dist/http/GrantsClient.d.ts +0 -1
  31. package/dist/http/GrantsClient.js +2 -2
  32. package/dist/http/InboxClient.d.ts +1 -177
  33. package/dist/http/InboxClient.js +0 -40
  34. package/dist/http/MarketplaceClient.d.ts +0 -1
  35. package/dist/http/MarketplaceClient.js +5 -4
  36. package/dist/http/MessagesClient.d.ts +0 -1
  37. package/dist/http/MessagesClient.js +3 -6
  38. package/dist/http/OrgsClient.d.ts +0 -1
  39. package/dist/http/OrgsClient.js +3 -3
  40. package/dist/http/PaymentsClient.d.ts +4 -2
  41. package/dist/http/PaymentsClient.js +27 -7
  42. package/dist/http/TaskClient.d.ts +0 -1
  43. package/dist/http/TaskClient.js +0 -1
  44. package/dist/http/TelemetryClient.d.ts +0 -1
  45. package/dist/http/TelemetryClient.js +0 -1
  46. package/dist/http/agreementFlows.d.ts +13 -5
  47. package/dist/http/agreementFlows.js +18 -24
  48. package/dist/http/index.d.ts +0 -1
  49. package/dist/index.d.ts +5 -3
  50. package/dist/index.js +5 -2
  51. package/dist/shared/apiError.d.ts +22 -1
  52. package/dist/shared/apiError.js +60 -3
  53. package/dist/shared/rateLimit.d.ts +12 -22
  54. package/dist/shared/rateLimit.js +18 -51
  55. package/dist/shared/runtimeLog.d.ts +0 -6
  56. package/dist/shared/runtimeLog.js +10 -7
  57. package/dist/types.d.ts +167 -1
  58. package/dist/types.js +20 -1
  59. package/dist/utils/urlUtils.js +3 -2
  60. package/package.json +1 -2
@@ -1,4 +1,3 @@
1
- import 'dotenv/config';
2
1
  import { type Creds, type Agreement, type EngagementKind, type BroadcastAudience } from '../types.js';
3
2
  /**
4
3
  * Shared proposal terms. When `engagementKind` is omitted the server defaults to `service`.
@@ -46,6 +45,33 @@ export interface ProposeDirectInput extends ProposeTerms {
46
45
  export type ProposeBroadcastInput = Omit<ProposeDirectInput, 'proposedTo'> & {
47
46
  audience?: BroadcastAudience;
48
47
  };
48
+ /**
49
+ * A trust link's agreement, as every caller sees it.
50
+ *
51
+ * A link is reach, not commerce: it carries no money, no escrow, no execution
52
+ * state and no approvals ledger. Handing the raw document over anyway put Mongo
53
+ * bookkeeping in front of an LLM, which is what ZIG-957 forbade.
54
+ *
55
+ * Lives here rather than in `capabilities/links.ts` because this is where the
56
+ * rule is applied (ZIG-1111); that module re-exports it so the public name is
57
+ * unchanged.
58
+ */
59
+ export declare function linkSummary(a: Agreement): Record<string, unknown>;
60
+ /**
61
+ * Every agreement document this client parses passes through here.
62
+ *
63
+ * The rule — a link is returned as its summary, anything else verbatim — used to
64
+ * be written out at six call sites across the SDK runner, the MCP tools and the
65
+ * capability layer, and three verbs never got it: `agreement_counter`,
66
+ * `agreement_fulfill` and `agreement_subcontract` returned the raw document
67
+ * (ZIG-1111). Applying it at the parse boundary means a new verb inherits the
68
+ * rule instead of remembering to opt in, and no surface can word its own verdict.
69
+ *
70
+ * Typed as `Agreement` on the way out: every key the summary keeps IS an
71
+ * Agreement field, so this narrows a document rather than returning a different
72
+ * shape.
73
+ */
74
+ export declare function shapeAgreement(a: Agreement): Agreement;
49
75
  /** @deprecated Alias for {@link ProposeDirectInput}. */
50
76
  export type ProposeAgreementData = ProposeDirectInput;
51
77
  export declare function proposeAgreement(proposalData: ProposeDirectInput, creds: Creds): Promise<Agreement>;
@@ -152,6 +178,7 @@ export interface GetMyAgreementsFilters {
152
178
  partyOnly?: boolean;
153
179
  }
154
180
  export declare function getMyAgreements(filters: GetMyAgreementsFilters | undefined, creds: Creds): Promise<Agreement[]>;
181
+ /** One agreement, shaped: a link comes back as its summary. */
155
182
  export declare function getAgreement(agreementId: string, creds: Creds): Promise<Agreement | null>;
156
183
  export interface CreateAgreementBody {
157
184
  proposedToId?: string;
@@ -181,6 +208,8 @@ export declare function fulfillAgreement(agreementId: string, creds: Creds): Pro
181
208
  ok: boolean;
182
209
  agreement: Agreement;
183
210
  }>;
211
+ /** What an open-broadcast claim turned out to be. ⚠️ SYNC: backend AgreementOpenService. */
212
+ export type ClaimedKind = 'link' | 'offer' | 'quest' | 'hand-off';
184
213
  /**
185
214
  * Claim an open agreement (ZIG-524 phase 2 / ZIG-525):
186
215
  * - open link invite (`engagementKind: link`, proposedTo everyone)
@@ -193,6 +222,7 @@ export declare function fulfillAgreement(agreementId: string, creds: Creds): Pro
193
222
  export declare function claimAgreement(agreementId: string, creds: Creds): Promise<{
194
223
  ok: boolean;
195
224
  agreement: Agreement;
225
+ kind?: ClaimedKind;
196
226
  }>;
197
227
  /**
198
228
  * Link types a caller may ASK for — the server's `CALLER_LINK_TYPES`, not its
@@ -251,6 +281,7 @@ export declare class AgreementClient {
251
281
  claimAgreement(id: string): Promise<{
252
282
  ok: boolean;
253
283
  agreement: Agreement;
284
+ kind?: ClaimedKind;
254
285
  }>;
255
286
  linkToChat(id: string, chatId: string, linkType?: ChatLinkType): Promise<unknown>;
256
287
  listChats(id: string): Promise<unknown[]>;
@@ -1,11 +1,10 @@
1
- import 'dotenv/config';
2
1
  import { runtimeLog } from '../shared/runtimeLog.js';
3
2
  import { getBackendUrl } from '../utils/urlUtils.js';
4
3
  import { OPEN_AGREEMENT_TARGET, ORG_AGREEMENT_TARGET, isBroadcastTarget } from '../types.js';
5
4
  import { throwApiError } from '../shared/apiError.js';
6
- // Lazy: read at call time so dotenv loaded after this module is imported
7
- // still takes effect. Baking it at module-load time would freeze the URL
8
- // before the caller's dotenv.config() runs.
5
+ // Lazy: read at call time so a `configureApiClient` call that lands after this
6
+ // module is imported still takes effect. Baking it at module-load time would
7
+ // freeze the URL before the host has configured one.
9
8
  function getAgreementBaseUrl() {
10
9
  return `${getBackendUrl()}/agreements`;
11
10
  }
@@ -25,6 +24,54 @@ function assertCreds(creds, op) {
25
24
  if (!creds?.agentId)
26
25
  throw new Error(`agentId is required for ${op}`);
27
26
  }
27
+ /**
28
+ * A trust link's agreement, as every caller sees it.
29
+ *
30
+ * A link is reach, not commerce: it carries no money, no escrow, no execution
31
+ * state and no approvals ledger. Handing the raw document over anyway put Mongo
32
+ * bookkeeping in front of an LLM, which is what ZIG-957 forbade.
33
+ *
34
+ * Lives here rather than in `capabilities/links.ts` because this is where the
35
+ * rule is applied (ZIG-1111); that module re-exports it so the public name is
36
+ * unchanged.
37
+ */
38
+ export function linkSummary(a) {
39
+ return {
40
+ agreementId: a.agreementId,
41
+ // Kept deliberately: callers branch on this, and a summary that hides what
42
+ // kind of thing it describes breaks the code it is meant to protect.
43
+ engagementKind: a.engagementKind,
44
+ status: a.status,
45
+ proposalStatus: a.proposalStatus,
46
+ parties: {
47
+ creatorAgent: a.parties?.creatorAgent ?? null,
48
+ providerAgent: a.parties?.providerAgent ?? null,
49
+ creator: a.parties?.creator ?? null,
50
+ proposedTo: a.parties?.proposedTo ?? null,
51
+ },
52
+ ...(a.description ? { description: a.description } : {}),
53
+ // Seat bookkeeping on an open invite — link state, not commerce.
54
+ ...(a.linkInvite ? { linkInvite: a.linkInvite } : {}),
55
+ createdAt: a.createdAt,
56
+ };
57
+ }
58
+ /**
59
+ * Every agreement document this client parses passes through here.
60
+ *
61
+ * The rule — a link is returned as its summary, anything else verbatim — used to
62
+ * be written out at six call sites across the SDK runner, the MCP tools and the
63
+ * capability layer, and three verbs never got it: `agreement_counter`,
64
+ * `agreement_fulfill` and `agreement_subcontract` returned the raw document
65
+ * (ZIG-1111). Applying it at the parse boundary means a new verb inherits the
66
+ * rule instead of remembering to opt in, and no surface can word its own verdict.
67
+ *
68
+ * Typed as `Agreement` on the way out: every key the summary keeps IS an
69
+ * Agreement field, so this narrows a document rather than returning a different
70
+ * shape.
71
+ */
72
+ export function shapeAgreement(a) {
73
+ return a.engagementKind === 'link' ? linkSummary(a) : a;
74
+ }
28
75
  export async function proposeAgreement(proposalData, creds) {
29
76
  if (!proposalData)
30
77
  throw new Error('Proposal data is required for proposal creation');
@@ -48,7 +95,7 @@ export async function proposeAgreement(proposalData, creds) {
48
95
  if (!data?.['agreement']) {
49
96
  throw new Error('Invalid response: expected { agreement } from POST /agreements/proposals');
50
97
  }
51
- return data['agreement'];
98
+ return shapeAgreement(data['agreement']);
52
99
  }
53
100
  /** Propose a contract to one party (user or agent). Server defaults `engagementKind` to `service`. */
54
101
  export async function proposeDirectTo(input, creds) {
@@ -96,7 +143,7 @@ export async function delegateAgreement(proposalData, creds) {
96
143
  if (!data?.['agreement']) {
97
144
  throw new Error('Invalid response: expected { agreement } from POST /agreements/:parentAgreementId/delegations');
98
145
  }
99
- return data['agreement'];
146
+ return shapeAgreement(data['agreement']);
100
147
  }
101
148
  /**
102
149
  * Approve or reject a pending agreement (ZIG-524 canonical client path).
@@ -110,7 +157,10 @@ export async function respondToAgreement(agreementId, action, creds, opts = {})
110
157
  if (!agreementId || !action)
111
158
  throw new Error('Agreement ID and action are required for proposal response');
112
159
  assertCreds(creds, 'proposal response');
113
- const agreement = opts.agreement ?? (await getAgreement(agreementId, creds));
160
+ // Unshaped on purpose: the approvals ledger below is exactly what the link
161
+ // summary drops, and resolving the caller's slot from a summary would fail
162
+ // every link approval with "no pending approval entry".
163
+ const agreement = opts.agreement ?? (await getAgreementDocument(agreementId, creds));
114
164
  if (!agreement) {
115
165
  throw new Error(`Agreement ${agreementId} not found`);
116
166
  }
@@ -213,7 +263,7 @@ export async function approveAgreementAsParty(agreementId, partyId, status, cred
213
263
  if (!data?.['agreement']) {
214
264
  throw new Error('Invalid response: expected { agreement } from PUT /agreements/:id/approvals/:partyId');
215
265
  }
216
- return data['agreement'];
266
+ return shapeAgreement(data['agreement']);
217
267
  }
218
268
  export async function counterAgreement(agreementId, counter, creds) {
219
269
  if (!agreementId)
@@ -232,7 +282,7 @@ export async function counterAgreement(agreementId, counter, creds) {
232
282
  if (!data?.['agreement']) {
233
283
  throw new Error('Invalid response: expected { agreement } from /agreements/:id/counter');
234
284
  }
235
- return data['agreement'];
285
+ return shapeAgreement(data['agreement']);
236
286
  }
237
287
  export async function getAgreementStatus(agreementId, creds) {
238
288
  if (!agreementId)
@@ -261,8 +311,7 @@ export async function listAgreements(filters = {}, creds) {
261
311
  assertCreds(creds, 'list agreements');
262
312
  // ZIG-699 — return an empty array only for a genuine empty 200 result. Any
263
313
  // failure (non-2xx / network) throws so the MCP tool reports a real error
264
- // instead of "you have no agreements". The status stays whitespace-delimited
265
- // so the tool's toolError/classifyToolError maps it to a stable code.
314
+ // instead of "you have no agreements". ZIG-1124 — HTTP failures are ApiError.
266
315
  // Canonical query path: GET /agreements?scope=&status=&engagementKind=&...
267
316
  const url = new URL(getAgreementBaseUrl());
268
317
  if (filters.status)
@@ -284,10 +333,12 @@ export async function listAgreements(filters = {}, creds) {
284
333
  if (!res.ok) {
285
334
  const body = await res.text().catch(() => '');
286
335
  runtimeLog.warn('AgreementClient', `⚠️ List agreements failed: ${res.status} ${res.statusText} ${body?.slice(0, 200)}`);
287
- throw new Error(`GET /agreements ${res.status} ${body?.slice(0, 200)}`);
336
+ throwApiError(res, body, `GET /agreements failed: ${res.status}`);
288
337
  }
289
338
  const data = await res.json().catch(() => null);
290
- return Array.isArray(data?.['agreements']) ? data['agreements'] : [];
339
+ return Array.isArray(data?.['agreements'])
340
+ ? data['agreements'].map(shapeAgreement)
341
+ : [];
291
342
  }
292
343
  export async function getMyAgreements(filters = {}, creds) {
293
344
  assertCreds(creds, 'get my agreements');
@@ -316,20 +367,29 @@ export async function getMyAgreements(filters = {}, creds) {
316
367
  if (!res.ok) {
317
368
  const body = await res.text().catch(() => '');
318
369
  runtimeLog.warn('AgreementClient', `⚠️ Get my agreements failed: ${res.status} ${res.statusText} ${body?.slice(0, 200)}`);
319
- throw new Error(`GET /agreements?scope=mine ${res.status} ${body?.slice(0, 200)}`);
370
+ throwApiError(res, body, `GET /agreements?scope=mine failed: ${res.status}`);
320
371
  }
321
372
  const data = await res.json().catch(() => null);
322
- return Array.isArray(data?.['agreements']) ? data['agreements'] : [];
373
+ return Array.isArray(data?.['agreements'])
374
+ ? data['agreements'].map(shapeAgreement)
375
+ : [];
323
376
  }
324
- export async function getAgreement(agreementId, creds) {
377
+ /**
378
+ * The full agreement document, unshaped — for this client's OWN reads.
379
+ *
380
+ * `respondToAgreement` resolves which approvals slot the caller may fill, and
381
+ * `claimOpenAgreement` routes on the broadcast's open side; both need fields the
382
+ * link summary deliberately drops. Callers outside this module get the shaped
383
+ * `getAgreement`.
384
+ */
385
+ async function getAgreementDocument(agreementId, creds) {
325
386
  if (!agreementId)
326
387
  return null;
327
388
  assertCreds(creds, 'get agreement');
328
389
  // ZIG-698 — a genuine 404 (and 200-with-no-agreement) is a real "not found"
329
390
  // and returns null. Every other failure (403/5xx/network) must throw so the
330
391
  // caller can tell "does not exist" from "could not fetch" instead of a 403
331
- // masquerading as not-found. The MCP tool classifies the thrown status via
332
- // toolError; the message keeps the status whitespace-delimited for that.
392
+ // masquerading as not-found. ZIG-1124 — thrown as ApiError for toolError.
333
393
  let res;
334
394
  try {
335
395
  res = await fetch(`${getAgreementBaseUrl()}/${agreementId}`, {
@@ -346,11 +406,16 @@ export async function getAgreement(agreementId, creds) {
346
406
  if (!res.ok) {
347
407
  const body = await res.text().catch(() => '');
348
408
  runtimeLog.warn('AgreementClient', `⚠️ Get agreement failed: ${res.status} ${res.statusText} ${body?.slice(0, 200)}`);
349
- throw new Error(`GET /agreements/${agreementId} ${res.status} ${body?.slice(0, 200)}`);
409
+ throwApiError(res, body, `GET /agreements/${agreementId} failed: ${res.status}`);
350
410
  }
351
411
  const data = await res.json().catch(() => null);
352
412
  return data?.['agreement'] ?? null;
353
413
  }
414
+ /** One agreement, shaped: a link comes back as its summary. */
415
+ export async function getAgreement(agreementId, creds) {
416
+ const agreement = await getAgreementDocument(agreementId, creds);
417
+ return agreement ? shapeAgreement(agreement) : null;
418
+ }
354
419
  export async function createAgreement(body, creds) {
355
420
  if (!body)
356
421
  throw new Error('Body is required for agreement creation');
@@ -369,7 +434,8 @@ export async function createAgreement(body, creds) {
369
434
  if (!data?.['agreement']) {
370
435
  throw new Error('Invalid response: expected { ok, agreement } from POST /agreements');
371
436
  }
372
- return data;
437
+ const envelope = data;
438
+ return { ...envelope, agreement: shapeAgreement(envelope.agreement) };
373
439
  }
374
440
  export async function revokeAgreement(agreementId, creds) {
375
441
  if (!agreementId)
@@ -389,7 +455,8 @@ export async function revokeAgreement(agreementId, creds) {
389
455
  if (!data?.['agreement']) {
390
456
  throw new Error('Invalid response: expected { ok, agreement } from DELETE /agreements/:id');
391
457
  }
392
- return data;
458
+ const envelope = data;
459
+ return { ...envelope, agreement: shapeAgreement(envelope.agreement) };
393
460
  }
394
461
  /** ZIG-832: mark an agreement fulfilled (complete). A provider closing its own
395
462
  * delivered work — party-gated server-side. */
@@ -409,7 +476,8 @@ export async function fulfillAgreement(agreementId, creds) {
409
476
  if (!data?.['agreement']) {
410
477
  throw new Error('Invalid response: expected { ok, agreement } from POST /agreements/:id/fulfill');
411
478
  }
412
- return data;
479
+ const envelope = data;
480
+ return { ...envelope, agreement: shapeAgreement(envelope.agreement) };
413
481
  }
414
482
  /**
415
483
  * Claim an open agreement (ZIG-524 phase 2 / ZIG-525):
@@ -433,7 +501,12 @@ export async function claimAgreement(agreementId, creds) {
433
501
  if (!data?.['agreement']) {
434
502
  throw new Error('Invalid response: expected { ok, agreement } from POST /agreements/:id/claim');
435
503
  }
436
- return data;
504
+ // ZIG-1155: the route reports which kind of broadcast this turned out to be.
505
+ // A caller cannot work it out from the row — post-claim no sentinel is left,
506
+ // and a quest (you do the work) reads the same shape as a standing offer (you
507
+ // pay for it) unless you know which slot you landed in.
508
+ const envelope = data;
509
+ return { ...envelope, agreement: shapeAgreement(envelope.agreement) };
437
510
  }
438
511
  // ---------------------------------------------------------------------------
439
512
  // Chat links
@@ -1,4 +1,3 @@
1
- import 'dotenv/config';
2
1
  export type ArtifactVisibility = 'chat' | 'agent-private';
3
2
  export interface ListArtifactsOptions {
4
3
  after?: string;
@@ -1,4 +1,3 @@
1
- import 'dotenv/config';
2
1
  import { runtimeLog } from '../shared/runtimeLog.js';
3
2
  import { throwApiError } from '../shared/apiError.js';
4
3
  import { getBackendUrl } from '../utils/urlUtils.js';
@@ -1,4 +1,3 @@
1
- import 'dotenv/config';
2
1
  import { type Creds } from '../types.js';
3
2
  /** Shape of GET /chats/mine items — the backend's ChatReadDto (ZIG-665). */
4
3
  export interface ChatSummary {
@@ -10,7 +9,9 @@ export interface ChatSummary {
10
9
  orgId: string | null;
11
10
  isOrgChat: boolean;
12
11
  }
13
- export declare function openConversation(participantId: string, creds: Creds): Promise<{
12
+ export declare function openConversation(participantId: string, creds: Creds, { newChat }?: {
13
+ newChat?: boolean;
14
+ }): Promise<{
14
15
  chatId: string;
15
16
  }>;
16
17
  export interface SendChatMessageInput {
@@ -75,7 +76,9 @@ export declare class ChatClient {
75
76
  * @param agentId Required for fleet keys (sent as X-Agent-Id). Omit for agent-scoped keys.
76
77
  */
77
78
  constructor(operatorKey: string, agentId?: string);
78
- open(participantId: string): Promise<{
79
+ open(participantId: string, opts?: {
80
+ newChat?: boolean;
81
+ }): Promise<{
79
82
  chatId: string;
80
83
  }>;
81
84
  addMember(input: AddChatMemberInput): Promise<AddChatMemberResult>;
@@ -1,4 +1,3 @@
1
- import 'dotenv/config';
2
1
  import { runtimeLog } from '../shared/runtimeLog.js';
3
2
  import { getBackendUrl } from '../utils/urlUtils.js';
4
3
  import { throwApiError } from '../shared/apiError.js';
@@ -18,14 +17,16 @@ function assertCreds(creds, op) {
18
17
  if (!creds?.agentId)
19
18
  throw new Error(`agentId is required for ${op}`);
20
19
  }
21
- export async function openConversation(participantId, creds) {
20
+ export async function openConversation(participantId, creds, { newChat = false } = {}) {
22
21
  if (!participantId)
23
22
  throw new Error('participantId is required for openConversation');
24
23
  assertCreds(creds, 'open conversation');
25
24
  const res = await fetch(`${getBackendUrl()}/chats`, {
26
25
  method: 'POST',
27
26
  headers: buildHeaders(creds),
28
- body: JSON.stringify({ participantId }),
27
+ // Only sent when asked for: an older backend ignores the field, so a
28
+ // caller that never wants a separate room behaves identically either way.
29
+ body: JSON.stringify({ participantId, ...(newChat ? { newChat: true } : {}) }),
29
30
  });
30
31
  if (!res.ok) {
31
32
  const body = await res.text().catch(() => '');
@@ -104,7 +105,7 @@ export async function listMyChats(creds) {
104
105
  assertCreds(creds, 'list my chats');
105
106
  // ZIG-699 — empty array only for a genuine empty 200; any failure (non-2xx /
106
107
  // network) throws so ziggs_chat_list reports a real error instead of "you
107
- // have no chats". Status stays whitespace-delimited for toolError classification.
108
+ // have no chats". ZIG-1124 — HTTP failures are ApiError (status/body/code).
108
109
  let res;
109
110
  try {
110
111
  res = await fetch(`${getBackendUrl()}/chats/mine`, {
@@ -119,7 +120,7 @@ export async function listMyChats(creds) {
119
120
  if (!res.ok) {
120
121
  const body = await res.text().catch(() => '');
121
122
  runtimeLog.warn('ChatClient', `⚠️ listMyChats failed: ${res.status} ${res.statusText} ${body?.slice(0, 200)}`);
122
- throw new Error(`GET /chats/mine ${res.status} ${body?.slice(0, 200)}`);
123
+ throwApiError(res, body, `GET /chats/mine failed: ${res.status}`);
123
124
  }
124
125
  const data = await res.json().catch(() => null);
125
126
  return Array.isArray(data?.['chats']) ? data['chats'] : [];
@@ -137,7 +138,7 @@ export class ChatClient {
137
138
  // standalone functions still assert it per call.
138
139
  this.creds = { operatorKey, agentId };
139
140
  }
140
- open(participantId) { return openConversation(participantId, this.creds); }
141
+ open(participantId, opts) { return openConversation(participantId, this.creds, opts); }
141
142
  addMember(input) { return addChatMember(input, this.creds); }
142
143
  sendMessage(input) { return sendChatMessage(input, this.creds); }
143
144
  listMine() { return listMyChats(this.creds); }
@@ -1,4 +1,3 @@
1
- import 'dotenv/config';
2
1
  import type { GrantView } from './grants.js';
3
2
  export declare function assertNoLeakedConnectionSecret(serialized: string): void;
4
3
  /** Thrown by ConnectionsClient with the HTTP status and raw body attached. */
@@ -1,5 +1,5 @@
1
- import 'dotenv/config';
2
1
  import { getBackendUrl } from '../utils/urlUtils.js';
2
+ import { throwApiError } from '../shared/apiError.js';
3
3
  import { buildOperatorHeaders } from './operatorHeaders.js';
4
4
  import { GrantsClient } from './GrantsClient.js';
5
5
  // ZIG-569 — defense-in-depth mirror of the backend leak-guard
@@ -169,10 +169,9 @@ export class ConnectionsClient {
169
169
  const response = await fetch(`${this.baseUrl}${path}`, init);
170
170
  const text = await response.text().catch(() => '');
171
171
  if (!response.ok) {
172
- const err = new Error(`${method} ${path} ${response.status} ${text.slice(0, 200)}`);
173
- err.status = response.status;
174
- err.body = text;
175
- throw err;
172
+ // ZIG-1124 — ApiError (status/body/code); ConnectionsError remains the
173
+ // documented duck type for callers that branch on `.status`.
174
+ throwApiError(response, text, `${method} ${path} failed: ${response.status}`);
176
175
  }
177
176
  return text;
178
177
  }
@@ -1,4 +1,3 @@
1
- import 'dotenv/config';
2
1
  /**
3
2
  * Labels-only pointer to context the agent could request access to (P4).
4
3
  * ZIG-927: covers chats, agreements, and connections; connection labels are the
@@ -1,5 +1,5 @@
1
- import 'dotenv/config';
2
1
  import { getBackendUrl } from '../utils/urlUtils.js';
2
+ import { throwApiError } from '../shared/apiError.js';
3
3
  import { buildOperatorHeaders } from './operatorHeaders.js';
4
4
  /**
5
5
  * P4 discovery — labels-only pointers to context the agent could REQUEST but
@@ -34,7 +34,7 @@ export class ContextDiscoveryClient {
34
34
  });
35
35
  const body = await res.text().catch(() => '');
36
36
  if (!res.ok) {
37
- throw new Error(`ContextDiscoveryClient.discoverGrantable ${res.status} ${body.slice(0, 200)}`);
37
+ throwApiError(res, body, `ContextDiscoveryClient.discoverGrantable failed: ${res.status}`);
38
38
  }
39
39
  const parsed = JSON.parse(body);
40
40
  return (parsed.items ?? []).map((i) => ({
@@ -1,4 +1,3 @@
1
- import 'dotenv/config';
2
1
  import type { GrantView } from './grants.js';
3
2
  /**
4
3
  * ZIG-1037 added `artifact` — the narrowest context scope: one specific artifact,
@@ -1,4 +1,3 @@
1
- import 'dotenv/config';
2
1
  import { getBackendUrl } from '../utils/urlUtils.js';
3
2
  import { buildOperatorHeaders } from './operatorHeaders.js';
4
3
  import { throwApiError } from '../shared/apiError.js';
@@ -1,4 +1,3 @@
1
- import 'dotenv/config';
2
1
  export declare const CONTEXT_READ_TYPES: readonly ["messages", "artifacts", "agreements", "tasks"];
3
2
  export type ContextReadType = (typeof CONTEXT_READ_TYPES)[number];
4
3
  /**
@@ -1,4 +1,3 @@
1
- import 'dotenv/config';
2
1
  import { getBackendUrl } from '../utils/urlUtils.js';
3
2
  import { pollSurfaceError } from '../shared/rateLimit.js';
4
3
  export const CONTEXT_READ_TYPES = [
@@ -105,18 +104,11 @@ export class ContextReadClient {
105
104
  const res = await fetch(url.toString(), { headers });
106
105
  const body = await res.text().catch(() => '');
107
106
  if (!res.ok) {
108
- // Carry the status like `snapshot()` does, so callers can branch on it.
109
107
  // A 403 here is a legitimate outcome, not a transport failure: addressing
110
108
  // and authorisation are separate, so an agent can be told about mail it
111
- // is not (or is no longer) allowed to open.
112
- // ZIG-1019: a 429 carries the server's own wait; everything else keeps
113
- // the plain status-tagged error callers already branch on.
114
- if (res.status === 429) {
115
- throw pollSurfaceError(`ContextReadClient.read ${type}`, res, body);
116
- }
117
- const err = new Error(`ContextReadClient.read ${type} ${res.status} ${body.slice(0, 200)}`);
118
- err.status = res.status;
119
- throw err;
109
+ // is not (or is no longer) allowed to open. ZIG-1124 — one ApiError shape
110
+ // (429 → RateLimitedError with Retry-After).
111
+ throw pollSurfaceError(`ContextReadClient.read ${type}`, res, body);
120
112
  }
121
113
  return JSON.parse(body);
122
114
  }
@@ -151,12 +143,8 @@ export class ContextReadClient {
151
143
  const res = await fetch(url.toString(), { headers });
152
144
  const body = await res.text().catch(() => '');
153
145
  if (!res.ok) {
154
- if (res.status === 429) {
155
- throw pollSurfaceError('ContextReadClient.snapshot', res, body);
156
- }
157
- const err = new Error(`ContextReadClient.snapshot ${res.status} ${body.slice(0, 200)}`);
158
- err.status = res.status;
159
- throw err;
146
+ // ZIG-1124 — ApiError for every non-OK (429 → RateLimitedError).
147
+ throw pollSurfaceError('ContextReadClient.snapshot', res, body);
160
148
  }
161
149
  return JSON.parse(body);
162
150
  }
@@ -1,4 +1,3 @@
1
- import 'dotenv/config';
2
1
  import type { GrantView, GrantScopeKind, GrantHealth } from './grants.js';
3
2
  export interface ListGrantsQuery {
4
3
  /**
@@ -1,5 +1,5 @@
1
- import 'dotenv/config';
2
1
  import { getBackendUrl } from '../utils/urlUtils.js';
2
+ import { throwApiError } from '../shared/apiError.js';
3
3
  import { buildOperatorHeaders } from './operatorHeaders.js';
4
4
  /**
5
5
  * ZIG-648 — unified grant listing across every rail. `GET /grants` returns the
@@ -41,7 +41,7 @@ export class GrantsClient {
41
41
  });
42
42
  const body = await res.text().catch(() => '');
43
43
  if (!res.ok) {
44
- throw new Error(`GrantsClient.listGrants ${res.status} ${body.slice(0, 200)}`);
44
+ throwApiError(res, body, `GrantsClient.listGrants failed: ${res.status}`);
45
45
  }
46
46
  const parsed = JSON.parse(body);
47
47
  return {