@ingram-cloud/sdk 1.4.0 → 1.6.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 (51) hide show
  1. package/README.md +34 -35
  2. package/dist/client.js +240 -23
  3. package/dist/index.js +4 -0
  4. package/dist/scopes.js +52 -0
  5. package/dist/zod/_actor.js +33 -0
  6. package/dist/zod/_page.js +18 -4
  7. package/dist/zod/agents.js +7 -3
  8. package/dist/zod/approvals.js +9 -0
  9. package/dist/zod/billing.js +136 -0
  10. package/dist/zod/budgets.js +2 -3
  11. package/dist/zod/connections.js +2 -3
  12. package/dist/zod/conversations.js +4 -11
  13. package/dist/zod/deployments.js +32 -0
  14. package/dist/zod/files.js +2 -9
  15. package/dist/zod/index.js +3 -0
  16. package/dist/zod/mcp.js +8 -10
  17. package/dist/zod/observability.js +38 -19
  18. package/dist/zod/projects.js +4 -4
  19. package/dist/zod/runs.js +19 -4
  20. package/dist/zod/schedules.js +2 -7
  21. package/dist/zod/skills.js +73 -0
  22. package/dist/zod/smith-revisions.js +2 -3
  23. package/dist/zod/smiths.js +6 -0
  24. package/dist/zod/tenant.js +24 -4
  25. package/dist/zod/vector-stores.js +6 -19
  26. package/package.json +25 -18
  27. package/ts/client.ts +456 -60
  28. package/ts/index.ts +4 -0
  29. package/ts/responses.ts +40 -2
  30. package/ts/scopes.ts +57 -0
  31. package/ts/zod/_actor.ts +36 -0
  32. package/ts/zod/_page.ts +19 -4
  33. package/ts/zod/agents.ts +7 -3
  34. package/ts/zod/approvals.ts +9 -0
  35. package/ts/zod/billing.ts +168 -0
  36. package/ts/zod/budgets.ts +2 -3
  37. package/ts/zod/connections.ts +2 -3
  38. package/ts/zod/conversations.ts +7 -11
  39. package/ts/zod/deployments.ts +36 -0
  40. package/ts/zod/files.ts +2 -9
  41. package/ts/zod/index.ts +3 -0
  42. package/ts/zod/mcp.ts +8 -11
  43. package/ts/zod/observability.ts +74 -24
  44. package/ts/zod/projects.ts +4 -4
  45. package/ts/zod/runs.ts +21 -4
  46. package/ts/zod/schedules.ts +2 -7
  47. package/ts/zod/skills.ts +85 -0
  48. package/ts/zod/smith-revisions.ts +2 -3
  49. package/ts/zod/smiths.ts +6 -0
  50. package/ts/zod/tenant.ts +33 -5
  51. package/ts/zod/vector-stores.ts +9 -19
package/README.md CHANGED
@@ -1,11 +1,10 @@
1
1
  # `@ingram-cloud/sdk`
2
2
 
3
- The Ingram Cloud `/v1` API wire contract in TypeScript — **Zod request/response
4
- schemas + SSE/webhook event types + JSON response types** plus a typed
5
- **management-plane client** built on it. The schemas are hand-authored and are
6
- the **source of truth for the wire**: the API imports the same schemas to
7
- validate requests and to emit its OpenAPI document, and the `IC*` response
8
- types are inferred from them.
3
+ The Ingram Cloud `/v1` API contract in TypeScript: Zod request/response
4
+ schemas, SSE/webhook event types, JSON response types, and a typed
5
+ management-plane client built on them. The schemas are hand-authored; the API
6
+ imports them to validate requests and to emit its OpenAPI document, and the
7
+ `IC*` response types are inferred from them.
9
8
 
10
9
  ```ts
11
10
  import { schemas } from "@ingram-cloud/sdk";
@@ -33,42 +32,42 @@ const smith = await ic.smiths.create({ external_id: "user-42" });
33
32
 
34
33
  ## Exports
35
34
 
36
- - `.` the `schemas` Zod map plus the SSE/webhook event types (`EVENT_TYPES`,
35
+ - `.`: the `schemas` Zod map plus the SSE/webhook event types (`EVENT_TYPES`,
37
36
  `webhookEvent`, `streamFrame`, …).
38
- - `./schemas` — just the Zod `schemas` map.
39
- - `./zod` the same schemas as individual named exports, one module per resource.
40
- - `./responses` the `IC*` TypeScript types for the JSON response bodies.
41
- Zod-free; `import type` these to stay dependency-light.
42
- - `./client` `IngramCloud`, the typed management-plane REST client. Method
43
- inputs are `z.input`-inferred from the same schemas the API validates with,
44
- so the client can't drift from the contract. Zod-free at runtime (type-only
45
- imports; transport is the global `fetch`). Auth is a pluggable token seam:
46
- a static bearer or a per-request minting function; smith-scoped calls made
47
- with a tenant token pass `{ smith }` (the `IC-Smith-Id` header). Non-2xx
48
- throws `ICError { status, code, requestId }`.
37
+ - `./schemas`: the Zod `schemas` map only.
38
+ - `./zod`: the same schemas as individual named exports, one module per resource.
39
+ - `./responses`: the `IC*` TypeScript types for the JSON response bodies.
40
+ Zod-free; `import type` these.
41
+ - `./client`: `IngramCloud`, the typed management-plane REST client. Method
42
+ inputs are `z.input`-inferred from the schemas the API validates with. Zod-free
43
+ at runtime (type-only imports; transport is the global `fetch`). Auth is a
44
+ static bearer or a per-request minting function. Smith-scoped calls made with
45
+ a tenant token pass `{ smith }`, sent as the `IC-Smith-Id` header. Non-2xx
46
+ throws `ICError { status, code, requestId }`. A 429 or 503 that names a
47
+ `Retry-After` is retried, up to four attempts and a minute's wait; a 402 is
48
+ never retried.
49
49
 
50
- The OpenAPI document is served by the API itself (`/openapi.json`), emitted from
51
- these schemas — it is no longer shipped as a file in this package.
50
+ The OpenAPI document is served by the API at `/openapi.json`, emitted from these
51
+ schemas.
52
52
 
53
- The client is the **management plane** only. The **data plane** stays on
54
- industry standards: chat rides the OpenAI-compatible surface use
55
- `@ingram-cloud/ai-sdk` and the standard `@ai-sdk/*` types for that. The
56
- native run stream is exposed raw (`smiths.runs.stream` returns the SSE
57
- `Response` unconsumed).
53
+ The client covers the management plane only. Chat goes through the
54
+ OpenAI-compatible surface: use `@ingram-cloud/ai-sdk` and the standard
55
+ `@ai-sdk/*` types. The native run stream is exposed raw: `smiths.runs.stream`
56
+ returns the SSE `Response` unconsumed.
58
57
 
59
58
  > Ships compiled ESM (`dist/`) alongside the TypeScript source (`ts/`). Node
60
- > and bundlers load `dist/` no transpile config needed. Types resolve straight
61
- > to the source, and Bun (the `bun` export condition) runs the source directly.
59
+ > and bundlers load `dist/`. Types resolve to the source, and Bun (the `bun`
60
+ > export condition) runs the source directly.
62
61
 
63
62
  ## Coverage
64
63
 
65
64
  Every resource's request bodies and non-streaming JSON responses are typed as
66
- precise Zod (one module per resource under `./zod`), and the `IC*` types are
67
- inferred from them. The **streaming/union** endpoints (`/runs` stream,
68
- `/chat/completions`, `/responses` — a stream *or* JSON from one handler), deployment
69
- **webhook acks**, and the OAuth **redirect** are not expressible as a single
70
- response schema, so they're not in the typed surface; the `{v:1}` webhook/feed
71
- envelope and the SSE run-stream frames are the hand-authored `./events` half.
65
+ Zod (one module per resource under `./zod`), with the `IC*` types inferred from
66
+ them. Not in the typed surface, because no single response schema expresses
67
+ them: the streaming/union endpoints (`/runs` stream, `/chat/completions`,
68
+ `/responses`, each a stream or JSON from one handler), deployment webhook acks,
69
+ and the OAuth redirect. The `{v:1}` webhook/feed envelope and the SSE
70
+ run-stream frames are the hand-authored `./events` half.
72
71
 
73
- The OpenAI-compatible stream chunks themselves are standard use the `@ai-sdk/*`
74
- types rather than redefining them here.
72
+ The OpenAI-compatible stream chunks are standard; use the `@ai-sdk/*` types for
73
+ them.
package/dist/client.js CHANGED
@@ -19,6 +19,61 @@ export class ICError extends Error {
19
19
  }
20
20
  }
21
21
  const enc = encodeURIComponent;
22
+ /** Attempts per request, including the first. Small on purpose: the server
23
+ * tells us when to come back, so this is a bound on pathological cases, not
24
+ * a backoff strategy. */
25
+ const MAX_ATTEMPTS = 4;
26
+ /** Longest we will sit out one `Retry-After`. A server (or an intermediary that
27
+ * never heard of this API) can name an hour; a client library must not silently
28
+ * block a caller for one. Past this we stop retrying and surface the refusal, so
29
+ * the caller decides. */
30
+ const MAX_WAIT_MS = 60_000;
31
+ /**
32
+ * How long this response says to wait, or null if it does not say — which is
33
+ * itself the answer: a 402 carries no `Retry-After` because the wallet will not
34
+ * refill because we asked twice.
35
+ *
36
+ * RFC 9110 allows both forms, and intermediaries do send the date one, so parse
37
+ * both. Anything unparseable is "no usable instruction", never a zero-delay
38
+ * hammer at an upstream that is already struggling.
39
+ */
40
+ function retryAfterMs(res, now) {
41
+ const raw = res.headers.get("retry-after")?.trim();
42
+ if (!raw)
43
+ return null;
44
+ const seconds = Number(raw);
45
+ const ms = Number.isFinite(seconds) ? seconds * 1000 : Date.parse(raw) - now;
46
+ if (!Number.isFinite(ms))
47
+ return null;
48
+ return Math.max(0, ms);
49
+ }
50
+ /** Retry only what retrying can fix, and only when told how long to wait. */
51
+ function retryDelay(res, now) {
52
+ if (res.status !== 429 && res.status !== 503)
53
+ return null;
54
+ const ms = retryAfterMs(res, now);
55
+ return ms === null || ms > MAX_WAIT_MS ? null : ms;
56
+ }
57
+ /** The `Retry-After` wait, abortable: a caller cancelling mid-wait should not
58
+ * sit out the rest of a Retry-After that can be tens of seconds — reject as
59
+ * soon as `signal` fires, the same way an aborted `transport()` call would. */
60
+ function sleep(ms, signal) {
61
+ if (!signal)
62
+ return new Promise((resolve) => setTimeout(resolve, ms));
63
+ if (signal.aborted)
64
+ return Promise.reject(signal.reason ?? new Error("aborted"));
65
+ return new Promise((resolve, reject) => {
66
+ const timer = setTimeout(() => {
67
+ signal.removeEventListener("abort", onAbort);
68
+ resolve();
69
+ }, ms);
70
+ const onAbort = () => {
71
+ clearTimeout(timer);
72
+ reject(signal.reason ?? new Error("aborted"));
73
+ };
74
+ signal.addEventListener("abort", onAbort, { once: true });
75
+ });
76
+ }
22
77
  function qs(query) {
23
78
  if (!query)
24
79
  return "";
@@ -31,6 +86,52 @@ function qs(query) {
31
86
  const s = p.toString();
32
87
  return s ? `?${s}` : "";
33
88
  }
89
+ /** Build the multipart body `/v1/skills` accepts.
90
+ *
91
+ * The path rides the part's *filename*, slashes and all — that is how the
92
+ * bundle's directory structure survives a multipart body. `FormData` in Node,
93
+ * Bun and browsers all pass it through verbatim. */
94
+ function bundleForm(bundle) {
95
+ const form = new FormData();
96
+ if (bundle instanceof Blob) {
97
+ form.append("file", bundle, "bundle.zip");
98
+ return form;
99
+ }
100
+ for (const file of bundle) {
101
+ // A `Uint8Array`'s buffer type is generic (and may be a `SharedArrayBuffer`),
102
+ // which `Blob`'s constructor does not accept — copy into a fresh one, whose
103
+ // buffer is always a plain `ArrayBuffer`.
104
+ const part = typeof file.content === "string" || file.content instanceof Blob
105
+ ? file.content
106
+ : new Uint8Array(file.content);
107
+ const blob = part instanceof Blob
108
+ ? part
109
+ : new Blob([part], { type: mediaTypeFor(file.path) });
110
+ form.append("files[]", blob, file.path);
111
+ }
112
+ return form;
113
+ }
114
+ /** A `Blob` built from a string has no type of its own; the server falls back to
115
+ * `application/octet-stream` when a part carries none, which would make every
116
+ * text file non-indexable. Name the common ones from the extension. */
117
+ function mediaTypeFor(path) {
118
+ const ext = path.slice(path.lastIndexOf(".") + 1).toLowerCase();
119
+ const known = {
120
+ md: "text/markdown",
121
+ markdown: "text/markdown",
122
+ txt: "text/plain",
123
+ json: "application/json",
124
+ yaml: "text/yaml",
125
+ yml: "text/yaml",
126
+ csv: "text/csv",
127
+ py: "text/x-python",
128
+ sh: "text/x-shellscript",
129
+ js: "text/javascript",
130
+ ts: "text/typescript",
131
+ html: "text/html",
132
+ };
133
+ return known[ext] ?? "application/octet-stream";
134
+ }
34
135
  export class IngramCloud {
35
136
  token;
36
137
  base;
@@ -59,14 +160,23 @@ export class IngramCloud {
59
160
  ...(opts.smith ? { "ic-smith-id": opts.smith } : {}),
60
161
  ...opts.headers,
61
162
  };
62
- const res = await this.transport(`${this.base}/v1${path}${qs(opts.query)}`, {
63
- ...this.requestInit,
64
- method,
65
- headers,
66
- body: opts.rawBody ??
67
- (opts.body !== undefined ? JSON.stringify(opts.body) : undefined),
68
- signal: opts.signal,
69
- });
163
+ let res;
164
+ for (let attempt = 1;; attempt++) {
165
+ res = await this.transport(`${this.base}/v1${path}${qs(opts.query)}`, {
166
+ ...this.requestInit,
167
+ method,
168
+ headers,
169
+ body: opts.rawBody ??
170
+ (opts.body !== undefined ? JSON.stringify(opts.body) : undefined),
171
+ signal: opts.signal,
172
+ });
173
+ if (res.ok || attempt >= MAX_ATTEMPTS)
174
+ break;
175
+ const wait = retryDelay(res, Date.now());
176
+ if (wait === null)
177
+ break;
178
+ await sleep(wait, opts.signal);
179
+ }
70
180
  if (!res.ok) {
71
181
  const body = await res.text().catch(() => "");
72
182
  let code = `http_${res.status}`;
@@ -121,14 +231,14 @@ export class IngramCloud {
121
231
  }),
122
232
  },
123
233
  revisions: {
124
- list: (pid, opts) => this.data("GET", `/smiths/${enc(pid)}/revisions`, opts),
234
+ list: (pid, query, opts) => this.page(`/smiths/${enc(pid)}/revisions`, query, opts),
125
235
  restore: (pid, version, body = {}, opts) => this.json("POST", `/smiths/${enc(pid)}/revisions/${version}/restore`, {
126
236
  ...opts,
127
237
  body,
128
238
  }),
129
239
  },
130
240
  connections: {
131
- list: (pid, opts) => this.data("GET", `/smiths/${enc(pid)}/connections`, opts),
241
+ list: (pid, query, opts) => this.page(`/smiths/${enc(pid)}/connections`, query, opts),
132
242
  get: (pid, cid, opts) => this.json("GET", `/smiths/${enc(pid)}/connections/${enc(cid)}`, opts),
133
243
  create: (pid, body, opts) => this.json("POST", `/smiths/${enc(pid)}/connections`, {
134
244
  ...opts,
@@ -151,7 +261,7 @@ export class IngramCloud {
151
261
  delete: (pid, provider, opts) => this.empty("DELETE", `/smiths/${enc(pid)}/model_keys/${enc(provider)}`, opts),
152
262
  },
153
263
  schedules: {
154
- list: (pid, opts) => this.data("GET", `/smiths/${enc(pid)}/schedules`, opts),
264
+ list: (pid, query, opts) => this.page(`/smiths/${enc(pid)}/schedules`, query, opts),
155
265
  create: (pid, body, opts) => this.json("POST", `/smiths/${enc(pid)}/schedules`, {
156
266
  ...opts,
157
267
  body,
@@ -213,6 +323,7 @@ export class IngramCloud {
213
323
  // ── Runs (tenant-wide feed) ─────────────────────────────────────────────
214
324
  runs = {
215
325
  list: (query, opts) => this.page("/runs", query, opts),
326
+ trace: (rid, opts) => this.json("GET", `/runs/${enc(rid)}/trace`, opts),
216
327
  };
217
328
  // ── Agents ──────────────────────────────────────────────────────────────
218
329
  agents = {
@@ -222,7 +333,7 @@ export class IngramCloud {
222
333
  update: (aid, body, opts) => this.json("PATCH", `/agents/${enc(aid)}`, { ...opts, body }),
223
334
  delete: (aid, opts) => this.empty("DELETE", `/agents/${enc(aid)}`, opts),
224
335
  versions: {
225
- list: (aid, opts) => this.data("GET", `/agents/${enc(aid)}/versions`, opts),
336
+ list: (aid, query, opts) => this.page(`/agents/${enc(aid)}/versions`, query, opts),
226
337
  /** Snapshot the draft as the next immutable version. */
227
338
  publish: (aid, body = {}, opts) => this.json("POST", `/agents/${enc(aid)}/versions`, {
228
339
  ...opts,
@@ -246,7 +357,9 @@ export class IngramCloud {
246
357
  * `{ name, csp?, permissions?, tool? }` sidecar. Replaces by name. */
247
358
  put: (aid, html, meta, opts) => {
248
359
  const form = new FormData();
249
- form.append("file", html instanceof Blob ? html : new Blob([html], { type: "text/html" }), `${meta.name}.html`);
360
+ form.append("file", html instanceof Blob
361
+ ? html
362
+ : new Blob([html], { type: "text/html" }), `${meta.name}.html`);
250
363
  form.append("metadata", JSON.stringify(meta));
251
364
  return this.json("POST", `/agents/${enc(aid)}/ui`, {
252
365
  ...opts,
@@ -260,12 +373,16 @@ export class IngramCloud {
260
373
  ...opts,
261
374
  headers: { accept: "text/html", ...opts?.headers },
262
375
  }).then((r) => r.text()),
263
- delete: (aid, name, opts) => this.json("DELETE", `/agents/${enc(aid)}/ui/${enc(name)}`, opts),
376
+ delete: (aid, name, opts) => this.empty("DELETE", `/agents/${enc(aid)}/ui/${enc(name)}`, opts),
264
377
  },
265
378
  };
266
379
  // ── Conversations (smith-scoped: pass `{ smith }` with a tenant token) ──
267
380
  conversations = {
268
- list: (query, opts) => this.page("/conversations", query, opts),
381
+ /** OpenAI `list` envelope; page forward with `after: page.last_id`. */
382
+ list: (query, opts) => this.json("GET", "/conversations", {
383
+ ...opts,
384
+ query,
385
+ }),
269
386
  create: (body = {}, opts) => this.json("POST", "/conversations", { ...opts, body }),
270
387
  get: (cnvId, opts) => this.json("GET", `/conversations/${enc(cnvId)}`, opts),
271
388
  /** OpenAI-style modify — a POST, not a PATCH. */
@@ -288,6 +405,13 @@ export class IngramCloud {
288
405
  events = {
289
406
  list: (query, opts) => this.page("/events", query, opts),
290
407
  };
408
+ /** What arrived, before anything interpreted it — including arrivals that
409
+ * matched no smith (`smith_id: ""`). The `iev_` ids `deployment.inbound`
410
+ * carries resolve here. */
411
+ inboundEvents = {
412
+ list: (query, opts) => this.page("/inbound_events", query, opts),
413
+ get: (ievId, opts) => this.json("GET", `/inbound_events/${enc(ievId)}`, opts),
414
+ };
291
415
  // ── Customers / budgets ─────────────────────────────────────────────────
292
416
  customers = {
293
417
  list: (query, opts) => this.page("/customers", query, opts),
@@ -297,7 +421,7 @@ export class IngramCloud {
297
421
  delete: (cid, opts) => this.empty("DELETE", `/customers/${enc(cid)}`, opts),
298
422
  };
299
423
  budgets = {
300
- list: (opts) => this.data("GET", "/budgets", opts),
424
+ list: (query, opts) => this.page("/budgets", query, opts),
301
425
  create: (body, opts) => this.json("POST", "/budgets", { ...opts, body }),
302
426
  get: (bid, opts) => this.json("GET", `/budgets/${enc(bid)}`, opts),
303
427
  update: (bid, body, opts) => this.json("PATCH", `/budgets/${enc(bid)}`, { ...opts, body }),
@@ -320,10 +444,25 @@ export class IngramCloud {
320
444
  list: (opts) => this.data("GET", "/catalog", opts),
321
445
  get: (slug, opts) => this.json("GET", `/catalog/${enc(slug)}`, opts),
322
446
  };
447
+ // ── Embeddings ──────────────────────────────────────────────────────────
448
+ /** Embed one string or a batch on the OpenAI-compatible wire. Pure tenant
449
+ * compute — no smith runs. Omit `model` for the project default. Reach for
450
+ * the `openai` SDK instead if you already hold one; this is the same route. */
451
+ embeddings = {
452
+ create: (body, opts) => this.json("POST", "/embeddings", { ...opts, body }),
453
+ };
323
454
  // ── Observability ───────────────────────────────────────────────────────
324
455
  traces = {
325
456
  list: (query, opts) => this.page("/traces", query, opts),
326
457
  get: (traceId, opts) => this.json("GET", `/traces/${enc(traceId)}`, opts),
458
+ /** Push spans from your own runtime or an OTel exporter. The tenant comes
459
+ * from the token; a smith-scoped token may only attribute to its own smith.
460
+ * Unknown `kind`s land as `runtime_event` rather than erroring. Returns the
461
+ * number written. */
462
+ ingest: (spans, opts) => this.json("POST", "/traces:ingest", {
463
+ ...opts,
464
+ body: { spans },
465
+ }),
327
466
  };
328
467
  usage = {
329
468
  /** Token/cost/run totals grouped by app, smith, model, or customer. */
@@ -352,10 +491,16 @@ export class IngramCloud {
352
491
  // ── Vector stores (the OpenAI Vector Stores API) ─────────────────────────
353
492
  vectorStores = {
354
493
  create: (body, opts) => this.json("POST", "/vector_stores", { ...opts, body }),
355
- list: (query, opts) => this.json("GET", "/vector_stores", { ...opts, query }),
494
+ list: (query, opts) => this.json("GET", "/vector_stores", {
495
+ ...opts,
496
+ query,
497
+ }),
356
498
  get: (vsId, opts) => this.json("GET", `/vector_stores/${enc(vsId)}`, opts),
357
499
  /** Modify (OpenAI uses `POST`, not `PATCH`). */
358
- update: (vsId, body, opts) => this.json("POST", `/vector_stores/${enc(vsId)}`, { ...opts, body }),
500
+ update: (vsId, body, opts) => this.json("POST", `/vector_stores/${enc(vsId)}`, {
501
+ ...opts,
502
+ body,
503
+ }),
359
504
  delete: (vsId, opts) => this.json("DELETE", `/vector_stores/${enc(vsId)}`, opts),
360
505
  search: (vsId, body, opts) => this.json("POST", `/vector_stores/${enc(vsId)}/search`, {
361
506
  ...opts,
@@ -378,6 +523,34 @@ export class IngramCloud {
378
523
  files: (vsId, batchId, query, opts) => this.json("GET", `/vector_stores/${enc(vsId)}/file_batches/${enc(batchId)}/files`, { ...opts, query }),
379
524
  },
380
525
  };
526
+ // ── Agent Skills — a folder anchored by SKILL.md, versioned, attached to
527
+ // agents. Upload takes either the bundle's files as path/content pairs, or
528
+ // the whole bundle as a zip Blob — the same two shapes /v1/skills accepts.
529
+ // Both are runtime-agnostic: nothing here touches a filesystem. ───────────
530
+ skills = {
531
+ list: (opts) => this.json("GET", "/skills", opts),
532
+ get: (id, opts) => this.json("GET", `/skills/${enc(id)}`, opts),
533
+ create: (bundle, opts) => this.json("POST", "/skills", {
534
+ ...opts,
535
+ rawBody: bundleForm(bundle),
536
+ }),
537
+ /** Move `default_version` to an existing version. */
538
+ update: (id, body, opts) => this.json("POST", `/skills/${enc(id)}`, {
539
+ ...opts,
540
+ body,
541
+ }),
542
+ delete: (id, opts) => this.request("DELETE", `/skills/${enc(id)}`, opts).then(() => undefined),
543
+ versions: {
544
+ list: (id, opts) => this.json("GET", `/skills/${enc(id)}/versions`, opts),
545
+ get: (id, version, opts) => this.json("GET", `/skills/${enc(id)}/versions/${version}`, opts),
546
+ create: (id, bundle, opts) => this.json("POST", `/skills/${enc(id)}/versions`, { ...opts, rawBody: bundleForm(bundle) }),
547
+ delete: (id, version, opts) => this.request("DELETE", `/skills/${enc(id)}/versions/${version}`, opts).then(() => undefined),
548
+ /** One file's bytes, or — with no `path` — the whole version as a zip.
549
+ * The raw `Response` (matching `files.content`), so a caller streams it
550
+ * through rather than buffering the whole zip into memory. */
551
+ content: (id, version, path, opts) => this.request("GET", `/skills/${enc(id)}/versions/${version}/content${path ? `?path=${encodeURIComponent(path)}` : ""}`, opts),
552
+ },
553
+ };
381
554
  // ── Tenant config ───────────────────────────────────────────────────────
382
555
  tenant = {
383
556
  usage: (opts) => this.json("GET", "/tenant/usage", opts),
@@ -426,10 +599,13 @@ export class IngramCloud {
426
599
  delete: (provider, opts) => this.empty("DELETE", `/tenant/model_keys/${enc(provider)}`, opts),
427
600
  },
428
601
  mcp: {
429
- list: (opts) => this.data("GET", "/tenant/mcp", opts),
602
+ list: (query, opts) => this.page("/tenant/mcp", query, opts),
430
603
  get: (name, opts) => this.json("GET", `/tenant/mcp/${enc(name)}`, opts),
431
604
  /** Register or replace a server (full replace; probes `tools/list`). */
432
- put: (name, body, opts) => this.json("PUT", `/tenant/mcp/${enc(name)}`, { ...opts, body }),
605
+ put: (name, body, opts) => this.json("PUT", `/tenant/mcp/${enc(name)}`, {
606
+ ...opts,
607
+ body,
608
+ }),
433
609
  refresh: (name, opts) => this.json("POST", `/tenant/mcp/${enc(name)}/refresh`, opts),
434
610
  delete: (name, opts) => this.empty("DELETE", `/tenant/mcp/${enc(name)}`, opts),
435
611
  },
@@ -473,11 +649,52 @@ export class IngramCloud {
473
649
  decline: (requestId, opts) => this.json("POST", `/oauth/authorize-requests/${enc(requestId)}/decline`, opts),
474
650
  },
475
651
  };
476
- // ── Organization (org token: projects + tokens) ─────────────────────────
477
- // Note: the `/v1/organization/billing/*` surface is not yet wrapped here.
652
+ // ── Organization (org token: projects, tokens, billing) ─────────────────
478
653
  organization = {
654
+ /** Platform credits — the org wallet that funds every project's runs.
655
+ * Amounts are integer minor units of the wallet's `currency`. */
656
+ billing: {
657
+ balance: (opts) => this.json("GET", "/organization/billing/balance", opts),
658
+ /** Money in (top-ups, grants, codes) and out (usage debits), newest first. */
659
+ ledger: (query, opts) => this.page("/organization/billing/ledger", query, opts),
660
+ /** Per-project draw for a calendar month (`period`, `YYYY-MM`). */
661
+ usage: (query, opts) => this.json("GET", "/organization/billing/usage", {
662
+ ...opts,
663
+ query,
664
+ }),
665
+ /** Daily per-project draw over a rolling window of `days`. */
666
+ usageSeries: (query, opts) => this.json("GET", "/organization/billing/usage/series", {
667
+ ...opts,
668
+ query,
669
+ }),
670
+ /** Open a Stripe Checkout Session to top up; send the user to its `url`. */
671
+ checkout: (body, opts) => this.json("POST", "/organization/billing/checkout", { ...opts, body }),
672
+ /** Add a card with no charge, unlocking the one-time welcome credit. */
673
+ setup: (body, opts) => this.json("POST", "/organization/billing/setup", {
674
+ ...opts,
675
+ body,
676
+ }),
677
+ redeem: (body, opts) => this.json("POST", "/organization/billing/redeem", { ...opts, body }),
678
+ /** Credit a returning Checkout Session. Safe to call twice — the ledger
679
+ * keys on the session id, so it can't double-credit. */
680
+ confirm: (body, opts) => this.json("POST", "/organization/billing/confirm", { ...opts, body }),
681
+ autoreload: {
682
+ get: (opts) => this.json("GET", "/organization/billing/autoreload", opts),
683
+ put: (body, opts) => this.json("PUT", "/organization/billing/autoreload", {
684
+ ...opts,
685
+ body,
686
+ }),
687
+ },
688
+ /** Charge the saved card now. `amount_cents` defaults to the auto-reload amount. */
689
+ reload: (body, opts) => this.json("POST", "/organization/billing/reload", { ...opts, body: body ?? {} }),
690
+ /** A Stripe billing-portal URL for managing cards and invoices. */
691
+ portal: (query, opts) => this.json("GET", "/organization/billing/portal", {
692
+ ...opts,
693
+ query,
694
+ }),
695
+ },
479
696
  projects: {
480
- list: (opts) => this.data("GET", "/organization/projects", opts),
697
+ list: (query, opts) => this.page("/organization/projects", query, opts),
481
698
  create: (body, opts) => this.json("POST", "/organization/projects", {
482
699
  ...opts,
483
700
  body,
package/dist/index.js CHANGED
@@ -13,7 +13,11 @@
13
13
  * `./events` is the hand-authored `{v:1}` webhook/feed envelope and the SSE
14
14
  * run-stream frames, which OpenAPI can't express.
15
15
  *
16
+ * `./scopes` is the closed permission vocabulary a smith token may carry — you
17
+ * must name the scopes you want when minting one.
18
+ *
16
19
  * See `../README.md`.
17
20
  */
18
21
  export { schemas } from "./schemas.js";
19
22
  export * from "./events.js";
23
+ export * from "./scopes.js";
package/dist/scopes.js ADDED
@@ -0,0 +1,52 @@
1
+ /**
2
+ * The closed permission vocabulary a smith token may carry, in mint order.
3
+ *
4
+ * It lives here rather than in the API because it is wire contract: a caller
5
+ * minting a token has to name the scopes it wants (`permissions` is required —
6
+ * there is no "grant everything" default), and the console's token form needs
7
+ * the same list to offer full access. One definition, so a new scope reaches
8
+ * every minting surface at once instead of drifting into a stale copy.
9
+ *
10
+ * Not in here: the admin markers (`tenant:*`, `operator:*`) and the account key
11
+ * (`organization:*`). Those are postures, not permissions — they are never a
12
+ * legal `permissions` entry, and the API refuses them as unknown scopes.
13
+ */
14
+ export const V1_SCOPES = [
15
+ "runs:read",
16
+ "runs:write",
17
+ "conversations:read",
18
+ "conversations:write",
19
+ "memories:read",
20
+ "memories:write",
21
+ "connections:read",
22
+ "connections:write",
23
+ "deployments:read",
24
+ "deployments:write",
25
+ "schedules:read",
26
+ "schedules:write",
27
+ "approvals:read",
28
+ "approvals:write",
29
+ "traces:read",
30
+ "traces:write",
31
+ "usage:read",
32
+ "usage:write",
33
+ "customers:read",
34
+ "customers:write",
35
+ "files:read",
36
+ "files:write",
37
+ "vector_stores:read",
38
+ "vector_stores:write",
39
+ // Smith-level provider keys (#170, end-user BYOK): an end-user sets their own
40
+ // key; a tenant token manages any of its smiths' keys.
41
+ "model_keys:read",
42
+ "model_keys:write",
43
+ // Agent Skills (#175): a tenant's skill bundles and their immutable versions.
44
+ "skills:read",
45
+ "skills:write",
46
+ // Embeddings: the stateless text→vector compute endpoint (POST /v1/embeddings).
47
+ // Write-only — it produces a result, it reads no stored state.
48
+ "embeddings:write",
49
+ ];
50
+ /** The read half of the vocabulary — the scope set for a token that must not
51
+ * change anything. Derived, so it cannot fall behind {@link V1_SCOPES}. */
52
+ export const V1_READ_SCOPES = V1_SCOPES.filter((s) => s.endsWith(":read"));
@@ -0,0 +1,33 @@
1
+ /**
2
+ * The acting principal — who a run or an event is attributable to.
3
+ *
4
+ * Resolved from the authenticated caller at the point of action and stamped on
5
+ * the record it produced; never inferred afterwards. Every event a run produces
6
+ * inherits the run's actor, so a whole turn is attributable to one identity.
7
+ */
8
+ import { z } from "zod";
9
+ export const Actor = z
10
+ .object({
11
+ /** `smith` — a smith acted (a smith-bound token, or the smith itself on an
12
+ * autonomous turn); `tenant` — a tenant-admin token acted on a smith's
13
+ * behalf; `operator` — Ingram staff acted through the operator console. */
14
+ kind: z.enum(["smith", "tenant", "operator"]),
15
+ /** The smith id, tenant id, or operator email, per `kind`. */
16
+ id: z.string(),
17
+ /** `jti` of the token that authorized the action. Empty when no token
18
+ * acted — a scheduled or channel-driven turn the platform ran itself, or a
19
+ * console session, which signs a short-lived per-request token that is never
20
+ * registered. Read it with `email`: both empty means the platform acted. */
21
+ token_id: z.string(),
22
+ /** The human behind the action, when one is named — the signed-in console user
23
+ * or the Ingram operator. Empty for a machine caller (an API token, a smith
24
+ * acting for itself) and for autonomous work.
25
+ *
26
+ * This is what makes a config change attributable to a *person* rather than to
27
+ * the tenant they share: console mutations all carry `kind: "tenant"`, so
28
+ * without this every colleague's action looked identical. Defaulted rather than
29
+ * optional so records written before it existed read as "no human named"
30
+ * instead of failing to parse. */
31
+ email: z.string().default(""),
32
+ })
33
+ .meta({ id: "Actor" });
package/dist/zod/_page.js CHANGED
@@ -1,8 +1,10 @@
1
1
  /**
2
- * The shared cursor-paginated list envelope. One definition so every paginated
3
- * `/v1` list reads back the same shape: `data` + the opaque `next_cursor` (null
4
- * on the last page) + `has_more`. The cursor is an opaque, short-lived token
5
- * pass it straight back as `?cursor=`, never parse it.
2
+ * The two `/v1` list envelopes, one definition each. Native resources page by
3
+ * keyset: `data` + the opaque `next_cursor` (null on the last page) + `has_more`
4
+ * the cursor is an opaque, short-lived token; pass it straight back as
5
+ * `?cursor=`, never parse it. OpenAI-mirrored resources use the OpenAI `list`
6
+ * envelope instead: `object:"list"` + `first_id`/`last_id` + `has_more`, paged
7
+ * by passing `last_id` back as `?after=`.
6
8
  */
7
9
  import { z } from "zod";
8
10
  /** Wrap an item schema as a cursor-paginated list out, named `id` in the spec. */
@@ -15,3 +17,15 @@ export function pageOut(item, id) {
15
17
  })
16
18
  .meta({ id });
17
19
  }
20
+ /** Wrap an item schema in the OpenAI `list` envelope, named `id` in the spec. */
21
+ export function oaiListOut(item, id) {
22
+ return z
23
+ .object({
24
+ object: z.literal("list"),
25
+ data: z.array(item),
26
+ first_id: z.string().nullable(),
27
+ last_id: z.string().nullable(),
28
+ has_more: z.boolean(),
29
+ })
30
+ .meta({ id });
31
+ }