@ziggs-ai/api-client 0.24.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -51,19 +51,22 @@ export const openConversationCapability = {
51
51
  names: { sdk: 'chat_open', mcp: 'ziggs_chat_open' },
52
52
  title: 'Start or reuse a conversation',
53
53
  descriptions: {
54
- 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. This is also how you reach the HUMAN who hired you when the work needs an answer only they have: pass their user id (context_snapshot lists it under users; on your agreement they are the payer/creator party), then chat_send in the chat it returns with no receiverId. Opening a room is issuing the other side access to it, so it needs something that authorizes that: they publish a listing, you share an org, you hold a live agreement with them (a link counts, so link_propose is how you get one with a peer in another org), or they claimed your invite. Someone else\'s agent is not opened directly, inside your org or outside it: open the chat with the person it answers for (their user id); which of their agents answers is theirs to decide, and an agent of theirs already in a room with you can be written to there. With none of those the call is refused and the refusal names the levers. When the person you cannot reach is somebody YOUR OWN PERSON already knows — their teammate, their partner, their customer — none of those levers is the right one: ask the person you work for to open a room with them and add you, in plain words in your conversation with them, then stop until you are in it. Claiming a listing or sending an invite is for a stranger you are doing business with, and using it on somebody your person could introduce you to in two clicks costs them a negotiation instead. To list chats you can already read, use grant_list scopeKind=chat.',
55
- 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. Opening a room is issuing the other side access to it, so it needs something that authorizes that: they publish a listing, you share an org, you hold a live agreement with them (a link counts, so ziggs_link_propose is how you get one with a peer in another org), or they claimed your invite. Someone else\'s agent is not opened directly, inside your org or outside it: open the chat with the person it answers for (their user id); which of their agents answers is theirs to decide, and an agent of theirs already in a room with you can be written to there. With none of those the call is refused and names the levers. When the person you cannot reach is somebody your own person already knows — their teammate, their partner, their customer — ask that person to open a room with them and add you, rather than reaching for a listing or an invite: those are for strangers you are doing business with.',
54
+ sdk: 'Open a conversation or explicitly invite people to a room. participantId opens/reuses the acting agent’s pair conversation with one person or agent; newChat opens a separate subject. participantIds creates a separate group with the acting caller and those people; include a stable idempotencyKey for retries. chatId plus participantIds invites people into that exact room, requiring room access and admission authority. Every person needs a valid contact basis: shared organization membership, a link, or an applicable agreement. Each participant keeps their own agent and work relationships; opening a group selects no service agreement. Group creation never expands an existing private DM. Invited teammates read from admission onward. participantIds takes people only: another agent joins a room when a person adds it in the web app, where its approval rules apply, and your own missing seat is asked for with access_request. Reach another person through their user id, not an unpublished agent of theirs. If you cannot reach someone your person knows, ask your person to introduce you in a room. Opening does not send a message; chat_send must name its intended receiverId to guarantee a target. To list accessible rooms, use grant_list scopeKind=chat.',
55
+ mcp: 'Open a conversation or explicitly invite people to a room. participantId opens/reuses the acting agent’s pair conversation with one person or agent; newChat opens a separate subject. participantIds creates a separate group with the acting caller and those people; include a stable idempotencyKey for retries. chatId plus participantIds invites people into that exact room, requiring room access and admission authority. Every person needs a valid contact basis: shared organization membership, a link, or an applicable agreement. Each participant keeps their own agent and work relationships; opening a group selects no service agreement. Group creation never expands an existing private DM. Invited teammates read from admission onward. participantIds takes people only: another agent joins a room when a person adds it in the web app, where its approval rules apply, and your own missing seat is asked for with ziggs_access_request. Reach another person through their user id, not an unpublished agent of theirs. For missing access to a known room, use ziggs_access_request; opening another room does not recover its history. Opening does not send a message; ziggs_chat_send must name its intended receiverId to guarantee a target.',
56
56
  },
57
57
  annotation: 'write',
58
58
  params: {
59
59
  participantId: {
60
60
  type: 'string',
61
- required: true,
62
61
  description: 'Known user or agent id from search, a directory, a chat, or an agreement. For a teammate or a linked person, their user id, not an agent of theirs. No existing chat is required; do not guess ids.',
63
62
  },
63
+ participantIds: { type: 'array', items: { type: 'string' }, description: '1–20 known user ids. Creates a separate group, or invites these people to chatId. Use instead of participantId. Each invitation is authorized separately.' },
64
+ chatId: { type: 'string', description: 'Existing room to invite participantIds into. Never creates another room. Omit creation options and agreementId.' },
65
+ idempotencyKey: { type: 'string', description: 'Required for group creation: a unique key for this conversation, reused unchanged on retries. A different participant set needs a new key.' },
66
+ createIfMissing: { type: 'boolean', description: 'Set false to reuse only an existing room for this acting agent and participant. A missing room is refused without creating a replacement.' },
64
67
  newChat: {
65
68
  type: 'boolean',
66
- description: 'Open a separate chat even though one is already open with this participant. ' +
69
+ description: 'Open a separate chat even though one is already open with the named participants. ' +
67
70
  'For when the conversation is genuinely its own subject and would confuse an ' +
68
71
  'existing thread. The separate chat does not become the main one, so a later ' +
69
72
  'call without this still returns the original. Leave unset to continue where ' +
@@ -77,14 +80,31 @@ export const openConversationCapability = {
77
80
  },
78
81
  needsAgentId: true,
79
82
  handler: async (args, env) => {
80
- if (!args['participantId'])
81
- throw new Error('participantId is required');
82
- const { chatId, reused } = await openConversation(args['participantId'], fullCreds(env), {
83
- newChat: args['newChat'] === true,
84
- ...(typeof args['agreementId'] === 'string' && args['agreementId']
85
- ? { agreementId: args['agreementId'] }
86
- : {}),
87
- });
83
+ if (!args.participantId && !Array.isArray(args.participantIds)) {
84
+ throw new Error('participantId is required, or use participantIds to invite people');
85
+ }
86
+ if (args.participantIds !== undefined) {
87
+ if (args.participantId !== undefined || !Array.isArray(args.participantIds) ||
88
+ args.participantIds.length < 1 || args.participantIds.length > 20 ||
89
+ args.participantIds.some(id => typeof id !== 'string' || !id.trim())) {
90
+ throw new Error('Use participantIds containing 1–20 people, without participantId');
91
+ }
92
+ if (!args.chatId && (typeof args.idempotencyKey !== 'string' || !args.idempotencyKey.trim())) {
93
+ throw new Error('Group creation requires an idempotencyKey; reuse it when retrying');
94
+ }
95
+ }
96
+ else if (args.chatId || args.idempotencyKey) {
97
+ throw new Error('chatId and idempotencyKey require participantIds');
98
+ }
99
+ const { chatId, reused } = await openConversation({
100
+ ...(typeof args.participantId === 'string' ? { participantId: args.participantId } : {}),
101
+ ...(Array.isArray(args.participantIds) ? { participantIds: args.participantIds } : {}),
102
+ ...(typeof args.chatId === 'string' ? { chatId: args.chatId } : {}),
103
+ ...(typeof args.idempotencyKey === 'string' ? { idempotencyKey: args.idempotencyKey } : {}),
104
+ ...(args.newChat === true ? { newChat: true } : {}),
105
+ ...(typeof args.createIfMissing === 'boolean' ? { createIfMissing: args.createIfMissing } : {}),
106
+ ...(typeof args.agreementId === 'string' && args.agreementId ? { agreementId: args.agreementId } : {}),
107
+ }, fullCreds(env));
88
108
  const lister = env.surface === 'mcp' ? 'ziggs_grant_list' : 'grant_list';
89
109
  const grantNote = `Use ${lister} scopeKind=chat to inspect your room grants. Opening a conversation does not send a message or confirm that anyone has read it.`;
90
110
  // word the note from the outcome instead of covering both cases.
@@ -92,13 +112,16 @@ export const openConversationCapability = {
92
112
  // withheld it — worse than silence, because the agent cannot even tell
93
113
  // there is something to look up. A backend too old to report it keeps the
94
114
  // old both-cases wording, and `reused` is simply absent from the result.
95
- const outcomeNote = reused === true
96
- ? 'Reused the conversation you already had with this participant, so it may already hold history — read it before you speak.'
97
- : reused === false
98
- ? 'Created a new conversation with this participant, so there is no history to catch up on.'
99
- : 'Conversation is open (or reused).';
115
+ const outcomeNote = args.chatId
116
+ ? 'The named people can participate in this room. Their history access remains bounded by their grants.'
117
+ : reused === true
118
+ ? 'Reused the conversation you already had with the named participants, so it may already hold history — read it before you speak.'
119
+ : reused === false
120
+ ? 'Created a new conversation with the named participants, so there is no history to catch up on.'
121
+ : 'Conversation is open (or reused).';
100
122
  return {
101
123
  chatId,
124
+ actingAgentId: fullCreds(env).agentId,
102
125
  ...(typeof reused === 'boolean' ? { reused } : {}),
103
126
  note: `${outcomeNote} ${grantNote}`,
104
127
  };
@@ -22,5 +22,6 @@ export declare const contextDiscoverGrantableCapability: CapabilityDefinition;
22
22
  export declare const contextDelegateCapability: CapabilityDefinition;
23
23
  export declare const openCapability: CapabilityDefinition;
24
24
  export declare const accessExplainCapability: CapabilityDefinition;
25
- export declare const CONTEXT_CAPABILITIES: CapabilityDefinition[];
26
25
  export declare const contextRequestCapability: CapabilityDefinition;
26
+ export declare const accessRequestCapability: CapabilityDefinition;
27
+ export declare const CONTEXT_CAPABILITIES: CapabilityDefinition[];
@@ -160,8 +160,8 @@ export const contextDiscoverGrantableCapability = {
160
160
  names: { sdk: 'context_discover_grantable', mcp: 'ziggs_context_discover_grantable' },
161
161
  title: 'Find context you cannot read yet',
162
162
  descriptions: {
163
- 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).',
164
- 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.',
163
+ 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 context your current read access covers. For a missing chat or agreement use access_request with its scopeRef; for a connection ask your human to grant it, 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).',
164
+ 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 a chat or agreement, create a pending request with ziggs_access_request; for a connection 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.',
165
165
  },
166
166
  annotation: 'read-only',
167
167
  params: {},
@@ -169,7 +169,9 @@ export const contextDiscoverGrantableCapability = {
169
169
  handler: async (_args, env) => {
170
170
  const creds = fullCreds(env);
171
171
  const items = await new ContextDiscoveryClient(creds.operatorKey, creds.agentId).discoverGrantable();
172
- return { count: items.length, items };
172
+ return { count: items.length, items: items.map((item) => ({ ...item,
173
+ actions: item.type === 'connection' ? [] : [requestAccessAction({ scopeKind: item.type, scopeId: item.scopeRef.id }, env)],
174
+ })) };
173
175
  },
174
176
  };
175
177
  export const contextDelegateCapability = {
@@ -289,15 +291,25 @@ function openBodyFromArgs(args) {
289
291
  return body;
290
292
  }
291
293
  function presentOpenResult(result, env) {
292
- // Never attach a ready write. An unreadable row already carries
293
- // access.continuation (owner-decision); this call must not execute it.
294
- const requestTool = env.surface === 'mcp' ? 'ziggs_context_request' : 'context_request';
294
+ // Expose a request-only next action, never execute it as part of a read.
295
+ const requestTool = env.surface === 'mcp' ? 'ziggs_access_request' : 'access_request';
295
296
  const chatTool = env.surface === 'mcp' ? 'ziggs_chat_open' : 'chat_open';
297
+ const resumeActions = (result.outcome === 'ok' && result.ref.kind === 'agreement'
298
+ ? result.page?.items ?? [] : []).flatMap((item) => {
299
+ if (!item || typeof item !== 'object')
300
+ return [];
301
+ const row = item;
302
+ const ask = row.grantRequest;
303
+ if (row.status !== 'active' || ask?.primitive !== 'context' || ask.kind !== 'access-request' || ask.holderId !== fullCreds(env).agentId || !ask.scope?.id || !['artifact', 'chat', 'agreement', 'task'].includes(ask.scope.kind ?? ''))
304
+ return [];
305
+ return [{ kind: 'resume', tool: env.surface === 'mcp' ? 'ziggs_open' : 'open', args: { [`${ask.scope.kind}Id`]: ask.scope.id }, effect: 'read_only', note: 'Approval recorded. Open the original scope; the server rechecks expiry and revocation.' }];
306
+ });
296
307
  return {
297
308
  ...result,
298
- actions: [],
309
+ actions: result.access.continuation?.request ? [requestAccessAction(result.access.continuation.request, env)] : result.ref.kind === 'artifact' && result.outcome === 'unreadable' ? [requestAccessAction({ scopeKind: 'artifact', scopeId: result.ref.id, permission: 'read', durationHours: 24 }, env)] : resumeActions,
310
+ actingAgentId: fullCreds(env).agentId,
299
311
  ...(result.ref.kind === 'artifact' && result.outcome !== 'ok' ? {
300
- requestAccessHint: `If a source you already read supplied this artifact reference and its human owner, ${requestTool} can ask that owner for temporary read access without a prior grant. It needs a conversation with the owner; ${chatTool} can open or reuse one, subject to authorization. Discover the request tool's schema before calling it. This response does not confirm existence or ownership. A request grants no access until the human owner approves.`,
312
+ requestAccessHint: `If a source you already read supplied this artifact reference and its human owner, ${requestTool} scopeKind=artifact can ask that owner for temporary read access without a prior grant. It needs a conversation with the owner; ${chatTool} can open or reuse one, subject to authorization. Discover the request tool's schema before calling it. This response does not confirm existence or ownership. A request grants no access until the human owner approves.`,
301
313
  } : {}),
302
314
  };
303
315
  }
@@ -342,14 +354,6 @@ export const accessExplainCapability = {
342
354
  return presentOpenResult(result, env);
343
355
  },
344
356
  };
345
- export const CONTEXT_CAPABILITIES = [
346
- contextReadCapability,
347
- openCapability,
348
- accessExplainCapability,
349
- contextDelegateCapability,
350
- contextExpandReachCapability,
351
- contextDiscoverGrantableCapability,
352
- ];
353
357
  export const contextRequestCapability = {
354
358
  key: 'context_request',
355
359
  names: { sdk: 'context_request', mcp: 'ziggs_context_request' },
@@ -381,3 +385,73 @@ export const contextRequestCapability = {
381
385
  return { ...result, outcome: 'pending', state: 'requested', appUrl: `${(env.webUrl ?? 'https://ziggsai.com').replace(/\/$/, '')}/app/agreements/${encodeURIComponent(result.agreementId)}`, note: 'Waiting for the artifact owner. No access granted yet; open the original artifact only after approval.' };
382
386
  },
383
387
  };
388
+ /** A suggested request still needs a purpose; it is never a grant or an approval. */
389
+ function requestAccessAction(args, env) {
390
+ return {
391
+ kind: 'request_access',
392
+ tool: env.surface === 'mcp' ? 'ziggs_access_request' : 'access_request',
393
+ args, requiredInput: args.scopeKind === 'artifact' ? ['reason', 'chatId', 'ownerId'] : ['reason'], effect: 'request_only',
394
+ requiresApproval: true,
395
+ };
396
+ }
397
+ export const accessRequestCapability = {
398
+ key: 'access_request',
399
+ names: { sdk: 'access_request', mcp: 'ziggs_access_request' },
400
+ title: 'Request access for yourself',
401
+ descriptions: {
402
+ sdk: 'Create a pending approval agreement for the calling agent to access one artifact, chat, agreement context, or task branch for 1–168 hours. This only requests permission: it never grants access, signs, or approves. After the authorized person approves, open the original resource. Artifact requests need a writable conversation with the named owner; other requests go to your accountable person, who must have authority over the scope. Read-only by default; permission=reply requests read and reply to one chat. No spending or work commitment. Reuse idempotencyKey for a retry.',
403
+ mcp: 'Ask for missing access through a pending approval agreement. Use this when open/access_explain reports an unreadable resource, or discovery/inbox names a scope you need. It requests access ONLY for this MCP agent; never grants access or approves anything. Scope is one artifact, chat, agreement context, or task branch (including subtasks), for 1–168 hours (default 24). Read-only by default; permission=reply requests read and reply in one chat. For earlier chat messages explicitly set temporal=from-start. Artifact requests require chatId of your existing writable conversation with the owner and that ownerId; other requests route to your accountable person, who must be allowed to share the resource. Surface appUrl, wait for the decision when asked, then open the original resource and resume. Pending grants no access. Never use issue_grant, change identity, create another room, or form a generic work agreement to recover missing access.',
404
+ },
405
+ annotation: 'write', needsAgentId: true,
406
+ params: {
407
+ scopeKind: { type: 'string', required: true, enum: ['artifact', 'chat', 'agreement', 'task'] },
408
+ scopeId: { type: 'string', required: true, description: 'Original returned resource id; this request does not open a new resource' },
409
+ reason: { type: 'string', required: true, description: 'Why access is needed and who will receive the reply or summary' },
410
+ permission: { type: 'string', enum: ['read', 'reply'], description: 'read by default; reply is read and reply and is only valid for a chat' },
411
+ temporal: { type: 'string', enum: ['from-now', 'from-start'], description: 'Chat history requested; default from-now. Earlier messages need from-start. Other resources use from-start.' },
412
+ durationHours: { type: 'number', description: 'Integer 1–168 hours after approval; default 24' },
413
+ chatId: { type: 'string', description: 'Artifact requests only: existing conversation with the owner' },
414
+ ownerId: { type: 'string', description: 'Artifact requests only: human account or participant reference in that conversation' },
415
+ idempotencyKey: { type: 'string', description: 'Reuse for the same logical request' },
416
+ },
417
+ handler: async (args, env) => {
418
+ for (const key of ['scopeKind', 'scopeId', 'reason']) {
419
+ if (typeof args[key] !== 'string' || !String(args[key]).trim())
420
+ throw new Error(`${key} is required`);
421
+ }
422
+ const scopeKind = args.scopeKind;
423
+ if (!['artifact', 'chat', 'agreement', 'task'].includes(scopeKind))
424
+ throw new Error('Unsupported scopeKind');
425
+ const permission = args.permission ?? 'read';
426
+ if (!['read', 'reply'].includes(String(permission)) || (permission === 'reply' && scopeKind !== 'chat'))
427
+ throw new Error('Only chat access may request reply permission');
428
+ if (args.temporal !== undefined && (!['from-now', 'from-start'].includes(String(args.temporal)) || (scopeKind !== 'chat' && args.temporal === 'from-now')))
429
+ throw new Error('Only chats support from-now access');
430
+ const durationHours = args.durationHours ?? 24;
431
+ if (typeof durationHours !== 'number' || !Number.isInteger(durationHours) || durationHours < 1 || durationHours > 168)
432
+ throw new Error('durationHours must be an integer between 1 and 168');
433
+ const creds = fullCreds(env);
434
+ const result = await new ContextGrantsClient(creds.operatorKey, creds.agentId, env.baseUrl).requestAccess({
435
+ scopeKind, scopeId: String(args.scopeId).trim(), reason: String(args.reason).trim(),
436
+ permission: permission, temporal: args.temporal,
437
+ durationHours, chatId: args.chatId, ownerId: args.ownerId,
438
+ }, { idempotencyKey: args.idempotencyKey, laneId: creds.laneId });
439
+ return {
440
+ ...result, outcome: 'pending', state: 'requested', actingAgentId: creds.agentId,
441
+ scope: { kind: scopeKind, id: args.scopeId },
442
+ appUrl: `${(env.webUrl ?? 'https://ziggsai.com').replace(/\/$/, '')}/app/agreements/${encodeURIComponent(result.agreementId)}`,
443
+ resumeAfterApproval: { tool: env.surface === 'mcp' ? 'ziggs_open' : 'open', args: { [`${scopeKind}Id`]: args.scopeId } },
444
+ note: 'No access granted. The authorized person must approve; then open the original resource using the same agent identity.',
445
+ };
446
+ },
447
+ };
448
+ export const CONTEXT_CAPABILITIES = [
449
+ contextReadCapability,
450
+ openCapability,
451
+ accessExplainCapability,
452
+ accessRequestCapability,
453
+ contextRequestCapability,
454
+ contextDelegateCapability,
455
+ contextExpandReachCapability,
456
+ contextDiscoverGrantableCapability,
457
+ ];
@@ -11,7 +11,7 @@ export { TASK_CAPABILITIES, listTasksCapability, cancelTaskCapability, presentTa
11
11
  export { AGREEMENT_PROPOSE_PROVIDER_ID_DESCRIPTION } from './proposeProviderId.js';
12
12
  export { MARKETPLACE_CAPABILITIES, marketplaceViewCapability } from './marketplace.js';
13
13
  export { GRANTS_CAPABILITIES, listGrantsCapability } from './grants.js';
14
- export { CONTEXT_CAPABILITIES, contextReadCapability, openCapability, accessExplainCapability, contextDelegateCapability, contextRequestCapability, contextExpandReachCapability, contextDiscoverGrantableCapability, contextBounds, resolveOrgScopeId, } from './context.js';
14
+ export { CONTEXT_CAPABILITIES, contextReadCapability, openCapability, accessExplainCapability, contextDelegateCapability, contextRequestCapability, accessRequestCapability, contextExpandReachCapability, contextDiscoverGrantableCapability, contextBounds, resolveOrgScopeId, } from './context.js';
15
15
  export { CONNECTION_CAPABILITIES, connectionProxyCapability, requestConnectionCapability, } from './connections.js';
16
16
  export { DISCOVERY_CAPABILITIES, agentSearchCapability, agentGetCapability, } from './discovery.js';
17
17
  export { ARTIFACT_CAPABILITIES, recordArtifactCapability, listArtifactsCapability, findArtifactsCapability, type UnsearchedContext, shareArtifactCapability, attachArtifactCapability, uploadArtifactUrlCapability, completeArtifactFileCapability, downloadArtifactCapability, reextractArtifactCapability, } from './artifacts.js';
@@ -11,7 +11,7 @@ export { TASK_CAPABILITIES, listTasksCapability, cancelTaskCapability, presentTa
11
11
  export { AGREEMENT_PROPOSE_PROVIDER_ID_DESCRIPTION } from './proposeProviderId.js';
12
12
  export { MARKETPLACE_CAPABILITIES, marketplaceViewCapability } from './marketplace.js';
13
13
  export { GRANTS_CAPABILITIES, listGrantsCapability } from './grants.js';
14
- export { CONTEXT_CAPABILITIES, contextReadCapability, openCapability, accessExplainCapability, contextDelegateCapability, contextRequestCapability, contextExpandReachCapability, contextDiscoverGrantableCapability, contextBounds, resolveOrgScopeId, } from './context.js';
14
+ export { CONTEXT_CAPABILITIES, contextReadCapability, openCapability, accessExplainCapability, contextDelegateCapability, contextRequestCapability, accessRequestCapability, contextExpandReachCapability, contextDiscoverGrantableCapability, contextBounds, resolveOrgScopeId, } from './context.js';
15
15
  export { CONNECTION_CAPABILITIES, connectionProxyCapability, requestConnectionCapability, } from './connections.js';
16
16
  export { DISCOVERY_CAPABILITIES, agentSearchCapability, agentGetCapability, } from './discovery.js';
17
17
  export { ARTIFACT_CAPABILITIES, recordArtifactCapability, listArtifactsCapability, findArtifactsCapability, shareArtifactCapability, attachArtifactCapability, uploadArtifactUrlCapability, completeArtifactFileCapability, downloadArtifactCapability, reextractArtifactCapability, } from './artifacts.js';
@@ -216,6 +216,11 @@ export function nextCall(env, capabilityKey, args, why, opts) {
216
216
  for (const [key, value] of Object.entries(args ?? {})) {
217
217
  if (!isFilled(value))
218
218
  continue;
219
+ if (key === 'participantIds' && Array.isArray(value) && value.some(isPersonaFace)) {
220
+ missing.push(key);
221
+ strippedFaces.push(key);
222
+ continue;
223
+ }
219
224
  if (ADDRESS_FIELDS.includes(key) &&
220
225
  isPersonaFace(value)) {
221
226
  missing.push(key);
@@ -225,11 +230,20 @@ export function nextCall(env, capabilityKey, args, why, opts) {
225
230
  filled[key] = value;
226
231
  }
227
232
  for (const name of requiredArgNames(capabilityKey, opts?.definition)) {
233
+ if (capabilityKey === 'chat_open' && name === 'participantId' && Array.isArray(filled.participantIds) && filled.participantIds.length > 0)
234
+ continue;
228
235
  if (!isFilled(filled[name])) {
229
236
  if (!missing.includes(name))
230
237
  missing.push(name);
231
238
  }
232
239
  }
240
+ if (capabilityKey === 'chat_open' && (!opts?.definition || 'participantIds' in opts.definition.params)) {
241
+ if (!filled.participantId && !(Array.isArray(filled.participantIds) && filled.participantIds.length > 0) && !missing.includes('participantId')) {
242
+ missing.push('participantId');
243
+ }
244
+ if (Array.isArray(filled.participantIds) && !filled.chatId && !filled.idempotencyKey)
245
+ missing.push('idempotencyKey');
246
+ }
233
247
  const hold = opts?.hold;
234
248
  const ready = missing.length === 0 && !hold;
235
249
  const outcome = opts?.outcome ??
@@ -13,9 +13,19 @@ import { type Creds } from '../types.js';
13
13
  * with without a second call.
14
14
  */
15
15
  export type ChatSummary = ChatReadDto;
16
- export declare function openConversation(participantId: string, creds: Creds, { newChat, agreementId }?: {
16
+ export interface OpenConversationInput {
17
+ participantId?: string;
18
+ participantIds?: string[];
19
+ chatId?: string;
20
+ idempotencyKey?: string;
21
+ newChat?: boolean;
22
+ agreementId?: string;
23
+ createIfMissing?: boolean;
24
+ }
25
+ export declare function openConversation(participant: string | OpenConversationInput, creds: Creds, { newChat, agreementId, createIfMissing }?: {
17
26
  newChat?: boolean;
18
27
  agreementId?: string;
28
+ createIfMissing?: boolean;
19
29
  }): Promise<{
20
30
  chatId: string;
21
31
  reused?: boolean;
@@ -33,10 +43,10 @@ export interface SendChatMessageInput {
33
43
  */
34
44
  to?: string;
35
45
  /**
36
- * Recipient id. Optional: when omitted, the backend infers the
37
- * receiver if the chat has exactly one other member (one agent, or one
38
- * other human). Provide it explicitly in chats with multiple members.
39
- * Sent on the wire as `{ id }`; the SDK still takes a string.
46
+ * Recipient id. Optional: when omitted, the one other participant in the
47
+ * room is the receiver; with several, the backend infers one when Jev is
48
+ * confident. Otherwise it stores room context without a specific wake.
49
+ * Explicit addressing is required for a guaranteed target.
40
50
  */
41
51
  receiverId?: string;
42
52
  text: string;
@@ -47,7 +57,6 @@ export interface SendChatMessageInput {
47
57
  /** Governing service agreement; independent of representation. */
48
58
  conversationAgreementId?: string;
49
59
  taskId?: string;
50
- replyToMessageId?: string;
51
60
  }
52
61
  /**
53
62
  * Where the server says a send landed.
@@ -119,9 +128,10 @@ export declare class ChatClient {
119
128
  * @param agentId Required for fleet keys (sent as X-Agent-Id). Omit for agent-scoped keys.
120
129
  */
121
130
  constructor(operatorKey: string, agentId?: string);
122
- open(participantId: string, opts?: {
131
+ open(participant: string | OpenConversationInput, opts?: {
123
132
  newChat?: boolean;
124
133
  agreementId?: string;
134
+ createIfMissing?: boolean;
125
135
  }): Promise<{
126
136
  chatId: string;
127
137
  reused?: boolean;
@@ -10,20 +10,20 @@ function assertCreds(creds, op) {
10
10
  if (!creds?.agentId)
11
11
  throw new Error(`agentId is required for ${op}`);
12
12
  }
13
- export async function openConversation(participantId, creds, { newChat = false, agreementId } = {}) {
14
- if (!participantId)
15
- throw new Error('participantId is required for openConversation');
13
+ export async function openConversation(participant, creds, { newChat = false, agreementId, createIfMissing } = {}) {
14
+ const input = typeof participant === 'string'
15
+ ? { participantId: participant, ...(newChat ? { newChat } : {}), ...(agreementId ? { agreementId } : {}), ...(createIfMissing !== undefined ? { createIfMissing } : {}) }
16
+ : participant;
17
+ if (!input || (!input.participantId && !input.participantIds?.length)) {
18
+ throw new Error('participantId is required, or use participantIds to invite people');
19
+ }
16
20
  assertCreds(creds, 'open conversation');
17
21
  const res = await fetch(`${getBackendUrl()}/chats`, {
18
22
  method: 'POST',
19
23
  headers: buildHeaders(creds),
20
24
  // Only sent when asked for: an older backend ignores the field, so a
21
25
  // caller that never wants a separate room behaves identically either way.
22
- body: JSON.stringify({
23
- participantId,
24
- ...(newChat ? { newChat: true } : {}),
25
- ...(agreementId ? { agreementId } : {}),
26
- }),
26
+ body: JSON.stringify(input),
27
27
  });
28
28
  if (!res.ok) {
29
29
  const body = await res.text().catch(() => '');
@@ -107,7 +107,6 @@ export async function sendChatMessage(input, creds) {
107
107
  underAgreementId: input.underAgreementId,
108
108
  conversationAgreementId: input.conversationAgreementId,
109
109
  taskId: input.taskId,
110
- replyToMessageId: input.replyToMessageId,
111
110
  }),
112
111
  });
113
112
  if (!res.ok) {
@@ -166,7 +165,7 @@ export class ChatClient {
166
165
  // standalone functions still assert it per call.
167
166
  this.creds = { operatorKey, agentId };
168
167
  }
169
- open(participantId, opts) { return openConversation(participantId, this.creds, opts); }
168
+ open(participant, opts) { return openConversation(participant, this.creds, opts); }
170
169
  addMember(input) { return addChatMember(input, this.creds); }
171
170
  sendMessage(input) { return sendChatMessage(input, this.creds); }
172
171
  listMine() { return listMyChats(this.creds); }
@@ -96,6 +96,24 @@ export declare class ContextGrantsClient {
96
96
  */
97
97
  constructor(operatorKey: string, agentId?: string, baseUrl?: string);
98
98
  issueGrant(input: IssueContextGrantInput): Promise<GrantView>;
99
+ /** Request-only endpoint: a successful call must still have granted no access. */
100
+ requestAccess(input: {
101
+ scopeKind: 'artifact' | 'chat' | 'agreement' | 'task';
102
+ scopeId: string;
103
+ reason: string;
104
+ permission?: 'read' | 'reply';
105
+ temporal?: 'from-now' | 'from-start';
106
+ durationHours?: number;
107
+ chatId?: string;
108
+ ownerId?: string;
109
+ }, opts?: {
110
+ idempotencyKey?: string;
111
+ laneId?: string;
112
+ }): Promise<{
113
+ status: 'pending_approval';
114
+ agreementId: string;
115
+ accessGranted: false;
116
+ }>;
99
117
  requestArtifactAccess(artifactId: string, input: {
100
118
  chatId: string;
101
119
  ownerId: string;
@@ -63,6 +63,25 @@ export class ContextGrantsClient {
63
63
  }
64
64
  return parsed.grant;
65
65
  }
66
+ /** Request-only endpoint: a successful call must still have granted no access. */
67
+ async requestAccess(input, opts) {
68
+ const res = await fetch(`${this.baseUrl}/context/access/requests`, {
69
+ method: 'POST',
70
+ headers: buildOperatorHeaders(this.operatorKey, this.agentId, {
71
+ 'content-type': 'application/json',
72
+ ...(opts?.idempotencyKey ? { 'Idempotency-Key': opts.idempotencyKey } : {}),
73
+ }, opts?.laneId),
74
+ body: JSON.stringify(input),
75
+ });
76
+ const body = await res.text().catch(() => '');
77
+ if (!res.ok)
78
+ throwApiError(res, body, `requestAccess failed: ${res.status}`);
79
+ const result = JSON.parse(body);
80
+ if (result.status !== 'pending_approval' || !result.agreementId || result.accessGranted !== false) {
81
+ throw new Error('Expected a pending access request with accessGranted=false');
82
+ }
83
+ return { status: 'pending_approval', agreementId: result.agreementId, accessGranted: false };
84
+ }
66
85
  async requestArtifactAccess(artifactId, input, opts) {
67
86
  const res = await fetch(`${this.baseUrl}/context/artifacts/${encodeURIComponent(artifactId)}/access-requests`, {
68
87
  method: 'POST',
@@ -32,6 +32,13 @@ export interface ContextOpenAccess {
32
32
  continuation?: {
33
33
  kind: 'owner_decision';
34
34
  summary: string;
35
+ request?: {
36
+ scopeKind: 'chat' | 'agreement' | 'task';
37
+ scopeId: string;
38
+ permission: 'read' | 'reply';
39
+ temporal: 'from-now' | 'from-start';
40
+ durationHours: number;
41
+ };
35
42
  };
36
43
  }
37
44
  export interface ContextOpenResult {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ziggs-ai/api-client",
3
- "version": "0.24.0",
3
+ "version": "0.26.0",
4
4
  "description": "HTTP and WebSocket client for the Ziggs backend API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",