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.
- package/dist/cjs/index.d.ts +7 -3
- package/dist/cjs/index.js +33 -15
- package/dist/esm/index.d.ts +7 -3
- package/dist/esm/index.js +33 -15
- package/package.json +1 -1
package/dist/cjs/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const VERSION = "0.1.
|
|
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
|
-
/**
|
|
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
|
|
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.
|
|
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
|
-
|
|
234
|
-
if (
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
271
|
-
const
|
|
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.');
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const VERSION = "0.1.
|
|
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
|
-
/**
|
|
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
|
|
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.
|
|
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
|
-
|
|
215
|
-
if (
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
252
|
-
const
|
|
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.
|
|
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",
|