@ziggs-ai/api-client 0.37.10 → 0.37.12

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.
@@ -1,4 +1,5 @@
1
1
  import { isContextAccessRequest } from '../shared/contextAccessRequest.js';
2
+ import { looksLikeHandle, parseRef } from '../handles.js';
2
3
  import { presentResourceActions } from './resourceActions.js';
3
4
  import { ContextReadClient, CONTEXT_READ_TYPES, CONTEXT_READ_VIA, parseVia, viaHint, } from '../http/ContextReadClient.js';
4
5
  import { ContextOpenClient } from '../http/ContextOpenClient.js';
@@ -255,31 +256,24 @@ export const contextDelegateCapability = {
255
256
  };
256
257
  },
257
258
  };
258
- const OPEN_ID_PARAMS = {
259
- artifactId: {
259
+ const OPEN_REF_PARAMS = {
260
+ ref: {
260
261
  type: 'string',
261
- description: 'Artifact id from a prior response. Pass exactly one id field.',
262
- },
263
- chatId: {
264
- type: 'string',
265
- description: 'Chat id from a prior response. Pass exactly one id field.',
266
- },
267
- taskId: {
268
- type: 'string',
269
- description: 'Task id from a prior response. Pass exactly one id field.',
270
- },
271
- agreementId: {
272
- type: 'string',
273
- description: 'Agreement id from a prior response. Pass exactly one id field.',
262
+ required: true,
263
+ description: 'What to open: a handle such as T7K2QXD (T task, A agreement, C chat, F artifact) in any case, a Ziggs web URL holding one, or an id from a prior response.',
274
264
  },
275
265
  };
276
266
  function openBodyFromArgs(args) {
277
- const body = {};
278
- for (const key of ['artifactId', 'chatId', 'taskId', 'agreementId']) {
279
- const value = args[key];
280
- if (typeof value === 'string' && value.trim())
281
- body[key] = value.trim();
282
- }
267
+ // A mistyped handle is refused here, before any call: its check letter
268
+ // fails. Any other id goes to the server, which knows older shapes.
269
+ const raw = typeof args.ref === 'string' ? args.ref.trim() : '';
270
+ const invalid = (message) => Object.assign(new Error(message), { code: 'INVALID_INPUT' });
271
+ if (!raw)
272
+ throw invalid('ref is required: a handle such as T7K2QXD, a Ziggs URL, or an id from a prior response');
273
+ const ref = parseRef(raw);
274
+ if (!ref && looksLikeHandle(raw))
275
+ throw invalid(`${raw} is not a handle: its check letter does not match. Check it for a typo`);
276
+ const body = { ref: ref?.id ?? raw };
283
277
  if (typeof args['cursor'] === 'string')
284
278
  body.cursor = args['cursor'];
285
279
  if (typeof args['after'] === 'string')
@@ -304,7 +298,7 @@ function presentOpenResult(result, env) {
304
298
  const ask = row.grantRequest;
305
299
  if (row.status !== 'active' || !isContextAccessRequest(ask) || !actingAgentId || ask.holderId !== actingAgentId || !ask.scope?.id || !['artifact', 'chat', 'agreement', 'task'].includes(ask.scope.kind ?? ''))
306
300
  return [];
307
- return [{ kind: 'resume', tool: env.surface === 'mcp' ? 'ziggs_open' : 'open', args: { [`${ask.scope.kind}Id`]: ask.scope.id }, effect: 'read_only', note: 'Approval recorded. Open the original scope; the server rechecks expiry and revocation.' }];
301
+ return [{ kind: 'resume', tool: env.surface === 'mcp' ? 'ziggs_open' : 'open', args: { ref: ask.scope.id }, effect: 'read_only', note: 'Approval recorded. Open the original scope; the server rechecks expiry and revocation.' }];
308
302
  });
309
303
  return {
310
304
  ...result,
@@ -353,16 +347,15 @@ export const openCapability = {
353
347
  names: { sdk: 'open', mcp: 'ziggs_open' },
354
348
  title: 'Open a returned resource',
355
349
  descriptions: {
356
- sdk: 'Read a returned resource: pass exactly one of artifactId, chatId, taskId, agreementId. The backend resolves access and returns scoped actions with required inputs and recovery calls. Opening executes none of those actions. Extraction pending/failed is not an empty document. To read a chat\'s messages under one agreement, use context_read with type=messages, via=chat:<id> and agreementId.',
350
+ sdk: 'Read a returned resource: pass its ref, a short handle such as T7K2QXD (the first letter is the kind), a Ziggs URL, or an id. The backend resolves access and returns scoped actions with required inputs and recovery calls. Opening executes none of those actions. Extraction pending/failed is not an empty document. To read a chat\'s messages under one agreement, use context_read with type=messages, via=chat:<id> and its agreement filter.',
357
351
  },
358
352
  annotation: 'read-only',
359
- // agreementId here is the agreement to open, one id among four
360
- // (OPEN_ID_PARAMS). It used to carry context_read's filter wording, so an
353
+ // ref names the one record to open (OPEN_REF_PARAMS). It used to carry context_read's filter wording, so an
361
354
  // agent passed it beside chatId as a filter and was refused by the
362
355
  // exactly-one rule. Filtering a room's messages to one agreement is
363
356
  // context_read's job, and the description says so.
364
357
  params: {
365
- ...OPEN_ID_PARAMS,
358
+ ...OPEN_REF_PARAMS,
366
359
  cursor: { type: 'string', description: 'Opaque cursor from a prior open page' },
367
360
  after: { type: 'string', description: 'ISO timestamp for a forward-delta' },
368
361
  limit: { type: 'number', description: 'Page size' },
@@ -379,10 +372,10 @@ export const accessExplainCapability = {
379
372
  names: { sdk: 'access_explain', mcp: 'ziggs_access_explain' },
380
373
  title: 'Explain access to a resource',
381
374
  descriptions: {
382
- sdk: 'Read-only access explanation for an ordinary returned id. Same one-of artifactId/chatId/taskId/agreementId as open. Returns scoped action states, required inputs and recovery calls. Does not return content and does not request or approve anything.',
375
+ sdk: 'Read-only access explanation for a returned resource. Same ref as open. Returns scoped action states, required inputs and recovery calls. Does not return content and does not request or approve anything.',
383
376
  },
384
377
  annotation: 'read-only',
385
- params: OPEN_ID_PARAMS,
378
+ params: OPEN_REF_PARAMS,
386
379
  needsAgentId: true,
387
380
  handler: async (args, env) => {
388
381
  const creds = fullCreds(env);
@@ -508,7 +501,7 @@ export const accessRequestCapability = {
508
501
  // A person scope opens nothing: the yes shows up in the chat list.
509
502
  ...(scopeKind === 'person'
510
503
  ? env.surface === 'mcp' ? { resumeAfterApproval: { tool: 'ziggs_chat_list', args: {} } } : {}
511
- : { resumeAfterApproval: { tool: env.surface === 'mcp' ? 'ziggs_open' : 'open', args: result.scope ? { [`${result.scope.kind}Id`]: result.scope.id } : { [`${scopeKind}Id`]: scopeId } } }),
504
+ : { resumeAfterApproval: { tool: env.surface === 'mcp' ? 'ziggs_open' : 'open', args: { ref: result.scope?.id ?? scopeId } } }),
512
505
  note: `${result.message ?? 'A request card is open.'} No access granted until they approve; then open the original resource using the same agent identity.`,
513
506
  };
514
507
  },
@@ -113,7 +113,7 @@ const BRIEFS = {
113
113
  },
114
114
  // Reading context
115
115
  ziggs_open: {
116
- does: 'Reads one artifact, chat, task or agreement by id (exactly one id), with the actions you may take and any recovery call. Hidden and unknown look the same. A chat\'s messages under one agreement: ziggs_context_read with agreementId.',
116
+ does: 'Reads one task, agreement, chat or artifact by ref: a handle like T7K2QXD, a Ziggs URL or an id. Returns the actions you may take. Hidden and unknown look the same. Messages under one agreement: ziggs_context_read.',
117
117
  },
118
118
  ziggs_access_explain: {
119
119
  does: 'Explains what you may do with one artifact, chat, task or agreement, and the recovery calls; runs none of them.',
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Short handles: the id of a task (T…), agreement (A…), chat (C…) or
3
+ * artifact (F…). A kind letter, five to eight letters from 0-9 A-Z without
4
+ * I, L, O and U, and a check letter. Read in any case; written upper case.
5
+ *
6
+ * Mirrors the server's rule so a mistyped handle is refused here, before any
7
+ * call. The server stays the judge of what the handle opens.
8
+ */
9
+ export declare const HANDLE_ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
10
+ export type HandleKind = 'task' | 'agreement' | 'chat' | 'artifact';
11
+ /** The handle's kind and its upper-case form, or null when it is not one. */
12
+ export declare function parseHandle(raw: string): {
13
+ kind: HandleKind;
14
+ id: string;
15
+ } | null;
16
+ /** The shape of a handle, whether or not its check letter holds. */
17
+ export declare function looksLikeHandle(raw: string): boolean;
18
+ /**
19
+ * What a `ref` names: a handle in any case, a Ziggs web URL that contains
20
+ * one, or an id of the older shape. Null when it names nothing.
21
+ */
22
+ export declare function parseRef(raw: unknown): {
23
+ kind: HandleKind;
24
+ id: string;
25
+ } | null;
26
+ /**
27
+ * Every handle named in free text, in order and once each, so an agent that
28
+ * reads "look at T7K2QXD" can open it without anyone pasting a form. Words
29
+ * that merely look like one fail the check letter and are left out.
30
+ */
31
+ export declare function findHandles(text: string): Array<{
32
+ kind: HandleKind;
33
+ id: string;
34
+ }>;
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Short handles: the id of a task (T…), agreement (A…), chat (C…) or
3
+ * artifact (F…). A kind letter, five to eight letters from 0-9 A-Z without
4
+ * I, L, O and U, and a check letter. Read in any case; written upper case.
5
+ *
6
+ * Mirrors the server's rule so a mistyped handle is refused here, before any
7
+ * call. The server stays the judge of what the handle opens.
8
+ */
9
+ export const HANDLE_ALPHABET = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
10
+ const LETTER_KIND = { T: 'task', A: 'agreement', C: 'chat', F: 'artifact' };
11
+ const N = HANDLE_ALPHABET.length;
12
+ function checkHolds(handle) {
13
+ let factor = 1;
14
+ let sum = 0;
15
+ for (let i = handle.length - 1; i >= 0; i--) {
16
+ let addend = factor * HANDLE_ALPHABET.indexOf(handle[i]);
17
+ factor = factor === 2 ? 1 : 2;
18
+ addend = Math.floor(addend / N) + (addend % N);
19
+ sum += addend;
20
+ }
21
+ return sum % N === 0;
22
+ }
23
+ /** The handle's kind and its upper-case form, or null when it is not one. */
24
+ export function parseHandle(raw) {
25
+ const handle = raw.trim().toUpperCase();
26
+ if (handle.length < 7 || handle.length > 10)
27
+ return null;
28
+ if (![...handle].every((c) => HANDLE_ALPHABET.includes(c)))
29
+ return null;
30
+ const kind = LETTER_KIND[handle[0]];
31
+ if (!kind || !checkHolds(handle))
32
+ return null;
33
+ return { kind, id: handle };
34
+ }
35
+ const OLD_SHAPES = [
36
+ [/^task_[0-9a-f-]{36}$/, 'task'],
37
+ [/^agr_[A-Za-z0-9_]+$/, 'agreement'],
38
+ [/^(art_|org-state-)[A-Za-z0-9_-]+$/, 'artifact'],
39
+ [/^[a-z]{24}$/, 'chat'],
40
+ ];
41
+ /** The shape of a handle, whether or not its check letter holds. */
42
+ export function looksLikeHandle(raw) {
43
+ return /^[TACF][0-9A-Z]{6,9}$/i.test(raw.trim());
44
+ }
45
+ function parseOne(token) {
46
+ const trimmed = token.trim();
47
+ for (const [shape, kind] of OLD_SHAPES)
48
+ if (shape.test(trimmed))
49
+ return { kind, id: trimmed };
50
+ return parseHandle(trimmed);
51
+ }
52
+ /**
53
+ * What a `ref` names: a handle in any case, a Ziggs web URL that contains
54
+ * one, or an id of the older shape. Null when it names nothing.
55
+ */
56
+ export function parseRef(raw) {
57
+ if (typeof raw !== 'string' || !raw.trim())
58
+ return null;
59
+ const value = raw.trim();
60
+ const direct = parseOne(value);
61
+ if (direct)
62
+ return direct;
63
+ if (!/[/?#]/.test(value))
64
+ return null;
65
+ let path = value;
66
+ try {
67
+ const url = new URL(value, 'https://ziggsai.com');
68
+ path = `${url.pathname}/${[...url.searchParams.values()].join('/')}`;
69
+ }
70
+ catch {
71
+ // Not a URL; split what was given.
72
+ }
73
+ for (const segment of path.split(/[/?#&=]/).reverse()) {
74
+ if (!segment)
75
+ continue;
76
+ const hit = parseOne(decodeURIComponent(segment));
77
+ if (hit)
78
+ return hit;
79
+ }
80
+ return null;
81
+ }
82
+ const CANDIDATE = /\b[TACF][0-9A-HJKMNP-TV-Z]{6,9}\b/gi;
83
+ /**
84
+ * Every handle named in free text, in order and once each, so an agent that
85
+ * reads "look at T7K2QXD" can open it without anyone pasting a form. Words
86
+ * that merely look like one fail the check letter and are left out.
87
+ */
88
+ export function findHandles(text) {
89
+ const out = [];
90
+ const seen = new Set();
91
+ for (const match of text.matchAll(CANDIDATE)) {
92
+ const hit = parseHandle(match[0]);
93
+ if (hit && !seen.has(hit.id)) {
94
+ seen.add(hit.id);
95
+ out.push(hit);
96
+ }
97
+ }
98
+ return out;
99
+ }
@@ -6,6 +6,8 @@ export interface ContextOpenRef {
6
6
  id: string;
7
7
  }
8
8
  export interface ContextOpenBody {
9
+ /** A handle (T…/A…/C…/F…), a Ziggs URL holding one, or an older id. */
10
+ ref?: string;
9
11
  artifactId?: string;
10
12
  chatId?: string;
11
13
  taskId?: string;
@@ -31,11 +31,11 @@ export class ContextOpenClient {
31
31
  return this.post('/context/access/explain', body);
32
32
  }
33
33
  async post(path, body) {
34
- const filled = ['artifactId', 'chatId', 'taskId', 'agreementId'].filter((key) => typeof body[key] === 'string' && body[key].trim());
34
+ const filled = ['ref', 'artifactId', 'chatId', 'taskId', 'agreementId'].filter((key) => typeof body[key] === 'string' && body[key].trim());
35
35
  if (filled.length !== 1) {
36
36
  // A caller's input mistake, named in the caller's words: the shared
37
37
  // INVALID_INPUT code, and no internal class name in the sentence.
38
- throw Object.assign(new Error('Pass exactly one of artifactId, chatId, taskId, agreementId.'), { code: 'INVALID_INPUT' });
38
+ throw Object.assign(new Error('Pass ref (a handle such as T7K2QXD, a Ziggs URL, or an id), or exactly one of artifactId, chatId, taskId, agreementId.'), { code: 'INVALID_INPUT' });
39
39
  }
40
40
  const url = `${this.baseUrl || getBackendUrl()}${path}`;
41
41
  const res = await fetch(url, {
package/dist/index.d.ts CHANGED
@@ -32,3 +32,4 @@ export type { EngagementGuide, EngagementGuideKind, EngagementOutcome, Engagemen
32
32
  export { isContextAccessRequest } from './shared/contextAccessRequest.js';
33
33
  export { AssistantConnectionClient, type AssistantWorkspace, } from './http/AssistantConnectionClient.js';
34
34
  export { withAssistantRequest, assistantRequestHeaders } from './http/operatorHeaders.js';
35
+ export { HANDLE_ALPHABET, findHandles, looksLikeHandle, parseHandle, parseRef, type HandleKind } from './handles.js';
package/dist/index.js CHANGED
@@ -29,3 +29,5 @@ export { inboxEngagement, isEchoOrDuplicate, isHireWorkOrder, contactPath, forma
29
29
  export { isContextAccessRequest } from './shared/contextAccessRequest.js';
30
30
  export { AssistantConnectionClient, } from './http/AssistantConnectionClient.js';
31
31
  export { withAssistantRequest, assistantRequestHeaders } from './http/operatorHeaders.js';
32
+ // Short handles: T…, A…, C…, F… (task, agreement, chat, artifact).
33
+ export { HANDLE_ALPHABET, findHandles, looksLikeHandle, parseHandle, parseRef } from './handles.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ziggs-ai/api-client",
3
- "version": "0.37.10",
3
+ "version": "0.37.12",
4
4
  "description": "HTTP and WebSocket client for the Ziggs backend API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",