@ziggs-ai/api-client 0.42.1 → 0.42.3

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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.42.3
6
+
7
+ ## 0.42.2
8
+
9
+ - `bucket_send` sends a page: `page` (a full HTML document) and `pageName` save the HTML as a file, and carry that exact version as the message's `page` (with its sha256); whoever reads the message opens the page, so nothing is shared. Without `replyTo` the message starts a thread of its own, which shows as that page; another page sent in the thread (`replyTo`) is its new version, and the tool says a new page names no `replyTo`, even in answer to someone. The result carries `page: {artifactId, address}`, the address being `/pages/<thread>`.
10
+
5
11
  ## 0.42.1
6
12
 
7
13
  ## 0.42.0
@@ -1,4 +1,5 @@
1
1
  import { BucketsClient, BUCKET_ITEM_KINDS, } from '../http/BucketsClient.js';
2
+ import { ArtifactsClient } from '../http/ArtifactsClient.js';
2
3
  import { fullCreds, rethrowWithContext } from './types.js';
3
4
  /**
4
5
  * The bucket operations as tools, one per route.
@@ -236,20 +237,25 @@ export const bucketSendCapability = {
236
237
  names: { sdk: 'bucket_send', mcp: 'ziggs_bucket_send' },
237
238
  title: 'Send in a bucket',
238
239
  descriptions: {
239
- sdk: "Send a message inside a bucket. Its readers are the bucket's people; name recipients among them. To reach anyone else, add them with bucket_add_members first. A request wakes the agents it is delivered to.",
240
+ sdk: "Send a message inside a bucket. Its readers are the bucket's people; name recipients among them. To reach anyone else, add them with bucket_add_members first. A request wakes the agents it is delivered to. With page, the message carries an HTML page: its thread shows as that page.",
240
241
  },
241
242
  annotation: 'destructive',
242
243
  needsAgentId: true,
243
244
  params: {
244
245
  bucketId: BUCKET_ID,
245
246
  text: { type: 'string', required: true },
247
+ page: {
248
+ type: 'string',
249
+ description: "A full HTML document (<!DOCTYPE html>), saved as a file that goes with the message. Without replyTo it starts a thread of its own, shown as the page; with replyTo that page's first message, it replaces that page.",
250
+ },
251
+ pageName: { type: 'string', description: 'Short, stable name of the page.' },
246
252
  recipients: {
247
253
  type: 'array',
248
254
  items: { type: 'string' },
249
255
  description: 'Who is told, among the bucket\'s people. Under work, name at least one person the work is with.',
250
256
  },
251
257
  intent: { type: 'string', enum: ['fyi', 'request'] },
252
- replyTo: { type: 'string' },
258
+ replyTo: { type: 'string', description: 'The message this answers. Not for a new page.' },
253
259
  operationId: { type: 'string', description: 'Reuse on retry.' },
254
260
  agreementId: {
255
261
  type: 'string',
@@ -257,7 +263,7 @@ export const bucketSendCapability = {
257
263
  },
258
264
  taskId: { type: 'string', description: 'The task under agreementId this is about.' },
259
265
  },
260
- handler: (args, env) => run('bucket_send', () => {
266
+ handler: (args, env) => run('bucket_send', async () => {
261
267
  const text = args['text'];
262
268
  if (typeof text !== 'string' || !text.trim())
263
269
  throw new Error('text is required');
@@ -267,17 +273,54 @@ export const bucketSendCapability = {
267
273
  const replyTo = args['replyTo'];
268
274
  if (replyTo !== undefined && (typeof replyTo !== 'string' || !replyTo.trim()))
269
275
  throw new Error('replyTo is a message id');
270
- const operationId = args['operationId'];
271
- return client(env).sendMessage(bucketId(args), {
272
- operationId: typeof operationId === 'string' && operationId.trim() ? operationId.trim() : `bucket-send-${globalThis.crypto.randomUUID()}`,
276
+ const named = args['operationId'];
277
+ const operationId = typeof named === 'string' && named.trim() ? named.trim() : `bucket-send-${globalThis.crypto.randomUUID()}`;
278
+ const page = await pageOf(args, env, operationId);
279
+ const sent = await client(env).sendMessage(bucketId(args), {
280
+ operationId,
273
281
  content: { text },
274
282
  deliveries: { kind: 'explicit', principalIds: principals(args['recipients'], 'recipients') ?? [] },
275
283
  intent,
276
284
  ...(typeof replyTo === 'string' ? { relationships: { replyToMessageId: replyTo.trim() } } : {}),
277
285
  ...mandateOf(args, env),
286
+ // The message carries the page. Without replyTo it starts a thread of
287
+ // its own. Whoever reads the message opens the page: no share needed.
288
+ ...(page ? { page } : {}),
278
289
  });
290
+ return page ? { ...sent, page: { artifactId: page.id, address: `/pages/${sent.message.rootMessageId}` } } : sent;
279
291
  }),
280
292
  };
293
+ /**
294
+ * The page a message carries: the HTML saved as a file, then linked by its
295
+ * exact version. The file is saved under the same retry identity as the
296
+ * message, so a retry links the same file.
297
+ */
298
+ async function pageOf(args, env, operationId) {
299
+ const html = args['page'];
300
+ if (html === undefined)
301
+ return null;
302
+ if (typeof html !== 'string' || !/^\s*(?:<!doctype\s+html|<html)(?:\s|>)/i.test(html)) {
303
+ throw new Error('page is a full HTML document starting with <!DOCTYPE html>');
304
+ }
305
+ const name = typeof args['pageName'] === 'string' ? args['pageName'].trim() : '';
306
+ if (!name)
307
+ throw new Error('pageName is required with page');
308
+ const creds = fullCreds(env);
309
+ // The stored text is trimmed; the link names exactly what is stored.
310
+ const text = html.trim();
311
+ const { artifactId } = await new ArtifactsClient(creds.operatorKey, creds.agentId).writeStrict({
312
+ text,
313
+ name,
314
+ contentType: 'document',
315
+ visibility: 'address',
316
+ serviceAgreementId: env.serviceAgreementId,
317
+ idempotencyKey: `page-${operationId}`,
318
+ });
319
+ if (!artifactId)
320
+ throw new Error('The page file was not saved');
321
+ const { createHash } = await import('node:crypto');
322
+ return { kind: 'artifact', id: artifactId, sha256: createHash('sha256').update(text, 'utf8').digest('hex') };
323
+ }
281
324
  /**
282
325
  * The work a send is under. Work under a hire keeps its mandate: the server
283
326
  * refuses a run's send that drops it. Named in the call, else the agreement
@@ -122,8 +122,17 @@ export interface BucketContents {
122
122
  /** Items held that the reader cannot read here, as they ask now. */
123
123
  outOfReach: number;
124
124
  }
125
- /** A message sent inside a bucket: its readers are the bucket's people. */
126
- export type BucketMessage = Omit<MessageCommand, 'audience'>;
125
+ /**
126
+ * A message sent inside a bucket: its readers are the bucket's people. `page`
127
+ * is the HTML file it carries, by its exact version (contracts 0.49).
128
+ */
129
+ export type BucketMessage = Omit<MessageCommand, 'audience'> & {
130
+ page?: {
131
+ kind: 'artifact';
132
+ id: string;
133
+ sha256: string;
134
+ };
135
+ };
127
136
  export interface BucketItemPage {
128
137
  /** Items everyone it is for can read right now. */
129
138
  items: Array<BucketItemRef & {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ziggs-ai/api-client",
3
- "version": "0.42.1",
3
+ "version": "0.42.3",
4
4
  "description": "HTTP and WebSocket client for the Ziggs backend API",
5
5
  "homepage": "https://ziggsai.com/docs",
6
6
  "bugs": {
@@ -31,7 +31,7 @@
31
31
  "test:watch": "node --import tsx/esm --test --watch test/*.test.ts"
32
32
  },
33
33
  "dependencies": {
34
- "@ziggs-ai/contracts": "0.49.0",
34
+ "@ziggs-ai/contracts": "0.49.1",
35
35
  "socket.io-client": "^4.7.0"
36
36
  },
37
37
  "keywords": [