@zoowork-ai/sdk 0.4.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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,366 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@zoowork-ai/sdk` (formerly `@zooclaw-agents/sdk`). Dates are the
4
+ day the behaviour was verified, not the day it was written.
5
+
6
+ ## 0.4.0 — 2026-08-25
7
+
8
+ ### Changed (breaking)
9
+
10
+ - **Renamed to `@zoowork-ai/sdk`.** The package, exports, and environment variables all
11
+ move from the ZooClaw name to ZooWork, with no compatibility aliases:
12
+ - Install `@zoowork-ai/sdk` instead of `@zooclaw-agents/sdk`.
13
+ - `createZooclawClient` → `createZooworkClient`; `ZooclawClient`, `ZooclawError`,
14
+ `ZooclawAuth`, `ZooclawConfig` → `Zoowork*`.
15
+ - `ZOOCLAW_API_KEY` / `ZOOCLAW_BASE_URL` → `ZOOWORK_API_KEY` / `ZOOWORK_BASE_URL`.
16
+ - Server-side identifiers are unchanged: API keys still start with `zct_`, and skill or
17
+ environment names the API returns (e.g. `zooclaw-tts`) are whatever the server says.
18
+
19
+ ## 0.3.4 — 2026-08-25
20
+
21
+ ### Fixed (documentation)
22
+
23
+ - **`config` keys are documented per platform**, which is what a caller actually needs:
24
+ `slack` takes `{ botToken, appToken }` (socket mode needs the app-level token too),
25
+ `wecom` takes `{ botId, secret }`, `feishu` takes `{ appId, appSecret, domain }` when you
26
+ skip the QR flow. They are camelCase; other keys are stored and ignored.
27
+ - **`ChannelPlatform` explains why only Feishu has a QR flow here**, because the two absences
28
+ are different. Slack structurally cannot have one — a Slack app is created by a person and
29
+ its tokens only exist in that person's browser, so guided setup anywhere ends in the same
30
+ two tokens you pass to `addChannel`. WeCom's flow exists in the product but is not exposed
31
+ on this API yet.
32
+
33
+ ## 0.3.3 — 2026-08-25
34
+
35
+ ### Changed
36
+
37
+ - **`ChannelPlatform` is `'feishu' | 'slack' | 'wecom'`.** 0.3.2 also listed `'mattermost'`,
38
+ which is the deployment's own internal connection rather than something an API caller binds;
39
+ it is filtered out of `listChannels` server-side and does not belong on this surface. The
40
+ type stays widened with `(string & {})`, so nothing that compiled before stops compiling.
41
+
42
+ ## 0.3.2 — 2026-08-25
43
+
44
+ Probed the platform axis, which 0.3.1 had not: the routes only name Feishu, but `platform`
45
+ is a free string and the server knows more than one.
46
+
47
+ ### Added
48
+
49
+ - **`ChannelPlatform`** — `'feishu' | 'slack' | 'wecom' | 'mattermost'`, widened with
50
+ `(string & {})` so a platform that ships later needs no SDK release.
51
+
52
+ ### Fixed (documentation)
53
+
54
+ - **Slack and WeCom bind through `addChannel`** — 0.3.1 read as though channels meant Feishu.
55
+ - **A Mattermost binding is invisible.** It binds, updates and removes normally, but the server
56
+ filters it out of every `listChannels` response, so an empty list is not proof nothing is bound.
57
+ - **WeChat cannot be bound here.** `weixin`/`wechat` answer `400 channel.weixin_setup_required`
58
+ naming a QR flow this API does not expose. Any other platform name answers
59
+ `400 channel.invalid_request`.
60
+ - **`addChannel` is an upsert**: the same `platform` + `account` twice answers 201 again and
61
+ overwrites, rather than conflicting.
62
+ - **`removeChannel` is idempotent, `updateChannel` is not** — removing an absent binding is
63
+ `200 { ok: true }`; updating one is `404 channel.not_found`.
64
+ - `dm_policy: 'pairing'` is rejected with `400 channel.pairing_unsupported`.
65
+
66
+ ## 0.3.1 — 2026-08-25
67
+
68
+ Channels, verified. 0.3.0 shipped the surface ahead of the deployment; this replaces its
69
+ guesses with what staging actually answered on 2026-08-25 (11 recorded fixtures, 10 new
70
+ response-contract tests). No signature changed — the corrections are in the docs and the
71
+ tests, and two of them would have cost you a debugging session:
72
+
73
+ ### Fixed (documentation and contract, not behaviour)
74
+
75
+ - **`addChannel`'s 201 means STORED, not WORKING.** Credentials are not validated at bind
76
+ time: bogus ones still answered 201 with `health: 'unknown'` / `status: 'configured'`, and
77
+ only turned `health: 'unhealthy'` / `status: 'error'` moments later. Read the verdict from a
78
+ follow-up `listChannels`.
79
+ - **`waitForFeishuSetup` does not return a terminal status for a session that stopped
80
+ existing.** A cancelled session answers `404 channel.feishu_session_not_found`, which the
81
+ helper surfaces as a thrown `ZooclawError` — 0.3.0's docs implied every ending came back as
82
+ a value. Whether natural expiry takes this path or reports `status: 'expired'` is still
83
+ unobserved; handle both.
84
+ - Three distinct 404 codes documented (`channel.feishu_session_not_found` /
85
+ `channel.not_found` / `service_api.not_found`), plus the tell for a deployment that lacks
86
+ the routes entirely: the engine passthrough envelope `{error:{type:'not_found'}}` instead of
87
+ this family's `{code, detail}`.
88
+ - Observed defaults recorded: `expires_in: 600`, `poll_interval: 5`; `enabled: false` moves
89
+ `status` to `'disabled'` and resets `health`; `brand: 'lark'` really does switch the URI host
90
+ to `open.larksuite.com`.
91
+
92
+ ## 0.3.0 — 2026-08-25
93
+
94
+ The surface, published the day the gateway release reached staging. Verified in 0.3.1.
95
+
96
+ ### Added
97
+
98
+ - **Channels.** Bind chat platforms to an API-created agent: `listChannels`, `addChannel`
99
+ (explicit platform config), `updateChannel`, `removeChannel`, and the Feishu/Lark QR device
100
+ flow — `startFeishuSetup` / `pollFeishuSetup` / `cancelFeishuSetup` plus `waitForFeishuSetup`,
101
+ which drives the poll loop at the server's suggested interval, returns every terminal
102
+ outcome (`success` / `expired` / `denied` / `error`) instead of throwing on the human ones,
103
+ and bounds in-flight polls the way `waitUntilRunning` does. New types: `AgentChannel`,
104
+ `AddChannelInput`, `UpdateChannelInput`, `FeishuSetupInput`, `FeishuSetupSession`,
105
+ `FeishuPollResult`. On gateway deployments without the channels release every route here
106
+ answers 404.
107
+ - `deleteAgent` doc: it is a soft delete, and on channel-capable gateways a successful delete
108
+ best-effort disables the agent's bound channels (cleanup failures never gate the delete).
109
+
110
+ ## 0.2.1 — 2026-08-25
111
+
112
+ ### Changed
113
+
114
+ - **`DEFAULT_BASE_URL` now points at the production API** (`https://clawapi.ecap.gsmo.ai/service/v1`).
115
+ A client with no `baseUrl` and no `ZOOCLAW_BASE_URL` — the recommended setup — now reaches
116
+ production, which is where API keys are issued. Verified end to end on 2026-08-25: create →
117
+ start → session → a real model turn → replay, all against production with a production key.
118
+ If you were relying on the previous default while pointing at another deployment, set
119
+ `ZOOCLAW_BASE_URL` (or pass `baseUrl`) explicitly.
120
+
121
+ ## 0.2.0 — 2026-08-19
122
+
123
+ Everything below was verified against staging on 2026-08-19: input echo with the
124
+ `processedAt` lifecycle, cursor pagination, `pse1:` stream resume, idempotent retry dedup,
125
+ full-object receipts, and `max_tokens` visibly capping a reply.
126
+
127
+ ### Added
128
+
129
+ - **Unified event history.** The events read surface now carries your own inputs
130
+ (`user.message`, `user.interrupt`, `user.tool_confirmation`, `system.message`) alongside
131
+ engine events — the log alone renders the whole conversation: `listAllEvents` follows the
132
+ server's `next_cursor`/`has_more` pagination (and still walks `after` against servers
133
+ without it), `listEvents`/`streamEvents` accept `cursor`, `listEventsPage` returns one page
134
+ with its pagination fields for hand-paging, streamed events carry a `cursor` resume token,
135
+ and events expose `id` and `processedAt`. `PUBLIC_INPUT_EVENT_TYPES` is exported next to
136
+ `SESSION_EVENT_TYPES`. Passing `after` anywhere selects the deprecated engine-only lane.
137
+ - **Event-level idempotency on `postEvents`** — give each event an `idempotency_key` and
138
+ timeout retries stop double-delivering; accepted events come back as full event objects
139
+ (`PostEventReceipt`).
140
+ - **`resource.model.max_tokens`** — output-token cap per model request, passed through on
141
+ create and config PUT and enforced by the platform.
142
+
143
+ ## 0.1.0 — 2026-08-17
144
+
145
+ **Breaking.** A trim, not a feature release: four pieces of `createAgent`'s surface either
146
+ raced the platform or answered 404, so they are gone rather than documented. The minor bump is
147
+ the surface change; nothing new was added.
148
+
149
+ ### Removed
150
+
151
+ - **`resource.warm`** — pre-warming the agent-scope sandbox at create races the platform's
152
+ credential injection (verified 2026-08-16, `zooclaw-engine#791`): the sandbox can come up
153
+ before the built-in-skill credentials land, and the env snapshot never refreshes, leaving
154
+ those skills permanently broken in that sandbox. Removing the parameter makes the race
155
+ unreachable instead of documenting it. `createAgent` also strips `warm` at runtime, so a JS
156
+ caller bypassing the types cannot resurrect it.
157
+ - **`resource.onboarding`** — the interactive onboarding interview is never what an API caller
158
+ wants. `createAgent` now always sends `onboarding: false`, and strips a caller-supplied value
159
+ at runtime alongside `warm`.
160
+ - **`putCredential()` / `listCredentials()`** — both answer 404 through the gateway. The
161
+ platform seeds model credentials itself at create; there is no supported way to store your
162
+ own or your end users' third-party credentials, so the methods no longer imply one.
163
+
164
+ ### Changed
165
+
166
+ - **`createAgent(input)` takes `ownership` as optional.** The gateway derives the tenant
167
+ anchors from your API key, so `{ resource }` is the whole input. The field is kept for
168
+ callers that reach the engine without the gateway.
169
+ - **`AgentResource` no longer carries an `[k: string]: unknown` index signature.** Unknown
170
+ fields are a type error now instead of passing silently — which is how `warm` and
171
+ `onboarding` would otherwise have kept compiling after removal.
172
+
173
+ ## 0.0.6 — 2026-08-14
174
+
175
+ Three engine surfaces that landed this week — the system-prompt pin, the artifacts control
176
+ plane, and outcome-gated cron — plus one new wire field. Everything below was driven live
177
+ through the `/service/v1` gateway on 2026-08-14 and is pinned by re-recorded fixtures.
178
+
179
+ ### Added
180
+
181
+ - **`getSystemPrompt(agentId)` / `previewSystemPrompt(agentId, input)` /
182
+ `upgradeSystemPrompt(agentId, input)`** — the pin as declared and the rendered template in
183
+ effect; deterministic assembly of the exact prompt for given runtime facts, without touching
184
+ a session (13 `slot_hashes`, `transcript` always `[]`); and the one write that moves the pin.
185
+ `resource.system_prompt` is typed on `AgentResource` (`{source:'platform',version}` |
186
+ `{source:'custom',base_version,template}`): a fresh create pins the active platform version
187
+ on its own, the pin never follows later activations on its own, and on PUT the section is
188
+ REPLACE-ON-WRITE like `tool_policy`. `upgradeSystemPrompt` is a real CAS —
189
+ `expected_config_version` must be current or the answer is `409 config_version_changed`
190
+ (both directions recorded as fixtures). The route uses the `{id}:verb` grammar, which the
191
+ gateway blocked until fix #3387 landed the same day this shipped — on older gateway
192
+ deployments this one method answers a gateway 404.
193
+ - **`listArtifacts` / `getArtifact` / `downloadArtifact` / `deleteArtifact`** — the control
194
+ plane over what the agent's own in-loop `artifact_publish` tool produced (publishing from
195
+ outside the loop still does not exist). These routes demand `owner_uid`+`org_id` selectors
196
+ and the gateway does not inject them, so the SDK derives both from the agent's own projection
197
+ and caches them per agent — the first artifact call costs one extra GET. `listArtifacts`
198
+ returns the page VERBATIM (`{artifacts, page, has_more}`): unlike `listEvents`, this list
199
+ says when it truncated, and flattening it away would have re-created that bug. The colon in
200
+ `:download` goes RAW on the wire — this family matches the literal colon, the opposite of
201
+ the environments family's `%3A`.
202
+ - **`OutcomeConfig`** on `SchedulePayload.outcome` and `AgentResource.outcome` — the
203
+ evaluate-revise-finalize gate for unattended cron fires (`command` or `rubric` evaluator,
204
+ `maxIterations` 1–5, `publish: after_satisfied | always | never`). Stored verbatim, no
205
+ defaults injected; a job-level value overrides the agent default and an explicit `null` opts
206
+ the job out. Cron fires only.
207
+ - **`EnvironmentVersionRecord.base_template_ref`** — new on the wire this week: the exact
208
+ base-image build a version layers on.
209
+
210
+ ### Changed
211
+
212
+ - Response fixtures re-recorded against staging 2026-08-14. Count-pinned assertions
213
+ (global skill catalog, org environment list) now assert against the recording instead of a
214
+ number that drifts.
215
+
216
+ ## 0.0.5 — 2026-08-10
217
+
218
+ ### Added
219
+
220
+ - **`listAgents(opts?)`** — `GET /agents` with `label.*` filters and `page`, unwrapping `{agents}`.
221
+ `{ labels: { workspace_id: '…' } }` resolves a ZooClaw chat-URL workspace id to its agent — the
222
+ missing "get your agent_id with nothing but your key" step.
223
+ Scope is the engine's `owner_uid AND org_id`, so an agent a colleague created in your org is
224
+ fetchable by id but absent from your list. Page size is fixed at 100 by the engine.
225
+ Verified against staging on 2026-08-10, the day the gateway opened collection-level `GET /agents`
226
+ (it had answered `404 service_api.not_found` until then — FEEDBACK #16).
227
+
228
+ ## 0.0.4 — 2026-08-07
229
+
230
+ The Developer Preview surface goes from "agents and sessions" to the whole management plane, and
231
+ **eight response types are corrected against real recorded staging responses**. If you read any of
232
+ the fields in the table below, read that section before upgrading — the corrections are the point of
233
+ this release, and one of them was an infinite loop.
234
+
235
+ Still a patch bump: the version stays in 0.0.x until the first formal release, so the number does
236
+ not signal stability. Treat the response-shape section as breaking regardless of what the number says.
237
+
238
+ ### Response shapes corrected
239
+
240
+ Every one of these was found by driving the real API, not by reading a spec. Each row is what the
241
+ SDK's type promised, what the server actually sends, and what it cost the caller.
242
+
243
+ **1. `ScheduleRecord` had `schedule` and `sessionTarget`. The server sends neither.**
244
+ The read shape and the write shape are different documents. The cadence you sent as
245
+ `schedule: { kind: 'cron', expr }` reads back at `scheduleSpec.cronExpressions[0]`; the target you
246
+ sent as `sessionTarget: 'isolated'` reads back at `execution.kind`; and `scheduleId` is the
247
+ fully-qualified `cron/{computer_id}/{agent_id}/{schedule_id}` — the id you chose is `name`.
248
+ _Caller impact:_ `record.schedule.expr` was `undefined`, and passing `record.scheduleId` back to
249
+ `getSchedule` built a path that could not resolve.
250
+
251
+ **2. `updateSchedule` promised a `getSchedule` → `PUT` round trip and stripped only one field.**
252
+ The real body is refused for **six**: `execution`, `originMetadata`, `creatorPrincipalRef`,
253
+ `contextSnapshot` and `sessionTarget` are `400`, and `scheduleSpec` is worse (see 3). All six are now
254
+ `never` in `ScheduleUpdate` and stripped at runtime, so the obvious JavaScript round trip works.
255
+ _Caller impact:_ read-modify-write on a schedule was a `400 invalid_request`.
256
+
257
+ **3. `scheduleSpec` on a `PUT` is a SILENT NO-OP.**
258
+ `{ scheduleSpec, enabled: false }` answers **HTTP 200**, applies `enabled`, and leaves the cadence
259
+ exactly as it was. To change the cadence send `schedule: { kind: 'cron', expr, tz }` — the input
260
+ vocabulary. _Caller impact:_ a successful-looking update that quietly did not change the schedule.
261
+
262
+ **4. `ScheduleRun`'s fields were invented.**
263
+ `runs[]` is ONE array with TWO row shapes, discriminated by `source`: `run_projection` rows carry
264
+ `fired_at` / `status` / `consecutive_errors` (the outcome), `temporal` rows carry `scheduled_at` /
265
+ `taken_at` / `workflow_id` / `temporal_run_id` (the dispatch). Rows are grouped by source, not sorted
266
+ by time. **Neither shape carries `session_id`** — there is no walk from a fire to the session it
267
+ created; match `channel: 'cron'` and the `session_key` prefix instead.
268
+ _Caller impact:_ `run.status` was `undefined` on half the rows and `runs[0]` was not the latest fire.
269
+
270
+ **5. `EnvironmentRecord.state` and `.ownership` do not exist.**
271
+ Lifecycle is `status` (`active` / `archived`) and the tenant is flat: `scope` + `org_id`. The record
272
+ also gained `latest_ready_version`, and the two version numbers are not the same number:
273
+ `latest_version` is `1` the instant you create an Environment while version 1 is still `queued`;
274
+ `latest_ready_version` is `null` until a build lands. **Pin `latest_ready_version`.**
275
+ _Caller impact:_ `env.state` and `env.ownership.org_id` were `undefined`, and pinning
276
+ `latest_version` on an agent answered `409 environment_not_ready`.
277
+
278
+ **6. `EnvironmentVersionRecord.state` does not exist — the field is `status`.**
279
+ The JSDoc told callers to poll `getEnvironmentVersion` until `state === 'ready'`. `state` is
280
+ permanently `undefined`, so that loop compares `undefined` to `'ready'` for as long as the process
281
+ lives. _Caller impact:_ **an infinite loop** — the same class of bug as the `actual_state` trap this
282
+ SDK was written to prevent. Also note `e2b_build_id` is populated while a version is still
283
+ `building`, so it is not a readiness signal either. `status === 'ready'` is.
284
+
285
+ **7. `SessionRecord` had no `run_status`, and `status` is not the outcome.**
286
+ `listSessions` rows carry `run_status` (`succeeded`, `running`, …) and have **no `status` key at
287
+ all**; `getSession` returns `status: null` for the very same session, alongside its own `run_status`.
288
+ `status` is now typed `string | null`. _Caller impact:_ **this is the one correction that affects
289
+ published 0.0.3 consumers** — `session.status` was typed `string | undefined`, and reading it got
290
+ `null` from a read and `undefined` from a list row, never an outcome.
291
+
292
+ **8. `SkillRecord.ownership` required both fields as strings.**
293
+ An `org`-scope skill answers `owner_uid: null` (it belongs to the org, not a person), and a `global`
294
+ catalog row answers **both** fields as `null`. `SkillRecord.ownership` is now deliberately looser
295
+ than `Ownership`. In the same response, `latest_version` comes back as the **string `"1"`** from the
296
+ multipart create while other surfaces spell it as a number — compare loosely, or `Number()` it.
297
+ _Caller impact:_ `ownership.owner_uid` was typed non-null and was `null`, and `latest_version === 1`
298
+ was `false`.
299
+
300
+ ### Platform behaviour you have to know
301
+
302
+ These are not SDK bugs and cannot be typed away. They are how the platform behaves.
303
+
304
+ - **`scheduleSpec` on a `PUT` is accepted, answers 200, and is ignored** (see 3). The response of a
305
+ no-op update is byte-identical to the response of a real one; only a follow-up `getSchedule` can
306
+ tell you which you got.
307
+ - **`triggerSchedule` on a DISABLED schedule answers `triggered: true`** while the run projection
308
+ records `status: "skipped"`. `triggered` means the fire was dispatched, never that the turn ran.
309
+ The outcome is only in `listScheduleRuns`, on the `run_projection` row.
310
+
311
+ ### Added
312
+
313
+ - **Schedules** — `listSchedules`, `createSchedule`, `getSchedule`, `updateSchedule`,
314
+ `deleteSchedule`, `triggerSchedule`, `listScheduleRuns`. Schedules outlive their agent: delete them
315
+ yourself before deleting the agent.
316
+ - **Environments** — `listEnvironments`, `getEnvironment`, `createEnvironment`,
317
+ `createEnvironmentVersion`, `getEnvironmentVersion`, `archiveEnvironment`. `archiveEnvironment`
318
+ percent-encodes the colon in `{id}:archive`; a raw `:` is a 404, which is the whole reason the
319
+ method exists.
320
+ - **Skill registry** — `uploadSkill`, `uploadSkillVersion`, `listSkills`, `deleteSkill`. The zip's
321
+ single top-level directory name must equal the `name` in `SKILL.md`'s frontmatter; `scope` may only
322
+ be `org` or `personal`.
323
+ - **Sessions** — `listSessions`, `archiveSession`, `deleteSession`, and `listAllEvents`, which pages
324
+ `listEvents` to the end. `listEvents` truncates at 500 with nothing in the response to say so.
325
+ - **Readiness** — `waitUntilRunning`, which polls `status.desired_state` and never `actual_state`.
326
+ `actual_state` is chat-channel health; an API-only agent parks at `activating` forever.
327
+ - **Approvals** — `listApprovals`, `resolveApproval`. The route answers, but no real pending approval
328
+ has ever been produced, so `ApprovalRecord`'s field names are unverified. Read defensively.
329
+ - **Automation and operations** — `wake`, `exec`. A non-zero `exec` exit is still HTTP 200: the
330
+ promise resolves, check `exit_code`.
331
+ - **MCP** — `McpServerDeclaration` on `AgentResource.mcp`. Unauthenticated remote servers only;
332
+ `credential` is accepted and stored but unusable through the gateway.
333
+
334
+ ### Fixed
335
+
336
+ - **`ZooclawError.type` was `undefined` for every agent-family error.** There are two error
337
+ envelopes: most families answer `{ error: { type, message } }`, the agents family answers
338
+ `{ code, detail }`. Only the first was parsed, so an agent `404` reached callers as
339
+ `type: undefined` with the message `HTTP 404`. Both are parsed now, and the codes are surfaced
340
+ verbatim — note the vocabularies differ (`not_found` vs `service_api.not_found`).
341
+ - `createEnvironment`'s doc comment told callers to poll for `state: 'ready'`. See correction 6.
342
+
343
+ ### Tests
344
+
345
+ `pnpm test` is now the whole gate, and it runs the type checker before the suite — about half of what
346
+ this SDK guarantees is type-level, and reintroducing a wrong response type fails `tsc` rather than an
347
+ assertion.
348
+
349
+ - `src/__fixtures__/` — 55 REAL recorded staging responses, scrubbed of ids and credentials but
350
+ otherwise byte-faithful, down to `latest_version: "1"` being a string. Never hand-authored: a
351
+ hand-written fixture encodes the same guess the type does, so it can only agree with a wrong type.
352
+ - `src/responses.test.ts` — every fixture replayed through the method that returns it. Asserts
353
+ declared fields, absent fields, `null` versus missing, and — for each record type — that the SDK
354
+ declares nothing no recorded response has ever carried. All eight corrections above are red under
355
+ that check if reintroduced.
356
+ - Tests now import through `src/index.ts`, the published entry point, and `src/index.test.ts` pins
357
+ the export set, so a symbol missing from the entry point is a failing test rather than a broken
358
+ consumer.
359
+ - `src/sse.test.ts` — the frame parser split into its own file.
360
+ - CI on push and pull request: Node 24, `pnpm install --frozen-lockfile`, `pnpm test`, `pnpm build`.
361
+ The staging probes in `examples/` are excluded from the test glob and never run in CI — they need a
362
+ real API key and mutate a live tenant.
363
+
364
+ ## 0.0.3 — 2026-08-05
365
+
366
+ Initial public release: agents, sessions, durable events and the SSE stream.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Serendipity One Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,198 @@
1
+ # @zoowork-ai/sdk
2
+
3
+ TypeScript SDK for the [ZooWork Managed Agents](https://github.com/SerendipityOneInc/zoowork-agents-docs) API. Developer Preview.
4
+
5
+ Zero runtime dependencies — it uses the platform `fetch`, which you can override for edge runtimes and tests. ESM only, Node 20+.
6
+
7
+ ```bash
8
+ npm install @zoowork-ai/sdk
9
+ ```
10
+
11
+ ## Quickstart
12
+
13
+ You need an API key (`zct_...`) issued for your organization — create one in the ZooWork App under **Settings → API Keys** (any personal org; enterprise orgs need the admin role), or ask your org admin for one. The secret is shown exactly once at creation. Keep it server-side: it authenticates as your whole organization, not as one end user.
14
+
15
+ ```ts
16
+ import { createZooworkClient } from '@zoowork-ai/sdk'
17
+
18
+ const zc = createZooworkClient({ apiKey: process.env.ZOOWORK_API_KEY })
19
+
20
+ // Or set ZOOWORK_API_KEY and pass nothing at all:
21
+ // const zc = createZooworkClient()
22
+ ```
23
+
24
+ The base URL has a working default, so you do not configure an endpoint. Override it with
25
+ `ZOOWORK_BASE_URL`, or with `baseUrl` on the call, to point at a different deployment.
26
+
27
+ ```ts
28
+ // 1. Create an agent. Ownership is derived from your key, so `resource` is all you
29
+ // send; the gateway also seeds the platform credentials the agent needs to call a model.
30
+ const agent = await zc.createAgent({
31
+ resource: { name: 'research-agent', model: { primary: 'litellm/claude-sonnet-5' } },
32
+ })
33
+
34
+ // 2. Start it. Without this, createSession() returns 409 agent_not_running.
35
+ await zc.startAgent(agent.agent_id)
36
+
37
+ // 3. Open a session with the first message already in it.
38
+ const session = await zc.createSession(agent.agent_id, {
39
+ initial_events: [{ type: 'user.message', content: 'What can you do?' }],
40
+ })
41
+ ```
42
+
43
+ ## Configuration
44
+
45
+ | Option | Environment variable | Default |
46
+ |---|---|---|
47
+ | `apiKey` | `ZOOWORK_API_KEY` | none - construction throws without one |
48
+ | `baseUrl` | `ZOOWORK_BASE_URL` | the public gateway (`DEFAULT_BASE_URL`) |
49
+ | `fetch` | - | `globalThis.fetch` |
50
+
51
+ An explicit option always beats the environment variable.
52
+
53
+ > **Finding the agent you built in the app.** The first path segment of a ZooWork chat URL
54
+ > (`/chat/<32-hex>/sessions/…`) is a *workspace* id, not an `agt_…`. Resolve it with
55
+ > `zc.listAgents({ labels: { workspace_id: '<32-hex>' } })`; a bare `zc.listAgents()` lists
56
+ > everything your key can see. Scope is `owner_uid AND org_id` — an agent a *colleague*
57
+ > created in your org is fetchable by id but will not appear in your list.
58
+
59
+ > **Wait on `status.desired_state`, never on `status.actual_state`.**
60
+ > `actual_state` reports chat-channel connectivity. An API-only agent has no channels,
61
+ > so it stays at `activating` forever and `active` is unreachable — a readiness loop
62
+ > that polls it never returns. `desired_state` flips to `running` in well under a second.
63
+ > `await zc.waitUntilRunning(agentId)` is that loop, written correctly.
64
+
65
+ ## Streaming a turn
66
+
67
+ `run.finished` ends a turn; assistant text arrives on `agent.assistant`.
68
+
69
+ ```ts
70
+ import { assistantText, isRunFinished, runOutcome, toolCall } from '@zoowork-ai/sdk'
71
+
72
+ for await (const ev of zc.streamEvents(agent.agent_id, session.session_id)) {
73
+ process.stdout.write(assistantText(ev)) // '' for every non-assistant event
74
+
75
+ const call = toolCall(ev) // present only on agent.tool; pair start/end by toolCallId
76
+ if (call?.phase === 'start') console.log(`\n[tool] ${call.toolName}`)
77
+
78
+ if (isRunFinished(ev)) {
79
+ console.log(`\n-> ${runOutcome(ev)}`) // succeeded | failed | aborted
80
+ break
81
+ }
82
+ }
83
+ ```
84
+
85
+ Three things worth knowing before you write that loop:
86
+
87
+ - **The stream is session-scoped and does not close when a turn ends.** The server closes it after an idle period. Break on `isRunFinished(ev)` yourself, or you block until that timeout.
88
+ - **It resumes.** Every frame carries a durable `seq`. After a dropped connection, restart with `{ after: lastSeq }` and the server replays from there — nothing lost, nothing duplicated.
89
+ - **REST and SSE spell the same event differently** (`event_type` vs `eventType`, and neither has a top-level `type`). The SDK normalizes both into one `SessionEvent`; you only ever read `eventType`.
90
+
91
+ ## Bring your own skill
92
+
93
+ A skill is a zip. One upload creates the skill *and* its first version; `putAgentSkill` attaches it.
94
+
95
+ ```ts
96
+ import { readFile } from 'node:fs/promises'
97
+
98
+ const skill = await zc.uploadSkill(await readFile('market-research.zip'), { scope: 'org' })
99
+ await zc.putAgentSkill(agent.agent_id, skill.skill_id)
100
+ ```
101
+
102
+ The zip's single top-level directory must be named exactly like the `name` in its `SKILL.md`
103
+ frontmatter — `market-research/SKILL.md` declaring `name: market-research`. A mismatch is a 400,
104
+ and it is the first one nearly everyone gets. `scope` is `org` or `personal`; the preinstalled
105
+ `global` skills are listable but not installable with an API key, so this is the only way to
106
+ control what a skill says. `uploadSkillVersion` publishes an update, and agents that installed it
107
+ unpinned follow along without another `putAgentSkill`.
108
+
109
+ ## Schedules, wake and exec
110
+
111
+ ```ts
112
+ await zc.createSchedule(agent.agent_id, {
113
+ schedule_id: 'daily-report',
114
+ schedule: { kind: 'cron', expr: '0 9 * * *', tz: 'Asia/Singapore' },
115
+ payload: { kind: 'agentTurn', message: 'Generate the daily report.' },
116
+ })
117
+
118
+ await zc.wake(agent.agent_id, { text: 'Review the pending deployment.' }) // at the next heartbeat
119
+
120
+ const { exit_code, stdout } = await zc.exec(agent.agent_id, ['bash', '-lc', 'pwd'])
121
+ ```
122
+
123
+ - **Schedules outlive their agent.** `stopAgent` and `deleteAgent` leave them running; list and
124
+ delete them yourself. Also available: `getSchedule`, `updateSchedule`, `triggerSchedule`,
125
+ `listScheduleRuns`.
126
+ - **`updateSchedule` must omit `sessionTarget`.** It is immutable, and echoing it back from a
127
+ `getSchedule` result — the obvious thing to do — is a 400. The types refuse it for you.
128
+ - **`exec` resolves on a failed command.** A non-zero exit is still HTTP 200: check `exit_code`,
129
+ don't wait for a rejection. It runs in `/workspace` and needs an agent-scope sandbox.
130
+ - **A cron job can carry an outcome gate.** `payload.outcome` says what "done" looks like — a
131
+ sandbox `command` whose exit 0 means satisfied, or an LLM `rubric` graded in a fresh context.
132
+ The run evaluates and revises itself up to `maxIterations` (1–5), and under the default
133
+ `publish: 'after_satisfied'` a result that failed evaluation is not announced. An agent-level
134
+ default lives at `resource.outcome`; a job's own `outcome` overrides it, and an explicit
135
+ `null` opts the job out. Cron fires only — heartbeats and interactive sessions never evaluate.
136
+
137
+ ## Sessions, approvals, environments
138
+
139
+ `listSessions`, `archiveSession` and `deleteSession` round out the session surface. There is no
140
+ `patchSession`: the gateway does not proxy `PATCH` at all (405), so session `metadata` is fixed at
141
+ creation time.
142
+
143
+ `listApprovals` / `resolveApproval` expose the approvals resource — `decision` is one of
144
+ `allow-once`, `allow-always`, `deny`. Note that human-in-the-loop is not usable end to end yet: an
145
+ agent parked on an approval spends its whole turn budget waiting.
146
+
147
+ `listEnvironments`, `getEnvironment`, `createEnvironment`, `createEnvironmentVersion`,
148
+ `getEnvironmentVersion` and `archiveEnvironment` manage prebuilt sandbox images (apt/npm/pip
149
+ packages, files, a build script, and an outbound allowlist). Two facts worth having before you
150
+ start: an agent's Environment **freezes on its first sandbox creation** — after that every change
151
+ is `409 environment_locked`, and stopping the agent does not clear it — and sandbox networking
152
+ defaults to unrestricted unless the Environment declares `networking: { type: 'limited' }`.
153
+
154
+ ## Artifacts and the system prompt
155
+
156
+ ```ts
157
+ const { artifacts, has_more } = await zc.listArtifacts(agent.agent_id)
158
+ const { url } = await zc.downloadArtifact(agent.agent_id, artifacts[0].artifact_id)
159
+
160
+ const { declaration, effective } = await zc.getSystemPrompt(agent.agent_id)
161
+ ```
162
+
163
+ Artifacts are published by the agent's own `artifact_publish` tool during a turn — there is no
164
+ API for publishing from outside the loop. This surface lists what the agent published,
165
+ re-resolves an access URL (`downloadArtifact` mints a fresh one; the URL is a revocable bearer
166
+ capability, so treat it like a secret), and deletes. These routes demand `owner_uid`/`org_id`
167
+ selectors; the SDK derives both from the agent's own projection and caches them, at the cost of
168
+ one extra GET on first use.
169
+
170
+ `getSystemPrompt` answers the pinned template version and the rendered result;
171
+ `previewSystemPrompt` assembles the exact prompt for a given set of runtime facts without
172
+ touching any session. The pin is set at create time and never follows a later platform
173
+ activation on its own — moving it is one explicit call, `upgradeSystemPrompt`, which takes
174
+ the agent's current `config_version` as a CAS (`409 config_version_changed` on a stale one)
175
+ and answers the new pin plus the version bump it cost.
176
+
177
+ ## Two helpers
178
+
179
+ ```ts
180
+ const agent = await zc.waitUntilRunning(agentId) // polls desired_state, not actual_state
181
+ const events = await zc.listAllEvents(agentId, sessionId) // pages past the silent 500 cap
182
+ ```
183
+
184
+ Each wraps a trap that is invisible from the outside: readiness lives in `status.desired_state`,
185
+ and `listEvents` truncates at 500 events with nothing in the response to say it did.
186
+
187
+ ## Documentation
188
+
189
+ Full guides and the capability matrix: **[zoowork-agents-docs](https://github.com/SerendipityOneInc/zoowork-agents-docs)**.
190
+
191
+ Runnable examples in [`examples/`](examples):
192
+
193
+ - [`live-smoke.ts`](examples/live-smoke.ts) — drive one agent through one turn and verify the REST and SSE reads agree.
194
+ - [`capability-probe.ts`](examples/capability-probe.ts) — create a throwaway agent, walk the whole lifecycle, and print a verdict per capability.
195
+
196
+ ## License
197
+
198
+ MIT