@ziggs-ai/api-client 0.9.3 → 0.9.5

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.
@@ -28,4 +28,12 @@ export declare const shareArtifactCapability: CapabilityDefinition;
28
28
  * deliverable to a task; use artifact_share to hand it to ONE agent instead.
29
29
  */
30
30
  export declare const attachArtifactCapability: CapabilityDefinition;
31
+ /**
32
+ * File deliverables (presign → PUT → complete). Text `artifact_record` stays
33
+ * for text bodies; use this rail when the payload is a file on object storage.
34
+ */
35
+ export declare const uploadArtifactUrlCapability: CapabilityDefinition;
36
+ export declare const completeArtifactFileCapability: CapabilityDefinition;
37
+ export declare const downloadArtifactCapability: CapabilityDefinition;
38
+ export declare const reextractArtifactCapability: CapabilityDefinition;
31
39
  export declare const ARTIFACT_CAPABILITIES: CapabilityDefinition[];
@@ -269,9 +269,199 @@ export const attachArtifactCapability = {
269
269
  };
270
270
  },
271
271
  };
272
+ /**
273
+ * File deliverables (presign → PUT → complete). Text `artifact_record` stays
274
+ * for text bodies; use this rail when the payload is a file on object storage.
275
+ */
276
+ export const uploadArtifactUrlCapability = {
277
+ key: 'artifact_upload_url',
278
+ names: { sdk: 'artifact_upload_url', mcp: 'ziggs_artifact_upload_url' },
279
+ descriptions: {
280
+ sdk: 'Start a file artifact upload. Returns a short-lived uploadUrl. PUT the exact ' +
281
+ 'byteSize bytes to that URL (Content-Type = mime), then call artifact_complete_file ' +
282
+ 'with the sha256 hex of those bytes. Pass contentBase64 (or have the runtime supply ' +
283
+ 'content) to have this tool PUT for you and return checksum. When extractionStatus ' +
284
+ 'becomes ok, read extracted text via context_read / the extracted-text child — not ' +
285
+ 'by stuffing file bytes into artifact_record.',
286
+ mcp: 'Start a file artifact upload (pdf/docx/hwpx/md/txt). Returns uploadUrl + artifactId. ' +
287
+ 'PUT the file bytes to uploadUrl with the declared mime, then call ' +
288
+ 'ziggs_artifact_complete_file with sha256 hex. Prefer contentBase64 so this tool PUTs ' +
289
+ 'and returns checksum for you. Scope optional (chatId / agreementId / taskId) like ' +
290
+ 'ziggs_artifact_record. Do not put large file bodies into ziggs_artifact_record. ' +
291
+ 'After extract finishes (extractionStatus=ok), read text from context / the derived ' +
292
+ 'extracted-text artifact.',
293
+ },
294
+ annotation: 'write',
295
+ params: {
296
+ filename: { type: 'string', required: true, description: 'Original filename' },
297
+ mime: {
298
+ type: 'string',
299
+ required: true,
300
+ description: 'Content-Type for the S3 PUT (e.g. text/plain; charset=utf-8)',
301
+ },
302
+ byteSize: {
303
+ type: 'number',
304
+ required: true,
305
+ description: 'Exact byte length of the object that will be PUT (UTF-8 length for text)',
306
+ },
307
+ format: {
308
+ type: 'string',
309
+ enum: ['pdf', 'docx', 'hwpx', 'md', 'txt'],
310
+ description: 'Optional; inferred from filename when omitted',
311
+ },
312
+ visibility: {
313
+ type: 'string',
314
+ enum: ['chat', 'agent-private'],
315
+ description: 'Defaults server-side when omitted',
316
+ },
317
+ chatId: { type: 'string', description: 'Optional chat scope' },
318
+ agreementId: {
319
+ type: 'string',
320
+ description: 'Optional agreement scope; mutually exclusive with chatId',
321
+ },
322
+ taskId: { type: 'string', description: 'Optional task binding' },
323
+ idempotencyKey: {
324
+ type: 'string',
325
+ description: 'Optional dedup key for the upload-url create',
326
+ },
327
+ contentBase64: {
328
+ type: 'string',
329
+ description: 'Optional base64 file bytes. When set, this tool PUTs to uploadUrl and returns checksum for ziggs_artifact_complete_file.',
330
+ },
331
+ },
332
+ needsAgentId: true,
333
+ handler: async (args, env) => {
334
+ const filename = args['filename'];
335
+ const mime = args['mime'];
336
+ const byteSize = args['byteSize'];
337
+ if (!filename?.trim())
338
+ throw new Error('filename is required');
339
+ if (!mime?.trim())
340
+ throw new Error('mime is required');
341
+ if (!Number.isFinite(byteSize) || byteSize < 1) {
342
+ throw new Error('byteSize must be a positive number');
343
+ }
344
+ const visibility = args['visibility'];
345
+ if (visibility != null &&
346
+ visibility !== 'chat' &&
347
+ visibility !== 'agent-private') {
348
+ throw new Error('visibility must be chat or agent-private');
349
+ }
350
+ const creds = fullCreds(env);
351
+ const result = await new ArtifactsClient(creds.operatorKey, creds.agentId, creds.laneId).uploadUrl({
352
+ filename,
353
+ mime,
354
+ byteSize: Math.trunc(byteSize),
355
+ format: args['format'],
356
+ visibility,
357
+ chatId: args['chatId'],
358
+ agreementId: args['agreementId'],
359
+ taskId: args['taskId'],
360
+ idempotencyKey: args['idempotencyKey'],
361
+ contentBase64: args['contentBase64'],
362
+ });
363
+ return {
364
+ ok: true,
365
+ ...result,
366
+ next: result.checksum
367
+ ? 'Call artifact_complete_file / ziggs_artifact_complete_file with this artifactId and checksum.'
368
+ : 'PUT the exact byteSize bytes to uploadUrl (Content-Type=mime), then call artifact_complete_file / ziggs_artifact_complete_file with sha256 hex of those bytes.',
369
+ };
370
+ },
371
+ };
372
+ export const completeArtifactFileCapability = {
373
+ key: 'artifact_complete_file',
374
+ names: { sdk: 'artifact_complete_file', mcp: 'ziggs_artifact_complete_file' },
375
+ descriptions: {
376
+ sdk: 'Finish a file upload after the S3 PUT. Pass artifactId + sha256 hex of the bytes ' +
377
+ 'you uploaded. Sets extractionStatus=pending and enqueues extract. Poll artifact_list ' +
378
+ 'or context until extractionStatus is ok/failed; download still works while pending.',
379
+ mcp: 'Finish a file upload after the S3 PUT (or after ziggs_artifact_upload_url returned ' +
380
+ 'checksum). Pass artifactId + sha256 hex. Enqueues text extract. When extractionStatus ' +
381
+ 'is ok, read extracted text via ziggs_context_read / grants — file bytes stay on ' +
382
+ 'ziggs_artifact_download.',
383
+ },
384
+ annotation: 'write',
385
+ params: {
386
+ artifactId: { type: 'string', required: true, description: 'From upload-url' },
387
+ checksum: {
388
+ type: 'string',
389
+ required: true,
390
+ description: 'sha256 hex of the bytes that were PUT',
391
+ },
392
+ },
393
+ needsAgentId: true,
394
+ handler: async (args, env) => {
395
+ const artifactId = args['artifactId'];
396
+ const checksum = args['checksum'];
397
+ if (!artifactId)
398
+ throw new Error('artifactId is required');
399
+ if (!checksum)
400
+ throw new Error('checksum is required');
401
+ const creds = fullCreds(env);
402
+ const artifact = await new ArtifactsClient(creds.operatorKey, creds.agentId, creds.laneId).completeFile(artifactId, checksum);
403
+ return {
404
+ ok: true,
405
+ artifact,
406
+ note: 'Extract runs asynchronously. When extractionStatus is ok, read the derived text; use artifact_download for original bytes.',
407
+ };
408
+ },
409
+ };
410
+ export const downloadArtifactCapability = {
411
+ key: 'artifact_download',
412
+ names: { sdk: 'artifact_download', mcp: 'ziggs_artifact_download' },
413
+ descriptions: {
414
+ sdk: 'Get a short-lived (60s) presigned download URL for a file artifact you can read. ' +
415
+ 'Returns downloadUrl, expiresAt, filename — fetch the URL yourself; do not expect ' +
416
+ 'inline file bytes in this response.',
417
+ mcp: 'Presigned download for a file artifact you can read. Returns { downloadUrl, expiresAt, ' +
418
+ 'filename } (TTL ~60s). GET the URL for bytes. For extracted plain text after extract ' +
419
+ 'ok, prefer ziggs_context_read / the extracted-text child over downloading the binary.',
420
+ },
421
+ annotation: 'read-only',
422
+ params: {
423
+ artifactId: { type: 'string', required: true, description: 'File artifact id' },
424
+ },
425
+ needsAgentId: true,
426
+ handler: async (args, env) => {
427
+ const artifactId = args['artifactId'];
428
+ if (!artifactId)
429
+ throw new Error('artifactId is required');
430
+ const creds = fullCreds(env);
431
+ const out = await new ArtifactsClient(creds.operatorKey, creds.agentId, creds.laneId).download(artifactId);
432
+ return { ok: true, ...out };
433
+ },
434
+ };
435
+ export const reextractArtifactCapability = {
436
+ key: 'artifact_reextract',
437
+ names: { sdk: 'artifact_reextract', mcp: 'ziggs_artifact_reextract' },
438
+ descriptions: {
439
+ sdk: 'Re-queue text extraction for a file artifact you authored when status is ok or failed ' +
440
+ '(pending → conflict). On success a new extracted-text row supersedes the previous one.',
441
+ mcp: 'Re-queue extract for a file you authored (extractionStatus ok or failed only). ' +
442
+ 'Creates a new derived text artifact and supersedes the old one — no in-place mutate.',
443
+ },
444
+ annotation: 'write',
445
+ params: {
446
+ artifactId: { type: 'string', required: true, description: 'File artifact id' },
447
+ },
448
+ needsAgentId: true,
449
+ handler: async (args, env) => {
450
+ const artifactId = args['artifactId'];
451
+ if (!artifactId)
452
+ throw new Error('artifactId is required');
453
+ const creds = fullCreds(env);
454
+ const artifact = await new ArtifactsClient(creds.operatorKey, creds.agentId, creds.laneId).reExtract(artifactId);
455
+ return { ok: true, artifact };
456
+ },
457
+ };
272
458
  export const ARTIFACT_CAPABILITIES = [
273
459
  recordArtifactCapability,
274
460
  listArtifactsCapability,
275
461
  shareArtifactCapability,
276
462
  attachArtifactCapability,
463
+ uploadArtifactUrlCapability,
464
+ completeArtifactFileCapability,
465
+ downloadArtifactCapability,
466
+ reextractArtifactCapability,
277
467
  ];
@@ -35,13 +35,23 @@ export const openConversationCapability = {
35
35
  handler: async (args, env) => {
36
36
  if (!args['participantId'])
37
37
  throw new Error('participantId is required');
38
- const { chatId } = await openConversation(args['participantId'], fullCreds(env), {
39
- newChat: args['newChat'] === true,
40
- });
38
+ const { chatId, reused } = await openConversation(args['participantId'], fullCreds(env), { newChat: args['newChat'] === true });
41
39
  const lister = env.surface === 'mcp' ? 'ziggs_grant_list' : 'grant_list';
40
+ const grantNote = `Your membership auto-mints a chat grant, so the chat also appears in ${lister} scopeKind=chat for context reads.`;
41
+ // ZIG-1216: word the note from the outcome instead of covering both cases.
42
+ // "Open (or reused)" told the agent the distinction existed and then
43
+ // withheld it — worse than silence, because the agent cannot even tell
44
+ // there is something to look up. A backend too old to report it keeps the
45
+ // old both-cases wording, and `reused` is simply absent from the result.
46
+ const outcomeNote = reused === true
47
+ ? 'Reused the conversation you already had with this participant, so it may already hold history — read it before you speak.'
48
+ : reused === false
49
+ ? 'Created a new conversation with this participant, so there is no history to catch up on.'
50
+ : 'Conversation is open (or reused).';
42
51
  return {
43
52
  chatId,
44
- note: `Conversation is open (or reused). Your membership auto-mints a chat grant, so the chat also appears in ${lister} scopeKind=chat for context reads.`,
53
+ ...(typeof reused === 'boolean' ? { reused } : {}),
54
+ note: `${outcomeNote} ${grantNote}`,
45
55
  };
46
56
  },
47
57
  };
@@ -7,5 +7,5 @@ export { GRANTS_CAPABILITIES, listGrantsCapability } from './grants.js';
7
7
  export { CONTEXT_CAPABILITIES, contextReadCapability, contextDelegateCapability, contextExpandReachCapability, contextDiscoverGrantableCapability, contextBounds, resolveOrgScopeId, } from './context.js';
8
8
  export { CONNECTION_CAPABILITIES, connectionProxyCapability, requestConnectionCapability, } from './connections.js';
9
9
  export { DISCOVERY_CAPABILITIES, agentSearchCapability, agentGetCapability, } from './discovery.js';
10
- export { ARTIFACT_CAPABILITIES, recordArtifactCapability, listArtifactsCapability, shareArtifactCapability, attachArtifactCapability, } from './artifacts.js';
10
+ export { ARTIFACT_CAPABILITIES, recordArtifactCapability, listArtifactsCapability, shareArtifactCapability, attachArtifactCapability, uploadArtifactUrlCapability, completeArtifactFileCapability, downloadArtifactCapability, reextractArtifactCapability, } from './artifacts.js';
11
11
  export { CHAT_CAPABILITIES, openConversationCapability } from './chat.js';
@@ -7,5 +7,5 @@ export { GRANTS_CAPABILITIES, listGrantsCapability } from './grants.js';
7
7
  export { CONTEXT_CAPABILITIES, contextReadCapability, contextDelegateCapability, contextExpandReachCapability, contextDiscoverGrantableCapability, contextBounds, resolveOrgScopeId, } from './context.js';
8
8
  export { CONNECTION_CAPABILITIES, connectionProxyCapability, requestConnectionCapability, } from './connections.js';
9
9
  export { DISCOVERY_CAPABILITIES, agentSearchCapability, agentGetCapability, } from './discovery.js';
10
- export { ARTIFACT_CAPABILITIES, recordArtifactCapability, listArtifactsCapability, shareArtifactCapability, attachArtifactCapability, } from './artifacts.js';
10
+ export { ARTIFACT_CAPABILITIES, recordArtifactCapability, listArtifactsCapability, shareArtifactCapability, attachArtifactCapability, uploadArtifactUrlCapability, completeArtifactFileCapability, downloadArtifactCapability, reextractArtifactCapability, } from './artifacts.js';
11
11
  export { CHAT_CAPABILITIES, openConversationCapability } from './chat.js';
@@ -37,6 +37,43 @@ export interface WriteArtifactInput {
37
37
  */
38
38
  idempotencyKey?: string;
39
39
  }
40
+ export type ArtifactFileFormat = 'pdf' | 'docx' | 'hwpx' | 'md' | 'txt';
41
+ export interface ArtifactUploadUrlInput {
42
+ filename: string;
43
+ mime: string;
44
+ /** Exact UTF-8 / file byte length of the object that will be PUT. */
45
+ byteSize: number;
46
+ format?: ArtifactFileFormat;
47
+ visibility?: ArtifactVisibility;
48
+ chatId?: string;
49
+ agreementId?: string;
50
+ taskId?: string;
51
+ service?: Record<string, unknown>;
52
+ idempotencyKey?: string;
53
+ /**
54
+ * Optional raw bytes. When set, the client PUTs to the presigned URL and
55
+ * returns `checksum` for {@link ArtifactsClient.completeFile}.
56
+ */
57
+ content?: Uint8Array | Buffer;
58
+ /** Optional base64 of the same bytes as `content`. */
59
+ contentBase64?: string;
60
+ }
61
+ export interface ArtifactUploadUrlResult {
62
+ artifactId: string;
63
+ uploadUrl: string;
64
+ expiresAt: string;
65
+ storageKey: string;
66
+ /** Set when this client performed the S3 PUT. */
67
+ putOk?: boolean;
68
+ /** sha256 hex when content was supplied and PUT succeeded. */
69
+ checksum?: string;
70
+ }
71
+ export type ArtifactFileView = Record<string, unknown> & {
72
+ artifactId?: string;
73
+ extractionStatus?: string | null;
74
+ extractionError?: string | null;
75
+ filename?: string | null;
76
+ };
40
77
  /**
41
78
  * Replaces `ContextReader`'s artifact reads + `ContextWriter`'s breadcrumb
42
79
  * writes. `visibility: 'agent-private'` is the new home for agent thoughts —
@@ -110,6 +147,19 @@ export declare class ArtifactsClient {
110
147
  attachToTask(artifactId: string, taskId: string, role?: 'input' | 'output'): Promise<{
111
148
  success: boolean;
112
149
  }>;
150
+ /**
151
+ * File artifact rail — presign PUT, then client uploads bytes to `uploadUrl`,
152
+ * then {@link completeFile}. Optional `content` / `contentBase64` performs the
153
+ * S3 PUT here and returns a sha256 so complete can follow immediately.
154
+ */
155
+ uploadUrl(input: ArtifactUploadUrlInput): Promise<ArtifactUploadUrlResult>;
156
+ completeFile(artifactId: string, checksum: string): Promise<ArtifactFileView>;
157
+ download(artifactId: string): Promise<{
158
+ downloadUrl: string;
159
+ expiresAt: string;
160
+ filename: string;
161
+ }>;
162
+ reExtract(artifactId: string): Promise<ArtifactFileView>;
113
163
  private _attach;
114
164
  /**
115
165
  * ZIG-1037 — at most one container, and none is fine.
@@ -2,6 +2,17 @@ import { runtimeLog } from '../shared/runtimeLog.js';
2
2
  import { throwApiError } from '../shared/apiError.js';
3
3
  import { getBackendUrl } from '../utils/urlUtils.js';
4
4
  import { buildOperatorHeaders } from './operatorHeaders.js';
5
+ function resolveUploadBytes(input) {
6
+ if (input.content != null) {
7
+ return Buffer.isBuffer(input.content)
8
+ ? input.content
9
+ : Buffer.from(input.content);
10
+ }
11
+ if (input.contentBase64 != null && input.contentBase64 !== '') {
12
+ return Buffer.from(input.contentBase64, 'base64');
13
+ }
14
+ return null;
15
+ }
5
16
  /**
6
17
  * Replaces `ContextReader`'s artifact reads + `ContextWriter`'s breadcrumb
7
18
  * writes. `visibility: 'agent-private'` is the new home for agent thoughts —
@@ -159,6 +170,128 @@ export class ArtifactsClient {
159
170
  role,
160
171
  });
161
172
  }
173
+ /**
174
+ * File artifact rail — presign PUT, then client uploads bytes to `uploadUrl`,
175
+ * then {@link completeFile}. Optional `content` / `contentBase64` performs the
176
+ * S3 PUT here and returns a sha256 so complete can follow immediately.
177
+ */
178
+ async uploadUrl(input) {
179
+ if (!input.filename?.trim()) {
180
+ throw new Error('ArtifactsClient.uploadUrl: filename is required');
181
+ }
182
+ if (!input.mime?.trim()) {
183
+ throw new Error('ArtifactsClient.uploadUrl: mime is required');
184
+ }
185
+ if (!Number.isInteger(input.byteSize) || input.byteSize < 1) {
186
+ throw new Error('ArtifactsClient.uploadUrl: byteSize must be a positive integer');
187
+ }
188
+ this._assertScopeXor({
189
+ text: '.',
190
+ chatId: input.chatId,
191
+ agreementId: input.agreementId,
192
+ });
193
+ const bytes = resolveUploadBytes(input);
194
+ if (bytes && bytes.byteLength !== input.byteSize) {
195
+ throw new Error(`ArtifactsClient.uploadUrl: byteSize ${input.byteSize} does not match content length ${bytes.byteLength}`);
196
+ }
197
+ const url = `${getBackendUrl()}/artifacts/upload-url`;
198
+ const res = await fetch(url, {
199
+ method: 'POST',
200
+ headers: this._headers(),
201
+ body: JSON.stringify({
202
+ filename: input.filename.trim(),
203
+ mime: input.mime.trim(),
204
+ byteSize: input.byteSize,
205
+ format: input.format,
206
+ visibility: input.visibility,
207
+ chatId: input.chatId,
208
+ agreementId: input.agreementId,
209
+ taskId: input.taskId,
210
+ service: input.service,
211
+ ...(input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {}),
212
+ }),
213
+ });
214
+ const text = await res.text().catch(() => '');
215
+ if (!res.ok) {
216
+ throwApiError(res, text, `artifact upload-url failed: ${res.status} ${res.statusText}`);
217
+ }
218
+ const parsed = text
219
+ ? JSON.parse(text)
220
+ : {};
221
+ if (!parsed.artifactId || !parsed.uploadUrl) {
222
+ throw new Error('artifact upload-url response missing artifactId/uploadUrl');
223
+ }
224
+ if (bytes) {
225
+ const put = await fetch(parsed.uploadUrl, {
226
+ method: 'PUT',
227
+ headers: { 'Content-Type': input.mime.trim() },
228
+ // Blob/Uint8Array — Buffer is not always a DOM BodyInit under tsc DOM libs.
229
+ body: new Uint8Array(bytes),
230
+ });
231
+ if (!put.ok) {
232
+ const putBody = await put.text().catch(() => '');
233
+ throw new Error(`S3 PUT failed: ${put.status} ${put.statusText}${putBody ? ` — ${putBody.slice(0, 200)}` : ''}`);
234
+ }
235
+ const { createHash } = await import('node:crypto');
236
+ parsed.checksum = createHash('sha256').update(bytes).digest('hex');
237
+ parsed.putOk = true;
238
+ }
239
+ return parsed;
240
+ }
241
+ async completeFile(artifactId, checksum) {
242
+ if (!artifactId)
243
+ throw new Error('ArtifactsClient.completeFile: artifactId is required');
244
+ if (!/^[a-f0-9]{64}$/i.test(checksum)) {
245
+ throw new Error('ArtifactsClient.completeFile: checksum must be sha256 hex');
246
+ }
247
+ const url = `${getBackendUrl()}/artifacts/${encodeURIComponent(artifactId)}/complete`;
248
+ const res = await fetch(url, {
249
+ method: 'POST',
250
+ headers: this._headers(),
251
+ body: JSON.stringify({ checksum: checksum.toLowerCase() }),
252
+ });
253
+ const text = await res.text().catch(() => '');
254
+ if (!res.ok) {
255
+ throwApiError(res, text, `artifact complete failed: ${res.status} ${res.statusText}`);
256
+ }
257
+ return text ? JSON.parse(text) : {};
258
+ }
259
+ async download(artifactId) {
260
+ if (!artifactId)
261
+ throw new Error('ArtifactsClient.download: artifactId is required');
262
+ const url = `${getBackendUrl()}/artifacts/${encodeURIComponent(artifactId)}/download`;
263
+ const res = await fetch(url, { headers: this._headers() });
264
+ const text = await res.text().catch(() => '');
265
+ if (!res.ok) {
266
+ throwApiError(res, text, `artifact download failed: ${res.status} ${res.statusText}`);
267
+ }
268
+ const parsed = text
269
+ ? JSON.parse(text)
270
+ : {};
271
+ if (!parsed.downloadUrl || !parsed.expiresAt || !parsed.filename) {
272
+ throw new Error('artifact download response incomplete');
273
+ }
274
+ return {
275
+ downloadUrl: parsed.downloadUrl,
276
+ expiresAt: parsed.expiresAt,
277
+ filename: parsed.filename,
278
+ };
279
+ }
280
+ async reExtract(artifactId) {
281
+ if (!artifactId)
282
+ throw new Error('ArtifactsClient.reExtract: artifactId is required');
283
+ const url = `${getBackendUrl()}/artifacts/${encodeURIComponent(artifactId)}/re-extract`;
284
+ const res = await fetch(url, {
285
+ method: 'POST',
286
+ headers: this._headers(),
287
+ body: JSON.stringify({}),
288
+ });
289
+ const text = await res.text().catch(() => '');
290
+ if (!res.ok) {
291
+ throwApiError(res, text, `artifact re-extract failed: ${res.status} ${res.statusText}`);
292
+ }
293
+ return text ? JSON.parse(text) : {};
294
+ }
162
295
  async _attach(path, body) {
163
296
  const res = await fetch(`${getBackendUrl()}${path}`, {
164
297
  method: 'POST',
@@ -13,6 +13,7 @@ export declare function openConversation(participantId: string, creds: Creds, {
13
13
  newChat?: boolean;
14
14
  }): Promise<{
15
15
  chatId: string;
16
+ reused?: boolean;
16
17
  }>;
17
18
  export interface SendChatMessageInput {
18
19
  chatId: string;
@@ -80,6 +81,7 @@ export declare class ChatClient {
80
81
  newChat?: boolean;
81
82
  }): Promise<{
82
83
  chatId: string;
84
+ reused?: boolean;
83
85
  }>;
84
86
  addMember(input: AddChatMemberInput): Promise<AddChatMemberResult>;
85
87
  sendMessage(input: SendChatMessageInput): Promise<SendChatMessageResult>;
@@ -36,7 +36,15 @@ export async function openConversation(participantId, creds, { newChat = false }
36
36
  if (!data?.['chatId'] || typeof data['chatId'] !== 'string') {
37
37
  throw new Error('Invalid response: expected { chatId } from POST /chats');
38
38
  }
39
- return { chatId: data['chatId'] };
39
+ // ZIG-1216: left UNDEFINED rather than defaulted when the backend does not
40
+ // send it. A backend from before this field cannot be assumed to have created
41
+ // the room — defaulting to false would report a reused conversation as fresh,
42
+ // which is worse than saying nothing.
43
+ const reused = data['reused'];
44
+ return {
45
+ chatId: data['chatId'],
46
+ ...(typeof reused === 'boolean' ? { reused } : {}),
47
+ };
40
48
  }
41
49
  /**
42
50
  * POST /chats/:chatId/members — add user/agent; agent-invite may return pending admission (ZIG-426).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ziggs-ai/api-client",
3
- "version": "0.9.3",
3
+ "version": "0.9.5",
4
4
  "description": "HTTP and WebSocket client for the Ziggs backend API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",