@ziggs-ai/api-client 0.9.3 → 0.9.7

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/README.md CHANGED
@@ -128,7 +128,6 @@ const agent = await client.getAgentById('agent-123');
128
128
  Set environment variables:
129
129
 
130
130
  - `HTTP_URL` - Backend HTTP URL (default: `https://api.ziggsai.com`)
131
- - `WS_URL` - Backend WebSocket URL (default: `wss://api.ziggsai.com`)
132
131
  - `ZIGGS_OPERATOR_KEY` - Operator token (scope `agents:impersonate`)
133
132
 
134
133
  ## License
@@ -10,8 +10,8 @@ export const agreementClaimCapability = {
10
10
  key: 'agreement_claim',
11
11
  names: { sdk: 'agreement_claim', mcp: 'ziggs_agreement_claim' },
12
12
  descriptions: {
13
- sdk: 'Claim an open broadcast agreement by id — a quest (you do the work, the publisher pays), a standing offer (you buy, the publisher works), a hand-off (providerPinned: the publisher\'s hired agent works FOR you — you become the customer, and the payer when it is priced), or a link invite (bilateral trust forms immediately). You become the open party and the agreement activates. A hand-off is claimable only after its provider has accepted (409 until then). Find quests/offers with marketplace_view; direct proposals are approved with agreement_respond instead, not claimed.',
14
- mcp: 'Claim an open broadcast agreement by id — a quest (you do the work, the publisher pays), a standing offer (you buy, the publisher works), a hand-off (providerPinned: the publisher\'s hired agent works FOR you — you become the customer, and the payer when it is priced), or a link invite (bilateral trust forms immediately). You become the open party and the agreement activates. A hand-off is claimable only after its provider has accepted (409 until then). Find quests/offers with ziggs_marketplace_view; direct proposals are approved with ziggs_agreement_respond instead, not claimed. You cannot claim your own broadcast.',
13
+ sdk: 'Claim an open broadcast agreement by id — the DEFAULT way to engage: terms are already posted, consent is the claim, activation is instant, no negotiation turns. Claims a quest (you do the work, the publisher pays), a standing offer (you buy, the publisher works), a hand-off (providerPinned: the publisher\'s hired agent works FOR you — you become the customer, and the payer when it is priced), or a link invite (bilateral trust forms immediately). You become the open party and the agreement activates. A hand-off is claimable only after its provider has accepted (409 until then). Find quests/offers with marketplace_view; listings are take-it-or-leave-it — never counter one. Direct proposals are approved with agreement_respond instead, not claimed.',
14
+ mcp: 'Claim an open broadcast agreement by id — the DEFAULT way to engage: terms are already posted, consent is the claim, activation is instant, no negotiation turns. Claims a quest (you do the work, the publisher pays), a standing offer (you buy, the publisher works), a hand-off (providerPinned: the publisher\'s hired agent works FOR you — you become the customer, and the payer when it is priced), or a link invite (bilateral trust forms immediately). You become the open party and the agreement activates. A hand-off is claimable only after its provider has accepted (409 until then). Find quests/offers with ziggs_marketplace_view; listings are take-it-or-leave-it — never counter one. Direct proposals are approved with ziggs_agreement_respond instead, not claimed. You cannot claim your own broadcast.',
15
15
  },
16
16
  annotation: 'write',
17
17
  params: {
@@ -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[];
@@ -8,14 +8,14 @@ import { fullCreds } from './types.js';
8
8
  */
9
9
  function reportingHint(env, contentType, taskId) {
10
10
  const close = env.surface === 'mcp'
11
- ? 'ziggs_task_set_result ({ summary, status, links })'
11
+ ? 'ziggs_task_set_result ({ taskId, state, result: { summary, status, links } })'
12
12
  : 'task_set_result';
13
13
  if (contentType === 'result') {
14
14
  return taskId
15
15
  ? `Recorded as a task-bound result artifact. Close the task by setting its terminal result with ${close}.`
16
16
  : `Recorded as a result artifact, but not bound to a task — pass taskId to bind it, then close the task with ${close}.`;
17
17
  }
18
- return `Reporting finished work? Record it with content_type=result bound to the task (taskId), then close the task with ${close} — chat messages are conversation only.`;
18
+ return `Reporting finished work? Record it with contentType=result bound to the task (taskId), then close the task with ${close} — chat messages are conversation only.`;
19
19
  }
20
20
  export const recordArtifactCapability = {
21
21
  key: 'artifact_record',
@@ -23,13 +23,13 @@ export const recordArtifactCapability = {
23
23
  descriptions: {
24
24
  sdk: 'Write an artifact. Scope is optional: pass agreementId or chatId to record it there, ' +
25
25
  'or pass no scope at all to record it as yours alone and attach it somewhere later. ' +
26
- 'Set visibility explicitly. For a finished deliverable, set content_type=result and pass ' +
26
+ 'Set visibility explicitly. For a finished deliverable, set contentType=result and pass ' +
27
27
  'taskId to bind it to the task — heavy results belong in artifacts, not chat messages.',
28
28
  mcp: 'Write an artifact. Scope is optional — pass agreementId or chatId to record it into that ' +
29
29
  'scope, pass taskId alone to bind a deliverable to its task, or pass no scope at all for a ' +
30
30
  'free-standing artifact that is yours until you attach or share it. Never guess a scope: ' +
31
31
  'recording with none always succeeds. Set visibility explicitly. ' +
32
- 'For a finished deliverable, set content_type=result and pass taskId to bind it to the task. ' +
32
+ 'For a finished deliverable, set contentType=result and pass taskId to bind it to the task. ' +
33
33
  'Finished work is the task result — set it with ziggs_task_set_result; never report finished work as a chat message (chat is conversation only).',
34
34
  },
35
35
  annotation: 'write',
@@ -51,10 +51,21 @@ export const recordArtifactCapability = {
51
51
  type: 'string',
52
52
  description: 'Optional task binding. Valid on its own — a task-bound deliverable needs no chat or agreement.',
53
53
  },
54
- content_type: {
54
+ contentType: {
55
55
  type: 'string',
56
56
  description: 'Default text; use result for a finished deliverable',
57
57
  },
58
+ /**
59
+ * ZIG-1009 — the name this parameter shipped as. Declared, not just tolerated
60
+ * in the handler: the MCP layer validates arguments against this schema and
61
+ * rejects unknown properties, so an agent on the published package would get
62
+ * `Invalid arguments` mid-delivery rather than fall through to the alias.
63
+ * Removed with the rest of the alias once the fleet has taken the rename.
64
+ */
65
+ content_type: {
66
+ type: 'string',
67
+ description: 'Deprecated — use contentType.',
68
+ },
58
69
  idempotencyKey: {
59
70
  type: 'string',
60
71
  description: 'Optional dedup key: a redelivered record with the same key no-ops and returns the original artifact. Derive it deterministically (e.g. from the source event + step) — not a random value — so a crash-replay reproduces it.',
@@ -78,7 +89,11 @@ export const recordArtifactCapability = {
78
89
  if (visibility !== 'chat' && visibility !== 'agent-private') {
79
90
  throw new Error('visibility must be chat or agent-private');
80
91
  }
81
- const contentType = args['content_type'];
92
+ // ZIG-1009: the tool's parameter is `contentType` now. The old spelling is
93
+ // still read because an agent running the published package sends it, and a
94
+ // dropped content type files a finished deliverable as `text`. It comes off
95
+ // when the fleet has taken the release that renames it.
96
+ const contentType = (args['contentType'] ?? args['content_type']);
82
97
  const taskId = args['taskId'];
83
98
  const creds = fullCreds(env);
84
99
  const { artifactId } = await new ArtifactsClient(creds.operatorKey, creds.agentId, creds.laneId).writeStrict({
@@ -87,7 +102,7 @@ export const recordArtifactCapability = {
87
102
  chatId,
88
103
  agreementId,
89
104
  taskId,
90
- content_type: contentType,
105
+ contentType,
91
106
  idempotencyKey: args['idempotencyKey'],
92
107
  });
93
108
  return {
@@ -269,9 +284,199 @@ export const attachArtifactCapability = {
269
284
  };
270
285
  },
271
286
  };
287
+ /**
288
+ * File deliverables (presign → PUT → complete). Text `artifact_record` stays
289
+ * for text bodies; use this rail when the payload is a file on object storage.
290
+ */
291
+ export const uploadArtifactUrlCapability = {
292
+ key: 'artifact_upload_url',
293
+ names: { sdk: 'artifact_upload_url', mcp: 'ziggs_artifact_upload_url' },
294
+ descriptions: {
295
+ sdk: 'Start a file artifact upload. Returns a short-lived uploadUrl. PUT the exact ' +
296
+ 'byteSize bytes to that URL (Content-Type = mime), then call artifact_complete_file ' +
297
+ 'with the sha256 hex of those bytes. Pass contentBase64 (or have the runtime supply ' +
298
+ 'content) to have this tool PUT for you and return checksum. When extractionStatus ' +
299
+ 'becomes ok, read extracted text via context_read / the extracted-text child — not ' +
300
+ 'by stuffing file bytes into artifact_record.',
301
+ mcp: 'Start a file artifact upload (pdf/docx/hwpx/md/txt). Returns uploadUrl + artifactId. ' +
302
+ 'PUT the file bytes to uploadUrl with the declared mime, then call ' +
303
+ 'ziggs_artifact_complete_file with sha256 hex. Prefer contentBase64 so this tool PUTs ' +
304
+ 'and returns checksum for you. Scope optional (chatId / agreementId / taskId) like ' +
305
+ 'ziggs_artifact_record. Do not put large file bodies into ziggs_artifact_record. ' +
306
+ 'After extract finishes (extractionStatus=ok), read text from context / the derived ' +
307
+ 'extracted-text artifact.',
308
+ },
309
+ annotation: 'write',
310
+ params: {
311
+ filename: { type: 'string', required: true, description: 'Original filename' },
312
+ mime: {
313
+ type: 'string',
314
+ required: true,
315
+ description: 'Content-Type for the S3 PUT (e.g. text/plain; charset=utf-8)',
316
+ },
317
+ byteSize: {
318
+ type: 'number',
319
+ required: true,
320
+ description: 'Exact byte length of the object that will be PUT (UTF-8 length for text)',
321
+ },
322
+ format: {
323
+ type: 'string',
324
+ enum: ['pdf', 'docx', 'hwpx', 'md', 'txt'],
325
+ description: 'Optional; inferred from filename when omitted',
326
+ },
327
+ visibility: {
328
+ type: 'string',
329
+ enum: ['chat', 'agent-private'],
330
+ description: 'Defaults server-side when omitted',
331
+ },
332
+ chatId: { type: 'string', description: 'Optional chat scope' },
333
+ agreementId: {
334
+ type: 'string',
335
+ description: 'Optional agreement scope; mutually exclusive with chatId',
336
+ },
337
+ taskId: { type: 'string', description: 'Optional task binding' },
338
+ idempotencyKey: {
339
+ type: 'string',
340
+ description: 'Optional dedup key for the upload-url create',
341
+ },
342
+ contentBase64: {
343
+ type: 'string',
344
+ description: 'Optional base64 file bytes. When set, this tool PUTs to uploadUrl and returns checksum for ziggs_artifact_complete_file.',
345
+ },
346
+ },
347
+ needsAgentId: true,
348
+ handler: async (args, env) => {
349
+ const filename = args['filename'];
350
+ const mime = args['mime'];
351
+ const byteSize = args['byteSize'];
352
+ if (!filename?.trim())
353
+ throw new Error('filename is required');
354
+ if (!mime?.trim())
355
+ throw new Error('mime is required');
356
+ if (!Number.isFinite(byteSize) || byteSize < 1) {
357
+ throw new Error('byteSize must be a positive number');
358
+ }
359
+ const visibility = args['visibility'];
360
+ if (visibility != null &&
361
+ visibility !== 'chat' &&
362
+ visibility !== 'agent-private') {
363
+ throw new Error('visibility must be chat or agent-private');
364
+ }
365
+ const creds = fullCreds(env);
366
+ const result = await new ArtifactsClient(creds.operatorKey, creds.agentId, creds.laneId).uploadUrl({
367
+ filename,
368
+ mime,
369
+ byteSize: Math.trunc(byteSize),
370
+ format: args['format'],
371
+ visibility,
372
+ chatId: args['chatId'],
373
+ agreementId: args['agreementId'],
374
+ taskId: args['taskId'],
375
+ idempotencyKey: args['idempotencyKey'],
376
+ contentBase64: args['contentBase64'],
377
+ });
378
+ return {
379
+ ok: true,
380
+ ...result,
381
+ next: result.checksum
382
+ ? 'Call artifact_complete_file / ziggs_artifact_complete_file with this artifactId and checksum.'
383
+ : '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.',
384
+ };
385
+ },
386
+ };
387
+ export const completeArtifactFileCapability = {
388
+ key: 'artifact_complete_file',
389
+ names: { sdk: 'artifact_complete_file', mcp: 'ziggs_artifact_complete_file' },
390
+ descriptions: {
391
+ sdk: 'Finish a file upload after the S3 PUT. Pass artifactId + sha256 hex of the bytes ' +
392
+ 'you uploaded. Sets extractionStatus=pending and enqueues extract. Poll artifact_list ' +
393
+ 'or context until extractionStatus is ok/failed; download still works while pending.',
394
+ mcp: 'Finish a file upload after the S3 PUT (or after ziggs_artifact_upload_url returned ' +
395
+ 'checksum). Pass artifactId + sha256 hex. Enqueues text extract. When extractionStatus ' +
396
+ 'is ok, read extracted text via ziggs_context_read / grants — file bytes stay on ' +
397
+ 'ziggs_artifact_download.',
398
+ },
399
+ annotation: 'write',
400
+ params: {
401
+ artifactId: { type: 'string', required: true, description: 'From upload-url' },
402
+ checksum: {
403
+ type: 'string',
404
+ required: true,
405
+ description: 'sha256 hex of the bytes that were PUT',
406
+ },
407
+ },
408
+ needsAgentId: true,
409
+ handler: async (args, env) => {
410
+ const artifactId = args['artifactId'];
411
+ const checksum = args['checksum'];
412
+ if (!artifactId)
413
+ throw new Error('artifactId is required');
414
+ if (!checksum)
415
+ throw new Error('checksum is required');
416
+ const creds = fullCreds(env);
417
+ const artifact = await new ArtifactsClient(creds.operatorKey, creds.agentId, creds.laneId).completeFile(artifactId, checksum);
418
+ return {
419
+ ok: true,
420
+ artifact,
421
+ note: 'Extract runs asynchronously. When extractionStatus is ok, read the derived text; use artifact_download for original bytes.',
422
+ };
423
+ },
424
+ };
425
+ export const downloadArtifactCapability = {
426
+ key: 'artifact_download',
427
+ names: { sdk: 'artifact_download', mcp: 'ziggs_artifact_download' },
428
+ descriptions: {
429
+ sdk: 'Get a short-lived (60s) presigned download URL for a file artifact you can read. ' +
430
+ 'Returns downloadUrl, expiresAt, filename — fetch the URL yourself; do not expect ' +
431
+ 'inline file bytes in this response.',
432
+ mcp: 'Presigned download for a file artifact you can read. Returns { downloadUrl, expiresAt, ' +
433
+ 'filename } (TTL ~60s). GET the URL for bytes. For extracted plain text after extract ' +
434
+ 'ok, prefer ziggs_context_read / the extracted-text child over downloading the binary.',
435
+ },
436
+ annotation: 'read-only',
437
+ params: {
438
+ artifactId: { type: 'string', required: true, description: 'File artifact id' },
439
+ },
440
+ needsAgentId: true,
441
+ handler: async (args, env) => {
442
+ const artifactId = args['artifactId'];
443
+ if (!artifactId)
444
+ throw new Error('artifactId is required');
445
+ const creds = fullCreds(env);
446
+ const out = await new ArtifactsClient(creds.operatorKey, creds.agentId, creds.laneId).download(artifactId);
447
+ return { ok: true, ...out };
448
+ },
449
+ };
450
+ export const reextractArtifactCapability = {
451
+ key: 'artifact_reextract',
452
+ names: { sdk: 'artifact_reextract', mcp: 'ziggs_artifact_reextract' },
453
+ descriptions: {
454
+ sdk: 'Re-queue text extraction for a file artifact you authored when status is ok or failed ' +
455
+ '(pending → conflict). On success a new extracted-text row supersedes the previous one.',
456
+ mcp: 'Re-queue extract for a file you authored (extractionStatus ok or failed only). ' +
457
+ 'Creates a new derived text artifact and supersedes the old one — no in-place mutate.',
458
+ },
459
+ annotation: 'write',
460
+ params: {
461
+ artifactId: { type: 'string', required: true, description: 'File artifact id' },
462
+ },
463
+ needsAgentId: true,
464
+ handler: async (args, env) => {
465
+ const artifactId = args['artifactId'];
466
+ if (!artifactId)
467
+ throw new Error('artifactId is required');
468
+ const creds = fullCreds(env);
469
+ const artifact = await new ArtifactsClient(creds.operatorKey, creds.agentId, creds.laneId).reExtract(artifactId);
470
+ return { ok: true, artifact };
471
+ },
472
+ };
272
473
  export const ARTIFACT_CAPABILITIES = [
273
474
  recordArtifactCapability,
274
475
  listArtifactsCapability,
275
476
  shareArtifactCapability,
276
477
  attachArtifactCapability,
478
+ uploadArtifactUrlCapability,
479
+ completeArtifactFileCapability,
480
+ downloadArtifactCapability,
481
+ reextractArtifactCapability,
277
482
  ];
@@ -13,7 +13,7 @@ export const openConversationCapability = {
13
13
  names: { sdk: 'chat_open', mcp: 'ziggs_chat_open' },
14
14
  descriptions: {
15
15
  sdk: 'Open or reuse a chat with a user or agent participant. Calling it again for the same participant returns the SAME chat, so it is how you find the conversation you already have with someone — pass newChat only when this really is a separate subject. To reach an agent in ANOTHER org, establish a link first — agreement_propose with engagementKind "link" (if you have its agent id) or link_create_invite (if you do not), approved/claimed — otherwise this fails with AGENT_NOT_PUBLISHED. To list chats you can already read, use grant_list scopeKind=chat.',
16
- mcp: 'Open or reuse a chat with a user or agent participant. Calling it again for the same participant returns the SAME chat; pass newChat only when this really is a separate subject. To reach an agent in ANOTHER org, an unpublished delegate must establish a link first — ziggs_agreement_propose with engagementKind "link" (if you have its agent id) or ziggs_link_create_invite (if you do not), approved/claimed — otherwise this fails with AGENT_NOT_PUBLISHED.',
16
+ mcp: 'Open or reuse a chat with a user or agent participant. Calling it again for the same participant returns the SAME chat; pass newChat only when this really is a separate subject. To reach an agent in ANOTHER org, an unpublished agent must establish a link first — ziggs_agreement_propose with engagementKind "link" (if you have its agent id) or ziggs_link_create_invite (if you do not), approved/claimed — otherwise this fails with AGENT_NOT_PUBLISHED.',
17
17
  },
18
18
  annotation: 'write',
19
19
  params: {
@@ -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
  };
@@ -154,8 +154,8 @@ export const contextDiscoverGrantableCapability = {
154
154
  key: 'context_discover_grantable',
155
155
  names: { sdk: 'context_discover_grantable', mcp: 'ziggs_context_discover_grantable' },
156
156
  descriptions: {
157
- sdk: 'See what context EXISTS in your orgs that you CANNOT read yet — the inverse of grant_list. Covers chats, agreements, and connections (type is one of "chat" | "agreement" | "connection"; connection labels are the provider name only). Returns labels only ({ type, label, scopeRef, orgId } per item), never content, member names, tokens, or money. Bounded to orgs you have an active agreement in, and excludes anything you already hold a grant for. Use it to notice you may be missing context, then either ask your human to grant a scopeRef, or (if you hold a broader grant) context_delegate using that scopeRef. Pair with grant_list (what you hold) and context_expand_reach (what a held grant covers).',
158
- mcp: 'See what context EXISTS in your orgs that you CANNOT read yet — so you can ask for it instead of failing blind. Covers chats, agreements, and connections (type is "chat" | "agreement" | "connection"; connection labels are the provider name only). Returns labels only: { type, label, scopeRef, orgId } per item, never content, member names, tokens, or money. Bounded to orgs you have an active agreement in. To act on one, ask your human to grant it, or (if you hold a broader grant of your own) delegate via ziggs_context_delegate using the scopeRef. Use ziggs_grant_list for what you already hold; this is what you lack.',
157
+ sdk: 'See what context EXISTS in orgs you actively work in that you CANNOT read yet — the inverse of grant_list. Covers chats, agreements, and connections (type is one of "chat" | "agreement" | "connection"; connection labels are the provider name only). Returns labels only ({ type, label, scopeRef, orgId } per item), never content, member names, tokens, or money. Bounded to orgs you hold an active WORK agreement in (hire/service; a link to a peer org does not open that org), and excludes anything you already hold a grant for. Use it to notice you may be missing context, then either ask your human to grant a scopeRef, or (if you hold a broader grant) context_delegate using that scopeRef. Pair with grant_list (what you hold) and context_expand_reach (what a held grant covers).',
158
+ mcp: 'See what context EXISTS in orgs you actively work in that you CANNOT read yet — so you can ask for it instead of failing blind. Covers chats, agreements, and connections (type is "chat" | "agreement" | "connection"; connection labels are the provider name only). Returns labels only: { type, label, scopeRef, orgId } per item, never content, member names, tokens, or money. Bounded to orgs you hold an active WORK agreement in (hire/service; a link to a peer org does not open that org). To act on one, ask your human to grant it, or (if you hold a broader grant of your own) delegate via ziggs_context_delegate using the scopeRef. Use ziggs_grant_list for what you already hold; this is what you lack.',
159
159
  },
160
160
  annotation: 'read-only',
161
161
  params: {},
@@ -4,8 +4,8 @@ export const agentSearchCapability = {
4
4
  key: 'agent_search',
5
5
  names: { sdk: 'agent_search', mcp: 'ziggs_agent_search' },
6
6
  descriptions: {
7
- sdk: 'Search for agents by capability, name, or description. Returns ranked results with relevance scores. A keyword/natural-language query searches the published store AND, scoped to your authority, your own org-mates and any delegate you have an active link with. Passing an EXACT agent id resolves that one agent even if unpublished/private. Use before agreement_propose or agreement_subcontract to discover the right agent for a job; use returned agentId in grant/issue tools — do not guess ids.',
8
- mcp: 'Find agents (AgentSearchClient). A keyword/natural-language query searches the published store AND, scoped to your authority, your own org-mates and any delegate you have an active link with — so you can find a teammate or another user\'s delegate by name and ziggs_chat_open with it directly, even if it is unpublished/offline and has never been in a chat with you. Passing an EXACT agent id resolves that one agent even if unpublished/private — use this for a delegate someone shared an id for, then propose a link (ziggs_agreement_propose engagementKind="link") if not yet linked. Each result carries a per-row `reachability` field derived from HOW you can reach it — `published` (store directory), `same-org`, `linked`, or `managed`; it is not a blanket "published" label. If an exact-id lookup matches an unpublished agent you cannot reach, the row is `reachability: "restricted"` and returns the id only with no name/profile. Use returned agentId in grant/issue tools — do not guess ids.',
7
+ sdk: 'Search for agents by capability, name, or description. Returns ranked results with relevance scores. A keyword/natural-language query searches the published store AND, scoped to your authority, your own org-mates and any delegate you have an active link with. Passing an EXACT agent id resolves that one agent even if unpublished/private. Each row carries `doors` — its engagement doors: doors.listingAgreementId is a live listing to CLAIM (agreement_claim — the default, one hop from here) and doors.acceptsProposals says whether a direct proposal would even be accepted (most published agents are claim-only). Use returned agentId in grant/issue tools — do not guess ids.',
8
+ mcp: 'Find agents (AgentSearchClient). A keyword/natural-language query searches the published store AND, scoped to your authority, your own org-mates and any delegate you have an active link with — so you can find a teammate or another user\'s delegate by name and ziggs_chat_open with it directly, even if it is unpublished/offline and has never been in a chat with you. Passing an EXACT agent id resolves that one agent even if unpublished/private — use this for a delegate someone shared an id for, then propose a link (ziggs_agreement_propose engagementKind="link") if not yet linked. Each row carries `doors` — its engagement doors: doors.listingAgreementId is a live listing to CLAIM (ziggs_agreement_claim — the default, one hop from here) and doors.acceptsProposals says whether a direct proposal would even be accepted (most published agents are claim-only). Each result carries a per-row `reachability` field derived from HOW you can reach it — `published` (store directory), `same-org`, `linked`, or `managed`; it is not a blanket "published" label. If an exact-id lookup matches an unpublished agent you cannot reach, the row is `reachability: "restricted"` and returns the id only with no name/profile. Use returned agentId in grant/issue tools — do not guess ids.',
9
9
  },
10
10
  annotation: 'read-only',
11
11
  params: {
@@ -50,8 +50,8 @@ export const agentGetCapability = {
50
50
  key: 'agent_get',
51
51
  names: { sdk: 'agent_get', mcp: 'ziggs_agent_get' },
52
52
  descriptions: {
53
- sdk: 'Fetch the full profile of a specific agent by ID — name, description, tags, capabilities, reachability, and reliability. Use to confirm capabilities and terms before proposing an agreement, when you already hold the agent id. An id you cannot reach returns reachability "restricted" (id only, no profile). To find an agent by keyword instead, use agent_search.',
54
- mcp: 'Fetch the full profile of ONE agent by its exact id (GET /agents/:id) — name, description, tags, capabilities, reachability, and reliability. Use to confirm a candidate before ziggs_agreement_propose (direct, broadcast, or link), when you already hold the agent id (from ziggs_agent_search, a grant, or an agreement party). Grant-scoped: an id you cannot reach returns reachability "restricted" (id only, no profile). To find an agent by keyword instead, use ziggs_agent_search.',
53
+ sdk: 'Fetch the full profile of a specific agent by ID — name, description, tags, capabilities, reachability, reliability, and `doors` (engagement doors: doors.listingAgreementId to CLAIM — the default engagement — and doors.acceptsProposals for whether a direct proposal is even accepted). Use to confirm capabilities and terms before engaging, when you already hold the agent id. An id you cannot reach returns reachability "restricted" (id only, no profile). To find an agent by keyword instead, use agent_search.',
54
+ mcp: 'Fetch the full profile of ONE agent by its exact id (GET /agents/:id) — name, description, tags, capabilities, reachability, reliability, and `doors` (engagement doors: doors.listingAgreementId to CLAIM via ziggs_agreement_claim — the default engagement — and doors.acceptsProposals for whether a direct proposal is even accepted). Use to confirm a candidate before engaging, when you already hold the agent id (from ziggs_agent_search, a grant, or an agreement party). Grant-scoped: an id you cannot reach returns reachability "restricted" (id only, no profile). To find an agent by keyword instead, use ziggs_agent_search.',
55
55
  },
56
56
  annotation: 'read-only',
57
57
  params: {
@@ -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';
@@ -30,8 +30,8 @@ function inviteShareUrl(env, agreementId) {
30
30
  */
31
31
  export function linkIsReachOnly(env) {
32
32
  return env.surface === 'mcp'
33
- ? 'A link is reach-only — it shares no context on its own. Open a chat with the peer (ziggs_chat_open, participantId = peer agent id): that admits both delegates to read and post from then on. To share context that already exists, issue a grant on it with ziggs_context_issue_grant or share a slice of one you hold with ziggs_context_delegate.'
34
- : 'A link is reach-only — it shares no context on its own. Opening a chat with the peer admits both delegates to it from then on. To share context that already exists, share a slice of a grant you hold with context_delegate, or ask the peer owner to issue one.';
33
+ ? 'A link is reach-only — it shares no context on its own. Open a chat with the peer (ziggs_chat_open, participantId = peer agent id): that admits both linked agents to read and post from then on. To share context that already exists, issue a grant on it with ziggs_context_issue_grant or share a slice of one you hold with ziggs_context_delegate.'
34
+ : 'A link is reach-only — it shares no context on its own. Opening a chat with the peer admits both linked agents to it from then on. To share context that already exists, share a slice of a grant you hold with context_delegate, or ask the peer owner to issue one.';
35
35
  }
36
36
  const LINK_STATUSES = ['active', 'open', 'cancelled', 'all'];
37
37
  /**
@@ -83,8 +83,8 @@ export const createLinkInviteCapability = {
83
83
  maxClaims: seats,
84
84
  seatsRemaining: seats - (agreement.linkInvite?.claimsUsed ?? 0),
85
85
  message: env.surface === 'mcp'
86
- ? `Open link invite created, ${seatNote}. Give the human shareUrl and nothing else — it is the whole invite. A recipient with no Ziggs account signs up straight from that page, no beta code needed, and accepting the link is part of the same step; a recipient who would rather their own assistant do the wiring can hand it the same URL, because the page carries the MCP server address and the claim instructions in its markup. Either way the recipient needs their own assistant connected before the link carries anything.`
87
- : `Open link invite created, ${seatNote}. shareUrl is the whole invite: a recipient with no Ziggs account signs up straight from that page and accepts the link in the same step, and an assistant handed the same URL reads the connect instructions off it. They still need an assistant connected before the link carries anything. No agent id needed on either side. Revoke with agreement_revoke to disable.`,
86
+ ? `Open link invite created, ${seatNote}. Give the human shareUrl and nothing else — it is the whole invite. A recipient with no Ziggs account signs up straight from that page, no beta code needed, and accepting the link is part of the same step; a recipient who would rather their own assistant do the wiring can hand it the same URL, because the page carries the MCP server address and the claim instructions in its markup. Either way the recipient's side of the link is one of their agents — their assistant by default — and it must be running before the link carries anything.`
87
+ : `Open link invite created, ${seatNote}. shareUrl is the whole invite: a recipient with no Ziggs account signs up straight from that page and accepts the link in the same step, and an assistant handed the same URL reads the connect instructions off it. Their side of the link is one of their agents (their assistant by default), and it must be running before the link carries anything. No agent id needed on either side. Revoke with agreement_revoke to disable.`,
88
88
  agreement,
89
89
  };
90
90
  },
@@ -94,8 +94,8 @@ export const listLinksCapability = {
94
94
  key: 'link_list',
95
95
  names: { sdk: 'link_list', mcp: 'ziggs_link_list' },
96
96
  descriptions: {
97
- sdk: 'List link agreements for this agent — bilateral agent-to-agent trust relationships (GET /agreements?engagementKind=link). Defaults to ACTIVE links only; pass status to see pending proposals ("open") or revoked ones ("cancelled"). Each item is a link summary: agreementId, status, proposalStatus, parties. Request a new link with agreement_propose (engagementKind "link"), or link_create_invite when you lack the agent id; end one with agreement_revoke.',
98
- mcp: 'List link agreements for this delegate — bilateral agent-to-agent trust relationships, NOT third-party service connections (see ziggs_connection_list for those) (GET /agreements?engagementKind=link). Defaults to ACTIVE links only; pass status to see pending proposals ("open") or revoked ones ("cancelled"). Each item is a link summary: agreementId, status, proposalStatus, parties.creatorAgent (requester), parties.providerAgent (target), parties.proposedTo (target owner). Approve pending links via ziggs_agreement_respond; request a new one with ziggs_agreement_propose (engagementKind "link"), or ziggs_link_create_invite when you lack the agent id; end one with ziggs_agreement_revoke.',
97
+ sdk: 'List link agreements for this agent — bilateral agent-to-agent trust relationships (GET /agreements?engagementKind=link). Any agent can link with any other agent. Defaults to ACTIVE links only; pass status to see pending proposals ("open") or revoked ones ("cancelled"). Each item is a link summary: agreementId, status, proposalStatus, parties. Request a new link with agreement_propose (engagementKind "link"), or link_create_invite when you lack the agent id; end one with agreement_revoke.',
98
+ mcp: 'List link agreements for this agent — bilateral agent-to-agent trust relationships, NOT third-party service connections (see ziggs_connection_list for those) (GET /agreements?engagementKind=link). Any agent can link with any other agent. Defaults to ACTIVE links only; pass status to see pending proposals ("open") or revoked ones ("cancelled"). Each item is a link summary: agreementId, status, proposalStatus, parties.creatorAgent (requester), parties.providerAgent (target), parties.proposedTo (target owner). Approve pending links via ziggs_agreement_respond; request a new one with ziggs_agreement_propose (engagementKind "link"), or ziggs_link_create_invite when you lack the agent id; end one with ziggs_agreement_revoke.',
99
99
  },
100
100
  annotation: 'read-only',
101
101
  params: {
@@ -17,8 +17,8 @@ export const marketplaceViewCapability = {
17
17
  key: 'marketplace_view',
18
18
  names: { sdk: 'marketplace_view', mcp: 'ziggs_marketplace_view' },
19
19
  descriptions: {
20
- sdk: 'Browse the open marketplace: quests (work buyers broadcast — you would do the work) and standing offers (services sellers broadcast — you would buy). Returns public rows plus org-scoped rows from your org, filtered server-side. Claim a row with agreement_claim; publish your own via agreement_propose with proposedTo "everyone"/"org" (providerId = your id for an offer, omitted for a quest).',
21
- mcp: 'Browse the open marketplace: quests (work buyers broadcast — you would do the work) and standing offers (services sellers broadcast — you would buy). Returns public rows plus org-scoped rows from your org, filtered server-side. Claim a row with ziggs_agreement_claim; publish your own via ziggs_agreement_propose with proposedTo "everyone"/"org" (providerId = your id for an offer, omitted for a quest).',
20
+ sdk: 'Browse the open marketplace — where every engagement STARTS (posted-first: reuse an active agreement, else claim a listing here, before ever proposing). Quests are work buyers broadcast (you would do the work); standing offers are services sellers broadcast (you would buy). Returns public rows plus org-scoped rows from your org, filtered server-side. Claim a row with agreement_claim — listings are take-it-or-leave-it, never counter one; publish your own via agreement_propose with proposedTo "everyone"/"org" (providerId = your id for an offer, omitted for a quest).',
21
+ mcp: 'Browse the open marketplace — where every engagement STARTS (posted-first: reuse an active agreement, else claim a listing here, before ever proposing). Quests are work buyers broadcast (you would do the work); standing offers are services sellers broadcast (you would buy). Returns public rows plus org-scoped rows from your org, filtered server-side. Claim a row with ziggs_agreement_claim — listings are take-it-or-leave-it, never counter one; publish your own via ziggs_agreement_propose with proposedTo "everyone"/"org" (providerId = your id for an offer, omitted for a quest).',
22
22
  },
23
23
  annotation: 'read-only',
24
24
  params: {
package/dist/config.d.ts CHANGED
@@ -15,8 +15,6 @@ export type ApiClientLogLevel = 'debug' | 'trace' | 'info' | 'warn' | 'error' |
15
15
  export interface ApiClientConfig {
16
16
  /** Backend base URL. Falls back to production when nothing sets it. */
17
17
  httpUrl?: string;
18
- /** WebSocket base URL. Falls back to production when nothing sets it. */
19
- wsUrl?: string;
20
18
  /** `runtimeLog` threshold. Falls back to `info`. */
21
19
  logLevel?: ApiClientLogLevel;
22
20
  }
package/dist/config.js CHANGED
@@ -26,8 +26,6 @@ let version = 0;
26
26
  export function configureApiClient(next) {
27
27
  if (next.httpUrl !== undefined)
28
28
  config.httpUrl = next.httpUrl;
29
- if (next.wsUrl !== undefined)
30
- config.wsUrl = next.wsUrl;
31
29
  if (next.logLevel !== undefined)
32
30
  config.logLevel = next.logLevel;
33
31
  version += 1;
@@ -20,7 +20,7 @@ export interface ListArtifactsResult {
20
20
  }
21
21
  export interface WriteArtifactInput {
22
22
  text: string;
23
- content_type?: string;
23
+ contentType?: string;
24
24
  visibility?: ArtifactVisibility;
25
25
  /** When the artifact is associated with a chat. */
26
26
  chatId?: string;
@@ -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,22 @@ 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>;
163
+ /** ZIG-1262 — shared fetch → text → throwApiError → JSON parse for file rail. */
164
+ private _post;
165
+ private _get;
113
166
  private _attach;
114
167
  /**
115
168
  * 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 —
@@ -81,7 +92,7 @@ export class ArtifactsClient {
81
92
  return this.write({
82
93
  ...artifactScopeForSession(sessionId),
83
94
  text,
84
- content_type: 'thought',
95
+ contentType: 'thought',
85
96
  visibility: 'agent-private',
86
97
  idempotencyKey: opts.idempotencyKey,
87
98
  });
@@ -117,7 +128,7 @@ export class ArtifactsClient {
117
128
  headers: this._headers(),
118
129
  body: JSON.stringify({
119
130
  text: input.text.trim(),
120
- content_type: input.content_type ?? 'text',
131
+ contentType: input.contentType ?? 'text',
121
132
  visibility: input.visibility ?? 'chat',
122
133
  chatId: input.chatId,
123
134
  agreementId: input.agreementId,
@@ -159,7 +170,90 @@ export class ArtifactsClient {
159
170
  role,
160
171
  });
161
172
  }
162
- async _attach(path, body) {
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 parsed = await this._post('/artifacts/upload-url', {
198
+ filename: input.filename.trim(),
199
+ mime: input.mime.trim(),
200
+ byteSize: input.byteSize,
201
+ format: input.format,
202
+ visibility: input.visibility,
203
+ chatId: input.chatId,
204
+ agreementId: input.agreementId,
205
+ taskId: input.taskId,
206
+ service: input.service,
207
+ ...(input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {}),
208
+ }, 'artifact upload-url');
209
+ if (!parsed.artifactId || !parsed.uploadUrl) {
210
+ throw new Error('artifact upload-url response missing artifactId/uploadUrl');
211
+ }
212
+ if (bytes) {
213
+ const put = await fetch(parsed.uploadUrl, {
214
+ method: 'PUT',
215
+ headers: { 'Content-Type': input.mime.trim() },
216
+ // Blob/Uint8Array — Buffer is not always a DOM BodyInit under tsc DOM libs.
217
+ body: new Uint8Array(bytes),
218
+ });
219
+ if (!put.ok) {
220
+ const putBody = await put.text().catch(() => '');
221
+ throw new Error(`S3 PUT failed: ${put.status} ${put.statusText}${putBody ? ` — ${putBody.slice(0, 200)}` : ''}`);
222
+ }
223
+ const { createHash } = await import('node:crypto');
224
+ parsed.checksum = createHash('sha256').update(bytes).digest('hex');
225
+ parsed.putOk = true;
226
+ }
227
+ return parsed;
228
+ }
229
+ async completeFile(artifactId, checksum) {
230
+ if (!artifactId)
231
+ throw new Error('ArtifactsClient.completeFile: artifactId is required');
232
+ if (!/^[a-f0-9]{64}$/i.test(checksum)) {
233
+ throw new Error('ArtifactsClient.completeFile: checksum must be sha256 hex');
234
+ }
235
+ return this._post(`/artifacts/${encodeURIComponent(artifactId)}/complete`, { checksum: checksum.toLowerCase() }, 'artifact complete');
236
+ }
237
+ async download(artifactId) {
238
+ if (!artifactId)
239
+ throw new Error('ArtifactsClient.download: artifactId is required');
240
+ const parsed = await this._get(`/artifacts/${encodeURIComponent(artifactId)}/download`, 'artifact download');
241
+ if (!parsed.downloadUrl || !parsed.expiresAt || !parsed.filename) {
242
+ throw new Error('artifact download response incomplete');
243
+ }
244
+ return {
245
+ downloadUrl: parsed.downloadUrl,
246
+ expiresAt: parsed.expiresAt,
247
+ filename: parsed.filename,
248
+ };
249
+ }
250
+ async reExtract(artifactId) {
251
+ if (!artifactId)
252
+ throw new Error('ArtifactsClient.reExtract: artifactId is required');
253
+ return this._post(`/artifacts/${encodeURIComponent(artifactId)}/re-extract`, {}, 'artifact re-extract');
254
+ }
255
+ /** ZIG-1262 — shared fetch → text → throwApiError → JSON parse for file rail. */
256
+ async _post(path, body, label) {
163
257
  const res = await fetch(`${getBackendUrl()}${path}`, {
164
258
  method: 'POST',
165
259
  headers: this._headers(),
@@ -167,8 +261,22 @@ export class ArtifactsClient {
167
261
  });
168
262
  const text = await res.text().catch(() => '');
169
263
  if (!res.ok) {
170
- throwApiError(res, text, `attachArtifact failed: ${res.status} ${res.statusText}`);
264
+ throwApiError(res, text, `${label} failed: ${res.status} ${res.statusText}`);
265
+ }
266
+ return (text ? JSON.parse(text) : {});
267
+ }
268
+ async _get(path, label) {
269
+ const res = await fetch(`${getBackendUrl()}${path}`, {
270
+ headers: this._headers(),
271
+ });
272
+ const text = await res.text().catch(() => '');
273
+ if (!res.ok) {
274
+ throwApiError(res, text, `${label} failed: ${res.status} ${res.statusText}`);
171
275
  }
276
+ return (text ? JSON.parse(text) : {});
277
+ }
278
+ async _attach(path, body) {
279
+ await this._post(path, body, 'attachArtifact');
172
280
  return { success: true };
173
281
  }
174
282
  /**
@@ -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).
@@ -27,9 +27,30 @@ export type OrgResolution = {
27
27
  * match. Ambiguous names return the candidates rather than guessing.
28
28
  */
29
29
  export declare function resolveOrgSelector(orgs: MyOrg[], selector: string): OrgResolution;
30
+ /** MCP OAuth auto-provisioned delegate id prefix (ZIG-545 / ZIG-852). */
31
+ export declare const MCP_OAUTH_DELEGATE_AGENT_ID_PREFIX = "claude-delegate--";
32
+ /** True when `agentId` is an inbound MCP OAuth auto-provisioned delegate. */
33
+ export declare function isMcpOAuthDelegateAgentId(agentId: string): boolean;
30
34
  /**
31
35
  * ZIG-640 / ZIG-956 — runtime acting org from the server (self-hire / agent
32
36
  * row): GET /agents/claude-delegate/access. Moved here from ziggs-mcp's inline
33
37
  * fetch (ZIG-894 "one client for every surface").
38
+ *
39
+ * Only valid for MCP OAuth Claude-delegate sessions. Hosted / fleet
40
+ * impersonation must use {@link fetchHostedAgentAccess} (ZIG-1272).
34
41
  */
35
42
  export declare function fetchDelegateAccess(creds: Creds, baseUrl?: string): Promise<Record<string, unknown>>;
43
+ /**
44
+ * ZIG-1272 — session access for hosted / fleet impersonation (owner key +
45
+ * X-Agent-Id, or an agent-scoped key that is not a Claude OAuth delegate).
46
+ *
47
+ * `GET /agents/claude-delegate/access` refuses those credentials (or reports
48
+ * disconnected), even though every other tool works. The credential's tenant
49
+ * is already on the actor (`GET /orgs/me` → `activeOrgId`).
50
+ */
51
+ export declare function fetchHostedAgentAccess(creds: Creds, baseUrl?: string): Promise<Record<string, unknown>>;
52
+ /**
53
+ * ZIG-1272 — pick the access reader that matches the acting agent.
54
+ * Claude OAuth delegates → self-hire status; everything else → hosted path.
55
+ */
56
+ export declare function fetchSessionAccess(creds: Creds, baseUrl?: string): Promise<Record<string, unknown>>;
@@ -42,10 +42,19 @@ export function resolveOrgSelector(orgs, selector) {
42
42
  return { status: 'ambiguous', matches: byName };
43
43
  return { status: 'not-found' };
44
44
  }
45
+ /** MCP OAuth auto-provisioned delegate id prefix (ZIG-545 / ZIG-852). */
46
+ export const MCP_OAUTH_DELEGATE_AGENT_ID_PREFIX = 'claude-delegate--';
47
+ /** True when `agentId` is an inbound MCP OAuth auto-provisioned delegate. */
48
+ export function isMcpOAuthDelegateAgentId(agentId) {
49
+ return !!agentId && agentId.startsWith(MCP_OAUTH_DELEGATE_AGENT_ID_PREFIX);
50
+ }
45
51
  /**
46
52
  * ZIG-640 / ZIG-956 — runtime acting org from the server (self-hire / agent
47
53
  * row): GET /agents/claude-delegate/access. Moved here from ziggs-mcp's inline
48
54
  * fetch (ZIG-894 "one client for every surface").
55
+ *
56
+ * Only valid for MCP OAuth Claude-delegate sessions. Hosted / fleet
57
+ * impersonation must use {@link fetchHostedAgentAccess} (ZIG-1272).
49
58
  */
50
59
  export async function fetchDelegateAccess(creds, baseUrl) {
51
60
  const url = `${baseUrl || getBackendUrl()}/agents/claude-delegate/access`;
@@ -59,3 +68,55 @@ export async function fetchDelegateAccess(creds, baseUrl) {
59
68
  }
60
69
  return body ? JSON.parse(body) : {};
61
70
  }
71
+ /**
72
+ * ZIG-1272 — session access for hosted / fleet impersonation (owner key +
73
+ * X-Agent-Id, or an agent-scoped key that is not a Claude OAuth delegate).
74
+ *
75
+ * `GET /agents/claude-delegate/access` refuses those credentials (or reports
76
+ * disconnected), even though every other tool works. The credential's tenant
77
+ * is already on the actor (`GET /orgs/me` → `activeOrgId`).
78
+ */
79
+ export async function fetchHostedAgentAccess(creds, baseUrl) {
80
+ const url = `${baseUrl || getBackendUrl()}/orgs/me`;
81
+ const res = await fetch(url, {
82
+ method: 'GET',
83
+ headers: buildOperatorHeaders(creds.operatorKey, creds.agentId),
84
+ });
85
+ const body = await res.text().catch(() => '');
86
+ if (!res.ok) {
87
+ throwApiError(res, body, `GET /orgs/me failed: ${res.status}`);
88
+ }
89
+ const parsed = body
90
+ ? JSON.parse(body)
91
+ : {};
92
+ const activeOrgId = typeof parsed.activeOrgId === 'string' && parsed.activeOrgId
93
+ ? parsed.activeOrgId
94
+ : null;
95
+ const match = (parsed.orgs ?? []).find((o) => o.orgId === activeOrgId);
96
+ // Prefer `activeOrg` (ZIG-1272) — membership list often omits the agent home
97
+ // org under fleet impersonation.
98
+ const fromActive = parsed.activeOrg;
99
+ const orgName = (typeof fromActive?.name === 'string' && fromActive.name) ||
100
+ match?.name ||
101
+ null;
102
+ const orgKind = (typeof fromActive?.kind === 'string' && fromActive.kind) ||
103
+ match?.kind ||
104
+ null;
105
+ return {
106
+ connected: true,
107
+ orgId: activeOrgId,
108
+ orgName,
109
+ orgKind,
110
+ switchOrgHint: 'Hosted agent session — acting org is the agent home / key workspace, not MCP OAuth consent.',
111
+ };
112
+ }
113
+ /**
114
+ * ZIG-1272 — pick the access reader that matches the acting agent.
115
+ * Claude OAuth delegates → self-hire status; everything else → hosted path.
116
+ */
117
+ export async function fetchSessionAccess(creds, baseUrl) {
118
+ if (isMcpOAuthDelegateAgentId(creds.agentId)) {
119
+ return fetchDelegateAccess(creds, baseUrl);
120
+ }
121
+ return fetchHostedAgentAccess(creds, baseUrl);
122
+ }
@@ -38,9 +38,11 @@ export declare function getActiveTasksForChat(chatId: string, creds: Creds): Pro
38
38
  export declare function cancelTask(taskId: string, creds: Creds): Promise<Task>;
39
39
  export declare function getSubtasks(parentTaskId: string, creds: Creds): Promise<Task[]>;
40
40
  export interface PlanReplaceStep {
41
- stepId: string;
41
+ /** Omit to let the server mint `step-<n>` (ZIG-1268). */
42
+ stepId?: string;
42
43
  description: string;
43
- order: number;
44
+ /** Omit to use the array index (ZIG-1268). */
45
+ order?: number;
44
46
  }
45
47
  export declare function replaceTaskPlan(taskId: string, steps: PlanReplaceStep[], creds: Creds): Promise<Task>;
46
48
  export declare function updateTaskPlanStep(taskId: string, stepId: string, status: string, result: unknown, creds: Creds): Promise<Task>;
@@ -21,7 +21,7 @@ export { PaymentsClient } from './PaymentsClient.js';
21
21
  export type { PaymentsError, WalletBalance, WalletRef, PaymentTransactionView, TransferResult, HoldResult, ReleaseResult, PaymentGrantView, PaymentGrantEnvelope, RevokeGrantResult, PaymentApproval, WaitForApprovalResult, } from './PaymentsClient.js';
22
22
  export { ConnectionsClient, assertNoLeakedConnectionSecret, } from './ConnectionsClient.js';
23
23
  export type { ConnectionsError, ConnectionProxyParams, ConnectionGrant, ConnectionWithGrants, McpConnectionRequestParams, } from './ConnectionsClient.js';
24
- export { fetchMyOrgs, resolveOrgSelector, fetchDelegateAccess, } from './OrgsClient.js';
24
+ export { fetchMyOrgs, resolveOrgSelector, fetchDelegateAccess, fetchHostedAgentAccess, fetchSessionAccess, isMcpOAuthDelegateAgentId, MCP_OAUTH_DELEGATE_AGENT_ID_PREFIX, } from './OrgsClient.js';
25
25
  export type { MyOrg, OrgResolution } from './OrgsClient.js';
26
26
  export { AgentSearchClient } from './AgentSearchClient.js';
27
27
  export { TelemetryClient } from './TelemetryClient.js';
@@ -14,7 +14,7 @@ export { ContextGrantsClient } from './ContextGrantsClient.js';
14
14
  export { grantCaveat, CONTEXT_GRANT_SCOPE_KINDS, GRANT_SCOPE_KINDS, } from './grants.js';
15
15
  export { PaymentsClient } from './PaymentsClient.js';
16
16
  export { ConnectionsClient, assertNoLeakedConnectionSecret, } from './ConnectionsClient.js';
17
- export { fetchMyOrgs, resolveOrgSelector, fetchDelegateAccess, } from './OrgsClient.js';
17
+ export { fetchMyOrgs, resolveOrgSelector, fetchDelegateAccess, fetchHostedAgentAccess, fetchSessionAccess, isMcpOAuthDelegateAgentId, MCP_OAUTH_DELEGATE_AGENT_ID_PREFIX, } from './OrgsClient.js';
18
18
  export { AgentSearchClient } from './AgentSearchClient.js';
19
19
  export { TelemetryClient } from './TelemetryClient.js';
20
20
  export { InboxClient } from './InboxClient.js';
package/dist/index.d.ts CHANGED
@@ -4,7 +4,7 @@ export * from './relay/provisionRelayWorkers.js';
4
4
  export { ConnectionManager } from './ConnectionManager.js';
5
5
  export { EntryTypes, ContentTypes, OPEN_AGREEMENT_TARGET, ORG_AGREEMENT_TARGET, BROADCAST_TARGETS, isBroadcastTarget, isPersonaRef, isRoomPresentationRef, isOpaquePresentationRef, AGREEMENT_ENGAGEMENT_KIND, isValidContentType, } from './types.js';
6
6
  export type { PrincipalPresentation } from './types.js';
7
- export { getBackendUrl, getWebSocketUrl } from './utils/urlUtils.js';
7
+ export { getBackendUrl } from './utils/urlUtils.js';
8
8
  export { configureApiClient, apiClientConfig } from './config.js';
9
9
  export type { ApiClientConfig, ApiClientLogLevel } from './config.js';
10
10
  export { runtimeLog, resetRuntimeLogLevelCache } from './shared/runtimeLog.js';
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ export * from './capabilities/index.js';
3
3
  export * from './relay/provisionRelayWorkers.js';
4
4
  export { ConnectionManager } from './ConnectionManager.js';
5
5
  export { EntryTypes, ContentTypes, OPEN_AGREEMENT_TARGET, ORG_AGREEMENT_TARGET, BROADCAST_TARGETS, isBroadcastTarget, isPersonaRef, isRoomPresentationRef, isOpaquePresentationRef, AGREEMENT_ENGAGEMENT_KIND, isValidContentType, } from './types.js';
6
- export { getBackendUrl, getWebSocketUrl } from './utils/urlUtils.js';
6
+ export { getBackendUrl } from './utils/urlUtils.js';
7
7
  // ZIG-652: the host injects the environment; this package never reads it.
8
8
  export { configureApiClient, apiClientConfig } from './config.js';
9
9
  export { runtimeLog, resetRuntimeLogLevelCache } from './shared/runtimeLog.js';
package/dist/types.d.ts CHANGED
@@ -244,7 +244,7 @@ export interface MessageMetadata {
244
244
  */
245
245
  presentation?: PrincipalPresentation | null;
246
246
  entryType?: string;
247
- content_type?: string;
247
+ contentType?: string;
248
248
  taskId?: string | null;
249
249
  /**
250
250
  * Optional task / agreement snapshot pinned by the backend on
@@ -1,2 +1 @@
1
1
  export declare function getBackendUrl(): string;
2
- export declare function getWebSocketUrl(): string;
@@ -3,7 +3,3 @@ export function getBackendUrl() {
3
3
  const url = apiClientConfig().httpUrl || 'https://api.ziggsai.com';
4
4
  return url.startsWith('http') ? url : `https://${url}`;
5
5
  }
6
- export function getWebSocketUrl() {
7
- const wsUrl = apiClientConfig().wsUrl || 'wss://api.ziggsai.com';
8
- return wsUrl.startsWith('ws') ? wsUrl : `wss://${wsUrl}`;
9
- }
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.7",
4
4
  "description": "HTTP and WebSocket client for the Ziggs backend API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",