@ziggs-ai/ziggs-mcp 0.37.3 → 0.37.4

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.
@@ -4,9 +4,9 @@
4
4
  * Every assistant start-up pays for one inbox read, and it was several times
5
5
  * the size of the connect text before any content was opened. This changes how
6
6
  * the result is written, never what it says: which deliveries are visible,
7
- * their order, their reach, the acknowledgement tokens, the counters and the
8
- * lease all come from the server unchanged. What goes is only what can be read
9
- * back from what stays:
7
+ * their order within a thread, their reach, the acknowledgement tokens, the
8
+ * counters and the lease all come from the server unchanged. What goes is only
9
+ * what can be read back from what stays:
10
10
  *
11
11
  * - a delivery field that is null, false or empty (absent reads the same);
12
12
  * - `partyKind` on a delivery, which `sources` already gives for its `partyId`,
@@ -24,17 +24,61 @@
24
24
  *
25
25
  * Acknowledgement stays exact: every `ackTo` — the envelope's, each row's and
26
26
  * the ack step's — is passed through byte for byte.
27
+ *
28
+ * The rows are written grouped rather than as one flat list: by who they are
29
+ * for, then by who sent them, then by the chat, task or agreement they are
30
+ * about. A flat list mixed the assistant's own work with mail it
31
+ * only passes along to its person and mail for the person's other
32
+ * assistants, and the split lived in one sentence of counts. Every row is
33
+ * still there, oldest first inside its thread; see {@link groupDeliveries}.
27
34
  */
28
35
  type Json = Record<string, unknown>;
29
36
  /** One delivery row, without what it would say by default or elsewhere. */
30
37
  export declare function compactDelivery(row: Json, kindOfParty: Map<string, unknown>, usualPartyId?: unknown): Json;
38
+ /** Who an inbox read belongs to: the connection's assistants and their person. */
39
+ export interface InboxReader {
40
+ agentIds: readonly string[];
41
+ /** The person the assistants work for; read off the human source when absent. */
42
+ personId?: string | null;
43
+ }
44
+ /** The sections of a grouped inbox, in the order they are written. */
45
+ export declare const INBOX_SECTIONS: readonly ["forYou", "forYourPerson", "forOtherAssistants", "context"];
46
+ export type InboxSection = (typeof INBOX_SECTIONS)[number];
47
+ /**
48
+ * Who a row is for. One rule for the grouping and for the counts in the
49
+ * notice, so the two cannot disagree: assigned to one of the reader's
50
+ * assistants is theirs to handle; unassigned and flagged for a human is the
51
+ * person's, which the assistant passes along; assigned to anyone else is
52
+ * another assistant's; the rest nobody is asked to act on.
53
+ */
54
+ export declare function sectionOf(row: Json, agentIds: readonly string[]): InboxSection;
55
+ /**
56
+ * The window's rows, grouped: section, then sender, then thread.
57
+ *
58
+ * Nothing is hidden and nothing is reordered within a thread. Senders and
59
+ * threads are written in the order their oldest row arrived, which is the
60
+ * order the server sends (oldest first), except that the reader's own actions
61
+ * and their person's come last in a section: they are a record of what this
62
+ * side did, not news from anyone.
63
+ *
64
+ * A row drops only what its place in the grouping says: the sender (the
65
+ * group's `from`), the thread's own id, the assignee in `forYou` when the
66
+ * connection has one assistant, and `needsHuman` in `forYourPerson`. A thread
67
+ * names the first agreement its rows sit under; a row under another one keeps
68
+ * its own, and a row under none says `agreementId: null`, so every row reads
69
+ * back exactly.
70
+ */
71
+ export declare function groupDeliveries(rows: readonly Json[], reader: InboxReader, compactRow?: (row: Json) => Json): Partial<Record<InboxSection, Json[]>>;
31
72
  /**
32
73
  * The compact inbox result. Takes the result exactly as it would otherwise be
33
74
  * returned and gives back the same information in fewer characters.
75
+ *
76
+ * With a reader, `deliveries` is written grouped ({@link groupDeliveries});
77
+ * without one there is no telling whose a row is, so it stays one list.
34
78
  */
35
- export declare function compactInboxResult(result: Json): Json;
79
+ export declare function compactInboxResult(result: Json, reader?: InboxReader): Json;
36
80
  /** The inbox result as one line of JSON: indentation is most of nothing. */
37
- export declare function compactTextResult(data: Json): {
81
+ export declare function compactTextResult(data: Json, reader?: InboxReader): {
38
82
  content: {
39
83
  type: "text";
40
84
  text: string;
@@ -4,9 +4,9 @@
4
4
  * Every assistant start-up pays for one inbox read, and it was several times
5
5
  * the size of the connect text before any content was opened. This changes how
6
6
  * the result is written, never what it says: which deliveries are visible,
7
- * their order, their reach, the acknowledgement tokens, the counters and the
8
- * lease all come from the server unchanged. What goes is only what can be read
9
- * back from what stays:
7
+ * their order within a thread, their reach, the acknowledgement tokens, the
8
+ * counters and the lease all come from the server unchanged. What goes is only
9
+ * what can be read back from what stays:
10
10
  *
11
11
  * - a delivery field that is null, false or empty (absent reads the same);
12
12
  * - `partyKind` on a delivery, which `sources` already gives for its `partyId`,
@@ -24,6 +24,13 @@
24
24
  *
25
25
  * Acknowledgement stays exact: every `ackTo` — the envelope's, each row's and
26
26
  * the ack step's — is passed through byte for byte.
27
+ *
28
+ * The rows are written grouped rather than as one flat list: by who they are
29
+ * for, then by who sent them, then by the chat, task or agreement they are
30
+ * about. A flat list mixed the assistant's own work with mail it
31
+ * only passes along to its person and mail for the person's other
32
+ * assistants, and the split lived in one sentence of counts. Every row is
33
+ * still there, oldest first inside its thread; see {@link groupDeliveries}.
27
34
  */
28
35
  const isRecord = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
29
36
  /** Session fields `ziggs_auth_status` carries; the inbox does not repeat them. */
@@ -84,11 +91,140 @@ function usualPartyOf(rows) {
84
91
  [best, most] = [id, n];
85
92
  return best;
86
93
  }
94
+ /** The sections of a grouped inbox, in the order they are written. */
95
+ export const INBOX_SECTIONS = ['forYou', 'forYourPerson', 'forOtherAssistants', 'context'];
96
+ /**
97
+ * Who a row is for. One rule for the grouping and for the counts in the
98
+ * notice, so the two cannot disagree: assigned to one of the reader's
99
+ * assistants is theirs to handle; unassigned and flagged for a human is the
100
+ * person's, which the assistant passes along; assigned to anyone else is
101
+ * another assistant's; the rest nobody is asked to act on.
102
+ */
103
+ export function sectionOf(row, agentIds) {
104
+ const assignee = row.assigneeId ? String(row.assigneeId) : '';
105
+ if (assignee && agentIds.includes(assignee))
106
+ return 'forYou';
107
+ if (!assignee && row.needsHuman === true)
108
+ return 'forYourPerson';
109
+ if (assignee)
110
+ return 'forOtherAssistants';
111
+ return 'context';
112
+ }
113
+ /** What a row is about: its chat, else its task, else its agreement, else itself. */
114
+ function anchorOf(row) {
115
+ if (row.chatId)
116
+ return ['chatId', String(row.chatId)];
117
+ if (row.taskId)
118
+ return ['taskId', String(row.taskId)];
119
+ if (row.agreementId)
120
+ return ['agreementId', String(row.agreementId)];
121
+ return [row.kind === 'artifact' ? 'artifactId' : 'resourceId', String(row.resourceId)];
122
+ }
123
+ /**
124
+ * The window's rows, grouped: section, then sender, then thread.
125
+ *
126
+ * Nothing is hidden and nothing is reordered within a thread. Senders and
127
+ * threads are written in the order their oldest row arrived, which is the
128
+ * order the server sends (oldest first), except that the reader's own actions
129
+ * and their person's come last in a section: they are a record of what this
130
+ * side did, not news from anyone.
131
+ *
132
+ * A row drops only what its place in the grouping says: the sender (the
133
+ * group's `from`), the thread's own id, the assignee in `forYou` when the
134
+ * connection has one assistant, and `needsHuman` in `forYourPerson`. A thread
135
+ * names the first agreement its rows sit under; a row under another one keeps
136
+ * its own, and a row under none says `agreementId: null`, so every row reads
137
+ * back exactly.
138
+ */
139
+ export function groupDeliveries(rows, reader, compactRow = (row) => ({ ...row })) {
140
+ const sections = new Map();
141
+ for (const row of rows) {
142
+ const section = sectionOf(row, reader.agentIds);
143
+ const from = row.actorId ? String(row.actorId) : null;
144
+ const anchor = anchorOf(row);
145
+ let senders = sections.get(section);
146
+ if (!senders)
147
+ sections.set(section, (senders = new Map()));
148
+ let group = senders.get(String(from));
149
+ if (!group)
150
+ senders.set(String(from), (group = { from, threads: new Map() }));
151
+ const key = anchor.join(':');
152
+ let thread = group.threads.get(key);
153
+ if (!thread)
154
+ group.threads.set(key, (thread = { anchor, rows: [] }));
155
+ thread.rows.push(row);
156
+ }
157
+ const own = (from) => from !== null && (reader.agentIds.includes(from) || from === reader.personId);
158
+ const out = {};
159
+ for (const section of INBOX_SECTIONS) {
160
+ const senders = sections.get(section);
161
+ if (!senders)
162
+ continue;
163
+ const ordered = [...senders.values()];
164
+ out[section] = [
165
+ ...ordered.filter((g) => !own(g.from)),
166
+ ...ordered.filter((g) => own(g.from)),
167
+ ].map((g) => {
168
+ const threads = [...g.threads.values()].map((t) => writeThread(t, section, reader, compactRow));
169
+ const count = [...g.threads.values()].reduce((n, t) => n + t.rows.length, 0);
170
+ return {
171
+ from: g.from,
172
+ ...(g.from !== null && reader.agentIds.includes(g.from) ? { you: true } : {}),
173
+ ...(g.from !== null && g.from === reader.personId ? { yourPerson: true } : {}),
174
+ count,
175
+ threads,
176
+ };
177
+ });
178
+ }
179
+ return out;
180
+ }
181
+ function writeThread(thread, section, reader, compactRow) {
182
+ const [field, id] = thread.anchor;
183
+ const lifted = field === 'agreementId'
184
+ ? null
185
+ : thread.rows.map((r) => r.agreementId).find(Boolean) ?? null;
186
+ const rows = thread.rows.map((original) => {
187
+ const row = compactRow(original);
188
+ delete row.actorId;
189
+ if (field !== 'resourceId' && field !== 'artifactId')
190
+ delete row[field];
191
+ if (lifted) {
192
+ if (row.agreementId === lifted)
193
+ delete row.agreementId;
194
+ else if (!row.agreementId)
195
+ row.agreementId = null;
196
+ }
197
+ if (section === 'forYou') {
198
+ if (reader.agentIds.length === 1)
199
+ delete row.assigneeId;
200
+ if (row.assignedToConnection === true)
201
+ delete row.assignedToConnection;
202
+ }
203
+ if (section === 'forYourPerson')
204
+ delete row.needsHuman;
205
+ return row;
206
+ });
207
+ return { [field]: id, ...(lifted ? { agreementId: lifted } : {}), rows };
208
+ }
209
+ /** The reader's assistants, without empty ids, and their person. */
210
+ function readerOf(result, reader) {
211
+ const agentIds = (reader?.agentIds ?? []).filter(Boolean);
212
+ if (!agentIds.length)
213
+ return null;
214
+ const human = Array.isArray(result.sources)
215
+ ? result.sources.find((s) => isRecord(s) && s.partyKind === 'human')
216
+ : undefined;
217
+ const personId = reader?.personId ?? (isRecord(human) ? String(human.partyId) : null);
218
+ return { agentIds, personId };
219
+ }
87
220
  /**
88
221
  * The compact inbox result. Takes the result exactly as it would otherwise be
89
222
  * returned and gives back the same information in fewer characters.
223
+ *
224
+ * With a reader, `deliveries` is written grouped ({@link groupDeliveries});
225
+ * without one there is no telling whose a row is, so it stays one list.
90
226
  */
91
- export function compactInboxResult(result) {
227
+ export function compactInboxResult(result, reader) {
92
228
  const kindOfParty = new Map();
93
229
  if (Array.isArray(result.sources))
94
230
  for (const s of result.sources)
@@ -101,7 +237,10 @@ export function compactInboxResult(result) {
101
237
  const usual = usualPartyOf(result.deliveries);
102
238
  if (usual !== undefined)
103
239
  out.deliveriesPartyId = usual;
104
- out.deliveries = result.deliveries.map((d) => isRecord(d) ? compactDelivery(d, kindOfParty, usual) : d);
240
+ const whose = readerOf(result, reader);
241
+ out.deliveries = whose
242
+ ? groupDeliveries(result.deliveries.filter(isRecord), whose, (d) => compactDelivery(d, kindOfParty, usual))
243
+ : result.deliveries.map((d) => isRecord(d) ? compactDelivery(d, kindOfParty, usual) : d);
105
244
  }
106
245
  if (isRecord(result.session))
107
246
  out.session = compactSession(result.session, result.connectionNotices);
@@ -135,9 +274,9 @@ export function compactInboxResult(result) {
135
274
  return out;
136
275
  }
137
276
  /** The inbox result as one line of JSON: indentation is most of nothing. */
138
- export function compactTextResult(data) {
277
+ export function compactTextResult(data, reader) {
139
278
  return {
140
- content: [{ type: 'text', text: JSON.stringify(compactInboxResult(data)) }],
279
+ content: [{ type: 'text', text: JSON.stringify(compactInboxResult(data, reader)) }],
141
280
  };
142
281
  }
143
282
  /**
@@ -152,19 +291,17 @@ export function windowNote(deliveries, selfAgentIds, outOfReach) {
152
291
  const rows = Array.isArray(deliveries) ? deliveries.filter(isRecord) : [];
153
292
  if (!rows.length)
154
293
  return '';
155
- const mine = rows.filter((d) => selfAgentIds.includes(String(d.assigneeId))).length;
156
- const person = rows.filter((d) => !d.assigneeId && d.needsHuman === true).length;
157
- const others = rows.filter((d) => d.assigneeId && !selfAgentIds.includes(String(d.assigneeId))).length;
158
- // Rows nobody is asked to handle (no assignee, not the person's) are context.
159
- const context = rows.length - mine - person - others;
160
- const n = (k, one, many) => `${k} ${k === 1 ? one : many}`;
161
- const parts = [n(mine, 'is assigned to you', 'are assigned to you')];
162
- if (person)
163
- parts.push(`${person} ${person === 1 ? 'needs' : 'need'} your person`);
164
- if (others)
165
- parts.push(`${others} ${others === 1 ? 'is' : 'are'} for other assistants`);
166
- if (context)
167
- parts.push(`${context} ${context === 1 ? 'is' : 'are'} context`);
294
+ // The same rule the grouping uses, so each count is its section's size.
295
+ const count = (s) => rows.filter((d) => sectionOf(d, selfAgentIds) === s).length;
296
+ const words = {
297
+ forYou: 'yours to handle',
298
+ forYourPerson: 'pass these along to your person',
299
+ forOtherAssistants: 'your person’s other assistants',
300
+ context: 'nobody is asked to act',
301
+ };
302
+ // forYou is always named, so "nothing for you" is said rather than implied.
303
+ const parts = INBOX_SECTIONS.filter((s) => s === 'forYou' || count(s) > 0).map((s) => `${s} ${count(s)} (${words[s]})`);
168
304
  const reach = outOfReach ? ` ${outOfReach} of them ${outOfReach === 1 ? 'is' : 'are'} out of reach.` : '';
169
- return `Those counts are decisions and tasks, not messages. This window has ${n(rows.length, 'delivery', 'deliveries')}: ${parts.join(', ')}.${reach}`;
305
+ const total = `${rows.length} ${rows.length === 1 ? 'delivery' : 'deliveries'}`;
306
+ return `Those counts are decisions and tasks, not messages. This window has ${total}, grouped by who they are for, then by sender: ${parts.join(', ')}.${reach}`;
170
307
  }
package/dist/tools.js CHANGED
@@ -692,12 +692,14 @@ export function registerZiggsTools(server, creds, cfg) {
692
692
  }
693
693
  }
694
694
  const news = formatInboxToolResult(inbox, null, cfg.ZIGGS_WEB_URL, undefined, reach, undefined, { agentId: creds.agentId, ownerUserId: ownerPrincipalId(creds, cfg) });
695
+ // Whose rows are whose, so the deliveries come back grouped.
696
+ const reader = { agentIds: [creds.agentId], personId: ownerPrincipalId(creds, cfg) };
695
697
  // A long-poll is a continuation of a session that already oriented
696
698
  // itself, so it returns news only. A cold call is the session start, and
697
699
  // carries what the other two orientation tools used to each be called
698
700
  // for: the binding, the decisions, and the card.
699
701
  if (waitSeconds != null)
700
- return compactTextResult(news);
702
+ return compactTextResult(news, reader);
701
703
  const [bindingSettled, readsSettled] = await sessionStart;
702
704
  const session = bindingSettled.status === 'fulfilled'
703
705
  ? bindingSettled.value
@@ -712,7 +714,7 @@ export function registerZiggsTools(server, creds, cfg) {
712
714
  ...news,
713
715
  session,
714
716
  sessionActionsFetchError: readsSettled.reason.message,
715
- });
717
+ }, reader);
716
718
  }
717
719
  const actions = buildSessionActions(inbox, readsSettled.value, creds, cfg);
718
720
  // No prose list of next actions rides here: the calls are in
@@ -729,7 +731,7 @@ export function registerZiggsTools(server, creds, cfg) {
729
731
  ...(note && typeof actions.notice === 'string'
730
732
  ? { notice: `${actions.notice} ${note}` }
731
733
  : {}),
732
- });
734
+ }, reader);
733
735
  }
734
736
  catch (e) {
735
737
  return toolError(e);
@@ -754,7 +756,10 @@ export function registerZiggsTools(server, creds, cfg) {
754
756
  registerStrictTool(server, 'ziggs_inbox_claim', {}, destructive('Claim the inbox for this worker'), async () => {
755
757
  try {
756
758
  const client = inboxFor(creds);
757
- return compactTextResult(await client.getInbox());
759
+ return compactTextResult(await client.getInbox(), {
760
+ agentIds: [creds.agentId],
761
+ personId: ownerPrincipalId(creds, cfg),
762
+ });
758
763
  }
759
764
  catch (e) {
760
765
  return toolError(e);
@@ -235,8 +235,8 @@ export class WorkspaceRouter {
235
235
  ...(name === 'ziggs_inbox' && args.waitSeconds == null ? { rules: sessionRules('directory') } : {}),
236
236
  ...(note && typeof pending.notice === 'string' ? { notice: `${pending.notice} ${note}` } : {}),
237
237
  session: { workspaces },
238
- instruction: `Assigned to any of agentIds is your work. ${ASK_YOUR_HUMAN}`,
239
- });
238
+ instruction: `deliveries.forYou is your work; deliveries.forYourPerson is for your person to see. ${ASK_YOUR_HUMAN}`,
239
+ }, { agentIds: self.agentIds ?? [] });
240
240
  }
241
241
  if (name === 'ziggs_inbox_peek') {
242
242
  return result(await this.client.request(`inbox/peek?wait=${Number(args.waitSeconds ?? 0)}`));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ziggs-ai/ziggs-mcp",
3
- "version": "0.37.3",
3
+ "version": "0.37.4",
4
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": {
@@ -47,7 +47,7 @@
47
47
  },
48
48
  "dependencies": {
49
49
  "@modelcontextprotocol/sdk": "^1.29.0",
50
- "@ziggs-ai/api-client": "0.37.1",
50
+ "@ziggs-ai/api-client": "0.37.4",
51
51
  "dotenv": "^16.6.1",
52
52
  "zod": "^3.24.2",
53
53
  "zod-to-json-schema": "^3.25.1"