guidinghand 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.
@@ -1,4 +1,4 @@
1
- export declare const VERSION = "0.1.0";
1
+ export declare const VERSION = "0.1.1";
2
2
  export type Metadata = Record<string, string>;
3
3
  export type Effort = 'low' | 'medium' | 'high' | null;
4
4
  export type Agent = {
@@ -310,9 +310,13 @@ declare class Tasks extends Resource {
310
310
  }
311
311
  declare class WebhookEndpoint extends Resource {
312
312
  retrieve(): Promise<Webhook>;
313
- /** Sets the endpoint. The signing secret is in the response when it's new (or with rotate_secret), never again. */
313
+ /**
314
+ * Sets the endpoint. Only the fields you pass change: without url the endpoint stays, without events the
315
+ * filter stays, so { rotate_secret: true } alone makes a new secret. The signing secret is in the response
316
+ * when it's new (or rotated), never again.
317
+ */
314
318
  update(params: {
315
- url: string;
319
+ url?: string;
316
320
  events?: WebhookEventType[];
317
321
  rotate_secret?: boolean;
318
322
  }): Promise<Webhook>;
package/dist/cjs/index.js CHANGED
@@ -11,7 +11,7 @@
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.GuidingHand = exports.WebhookVerificationError = exports.NeedsInputError = exports.SessionExpiredError = exports.TimeoutError = exports.APIConnectionError = exports.APIError = exports.RateLimitError = exports.ConflictError = exports.NotFoundError = exports.PermissionDeniedError = exports.PaymentRequiredError = exports.AuthenticationError = exports.InvalidRequestError = exports.GuidingHandError = exports.VERSION = void 0;
13
13
  exports.verifyWebhook = verifyWebhook;
14
- exports.VERSION = '0.1.0';
14
+ exports.VERSION = '0.1.1';
15
15
  // ---------- errors ----------
16
16
  class GuidingHandError extends Error {
17
17
  status;
@@ -123,6 +123,9 @@ class GuidingHand {
123
123
  }
124
124
  if (res.ok)
125
125
  return (raw ? new Uint8Array(await res.arrayBuffer()) : await res.json());
126
+ // A retried delete that finds it gone: the first attempt did it (its answer was lost on the way back).
127
+ if (method === 'DELETE' && attempt > 0 && res.status === 404)
128
+ return { object: path.split('/')[1]?.replace(/s$/, '') ?? 'object', deleted: true };
126
129
  if (canRetry && attempt < this.maxRetries && (res.status === 429 || res.status >= 500)) {
127
130
  const after = Number(res.headers.get('retry-after'));
128
131
  await sleep(Number.isFinite(after) && after > 0 ? after * 1000 : 500 * 2 ** attempt);
@@ -216,7 +219,9 @@ class Tasks extends Resource {
216
219
  let after = 0, answered = '';
217
220
  while (!task.done) {
218
221
  if (Date.now() > until) {
219
- await this.stop(task.task_id).catch(() => { });
222
+ const stopped = await this.stop(task.task_id).catch(() => null);
223
+ if (stopped && stopped.done && !stopped.interrupted)
224
+ return stopped; // it finished just as time ran out
220
225
  throw new TimeoutError(`Task ${task.task_id} took longer than ${Math.round(timeout / 1000)} s; stopped it.`, 0, 'timeout');
221
226
  }
222
227
  const r = await this.events(task.task_id, { after, wait_ms: Math.min(25_000, Math.max(0, until - Date.now())) });
@@ -230,17 +235,24 @@ class Tasks extends Resource {
230
235
  const id = p.type === 'question' ? p.question_id : p.approval_id;
231
236
  if (id === answered)
232
237
  continue; // answered already; the task is picking it up
233
- if (p.type === 'question') {
234
- if (!onQuestion)
235
- throw new NeedsInputError(task);
236
- await this.respond(task.task_id, { question_id: p.question_id, answer: String(await onQuestion(p, task)) });
238
+ try {
239
+ if (p.type === 'question') {
240
+ if (!onQuestion)
241
+ throw new NeedsInputError(task);
242
+ await this.respond(task.task_id, { question_id: p.question_id, answer: String(await onQuestion(p, task)) });
243
+ }
244
+ else {
245
+ if (!onApproval)
246
+ throw new NeedsInputError(task);
247
+ const d = await onApproval(p, task);
248
+ const decision = d && typeof d === 'object' ? d : { decision: d === true || d === 'approve' ? 'approve' : 'deny' };
249
+ await this.respond(task.task_id, { approval_id: p.approval_id, ...decision });
250
+ }
237
251
  }
238
- else {
239
- if (!onApproval)
240
- throw new NeedsInputError(task);
241
- const d = await onApproval(p, task);
242
- const decision = typeof d === 'object' ? d : { decision: d === true || d === 'approve' ? 'approve' : 'deny' };
243
- await this.respond(task.task_id, { approval_id: p.approval_id, ...decision });
252
+ catch (e) {
253
+ // Answered from somewhere else (the console, another process) or the task ended meanwhile: keep following.
254
+ if (!(e instanceof ConflictError))
255
+ throw e;
244
256
  }
245
257
  answered = id;
246
258
  }
@@ -250,7 +262,11 @@ class Tasks extends Resource {
250
262
  // ---------- webhooks ----------
251
263
  class WebhookEndpoint extends Resource {
252
264
  retrieve() { return this.client.request('GET', '/webhook'); }
253
- /** Sets the endpoint. The signing secret is in the response when it's new (or with rotate_secret), never again. */
265
+ /**
266
+ * Sets the endpoint. Only the fields you pass change: without url the endpoint stays, without events the
267
+ * filter stays, so { rotate_secret: true } alone makes a new secret. The signing secret is in the response
268
+ * when it's new (or rotated), never again.
269
+ */
254
270
  update(params) { return this.client.request('PUT', '/webhook', { body: params }); }
255
271
  delete() { return this.client.request('PUT', '/webhook', { body: { url: null } }); }
256
272
  }
@@ -267,8 +283,10 @@ async function verifyWebhook(payload, signatureHeader, secret, { tolerance = 300
267
283
  if (Math.abs(Date.now() / 1000 - t) > tolerance)
268
284
  throw fail('The webhook’s timestamp is too old.');
269
285
  const body = typeof payload === 'string' ? payload : new TextDecoder().decode(payload);
270
- const key = await crypto.subtle.importKey('raw', new TextEncoder().encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
271
- const mac = new Uint8Array(await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(`${t}.${body}`)));
286
+ // Node 18 has WebCrypto at node:crypto but not as a global; Node 20+, Deno, Bun and Workers have both.
287
+ const subtle = globalThis.crypto?.subtle ?? (await Promise.resolve(`${'node:crypto'}`).then(s => require(s))).webcrypto.subtle;
288
+ const key = await subtle.importKey('raw', new TextEncoder().encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
289
+ const mac = new Uint8Array(await subtle.sign('HMAC', key, new TextEncoder().encode(`${t}.${body}`)));
272
290
  const want = [...mac].map((b) => b.toString(16).padStart(2, '0')).join('');
273
291
  if (want.length !== v1.length)
274
292
  throw fail('The webhook’s signature doesn’t match.');
@@ -1,4 +1,4 @@
1
- export declare const VERSION = "0.1.0";
1
+ export declare const VERSION = "0.1.1";
2
2
  export type Metadata = Record<string, string>;
3
3
  export type Effort = 'low' | 'medium' | 'high' | null;
4
4
  export type Agent = {
@@ -310,9 +310,13 @@ declare class Tasks extends Resource {
310
310
  }
311
311
  declare class WebhookEndpoint extends Resource {
312
312
  retrieve(): Promise<Webhook>;
313
- /** Sets the endpoint. The signing secret is in the response when it's new (or with rotate_secret), never again. */
313
+ /**
314
+ * Sets the endpoint. Only the fields you pass change: without url the endpoint stays, without events the
315
+ * filter stays, so { rotate_secret: true } alone makes a new secret. The signing secret is in the response
316
+ * when it's new (or rotated), never again.
317
+ */
314
318
  update(params: {
315
- url: string;
319
+ url?: string;
316
320
  events?: WebhookEventType[];
317
321
  rotate_secret?: boolean;
318
322
  }): Promise<Webhook>;
package/dist/esm/index.js CHANGED
@@ -7,7 +7,7 @@
7
7
  // // send session.invite_url to the person at the computer, then:
8
8
  // await gh.sessions.waitForConnection(session.session_id);
9
9
  // const task = await gh.tasks.run(session.session_id, { prompt: 'Turn on Dark Mode', onQuestion: async (q) => 'Work', onApproval: async () => true });
10
- export const VERSION = '0.1.0';
10
+ export const VERSION = '0.1.1';
11
11
  // ---------- errors ----------
12
12
  export class GuidingHandError extends Error {
13
13
  status;
@@ -105,6 +105,9 @@ export class GuidingHand {
105
105
  }
106
106
  if (res.ok)
107
107
  return (raw ? new Uint8Array(await res.arrayBuffer()) : await res.json());
108
+ // A retried delete that finds it gone: the first attempt did it (its answer was lost on the way back).
109
+ if (method === 'DELETE' && attempt > 0 && res.status === 404)
110
+ return { object: path.split('/')[1]?.replace(/s$/, '') ?? 'object', deleted: true };
108
111
  if (canRetry && attempt < this.maxRetries && (res.status === 429 || res.status >= 500)) {
109
112
  const after = Number(res.headers.get('retry-after'));
110
113
  await sleep(Number.isFinite(after) && after > 0 ? after * 1000 : 500 * 2 ** attempt);
@@ -197,7 +200,9 @@ class Tasks extends Resource {
197
200
  let after = 0, answered = '';
198
201
  while (!task.done) {
199
202
  if (Date.now() > until) {
200
- await this.stop(task.task_id).catch(() => { });
203
+ const stopped = await this.stop(task.task_id).catch(() => null);
204
+ if (stopped && stopped.done && !stopped.interrupted)
205
+ return stopped; // it finished just as time ran out
201
206
  throw new TimeoutError(`Task ${task.task_id} took longer than ${Math.round(timeout / 1000)} s; stopped it.`, 0, 'timeout');
202
207
  }
203
208
  const r = await this.events(task.task_id, { after, wait_ms: Math.min(25_000, Math.max(0, until - Date.now())) });
@@ -211,17 +216,24 @@ class Tasks extends Resource {
211
216
  const id = p.type === 'question' ? p.question_id : p.approval_id;
212
217
  if (id === answered)
213
218
  continue; // answered already; the task is picking it up
214
- if (p.type === 'question') {
215
- if (!onQuestion)
216
- throw new NeedsInputError(task);
217
- await this.respond(task.task_id, { question_id: p.question_id, answer: String(await onQuestion(p, task)) });
219
+ try {
220
+ if (p.type === 'question') {
221
+ if (!onQuestion)
222
+ throw new NeedsInputError(task);
223
+ await this.respond(task.task_id, { question_id: p.question_id, answer: String(await onQuestion(p, task)) });
224
+ }
225
+ else {
226
+ if (!onApproval)
227
+ throw new NeedsInputError(task);
228
+ const d = await onApproval(p, task);
229
+ const decision = d && typeof d === 'object' ? d : { decision: d === true || d === 'approve' ? 'approve' : 'deny' };
230
+ await this.respond(task.task_id, { approval_id: p.approval_id, ...decision });
231
+ }
218
232
  }
219
- else {
220
- if (!onApproval)
221
- throw new NeedsInputError(task);
222
- const d = await onApproval(p, task);
223
- const decision = typeof d === 'object' ? d : { decision: d === true || d === 'approve' ? 'approve' : 'deny' };
224
- await this.respond(task.task_id, { approval_id: p.approval_id, ...decision });
233
+ catch (e) {
234
+ // Answered from somewhere else (the console, another process) or the task ended meanwhile: keep following.
235
+ if (!(e instanceof ConflictError))
236
+ throw e;
225
237
  }
226
238
  answered = id;
227
239
  }
@@ -231,7 +243,11 @@ class Tasks extends Resource {
231
243
  // ---------- webhooks ----------
232
244
  class WebhookEndpoint extends Resource {
233
245
  retrieve() { return this.client.request('GET', '/webhook'); }
234
- /** Sets the endpoint. The signing secret is in the response when it's new (or with rotate_secret), never again. */
246
+ /**
247
+ * Sets the endpoint. Only the fields you pass change: without url the endpoint stays, without events the
248
+ * filter stays, so { rotate_secret: true } alone makes a new secret. The signing secret is in the response
249
+ * when it's new (or rotated), never again.
250
+ */
235
251
  update(params) { return this.client.request('PUT', '/webhook', { body: params }); }
236
252
  delete() { return this.client.request('PUT', '/webhook', { body: { url: null } }); }
237
253
  }
@@ -248,8 +264,10 @@ export async function verifyWebhook(payload, signatureHeader, secret, { toleranc
248
264
  if (Math.abs(Date.now() / 1000 - t) > tolerance)
249
265
  throw fail('The webhook’s timestamp is too old.');
250
266
  const body = typeof payload === 'string' ? payload : new TextDecoder().decode(payload);
251
- const key = await crypto.subtle.importKey('raw', new TextEncoder().encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
252
- const mac = new Uint8Array(await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(`${t}.${body}`)));
267
+ // Node 18 has WebCrypto at node:crypto but not as a global; Node 20+, Deno, Bun and Workers have both.
268
+ const subtle = globalThis.crypto?.subtle ?? (await import('node:crypto')).webcrypto.subtle;
269
+ const key = await subtle.importKey('raw', new TextEncoder().encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
270
+ const mac = new Uint8Array(await subtle.sign('HMAC', key, new TextEncoder().encode(`${t}.${body}`)));
253
271
  const want = [...mac].map((b) => b.toString(16).padStart(2, '0')).join('');
254
272
  if (want.length !== v1.length)
255
273
  throw fail('The webhook’s signature doesn’t match.');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "guidinghand",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "GuidingHand SDK: put an AI agent on your customer's computer with one link. Sessions, tasks, agents, recordings and webhooks.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://guidinghand.ai",