@happyrobot-ai/sdk 0.1.0 → 0.1.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/README.md ADDED
@@ -0,0 +1,774 @@
1
+ # @happyrobot/sdk
2
+
3
+ TypeScript SDK for the HappyRobot Public API.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @happyrobot/sdk
9
+ # or
10
+ pnpm add @happyrobot/sdk
11
+ # or
12
+ yarn add @happyrobot/sdk
13
+ ```
14
+
15
+ ## Quick Start
16
+
17
+ ```ts
18
+ import { HappyRobotClient } from "@happyrobot/sdk";
19
+
20
+ const client = new HappyRobotClient({ apiKey: "sk_live_..." });
21
+
22
+ // List workflows
23
+ const { data } = await client.workflows.list();
24
+
25
+ // Trigger a run
26
+ const { run_id } = await client.workflows.triggerRun("my-workflow", {
27
+ payload: { phone: "+1234567890" },
28
+ });
29
+ ```
30
+
31
+ ## Configuration
32
+
33
+ ```ts
34
+ const client = new HappyRobotClient({
35
+ apiKey: "sk_live_...", // Required. API key (sk_live_... or sk_test_...)
36
+ timeout: 30_000, // Optional. Request timeout in ms. Defaults to 30000
37
+ maxRetries: 2, // Optional. Retries on 429/5xx with exponential backoff. Defaults to 2
38
+ fetch: customFetch, // Optional. Custom fetch implementation
39
+ });
40
+ ```
41
+
42
+ ## Helpers
43
+
44
+ High-level convenience functions available via `@happyrobot/sdk/helpers`.
45
+
46
+ ### `triggerAndWait`
47
+
48
+ Trigger a workflow run and poll until it reaches a terminal status.
49
+
50
+ ```ts
51
+ import { triggerAndWait } from "@happyrobot/sdk/helpers";
52
+
53
+ const { run, sessions } = await triggerAndWait(client, {
54
+ workflowId: "my-workflow",
55
+ payload: { phone: "+1234567890" },
56
+ timeoutMs: 300_000, // Optional. Defaults to 5 minutes
57
+ pollIntervalMs: 2_000, // Optional. Defaults to 2 seconds
58
+ fetchSessions: true, // Optional. Fetch sessions on completion. Defaults to true
59
+ environment: "production", // Optional. Defaults to "production"
60
+ });
61
+
62
+ console.log(run.status); // "completed" | "failed" | "canceled" | ...
63
+ ```
64
+
65
+ ### `createVoiceAgent`
66
+
67
+ Create a voice agent workflow from a template with optional publishing.
68
+
69
+ ```ts
70
+ import { createVoiceAgent } from "@happyrobot/sdk/helpers";
71
+
72
+ const { workflow, publishResult } = await createVoiceAgent(client, {
73
+ name: "Sales Agent",
74
+ template: "voice-agent", // "voice-agent" (outbound) or "inbound-voice-agent"
75
+ prompt: "You are a helpful sales assistant...",
76
+ initialMessage: "Hi, how can I help you today?",
77
+ publish: true,
78
+ environment: "production",
79
+ folderId: "folder-id", // Optional
80
+ });
81
+ ```
82
+
83
+ ### `createFromTemplate`
84
+
85
+ Create any workflow from a template with custom inputs.
86
+
87
+ ```ts
88
+ import { createFromTemplate } from "@happyrobot/sdk/helpers";
89
+
90
+ const { workflow } = await createFromTemplate(client, {
91
+ template: "voice-agent",
92
+ name: "My Agent",
93
+ inputs: { agent_name: "My Agent" },
94
+ publish: false,
95
+ });
96
+ ```
97
+
98
+ ## Resources
99
+
100
+ | Property | Class | Description |
101
+ |---|---|---|
102
+ | `client.workflows` | `WorkflowsResource` | Workflow CRUD, publishing, runs, and templates |
103
+ | `client.versions` | `VersionsResource` | Version management (fork, publish, lock, test) |
104
+ | `client.nodes` | `NodesResource` | Node CRUD, config schema, and available variables |
105
+ | `client.runs` | `RunsResource` | Run details, cancellation, and annotations |
106
+ | `client.sessions` | `SessionsResource` | Session details, messages, and SSE streaming |
107
+ | `client.messages` | `MessagesResource` | Message quality flags |
108
+ | `client.variables` | `VariablesResource` | Workflow-scoped variables |
109
+ | `client.phoneNumbers` | `PhoneNumbersResource` | Phone number management |
110
+ | `client.sipTrunks` | `SipTrunksResource` | SIP trunk management |
111
+ | `client.integrations` | `IntegrationsResource` | Integration and event discovery |
112
+ | `client.contacts` | `ContactsResource` | Contact lookup and history |
113
+ | `client.knowledgeBases` | `KnowledgeBasesResource` | Knowledge base document management |
114
+ | `client.workflowFolders` | `WorkflowFoldersResource` | Workflow folder organization |
115
+ | `client.mcp` | `MCPResource` | MCP server management |
116
+ | `client.usage` | `UsageResource` | Usage metrics (call minutes, tokens, etc.) |
117
+ | `client.billing` | `BillingResource` | Billing usage details and totals |
118
+ | `client.apiKey` | `ApiKeyResource` | API key introspection |
119
+ | `client.adversarialSuites` | `AdversarialSuitesResource` | Adversarial suite management and execution |
120
+ | `client.adversarialTests` | `AdversarialTestsResource` | Adversarial test management and execution |
121
+ | `client.northstars` | `NorthstarsResource` | Northstar quality criteria management |
122
+ | `client.customEvals` | `CustomEvalsResource` | Custom eval management and execution |
123
+ | `client.issues` | `IssuesResource` | Quality issue (flag) status management |
124
+ | `client.auditRemarks` | `AuditRemarksResource` | Audit remark feedback management |
125
+
126
+ ---
127
+
128
+ ## `client.workflows`
129
+
130
+ ```ts
131
+ // List workflows (paginated)
132
+ const { data, pagination } = await client.workflows.list({ page: 1, limit: 20 });
133
+
134
+ // Iterate all workflows across pages
135
+ for await (const wf of client.workflows.listAll()) { ... }
136
+
137
+ // Create a workflow
138
+ const wf = await client.workflows.create({ name: "My Workflow" });
139
+
140
+ // Get by ID or slug
141
+ const wf = await client.workflows.get("my-workflow");
142
+
143
+ // Update
144
+ await client.workflows.update("my-workflow", { name: "New Name" });
145
+
146
+ // Delete
147
+ await client.workflows.delete("my-workflow");
148
+
149
+ // Duplicate
150
+ await client.workflows.duplicate("my-workflow", { name: "Copy" });
151
+
152
+ // Publish / unpublish
153
+ await client.workflows.publish("my-workflow");
154
+ await client.workflows.unpublish("my-workflow");
155
+
156
+ // Cancel all active runs
157
+ await client.workflows.cancelRuns("my-workflow");
158
+
159
+ // List templates
160
+ const { data } = await client.workflows.listTemplates();
161
+
162
+ // List versions
163
+ const { data } = await client.workflows.listVersions("my-workflow");
164
+
165
+ // List runs
166
+ const { data } = await client.workflows.listRuns("my-workflow");
167
+
168
+ // Trigger a run
169
+ const { run_id } = await client.workflows.triggerRun("my-workflow", { payload: { phone: "+1..." } });
170
+ ```
171
+
172
+ | Method | HTTP | Path | Description |
173
+ |---|---|---|---|
174
+ | `list(query?)` | GET | `/workflows` | List workflows, paginated |
175
+ | `listAll(query?)` | GET | `/workflows` | Async generator over all pages |
176
+ | `create(body)` | POST | `/workflows` | Create a new workflow |
177
+ | `get(workflowId)` | GET | `/workflows/:id` | Get workflow by ID or slug |
178
+ | `update(workflowId, body)` | PATCH | `/workflows/:id` | Update name, description, etc. |
179
+ | `delete(workflowId)` | DELETE | `/workflows/:id` | Delete a workflow |
180
+ | `duplicate(workflowId, body?)` | POST | `/workflows/:id/duplicate` | Duplicate a workflow |
181
+ | `publish(workflowId, body?)` | POST | `/workflows/:id/publish` | Publish the latest version |
182
+ | `unpublish(workflowId)` | POST | `/workflows/:id/unpublish` | Unpublish a workflow |
183
+ | `cancelRuns(workflowId)` | POST | `/workflows/:id/cancel-runs` | Cancel all active runs |
184
+ | `listTemplates(query?)` | GET | `/workflows/templates` | List workflow templates |
185
+ | `listVersions(workflowId, query?)` | GET | `/workflows/:id/versions` | List versions for a workflow |
186
+ | `listRuns(workflowId, query?)` | GET | `/workflows/:id/runs` | List runs for a workflow |
187
+ | `triggerRun(workflowId, body?)` | POST | `/workflows/:id/runs` | Trigger a new run |
188
+
189
+ ---
190
+
191
+ ## `client.versions`
192
+
193
+ ```ts
194
+ const version = await client.versions.get("version-id");
195
+ await client.versions.update("version-id", { name: "v2" });
196
+ await client.versions.fork("version-id");
197
+ await client.versions.publish("version-id");
198
+ await client.versions.unpublish("version-id");
199
+ await client.versions.lock("version-id");
200
+ await client.versions.unlock("version-id");
201
+ await client.versions.testAll("version-id");
202
+ const issues = await client.versions.getPromptIssues("version-id");
203
+ ```
204
+
205
+ | Method | HTTP | Path | Description |
206
+ |---|---|---|---|
207
+ | `get(versionId)` | GET | `/versions/:id` | Get version with node summary and changelog |
208
+ | `update(versionId, body)` | PATCH | `/versions/:id` | Update name or description |
209
+ | `fork(versionId)` | POST | `/versions/:id/fork` | Clone a version |
210
+ | `publish(versionId, body?)` | POST | `/versions/:id/publish` | Publish a version |
211
+ | `unpublish(versionId)` | POST | `/versions/:id/unpublish` | Unpublish a version |
212
+ | `lock(versionId)` | POST | `/versions/:id/lock` | Lock a version (prevent edits) |
213
+ | `unlock(versionId)` | POST | `/versions/:id/unlock` | Unlock a version |
214
+ | `testAll(versionId)` | POST | `/versions/:id/test-all` | Run test-all validation |
215
+ | `getPromptIssues(versionId)` | GET | `/versions/:id/prompt-issues` | Get prompt quality issues for all prompt nodes |
216
+
217
+ ---
218
+
219
+ ## `client.nodes`
220
+
221
+ Nodes are always scoped to a version.
222
+
223
+ ```ts
224
+ const nodes = await client.nodes.list("version-id");
225
+ await client.nodes.addBatch("version-id", { nodes: [...] });
226
+ const node = await client.nodes.get("version-id", "node-id");
227
+ await client.nodes.update("version-id", "node-id", { config: { ... } });
228
+ await client.nodes.delete("version-id", "node-id");
229
+ const schema = await client.nodes.getConfigSchema("version-id", "node-id");
230
+ const vars = await client.nodes.getAvailableVars("version-id", "node-id");
231
+ const result = await client.nodes.test("version-id", "node-id");
232
+ ```
233
+
234
+ | Method | HTTP | Path | Description |
235
+ |---|---|---|---|
236
+ | `list(versionId)` | GET | `/versions/:vId/nodes` | List all nodes in a version |
237
+ | `addBatch(versionId, body)` | POST | `/versions/:vId/nodes` | Add nodes in batch (1–50) |
238
+ | `get(versionId, nodeId)` | GET | `/versions/:vId/nodes/:nId` | Get a single node |
239
+ | `update(versionId, nodeId, body)` | PUT | `/versions/:vId/nodes/:nId` | Update a node |
240
+ | `delete(versionId, nodeId)` | DELETE | `/versions/:vId/nodes/:nId` | Delete a node |
241
+ | `getConfigSchema(versionId, nodeId)` | GET | `/versions/:vId/nodes/:nId/config-schema` | Get field types and requirements |
242
+ | `getAvailableVars(versionId, nodeId)` | GET | `/versions/:vId/nodes/:nId/available-vars` | Get upstream variables available to this node |
243
+ | `test(versionId, nodeId, body?)` | POST | `/versions/:vId/nodes/:nId/test` | Test a single node (version must not be published) |
244
+
245
+ ---
246
+
247
+ ## `client.runs`
248
+
249
+ ```ts
250
+ const run = await client.runs.get("run-id");
251
+ const sessions = await client.runs.getSessions("run-id");
252
+ const recordings = await client.runs.getRecordings("run-id");
253
+ const flags = await client.runs.getFlags("run-id");
254
+ await client.runs.cancel("run-id");
255
+ await client.runs.mark("run-id", { annotation: "correct" });
256
+ ```
257
+
258
+ | Method | HTTP | Path | Description |
259
+ |---|---|---|---|
260
+ | `get(runId)` | GET | `/runs/:id` | Get run details |
261
+ | `getSessions(runId, query?)` | GET | `/runs/:id/sessions` | Get sessions for a run |
262
+ | `getRecordings(runId)` | GET | `/runs/:id/recordings` | Get recordings for a run |
263
+ | `getFlags(runId, query?)` | GET | `/runs/:id/flags` | Get quality flags for a run |
264
+ | `cancel(runId)` | POST | `/runs/:id/cancel` | Cancel a run |
265
+ | `mark(runId, body)` | POST | `/runs/:id/mark` | Annotate a run (correct / incorrect / critical) |
266
+
267
+ ---
268
+
269
+ ## `client.sessions`
270
+
271
+ ```ts
272
+ const session = await client.sessions.get("session-id");
273
+ const { data } = await client.sessions.getMessages("session-id");
274
+
275
+ // SSE streaming
276
+ for await (const event of await client.sessions.stream("session-id")) {
277
+ if (event.event === "message") console.log(event.data.role, event.data.content);
278
+ if (event.event === "session_ended") break;
279
+ }
280
+ ```
281
+
282
+ | Method | HTTP | Path | Description |
283
+ |---|---|---|---|
284
+ | `get(sessionId)` | GET | `/sessions/:id` | Get session details |
285
+ | `getMessages(sessionId, query?)` | GET | `/sessions/:id/messages` | Get paginated messages for a session |
286
+ | `stream(sessionId, query?)` | GET | `/sessions/:id/stream` | Open SSE stream of session messages |
287
+
288
+ ---
289
+
290
+ ## `client.messages`
291
+
292
+ ```ts
293
+ const { data } = await client.messages.listFlags("message-id");
294
+ await client.messages.createFlag("message-id", { type: "incorrect", note: "..." });
295
+ ```
296
+
297
+ | Method | HTTP | Path | Description |
298
+ |---|---|---|---|
299
+ | `listFlags(messageId, query?)` | GET | `/messages/:id/flags` | List quality flags for a message |
300
+ | `createFlag(messageId, body)` | POST | `/messages/:id/flags` | Create a quality flag on a message |
301
+
302
+ ---
303
+
304
+ ## `client.variables`
305
+
306
+ Variables are scoped to a workflow.
307
+
308
+ ```ts
309
+ const { data } = await client.variables.list("workflow-id");
310
+ await client.variables.create("workflow-id", { name: "MY_VAR", value: "hello" });
311
+ await client.variables.update("workflow-id", "variable-id", { value: "world" });
312
+ await client.variables.delete("workflow-id", "variable-id");
313
+ ```
314
+
315
+ | Method | HTTP | Path | Description |
316
+ |---|---|---|---|
317
+ | `list(workflowId, query?)` | GET | `/workflows/:wId/variables` | List variables for a workflow |
318
+ | `create(workflowId, body)` | POST | `/workflows/:wId/variables` | Create a variable |
319
+ | `update(workflowId, variableId, body)` | PATCH | `/workflows/:wId/variables/:vId` | Update a variable |
320
+ | `delete(workflowId, variableId)` | DELETE | `/workflows/:wId/variables/:vId` | Delete a variable |
321
+
322
+ ---
323
+
324
+ ## `client.phoneNumbers`
325
+
326
+ ```ts
327
+ const numbers = await client.phoneNumbers.list();
328
+ await client.phoneNumbers.buy({ area_code: "415" });
329
+ await client.phoneNumbers.update("id", { name: "Main Line" });
330
+ await client.phoneNumbers.getUsage("id");
331
+ await client.phoneNumbers.freeUp("id");
332
+ await client.phoneNumbers.delete("id");
333
+ await client.phoneNumbers.removeFromWorkflow("id", { use_case_id: "..." });
334
+ await client.phoneNumbers.getTollFreeVerification("id");
335
+ await client.phoneNumbers.createTollFreeVerification("id", { ... });
336
+ await client.phoneNumbers.deleteTollFreeVerification("verification-sid");
337
+ await client.phoneNumbers.createSipTrunk("id");
338
+ await client.phoneNumbers.validateTollFreeNumbers({ phone_numbers: [...] });
339
+ ```
340
+
341
+ | Method | HTTP | Path | Description |
342
+ |---|---|---|---|
343
+ | `list(query?)` | GET | `/phone-numbers` | List all phone numbers |
344
+ | `buy(body)` | POST | `/phone-numbers` | Purchase a new phone number |
345
+ | `update(id, body)` | PUT | `/phone-numbers/:id` | Update name or caller ID |
346
+ | `getUsage(id)` | GET | `/phone-numbers/:id/usage` | Get usage info |
347
+ | `getTollFreeVerification(id)` | GET | `/phone-numbers/:id/tollfree-verification` | Get toll-free verification status |
348
+ | `createTollFreeVerification(id, body)` | POST | `/phone-numbers/:id/tollfree-verification` | Start toll-free verification |
349
+ | `deleteTollFreeVerification(verificationSid)` | DELETE | `/phone-numbers/tollfree-verification/:sid` | Delete a toll-free verification |
350
+ | `createSipTrunk(id)` | POST | `/phone-numbers/:id/sip-trunk` | Create a SIP trunk for a number |
351
+ | `freeUp(id)` | POST | `/phone-numbers/:id/free-up-number` | Release a number from workflows |
352
+ | `delete(id)` | POST | `/phone-numbers/:id/delete-number` | Permanently delete a number |
353
+ | `removeFromWorkflow(id, body)` | POST | `/phone-numbers/:id/remove-from-workflow` | Remove from a specific workflow |
354
+ | `validateTollFreeNumbers(body)` | POST | `/phone-numbers/validate-toll-free-numbers` | Check for conflicts |
355
+
356
+ ---
357
+
358
+ ## `client.sipTrunks`
359
+
360
+ ```ts
361
+ const trunks = await client.sipTrunks.list();
362
+ const options = await client.sipTrunks.listOptions();
363
+ await client.sipTrunks.create({ name: "Main Trunk", ... });
364
+ await client.sipTrunks.createBulk({ trunks: [...] });
365
+ const trunk = await client.sipTrunks.get("trunk-id");
366
+ await client.sipTrunks.update("trunk-id", { name: "Updated" });
367
+ await client.sipTrunks.delete("trunk-id");
368
+ ```
369
+
370
+ | Method | HTTP | Path | Description |
371
+ |---|---|---|---|
372
+ | `list()` | GET | `/sip-trunks` | List all SIP trunks |
373
+ | `listOptions()` | GET | `/sip-trunks/options` | Lightweight list of trunk options |
374
+ | `create(body)` | POST | `/sip-trunks` | Create a SIP trunk |
375
+ | `createBulk(body)` | POST | `/sip-trunks/bulk` | Create multiple SIP trunks |
376
+ | `get(trunkId)` | GET | `/sip-trunks/:id` | Get a SIP trunk |
377
+ | `update(trunkId, body)` | PUT | `/sip-trunks/:id` | Update a SIP trunk |
378
+ | `delete(trunkId)` | DELETE | `/sip-trunks/:id` | Delete a SIP trunk |
379
+
380
+ ---
381
+
382
+ ## `client.integrations`
383
+
384
+ ```ts
385
+ const integrations = await client.integrations.list();
386
+ const integration = await client.integrations.get("integration-id");
387
+ await client.integrations.createCredential("integration-id", { name: "My Creds", fields: { ... } });
388
+
389
+ // Integration-specific sub-resources
390
+ await client.integrations.googleSheets.spreadsheets();
391
+ await client.integrations.googleSheets.worksheets({ spreadsheet_id: "..." });
392
+ await client.integrations.googleSheets.columns({ spreadsheet_id: "...", worksheet_id: "..." });
393
+ await client.integrations.googleSheets.rows({ spreadsheet_id: "...", worksheet_id: "..." });
394
+
395
+ await client.integrations.slack.channels();
396
+ await client.integrations.slack.users();
397
+
398
+ await client.integrations.teams.teams();
399
+ await client.integrations.teams.channels();
400
+ await client.integrations.teams.users();
401
+
402
+ await client.integrations.twilioSms.phoneNumbers();
403
+
404
+ await client.integrations.whatsApp.businesses({ credential_id: "..." });
405
+ await client.integrations.whatsApp.businessAccounts({ credential_id: "...", business_id: "..." });
406
+ await client.integrations.whatsApp.phoneNumbers({ credential_id: "...", business_account_id: "..." });
407
+ await client.integrations.whatsApp.messageTemplates({ credential_id: "...", business_account_id: "..." });
408
+ ```
409
+
410
+ | Method | HTTP | Path | Description |
411
+ |---|---|---|---|
412
+ | `list(query?)` | GET | `/integrations` | List all integrations |
413
+ | `get(id)` | GET | `/integrations/:id` | Get integration and its events |
414
+ | `createCredential(id, body)` | POST | `/integrations/:id/create-credential` | Create a credential for a form-based integration |
415
+ | `googleSheets.spreadsheets(query?)` | GET | `/integrations/google-sheets/spreadsheets` | List spreadsheets |
416
+ | `googleSheets.worksheets(query)` | GET | `/integrations/google-sheets/worksheets` | List worksheets in a spreadsheet |
417
+ | `googleSheets.columns(query)` | GET | `/integrations/google-sheets/columns` | List columns in a worksheet |
418
+ | `googleSheets.rows(query)` | GET | `/integrations/google-sheets/rows` | List rows in a worksheet |
419
+ | `slack.channels(query?)` | GET | `/integrations/slack/channels` | List Slack channels |
420
+ | `slack.users(query?)` | GET | `/integrations/slack/users` | List Slack users |
421
+ | `teams.teams(query?)` | GET | `/integrations/teams/teams` | List Microsoft Teams teams |
422
+ | `teams.channels(query?)` | GET | `/integrations/teams/channels` | List Microsoft Teams channels |
423
+ | `teams.users(query?)` | GET | `/integrations/teams/users` | List Microsoft Teams users |
424
+ | `twilioSms.phoneNumbers(query?)` | GET | `/integrations/twilio-sms/phone-numbers` | List Twilio SMS numbers |
425
+ | `whatsApp.businesses(query)` | GET | `/integrations/whatsapp/businesses` | List WhatsApp businesses |
426
+ | `whatsApp.businessAccounts(query)` | GET | `/integrations/whatsapp/business-accounts` | List WhatsApp business accounts |
427
+ | `whatsApp.phoneNumbers(query)` | GET | `/integrations/whatsapp/phone-numbers` | List WhatsApp phone numbers |
428
+ | `whatsApp.messageTemplates(query)` | GET | `/integrations/whatsapp/message-templates` | List WhatsApp message templates |
429
+
430
+ ---
431
+
432
+ ## `client.contacts`
433
+
434
+ ```ts
435
+ const { data } = await client.contacts.list({ search: "John" });
436
+ const contact = await client.contacts.resolve({ phone_number: "+14155551234" });
437
+ const contact = await client.contacts.get("contact-id");
438
+ const interactions = await client.contacts.getInteractions("contact-id");
439
+ const memories = await client.contacts.getMemories("contact-id");
440
+ ```
441
+
442
+ | Method | HTTP | Path | Description |
443
+ |---|---|---|---|
444
+ | `list(query?)` | GET | `/contacts` | List contacts (cursor-paginated) |
445
+ | `resolve(query)` | GET | `/contacts/resolve` | Look up a contact by phone number or email |
446
+ | `get(contactId)` | GET | `/contacts/:id` | Get contact by ID |
447
+ | `getInteractions(contactId)` | GET | `/contacts/:id/interactions` | Get call/message history for a contact |
448
+ | `getMemories(contactId)` | GET | `/contacts/:id/memories` | Get AI memories for a contact |
449
+
450
+ ---
451
+
452
+ ## `client.knowledgeBases`
453
+
454
+ ```ts
455
+ const kbs = await client.knowledgeBases.list();
456
+ const files = await client.knowledgeBases.listFiles("kb-id");
457
+ const urls = await client.knowledgeBases.getUploadUrls("kb-id", { files: [{ name: "doc.pdf", content_type: "application/pdf" }] });
458
+ await client.knowledgeBases.triggerChunking("kb-id");
459
+ await client.knowledgeBases.deleteFile("kb-id", "file-id");
460
+ await client.knowledgeBases.delete("kb-id");
461
+ ```
462
+
463
+ | Method | HTTP | Path | Description |
464
+ |---|---|---|---|
465
+ | `list()` | GET | `/knowledge-bases` | List all knowledge bases |
466
+ | `listFiles(kbId)` | GET | `/knowledge-bases/:id/files` | List documents in a knowledge base |
467
+ | `getUploadUrls(kbId, body)` | POST | `/knowledge-bases/:id/upload-urls` | Get pre-signed S3 upload URLs |
468
+ | `triggerChunking(kbId)` | POST | `/knowledge-bases/:id/trigger-chunking` | Start document processing/chunking |
469
+ | `deleteFile(kbId, fileId)` | DELETE | `/knowledge-bases/:id/files/:fileId` | Delete a file from a knowledge base |
470
+ | `delete(kbId)` | DELETE | `/knowledge-bases/:id` | Delete a knowledge base |
471
+
472
+ ---
473
+
474
+ ## `client.workflowFolders`
475
+
476
+ ```ts
477
+ const { data } = await client.workflowFolders.list();
478
+ await client.workflowFolders.create({ name: "Production" });
479
+ const folder = await client.workflowFolders.get("folder-id");
480
+ await client.workflowFolders.update("folder-id", { name: "Updated" });
481
+ await client.workflowFolders.delete("folder-id");
482
+ ```
483
+
484
+ | Method | HTTP | Path | Description |
485
+ |---|---|---|---|
486
+ | `list(query?)` | GET | `/workflow-folders` | List folders (paginated) |
487
+ | `create(body)` | POST | `/workflow-folders` | Create a folder |
488
+ | `get(folderId)` | GET | `/workflow-folders/:id` | Get a folder |
489
+ | `update(folderId, body)` | PUT | `/workflow-folders/:id` | Update a folder |
490
+ | `delete(folderId)` | DELETE | `/workflow-folders/:id` | Delete a folder |
491
+
492
+ ---
493
+
494
+ ## `client.mcp`
495
+
496
+ ```ts
497
+ const { data } = await client.mcp.list();
498
+ await client.mcp.create({ name: "My MCP", url: "https://..." });
499
+ await client.mcp.refresh("mcp-id");
500
+ ```
501
+
502
+ | Method | HTTP | Path | Description |
503
+ |---|---|---|---|
504
+ | `list(query?)` | GET | `/mcp` | List MCP servers (paginated) |
505
+ | `create(body)` | POST | `/mcp` | Register a new MCP server |
506
+ | `refresh(mcpId)` | POST | `/mcp/:id/refresh` | Re-discover tools for an MCP server |
507
+
508
+ ---
509
+
510
+ ## `client.usage`
511
+
512
+ All methods accept a `UsageQuery` with `workflow_id`, `start_date`, and `end_date`.
513
+
514
+ ```ts
515
+ const data = await client.usage.callMinutes({ workflow_id: "wf-id", start_date: "2024-01-01", end_date: "2024-01-31" });
516
+ await client.usage.llmTokens({ ... });
517
+ await client.usage.textCount({ ... });
518
+ await client.usage.ocrCount({ ... });
519
+ await client.usage.rpaMinutes({ ... });
520
+ ```
521
+
522
+ | Method | HTTP | Path | Description |
523
+ |---|---|---|---|
524
+ | `callMinutes(query)` | GET | `/usage/call-minutes` | Voice call minutes consumed |
525
+ | `llmTokens(query)` | GET | `/usage/llm-tokens` | LLM tokens consumed |
526
+ | `textCount(query)` | GET | `/usage/text-count` | Text messages sent |
527
+ | `ocrCount(query)` | GET | `/usage/ocr-count` | OCR operations performed |
528
+ | `rpaMinutes(query)` | GET | `/usage/rpa-minutes` | RPA automation minutes consumed |
529
+
530
+ ---
531
+
532
+ ## `client.billing`
533
+
534
+ ```ts
535
+ const details = await client.billing.getDetails({ start: "2024-01-01", end: "2024-01-31" });
536
+ const totals = await client.billing.getTotals({ start: "2024-01-01", end: "2024-01-31" });
537
+ ```
538
+
539
+ | Method | HTTP | Path | Description |
540
+ |---|---|---|---|
541
+ | `getDetails(query)` | GET | `/billing/usage/details` | Detailed billing line items |
542
+ | `getTotals(query)` | GET | `/billing/usage/totals` | Aggregated billing totals |
543
+
544
+ ---
545
+
546
+ ## `client.apiKey`
547
+
548
+ ```ts
549
+ const info = await client.apiKey.describe();
550
+ ```
551
+
552
+ | Method | HTTP | Path | Description |
553
+ |---|---|---|---|
554
+ | `describe()` | GET | `/api-key/describe` | Introspect the current API key (ID, name, org, etc.) |
555
+
556
+ ---
557
+
558
+ ## `client.adversarialSuites`
559
+
560
+ Adversarial suites group multiple adversarial tests and can auto-generate them from a prompt.
561
+
562
+ ```ts
563
+ const { suite } = await client.adversarialSuites.get("suite-id");
564
+ await client.adversarialSuites.update("suite-id", { name: "New Name", generation_count: 10 });
565
+ await client.adversarialSuites.delete("suite-id");
566
+
567
+ // Generate tests from the suite's generation_prompt using AI
568
+ await client.adversarialSuites.generate("suite-id", { version_id: "v-id" });
569
+
570
+ // Generate a mermaid workflow graph for visualization
571
+ await client.adversarialSuites.generateGraph("suite-id", { version_id: "v-id" });
572
+
573
+ // Run all tests in the suite
574
+ const { suite_run_id } = await client.adversarialSuites.run("suite-id", { version_id: "v-id" });
575
+
576
+ // Poll results
577
+ const { runs } = await client.adversarialSuites.listRuns("suite-id");
578
+ const { run } = await client.adversarialSuites.getRun("suite-run-id");
579
+ const { test_runs } = await client.adversarialSuites.getRunTestRuns("suite-run-id");
580
+ ```
581
+
582
+ | Method | HTTP | Path | Description |
583
+ |---|---|---|---|
584
+ | `get(suiteId)` | GET | `/adversarial-suites/:id` | Get suite with optional mermaid graph |
585
+ | `update(suiteId, body)` | PATCH | `/adversarial-suites/:id` | Update name, model, timeout, generation settings |
586
+ | `delete(suiteId)` | DELETE | `/adversarial-suites/:id` | Delete a suite |
587
+ | `generate(suiteId, body?)` | POST | `/adversarial-suites/:id/generate` | AI-generate tests from the suite's generation prompt |
588
+ | `generateGraph(suiteId, body)` | POST | `/adversarial-suites/:id/generate-graph` | Generate a mermaid workflow visualization |
589
+ | `run(suiteId, body)` | POST | `/adversarial-suites/:id/run` | Execute all tests asynchronously, returns `suite_run_id` |
590
+ | `listRuns(suiteId, query?)` | GET | `/adversarial-suites/:id/runs` | List suite runs |
591
+ | `getRun(suiteRunId)` | GET | `/adversarial-suites/runs/:runId` | Get a suite run with aggregate pass/fail counts |
592
+ | `getRunTestRuns(suiteRunId)` | GET | `/adversarial-suites/runs/:runId/test-runs` | List individual test results within a suite run |
593
+
594
+ ---
595
+
596
+ ## `client.adversarialTests`
597
+
598
+ Individual adversarial tests simulate a user trying to break the agent's behavior.
599
+
600
+ ```ts
601
+ const { test } = await client.adversarialTests.get("test-id");
602
+ await client.adversarialTests.update("test-id", { adversarial_prompt: "Try to get PII", timeout_seconds: 120 });
603
+ await client.adversarialTests.delete("test-id");
604
+
605
+ // Run a single test
606
+ const { test_run_id } = await client.adversarialTests.run("test-id", { version_id: "v-id" });
607
+
608
+ // Poll results
609
+ const { runs } = await client.adversarialTests.listRuns("test-id");
610
+ const { run } = await client.adversarialTests.getRun("run-id");
611
+ const { messages } = await client.adversarialTests.getRunMessages("run-id");
612
+ ```
613
+
614
+ | Method | HTTP | Path | Description |
615
+ |---|---|---|---|
616
+ | `get(testId)` | GET | `/adversarial-tests/:id` | Get test details |
617
+ | `update(testId, body)` | PATCH | `/adversarial-tests/:id` | Update prompt, model, variables, timeout |
618
+ | `delete(testId)` | DELETE | `/adversarial-tests/:id` | Delete a test |
619
+ | `run(testId, body)` | POST | `/adversarial-tests/:id/run` | Execute a test asynchronously, returns `test_run_id` |
620
+ | `listRuns(testId, query?)` | GET | `/adversarial-tests/:id/runs` | List test runs with audit remarks |
621
+ | `getRun(runId)` | GET | `/adversarial-tests/runs/:runId` | Get a single run with full audit remarks |
622
+ | `getRunMessages(runId)` | GET | `/adversarial-tests/runs/:runId/messages` | Get the full conversation from a test run |
623
+
624
+ ---
625
+
626
+ ## `client.northstars`
627
+
628
+ Northstars are quality criteria that define how the agent should behave. They are used to automatically grade conversations.
629
+
630
+ ```ts
631
+ const { northstar } = await client.northstars.get("northstar-id");
632
+ await client.northstars.update("northstar-id", { enabled: true, priority: "high" });
633
+ await client.northstars.delete("northstar-id");
634
+
635
+ // Get full regeneration history
636
+ const { history } = await client.northstars.getHistory("northstar-id");
637
+
638
+ // Rate a northstar (-2 = strongly wrong, +2 = strongly correct)
639
+ const { feedback } = await client.northstars.submitFeedback("northstar-id", {
640
+ correctness: 2,
641
+ feedback: "This criterion is perfect.",
642
+ trigger_regeneration: false,
643
+ });
644
+
645
+ await client.northstars.deleteFeedback("northstar-id");
646
+ ```
647
+
648
+ | Method | HTTP | Path | Description |
649
+ |---|---|---|---|
650
+ | `get(northstarId)` | GET | `/northstars/:id` | Get a northstar |
651
+ | `update(northstarId, body)` | PATCH | `/northstars/:id` | Update name, description, examples, category, priority, or enabled state |
652
+ | `delete(northstarId)` | DELETE | `/northstars/:id` | Delete a northstar |
653
+ | `getHistory(northstarId)` | GET | `/northstars/:id/history` | Get the full regeneration chain, oldest first |
654
+ | `submitFeedback(northstarId, body)` | POST | `/northstars/:id/feedback` | Rate a northstar's correctness (−2 to +2), optionally trigger regeneration |
655
+ | `deleteFeedback(northstarId)` | DELETE | `/northstars/:id/feedback` | Remove your feedback on a northstar |
656
+
657
+ ---
658
+
659
+ ## `client.customEvals`
660
+
661
+ Custom evals test a specific prompt node against expected outputs or northstar criteria.
662
+
663
+ ```ts
664
+ const { test } = await client.customEvals.get("eval-id");
665
+ await client.customEvals.update("eval-id", {
666
+ name: "Greeting check",
667
+ expected_response: "Hello, how can I help you?",
668
+ });
669
+ await client.customEvals.delete("eval-id");
670
+
671
+ // Run an eval
672
+ const { run_id } = await client.customEvals.run("eval-id", { version_id: "v-id" });
673
+
674
+ // Poll results
675
+ const { runs } = await client.customEvals.listRuns("eval-id");
676
+ ```
677
+
678
+ | Method | HTTP | Path | Description |
679
+ |---|---|---|---|
680
+ | `get(evalId)` | GET | `/custom-evals/:id` | Get a custom eval |
681
+ | `update(evalId, body)` | PATCH | `/custom-evals/:id` | Update messages, expected outputs, variables, or northstar IDs |
682
+ | `delete(evalId)` | DELETE | `/custom-evals/:id` | Delete a custom eval |
683
+ | `run(evalId, body)` | POST | `/custom-evals/:id/run` | Execute the eval asynchronously, returns `run_id` |
684
+ | `listRuns(evalId, query?)` | GET | `/custom-evals/:id/runs` | List runs with pass/fail and judge reasoning |
685
+
686
+ ---
687
+
688
+ ## `client.issues`
689
+
690
+ Issues (quality flags) are raised when a conversation fails a northstar or quality check.
691
+
692
+ ```ts
693
+ await client.issues.update("issue-id", { status: "approved" });
694
+ ```
695
+
696
+ | Method | HTTP | Path | Description |
697
+ |---|---|---|---|
698
+ | `update(issueId, body)` | PATCH | `/issues/:id` | Update issue status (`open` / `approved` / `rejected` / `closed`) |
699
+
700
+ ---
701
+
702
+ ## `client.auditRemarks`
703
+
704
+ Audit remarks are individual northstar grades attached to a conversation. Feedback on audit remarks improves future grading.
705
+
706
+ ```ts
707
+ const feedback = await client.auditRemarks.getFeedback("audit-remark-id");
708
+
709
+ // Thumbs up — automatically adds the remark as a northstar positive example
710
+ await client.auditRemarks.submitFeedback("audit-remark-id", {
711
+ polarity: true,
712
+ });
713
+
714
+ // Thumbs down with attribution
715
+ await client.auditRemarks.submitFeedback("audit-remark-id", {
716
+ polarity: false,
717
+ issue_attribution: "northstar",
718
+ comment: "The northstar criteria was wrong here.",
719
+ });
720
+
721
+ await client.auditRemarks.deleteFeedback("audit-remark-id");
722
+ ```
723
+
724
+ | Method | HTTP | Path | Description |
725
+ |---|---|---|---|
726
+ | `getFeedback(auditRemarkId)` | GET | `/audit-remarks/:id/feedback` | Get your feedback for this remark, or `null` |
727
+ | `submitFeedback(auditRemarkId, body)` | POST | `/audit-remarks/:id/feedback` | Submit thumbs up/down; a thumbs-up adds the remark as a northstar example |
728
+ | `deleteFeedback(auditRemarkId)` | DELETE | `/audit-remarks/:id/feedback` | Delete your feedback |
729
+
730
+ ---
731
+
732
+ ## Pagination
733
+
734
+ List methods that are paginated return a `PaginatedResponse<T>`:
735
+
736
+ ```ts
737
+ interface PaginatedResponse<T> {
738
+ data: T[];
739
+ pagination: {
740
+ page: number;
741
+ page_size: number;
742
+ total_pages: number;
743
+ total_records: number;
744
+ has_next_page: boolean;
745
+ has_previous_page: boolean;
746
+ };
747
+ }
748
+ ```
749
+
750
+ For resources that support `listAll()`, use the async generator to iterate all pages automatically:
751
+
752
+ ```ts
753
+ for await (const workflow of client.workflows.listAll({ folder_id: "..." })) {
754
+ console.log(workflow.name);
755
+ }
756
+ ```
757
+
758
+ ## Error Handling
759
+
760
+ ```ts
761
+ import { ApiError, AuthenticationError, NotFoundError } from "@happyrobot/sdk";
762
+
763
+ try {
764
+ const wf = await client.workflows.get("nonexistent");
765
+ } catch (err) {
766
+ if (err instanceof NotFoundError) {
767
+ console.log("Workflow not found");
768
+ } else if (err instanceof AuthenticationError) {
769
+ console.log("Invalid API key");
770
+ } else if (err instanceof ApiError) {
771
+ console.log(err.status, err.message);
772
+ }
773
+ }
774
+ ```
package/core/http.js CHANGED
@@ -21,7 +21,11 @@ class HttpClient {
21
21
  throw new Error("apiKey is required");
22
22
  }
23
23
  this.apiKey = config.apiKey;
24
- this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
24
+ const customBaseUrl = config.baseUrl;
25
+ if (customBaseUrl && !config.apiKey.startsWith("sk_test_")) {
26
+ throw new Error("baseUrl can only be overridden with a test API key (sk_test_...)");
27
+ }
28
+ this.baseUrl = (customBaseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
25
29
  this.timeout = config.timeout ?? DEFAULT_TIMEOUT;
26
30
  this.maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;
27
31
  this.fetchFn = config.fetch ?? globalThis.fetch.bind(globalThis);
package/core/types.d.ts CHANGED
@@ -4,8 +4,6 @@
4
4
  export interface ClientConfig {
5
5
  /** API key (sk_live_... or sk_test_...). Required. */
6
6
  apiKey: string;
7
- /** Base URL of the API. Defaults to "https://platform.happyrobot.ai/api/v2". */
8
- baseUrl?: string;
9
7
  /** Request timeout in milliseconds. Defaults to 30000. */
10
8
  timeout?: number;
11
9
  /** Max retries on 429/5xx. Defaults to 2. Uses exponential backoff. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@happyrobot-ai/sdk",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "TypeScript SDK for the HappyRobot Public API",
5
5
  "main": "./index.js",
6
6
  "module": "./index.mjs",
@@ -21,7 +21,8 @@
21
21
  "**/*.js",
22
22
  "**/*.mjs",
23
23
  "**/*.d.ts",
24
- "**/*.d.ts.map"
24
+ "**/*.d.ts.map",
25
+ "README.md"
25
26
  ],
26
27
  "engines": {
27
28
  "node": ">=18"