@ziggs-ai/api-client 0.21.0 → 0.22.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.
@@ -86,7 +86,7 @@ export const openConversationCapability = {
86
86
  : {}),
87
87
  });
88
88
  const lister = env.surface === 'mcp' ? 'ziggs_grant_list' : 'grant_list';
89
- const grantNote = `Opening the room issued you a grant on it, so the chat also appears in ${lister} scopeKind=chat for context reads.`;
89
+ 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
90
  // word the note from the outcome instead of covering both cases.
91
91
  // "Open (or reused)" told the agent the distinction existed and then
92
92
  // withheld it — worse than silence, because the agent cannot even tell
@@ -83,8 +83,8 @@ export const contextReadCapability = {
83
83
  after: { type: 'string', description: 'ISO timestamp for forward-delta (messages/artifacts)' },
84
84
  direction: {
85
85
  type: 'string',
86
- enum: ['forward'],
87
- description: 'Use "forward" with after for a message forward-delta',
86
+ enum: ['forward', 'backward'],
87
+ description: 'Use "forward" with after for a message forward-delta; "backward" starts a newest-first artifact inventory',
88
88
  },
89
89
  limit: { type: 'number', description: 'Page size (server default when omitted)' },
90
90
  state: { type: 'string', description: 'Task state filter (tasks only)' },
@@ -114,8 +114,8 @@ export const contextReadCapability = {
114
114
  throw new Error(`${type} reads accept via=${viaHint(type)} — not ${parsed.kind}:<id>`);
115
115
  }
116
116
  const direction = args['direction'];
117
- if (direction !== undefined && direction !== 'forward') {
118
- throw new Error('direction must be "forward"');
117
+ if (direction !== undefined && direction !== 'forward' && direction !== 'backward') {
118
+ throw new Error('direction must be "forward" or "backward"');
119
119
  }
120
120
  return new ContextReadClient(creds.operatorKey, creds.agentId, undefined, creds.laneId).read(type, {
121
121
  via,
@@ -6,7 +6,7 @@ export const agentSearchCapability = {
6
6
  title: 'Search for agents',
7
7
  descriptions: {
8
8
  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 your own org-mates. A link does NOT make the other person\'s agents findable — how they staff their side is theirs, and it changes without telling anyone; reach a linked person through the link itself (link_list gives their addressable peer id, then chat_open). Passing an EXACT agent id resolves that one agent even if unpublished/private, when you can reach it. 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.',
9
- mcp: 'Find agents (AgentSearchClient). A keyword/natural-language query searches the published store AND your own org-mates — so you can find a teammate\'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. A link does NOT make the other person\'s agents findable: a link makes the PERSON addressable, never their staffing list. To reach someone you are linked with, take their peer id from ziggs_link_list and ziggs_chat_open with that; their agents become known to you by turning up in rooms. Passing an EXACT agent id resolves that one agent even if unpublished/private, when you can reach it — an id you cannot reach comes back `reachability: "restricted"` with no name/profile, and a linked person\'s unpublished agent is one of those. 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`, `managed`, or `engaged`; it is not a blanket "published" label. Use returned agentId in grant/issue tools — do not guess ids.',
9
+ mcp: 'Find agents (AgentSearchClient). A keyword/natural-language query searches the published store AND your own org-mates — so you can find a teammate\'s assistant by the teammate\'s name or by its own name and ziggs_chat_open with it directly, even if it is unpublished/offline and has never been in a chat with you; a same-org hit carries `person` (the teammate\'s userId and name), and ziggs_chat_open with that userId reaches the person themselves. A link does NOT make the other person\'s agents findable: a link makes the PERSON addressable, never their staffing list. To reach someone you are linked with, take their peer id from ziggs_link_list and ziggs_chat_open with that; their agents become known to you by turning up in rooms. Passing an EXACT agent id resolves that one agent even if unpublished/private, when you can reach it — an id you cannot reach comes back `reachability: "restricted"` with no name/profile, and a linked person\'s unpublished agent is one of those. 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`, `managed`, or `engaged`; it is not a blanket "published" label. Use returned agentId in grant/issue tools — do not guess ids.',
10
10
  },
11
11
  annotation: 'read-only',
12
12
  params: {
@@ -37,9 +37,15 @@ export const agentSearchCapability = {
37
37
  return {
38
38
  count: 0,
39
39
  agents: [],
40
- searched: ['published store', 'your org-mates'],
41
- hint: 'Zero hits means no agent profile matched these terms — discovery itself is up. ' +
42
- 'Matching is lexical against agent name/description/tags, so try shorter or different keywords. ' +
40
+ searched: [
41
+ 'published store: agents by name, description and tags',
42
+ 'your org: agents by name, description and tags',
43
+ 'your org: people by name (returns their agents, with person.userId)',
44
+ ],
45
+ hint: 'Zero hits means nothing matched these terms; discovery itself is up. ' +
46
+ 'People are not agents: a teammate\'s assistant is found by the teammate\'s name or by its own name, and a hit carries person.userId. ' +
47
+ 'To reach the person themselves, open a chat with that user id (chat_open participantId). ' +
48
+ 'Matching is lexical, so try shorter or different keywords. ' +
43
49
  'If you already know the agent, pass its exact agent id as the query to resolve it directly. ' +
44
50
  'Searching for someone you are LINKED with will always miss: a link makes the person addressable, ' +
45
51
  'not their agents. Take their peer id from link_list and open a chat with the person instead.',
@@ -0,0 +1,51 @@
1
+ /**
2
+ * One decision, one sentence, on every rail.
3
+ *
4
+ * When somebody answers a request an agent made (an access request on an
5
+ * artifact, a connection request, its own proposal), the agent hears about it
6
+ * as an agreement row whose `reason` names the decision. Rendered generically
7
+ * ("open the agreement <id>") that row says nothing about what was decided,
8
+ * and the agent learned it was approved or refused only if it happened to list
9
+ * its agreements. The hosted brain already had words for these reasons; the
10
+ * MCP rail did not. Both now call this function, so they say the same thing.
11
+ *
12
+ * The sentence is server state only. It never interpolates anything a
13
+ * counterparty wrote: no titles, no messages, no descriptions. Ids, an actor id
14
+ * and an instant are all it carries.
15
+ */
16
+ export type DecisionOutcome = 'approved' | 'rejected' | 'fulfilled' | 'held';
17
+ export interface DecisionWordsInput {
18
+ /** The delivery row's `reason`. Anything that is not a decision maps to null. */
19
+ reason: string | null | undefined;
20
+ /** Who answered (the row's `actorId`). Absent on some emits. */
21
+ actorId?: string | null;
22
+ /** When it was answered (the row's `ts`), as an ISO instant. */
23
+ ts?: string | null;
24
+ /** The request agreement. */
25
+ agreementId?: string | null;
26
+ /**
27
+ * What the request was about, when the caller knows it. Named only when
28
+ * `kind` is `artifact`; today's delivery row does not carry it, so the
29
+ * sentence sends the reader to the agreement to find the artifact instead.
30
+ */
31
+ scope?: {
32
+ kind: string;
33
+ id: string;
34
+ } | null;
35
+ /**
36
+ * Which surface's tool names to use. MCP registers `ziggs_open` and
37
+ * `ziggs_context_read`; the hosted SDK registers `open` and `context_read`.
38
+ */
39
+ surface?: 'mcp' | 'sdk';
40
+ }
41
+ export interface DecisionWords {
42
+ outcome: DecisionOutcome;
43
+ /** Plain, model-facing, server state only. */
44
+ sentence: string;
45
+ }
46
+ /**
47
+ * Map a delivery row's decision reason to words. Returns null when the reason
48
+ * is not a decision (an ordinary doorbell, a store claim, a withheld contact),
49
+ * so a caller can use it as the test for "does this row need explaining".
50
+ */
51
+ export declare function decisionWords(input: DecisionWordsInput): DecisionWords | null;
@@ -0,0 +1,86 @@
1
+ /**
2
+ * One decision, one sentence, on every rail.
3
+ *
4
+ * When somebody answers a request an agent made (an access request on an
5
+ * artifact, a connection request, its own proposal), the agent hears about it
6
+ * as an agreement row whose `reason` names the decision. Rendered generically
7
+ * ("open the agreement <id>") that row says nothing about what was decided,
8
+ * and the agent learned it was approved or refused only if it happened to list
9
+ * its agreements. The hosted brain already had words for these reasons; the
10
+ * MCP rail did not. Both now call this function, so they say the same thing.
11
+ *
12
+ * The sentence is server state only. It never interpolates anything a
13
+ * counterparty wrote: no titles, no messages, no descriptions. Ids, an actor id
14
+ * and an instant are all it carries.
15
+ */
16
+ const REFUSED_REASONS = new Set([
17
+ 'approval:rejected',
18
+ 'context_request_rejected',
19
+ 'connection_request_rejected',
20
+ ]);
21
+ function requestNoun(reason) {
22
+ if (reason.startsWith('context_request_'))
23
+ return 'access request';
24
+ if (reason.startsWith('connection_request_'))
25
+ return 'connection request';
26
+ return 'request';
27
+ }
28
+ /**
29
+ * Map a delivery row's decision reason to words. Returns null when the reason
30
+ * is not a decision (an ordinary doorbell, a store claim, a withheld contact),
31
+ * so a caller can use it as the test for "does this row need explaining".
32
+ */
33
+ export function decisionWords(input) {
34
+ const reason = typeof input.reason === 'string' ? input.reason : '';
35
+ if (!reason)
36
+ return null;
37
+ const tool = (key) => input.surface === 'sdk' ? key : `ziggs_${key}`;
38
+ const on = input.agreementId
39
+ ? `agreement ${input.agreementId}`
40
+ : 'this agreement';
41
+ const by = input.actorId ? ` by ${input.actorId}` : '';
42
+ const at = input.ts ? ` at ${input.ts}` : '';
43
+ const artifactId = input.scope?.kind === 'artifact' && input.scope.id ? input.scope.id : null;
44
+ if (REFUSED_REASONS.has(reason)) {
45
+ return {
46
+ outcome: 'rejected',
47
+ sentence: `Your ${requestNoun(reason)} on ${on} was refused${by}${at}. ` +
48
+ 'Report the unresolved dependency where you asked, and ask what to change.',
49
+ };
50
+ }
51
+ if (reason === 'context_request_fulfilled') {
52
+ const open = artifactId
53
+ ? `Open the artifact ${artifactId} with ${tool('context_read')} or ${tool('open')}; `
54
+ : `Open the agreement with ${tool('open')} to find the artifact, then open the artifact with ${tool('context_read')} or ${tool('open')}; `;
55
+ return {
56
+ outcome: 'fulfilled',
57
+ sentence: `Your access request on ${on} was approved${by}${at}. ` +
58
+ open +
59
+ 'the grant is bounded, check its expiry on the agreement.',
60
+ };
61
+ }
62
+ if (reason === 'connection_request_fulfilled') {
63
+ return {
64
+ outcome: 'fulfilled',
65
+ sentence: `Your connection request on ${on} was fulfilled${by}${at}. ` +
66
+ `The connection is ready to use; open the agreement with ${tool('open')} for its connection and grant ids and the bound on the grant.`,
67
+ };
68
+ }
69
+ if (reason === 'approval:approved') {
70
+ return {
71
+ outcome: 'approved',
72
+ sentence: `An approval on ${on} was given${by}${at}. ` +
73
+ 'If it answers a request you made, carry out the next step you promised where you asked; ' +
74
+ `open the agreement with ${tool('open')} to see which slot was answered. ` +
75
+ 'Approval of your own request is not a new hire and needs no introduction.',
76
+ };
77
+ }
78
+ if (reason === 'formation_held') {
79
+ return {
80
+ outcome: 'held',
81
+ sentence: `${on.charAt(0).toUpperCase()}${on.slice(1)} is held${at}: a consent it needs has not been given yet, so nothing is granted. ` +
82
+ `Open the agreement with ${tool('open')} to see whose decision it waits on.`,
83
+ };
84
+ }
85
+ return null;
86
+ }
@@ -1,5 +1,5 @@
1
1
  import type { ChatReadDto } from '@ziggs-ai/contracts';
2
- import type { SendChatMessageResult as WireSendChatMessageResult } from '@ziggs-ai/contracts';
2
+ import type { MessageDelivery, SendChatMessageResult as WireSendChatMessageResult } from '@ziggs-ai/contracts';
3
3
  import { type Creds } from '../types.js';
4
4
  /**
5
5
  * One room from `GET /chats/mine`, as the server sends it.
@@ -48,29 +48,29 @@ export interface SendChatMessageInput {
48
48
  /**
49
49
  * Where the server says a send landed.
50
50
  *
51
- * Named apart from the server's own `MessageDelivery` on purpose: the contracts
52
- * package is generated from the running server's spec and has not shipped this
53
- * shape yet, and a hand-written type under the contract's name would be a
54
- * second opinion about the wire. When the generated type lands, this alias is
55
- * replaced by importing it rather than kept beside it.
51
+ * The contract's own shape, re-exported rather than restated: it shipped in
52
+ * contracts 0.7.7, and the hand-written stand-in that carried it until then is
53
+ * gone. What the fields mean is worth keeping in front of a caller, because
54
+ * the receipt is easy to over-read:
56
55
  *
57
56
  * The caller may omit the receiver and have it inferred, or have it widened to
58
57
  * the room's humans, so the destination is the server's answer rather than the
59
58
  * caller's request. `accepted` is the message being created — not a claim that
60
- * anyone read it. Routing is asynchronous and unconfirmed by this receipt.
61
- * Legacy servers inferred wakesAgent from recipient type; that field is not
62
- * evidence of assignment or execution, including when it is false.
59
+ * anyone read it. Routing is asynchronous and unconfirmed by this receipt; the
60
+ * `wakesAgent` field that once rode along was removed for exactly that reason
61
+ * and is not coming back.
62
+ */
63
+ export type { MessageDelivery } from '@ziggs-ai/contracts';
64
+ /**
65
+ * The send result as a caller here holds it.
66
+ *
67
+ * `delivery` is optional on the way in and required in the contract, which is
68
+ * not a disagreement about the wire: this client is installed independently of
69
+ * the backend it talks to, and a server from before the field existed answers
70
+ * without one.
63
71
  */
64
- export interface SendDeliveryReport {
65
- accepted: boolean;
66
- to: {
67
- id: string;
68
- type: string;
69
- };
70
- broadcast: boolean;
71
- }
72
72
  export type SendChatMessageResult = Omit<WireSendChatMessageResult, 'delivery'> & {
73
- delivery?: SendDeliveryReport;
73
+ delivery?: MessageDelivery;
74
74
  };
75
75
  export type ContextTemporal = 'from-now' | 'from-start';
76
76
  export interface AddChatMemberInput {
@@ -27,7 +27,7 @@ export interface ContextReadQuery {
27
27
  cursor?: string;
28
28
  limit?: number;
29
29
  after?: string;
30
- direction?: 'forward';
30
+ direction?: 'forward' | 'backward';
31
31
  state?: string;
32
32
  contextGrantId?: string;
33
33
  }
@@ -58,6 +58,13 @@ export interface ContextSnapshotParticipant {
58
58
  export interface ContextSnapshotResult {
59
59
  history: unknown[];
60
60
  agreements: unknown[];
61
+ /** Bounded room inventory, without bodies. Absent on older backends. */
62
+ artifacts?: ContextReadEnvelope<{
63
+ artifactId: string;
64
+ name: string | null;
65
+ contentType: string;
66
+ timestamp: string | null;
67
+ }>;
61
68
  agents: Array<ContextSnapshotParticipant & {
62
69
  agentId: string;
63
70
  }>;
@@ -137,8 +137,20 @@ export type PlanStepPatch = {
137
137
  status: 'pending' | 'in_progress' | 'completed' | 'skipped';
138
138
  result?: unknown;
139
139
  };
140
- export declare function updateTaskPlanSteps(taskId: string, patches: PlanStepPatch[], creds: Creds): Promise<TaskWriteConfirm>;
141
- export declare function updateTaskPlanStep(taskId: string, stepId: string, status: string, result: unknown, creds: Creds): Promise<TaskWriteConfirm>;
140
+ /**
141
+ * Who the holder is waiting on, because its last act was asking them.
142
+ *
143
+ * Rides the progress write because that is when it is known and that is what
144
+ * makes it false again: the server clears it on the next progress write that
145
+ * does not restate it, and on any terminal transition. A holder that did
146
+ * something else after asking is no longer waiting.
147
+ */
148
+ export type TaskWaitingOn = {
149
+ kind: 'user' | 'agent';
150
+ id: string;
151
+ };
152
+ export declare function updateTaskPlanSteps(taskId: string, patches: PlanStepPatch[], creds: Creds, waitingOn?: TaskWaitingOn | null): Promise<TaskWriteConfirm>;
153
+ export declare function updateTaskPlanStep(taskId: string, stepId: string, status: string, result: unknown, creds: Creds, waitingOn?: TaskWaitingOn | null): Promise<TaskWriteConfirm>;
142
154
  export declare function reportTask(taskId: string, message: string | undefined, creds: Creds): Promise<TaskWriteConfirm>;
143
155
  export declare function setTaskSatisfaction(taskId: string, satisfaction: 'positive' | 'negative', creds: Creds): Promise<TaskWriteConfirm>;
144
156
  export interface ListTasksOptions {
@@ -277,13 +277,17 @@ export async function replaceTaskPlan(taskId, steps, creds) {
277
277
  throw new Error('Invalid response: task write confirm not found');
278
278
  return confirm;
279
279
  }
280
- export async function updateTaskPlanSteps(taskId, patches, creds) {
280
+ export async function updateTaskPlanSteps(taskId, patches, creds, waitingOn) {
281
281
  if (!taskId)
282
282
  throw new Error('updateTaskPlanSteps: taskId is required');
283
- if (!patches || !Array.isArray(patches) || patches.length === 0) {
283
+ const named = Array.isArray(patches) ? patches : [];
284
+ // A bare waiting-on write is the ask that produced no other progress, and
285
+ // the route takes it on its own. Without this the only way to say "I asked
286
+ // Dana" was to invent a plan step to tick.
287
+ if (named.length === 0 && !waitingOn) {
284
288
  throw new Error('updateTaskPlanSteps: patches must name at least one step');
285
289
  }
286
- for (const patch of patches) {
290
+ for (const patch of named) {
287
291
  if (!patch?.stepId)
288
292
  throw new Error('updateTaskPlanSteps: each patch needs a stepId');
289
293
  if (!patch?.status)
@@ -293,7 +297,10 @@ export async function updateTaskPlanSteps(taskId, patches, creds) {
293
297
  const res = await fetch(`${getTaskBaseUrl()}/${taskId}/plan`, {
294
298
  method: 'PATCH',
295
299
  headers: buildHeaders(creds),
296
- body: JSON.stringify({ patches }),
300
+ body: JSON.stringify({
301
+ ...(named.length > 0 ? { patches: named } : {}),
302
+ ...(waitingOn !== undefined ? { waitingOn } : {}),
303
+ }),
297
304
  });
298
305
  if (!res.ok) {
299
306
  const responseBody = await res.text().catch(() => '');
@@ -305,7 +312,7 @@ export async function updateTaskPlanSteps(taskId, patches, creds) {
305
312
  throw new Error('Invalid response: task write confirm not found');
306
313
  return confirm;
307
314
  }
308
- export async function updateTaskPlanStep(taskId, stepId, status, result, creds) {
315
+ export async function updateTaskPlanStep(taskId, stepId, status, result, creds, waitingOn) {
309
316
  if (!taskId)
310
317
  throw new Error('updateTaskPlanStep: taskId is required');
311
318
  if (!stepId)
@@ -316,7 +323,15 @@ export async function updateTaskPlanStep(taskId, stepId, status, result, creds)
316
323
  const res = await fetch(`${getTaskBaseUrl()}/${taskId}/plan`, {
317
324
  method: 'PATCH',
318
325
  headers: buildHeaders(creds),
319
- body: JSON.stringify({ stepId, status, ...(result !== undefined && { result }) }),
326
+ // One write, not two: the server clears `waitingOn` on a progress write
327
+ // that does not restate it, so posting the tick and the ask separately
328
+ // would set it and then immediately drop it.
329
+ body: JSON.stringify({
330
+ stepId,
331
+ status,
332
+ ...(result !== undefined && { result }),
333
+ ...(waitingOn !== undefined ? { waitingOn } : {}),
334
+ }),
320
335
  });
321
336
  if (!res.ok) {
322
337
  const responseBody = await res.text().catch(() => '');
package/dist/index.d.ts CHANGED
@@ -12,6 +12,8 @@ export { parseErrorMessage, parseErrorCode, throwApiError, } from './shared/apiE
12
12
  export { ApiError } from './types.js';
13
13
  export type { Creds, Task, TaskState, PlanStep, PlanStepStatus, Agreement, AgreementParties, AgreementPartySide, EngagementKind, BroadcastAudience, EntryType, ContentType, MessageMetadata, MessageHandler, InboxDeliveryKind, InboxDeliveryRef, InboxChatNews, InboxProposalRef, InboxTaskRef, InboxRequestRef, InboxConnectionRequestRef, InboxHumanAttention, InboxEnvelope, InboxAckResult, InboxPeek, InboxReadOptions, } from './types.js';
14
14
  export type { ProposeTerms, ProposeDirectInput, ProposeBroadcastInput, ProposeAgreementData, ClaimOptions, } from './http/AgreementClient.js';
15
+ export { decisionWords } from './decisionWords.js';
16
+ export type { DecisionOutcome, DecisionWords, DecisionWordsInput, } from './decisionWords.js';
15
17
  export { planInboxAck, planPartialInboxAck, inboxAckAllowed, inboxAckHandledIds, } from './inboxAck.js';
16
18
  export type { InboxAckDecision, InboxAckStep, InboxAckEnvelope, PlanInboxAckOptions, } from './inboxAck.js';
17
19
  export { agreementLaneId, agreementIdFromLane, isAgreementLaneId, isAgreementId } from '@ziggs-ai/contracts';
package/dist/index.js CHANGED
@@ -13,6 +13,9 @@ export { RateLimitedError, isRateLimited, parseRetryAfterMs, pollSurfaceError, }
13
13
  // half of them had drifted).
14
14
  export { parseErrorMessage, parseErrorCode, throwApiError, } from './shared/apiError.js';
15
15
  export { ApiError } from './types.js';
16
+ // One decision, one sentence, on every rail: the reason a delivery row carries
17
+ // when somebody answered a request this agent made, rendered as words.
18
+ export { decisionWords } from './decisionWords.js';
16
19
  export { planInboxAck, planPartialInboxAck, inboxAckAllowed, inboxAckHandledIds, } from './inboxAck.js';
17
20
  export { agreementLaneId, agreementIdFromLane, isAgreementLaneId, isAgreementId } from '@ziggs-ai/contracts';
18
21
  export * from './shared/operatorKey.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ziggs-ai/api-client",
3
- "version": "0.21.0",
3
+ "version": "0.22.0",
4
4
  "description": "HTTP and WebSocket client for the Ziggs backend API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -27,8 +27,8 @@
27
27
  "test:watch": "node --import tsx/esm --test --watch test/*.test.ts"
28
28
  },
29
29
  "dependencies": {
30
- "socket.io-client": "^4.7.0",
31
- "@ziggs-ai/contracts": "^0.9.0"
30
+ "@ziggs-ai/contracts": "^0.9.0",
31
+ "socket.io-client": "^4.7.0"
32
32
  },
33
33
  "keywords": [
34
34
  "ziggs",