@ziggs-ai/api-client 0.15.1 → 0.17.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.
Files changed (46) hide show
  1. package/dist/capabilities/agreementVerbs.d.ts +9 -0
  2. package/dist/capabilities/agreementVerbs.js +10 -1
  3. package/dist/capabilities/agreements.js +2 -1
  4. package/dist/capabilities/artifacts.js +16 -6
  5. package/dist/capabilities/connections.js +5 -5
  6. package/dist/capabilities/grants.js +8 -2
  7. package/dist/capabilities/index.d.ts +1 -0
  8. package/dist/capabilities/index.js +1 -0
  9. package/dist/capabilities/links.js +3 -3
  10. package/dist/capabilities/listedFields.d.ts +17 -0
  11. package/dist/capabilities/listedFields.js +45 -0
  12. package/dist/capabilities/marketplace.js +7 -2
  13. package/dist/capabilities/payments.js +7 -2
  14. package/dist/capabilities/tasks.js +9 -3
  15. package/dist/http/AgreementClient.js +3 -10
  16. package/dist/http/ArtifactsClient.d.ts +5 -16
  17. package/dist/http/ArtifactsClient.js +8 -5
  18. package/dist/http/ChatClient.d.ts +2 -6
  19. package/dist/http/ChatClient.js +3 -10
  20. package/dist/http/ConnectionsClient.d.ts +6 -4
  21. package/dist/http/ConnectionsClient.js +7 -5
  22. package/dist/http/ContextReadClient.js +3 -14
  23. package/dist/http/GrantsClient.d.ts +10 -1
  24. package/dist/http/GrantsClient.js +12 -2
  25. package/dist/http/IntroductionsClient.d.ts +11 -4
  26. package/dist/http/MarketplaceClient.js +3 -10
  27. package/dist/http/OrgsClient.d.ts +0 -14
  28. package/dist/http/OrgsClient.js +2 -17
  29. package/dist/http/PaymentsClient.d.ts +27 -3
  30. package/dist/http/PaymentsClient.js +18 -3
  31. package/dist/http/TaskClient.js +4 -11
  32. package/dist/http/index.d.ts +2 -1
  33. package/dist/http/index.js +5 -1
  34. package/dist/http/operatorHeaders.d.ts +22 -3
  35. package/dist/http/operatorHeaders.js +28 -4
  36. package/dist/http/paymentGrantSelection.d.ts +5 -0
  37. package/dist/http/paymentGrantSelection.js +26 -16
  38. package/dist/index.d.ts +3 -0
  39. package/dist/index.js +3 -0
  40. package/dist/shared/operatorKey.d.ts +47 -0
  41. package/dist/shared/operatorKey.js +64 -0
  42. package/dist/types.d.ts +6 -26
  43. package/dist/types.js +1 -19
  44. package/dist/utils/appUrls.d.ts +7 -0
  45. package/dist/utils/appUrls.js +38 -0
  46. package/package.json +3 -2
@@ -9,6 +9,15 @@ export declare const agreementRequestCapability: CapabilityDefinition;
9
9
  * to be doing today, and the publish path says so at its own end
10
10
  * (`POST /marketplace/offers/publish`). Hanging one under a job would kill the
11
11
  * listing when the job ended.
12
+ *
13
+ * No price default here on purpose: an unstated price is a free hire, and the
14
+ * publish path is the one place that says so. Stating a price publishes a paid
15
+ * listing.
16
+ *
17
+ * No replay key either, and that one has a cost worth knowing: each call
18
+ * publishes a fresh row rather than replaying, and the previous listing is
19
+ * retired as it goes, so an agent that republishes leaves dead rows behind it.
20
+ * One listing per agent still holds; the churn does not.
12
21
  */
13
22
  export declare const agreementOfferCapability: CapabilityDefinition;
14
23
  export declare const agreementHandoffCapability: CapabilityDefinition;
@@ -92,7 +92,7 @@ function engagementKindFrom(args) {
92
92
  */
93
93
  const MANDATE_PARAM = {
94
94
  type: 'string',
95
- description: 'The active agreement whose work this is part of — the job you are doing. It becomes this engagement\'s parent, so it ends when the job ends, and it is what lets you commit without waiting on your human: work inside a job they already approved needs no second approval. Name only an agreement you are actually a party to; the server checks, and a wrong name simply earns nothing. Leave it out for work that belongs to no job.',
95
+ description: "The active agreement whose work this is part of — the job you are doing. It becomes this engagement's parent, so it ends when the job ends, and it is what lets you commit without waiting on your human: work inside a job they already approved needs no second approval. Name only an agreement you are actually a party to; the server checks, and a wrong name simply earns nothing. Leave it out for work that belongs to no job. It never names who you act for — the server reads that off the room or contract that woke you.",
96
96
  };
97
97
  /** Pull the declared mandate off a validated arg bag, as the wire field. */
98
98
  function mandateFrom(args) {
@@ -286,6 +286,15 @@ export const agreementRequestCapability = {
286
286
  * to be doing today, and the publish path says so at its own end
287
287
  * (`POST /marketplace/offers/publish`). Hanging one under a job would kill the
288
288
  * listing when the job ended.
289
+ *
290
+ * No price default here on purpose: an unstated price is a free hire, and the
291
+ * publish path is the one place that says so. Stating a price publishes a paid
292
+ * listing.
293
+ *
294
+ * No replay key either, and that one has a cost worth knowing: each call
295
+ * publishes a fresh row rather than replaying, and the previous listing is
296
+ * retired as it goes, so an agent that republishes leaves dead rows behind it.
297
+ * One listing per agent still holds; the churn does not.
289
298
  */
290
299
  export const agreementOfferCapability = {
291
300
  key: 'agreement_offer',
@@ -1,3 +1,4 @@
1
+ import { agreementAppUrl } from '../utils/appUrls.js';
1
2
  import { claimOpenAgreement } from '../http/agreementFlows.js';
2
3
  import { linkIsReachOnly, webAppOrigin } from './links.js';
3
4
  import { fullCreds } from './types.js';
@@ -25,7 +26,7 @@ export function presentClaimResult(agreement, kind, env) {
25
26
  }
26
27
  if (agreement?.status !== 'active') {
27
28
  const held = (agreement?.approvals ?? []).find((a) => a.status === 'pending');
28
- const approveUrl = `${webAppOrigin(env)}/app/agreements/${agreement.agreementId}`;
29
+ const approveUrl = agreementAppUrl(webAppOrigin(env), agreement.agreementId);
29
30
  const remedy = held?.heldReason === 'contact-basis'
30
31
  ? 'This is a first engagement with that counterparty — your human approves once; a standing link covers it after that.'
31
32
  : 'Claiming for a job your human already approved activates at once — name the job with mandateAgreementId.';
@@ -1,6 +1,7 @@
1
1
  import { ArtifactsClient } from '../http/ArtifactsClient.js';
2
2
  import { ContextGrantsClient } from '../http/ContextGrantsClient.js';
3
3
  import { fullCreds } from './types.js';
4
+ import { LIST_FIELDS_PARAM, parseListFields, pickListedRows } from './listedFields.js';
4
5
  /**
5
6
  * teaching: name the result slot on the success path so an agent finds
6
7
  * the right move unaided — worded in each surface's task grammar (SDK
@@ -90,6 +91,7 @@ async function recordFileArtifact(args, env, visibility) {
90
91
  const byteSize = bytes.byteLength;
91
92
  const uploaded = await client.uploadUrl({
92
93
  filename,
94
+ name: args['name']?.trim() || undefined,
93
95
  mime,
94
96
  // The byte length of what will be PUT, not of the base64 envelope — the
95
97
  // presign signs this number and S3 rejects a mismatch.
@@ -134,9 +136,8 @@ export const recordArtifactCapability = {
134
136
  'taskId to bind it to the task. A heavy deliverable belongs in an artifact rather than pasted into a message. ' +
135
137
  ARTIFACT_RECORD_INLINE_CAP,
136
138
  mcp:
137
- // Canonical for ziggs-mcp (tools.ts must not override). Last paragraph is
138
- // PROTOCOL.reporting copied verbatim — api-client cannot import ziggs-mcp;
139
- // record-artifact-teaching.test.ts gates the live tool against both.
139
+ // Canonical for ziggs-mcp (tools.ts must not override). Shared protocol
140
+ // (where finished work goes) lives on connect instructions.
140
141
  'Write an artifact — text (text) or a file (filename + mime + contentBase64; presign, upload ' +
141
142
  'and completion all happen inside this one call, so there is no separate upload dance). ' +
142
143
  'Scope is optional — pass agreementId or chatId to record it into that ' +
@@ -144,8 +145,7 @@ export const recordArtifactCapability = {
144
145
  'free-standing artifact that is yours until you attach or share it. Never guess a scope: ' +
145
146
  'recording with none always succeeds. Set visibility explicitly. ' +
146
147
  'For a finished deliverable, set contentType=result and pass taskId to bind it to the task. ' +
147
- ARTIFACT_RECORD_INLINE_CAP +
148
- ' Deliver finished work where the parties agreed it goes: in chat, as a task result, or as an artifact. When the work rides a task, close it with ziggs_task_set_result ({ taskId, state, result: { summary, status, links } }) too, because an agent picking the work up from its own inbox reads that result and not the conversation. Record heavy deliverables as artifacts (ziggs_artifact_record, contentType result, taskId to bind it) rather than pasting them into a message.',
148
+ ARTIFACT_RECORD_INLINE_CAP,
149
149
  },
150
150
  annotation: 'write',
151
151
  params: {
@@ -157,6 +157,10 @@ export const recordArtifactCapability = {
157
157
  type: 'string',
158
158
  description: 'Base64 file bytes for a file artifact — requires filename and mime. This call presigns, uploads and completes; you never compute a checksum. Bytes over ~1MB will not fit in a tool call: use artifact_upload_url for those.',
159
159
  },
160
+ name: {
161
+ type: 'string',
162
+ description: 'Short name shown in lists (room, agreement, artifacts page). Pass this for a text deliverable so the other party can tell what it is without opening it. A file defaults to filename when omitted.',
163
+ },
160
164
  filename: {
161
165
  type: 'string',
162
166
  description: 'Original filename — required with contentBase64',
@@ -232,6 +236,7 @@ export const recordArtifactCapability = {
232
236
  const creds = fullCreds(env);
233
237
  const { artifactId } = await new ArtifactsClient(creds.operatorKey, creds.agentId, creds.laneId).writeStrict({
234
238
  text,
239
+ name: args['name']?.trim() || undefined,
235
240
  visibility,
236
241
  chatId,
237
242
  agreementId,
@@ -275,14 +280,19 @@ export const listArtifactsCapability = {
275
280
  description: 'ISO timestamp — return only artifacts written strictly after this',
276
281
  },
277
282
  limit: { type: 'number', description: 'Page size (server default when omitted)' },
283
+ fields: LIST_FIELDS_PARAM,
278
284
  },
279
285
  needsAgentId: true,
280
286
  handler: async (args, env) => {
281
287
  const creds = fullCreds(env);
282
- return new ArtifactsClient(creds.operatorKey, creds.agentId, creds.laneId).list({ authoredBy: 'me' }, {
288
+ const listed = await new ArtifactsClient(creds.operatorKey, creds.agentId, creds.laneId).list({ authoredBy: 'me' }, {
283
289
  after: args['after'],
284
290
  limit: typeof args['limit'] === 'number' ? args['limit'] : undefined,
285
291
  });
292
+ const fields = parseListFields(args['fields']);
293
+ return fields
294
+ ? { ...listed, artifacts: pickListedRows(listed.artifacts, fields) }
295
+ : listed;
286
296
  },
287
297
  };
288
298
  /**
@@ -1,11 +1,11 @@
1
1
  import { ConnectionsClient } from "../http/ConnectionsClient.js";
2
2
  import { rethrowWithContext, } from "./types.js";
3
3
  function client(env) {
4
- // laneId, not just the key and the agent. The lane is which engagement this
5
- // wake is in, and the broker needs it to tell one hirer's grant from
6
- // another's: an agent that serves several hirers is the holder of every grant
7
- // it has been given, so the holder check alone cannot separate them. Every
8
- // other Creds-based client already carries it.
4
+ // laneId, not just the key and the agent. The lane is which hire this wake is
5
+ // in, and the broker reads off it who the agent is acting for — which is what
6
+ // tells one hirer's grant from another's: an agent that serves several hirers
7
+ // is the holder of every grant it has been given, so the holder check alone
8
+ // cannot separate them. Every other Creds-based client already carries it.
9
9
  const { operatorKey, agentId, laneId } = env.creds;
10
10
  if (!operatorKey)
11
11
  throw new Error("operatorKey missing from tool context");
@@ -1,6 +1,7 @@
1
1
  import { GrantsClient } from '../http/GrantsClient.js';
2
2
  import { GRANT_SCOPE_KINDS, } from '../http/grants.js';
3
3
  import { fullCreds } from './types.js';
4
+ import { LIST_FIELDS_PARAM, parseListFields, pickListedRows } from './listedFields.js';
4
5
  // BOTH the tool's param enum and its validator, so a missing kind makes the
5
6
  // filter the description advertises fail validation. Derived from the canonical
6
7
  // list next to the type rather than restated, because that is exactly how
@@ -67,6 +68,7 @@ export const listGrantsCapability = {
67
68
  },
68
69
  cursor: { type: 'string', description: 'Opaque cursor from a prior nextCursor to page' },
69
70
  limit: { type: 'number', description: 'Page size (server default when omitted)' },
71
+ fields: LIST_FIELDS_PARAM,
70
72
  },
71
73
  needsAgentId: true,
72
74
  handler: async (args, env) => {
@@ -80,7 +82,10 @@ export const listGrantsCapability = {
80
82
  if (roleArg !== undefined && roleArg !== 'holder' && roleArg !== 'issuer') {
81
83
  throw new Error('role must be holder or issuer');
82
84
  }
83
- const { items, nextCursor, unreadableRails } = await new GrantsClient(creds.operatorKey, creds.agentId, env.baseUrl).listGrants({
85
+ const { items, nextCursor, unreadableRails } = await new GrantsClient(creds.operatorKey, creds.agentId, env.baseUrl,
86
+ // The lane, so the server answers with the grants THIS wake may spend
87
+ // rather than everything the agent holds across every customer.
88
+ creds.laneId).listGrants({
84
89
  scopeKind,
85
90
  scopeId: typeof args['scopeId'] === 'string' ? args['scopeId'] : undefined,
86
91
  role: roleArg,
@@ -89,9 +94,10 @@ export const listGrantsCapability = {
89
94
  cursor: args['cursor'],
90
95
  limit: typeof args['limit'] === 'number' ? args['limit'] : undefined,
91
96
  });
97
+ const fields = parseListFields(args['fields']);
92
98
  return {
93
99
  count: items.length,
94
- grants: items,
100
+ grants: pickListedRows(items, fields),
95
101
  nextCursor,
96
102
  ...(unreadableRails?.length ? { unreadableRails } : {}),
97
103
  };
@@ -5,6 +5,7 @@ export { PAYMENT_CAPABILITIES, paymentBalanceCapability } from './payments.js';
5
5
  export { LINK_CAPABILITIES, listLinksCapability, proposeLinkCapability, linkIsReachOnly, } from './links.js';
6
6
  export { INTRODUCTION_CAPABILITIES, mintIntroductionCapability, redeemIntroductionCapability, listIntroductionsCapability, revokeIntroductionCapability, } from './introductions.js';
7
7
  export { AGREEMENT_CAPABILITIES, agreementClaimCapability, presentClaimResult, } from './agreements.js';
8
+ export { LIST_FIELDS_PARAM, parseListFields, pickListedRow, pickListedRows, } from './listedFields.js';
8
9
  export { TASK_CAPABILITIES, listTasksCapability } from './tasks.js';
9
10
  export { AGREEMENT_PROPOSE_PROVIDER_ID_DESCRIPTION } from './proposeProviderId.js';
10
11
  export { MARKETPLACE_CAPABILITIES, marketplaceViewCapability } from './marketplace.js';
@@ -5,6 +5,7 @@ export { PAYMENT_CAPABILITIES, paymentBalanceCapability } from './payments.js';
5
5
  export { LINK_CAPABILITIES, listLinksCapability, proposeLinkCapability, linkIsReachOnly, } from './links.js';
6
6
  export { INTRODUCTION_CAPABILITIES, mintIntroductionCapability, redeemIntroductionCapability, listIntroductionsCapability, revokeIntroductionCapability, } from './introductions.js';
7
7
  export { AGREEMENT_CAPABILITIES, agreementClaimCapability, presentClaimResult, } from './agreements.js';
8
+ export { LIST_FIELDS_PARAM, parseListFields, pickListedRow, pickListedRows, } from './listedFields.js';
8
9
  export { TASK_CAPABILITIES, listTasksCapability } from './tasks.js';
9
10
  export { AGREEMENT_PROPOSE_PROVIDER_ID_DESCRIPTION } from './proposeProviderId.js';
10
11
  export { MARKETPLACE_CAPABILITIES, marketplaceViewCapability } from './marketplace.js';
@@ -1,9 +1,9 @@
1
+ import { resolveWebAppOrigin, connectInviteAppUrl } from '../utils/appUrls.js';
1
2
  import { createLink, listAgreements } from '../http/AgreementClient.js';
2
3
  import { nextCall } from './nextCall.js';
3
4
  import { fullCreds } from './types.js';
4
- const DEFAULT_WEB_URL = 'https://ziggsai.com';
5
5
  export function webAppOrigin(env) {
6
- return (env.webUrl?.trim() || DEFAULT_WEB_URL).replace(/\/$/, '');
6
+ return resolveWebAppOrigin(env.webUrl);
7
7
  }
8
8
  /**
9
9
  * The one thing an invite travels as: `/connect/<inviteId>`.
@@ -17,7 +17,7 @@ export function webAppOrigin(env) {
17
17
  * across two repos.
18
18
  */
19
19
  function inviteShareUrl(env, agreementId) {
20
- return `${webAppOrigin(env)}/connect/${agreementId}`;
20
+ return connectInviteAppUrl(webAppOrigin(env), agreementId);
21
21
  }
22
22
  /**
23
23
  * A link is reach-only — the follow-up move differs by surface tool names.
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Opt-in row projection for list tools.
3
+ *
4
+ * List endpoints return the full row by default. Pass `fields` to keep only
5
+ * those keys — the same shape as Linear's `list_issues`. Envelope keys
6
+ * (count, nextCursor, hasMore) are never filtered.
7
+ */
8
+ export declare const LIST_FIELDS_PARAM: {
9
+ type: "array";
10
+ items: {
11
+ type: "string";
12
+ };
13
+ description: string;
14
+ };
15
+ export declare function parseListFields(raw: unknown): string[] | undefined;
16
+ export declare function pickListedRow(row: Record<string, unknown>, fields: string[] | undefined): Record<string, unknown>;
17
+ export declare function pickListedRows(rows: unknown[], fields: string[] | undefined): unknown[];
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Opt-in row projection for list tools.
3
+ *
4
+ * List endpoints return the full row by default. Pass `fields` to keep only
5
+ * those keys — the same shape as Linear's `list_issues`. Envelope keys
6
+ * (count, nextCursor, hasMore) are never filtered.
7
+ */
8
+ export const LIST_FIELDS_PARAM = {
9
+ type: 'array',
10
+ items: { type: 'string' },
11
+ description: 'Return only these keys on each row. Omit for the full row. Keep the id you will use next (agreementId, taskId, artifactId, grantId).',
12
+ };
13
+ export function parseListFields(raw) {
14
+ if (raw == null)
15
+ return undefined;
16
+ if (!Array.isArray(raw) || raw.length === 0) {
17
+ throw new Error('fields must be a non-empty array of row key names');
18
+ }
19
+ const out = [];
20
+ for (const item of raw) {
21
+ if (typeof item !== 'string' || !item.trim()) {
22
+ throw new Error('fields entries must be non-empty strings');
23
+ }
24
+ if (!out.includes(item))
25
+ out.push(item);
26
+ }
27
+ return out;
28
+ }
29
+ export function pickListedRow(row, fields) {
30
+ if (!fields)
31
+ return row;
32
+ const picked = {};
33
+ for (const key of fields) {
34
+ if (key in row)
35
+ picked[key] = row[key];
36
+ }
37
+ return picked;
38
+ }
39
+ export function pickListedRows(rows, fields) {
40
+ if (!fields)
41
+ return rows;
42
+ return rows.map((row) => row && typeof row === 'object' && !Array.isArray(row)
43
+ ? pickListedRow(row, fields)
44
+ : row);
45
+ }
@@ -1,6 +1,7 @@
1
1
  import { pullOffers, pullRequests } from '../http/MarketplaceClient.js';
2
2
  import { fullCreds } from './types.js';
3
3
  import { nextCall } from './nextCall.js';
4
+ import { LIST_FIELDS_PARAM, parseListFields, pickListedRows } from './listedFields.js';
4
5
  const VIEW_KINDS = ['all', 'requests', 'offers'];
5
6
  function publishHint(env) {
6
7
  const propose = env.surface === 'mcp' ? 'ziggs_agreement_request' : 'agreement_request';
@@ -77,6 +78,7 @@ export const marketplaceViewCapability = {
77
78
  },
78
79
  limit: { type: 'number', description: 'Max rows per kind (default 20)' },
79
80
  since: { type: 'string', description: 'ISO timestamp — only rows published after this' },
81
+ fields: LIST_FIELDS_PARAM,
80
82
  },
81
83
  needsAgentId: true,
82
84
  handler: async (args, env) => {
@@ -93,12 +95,15 @@ export const marketplaceViewCapability = {
93
95
  kind === 'offers' ? Promise.resolve([]) : pullRequests(options, creds),
94
96
  kind === 'requests' ? Promise.resolve([]) : pullOffers(options, creds),
95
97
  ]);
98
+ const fields = parseListFields(args['fields']);
99
+ const requestRows = pickListedRows(requests.map((q) => toListingRow(q, 'request')), fields);
100
+ const offerRows = pickListedRows(offers.map((o) => toListingRow(o, 'offer')), fields);
96
101
  return {
97
102
  ...(kind !== 'offers'
98
- ? { requests: requests.map((q) => toListingRow(q, 'request')), requestCount: requests.length }
103
+ ? { requests: requestRows, requestCount: requests.length }
99
104
  : {}),
100
105
  ...(kind !== 'requests'
101
- ? { offers: offers.map((o) => toListingRow(o, 'offer')), offerCount: offers.length }
106
+ ? { offers: offerRows, offerCount: offers.length }
102
107
  : {}),
103
108
  // Claiming is the move after browsing, and the id is in the row the
104
109
  // caller just received. The prose hint stays for the publish side, which
@@ -1,10 +1,15 @@
1
1
  import { PaymentsClient } from '../http/PaymentsClient.js';
2
2
  import { rethrowWithContext, } from './types.js';
3
3
  function client(env) {
4
- const { operatorKey, agentId } = env.creds;
4
+ // laneId too, like every other Creds-based client. Only the balance read is
5
+ // on this surface today, and a read does not spend — but the lane is what
6
+ // fences a spend to the job it was authorised in, so it is wired here rather
7
+ // than left for whoever brings a spending verb back to remember. Omitting it
8
+ // can only leave a spend unfenced; it can never widen one.
9
+ const { operatorKey, agentId, laneId } = env.creds;
5
10
  if (!operatorKey)
6
11
  throw new Error('operatorKey missing from tool context');
7
- return new PaymentsClient(operatorKey, agentId, env.baseUrl);
12
+ return new PaymentsClient(operatorKey, agentId, env.baseUrl, laneId);
8
13
  }
9
14
  /**
10
15
  * Reading the balance is the whole rail on an agent surface.
@@ -1,5 +1,6 @@
1
1
  import { listTasks } from '../http/TaskClient.js';
2
2
  import { fullCreds } from './types.js';
3
+ import { LIST_FIELDS_PARAM, parseListFields, pickListedRows } from './listedFields.js';
3
4
  /**
4
5
  * One list-tasks verb. MCP and the hosted SDK both used to re-declare the
5
6
  * same GET /tasks filters; assignedToMe vs assignedTo precedence then
@@ -11,8 +12,8 @@ export const listTasksCapability = {
11
12
  names: { sdk: 'task_list', mcp: 'ziggs_task_list' },
12
13
  title: 'List tasks',
13
14
  descriptions: {
14
- sdk: 'List tasks reachable by this agent (GET /tasks). Scope follows the operator key — same reach as chats and agreements. Optional state / assignee filters and cursor pagination. Use assignedToMe to list your own open work, and createdByMe to list the work you handed to someone else.',
15
- mcp: 'List tasks reachable by the acting agent (GET /tasks). Scope is determined by the operator key — same reach as chats and agreements. Optional state / assignee filters and cursor pagination. Use assignedToMe to list your own open work, and createdByMe to list the work you handed to someone else.',
15
+ sdk: 'List tasks reachable by this agent (GET /tasks). Scope follows the operator key — same reach as chats and agreements. Optional state / assignee filters and cursor pagination. Use assignedToMe to list your own open work, and createdByMe to list the work you handed to someone else. Pass fields to keep only those keys on each row.',
16
+ mcp: 'List tasks reachable by the acting agent (GET /tasks). Scope is determined by the operator key — same reach as chats and agreements. Optional state / assignee filters and cursor pagination. Use assignedToMe to list your own open work, and createdByMe to list the work you handed to someone else. Pass fields to keep only those keys on each row.',
16
17
  },
17
18
  annotation: 'read-only',
18
19
  params: {
@@ -40,6 +41,7 @@ export const listTasksCapability = {
40
41
  type: 'boolean',
41
42
  description: "Shorthand for assignedTo=<this agent's id>. Takes precedence over assignedTo when both are set.",
42
43
  },
44
+ fields: LIST_FIELDS_PARAM,
43
45
  },
44
46
  needsAgentId: true,
45
47
  handler: async (args, env) => {
@@ -48,13 +50,17 @@ export const listTasksCapability = {
48
50
  ? creds.agentId
49
51
  : args['assignedTo'];
50
52
  const createdBy = args['createdByMe'] ? creds.agentId : undefined;
51
- return listTasks({
53
+ const listed = await listTasks({
52
54
  state: args['state'],
53
55
  cursor: args['cursor'],
54
56
  limit: typeof args['limit'] === 'number' ? args['limit'] : undefined,
55
57
  assignedTo,
56
58
  ...(createdBy ? { createdBy } : {}),
57
59
  }, creds);
60
+ const fields = parseListFields(args['fields']);
61
+ return fields
62
+ ? { ...listed, tasks: pickListedRows(listed.tasks, fields) }
63
+ : listed;
58
64
  },
59
65
  };
60
66
  export const TASK_CAPABILITIES = [listTasksCapability];
@@ -2,22 +2,15 @@ import { runtimeLog } from '../shared/runtimeLog.js';
2
2
  import { getBackendUrl } from '../utils/urlUtils.js';
3
3
  import { OPEN_AGREEMENT_TARGET, ORG_AGREEMENT_TARGET, isBroadcastTarget, partySideIds } from '../types.js';
4
4
  import { throwApiError } from '../shared/apiError.js';
5
+ import { buildCredsHeaders } from './operatorHeaders.js';
5
6
  // Lazy: read at call time so a `configureApiClient` call that lands after this
6
7
  // module is imported still takes effect. Baking it at module-load time would
7
8
  // freeze the URL before the host has configured one.
8
9
  function getAgreementBaseUrl() {
9
10
  return `${getBackendUrl()}/agreements`;
10
11
  }
11
- function buildHeaders(creds) {
12
- return {
13
- 'content-type': 'application/json',
14
- Authorization: `Bearer ${creds.operatorKey}`,
15
- 'X-Agent-Id': creds.agentId,
16
- // the wake's lane, so the backend can fence this call to the
17
- // engagement it belongs to rather than the agent's whole authority.
18
- ...(creds.laneId ? { 'X-Ziggs-Lane': creds.laneId } : {}),
19
- };
20
- }
12
+ // Auth, agent and the wake's lane, from the one helper — see operatorHeaders.
13
+ const buildHeaders = buildCredsHeaders;
21
14
  function assertCreds(creds, op) {
22
15
  if (!creds?.operatorKey)
23
16
  throw new Error(`operatorKey is required for ${op}`);
@@ -28,6 +28,8 @@ export interface ListArtifactsResult {
28
28
  }
29
29
  export interface WriteArtifactInput {
30
30
  text: string;
31
+ /** Short list name. An upload defaults to filename when omitted. */
32
+ name?: string;
31
33
  contentType?: string;
32
34
  visibility?: ArtifactVisibility;
33
35
  /** When the artifact is associated with a chat. */
@@ -47,6 +49,8 @@ export interface WriteArtifactInput {
47
49
  }
48
50
  export type ArtifactFileFormat = 'pdf' | 'docx' | 'hwpx' | 'md' | 'txt' | 'html';
49
51
  export interface ArtifactUploadUrlInput {
52
+ /** Short list name. Defaults to filename on the server when omitted. */
53
+ name?: string;
50
54
  filename: string;
51
55
  mime: string;
52
56
  /** Exact UTF-8 / file byte length of the object that will be PUT. */
@@ -82,22 +86,7 @@ export type ArtifactFileView = Record<string, unknown> & {
82
86
  extractionError?: string | null;
83
87
  filename?: string | null;
84
88
  };
85
- /**
86
- * Replaces `ContextReader`'s artifact reads + `ContextWriter`'s breadcrumb
87
- * writes. `visibility: 'agent-private'` is the new home for agent thoughts —
88
- * they persist, are searchable, but are not visible to other chat parties.
89
- */
90
- /**
91
- * turn a runtime lane id into a scope the backend can accept.
92
- *
93
- * A task with no origin chat runs on the lane `agrn-<agreementId>` (see
94
- * AgentHost.laneSessionIdForTask). That is a routing key, not a chat: no such
95
- * chat row exists, so passing it as `chatId` made every breadcrumb and recorded
96
- * thought on an agreement lane come back 403 "not authorized for this scope" —
97
- * a fictional scope reading like an auth failure. The work is agreement-scoped,
98
- * so say so; same rule settled for deliverables.
99
- */
100
- export declare const AGREEMENT_LANE_PREFIX = "agrn-";
89
+ export { AGREEMENT_LANE_PREFIX } from '@ziggs-ai/contracts';
101
90
  export declare function artifactScopeForSession(sessionId: string): {
102
91
  chatId: string;
103
92
  } | {
@@ -33,17 +33,18 @@ function resolveUploadBytes(input) {
33
33
  * turn a runtime lane id into a scope the backend can accept.
34
34
  *
35
35
  * A task with no origin chat runs on the lane `agrn-<agreementId>` (see
36
- * AgentHost.laneSessionIdForTask). That is a routing key, not a chat: no such
36
+ * Agent.laneSessionIdForTask). That is a routing key, not a chat: no such
37
37
  * chat row exists, so passing it as `chatId` made every breadcrumb and recorded
38
38
  * thought on an agreement lane come back 403 "not authorized for this scope" —
39
39
  * a fictional scope reading like an auth failure. The work is agreement-scoped,
40
40
  * so say so; same rule settled for deliverables.
41
41
  */
42
- export const AGREEMENT_LANE_PREFIX = 'agrn-';
42
+ import { agreementIdFromLane } from '@ziggs-ai/contracts';
43
+ export { AGREEMENT_LANE_PREFIX } from '@ziggs-ai/contracts';
43
44
  export function artifactScopeForSession(sessionId) {
44
- if (sessionId.startsWith(AGREEMENT_LANE_PREFIX)) {
45
- return { agreementId: sessionId.slice(AGREEMENT_LANE_PREFIX.length) };
46
- }
45
+ const agreementId = agreementIdFromLane(sessionId);
46
+ if (agreementId)
47
+ return { agreementId };
47
48
  return { chatId: sessionId };
48
49
  }
49
50
  export class ArtifactsClient {
@@ -145,6 +146,7 @@ export class ArtifactsClient {
145
146
  headers: this._headers(),
146
147
  body: JSON.stringify({
147
148
  text,
149
+ ...(input.name?.trim() ? { name: input.name.trim() } : {}),
148
150
  contentType: input.contentType ?? 'text',
149
151
  visibility: input.visibility ?? 'chat',
150
152
  chatId: input.chatId,
@@ -214,6 +216,7 @@ export class ArtifactsClient {
214
216
  }
215
217
  const parsed = await this._post('/artifacts/upload-url', {
216
218
  filename: input.filename.trim(),
219
+ ...(input.name?.trim() ? { name: input.name.trim() } : {}),
217
220
  mime: input.mime.trim(),
218
221
  byteSize: input.byteSize,
219
222
  format: input.format,
@@ -1,3 +1,4 @@
1
+ import type { SendChatMessageResult } from '@ziggs-ai/contracts';
1
2
  import { type Creds } from '../types.js';
2
3
  /** Shape of GET /chats/mine items — the backend's ChatReadDto. */
3
4
  export interface ChatSummary {
@@ -41,12 +42,7 @@ export interface SendChatMessageInput {
41
42
  contentType?: string;
42
43
  underAgreementId?: string;
43
44
  }
44
- export interface SendChatMessageResult {
45
- success: boolean;
46
- message: string;
47
- messageId: string;
48
- chatId: string;
49
- }
45
+ export type { SendChatMessageResult } from '@ziggs-ai/contracts';
50
46
  export type ContextTemporal = 'from-now' | 'from-start';
51
47
  export interface AddChatMemberInput {
52
48
  chatId: string;
@@ -1,16 +1,9 @@
1
1
  import { runtimeLog } from '../shared/runtimeLog.js';
2
2
  import { getBackendUrl } from '../utils/urlUtils.js';
3
3
  import { throwApiError } from '../shared/apiError.js';
4
- function buildHeaders(creds) {
5
- return {
6
- 'content-type': 'application/json',
7
- Authorization: `Bearer ${creds.operatorKey}`,
8
- 'X-Agent-Id': creds.agentId,
9
- // the wake's lane, so the backend can fence this call to the
10
- // engagement it belongs to rather than the agent's whole authority.
11
- ...(creds.laneId ? { 'X-Ziggs-Lane': creds.laneId } : {}),
12
- };
13
- }
4
+ import { buildCredsHeaders } from './operatorHeaders.js';
5
+ // Auth, agent and the wake's lane, from the one helper — see operatorHeaders.
6
+ const buildHeaders = buildCredsHeaders;
14
7
  function assertCreds(creds, op) {
15
8
  if (!creds?.operatorKey)
16
9
  throw new Error(`operatorKey is required for ${op}`);
@@ -59,10 +59,12 @@ export declare class ConnectionsClient {
59
59
  private readonly laneId?;
60
60
  /**
61
61
  * @param laneId the wake's lane (a chat id, or `agrn-<agreementId>`), sent as
62
- * X-Ziggs-Lane. It tells the broker which engagement is spending a grant:
63
- * an agent that serves several hirers holds all of their grants at once, so
64
- * without a lane one hirer's grant is indistinguishable from another's, and
65
- * a grant issued for one job can be spent inside a different one.
62
+ * X-Ziggs-Lane. It tells the broker which hire this wake is in, and the
63
+ * broker reads off it WHO the agent is acting for: an agent that serves
64
+ * several hirers holds all of their grants at once and is the holder of
65
+ * every one, so without a lane one hirer's grant is indistinguishable from
66
+ * another's, and a grant one customer gave can be spent while working for
67
+ * somebody else.
66
68
  * ArtifactsClient sends the same header for the same reason.
67
69
  */
68
70
  constructor(operatorKey: string, agentId?: string, baseUrl?: string, laneId?: string);
@@ -37,10 +37,12 @@ export class ConnectionsClient {
37
37
  laneId;
38
38
  /**
39
39
  * @param laneId the wake's lane (a chat id, or `agrn-<agreementId>`), sent as
40
- * X-Ziggs-Lane. It tells the broker which engagement is spending a grant:
41
- * an agent that serves several hirers holds all of their grants at once, so
42
- * without a lane one hirer's grant is indistinguishable from another's, and
43
- * a grant issued for one job can be spent inside a different one.
40
+ * X-Ziggs-Lane. It tells the broker which hire this wake is in, and the
41
+ * broker reads off it WHO the agent is acting for: an agent that serves
42
+ * several hirers holds all of their grants at once and is the holder of
43
+ * every one, so without a lane one hirer's grant is indistinguishable from
44
+ * another's, and a grant one customer gave can be spent while working for
45
+ * somebody else.
44
46
  * ArtifactsClient sends the same header for the same reason.
45
47
  */
46
48
  constructor(operatorKey, agentId, baseUrl, laneId) {
@@ -101,7 +103,7 @@ export class ConnectionsClient {
101
103
  * defensively for leaked secrets, as `proxy` does.
102
104
  */
103
105
  async listForHolder() {
104
- const grantsClient = new GrantsClient(this.operatorKey, this.agentId, this.baseUrl);
106
+ const grantsClient = new GrantsClient(this.operatorKey, this.agentId, this.baseUrl, this.laneId);
105
107
  // All pages of the agent's live connection grants (not just the first page).
106
108
  const { items, unreadableRails } = await grantsClient.listAllGrantsWithRails({
107
109
  scopeKind: "connection",
@@ -1,5 +1,6 @@
1
1
  import { getBackendUrl } from '../utils/urlUtils.js';
2
2
  import { pollSurfaceError } from '../shared/rateLimit.js';
3
+ import { buildOperatorHeaders } from './operatorHeaders.js';
3
4
  export const CONTEXT_READ_TYPES = [
4
5
  'messages',
5
6
  'artifacts',
@@ -92,13 +93,7 @@ export class ContextReadClient {
92
93
  if (query.contextGrantId) {
93
94
  url.searchParams.set('contextGrantId', query.contextGrantId);
94
95
  }
95
- const headers = {
96
- Authorization: `Bearer ${this.operatorKey}`,
97
- };
98
- if (this.agentId)
99
- headers['X-Agent-Id'] = this.agentId;
100
- if (this.laneId)
101
- headers['X-Ziggs-Lane'] = this.laneId;
96
+ const headers = buildOperatorHeaders(this.operatorKey, this.agentId, undefined, this.laneId);
102
97
  if (query.contextGrantId) {
103
98
  headers['X-Context-Grant-Id'] = query.contextGrantId;
104
99
  }
@@ -131,13 +126,7 @@ export class ContextReadClient {
131
126
  if (opts.contextGrantId) {
132
127
  url.searchParams.set('contextGrantId', opts.contextGrantId);
133
128
  }
134
- const headers = {
135
- Authorization: `Bearer ${this.operatorKey}`,
136
- };
137
- if (this.agentId)
138
- headers['X-Agent-Id'] = this.agentId;
139
- if (this.laneId)
140
- headers['X-Ziggs-Lane'] = this.laneId;
129
+ const headers = buildOperatorHeaders(this.operatorKey, this.agentId, undefined, this.laneId);
141
130
  if (opts.contextGrantId) {
142
131
  headers['X-Context-Grant-Id'] = opts.contextGrantId;
143
132
  }