guidinghand 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 GuidingHand
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,128 @@
1
+ # GuidingHand for JavaScript and TypeScript
2
+
3
+ Put an AI agent on your customer's Mac or PC with one link. Create a session, send its invite link, and run tasks in plain language: the agent works on their screen, asks when it needs something, and every screen is recorded for replay.
4
+
5
+ ```bash
6
+ npm install guidinghand
7
+ ```
8
+
9
+ No dependencies. Node 18+, Deno, Bun and edge runtimes. Types included. Keep your API key on the server: it isn't meant for browsers.
10
+
11
+ ## Quickstart
12
+
13
+ ```ts
14
+ import GuidingHand from 'guidinghand';
15
+
16
+ const gh = new GuidingHand(); // reads GUIDINGHAND_API_KEY (Settings → API keys in the console)
17
+
18
+ // 1. A session: send invite_url to the person at the computer
19
+ const session = await gh.sessions.create({ agent_id: 'default', metadata: { ticket: 'T-123' } });
20
+ console.log('Send this link:', session.invite_url); // https://guidinghand.ai/acme/K7QM-24XP
21
+
22
+ // 2. Wait for their computer to connect
23
+ await gh.sessions.waitForConnection(session.session_id);
24
+
25
+ // 3. Run a task, answering the agent's questions and approvals as they come
26
+ const task = await gh.tasks.run(session.session_id, {
27
+ prompt: 'Turn on Dark Mode',
28
+ onEvent: (e) => console.log(e.type, e.message),
29
+ onQuestion: async (q) => 'Work', // q.question, q.options
30
+ onApproval: async (a) => a.risk !== 'high', // true/'approve' or false/'deny'
31
+ });
32
+
33
+ console.log(task.status, task.result); // completed "Dark Mode is on."
34
+ console.log('Replay:', task.replay_url);
35
+ ```
36
+
37
+ ## Agents
38
+
39
+ An agent is your configuration: instructions (added under GuidingHand's own rules, which always win), effort and a greeting for the invite page. Every org has a `default` agent.
40
+
41
+ ```ts
42
+ await gh.agents.create({ agent_id: 'billing', name: 'Billing help', instructions: 'Open the Billing app from the Dock.', effort: 'medium', greeting: 'Hi, this is Acme billing.' });
43
+ await gh.agents.update('billing', { effort: 'high' });
44
+ const { data } = await gh.agents.list();
45
+ await gh.agents.delete('billing');
46
+ ```
47
+
48
+ ## Sessions
49
+
50
+ ```ts
51
+ const s = await gh.sessions.create({ agent_id: 'billing' }); // s.code, s.invite_url, s.status: 'waiting'
52
+ await gh.sessions.retrieve(s.session_id); // status: waiting | connected | disconnected | expired
53
+ for await (const s of gh.sessions.listAll({ agent_id: 'billing' })) console.log(s.code, s.status);
54
+ await gh.sessions.delete(s.session_id); // also deletes its tasks and recordings
55
+ ```
56
+
57
+ ## Tasks
58
+
59
+ `run()` is the easy way. Underneath it are:
60
+
61
+ ```ts
62
+ const task = await gh.tasks.create(sessionId, { prompt: 'Install the VPN profile', request_id: 'ticket-123' }); // request_id: safe to retry
63
+
64
+ for await (const event of gh.tasks.stream(task.task_id)) console.log(event.type, event.message);
65
+
66
+ const t = await gh.tasks.retrieve(task.task_id, { include: ['events'] });
67
+ if (t.pending?.type === 'question') await gh.tasks.respond(t.task_id, { question_id: t.pending.question_id, answer: 'Work' });
68
+ if (t.pending?.type === 'approval') await gh.tasks.respond(t.task_id, { approval_id: t.pending.approval_id, decision: 'deny', note: 'Not that one' });
69
+ await gh.tasks.stop(t.task_id);
70
+
71
+ const page = await gh.tasks.list({ status: 'completed', limit: 50 }); // { data, has_more, next_cursor }
72
+ for await (const t of gh.tasks.listAll({ agent_id: 'billing' })) { /* every page */ }
73
+ ```
74
+
75
+ ## Recordings
76
+
77
+ ```ts
78
+ const rec = await gh.tasks.recording(taskId); // rec.frames: [{ seq, t_ms, width, height, url }]
79
+ const png = await gh.tasks.recordingFrame(taskId, 1); // Uint8Array
80
+ ```
81
+
82
+ The console replays them with the agent's cursor at `task.replay_url`.
83
+
84
+ ## Webhooks
85
+
86
+ ```ts
87
+ const { secret } = await gh.webhook.update({ url: 'https://example.com/guidinghand', events: ['task.completed', 'task.waiting_for_user'] });
88
+ ```
89
+
90
+ Verify each delivery with the raw request body:
91
+
92
+ ```ts
93
+ import { verifyWebhook } from 'guidinghand';
94
+
95
+ app.post('/guidinghand', express.raw({ type: 'application/json' }), async (req, res) => {
96
+ const event = await verifyWebhook(req.body, req.header('GuidingHand-Signature'), process.env.GUIDINGHAND_WEBHOOK_SECRET);
97
+ if (event.type === 'task.completed') console.log(event.data.task.result);
98
+ res.sendStatus(200);
99
+ });
100
+ ```
101
+
102
+ ## Errors
103
+
104
+ Every error is a `GuidingHandError` with `status`, `type`, `message` and `extra`:
105
+
106
+ | Class | Status | When |
107
+ |---|---|---|
108
+ | `InvalidRequestError` | 400 | a missing or wrong field |
109
+ | `AuthenticationError` | 401 | no key, or a revoked one |
110
+ | `PaymentRequiredError` | 402 | the org's plan doesn't allow it (free minutes used up) |
111
+ | `PermissionDeniedError` | 403 | your role can't do this |
112
+ | `NotFoundError` | 404 | not in this org |
113
+ | `ConflictError` | 409 | no computer connected, a task already running (`extra.active_task_id`), nothing pending |
114
+ | `RateLimitError` | 429 | too many requests, or the plan's tasks at once |
115
+ | `APIError` | 5xx | our side |
116
+ | `APIConnectionError`, `TimeoutError` | | network |
117
+ | `NeedsInputError` | | `run()` got a question or approval without a handler (`error.task`) |
118
+ | `SessionExpiredError` | | `waitForConnection()` on a code that expired |
119
+
120
+ Reads, deletes and starts with a `request_id` are retried on connection errors, 429 and 5xx (`maxRetries`, default 2).
121
+
122
+ ## Options
123
+
124
+ ```ts
125
+ new GuidingHand({ apiKey, baseUrl: 'https://dev.guidinghand.ai', timeout: 60_000, maxRetries: 2 });
126
+ ```
127
+
128
+ API reference: https://guidinghand.ai/openapi.json
@@ -0,0 +1,327 @@
1
+ export declare const VERSION = "0.1.0";
2
+ export type Metadata = Record<string, string>;
3
+ export type Effort = 'low' | 'medium' | 'high' | null;
4
+ export type Agent = {
5
+ object: 'agent';
6
+ agent_id: string;
7
+ name: string;
8
+ instructions: string;
9
+ effort: Effort;
10
+ greeting: string;
11
+ is_default: boolean;
12
+ invite_url_template: string;
13
+ created_at: string | null;
14
+ updated_at: string | null;
15
+ };
16
+ export type Device = {
17
+ os: 'mac' | 'windows' | 'linux' | 'unknown';
18
+ name: string | null;
19
+ width: number;
20
+ height: number;
21
+ app_version: string | null;
22
+ };
23
+ export type SessionStatus = 'waiting' | 'connected' | 'disconnected' | 'expired';
24
+ export type Session = {
25
+ object: 'session';
26
+ session_id: string;
27
+ code: string;
28
+ agent_id: string;
29
+ invite_url: string;
30
+ status: SessionStatus;
31
+ device: Device | null;
32
+ metadata: Metadata;
33
+ created_at: string;
34
+ paired_at: string | null;
35
+ last_active_at: string;
36
+ expires_at: string | null;
37
+ task_count?: number;
38
+ latest_task_id?: string | null;
39
+ };
40
+ export type TaskStatus = 'queued' | 'running' | 'waiting_for_user' | 'waiting_for_approval' | 'completed' | 'failed' | 'stopped';
41
+ export type Question = {
42
+ type: 'question';
43
+ question_id: string;
44
+ question: string;
45
+ options: string[];
46
+ };
47
+ export type Approval = {
48
+ type: 'approval';
49
+ approval_id: string;
50
+ action: string;
51
+ risk: 'low' | 'medium' | 'high';
52
+ };
53
+ export type EventType = 'started' | 'progress' | 'thinking' | 'action' | 'message' | 'question' | 'answer' | 'approval_required' | 'approved' | 'denied' | 'completed' | 'error' | 'stopped';
54
+ export type TaskEvent = {
55
+ cursor: number;
56
+ type: EventType;
57
+ message: string;
58
+ ts: string;
59
+ data?: Record<string, unknown>;
60
+ };
61
+ export type Task = {
62
+ object: 'task';
63
+ task_id: string;
64
+ session_id: string;
65
+ agent_id: string;
66
+ status: TaskStatus;
67
+ done: boolean;
68
+ prompt: string;
69
+ result: string | null;
70
+ error: string | null;
71
+ pending: Question | Approval | null;
72
+ cursor?: number;
73
+ metadata: Metadata;
74
+ created_at: string;
75
+ updated_at: string;
76
+ active_seconds: number;
77
+ billed_minutes: number | null;
78
+ replay_url: string | null;
79
+ recording?: {
80
+ frames: number;
81
+ };
82
+ events?: TaskEvent[];
83
+ trace?: unknown[];
84
+ interrupted?: boolean;
85
+ };
86
+ export type Frame = {
87
+ seq: number;
88
+ t_ms: number;
89
+ after_event: number;
90
+ width: number;
91
+ height: number;
92
+ url: string;
93
+ };
94
+ export type Recording = {
95
+ object: 'recording';
96
+ task_id: string;
97
+ frames: Frame[];
98
+ };
99
+ export type WebhookEventType = 'session.connected' | 'session.disconnected' | 'task.started' | 'task.waiting_for_user' | 'task.waiting_for_approval' | 'task.completed' | 'task.failed' | 'task.stopped';
100
+ export type Webhook = {
101
+ object: 'webhook';
102
+ url: string | null;
103
+ events: WebhookEventType[];
104
+ has_secret: boolean;
105
+ secret?: string;
106
+ event_types: WebhookEventType[];
107
+ };
108
+ export type WebhookEvent = {
109
+ id: string;
110
+ type: WebhookEventType;
111
+ created_at: string;
112
+ org_id: string;
113
+ data: {
114
+ task?: Task;
115
+ session?: Session;
116
+ };
117
+ };
118
+ export type Page<T> = {
119
+ data: T[];
120
+ has_more: boolean;
121
+ next_cursor: string | null;
122
+ };
123
+ export type Deleted = {
124
+ object: string;
125
+ deleted: true;
126
+ };
127
+ export declare class GuidingHandError extends Error {
128
+ status: number;
129
+ type: string;
130
+ body: unknown;
131
+ extra: Record<string, unknown>;
132
+ constructor(message: string, status?: number, type?: string, body?: unknown);
133
+ }
134
+ export declare class InvalidRequestError extends GuidingHandError {
135
+ }
136
+ export declare class AuthenticationError extends GuidingHandError {
137
+ }
138
+ export declare class PaymentRequiredError extends GuidingHandError {
139
+ }
140
+ export declare class PermissionDeniedError extends GuidingHandError {
141
+ }
142
+ export declare class NotFoundError extends GuidingHandError {
143
+ }
144
+ export declare class ConflictError extends GuidingHandError {
145
+ }
146
+ export declare class RateLimitError extends GuidingHandError {
147
+ }
148
+ export declare class APIError extends GuidingHandError {
149
+ }
150
+ export declare class APIConnectionError extends GuidingHandError {
151
+ }
152
+ export declare class TimeoutError extends GuidingHandError {
153
+ }
154
+ export declare class SessionExpiredError extends GuidingHandError {
155
+ }
156
+ /** A task waits on a question or approval and `run()` got no handler for it. */
157
+ export declare class NeedsInputError extends GuidingHandError {
158
+ task: Task;
159
+ constructor(task: Task);
160
+ }
161
+ export declare class WebhookVerificationError extends GuidingHandError {
162
+ }
163
+ export type ClientOptions = {
164
+ /** An org API key (gh_live_…). Defaults to the GUIDINGHAND_API_KEY environment variable. */
165
+ apiKey?: string;
166
+ /** Defaults to https://guidinghand.ai (use https://dev.guidinghand.ai for Stripe test mode). */
167
+ baseUrl?: string;
168
+ /** Per request, in ms (long polls add their wait). Default 60 000. */
169
+ timeout?: number;
170
+ /** Retries on connection errors, 429 and 5xx, for reads and for starts with a request_id. Default 2. */
171
+ maxRetries?: number;
172
+ fetch?: typeof fetch;
173
+ };
174
+ type Req = {
175
+ query?: Record<string, string | number | undefined | null>;
176
+ body?: unknown;
177
+ retry?: boolean;
178
+ wait?: number;
179
+ raw?: boolean;
180
+ };
181
+ export declare class GuidingHand {
182
+ readonly baseUrl: string;
183
+ readonly agents: Agents;
184
+ readonly sessions: Sessions;
185
+ readonly tasks: Tasks;
186
+ readonly webhook: WebhookEndpoint;
187
+ private apiKey;
188
+ private timeout;
189
+ private maxRetries;
190
+ private fetchImpl;
191
+ constructor(opts?: ClientOptions);
192
+ /** @internal */
193
+ request<T>(method: string, path: string, { query, body, retry, wait, raw }?: Req): Promise<T>;
194
+ /** @internal Every item of a paged list. */
195
+ pages<T>(path: string, query: Record<string, string | number | undefined | null>): AsyncGenerator<T>;
196
+ }
197
+ export default GuidingHand;
198
+ declare class Resource {
199
+ protected client: GuidingHand;
200
+ constructor(client: GuidingHand);
201
+ }
202
+ export type AgentCreate = {
203
+ name: string;
204
+ agent_id?: string;
205
+ instructions?: string;
206
+ effort?: Effort;
207
+ greeting?: string;
208
+ };
209
+ export type AgentUpdate = Partial<Omit<AgentCreate, 'agent_id'>>;
210
+ declare class Agents extends Resource {
211
+ list(): Promise<Page<Agent>>;
212
+ create(params: AgentCreate): Promise<Agent>;
213
+ retrieve(agentId: string): Promise<Agent>;
214
+ update(agentId: string, params: AgentUpdate): Promise<Agent>;
215
+ delete(agentId: string): Promise<Deleted>;
216
+ }
217
+ export type SessionCreate = {
218
+ agent_id?: string;
219
+ metadata?: Metadata;
220
+ };
221
+ export type SessionList = {
222
+ agent_id?: string;
223
+ limit?: number;
224
+ cursor?: string;
225
+ };
226
+ declare class Sessions extends Resource {
227
+ /** Makes a code and its invite link. Send `invite_url` to the person at the computer. */
228
+ create(params?: SessionCreate): Promise<Session & {
229
+ session_token: string;
230
+ }>;
231
+ list(params?: SessionList): Promise<Page<Session>>;
232
+ /** Every session, newest first, across pages. */
233
+ listAll(params?: Omit<SessionList, 'limit' | 'cursor'>): AsyncGenerator<Session>;
234
+ retrieve(sessionId: string): Promise<Session>;
235
+ /** Disconnects the computer and deletes the session's tasks and recordings. */
236
+ delete(sessionId: string): Promise<Deleted>;
237
+ /** Resolves once the person's computer is connected. */
238
+ waitForConnection(sessionId: string, { timeout, pollInterval }?: {
239
+ timeout?: number;
240
+ pollInterval?: number;
241
+ }): Promise<Session>;
242
+ }
243
+ export type TaskCreate = {
244
+ prompt: string;
245
+ agent_id?: string;
246
+ request_id?: string;
247
+ metadata?: Metadata;
248
+ };
249
+ export type TaskList = {
250
+ session_id?: string;
251
+ agent_id?: string;
252
+ status?: TaskStatus;
253
+ limit?: number;
254
+ cursor?: string;
255
+ };
256
+ export type Respond = {
257
+ question_id: string;
258
+ answer: string;
259
+ } | {
260
+ approval_id: string;
261
+ decision: 'approve' | 'deny';
262
+ note?: string;
263
+ };
264
+ export type RunOptions = TaskCreate & {
265
+ /** Every event, as it happens. */
266
+ onEvent?: (event: TaskEvent, task: Task) => void | Promise<void>;
267
+ /** The agent asks something: return the answer. */
268
+ onQuestion?: (question: Question, task: Task) => string | Promise<string>;
269
+ /** The agent wants to do something consequential: return true (or 'approve') to let it, false (or 'deny', or { decision, note }) not to. */
270
+ onApproval?: (approval: Approval, task: Task) => boolean | 'approve' | 'deny' | {
271
+ decision: 'approve' | 'deny';
272
+ note?: string;
273
+ } | Promise<boolean | 'approve' | 'deny' | {
274
+ decision: 'approve' | 'deny';
275
+ note?: string;
276
+ }>;
277
+ /** Give up (and stop the task) after this long, in ms. */
278
+ timeout?: number;
279
+ };
280
+ declare class Tasks extends Resource {
281
+ /** Starts a task on the session's computer (it must be connected). */
282
+ create(sessionId: string, params: TaskCreate): Promise<Task>;
283
+ list(params?: TaskList): Promise<Page<Task>>;
284
+ listAll(params?: Omit<TaskList, 'limit' | 'cursor'>): AsyncGenerator<Task>;
285
+ retrieve(taskId: string, { include }?: {
286
+ include?: ('events' | 'trace')[];
287
+ }): Promise<Task>;
288
+ /** Events after `after`; with `wait_ms`, waits (long poll) for the next one. */
289
+ events(taskId: string, { after, wait_ms }?: {
290
+ after?: number;
291
+ wait_ms?: number;
292
+ }): Promise<{
293
+ data: TaskEvent[];
294
+ cursor: number;
295
+ task: Task;
296
+ }>;
297
+ respond(taskId: string, params: Respond): Promise<Task>;
298
+ stop(taskId: string): Promise<Task & {
299
+ interrupted: boolean;
300
+ }>;
301
+ recording(taskId: string): Promise<Recording>;
302
+ /** One recorded screen, as PNG bytes. */
303
+ recordingFrame(taskId: string, seq: number): Promise<Uint8Array>;
304
+ /** Every event of a task as it happens, until it's done. `for await (const e of gh.tasks.stream(id))` */
305
+ stream(taskId: string, { after }?: {
306
+ after?: number;
307
+ }): AsyncGenerator<TaskEvent, Task>;
308
+ /** Starts a task and sees it through: events to onEvent, questions to onQuestion, approvals to onApproval. Resolves with the finished task. */
309
+ run(sessionId: string, { onEvent, onQuestion, onApproval, timeout, ...params }: RunOptions): Promise<Task>;
310
+ }
311
+ declare class WebhookEndpoint extends Resource {
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. */
314
+ update(params: {
315
+ url: string;
316
+ events?: WebhookEventType[];
317
+ rotate_secret?: boolean;
318
+ }): Promise<Webhook>;
319
+ delete(): Promise<Webhook>;
320
+ }
321
+ /**
322
+ * Checks a webhook's GuidingHand-Signature header against the raw request body and your signing secret, and
323
+ * returns the parsed event. Throws WebhookVerificationError if it doesn't match or is older than `tolerance` s.
324
+ */
325
+ export declare function verifyWebhook(payload: string | Uint8Array, signatureHeader: string | null | undefined, secret: string, { tolerance }?: {
326
+ tolerance?: number;
327
+ }): Promise<WebhookEvent>;