@ziggs-ai/api-client 0.14.3 → 0.16.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 +29 -3
  3. package/dist/capabilities/agreements.js +2 -1
  4. package/dist/capabilities/artifacts.js +16 -6
  5. package/dist/capabilities/connections.d.ts +1 -1
  6. package/dist/capabilities/connections.js +82 -59
  7. package/dist/capabilities/grants.js +8 -2
  8. package/dist/capabilities/index.d.ts +1 -0
  9. package/dist/capabilities/index.js +1 -0
  10. package/dist/capabilities/links.js +12 -4
  11. package/dist/capabilities/listedFields.d.ts +17 -0
  12. package/dist/capabilities/listedFields.js +45 -0
  13. package/dist/capabilities/marketplace.js +7 -2
  14. package/dist/capabilities/payments.js +7 -2
  15. package/dist/capabilities/tasks.js +9 -3
  16. package/dist/http/AgreementClient.d.ts +29 -6
  17. package/dist/http/AgreementClient.js +2 -0
  18. package/dist/http/ArtifactsClient.d.ts +5 -16
  19. package/dist/http/ArtifactsClient.js +8 -5
  20. package/dist/http/ChatClient.d.ts +2 -6
  21. package/dist/http/ConnectionsClient.d.ts +15 -4
  22. package/dist/http/ConnectionsClient.js +53 -39
  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.d.ts +7 -0
  27. package/dist/http/MarketplaceClient.js +12 -3
  28. package/dist/http/OrgsClient.d.ts +0 -14
  29. package/dist/http/OrgsClient.js +2 -17
  30. package/dist/http/PaymentsClient.d.ts +47 -5
  31. package/dist/http/PaymentsClient.js +66 -22
  32. package/dist/http/TaskClient.js +1 -1
  33. package/dist/http/agreementFlows.js +9 -3
  34. package/dist/http/index.d.ts +1 -1
  35. package/dist/http/index.js +1 -1
  36. package/dist/http/paymentGrantSelection.d.ts +58 -0
  37. package/dist/http/paymentGrantSelection.js +121 -0
  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) {
@@ -122,10 +122,27 @@ const COUNTERPARTY_PARAM = {
122
122
  required: true,
123
123
  description: 'Agent or user id of the counterparty.',
124
124
  };
125
+ /**
126
+ * Optional, because the server has always accepted a proposal without one and
127
+ * the client was the only thing refusing.
128
+ *
129
+ * The room is where a conversation about the terms happens; it is not how the
130
+ * counterparty is TOLD. A directed proposal is delivered to the party slots
131
+ * (the create-time agreement event targets parties, not a chat), so a chatless
132
+ * proposal lands in their inbox exactly like any other. What is lost is
133
+ * narrower than it sounds: the notice posted INTO a room, which is worth
134
+ * nothing when there is no room.
135
+ *
136
+ * Requiring it here meant a bookkeeping agreement between parties who have no
137
+ * conversation had to invent a chat id to exist. The lab's own self-hire did:
138
+ * it passed `lab-hire-<run>`, a chat that does not exist, which the server
139
+ * tolerates by falling back to the credential's tenant. A fiction the caller
140
+ * was forced to write down is worse than an absent field.
141
+ */
125
142
  const CHAT_PARAM = {
126
143
  type: 'string',
127
- required: true,
128
- description: 'The room this is proposed in — the counterparty reads it there.',
144
+ required: false,
145
+ description: 'The room this is proposed in, when there is one — the counterparty can read and discuss the terms there. Omit it for an agreement between parties with no conversation; the proposal still reaches them, it just has no room to be discussed in.',
129
146
  };
130
147
  /** Marketplace follow-up: a published listing is claimed, never countered. */
131
148
  function publishedNext(env) {
@@ -269,6 +286,15 @@ export const agreementRequestCapability = {
269
286
  * to be doing today, and the publish path says so at its own end
270
287
  * (`POST /marketplace/offers/publish`). Hanging one under a job would kill the
271
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.
272
298
  */
273
299
  export const agreementOfferCapability = {
274
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/work/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,4 +1,4 @@
1
- import { type CapabilityDefinition } from './types.js';
1
+ import { type CapabilityDefinition } from "./types.js";
2
2
  export declare const connectionProxyCapability: CapabilityDefinition;
3
3
  export declare const requestConnectionCapability: CapabilityDefinition;
4
4
  export declare const CONNECTION_CAPABILITIES: CapabilityDefinition[];
@@ -1,115 +1,138 @@
1
- import { ConnectionsClient } from '../http/ConnectionsClient.js';
2
- import { rethrowWithContext, } from './types.js';
1
+ import { ConnectionsClient } from "../http/ConnectionsClient.js";
2
+ import { rethrowWithContext, } from "./types.js";
3
3
  function client(env) {
4
- const { operatorKey, agentId } = env.creds;
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
+ const { operatorKey, agentId, laneId } = env.creds;
5
10
  if (!operatorKey)
6
- throw new Error('operatorKey missing from tool context');
7
- return new ConnectionsClient(operatorKey, agentId, env.baseUrl);
11
+ throw new Error("operatorKey missing from tool context");
12
+ return new ConnectionsClient(operatorKey, agentId, env.baseUrl, laneId);
8
13
  }
9
14
  export const connectionProxyCapability = {
10
- key: 'connection_proxy',
11
- names: { sdk: 'connection_proxy', mcp: 'ziggs_connection_proxy' },
12
- title: 'Use a stored connection',
15
+ key: "connection_proxy",
16
+ names: { sdk: "connection_proxy", mcp: "ziggs_connection_proxy" },
17
+ title: "Use a stored connection",
13
18
  descriptions: {
14
19
  sdk: "Use a named-connector stored connection (e.g. the owner's GitHub/Jira) without ever seeing the credential. " +
15
- 'Calls the backend connections proxy with a grant the owner issued to this agent. ' +
20
+ "Calls the backend connections proxy with a grant the owner issued to this agent. " +
16
21
  'Not for remote MCP servers (provider "mcp") — those use mcp_tool_call / mcp_tools_list; proxy refuses them with "Unknown provider: mcp". ' +
17
22
  "Don't know connectionId/grantId yet? Use grant_list (scopeKind=connection) or connection_list_grants first.",
18
23
  mcp: "Use a named-connector stored connection (e.g. the owner's GitHub/Jira — NOT an agent-to-agent Link, see ziggs_link_list) without ever seeing the credential. " +
19
- 'Calls the backend connections proxy with a grant the owner issued to this agent. ' +
24
+ "Calls the backend connections proxy with a grant the owner issued to this agent. " +
20
25
  'Not for remote MCP servers (provider "mcp") — those use ziggs_mcp_tool_call / ziggs_mcp_tools_list; proxy refuses them with "Unknown provider: mcp". ' +
21
26
  "Don't know connectionId/grantId yet? Call ziggs_connection_list first.",
22
27
  },
23
- annotation: 'write',
28
+ annotation: "write",
24
29
  params: {
25
- connectionId: { type: 'string', required: true, description: 'Connection to act on' },
30
+ connectionId: {
31
+ type: "string",
32
+ required: true,
33
+ description: "Connection to act on",
34
+ },
26
35
  grantId: {
27
- type: 'string',
36
+ type: "string",
37
+ required: true,
38
+ description: "Grant the owner issued to this agent for the connection",
39
+ },
40
+ action: {
41
+ type: "string",
28
42
  required: true,
29
- description: 'Grant the owner issued to this agent for the connection',
43
+ description: "Provider action, e.g. repo:read",
44
+ },
45
+ payload: {
46
+ type: "object",
47
+ description: "Action-specific arguments (provider-defined)",
30
48
  },
31
- action: { type: 'string', required: true, description: 'Provider action, e.g. repo:read' },
32
- payload: { type: 'object', description: 'Action-specific arguments (provider-defined)' },
33
49
  },
34
50
  needsAgentId: true,
35
51
  handler: async (args, env) => {
36
- if (!args['connectionId'])
37
- throw new Error('connectionId is required');
38
- if (!args['grantId'])
39
- throw new Error('grantId is required');
40
- if (!args['action'])
41
- throw new Error('action is required');
52
+ if (!args["connectionId"])
53
+ throw new Error("connectionId is required");
54
+ if (!args["grantId"])
55
+ throw new Error("grantId is required");
56
+ if (!args["action"])
57
+ throw new Error("action is required");
42
58
  try {
43
59
  const result = await client(env).proxy({
44
- connectionId: args['connectionId'],
45
- grantId: args['grantId'],
46
- action: args['action'],
47
- payload: args['payload'],
60
+ connectionId: args["connectionId"],
61
+ grantId: args["grantId"],
62
+ action: args["action"],
63
+ payload: args["payload"],
48
64
  });
49
- return { ok: true, action: args['action'], result };
65
+ return { ok: true, action: args["action"], result };
50
66
  }
51
67
  catch (e) {
52
- rethrowWithContext(e, 'Connection proxy failed');
68
+ rethrowWithContext(e, "Connection proxy failed");
53
69
  }
54
70
  },
55
71
  };
56
72
  export const requestConnectionCapability = {
57
- key: 'connection_request',
58
- names: { sdk: 'connection_request', mcp: 'ziggs_connection_request' },
59
- title: 'Ask your principal for a connection',
73
+ key: "connection_request",
74
+ names: { sdk: "connection_request", mcp: "ziggs_connection_request" },
75
+ title: "Ask your principal for a connection",
60
76
  descriptions: {
61
- sdk: 'Ask your principal (the human) to connect a remote MCP server and grant you the listed tools. ' +
62
- 'Opens a connection-consent agreement as an approvable card in the chat you pass — the human approves it there like any other agreement. ' +
63
- 'On approval the server is connected (browser OAuth if needed) and you are granted the tools; call them with mcp_tool_call / mcp_tools_list (not connection_proxy).',
64
- mcp: 'Ask your principal (the human) to connect a remote MCP server and grant you the listed tools. ' +
65
- 'Opens a connection-consent agreement as an approvable card in the chat you pass — the human approves it there like any other agreement (there is no MCP tool to approve it, so tell them to approve it in the chat). ' +
66
- 'On approval the server is connected (browser OAuth if needed) and you are granted the tools; the result shows up in ziggs_connection_list for use with ziggs_mcp_tools_list / ziggs_mcp_tool_call.',
77
+ sdk: "Ask your principal (the human) to connect a remote MCP server and grant you the listed tools. " +
78
+ "Opens a connection-consent agreement as an approvable card in the chat you pass — the human approves it there like any other agreement. " +
79
+ "On approval the server is connected (browser OAuth if needed) and you are granted the tools; call them with mcp_tool_call / mcp_tools_list (not connection_proxy).",
80
+ mcp: "Ask your principal (the human) to connect a remote MCP server and grant you the listed tools. " +
81
+ "Opens a connection-consent agreement as an approvable card in the chat you pass — the human approves it there like any other agreement (there is no MCP tool to approve it, so tell them to approve it in the chat). " +
82
+ "On approval the server is connected (browser OAuth if needed) and you are granted the tools; the result shows up in ziggs_connection_list for use with ziggs_mcp_tools_list / ziggs_mcp_tool_call.",
67
83
  },
68
- annotation: 'write',
84
+ annotation: "write",
69
85
  params: {
70
86
  chatId: {
71
- type: 'string',
87
+ type: "string",
88
+ required: true,
89
+ description: "The chat you are working in — the consent card is opened there",
90
+ },
91
+ serverUrl: {
92
+ type: "string",
72
93
  required: true,
73
- description: 'The chat you are working in — the consent card is opened there',
94
+ description: "Remote MCP server URL (https)",
74
95
  },
75
- serverUrl: { type: 'string', required: true, description: 'Remote MCP server URL (https)' },
76
96
  tools: {
77
- type: 'array',
78
- items: { type: 'string' },
97
+ type: "array",
98
+ items: { type: "string" },
79
99
  required: true,
80
100
  description: "Tool names you want — become the grant's allowed_actions caveats",
81
101
  },
82
- reason: { type: 'string', description: 'Plain-language reason shown to the human deciding' },
102
+ reason: {
103
+ type: "string",
104
+ description: "Plain-language reason shown to the human deciding",
105
+ },
83
106
  },
84
107
  needsAgentId: true,
85
108
  handler: async (args, env) => {
86
- if (!args['chatId'])
87
- throw new Error('chatId is required');
88
- if (!args['serverUrl'])
89
- throw new Error('serverUrl is required');
90
- const tools = args['tools'];
109
+ if (!args["chatId"])
110
+ throw new Error("chatId is required");
111
+ if (!args["serverUrl"])
112
+ throw new Error("serverUrl is required");
113
+ const tools = args["tools"];
91
114
  if (!Array.isArray(tools) || tools.length === 0) {
92
- throw new Error('tools must be a non-empty array of tool names');
115
+ throw new Error("tools must be a non-empty array of tool names");
93
116
  }
94
117
  try {
95
118
  const result = await client(env).requestMcpConnection({
96
- chatId: args['chatId'],
97
- serverUrl: args['serverUrl'],
119
+ chatId: args["chatId"],
120
+ serverUrl: args["serverUrl"],
98
121
  tools: tools,
99
- reason: args['reason'],
122
+ reason: args["reason"],
100
123
  });
101
124
  return {
102
125
  ok: true,
103
126
  ...result,
104
- note: env.surface === 'mcp'
105
- ? 'A connection-consent card is now in the chat awaiting your principal. Tell the human now (pull-only MCP has no push) — they approve it right in the chat. ' +
106
- 'Once approved, the connection + grant appear in ziggs_connection_list for ziggs_mcp_tools_list / ziggs_mcp_tool_call.'
107
- : 'A connection-consent card is now in the chat awaiting your principal — they approve it right there. ' +
108
- 'Once approved, the connection + grant appear in grant_list (scopeKind=connection) / connection_list_grants for mcp_tools_list / mcp_tool_call.',
127
+ note: env.surface === "mcp"
128
+ ? "A connection-consent card is now in the chat awaiting your principal. Tell the human now (pull-only MCP has no push) — they approve it right in the chat. " +
129
+ "Once approved, the connection + grant appear in ziggs_connection_list for ziggs_mcp_tools_list / ziggs_mcp_tool_call."
130
+ : "A connection-consent card is now in the chat awaiting your principal — they approve it right there. " +
131
+ "Once approved, the connection + grant appear in grant_list (scopeKind=connection) / connection_list_grants for mcp_tools_list / mcp_tool_call.",
109
132
  };
110
133
  }
111
134
  catch (e) {
112
- rethrowWithContext(e, 'Connection request failed');
135
+ rethrowWithContext(e, "Connection request failed");
113
136
  }
114
137
  },
115
138
  };
@@ -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.
@@ -159,7 +159,7 @@ export const proposeLinkCapability = {
159
159
  handler: async (args, env) => {
160
160
  const to = args['to']?.trim();
161
161
  const maxClaims = args['maxClaims'];
162
- const { shareUrl } = await createLink({
162
+ const { shareUrl, agreementId } = await createLink({
163
163
  ...(to ? { to } : {}),
164
164
  ...(args['message'] ? { description: args['message'] } : {}),
165
165
  ...(maxClaims == null || to ? {} : { maxClaims }),
@@ -168,9 +168,17 @@ export const proposeLinkCapability = {
168
168
  // back. That is the whole discipline: the caller knows whether it named
169
169
  // somebody, so branching on it discloses nothing, while branching on
170
170
  // anything the server learned about the target would.
171
+ //
172
+ // `agreementId` follows the same discipline: the route carries it for an
173
+ // agent id and an open invite and never for an email, so which of the two it
174
+ // is depends on what the caller passed and on nothing the server learned. It
175
+ // is what lets a caller reference the row and check on it; without it,
176
+ // several agents connecting in one run each minted a link toward the others
177
+ // in both directions, because none of them could ask.
171
178
  return {
172
179
  status: 'sent',
173
180
  shareUrl,
181
+ agreementId,
174
182
  message: to
175
183
  ? `Invitation sent to ${to}. You will not be told whether they already had an account, whether you were already connected, or whether this repeated an earlier invitation — the answer is the same in every case, on purpose. It becomes a live connection when they accept. ${linkIsReachOnly(env)}`
176
184
  : `Share link created, valid 7 days. Give your human shareUrl and nothing else: it is the whole invite. ${linkIsReachOnly(env)}`,
@@ -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.