@pouchy_ai/admin-sdk 0.5.0 → 0.6.1

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,63 @@
2
2
 
3
3
  All notable changes to `@pouchy_ai/admin-sdk` are documented here.
4
4
 
5
+ ## 0.6.1 — 2026-07-27
6
+
7
+ Fix: the four long-running requests are no longer cut at 30s while the server is
8
+ still working.
9
+
10
+ - **Per-route request deadlines.** `POST /knowledge`, `POST /knowledge/file`,
11
+ `POST /knowledge/url` and `DELETE /users/{instanceId}` are the Admin API
12
+ handlers that declare `maxDuration: 300` — their response headers only arrive
13
+ when the ingest (chunk + summarize + embed, after OCR / Whisper / vision or an
14
+ SSRF-guarded page fetch) or the recursive GDPR erasure has FINISHED. Under the
15
+ flat 30s deadline a real PDF or a data-heavy instance rejected
16
+ `request timed out after 30000ms` while the operation kept running and
17
+ billing server-side, and the natural retry then raced the still-running first
18
+ ingest. Those four now default to `LONG_WORK_TIMEOUT_MS` (310s — the server's
19
+ own ceiling plus headroom); every other request keeps the 30s default, so a
20
+ genuinely hung server still fails fast.
21
+
22
+ This is the same defect the companion JS SDK fixed as `BUFFERED_TURN_TIMEOUT_MS`
23
+ (CR-141); 0.6.0 added `/knowledge/file` and `/knowledge/url` behind the old
24
+ flat deadline without porting it.
25
+
26
+ - **`timeoutMs` still wins.** A host that sets `timeoutMs` explicitly gets it on
27
+ every request, long or short — only the DEFAULT is route-aware. No API surface
28
+ added or removed; purely widening for calls that previously failed.
29
+
30
+ - New exports for hosts that want the contract in code:
31
+ `LONG_WORK_TIMEOUT_MS`, `LONG_WORK_REQUESTS`, `requestDeadlineMs(method, path)`.
32
+
33
+ ## 0.6.0 — 2026-07-21
34
+
35
+ Additive: knowledge ingestion parity + headless agent version control. Both
36
+ close a documented owner-console-only gap so the whole build-and-serve loop is
37
+ now reachable with a `pchy_admin_…` key.
38
+
39
+ - **Knowledge ingestion beyond raw text.** Three new methods mirror the
40
+ dashboard's knowledge tab, sharing the exact server-side pipelines:
41
+ - **`ingestKnowledgeFile({ dataUrl, name?, kind?, locale?, replaceDocId? })`**
42
+ — ingest a PDF / audio / video / image data URL; the server runs the same
43
+ understanding (OCR / Whisper / vision caption) as the SDK personal-materials
44
+ path, then chunks + embeds into the shared corpus.
45
+ - **`ingestKnowledgeUrl({ url, name?, locale?, replaceDocId? })`** — ingest a
46
+ web page (https only, SSRF-guarded fetch, HTML reduced to text).
47
+ - **`searchKnowledge(query)`** — recall probe: run the SAME retrieval an
48
+ instance turn uses and get the ranked chunks back, without burning an LLM
49
+ turn. `mode` is `semantic` or `lexical`.
50
+ - **Agent version control (headless GitOps of a persona).** Six methods:
51
+ `listAgentVersions`, `getAgentVersion`, `diffAgentVersions`, `rollbackAgent`,
52
+ `getAgentPromotion`, `promoteAgent`. Inspect the rolling revision history,
53
+ diff any two revisions (or against `current`), roll back (git-revert
54
+ semantics), and drive staging→prod promotion — all previously
55
+ owner-session-only.
56
+
57
+ Server: adds `POST /v1/admin/knowledge/{file,url,search}`,
58
+ `GET /v1/admin/agents/{agentId}/versions[/{rev}|/diff]`,
59
+ `POST /v1/admin/agents/{agentId}/versions/rollback`, and
60
+ `GET|POST /v1/admin/agents/{agentId}/promote`. No breaking changes.
61
+
5
62
  ## 0.5.0 — 2026-07-17
6
63
 
7
64
  Additive: end-user listing pagination.
package/README.md CHANGED
@@ -62,10 +62,24 @@ createAdminClient({
62
62
  adminKey: 'pchy_admin_…', // required
63
63
  baseUrl: 'https://pouchy.ai/v1/admin', // optional (self-host / staging)
64
64
  fetch: myFetch, // optional (Node <18, or tests)
65
- timeoutMs: 30_000 // optional per-request timeout (default 30s)
65
+ timeoutMs: 30_000 // optional per-request timeout see below
66
66
  });
67
67
  ```
68
68
 
69
+ `timeoutMs` defaults to **30s**, except for the four requests whose server
70
+ handler declares `maxDuration: 300` and therefore answers only when the work is
71
+ finished — `POST /knowledge`, `POST /knowledge/file`, `POST /knowledge/url`
72
+ (chunk + summarize + embed, after OCR / Whisper / vision or a page fetch) and
73
+ `DELETE /users/{instanceId}` (recursive GDPR erasure). Those default to **310s**
74
+ (`LONG_WORK_TIMEOUT_MS`), so a client abort can only ever mean "the server really
75
+ is hung", never "the server is still working" — a shorter deadline there reports
76
+ a failure for an ingest that is succeeding, and the retry it invites races the
77
+ still-running first one.
78
+
79
+ Setting `timeoutMs` explicitly always wins and applies to **every** request, long
80
+ or short. `requestDeadlineMs(method, path)` returns the default a given request
81
+ would use.
82
+
69
83
  ## Errors
70
84
 
71
85
  Every method throws `AdminApiError` on failure — a non-2xx response, a network
@@ -86,10 +100,11 @@ try {
86
100
  | Area | Methods |
87
101
  | --- | --- |
88
102
  | Agents | `listAgents` · `createAgent` · `getAgent` · `updateAgent` · `deleteAgent` |
103
+ | Agent versions | `listAgentVersions` · `getAgentVersion` · `diffAgentVersions({ from?, to? })` · `rollbackAgent` · `getAgentPromotion` · `promoteAgent` (staging→prod) |
89
104
  | Voices | `listVoices({ gender?, age?, locale? })` — catalog for programmatic voice selection (each `CatalogVoice` carries `age`) |
90
105
  | Secret keys | `listKeys` · `createKey` · `revokeKey` · `rotateKey` (24 h grace) |
91
106
  | End users | `listUsers({ limit?, cursor? })` (cursor-paginated — the response's `nextCursor` feeds the next page; filter variants: `external_user_id` / `external_user_prefix`) · `setUserSuspended` · `deleteUser` · `getUserWallet` · `getUserTraces` · `importUsers` · `exportUser` · `getUserSessions` · `getUserTurns` |
92
- | Knowledge | `listKnowledge` · `ingestKnowledge` · `deleteKnowledge` |
107
+ | Knowledge | `listKnowledge` · `ingestKnowledge` · `ingestKnowledgeFile` (PDF/audio/video/image) · `ingestKnowledgeUrl` (web page) · `searchKnowledge` (recall probe) · `deleteKnowledge` |
93
108
  | Skills | `listSkills` · `installSkill` · `updateSkill` · `setSkillRate` · `setSkillDailyCap` · `grantSkill` (free-HTTP) · `compileSkill` (prose→tools) · `uninstallSkill` |
94
109
  | Credentials | `listCredentials` · `putCredentials` · `deleteCredentials` |
95
110
  | Channels | `listChannels` · `createChannel` · `getChannel` · `updateChannel` · `deleteChannel` |
package/dist/index.d.ts CHANGED
@@ -1,5 +1,34 @@
1
- export declare const ADMIN_SDK_VERSION = "0.5.0";
1
+ export declare const ADMIN_SDK_VERSION = "0.6.1";
2
2
  export declare const DEFAULT_BASE_URL = "https://pouchy.ai/v1/admin";
3
+ /** Deadline for the routes whose server handler declares `maxDuration: 300` —
4
+ * the server's own ceiling plus headroom, so a client abort can only ever mean
5
+ * "the server really is hung", never "the server is still working".
6
+ *
7
+ * Why this exists: a flat client deadline BELOW a route's declared server
8
+ * ceiling turns a succeeding, billed operation into a client-side
9
+ * `request timed out`. The companion JS SDK hit exactly this (CR-141) and
10
+ * fixed it with `BUFFERED_TURN_TIMEOUT_MS = 310_000`; on knowledge ingest it
11
+ * is worse than a bad error message, because the integrator's natural retry
12
+ * then races the still-running first ingest — the register's CR-14 interleave,
13
+ * made systematic instead of rare. This SDK shipped the same flat 30s and, in
14
+ * 0.6.0, ADDED two more long routes behind it (`/knowledge/file`,
15
+ * `/knowledge/url`). Same number as the JS constant, for the same reason. */
16
+ export declare const LONG_WORK_TIMEOUT_MS = 310000;
17
+ /** The Admin API requests whose handler declares `maxDuration: 300`.
18
+ *
19
+ * Derived from the route files, not guessed — `sdk-deadlines.drift.test.ts`
20
+ * binds this list to `export const config = { maxDuration }` in
21
+ * `src/routes/v1/admin/**` in BOTH directions, so a new long-running admin
22
+ * route (or a renamed one) fails a merge instead of silently shipping a
23
+ * 30s client cut. Paths are SDK-relative (the base URL already carries
24
+ * `/v1/admin`). */
25
+ export declare const LONG_WORK_REQUESTS: ReadonlyArray<{
26
+ method: string;
27
+ path: RegExp;
28
+ }>;
29
+ /** The deadline a given request runs under when the host set no `timeoutMs`.
30
+ * Exported so the contract is assertable without timing anything. */
31
+ export declare function requestDeadlineMs(method: string, path: string): number;
3
32
  export interface AdminClientOptions {
4
33
  /** A project Admin key (`pchy_admin_…`) from the dashboard Admin Keys page. */
5
34
  adminKey: string;
@@ -7,8 +36,12 @@ export interface AdminClientOptions {
7
36
  baseUrl?: string;
8
37
  /** Inject a fetch impl (Node <18, or for tests). Default global fetch. */
9
38
  fetch?: typeof fetch;
10
- /** Per-request timeout in ms (default 30s). A request that outlives it
11
- * rejects with an AdminApiError(status 0). */
39
+ /** Per-request timeout in ms. Default 30s, EXCEPT the requests whose server
40
+ * handler declares `maxDuration: 300` (knowledge ingest by text/file/url,
41
+ * and the GDPR user delete), which default to
42
+ * {@link LONG_WORK_TIMEOUT_MS}. Setting this explicitly always wins and
43
+ * applies to every request, long or short. A request that outlives its
44
+ * deadline rejects with an AdminApiError(status 0). */
12
45
  timeoutMs?: number;
13
46
  }
14
47
  /** Thrown on any non-2xx response. `status` is the HTTP status; `message` is the
@@ -132,6 +165,62 @@ export interface AdminClient {
132
165
  deleteAgent(agentId: string): Promise<{
133
166
  deleted: boolean;
134
167
  }>;
168
+ /** Archived template revisions, newest first. The live/current template is
169
+ * NOT in here — it lives on the agent doc; a snapshot is created only when a
170
+ * persona edit supersedes a rev. */
171
+ listAgentVersions(agentId: string): Promise<{
172
+ versions: Array<{
173
+ version: number;
174
+ templateRev: number;
175
+ archivedAt: string;
176
+ name: string;
177
+ }>;
178
+ }>;
179
+ /** One archived template snapshot — the full frozen agent for that revision. */
180
+ getAgentVersion(agentId: string, rev: number): Promise<{
181
+ version: Agent;
182
+ }>;
183
+ /** Field-level diff between two revisions. `from`/`to` are each an archived
184
+ * version number or `current` (default) = the live template; only changed
185
+ * fields are returned. */
186
+ diffAgentVersions(agentId: string, opts?: {
187
+ from?: number | 'current';
188
+ to?: number | 'current';
189
+ }): Promise<{
190
+ from: string;
191
+ to: string;
192
+ diff: Array<{
193
+ field: string;
194
+ from: unknown;
195
+ to: unknown;
196
+ }>;
197
+ }>;
198
+ /** Roll an agent back to an archived revision. git-revert semantics: the
199
+ * snapshot re-applies as a NEW edit (rev moves forward), the current state is
200
+ * archived first. */
201
+ rollbackAgent(agentId: string, version: number): Promise<{
202
+ agent: Agent;
203
+ }>;
204
+ /** Promotion status: the head (staging) vs the pinned production version, plus
205
+ * the diff a promotion would ship (empty when in sync). `pending` is false
206
+ * when the agent was never promoted (live follows the head). */
207
+ getAgentPromotion(agentId: string): Promise<{
208
+ stagingRev: number;
209
+ prodRev: number | null;
210
+ pending: boolean;
211
+ diff: Array<{
212
+ field: string;
213
+ from: unknown;
214
+ to: unknown;
215
+ }>;
216
+ }>;
217
+ /** Promote the head to production: freezes the head as an archived version and
218
+ * pins prodRev WITHOUT bumping the rev. Live instances re-resolve on their
219
+ * next mint; test instances always run the head. */
220
+ promoteAgent(agentId: string): Promise<{
221
+ prodRev: number;
222
+ templateRev: number;
223
+ }>;
135
224
  listKeys(): Promise<{
136
225
  keys: SecretKey[];
137
226
  }>;
@@ -230,6 +319,48 @@ export interface AdminClient {
230
319
  chunks: number;
231
320
  };
232
321
  }>;
322
+ /** Ingest a RAW file (PDF / audio / video / image data URL) into the shared
323
+ * corpus — same server-side understanding (OCR / Whisper / vision caption)
324
+ * as the SDK personal-materials path, then chunk + embed. `replaceDocId`
325
+ * updates an existing doc in place. */
326
+ ingestKnowledgeFile(input: {
327
+ dataUrl: string;
328
+ name?: string;
329
+ kind?: string;
330
+ locale?: string;
331
+ replaceDocId?: string;
332
+ }): Promise<{
333
+ ok: boolean;
334
+ doc: {
335
+ docId: string;
336
+ chunks: number;
337
+ };
338
+ summary?: string;
339
+ }>;
340
+ /** Ingest a WEB PAGE by URL (https only, SSRF-guarded fetch, HTML reduced to
341
+ * readable text server-side) into the shared corpus. */
342
+ ingestKnowledgeUrl(input: {
343
+ url: string;
344
+ name?: string;
345
+ locale?: string;
346
+ replaceDocId?: string;
347
+ }): Promise<{
348
+ ok: boolean;
349
+ doc: {
350
+ docId: string;
351
+ chunks: number;
352
+ };
353
+ summary?: string;
354
+ sourceUrl: string;
355
+ }>;
356
+ /** Recall probe: run the SAME retrieval an instance turn uses and get the
357
+ * ranked chunks back, WITHOUT burning an LLM turn. `mode` is the recall
358
+ * truth — `semantic` (vector path answered) or `lexical` (fallback). */
359
+ searchKnowledge(query: string): Promise<{
360
+ query: string;
361
+ mode: 'semantic' | 'lexical';
362
+ hits: unknown[];
363
+ }>;
233
364
  deleteKnowledge(docId: string): Promise<{
234
365
  deleted: boolean;
235
366
  }>;
package/dist/index.js CHANGED
@@ -8,10 +8,53 @@
8
8
  // import { createAdminClient } from '@pouchy_ai/admin-sdk';
9
9
  // const admin = createAdminClient({ adminKey: process.env.POUCHY_ADMIN_KEY! });
10
10
  // const { agents } = await admin.listAgents();
11
- export const ADMIN_SDK_VERSION = '0.5.0';
11
+ export const ADMIN_SDK_VERSION = '0.6.1';
12
12
  export const DEFAULT_BASE_URL = 'https://pouchy.ai/v1/admin';
13
13
  /** Default per-request timeout (ms). A hung upstream otherwise never rejects. */
14
14
  const DEFAULT_TIMEOUT_MS = 30_000;
15
+ /** Deadline for the routes whose server handler declares `maxDuration: 300` —
16
+ * the server's own ceiling plus headroom, so a client abort can only ever mean
17
+ * "the server really is hung", never "the server is still working".
18
+ *
19
+ * Why this exists: a flat client deadline BELOW a route's declared server
20
+ * ceiling turns a succeeding, billed operation into a client-side
21
+ * `request timed out`. The companion JS SDK hit exactly this (CR-141) and
22
+ * fixed it with `BUFFERED_TURN_TIMEOUT_MS = 310_000`; on knowledge ingest it
23
+ * is worse than a bad error message, because the integrator's natural retry
24
+ * then races the still-running first ingest — the register's CR-14 interleave,
25
+ * made systematic instead of rare. This SDK shipped the same flat 30s and, in
26
+ * 0.6.0, ADDED two more long routes behind it (`/knowledge/file`,
27
+ * `/knowledge/url`). Same number as the JS constant, for the same reason. */
28
+ export const LONG_WORK_TIMEOUT_MS = 310_000;
29
+ /** The Admin API requests whose handler declares `maxDuration: 300`.
30
+ *
31
+ * Derived from the route files, not guessed — `sdk-deadlines.drift.test.ts`
32
+ * binds this list to `export const config = { maxDuration }` in
33
+ * `src/routes/v1/admin/**` in BOTH directions, so a new long-running admin
34
+ * route (or a renamed one) fails a merge instead of silently shipping a
35
+ * 30s client cut. Paths are SDK-relative (the base URL already carries
36
+ * `/v1/admin`). */
37
+ export const LONG_WORK_REQUESTS = [
38
+ // Synchronous ingest: chunk + summarize + embed before headers.
39
+ { method: 'POST', path: /^\/knowledge$/ },
40
+ // …plus OCR / Whisper / vision understanding of the raw file first.
41
+ { method: 'POST', path: /^\/knowledge\/file$/ },
42
+ // …plus an SSRF-guarded fetch + HTML reduction first.
43
+ { method: 'POST', path: /^\/knowledge\/url$/ },
44
+ // GDPR erasure: recursive delete of the instance's whole users/** subtree
45
+ // plus the top-level social graph. A client abort here is the worst of the
46
+ // four — the operator is left not knowing whether the wipe completed.
47
+ { method: 'DELETE', path: /^\/users\/[^/]+$/ }
48
+ ];
49
+ /** The deadline a given request runs under when the host set no `timeoutMs`.
50
+ * Exported so the contract is assertable without timing anything. */
51
+ export function requestDeadlineMs(method, path) {
52
+ const bare = path.split('?')[0];
53
+ const m = method.toUpperCase();
54
+ return LONG_WORK_REQUESTS.some((r) => r.method === m && r.path.test(bare))
55
+ ? LONG_WORK_TIMEOUT_MS
56
+ : DEFAULT_TIMEOUT_MS;
57
+ }
15
58
  /** Thrown on any non-2xx response. `status` is the HTTP status; `message` is the
16
59
  * server's `error` string when present. */
17
60
  export class AdminApiError extends Error {
@@ -37,8 +80,11 @@ export function createAdminClient(opts) {
37
80
  const f = opts.fetch ?? globalThis.fetch;
38
81
  if (!f)
39
82
  throw new AdminApiError('no fetch available — pass opts.fetch on Node <18', 0);
40
- const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
41
83
  async function request(method, path, body) {
84
+ // An explicit host `timeoutMs` always wins (JS SDK contract); otherwise
85
+ // the deadline is sized to the ROUTE, so the four handlers that declare
86
+ // `maxDuration: 300` are not cut at 30s while they are still working.
87
+ const timeoutMs = opts.timeoutMs ?? requestDeadlineMs(method, path);
42
88
  // Every failure surfaces as an AdminApiError (the doc contract): a
43
89
  // network/DNS error or a timeout would otherwise escape as a raw
44
90
  // `TypeError: fetch failed` / AbortError. Status 0 = never reached the
@@ -70,6 +116,12 @@ export function createAdminClient(opts) {
70
116
  getAgent: (id) => request('GET', `/agents/${encodeURIComponent(id)}`),
71
117
  updateAgent: (id, patch) => request('PATCH', `/agents/${encodeURIComponent(id)}`, patch),
72
118
  deleteAgent: (id) => request('DELETE', `/agents/${encodeURIComponent(id)}`),
119
+ listAgentVersions: (id) => request('GET', `/agents/${encodeURIComponent(id)}/versions`),
120
+ getAgentVersion: (id, rev) => request('GET', `/agents/${encodeURIComponent(id)}/versions/${rev}`),
121
+ diffAgentVersions: (id, opts = {}) => request('GET', `/agents/${encodeURIComponent(id)}/versions/diff${qs(opts)}`),
122
+ rollbackAgent: (id, version) => request('POST', `/agents/${encodeURIComponent(id)}/versions/rollback`, { version }),
123
+ getAgentPromotion: (id) => request('GET', `/agents/${encodeURIComponent(id)}/promote`),
124
+ promoteAgent: (id) => request('POST', `/agents/${encodeURIComponent(id)}/promote`, {}),
73
125
  listKeys: () => request('GET', '/keys'),
74
126
  createKey: (input) => request('POST', '/keys', input),
75
127
  revokeKey: (id) => request('DELETE', `/keys/${encodeURIComponent(id)}`),
@@ -85,6 +137,9 @@ export function createAdminClient(opts) {
85
137
  getUserTurns: (id, sessionId, params = {}) => request('GET', `/users/${encodeURIComponent(id)}/sessions/${encodeURIComponent(sessionId)}/turns${qs(params)}`),
86
138
  listKnowledge: () => request('GET', '/knowledge'),
87
139
  ingestKnowledge: (input) => request('POST', '/knowledge', input),
140
+ ingestKnowledgeFile: (input) => request('POST', '/knowledge/file', input),
141
+ ingestKnowledgeUrl: (input) => request('POST', '/knowledge/url', input),
142
+ searchKnowledge: (query) => request('POST', '/knowledge/search', { query }),
88
143
  deleteKnowledge: (id) => request('DELETE', `/knowledge/${encodeURIComponent(id)}`),
89
144
  listSkills: () => request('GET', '/skills'),
90
145
  installSkill: (input) => request('POST', '/skills', input),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pouchy_ai/admin-sdk",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
4
4
  "description": "Typed TypeScript client for the Pouchy Admin API \u2014 manage agents, keys, end users, knowledge, skills, channels, schedules, webhooks and credentials headlessly, with a project Admin key.",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE",