robotrock 0.6.0 → 0.8.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/dist/index.js CHANGED
@@ -1,47 +1,533 @@
1
+ // src/schemas/index.ts
2
+ import { z } from "zod";
1
3
  import {
2
- RobotRock,
3
- RobotRockError,
4
- TaskExpiredError,
5
- TaskTimeoutError,
6
- attachWebhookToActions,
7
- createClient,
8
- resolveRobotRockClient,
9
- resolveRobotRockConfig,
10
- toDiscriminatedApprovalResult
11
- } from "./chunk-ZCMXX7IG.js";
12
- import {
13
- DEFAULT_TASK_PRIORITY,
14
- DEFAULT_THREAD_UPDATE_STATUS,
15
- LOWEST_TASK_PRIORITY,
16
- TASK_PRIORITY_RANK,
17
- assignToSchema,
18
- createTaskBodySchema,
19
- taskContextSchema,
20
- taskPriorities,
21
- taskPrioritySchema,
22
- threadUpdateBodySchema,
23
- threadUpdateInputSchema,
24
- threadUpdateStatusSchema,
25
- threadUpdateStatuses
26
- } from "./chunk-TAWMIQAM.js";
4
+ isPublicHttpUrl,
5
+ PUBLIC_HTTP_URL_ERROR
6
+ } from "@robotrock/core/utils";
7
+ import { validateContextPublicUrls } from "@robotrock/core/schemas";
8
+ var safeUrlSchema = z.string().refine((url) => isPublicHttpUrl(url), {
9
+ message: PUBLIC_HTTP_URL_ERROR
10
+ });
11
+ var jsonSchema7Schema = z.custom(
12
+ (val) => typeof val === "object" && val !== null,
13
+ { message: "Must be a valid JSON Schema object" }
14
+ );
15
+ var uiSchemaSchema = z.custom((val) => typeof val === "object" && val !== null, {
16
+ message: "Must be a valid UiSchema object"
17
+ });
18
+ var webhookHandlerSchema = z.object({
19
+ type: z.literal("webhook"),
20
+ url: safeUrlSchema,
21
+ headers: z.record(z.string(), z.string())
22
+ });
23
+ var triggerHandlerSchema = webhookHandlerSchema.extend({
24
+ type: z.literal("trigger"),
25
+ tokenId: z.string().min(1)
26
+ });
27
+ var handlerSchema = z.discriminatedUnion("type", [webhookHandlerSchema, triggerHandlerSchema]);
28
+ var taskActionSchema = z.object({
29
+ id: z.string().min(1),
30
+ title: z.string().min(1),
31
+ description: z.string().optional(),
32
+ schema: jsonSchema7Schema.optional(),
33
+ ui: uiSchemaSchema.optional(),
34
+ data: z.record(z.string(), z.unknown()).optional(),
35
+ handlers: z.array(handlerSchema).min(1).optional()
36
+ });
37
+ var uiFieldSchemaSchema = z.object({
38
+ "ui:widget": z.string().optional(),
39
+ "ui:title": z.string().optional(),
40
+ "ui:description": z.string().optional(),
41
+ "ui:options": z.record(z.string(), z.unknown()).optional(),
42
+ items: z.lazy(() => z.record(z.string(), uiFieldSchemaSchema)).optional()
43
+ }).passthrough();
44
+ var contextUiSchema = z.record(z.string(), uiFieldSchemaSchema).optional();
45
+ var contextDataSchema = z.object({
46
+ data: z.record(z.string(), z.unknown()),
47
+ ui: contextUiSchema
48
+ }).optional();
49
+ var taskContextObjectSchema = z.object({
50
+ app: z.string().min(1).optional(),
51
+ type: z.string().min(1),
52
+ name: z.string().min(1),
53
+ description: z.string().optional(),
54
+ validUntil: z.string().optional(),
55
+ context: contextDataSchema,
56
+ version: z.literal(2).optional(),
57
+ actions: z.array(taskActionSchema).min(1, "At least one action is required")
58
+ });
59
+ function refineContextPublicUrls(data, ctx) {
60
+ const error = validateContextPublicUrls(data.context);
61
+ if (error) {
62
+ ctx.addIssue({
63
+ code: z.ZodIssueCode.custom,
64
+ message: error,
65
+ path: ["context"]
66
+ });
67
+ }
68
+ }
69
+ var taskContextSchema = taskContextObjectSchema.superRefine(
70
+ refineContextPublicUrls
71
+ );
72
+ var assignToSchema = z.object({
73
+ users: z.array(z.string().email()).optional(),
74
+ groups: z.array(z.string().min(1)).optional()
75
+ }).refine(
76
+ (data) => {
77
+ const groups = data.groups ?? [];
78
+ if (groups.includes("all") && groups.length > 1) {
79
+ return false;
80
+ }
81
+ return true;
82
+ },
83
+ { message: 'Cannot combine "all" with other group slugs' }
84
+ );
85
+ var threadUpdateMessageSchema = z.string().min(1).max(500);
86
+ var threadUpdateStatuses = [
87
+ "info",
88
+ "queued",
89
+ "running",
90
+ "waiting",
91
+ "succeeded",
92
+ "failed",
93
+ "cancelled"
94
+ ];
95
+ var threadUpdateStatusSchema = z.enum(threadUpdateStatuses);
96
+ var DEFAULT_THREAD_UPDATE_STATUS = "info";
97
+ var threadUpdateInputSchema = z.object({
98
+ message: threadUpdateMessageSchema,
99
+ status: threadUpdateStatusSchema.optional()
100
+ });
101
+ var taskPriorities = ["low", "normal", "high", "urgent"];
102
+ var taskPrioritySchema = z.enum(taskPriorities);
103
+ var DEFAULT_TASK_PRIORITY = "normal";
104
+ var LOWEST_TASK_PRIORITY = "low";
105
+ var TASK_PRIORITY_RANK = {
106
+ low: 1,
107
+ normal: 2,
108
+ high: 3,
109
+ urgent: 4
110
+ };
111
+ var nonNegativeInt = z.number().int().nonnegative();
112
+ var agentCostTokensSchema = z.object({
113
+ input: nonNegativeInt.optional(),
114
+ output: nonNegativeInt.optional(),
115
+ total: nonNegativeInt.optional()
116
+ });
117
+ var agentCostSchema = z.object({
118
+ tokens: agentCostTokensSchema.optional(),
119
+ eur: z.number().nonnegative().optional(),
120
+ usd: z.number().nonnegative().optional()
121
+ });
122
+ var AGENT_INFO_MAX_KEYS = 32;
123
+ var agentTelemetrySchema = z.object({
124
+ version: z.string().min(1).optional(),
125
+ toolCallCount: nonNegativeInt.optional(),
126
+ cost: agentCostSchema.optional(),
127
+ info: z.record(z.string(), z.unknown()).optional()
128
+ }).refine(
129
+ (data) => {
130
+ const keys = data.info ? Object.keys(data.info) : [];
131
+ return keys.length <= AGENT_INFO_MAX_KEYS;
132
+ },
133
+ { message: `agent.info may contain at most ${AGENT_INFO_MAX_KEYS} keys` }
134
+ );
135
+ var createTaskBodySchema = taskContextObjectSchema.extend({
136
+ assignTo: assignToSchema.optional(),
137
+ /**
138
+ * Groups related tasks together. When omitted, the server generates one and
139
+ * returns it so the caller can reuse it on later tasks in the same thread.
140
+ */
141
+ threadId: z.string().min(1).optional(),
142
+ /**
143
+ * Optional thread priority. When set, applies to the whole thread and
144
+ * overwrites any previous priority. Omit on later tasks to leave unchanged.
145
+ */
146
+ priority: taskPrioritySchema.optional(),
147
+ /**
148
+ * Optional initial status update logged against the task's thread. Shows in
149
+ * the inbox status bar and the thread update log.
150
+ */
151
+ update: threadUpdateInputSchema.optional(),
152
+ agent: agentTelemetrySchema.optional()
153
+ }).superRefine(refineContextPublicUrls);
154
+ var threadUpdateBodySchema = threadUpdateInputSchema;
155
+
156
+ // src/approval-result.ts
157
+ var TaskTimeoutError = class extends Error {
158
+ constructor(message) {
159
+ super(message);
160
+ this.name = "TaskTimeoutError";
161
+ }
162
+ };
163
+ var TaskExpiredError = class extends Error {
164
+ constructor(message) {
165
+ super(message);
166
+ this.name = "TaskExpiredError";
167
+ }
168
+ };
169
+ function toDiscriminatedApprovalResult(actions, task) {
170
+ void actions;
171
+ if (!task.handled) {
172
+ throw new Error("Task has no handled result");
173
+ }
174
+ return {
175
+ actionId: task.handled.action.id,
176
+ data: task.handled.action.data,
177
+ handledBy: task.handled.handledBy,
178
+ handledAt: new Date(task.handledAt ?? Date.now()),
179
+ taskId: task.id
180
+ };
181
+ }
182
+
183
+ // src/client.ts
184
+ var DEFAULT_POLL_INTERVAL_MS = 2e3;
185
+ var DEFAULT_TIMEOUT_MS = 24 * 60 * 60 * 1e3;
186
+ function sleep(ms) {
187
+ return new Promise((resolve) => setTimeout(resolve, ms));
188
+ }
189
+ function parseValidUntilMs(value) {
190
+ if (value === void 0) {
191
+ return void 0;
192
+ }
193
+ if (value instanceof Date) {
194
+ const ms = value.getTime();
195
+ return Number.isNaN(ms) ? void 0 : ms;
196
+ }
197
+ if (typeof value === "number") {
198
+ return Number.isFinite(value) ? value : void 0;
199
+ }
200
+ const parsed = Date.parse(value);
201
+ return Number.isNaN(parsed) ? void 0 : parsed;
202
+ }
203
+ function serializeValidUntil(value) {
204
+ if (value instanceof Date) {
205
+ const ms = value.getTime();
206
+ if (Number.isNaN(ms)) {
207
+ throw new RobotRockError("Invalid validUntil: Date is invalid", 400);
208
+ }
209
+ return value.toISOString();
210
+ }
211
+ if (typeof value === "string" && !Number.isNaN(Date.parse(value))) {
212
+ return new Date(value).toISOString();
213
+ }
214
+ throw new RobotRockError("Invalid validUntil: expected a Date or parseable date string", 400);
215
+ }
216
+ var RobotRockError = class extends Error {
217
+ constructor(message, statusCode, response) {
218
+ super(message);
219
+ this.statusCode = statusCode;
220
+ this.response = response;
221
+ this.name = "RobotRockError";
222
+ }
223
+ };
224
+ var RobotRock = class {
225
+ apiKey;
226
+ baseUrl;
227
+ app;
228
+ version;
229
+ webhook;
230
+ polling;
231
+ constructor(config) {
232
+ if (config.webhook && config.polling) {
233
+ throw new Error(
234
+ "RobotRock client cannot configure both webhook and polling. Use webhook for callbacks or polling to block until handled."
235
+ );
236
+ }
237
+ const apiKey = config.apiKey ?? process.env.ROBOTROCK_API_KEY;
238
+ if (!apiKey) {
239
+ throw new Error(
240
+ "RobotRock API key is required. Set ROBOTROCK_API_KEY or pass apiKey when creating the client."
241
+ );
242
+ }
243
+ this.apiKey = apiKey;
244
+ const rawBase = config.baseUrl ?? "https://api.robotrock.io/v1";
245
+ this.baseUrl = rawBase.replace(/\/+$/, "");
246
+ this.app = config.app;
247
+ this.version = config.version ?? 2;
248
+ this.webhook = config.webhook;
249
+ this.polling = config.polling ?? {};
250
+ }
251
+ /**
252
+ * Create a task via POST /v1 without waiting for a human response.
253
+ */
254
+ async createTask(task) {
255
+ const normalizedTask = normalizeSendToHumanInput(task, {
256
+ webhook: this.webhook,
257
+ app: this.app,
258
+ version: this.version
259
+ });
260
+ const bodyPayload = {
261
+ ...normalizedTask,
262
+ ...task.assignTo !== void 0 ? { assignTo: task.assignTo } : {},
263
+ ...task.threadId !== void 0 ? { threadId: task.threadId } : {},
264
+ ...task.priority !== void 0 ? { priority: task.priority } : {},
265
+ ...task.update !== void 0 ? { update: task.update } : {},
266
+ ...task.agent !== void 0 ? { agent: task.agent } : {}
267
+ };
268
+ const validation = createTaskBodySchema.safeParse(bodyPayload);
269
+ if (!validation.success) {
270
+ throw new RobotRockError(
271
+ `Invalid task: ${validation.error.issues[0]?.message}`,
272
+ 400,
273
+ validation.error.issues
274
+ );
275
+ }
276
+ const headers = {
277
+ "Content-Type": "application/json",
278
+ "X-Api-Key": this.apiKey
279
+ };
280
+ if (task.idempotencyKey) {
281
+ headers["Idempotency-Key"] = task.idempotencyKey;
282
+ }
283
+ const response = await fetch(`${this.baseUrl}/`, {
284
+ method: "POST",
285
+ headers,
286
+ body: JSON.stringify(validation.data)
287
+ });
288
+ const data = await parseResponseBody(response);
289
+ if (!response.ok) {
290
+ throw new RobotRockError(
291
+ getErrorMessage(data, "Failed to create task"),
292
+ response.status,
293
+ data
294
+ );
295
+ }
296
+ return data.task;
297
+ }
298
+ async sendToHuman(task) {
299
+ const normalizedTask = normalizeSendToHumanInput(task, {
300
+ webhook: this.webhook,
301
+ app: this.app,
302
+ version: this.version
303
+ });
304
+ const createdTaskTask = await this.createTask(task);
305
+ const hasHandlers = normalizedTask.actions.some(
306
+ (action) => Array.isArray(action.handlers) && action.handlers.length > 0
307
+ );
308
+ if (hasHandlers) {
309
+ return {
310
+ mode: "created",
311
+ task: createdTaskTask
312
+ };
313
+ }
314
+ const timeoutMs = this.polling.timeoutMs ?? DEFAULT_TIMEOUT_MS;
315
+ const pollIntervalMs = this.polling.intervalMs ?? DEFAULT_POLL_INTERVAL_MS;
316
+ const pollingDeadline = Date.now() + timeoutMs;
317
+ const validUntilMs = parseValidUntilMs(createdTaskTask.validUntil);
318
+ const deadline = validUntilMs !== void 0 ? Math.min(pollingDeadline, validUntilMs) : pollingDeadline;
319
+ const taskId = createdTaskTask.taskId;
320
+ while (Date.now() < deadline) {
321
+ const existing = await this.getTask(taskId);
322
+ if (existing?.status === "handled" && existing.handled) {
323
+ return {
324
+ mode: "handled",
325
+ task: createdTaskTask,
326
+ ...toDiscriminatedApprovalResult(
327
+ normalizedTask.actions,
328
+ existing
329
+ )
330
+ };
331
+ }
332
+ if (existing?.status === "expired" || existing && Date.now() >= existing.validUntil) {
333
+ throw new TaskExpiredError("Task reached validUntil before a human completed it");
334
+ }
335
+ const remainingMs = deadline - Date.now();
336
+ await sleep(Math.min(pollIntervalMs, Math.max(0, remainingMs)));
337
+ }
338
+ if (validUntilMs !== void 0 && Date.now() >= validUntilMs) {
339
+ throw new TaskExpiredError("Task reached validUntil before a human completed it");
340
+ }
341
+ throw new TaskTimeoutError(`No human response within ${timeoutMs}ms`);
342
+ }
343
+ /**
344
+ * Get a task by public task id (returned as `task.taskId` from {@link sendToHuman}).
345
+ */
346
+ async getTask(taskId) {
347
+ const response = await fetch(`${this.baseUrl}/tasks/${taskId}`, {
348
+ method: "GET",
349
+ headers: {
350
+ "X-Api-Key": this.apiKey
351
+ }
352
+ });
353
+ if (response.status === 404) {
354
+ return null;
355
+ }
356
+ const data = await parseResponseBody(response);
357
+ if (!response.ok) {
358
+ throw new RobotRockError(
359
+ getErrorMessage(data, "Failed to get task"),
360
+ response.status,
361
+ data
362
+ );
363
+ }
364
+ return data;
365
+ }
366
+ /**
367
+ * Log a status update against a thread. The update shows in the inbox status
368
+ * bar and thread update log for every task in the thread.
369
+ */
370
+ async sendUpdate({ threadId, message, status }) {
371
+ if (!threadId) {
372
+ throw new RobotRockError("threadId is required to send an update", 400);
373
+ }
374
+ const validation = threadUpdateBodySchema.safeParse({ message, status });
375
+ if (!validation.success) {
376
+ throw new RobotRockError(
377
+ `Invalid update: ${validation.error.issues[0]?.message}`,
378
+ 400,
379
+ validation.error.issues
380
+ );
381
+ }
382
+ const response = await fetch(
383
+ `${this.baseUrl}/threads/${encodeURIComponent(threadId)}/updates`,
384
+ {
385
+ method: "POST",
386
+ headers: {
387
+ "Content-Type": "application/json",
388
+ "X-Api-Key": this.apiKey
389
+ },
390
+ body: JSON.stringify(validation.data)
391
+ }
392
+ );
393
+ const data = await parseResponseBody(response);
394
+ if (!response.ok) {
395
+ throw new RobotRockError(
396
+ getErrorMessage(data, "Failed to send update"),
397
+ response.status,
398
+ data
399
+ );
400
+ }
401
+ return data.update;
402
+ }
403
+ async cancelTask(taskId) {
404
+ const response = await fetch(`${this.baseUrl}/tasks/${taskId}/cancel`, {
405
+ method: "POST",
406
+ headers: {
407
+ "X-Api-Key": this.apiKey
408
+ }
409
+ });
410
+ if (!response.ok) {
411
+ const data = await parseResponseBody(response);
412
+ throw new RobotRockError(
413
+ getErrorMessage(data, "Failed to cancel task"),
414
+ response.status,
415
+ data
416
+ );
417
+ }
418
+ }
419
+ };
420
+ function createClient(config) {
421
+ return new RobotRock(config);
422
+ }
423
+ function attachWebhookToActions(actions, webhook) {
424
+ return actions.map((action) => ({
425
+ ...action,
426
+ handlers: webhookToHandlers(webhook)
427
+ }));
428
+ }
429
+ function webhookToHandlers(webhook) {
430
+ return [
431
+ {
432
+ type: "webhook",
433
+ url: webhook.url,
434
+ headers: webhook.headers ?? {}
435
+ }
436
+ ];
437
+ }
438
+ function normalizeSendToHumanInput(task, clientDefaults) {
439
+ const {
440
+ actions,
441
+ idempotencyKey: _idempotencyKey,
442
+ assignTo: _assignTo,
443
+ threadId: _threadId,
444
+ priority: _priority,
445
+ update: _update,
446
+ validUntil,
447
+ app: taskApp,
448
+ ...rest
449
+ } = task;
450
+ const webhook = clientDefaults.webhook;
451
+ const normalizedActions = webhook ? attachWebhookToActions(actions, webhook) : actions;
452
+ const app = taskApp ?? clientDefaults.app;
453
+ return {
454
+ ...rest,
455
+ version: clientDefaults.version,
456
+ ...app ? { app } : {},
457
+ ...validUntil !== void 0 ? { validUntil: serializeValidUntil(validUntil) } : {},
458
+ actions: normalizedActions
459
+ };
460
+ }
461
+ async function parseResponseBody(response) {
462
+ const contentType = response.headers.get("content-type") ?? "";
463
+ const bodyText = await response.text();
464
+ if (!bodyText) {
465
+ return null;
466
+ }
467
+ if (contentType.toLowerCase().includes("application/json")) {
468
+ try {
469
+ return JSON.parse(bodyText);
470
+ } catch {
471
+ }
472
+ }
473
+ try {
474
+ return JSON.parse(bodyText);
475
+ } catch {
476
+ return bodyText;
477
+ }
478
+ }
479
+ function getErrorMessage(data, fallback) {
480
+ if (data && typeof data === "object" && !Array.isArray(data)) {
481
+ const maybeMessage = data.message;
482
+ if (typeof maybeMessage === "string" && maybeMessage.trim()) {
483
+ return maybeMessage;
484
+ }
485
+ }
486
+ if (typeof data === "string" && data.trim()) {
487
+ const compact = data.replace(/\s+/g, " ").trim();
488
+ const snippet = compact.length > 180 ? `${compact.slice(0, 180)}...` : compact;
489
+ return `${fallback}. Server returned non-JSON response: ${snippet}`;
490
+ }
491
+ return fallback;
492
+ }
493
+
494
+ // src/env.ts
495
+ var DEFAULT_BASE_URL = "https://api.robotrock.io/v1";
496
+ function resolveRobotRockConfig(overrides) {
497
+ const apiKey = overrides?.apiKey ?? process.env.ROBOTROCK_API_KEY;
498
+ if (!apiKey) {
499
+ throw new Error(
500
+ "RobotRock API key is required. Set ROBOTROCK_API_KEY or pass apiKey when creating the client."
501
+ );
502
+ }
503
+ const baseUrl = overrides?.baseUrl ?? process.env.ROBOTROCK_BASE_URL ?? process.env.ROBOTROCK_API_URL ?? DEFAULT_BASE_URL;
504
+ const app = overrides?.app ?? process.env.ROBOTROCK_APP;
505
+ return app ? { apiKey, baseUrl, app } : { apiKey, baseUrl };
506
+ }
507
+ function resolveRobotRockClient(client, configOverrides) {
508
+ if (client) {
509
+ return client;
510
+ }
511
+ return createClient(resolveRobotRockConfig(configOverrides));
512
+ }
27
513
 
28
514
  // src/webhook.ts
29
515
  import { createHmac, timingSafeEqual } from "crypto";
30
- import { z } from "zod";
516
+ import { z as z2 } from "zod";
31
517
  var ROBOTROCK_SIGNATURE_HEADER = "x-robotrock-signature";
32
- var robotRockWebhookPayloadBodySchema = z.object({
33
- taskId: z.string().min(1),
34
- action: z.object({
35
- id: z.string().min(1),
36
- title: z.string().min(1),
37
- data: z.unknown()
518
+ var robotRockWebhookPayloadBodySchema = z2.object({
519
+ taskId: z2.string().min(1),
520
+ action: z2.object({
521
+ id: z2.string().min(1),
522
+ title: z2.string().min(1),
523
+ data: z2.unknown()
38
524
  }),
39
- handledBy: z.string().min(1).optional(),
40
- handledAt: z.string().min(1),
41
- handlerType: z.string().min(1)
525
+ handledBy: z2.string().min(1).optional(),
526
+ handledAt: z2.string().min(1),
527
+ handlerType: z2.string().min(1)
42
528
  });
43
529
  var robotRockWebhookPayloadSchema = robotRockWebhookPayloadBodySchema.extend({
44
- headers: z.record(z.string())
530
+ headers: z2.record(z2.string(), z2.string())
45
531
  });
46
532
  var RobotRockWebhookError = class extends Error {
47
533
  constructor(message, code, details) {
@@ -140,6 +626,9 @@ export {
140
626
  TASK_PRIORITY_RANK,
141
627
  TaskExpiredError,
142
628
  TaskTimeoutError,
629
+ agentCostSchema,
630
+ agentCostTokensSchema,
631
+ agentTelemetrySchema,
143
632
  assignToSchema,
144
633
  attachWebhookToActions,
145
634
  createClient,