@ziggs-ai/ziggs-mcp 0.4.0 → 0.5.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.
@@ -21,16 +21,15 @@ export interface ReadPlanResult {
21
21
  /**
22
22
  * ZIG-634: replace the free-text `nextActions` hints with typed `readPlan`
23
23
  * call objects — tool name + pre-filled args — so the most common loop
24
- * (inbox → read each chat with news → ack) needs no guesswork.
24
+ * (inbox → read what was addressed to you → ack) needs no guesswork.
25
25
  *
26
26
  * Safe by construction: every entry is synthesized purely from fields already
27
- * on the envelope (scope kinds, ids, per-chat counts, latestAt). No new data
28
- * is read and no new permission check runs — this is the same information the
29
- * caller already received, restated as runnable calls.
27
+ * on the envelope (delivery kinds and ids, the per-chat fold, ackTo). No new
28
+ * data is read and no new permission check runs — this is the same information
29
+ * the caller already received, restated as runnable calls.
30
30
  *
31
31
  * Mapping honours how reads resolve server-side: messages read only via chat,
32
- * artifacts via chat or agreement. For multi-chat scopes (org / agreement) we
33
- * use the per-chat breakdown (ZIG-543) to name the chatIds.
32
+ * artifacts via chat or agreement.
34
33
  */
35
34
  export declare function buildReadPlan(inbox: InboxEnvelope, grantsByScope?: Map<string, ScopeGrantTag>): ReadPlanResult;
36
35
  /**
@@ -70,7 +69,8 @@ export declare function indexReachByScope(reach: GrantView[]): Map<string, Scope
70
69
  * and append readPlan last so each inbox call self-narrates the follow-up
71
70
  * calls (ZIG-634) without disturbing the leading humanAttention key.
72
71
  *
73
- * When `reach` (the caller's own live grants) is passed, each scope is tagged
74
- * with its covering grant (ZIG-635) so the agent can pin X-Context-Grant-Id.
72
+ * When `reach` (the caller's own live grants) is passed, the read plan pins
73
+ * each read's covering grant (ZIG-635) so the agent can present
74
+ * X-Context-Grant-Id without a separate discover round-trip.
75
75
  */
76
76
  export declare function formatInboxToolResult(inbox: InboxEnvelope, ack?: InboxAckResult | null, webOrigin?: string, activeTasks?: Task[], reach?: GrantView[], activeTasksError?: string): Record<string, unknown>;
@@ -1,6 +1,6 @@
1
1
  import { grantCaveat } from '@ziggs-ai/api-client';
2
2
  import { formatPendingDecisionsPayload, resolveWebAppOrigin, } from './pendingDecisions.js';
3
- /** Keep the plan bounded; the full scopes array still carries everything. */
3
+ /** Keep the plan bounded; the full deliveries array still carries everything. */
4
4
  const MAX_READ_PLAN = 12;
5
5
  function readContextCall(type, kind, id, grantId) {
6
6
  // ZIG-660: pin the covering grant so the read presents the right
@@ -15,25 +15,24 @@ function readContextCall(type, kind, id, grantId) {
15
15
  /**
16
16
  * ZIG-634: replace the free-text `nextActions` hints with typed `readPlan`
17
17
  * call objects — tool name + pre-filled args — so the most common loop
18
- * (inbox → read each chat with news → ack) needs no guesswork.
18
+ * (inbox → read what was addressed to you → ack) needs no guesswork.
19
19
  *
20
20
  * Safe by construction: every entry is synthesized purely from fields already
21
- * on the envelope (scope kinds, ids, per-chat counts, latestAt). No new data
22
- * is read and no new permission check runs — this is the same information the
23
- * caller already received, restated as runnable calls.
21
+ * on the envelope (delivery kinds and ids, the per-chat fold, ackTo). No new
22
+ * data is read and no new permission check runs — this is the same information
23
+ * the caller already received, restated as runnable calls.
24
24
  *
25
25
  * Mapping honours how reads resolve server-side: messages read only via chat,
26
- * artifacts via chat or agreement. For multi-chat scopes (org / agreement) we
27
- * use the per-chat breakdown (ZIG-543) to name the chatIds.
26
+ * artifacts via chat or agreement.
28
27
  */
29
28
  export function buildReadPlan(inbox, grantsByScope) {
30
29
  const proposals = inbox.proposalsAwaitingMe ?? [];
31
30
  const connectionRequests = inbox.connectionRequestsAwaitingMe ?? [];
32
- const scopes = inbox.scopes ?? [];
31
+ const deliveries = inbox.deliveries ?? [];
33
32
  // ZIG-660: dedup by call signature so the same (type, via) can't appear
34
- // twice when one chat is covered by both its own grant and an org/agreement
35
- // grant. Collect candidates uncapped; the cap is applied once, after the ack
36
- // is reserved, so the ack step always survives.
33
+ // twice when several deliveries land in one chat. Collect candidates
34
+ // uncapped; the cap is applied once, after the ack is reserved, so the ack
35
+ // step always survives.
37
36
  const candidates = [];
38
37
  const seen = new Set();
39
38
  const add = (key, call) => {
@@ -58,57 +57,35 @@ export function buildReadPlan(inbox, grantsByScope) {
58
57
  why: 'connection request awaiting your response — wait for the human to approve/reject',
59
58
  });
60
59
  }
61
- // The covering grant for reads derived from a scope is that scope's own grant
62
- // (ZIG-635) the same grant tagged onto the scope entry.
63
- const grantFor = (s) => grantsByScope?.get(`${s.scope.kind}:${s.scope.id}`)?.grantId;
64
- // Reads point each scope's news at the call that opens it.
65
- for (const s of scopes) {
66
- const { kind, id } = s.scope;
67
- const grantId = grantFor(s);
68
- const read = (type, viaKind, viaId) => add(`read:${type}:${viaKind}:${viaId}`, readContextCall(type, viaKind, viaId, grantId));
69
- if (kind === 'chat') {
70
- if (s.newMessages)
71
- read('messages', 'chat', id);
72
- if (s.newArtifacts)
73
- read('artifacts', 'chat', id);
74
- }
75
- else if (kind === 'agreement') {
76
- // Messages resolve only via chat — name the chats from the breakdown.
77
- for (const c of s.chats ?? []) {
78
- if (c.newMessages)
79
- read('messages', 'chat', c.chatId);
80
- }
81
- // Artifacts (incl. task-result artifacts) read directly via the agreement.
82
- if (s.newArtifacts)
83
- read('artifacts', 'agreement', id);
84
- }
85
- else {
86
- // org: both messages and artifacts resolve per chat only.
87
- for (const c of s.chats ?? []) {
88
- if (c.newMessages)
89
- read('messages', 'chat', c.chatId);
90
- if (c.newArtifacts)
91
- read('artifacts', 'chat', c.chatId);
92
- }
60
+ // ZIG-635: pin the covering grant for a chat/agreement read when the caller
61
+ // holds one, so the read presents the right X-Context-Grant-Id without a
62
+ // separate discover round-trip. Untagged reads still work by id.
63
+ const grantFor = (kind, id) => grantsByScope?.get(`${kind}:${id}`)?.grantId;
64
+ const read = (type, viaKind, viaId) => add(`read:${type}:${viaKind}:${viaId}`, readContextCall(type, viaKind, viaId, grantFor(viaKind, viaId)));
65
+ // Reads one call per place mail actually landed. The delivery names the
66
+ // chat or agreement directly, so nothing has to be inferred from a scope.
67
+ for (const d of deliveries) {
68
+ if (d.kind === 'message' && d.chatId)
69
+ read('messages', 'chat', d.chatId);
70
+ else if (d.kind === 'artifact') {
71
+ if (d.chatId)
72
+ read('artifacts', 'chat', d.chatId);
73
+ else if (d.agreementId)
74
+ read('artifacts', 'agreement', d.agreementId);
93
75
  }
94
76
  }
95
- // Close the loop: reading never clears the inbox — pre-fill the ack call with
96
- // each scope's own latestAt (only scopes that actually have a high-water mark).
97
- const ackTargets = scopes
98
- .filter((s) => s.latestAt)
99
- .map((s) => ({ kind: s.scope.kind, id: s.scope.id, upTo: s.latestAt }));
100
77
  // ZIG-660: reserve a slot for the ack before capping, so the pre-filled ack
101
78
  // never gets squeezed out exactly when there's the most news. Report how many
102
79
  // read/decision candidates the cap dropped as an explicit count.
103
- const reserve = ackTargets.length ? 1 : 0;
80
+ const reserve = inbox.ackTo ? 1 : 0;
104
81
  const budget = Math.max(0, MAX_READ_PLAN - reserve);
105
82
  const truncated = Math.max(0, candidates.length - budget);
106
83
  const plan = candidates.slice(0, budget);
107
- if (ackTargets.length) {
84
+ if (inbox.ackTo) {
108
85
  plan.push({
109
86
  tool: 'ziggs_inbox',
110
- args: { ack: ackTargets },
111
- why: 'reading does not clear the inbox — ack the scopes you handled (drop any you did not)',
87
+ args: { ack: inbox.ackTo },
88
+ why: 'reading does not clear the inbox — ack once you have handled everything above',
112
89
  });
113
90
  }
114
91
  return { plan, truncated };
@@ -186,34 +163,18 @@ export function indexReachByScope(reach) {
186
163
  }
187
164
  return byScope;
188
165
  }
189
- /**
190
- * ZIG-635: tag each inbox scope with its covering grant. Scopes with no
191
- * matching live grant (e.g. reachable via membership, not a grant) are left
192
- * untagged — the agent keeps navigating by id, never a fabricated grant.
193
- */
194
- function tagScopesWithGrants(scopes, byScope) {
195
- if (!byScope?.size)
196
- return scopes;
197
- return scopes.map((s) => {
198
- const tag = byScope.get(`${s.scope.kind}:${s.scope.id}`);
199
- return tag ? { ...s, grant: tag } : s;
200
- });
201
- }
202
166
  /**
203
167
  * Put humanAttention first so MCP hosts surface it before counts (ZIG-482),
204
168
  * and append readPlan last so each inbox call self-narrates the follow-up
205
169
  * calls (ZIG-634) without disturbing the leading humanAttention key.
206
170
  *
207
- * When `reach` (the caller's own live grants) is passed, each scope is tagged
208
- * with its covering grant (ZIG-635) so the agent can pin X-Context-Grant-Id.
171
+ * When `reach` (the caller's own live grants) is passed, the read plan pins
172
+ * each read's covering grant (ZIG-635) so the agent can present
173
+ * X-Context-Grant-Id without a separate discover round-trip.
209
174
  */
210
175
  export function formatInboxToolResult(inbox, ack, webOrigin, activeTasks, reach, activeTasksError) {
211
- // ZIG-660: build the grant index once and feed both the read plan (grant
212
- // pinning) and the scope tags from it — buildReadPlan no longer runs before
213
- // the grants are available.
214
176
  const byScope = reach?.length ? indexReachByScope(reach) : undefined;
215
177
  const { plan: readPlan, truncated: readPlanTruncated } = buildReadPlan(inbox, byScope);
216
- const scopes = tagScopesWithGrants(inbox.scopes ?? [], byScope);
217
178
  const origin = resolveWebAppOrigin(webOrigin);
218
179
  // ZIG-659: the inbox reports session-start counts and points to
219
180
  // ziggs_pending_decisions for the sessionChatCard — it no longer re-emits the
@@ -248,11 +209,7 @@ export function formatInboxToolResult(inbox, ack, webOrigin, activeTasks, reach,
248
209
  };
249
210
  const { humanAttention, ...rest } = inbox;
250
211
  const payload = ack
251
- ? { acked: ack.acked, ...rest, scopes, ...tail }
252
- : { ...rest, scopes, ...tail };
253
- return humanAttention
254
- ? { humanAttention, ...payload }
255
- : ack
256
- ? payload
257
- : { ...inbox, scopes, ...tail };
212
+ ? { ackedUpTo: ack.ackedUpTo, ...rest, ...tail }
213
+ : { ...rest, ...tail };
214
+ return humanAttention ? { humanAttention, ...payload } : payload;
258
215
  }
package/dist/tools.js CHANGED
@@ -453,7 +453,7 @@ export function registerZiggsTools(server, creds, cfg) {
453
453
  return toolError(e.message);
454
454
  }
455
455
  });
456
- server.tool('ziggs_propose_agreement', 'Propose a direct agreement to one counterparty (proposedTo) in a chat. The payer is always derived server-side as the non-providing side — there is no payer input. Omit providerId (or set it to proposedTo) to commission the recipient (they work, your side pays). Set providerId to your own agent id to offer (you work, proposedTo pays). Set providerId to a third-party agent id to broker (they work, proposedTo pays) — that provider must have an active published offer whose terms match this proposal (price, lifecycle, engagementKind, etc.) or the call fails naming the mismatched field. engagementKind "service" (default) = one deliverable; "hire" = ongoing engagement. price is recorded on the agreement but does not itself trigger a transfer.', {
456
+ server.tool('ziggs_propose_agreement', 'Propose a direct agreement to one counterparty (proposedTo) in a chat. The payer is always derived server-side as the non-providing side — there is no payer input. Omit providerId (or set it to proposedTo) to commission the recipient (they work, your side pays). Set providerId to your own agent id to offer (you work, proposedTo pays). Set providerId to a third-party agent id to broker (they work, proposedTo pays) — that provider must have an active published offer whose terms match this proposal (price, lifecycle, engagementKind, etc.) or the call fails naming the mismatched field. engagementKind "service" (default) = one deliverable; "hire" = ongoing engagement. Agreements are STANDING by default (lifecycle "open": no expiry, unlimited tasks) — hire once, then keep spawning tasks under the same agreement; set expiresAt (time-bound) or maxExecutions (count-bound) only when the engagement should end on its own. price is recorded on the agreement but does not itself trigger a transfer.', {
457
457
  proposedTo: z.string(),
458
458
  chatId: z.string(),
459
459
  description: z.string(),
@@ -466,7 +466,25 @@ export function registerZiggsTools(server, creds, cfg) {
466
466
  .enum(['hire', 'service'])
467
467
  .optional()
468
468
  .describe("'service' (default) = one-off deliverable; 'hire' = ongoing engagement"),
469
- }, WRITE, async ({ proposedTo, chatId, description, providerId, price, engagementKind }) => {
469
+ expiresAt: z
470
+ .string()
471
+ .optional()
472
+ .describe('ISO date: the agreement ends (is cancelled, tasks and all) at this time. Omit for a standing agreement.'),
473
+ maxExecutions: z
474
+ .number()
475
+ .int()
476
+ .positive()
477
+ .optional()
478
+ .describe('The agreement auto-fulfills after this many completed tasks. Omit for unlimited tasks.'),
479
+ lifecycle: z
480
+ .enum(['open', 'time-bound', 'count-bound'])
481
+ .optional()
482
+ .describe("Usually inferred: expiresAt → 'time-bound', maxExecutions → 'count-bound', neither → 'open' (standing)."),
483
+ billing: z
484
+ .enum(['total', 'per_task'])
485
+ .optional()
486
+ .describe("How price reads. 'total' (default) = one price for the whole engagement, escrowed now and paid at the end. 'per_task' = a RATE charged for each completed task, paid as work lands — requires a standing (open) agreement, and is the default for a hire. Never send 'per_task' for a one-off price or the payer is charged it once per task."),
487
+ }, WRITE, async ({ proposedTo, chatId, description, providerId, price, engagementKind, expiresAt, maxExecutions, lifecycle, billing, }) => {
470
488
  try {
471
489
  const agreement = await proposeDirectTo({
472
490
  proposedTo,
@@ -475,6 +493,10 @@ export function registerZiggsTools(server, creds, cfg) {
475
493
  providerId: providerId?.trim() || proposedTo,
476
494
  price,
477
495
  engagementKind: engagementKind ?? 'service',
496
+ expiresAt,
497
+ maxExecutions,
498
+ lifecycle,
499
+ billing,
478
500
  }, creds);
479
501
  return textResult({ agreement });
480
502
  }
@@ -544,7 +566,7 @@ export function registerZiggsTools(server, creds, cfg) {
544
566
  return toolError(e.message);
545
567
  }
546
568
  });
547
- server.tool('ziggs_fulfill_agreement', 'Mark an agreement you PROVIDE as fulfilled/complete once its work is delivered (POST /agreements/:id/fulfill) closes the engagement so it no longer reads as in-progress. Party-gated server-side: only the providing side can fulfill. Use on your hire after the final deliverable is done and delivered.', {
569
+ server.tool('ziggs_fulfill_agreement', 'END an agreement you PROVIDE permanently (POST /agreements/:id/fulfill). Fulfilling terminates the whole relationship, not one deliverable: every grant the agreement conferred (context, connection, payment) is revoked, its shared space is torn down, and it cannot be reopened the counterparty would have to re-hire you from scratch. Finished WORK is reported with ziggs_set_task_result, which closes the task and leaves the agreement standing for the next one. Only fulfill a count/time-bound engagement whose full scope is delivered and where nothing more is expected — never a standing hire that just finished a task. Party-gated server-side: only the providing side can fulfill.', {
548
570
  agreementId: z.string().describe('The agreement you provide, to mark fulfilled'),
549
571
  }, WRITE, async ({ agreementId }) => {
550
572
  try {
@@ -557,13 +579,9 @@ export function registerZiggsTools(server, creds, cfg) {
557
579
  });
558
580
  server.tool('ziggs_inbox', ZIGGS_INBOX_DESCRIPTION, {
559
581
  ack: z
560
- .array(z.object({
561
- kind: z.enum(['chat', 'agreement', 'org']),
562
- id: z.string(),
563
- upTo: z.string().describe('ISO timestamp handled up to (inclusive)'),
564
- }))
582
+ .string()
565
583
  .optional()
566
- .describe('Scopes you finished handling — acked before fetching, monotonic'),
584
+ .describe("The envelope's `ackTo` from a previous call, once you have handled everything it carried — acked before fetching, monotonic (an older value is a no-op)."),
567
585
  waitSeconds: z
568
586
  .number()
569
587
  .optional()
@@ -571,7 +589,7 @@ export function registerZiggsTools(server, creds, cfg) {
571
589
  }, READ_ONLY, async ({ ack, waitSeconds }) => {
572
590
  try {
573
591
  const client = new InboxClient(creds.operatorKey, creds.agentId);
574
- const acked = ack?.length ? await client.ack(ack) : null;
592
+ const acked = ack ? await client.ack(ack) : null;
575
593
  const inbox = await client.getInbox(waitSeconds != null ? { waitSeconds } : {});
576
594
  let activeTasks = [];
577
595
  let activeTasksError;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ziggs-ai/ziggs-mcp",
3
- "version": "0.4.0",
4
- "description": "MCP server for Claude Code, Cursor, and other MCP hosts act as your Ziggs delegate agent",
3
+ "version": "0.5.0",
4
+ "description": "MCP server for Claude Code, Cursor, and other MCP hosts \u2014 act as your Ziggs delegate agent",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "ziggs-mcp": "./dist/index.js"
@@ -36,7 +36,7 @@
36
36
  },
37
37
  "dependencies": {
38
38
  "@modelcontextprotocol/sdk": "^1.29.0",
39
- "@ziggs-ai/api-client": "^0.4.0",
39
+ "@ziggs-ai/api-client": "^0.5.0",
40
40
  "dotenv": "^16.6.1",
41
41
  "zod": "^3.24.2"
42
42
  },