@crouter/sdk 0.3.377
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 +170 -0
- package/dist/client.d.ts +153 -0
- package/dist/client.js +491 -0
- package/dist/error-codes.d.ts +7 -0
- package/dist/error-codes.js +35 -0
- package/dist/errors.d.ts +43 -0
- package/dist/errors.js +76 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +13 -0
- package/dist/keygen-cli.d.ts +2 -0
- package/dist/keygen-cli.js +6 -0
- package/dist/keygen-command.d.ts +6 -0
- package/dist/keygen-command.js +157 -0
- package/dist/oauth/index.d.ts +147 -0
- package/dist/oauth/index.js +377 -0
- package/dist/oauth/keygen.d.ts +18 -0
- package/dist/oauth/keygen.js +46 -0
- package/dist/resources/activity.d.ts +22 -0
- package/dist/resources/activity.js +43 -0
- package/dist/resources/attachments.d.ts +15 -0
- package/dist/resources/attachments.js +13 -0
- package/dist/resources/bash.d.ts +9 -0
- package/dist/resources/bash.js +15 -0
- package/dist/resources/canvas/history.d.ts +11 -0
- package/dist/resources/canvas/history.js +20 -0
- package/dist/resources/canvas.d.ts +19 -0
- package/dist/resources/canvas.js +42 -0
- package/dist/resources/crons.d.ts +15 -0
- package/dist/resources/crons.js +33 -0
- package/dist/resources/custom-objects.d.ts +20 -0
- package/dist/resources/custom-objects.js +84 -0
- package/dist/resources/files.d.ts +34 -0
- package/dist/resources/files.js +34 -0
- package/dist/resources/forward.d.ts +7 -0
- package/dist/resources/forward.js +64 -0
- package/dist/resources/human/inbox.d.ts +14 -0
- package/dist/resources/human/inbox.js +30 -0
- package/dist/resources/human/requests.d.ts +13 -0
- package/dist/resources/human/requests.js +26 -0
- package/dist/resources/human.d.ts +8 -0
- package/dist/resources/human.js +10 -0
- package/dist/resources/identifiers.d.ts +8 -0
- package/dist/resources/identifiers.js +33 -0
- package/dist/resources/memory.d.ts +69 -0
- package/dist/resources/memory.js +30 -0
- package/dist/resources/models/config.d.ts +8 -0
- package/dist/resources/models/config.js +9 -0
- package/dist/resources/models/credentials.d.ts +10 -0
- package/dist/resources/models/credentials.js +17 -0
- package/dist/resources/models.d.ts +8 -0
- package/dist/resources/models.js +10 -0
- package/dist/resources/node-stream.d.ts +34 -0
- package/dist/resources/node-stream.js +176 -0
- package/dist/resources/nodes/jobs.d.ts +9 -0
- package/dist/resources/nodes/jobs.js +14 -0
- package/dist/resources/nodes/result.d.ts +8 -0
- package/dist/resources/nodes/result.js +11 -0
- package/dist/resources/nodes/worktree.d.ts +9 -0
- package/dist/resources/nodes/worktree.js +14 -0
- package/dist/resources/nodes.d.ts +23 -0
- package/dist/resources/nodes.js +47 -0
- package/dist/resources/providers.d.ts +51 -0
- package/dist/resources/providers.js +15 -0
- package/dist/resources/questions.d.ts +49 -0
- package/dist/resources/questions.js +21 -0
- package/dist/resources/request.d.ts +3 -0
- package/dist/resources/request.js +11 -0
- package/dist/resources/run-reply.d.ts +79 -0
- package/dist/resources/run-reply.js +84 -0
- package/dist/resources/run-stream.d.ts +40 -0
- package/dist/resources/run-stream.js +206 -0
- package/dist/resources/runs.d.ts +184 -0
- package/dist/resources/runs.js +197 -0
- package/dist/resources/shares.d.ts +40 -0
- package/dist/resources/shares.js +19 -0
- package/dist/resources/uploads.d.ts +27 -0
- package/dist/resources/uploads.js +11 -0
- package/dist/schema.d.ts +7 -0
- package/dist/schema.js +10 -0
- package/dist/stores/postgres.d.ts +29 -0
- package/dist/stores/postgres.js +86 -0
- package/dist/types.d.ts +80 -0
- package/dist/types.js +1 -0
- package/package.json +55 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { APIError } from '../errors.js';
|
|
2
|
+
import { RunStream } from './run-stream.js';
|
|
3
|
+
import { RunReply } from './run-reply.js';
|
|
4
|
+
import { resolveJsonSchema } from '../schema.js';
|
|
5
|
+
export class Runs {
|
|
6
|
+
request;
|
|
7
|
+
openEvents;
|
|
8
|
+
questions;
|
|
9
|
+
constructor(request, openEvents, questions) {
|
|
10
|
+
this.request = request;
|
|
11
|
+
this.openEvents = openEvents;
|
|
12
|
+
this.questions = questions;
|
|
13
|
+
}
|
|
14
|
+
async start(params, options = {}) {
|
|
15
|
+
const { idempotencyKey = crypto.randomUUID(), maxRetries = 2, ...rest } = options;
|
|
16
|
+
const body = params.output_schema === undefined ? params : { ...params, output_schema: resolveJsonSchema(params.output_schema) };
|
|
17
|
+
for (let attempt = 0;; attempt++) {
|
|
18
|
+
try {
|
|
19
|
+
return await this.request('POST', '/v1/runs', body, { ...rest, maxRetries: 0, headers: { ...rest.headers, 'Idempotency-Key': idempotencyKey } });
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
if (attempt >= maxRetries || !(error instanceof APIError) ||
|
|
23
|
+
!(error.retryable === true || error.code === 'transport_error' || error.code === 'daemon_request_interrupted' || error.code === 'connection_error' || error.code === 'request_timeout'))
|
|
24
|
+
throw error;
|
|
25
|
+
await pause(error.retryAfterS ? error.retryAfterS * 1_000 : 500 * 2 ** attempt, rest.signal);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
get(runId, options = {}) {
|
|
30
|
+
const { wait, ...rest } = options;
|
|
31
|
+
if (wait !== undefined && (!Number.isInteger(wait) || wait < 1 || wait > 25))
|
|
32
|
+
throw new RangeError('wait must be 1–25 seconds');
|
|
33
|
+
return this.request('GET', `${runPath(runId)}${query({ wait })}`, undefined, { ...rest, timeout: wait ? (rest.timeout ?? 30_000) + wait * 1_000 : rest.timeout });
|
|
34
|
+
}
|
|
35
|
+
list(filters = {}, options) {
|
|
36
|
+
return this.request('GET', `/v1/runs${query(filters)}`, undefined, options);
|
|
37
|
+
}
|
|
38
|
+
/** Every run across pages, most recently active first: `for await (const run of runs.listAll())`. Fetches
|
|
39
|
+
* the next page (`limit` per page) only as you iterate; `break` stops fetching. */
|
|
40
|
+
async *listAll(filters = {}, options) {
|
|
41
|
+
for (let after;;) {
|
|
42
|
+
const page = await this.list({ ...filters, ...(after === undefined ? {} : { after }) }, options);
|
|
43
|
+
yield* page.runs;
|
|
44
|
+
if (!page.next)
|
|
45
|
+
return;
|
|
46
|
+
after = page.next;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** Send a message to a run. `start_turn: true` (the default) answers it in its
|
|
50
|
+
* own turn: a running run receives it when its current turn ends, and a
|
|
51
|
+
* message sent before the first turn has started queues behind that turn,
|
|
52
|
+
* because a run's `runs.start` prompt is always its first turn's input.
|
|
53
|
+
* `start_turn: false` stores it without starting a turn; it is delivered as
|
|
54
|
+
* context at the start of the next turn, ahead of that turn's input. For a
|
|
55
|
+
* new run that is the first turn only if it is stored before the run claims
|
|
56
|
+
* its first turn's messages; to guarantee it, pass `message` to `runs.start`. */
|
|
57
|
+
async message(runId, message, options = {}) {
|
|
58
|
+
const { maxRetries = 2, start_turn, ...rest } = options;
|
|
59
|
+
for (let attempt = 0;; attempt++) {
|
|
60
|
+
try {
|
|
61
|
+
return await this.request('POST', `${runPath(runId)}/messages`, { message, ...(start_turn === undefined ? {} : { start_turn }) }, { ...rest, maxRetries: 0 });
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
if (attempt >= maxRetries || !(error instanceof APIError) || error.retryable !== true || error.status === 0)
|
|
65
|
+
throw error;
|
|
66
|
+
await pause(error.retryAfterS ? error.retryAfterS * 1_000 : 500 * 2 ** attempt, rest.signal);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
interrupt(runId, options = {}) {
|
|
71
|
+
const { children, ...rest } = options;
|
|
72
|
+
return this.request('POST', `${runPath(runId)}/interrupt`, children === undefined ? {} : { children }, rest);
|
|
73
|
+
}
|
|
74
|
+
cancel(runId, options = {}) {
|
|
75
|
+
const { keepSchedules, ...rest } = options;
|
|
76
|
+
return this.request('POST', `${runPath(runId)}/cancel`, keepSchedules === undefined ? {} : { keep_schedules: keepSchedules }, rest);
|
|
77
|
+
}
|
|
78
|
+
/** Delete the run, running or not: the runtime stops its turns and sandboxes itself, so there is no need to
|
|
79
|
+
* `cancel` first. Deleting a run that is already gone throws `NotFoundError` (404).
|
|
80
|
+
* `uploads_pending` counts the run's released uploads storage has not yet confirmed deleted; the runtime retries those. */
|
|
81
|
+
delete(runId, options) {
|
|
82
|
+
return this.request('DELETE', runPath(runId), undefined, options);
|
|
83
|
+
}
|
|
84
|
+
/** Rename the caller's own run. The name is trimmed, must not be empty, and is at most 200 characters. */
|
|
85
|
+
rename(runId, name, options) {
|
|
86
|
+
return this.request('POST', `${runPath(runId)}/rename`, { name }, options);
|
|
87
|
+
}
|
|
88
|
+
trace(runId, options = {}) {
|
|
89
|
+
const { format, ...rest } = options;
|
|
90
|
+
return this.request('GET', `${runPath(runId)}/trace${query({ format })}`, undefined, rest);
|
|
91
|
+
}
|
|
92
|
+
events(runId, options = {}) {
|
|
93
|
+
return new RunStream((after, signal) => this.openEvents(runId, after, signal), options.after, options.signal, options.onReconnect);
|
|
94
|
+
}
|
|
95
|
+
/** Follow the reply to one message: `sent` is what `runs.message` returned, or `runs.start`'s `message`. Reads the run's events after
|
|
96
|
+
* `sent.sequence_number`, keeps the root's, starts at the turn that delivers the message and ends after that
|
|
97
|
+
* turn's `turn.completed` (or the run's settlement). Several messages delivered in one turn share that turn's reply.
|
|
98
|
+
* An expired cursor throws `stream_gap`. */
|
|
99
|
+
reply(runId, sent, options = {}) {
|
|
100
|
+
if (sent.run_id !== runId)
|
|
101
|
+
throw new TypeError('sent belongs to a different run');
|
|
102
|
+
return new RunReply(() => this.events(runId, { after: sent.sequence_number, signal: options.signal }), sent);
|
|
103
|
+
}
|
|
104
|
+
stream(params, options = {}) {
|
|
105
|
+
const controller = new AbortController();
|
|
106
|
+
options.signal?.addEventListener('abort', () => controller.abort(), { once: true });
|
|
107
|
+
const started = this.start(params, options);
|
|
108
|
+
return new RunStream(async (after, signal) => {
|
|
109
|
+
const run = await started;
|
|
110
|
+
return this.openEvents(run.run_id, after, signal);
|
|
111
|
+
}, 0, controller.signal);
|
|
112
|
+
}
|
|
113
|
+
/** Long-polls `runs.get` until the run is `settled`, `waiting_on_user`, or `idle`, and returns that response
|
|
114
|
+
* unchanged (`result` is not parsed). Aborting `signal` rejects with its reason and leaves the run as it is. */
|
|
115
|
+
async wait(runId, options = {}) {
|
|
116
|
+
return abortable(options.signal, async () => {
|
|
117
|
+
for (;;) {
|
|
118
|
+
const result = await this.get(runId, { ...options, wait: 25 });
|
|
119
|
+
if (result.status === 'settled' || result.status === 'waiting_on_user' || result.status === 'idle')
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
/** Start a run with `output_schema` and wait for it to settle; resolves with the outcome and `output_parsed`
|
|
125
|
+
* typed to the schema. `onQuestion` answers each open question once; without it, a question rejects with
|
|
126
|
+
* `APIError` code `waiting_on_user`. A run that stops without settling rejects `run_idle` (untouched:
|
|
127
|
+
* send `runs.message` or `runs.cancel`). Aborting `signal` rejects with its reason; the run is never canceled. */
|
|
128
|
+
async parse(params, options = {}) {
|
|
129
|
+
const { onQuestion, idempotencyKey, ...rest } = options;
|
|
130
|
+
return abortable(rest.signal, () => this.parseRun(params, onQuestion, idempotencyKey, rest));
|
|
131
|
+
}
|
|
132
|
+
async parseRun(params, onQuestion, idempotencyKey, rest) {
|
|
133
|
+
const run = await this.start(params, { ...rest, ...(idempotencyKey === undefined ? {} : { idempotencyKey }) });
|
|
134
|
+
const answered = new Set();
|
|
135
|
+
for (;;) {
|
|
136
|
+
const current = await this.wait(run.run_id, rest);
|
|
137
|
+
if (current.status === 'settled' && current.outcome) {
|
|
138
|
+
const outcome = current.outcome;
|
|
139
|
+
if (outcome.kind === 'result')
|
|
140
|
+
return { ...outcome, output_parsed: current.result };
|
|
141
|
+
return { ...outcome, output_parsed: null };
|
|
142
|
+
}
|
|
143
|
+
if (current.status === 'idle') {
|
|
144
|
+
throw new APIError(0, 'run_idle', 'the run stopped without settling; send runs.message or runs.cancel', undefined, undefined, { retryable: false, run_id: run.run_id });
|
|
145
|
+
}
|
|
146
|
+
const open = current.pending_question_ids.filter((id) => !answered.has(id));
|
|
147
|
+
if (!onQuestion) {
|
|
148
|
+
const questionId = open[0] ?? current.pending_question_ids[0];
|
|
149
|
+
throw new APIError(0, 'waiting_on_user', 'the run is waiting on a question and no onQuestion was given', undefined, undefined, { retryable: false, run_id: run.run_id, ...(questionId === undefined ? {} : { question_id: questionId }) });
|
|
150
|
+
}
|
|
151
|
+
if (!this.questions)
|
|
152
|
+
throw new TypeError('runs.parse needs the client questions resource to answer a question');
|
|
153
|
+
for (const id of open) {
|
|
154
|
+
const question = await this.questions.get(id, rest);
|
|
155
|
+
const responses = await onQuestion(question);
|
|
156
|
+
answered.add(id);
|
|
157
|
+
await this.questions.answer(id, responses, rest);
|
|
158
|
+
}
|
|
159
|
+
// Every listed question already answered: the run has not moved on yet, so poll again after a pause.
|
|
160
|
+
if (open.length === 0)
|
|
161
|
+
await pause(500, rest.signal);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function runPath(id) {
|
|
166
|
+
if (!/^[A-Za-z0-9_-]+$/.test(id))
|
|
167
|
+
throw new TypeError('Invalid run id');
|
|
168
|
+
return `/v1/runs/${encodeURIComponent(id)}`;
|
|
169
|
+
}
|
|
170
|
+
function query(fields) {
|
|
171
|
+
const params = new URLSearchParams();
|
|
172
|
+
for (const [key, value] of Object.entries(fields))
|
|
173
|
+
if (value !== undefined)
|
|
174
|
+
params.set(key, String(value));
|
|
175
|
+
return params.size ? `?${params}` : '';
|
|
176
|
+
}
|
|
177
|
+
/** SDK-RUN-27: an aborted helper rejects with the signal's own reason, whatever the in-flight request raised. */
|
|
178
|
+
async function abortable(signal, run) {
|
|
179
|
+
if (signal?.aborted)
|
|
180
|
+
throw signal.reason;
|
|
181
|
+
try {
|
|
182
|
+
return await run();
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
if (signal?.aborted)
|
|
186
|
+
throw signal.reason;
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
async function pause(ms, signal) {
|
|
191
|
+
if (signal?.aborted)
|
|
192
|
+
throw signal.reason;
|
|
193
|
+
await new Promise((resolve, reject) => {
|
|
194
|
+
const timer = setTimeout(resolve, ms);
|
|
195
|
+
signal?.addEventListener('abort', () => { clearTimeout(timer); reject(signal.reason); }, { once: true });
|
|
196
|
+
});
|
|
197
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { RequestOptions } from '../types.js';
|
|
2
|
+
import type { Request } from './request.js';
|
|
3
|
+
/** `shares.create` input. */
|
|
4
|
+
export interface ShareCreateParams {
|
|
5
|
+
/** Absolute path of a file the caller may read. Without `crtr:files:share`, only files in the caller's own app space. */
|
|
6
|
+
path: string;
|
|
7
|
+
/** ISO 8601 time in the future; absent or null keeps the share until it is deleted. */
|
|
8
|
+
expires_at?: string | null;
|
|
9
|
+
}
|
|
10
|
+
/** A newly created share: a frozen copy of the file at a public URL. */
|
|
11
|
+
export interface ShareCreated {
|
|
12
|
+
share_id: string;
|
|
13
|
+
url: string;
|
|
14
|
+
expires_at: string | null;
|
|
15
|
+
}
|
|
16
|
+
/** A share the caller created. */
|
|
17
|
+
export interface Share {
|
|
18
|
+
share_id: string;
|
|
19
|
+
url: string;
|
|
20
|
+
source_path: string;
|
|
21
|
+
created_by: string;
|
|
22
|
+
created_at: string;
|
|
23
|
+
expires_at: string | null;
|
|
24
|
+
status: 'on' | 'off';
|
|
25
|
+
}
|
|
26
|
+
/** Public share links (app listener only). List and delete act on the caller's own shares. */
|
|
27
|
+
export declare class Shares {
|
|
28
|
+
private readonly request;
|
|
29
|
+
constructor(request: Request);
|
|
30
|
+
/** Never retried: a retry could create a second share. */
|
|
31
|
+
create(params: ShareCreateParams, options?: RequestOptions): Promise<ShareCreated>;
|
|
32
|
+
list(options?: RequestOptions): Promise<{
|
|
33
|
+
shares: Share[];
|
|
34
|
+
}>;
|
|
35
|
+
/** Turn a share off and delete its stored copy. */
|
|
36
|
+
delete(shareId: string, options?: RequestOptions): Promise<{
|
|
37
|
+
share_id: string;
|
|
38
|
+
status: 'off';
|
|
39
|
+
}>;
|
|
40
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { pathSegment } from './identifiers.js';
|
|
2
|
+
/** Public share links (app listener only). List and delete act on the caller's own shares. */
|
|
3
|
+
export class Shares {
|
|
4
|
+
request;
|
|
5
|
+
constructor(request) {
|
|
6
|
+
this.request = request;
|
|
7
|
+
}
|
|
8
|
+
/** Never retried: a retry could create a second share. */
|
|
9
|
+
create(params, options = {}) {
|
|
10
|
+
return this.request('POST', '/v1/shares', params, { ...options, maxRetries: 0 });
|
|
11
|
+
}
|
|
12
|
+
list(options) {
|
|
13
|
+
return this.request('GET', '/v1/shares', undefined, options);
|
|
14
|
+
}
|
|
15
|
+
/** Turn a share off and delete its stored copy. */
|
|
16
|
+
delete(shareId, options) {
|
|
17
|
+
return this.request('DELETE', `/v1/shares/${encodeURIComponent(pathSegment(shareId, 'share id'))}`, undefined, options);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { RequestOptions } from '../types.js';
|
|
2
|
+
import type { Request } from './request.js';
|
|
3
|
+
/** `uploads.create` input: the file the caller will PUT to `upload_link`. At most 100 MiB. */
|
|
4
|
+
export interface UploadCreateParams {
|
|
5
|
+
/** File name, stored as the last segment of `url`. */
|
|
6
|
+
name: string;
|
|
7
|
+
/** The media type the bytes are PUT with. */
|
|
8
|
+
media_type: string;
|
|
9
|
+
/** Exact byte length, a non-negative integer. */
|
|
10
|
+
size: number;
|
|
11
|
+
}
|
|
12
|
+
/** A reserved upload. PUT the bytes to `upload_link` before `expires_at`, then put `url` in a run prompt or message. */
|
|
13
|
+
export interface Upload {
|
|
14
|
+
upload_id: string;
|
|
15
|
+
/** The storage URL a run's prompt or message names; the runtime fetches it into the run's `attachments/`. */
|
|
16
|
+
url: string;
|
|
17
|
+
/** Signed PUT link for the bytes. */
|
|
18
|
+
upload_link: string;
|
|
19
|
+
expires_at: string;
|
|
20
|
+
}
|
|
21
|
+
/** Uploads to the runtime's storage (app listener only). */
|
|
22
|
+
export declare class Uploads {
|
|
23
|
+
private readonly request;
|
|
24
|
+
constructor(request: Request);
|
|
25
|
+
/** Reserve an upload. Never retried: a retry would reserve a second upload. */
|
|
26
|
+
create(params: UploadCreateParams, options?: RequestOptions): Promise<Upload>;
|
|
27
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Uploads to the runtime's storage (app listener only). */
|
|
2
|
+
export class Uploads {
|
|
3
|
+
request;
|
|
4
|
+
constructor(request) {
|
|
5
|
+
this.request = request;
|
|
6
|
+
}
|
|
7
|
+
/** Reserve an upload. Never retried: a retry would reserve a second upload. */
|
|
8
|
+
create(params, options = {}) {
|
|
9
|
+
return this.request('POST', '/v1/uploads', params, { ...options, maxRetries: 0 });
|
|
10
|
+
}
|
|
11
|
+
}
|
package/dist/schema.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { JsonSchema } from './types.js';
|
|
2
|
+
/** Accept a plain JSON Schema object or anything with `toJSONSchema()` (a zod
|
|
3
|
+
* v4 object satisfies this directly) and resolve it to the plain object the
|
|
4
|
+
* wire format (`output_schema`, a JSON string) needs. */
|
|
5
|
+
export declare function resolveJsonSchema(schema: JsonSchema | {
|
|
6
|
+
toJSONSchema(): JsonSchema;
|
|
7
|
+
}): JsonSchema;
|
package/dist/schema.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Accept a plain JSON Schema object or anything with `toJSONSchema()` (a zod
|
|
2
|
+
* v4 object satisfies this directly) and resolve it to the plain object the
|
|
3
|
+
* wire format (`output_schema`, a JSON string) needs. */
|
|
4
|
+
export function resolveJsonSchema(schema) {
|
|
5
|
+
const withMethod = schema;
|
|
6
|
+
if (typeof withMethod.toJSONSchema === 'function') {
|
|
7
|
+
return schema.toJSONSchema();
|
|
8
|
+
}
|
|
9
|
+
return schema;
|
|
10
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Connection, ConnectionStore } from '../oauth/index.js';
|
|
2
|
+
interface QueryResult {
|
|
3
|
+
rows: Record<string, unknown>[];
|
|
4
|
+
}
|
|
5
|
+
interface QueryClient {
|
|
6
|
+
query(sql: string, values?: unknown[]): Promise<QueryResult>;
|
|
7
|
+
release(): void;
|
|
8
|
+
}
|
|
9
|
+
export interface PostgresPool {
|
|
10
|
+
query(sql: string, values?: unknown[]): Promise<QueryResult>;
|
|
11
|
+
connect(): Promise<QueryClient>;
|
|
12
|
+
}
|
|
13
|
+
export interface PostgresConnectionStoreOptions {
|
|
14
|
+
/** Unquoted PostgreSQL identifier, optionally schema-qualified. Default: connection. */
|
|
15
|
+
table?: string;
|
|
16
|
+
}
|
|
17
|
+
/** A shared PostgreSQL connection store with a transaction-scoped, per-user advisory lock. */
|
|
18
|
+
export declare class PostgresConnectionStore implements ConnectionStore {
|
|
19
|
+
private readonly pool;
|
|
20
|
+
private readonly transaction;
|
|
21
|
+
private readonly table;
|
|
22
|
+
constructor(pool: PostgresPool, options?: PostgresConnectionStoreOptions);
|
|
23
|
+
lock<T>(userId: string, fn: () => Promise<T>): Promise<T>;
|
|
24
|
+
load(userId: string): Promise<Connection | null>;
|
|
25
|
+
save(connection: Connection): Promise<void>;
|
|
26
|
+
/** Connections not saved since `before`, oldest first. Save activity is not necessarily refresh activity. */
|
|
27
|
+
listStale(before: Date): Promise<Connection[]>;
|
|
28
|
+
}
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
function tableName(name) {
|
|
3
|
+
const parts = name.split('.');
|
|
4
|
+
if (parts.length < 1 || parts.length > 2 || parts.some((part) => !/^[a-z_][a-z_0-9]*$/.test(part))) {
|
|
5
|
+
throw new TypeError('table must be a lowercase PostgreSQL identifier, optionally schema-qualified');
|
|
6
|
+
}
|
|
7
|
+
return parts.map((part) => `"${part}"`).join('.');
|
|
8
|
+
}
|
|
9
|
+
function toConnection(row) {
|
|
10
|
+
const expiry = row.access_token_expires_at;
|
|
11
|
+
if (!(expiry instanceof Date) || !Number.isFinite(expiry.getTime())) {
|
|
12
|
+
throw new TypeError('access_token_expires_at must be a PostgreSQL timestamptz (pg Date)');
|
|
13
|
+
}
|
|
14
|
+
return {
|
|
15
|
+
userId: row.user_id,
|
|
16
|
+
runtimeUrl: row.runtime_url,
|
|
17
|
+
refreshToken: row.refresh_token,
|
|
18
|
+
grantId: row.grant_id,
|
|
19
|
+
accessToken: row.access_token,
|
|
20
|
+
accessTokenExpiresAt: expiry.getTime(),
|
|
21
|
+
...(row.id_token == null ? {} : { idToken: row.id_token }),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/** A shared PostgreSQL connection store with a transaction-scoped, per-user advisory lock. */
|
|
25
|
+
export class PostgresConnectionStore {
|
|
26
|
+
pool;
|
|
27
|
+
transaction = new AsyncLocalStorage();
|
|
28
|
+
table;
|
|
29
|
+
constructor(pool, options = {}) {
|
|
30
|
+
this.pool = pool;
|
|
31
|
+
this.table = tableName(options.table ?? 'connection');
|
|
32
|
+
}
|
|
33
|
+
async lock(userId, fn) {
|
|
34
|
+
const current = this.transaction.getStore();
|
|
35
|
+
if (current) {
|
|
36
|
+
await current.query('SELECT pg_advisory_xact_lock(hashtextextended($1, 0))', [userId]);
|
|
37
|
+
return fn();
|
|
38
|
+
}
|
|
39
|
+
const db = await this.pool.connect();
|
|
40
|
+
let begun = false;
|
|
41
|
+
try {
|
|
42
|
+
await db.query('BEGIN');
|
|
43
|
+
begun = true;
|
|
44
|
+
await db.query('SELECT pg_advisory_xact_lock(hashtextextended($1, 0))', [userId]);
|
|
45
|
+
const result = await this.transaction.run(db, fn);
|
|
46
|
+
await db.query('COMMIT');
|
|
47
|
+
begun = false;
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
if (begun) {
|
|
52
|
+
try {
|
|
53
|
+
await db.query('ROLLBACK');
|
|
54
|
+
}
|
|
55
|
+
catch { /* Preserve the original failure. */ }
|
|
56
|
+
}
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
db.release();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async load(userId) {
|
|
64
|
+
const result = await (this.transaction.getStore() ?? this.pool).query(`SELECT user_id, runtime_url, refresh_token, grant_id, access_token, access_token_expires_at, id_token FROM ${this.table} WHERE user_id = $1`, [userId]);
|
|
65
|
+
return result.rows[0] ? toConnection(result.rows[0]) : null;
|
|
66
|
+
}
|
|
67
|
+
async save(connection) {
|
|
68
|
+
const expiry = new Date(connection.accessTokenExpiresAt);
|
|
69
|
+
if (!Number.isFinite(expiry.getTime()))
|
|
70
|
+
throw new TypeError('accessTokenExpiresAt must be a valid timestamp');
|
|
71
|
+
await (this.transaction.getStore() ?? this.pool).query(`INSERT INTO ${this.table} (user_id, runtime_url, refresh_token, grant_id, access_token, access_token_expires_at, id_token, updated_at)
|
|
72
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, clock_timestamp())
|
|
73
|
+
ON CONFLICT (user_id) DO UPDATE SET runtime_url = EXCLUDED.runtime_url, refresh_token = EXCLUDED.refresh_token,
|
|
74
|
+
grant_id = EXCLUDED.grant_id, access_token = EXCLUDED.access_token,
|
|
75
|
+
access_token_expires_at = EXCLUDED.access_token_expires_at, id_token = EXCLUDED.id_token, updated_at = clock_timestamp()`, [connection.userId, connection.runtimeUrl, connection.refreshToken, connection.grantId,
|
|
76
|
+
connection.accessToken, expiry, connection.idToken ?? null]);
|
|
77
|
+
}
|
|
78
|
+
/** Connections not saved since `before`, oldest first. Save activity is not necessarily refresh activity. */
|
|
79
|
+
async listStale(before) {
|
|
80
|
+
if (!Number.isFinite(before.getTime()))
|
|
81
|
+
throw new TypeError('before must be a valid Date');
|
|
82
|
+
const result = await (this.transaction.getStore() ?? this.pool).query(`SELECT user_id, runtime_url, refresh_token, grant_id, access_token, access_token_expires_at, id_token
|
|
83
|
+
FROM ${this.table} WHERE updated_at < $1 ORDER BY updated_at ASC, user_id ASC`, [before]);
|
|
84
|
+
return result.rows.map(toConnection);
|
|
85
|
+
}
|
|
86
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { CloseRequest, CreateNodeRequest, EnsureProfileRequest, NodeEventsQuery, ModelAuthReadinessDTO, ModelAuthReadinessQuery, NodeOutcomeDTO, SendMessageRequest, StartupPhaseDTO } from '@crouter/api';
|
|
2
|
+
/** A JSON Schema object. The daemon validates structured results against it. */
|
|
3
|
+
export type JsonSchema = Record<string, unknown>;
|
|
4
|
+
/**
|
|
5
|
+
* A structured-result schema: a JSON string, a JSON Schema object, or anything exposing
|
|
6
|
+
* `toJSONSchema()` — which a Zod v4 object does.
|
|
7
|
+
*/
|
|
8
|
+
export type OutputSchema = string | JsonSchema | {
|
|
9
|
+
toJSONSchema(): JsonSchema;
|
|
10
|
+
};
|
|
11
|
+
/** The daemon's create request, widening `output_schema` to accept schema objects. */
|
|
12
|
+
export type NodeCreateParams = Omit<CreateNodeRequest, 'output_schema'> & {
|
|
13
|
+
output_schema?: OutputSchema;
|
|
14
|
+
};
|
|
15
|
+
/** The final argument of every request-capable method. */
|
|
16
|
+
export interface RequestOptions {
|
|
17
|
+
/** Merged into this request's headers. */
|
|
18
|
+
headers?: Record<string, string>;
|
|
19
|
+
/** Aborts the request, rejecting with `APIUserAbortError`. */
|
|
20
|
+
signal?: AbortSignal;
|
|
21
|
+
/** Wall clock in milliseconds for this request, overriding the client's default. */
|
|
22
|
+
timeout?: number;
|
|
23
|
+
/** Transient-failure retries for this request. Never applied to `POST` or `PATCH`. */
|
|
24
|
+
maxRetries?: number;
|
|
25
|
+
}
|
|
26
|
+
/** Options for one outcome poll. */
|
|
27
|
+
export interface NodeOutcomeOptions extends RequestOptions {
|
|
28
|
+
/** One daemon long poll in seconds, from 0 through 25. */
|
|
29
|
+
wait?: number;
|
|
30
|
+
}
|
|
31
|
+
/** Options for an existing node's event stream. Streams have no wall-clock timeout. */
|
|
32
|
+
export interface NodeEventsOptions extends Omit<RequestOptions, 'timeout'>, NodeEventsQuery {
|
|
33
|
+
}
|
|
34
|
+
/** A message to a running node. The sender is filled in by the client. */
|
|
35
|
+
export type MessageParams = Omit<SendMessageRequest, 'from'>;
|
|
36
|
+
/** What `nodes.cancel` closes, including whether to cascade to descendants. */
|
|
37
|
+
export type CancelParams = CloseRequest;
|
|
38
|
+
/** The outcome of a `parse` run, with the structured result typed from the schema. */
|
|
39
|
+
export type ParsedOutcome<T> = (Extract<NodeOutcomeDTO, {
|
|
40
|
+
kind: 'result';
|
|
41
|
+
}> & {
|
|
42
|
+
output_parsed: T;
|
|
43
|
+
}) | (Exclude<NodeOutcomeDTO, {
|
|
44
|
+
kind: 'result';
|
|
45
|
+
}> & {
|
|
46
|
+
output_parsed: null;
|
|
47
|
+
});
|
|
48
|
+
/** The value type an `OutputSchema` describes, inferred from a Zod or Standard Schema object. */
|
|
49
|
+
export type SchemaOutput<T> = T extends {
|
|
50
|
+
_output: infer Output;
|
|
51
|
+
} ? Output : T extends {
|
|
52
|
+
'~standard': {
|
|
53
|
+
types?: {
|
|
54
|
+
output: infer Output;
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
} ? Output : unknown;
|
|
58
|
+
/** The profile settings `profiles.ensure` creates or updates. */
|
|
59
|
+
export type EnsureProfileParams = EnsureProfileRequest;
|
|
60
|
+
/** The launch-selecting fields used to resolve the provider readiness check. */
|
|
61
|
+
export type AuthStatusParams = ModelAuthReadinessQuery;
|
|
62
|
+
/** Whether this client can reach a daemon that can run a model, and what to do if it cannot. */
|
|
63
|
+
export interface AuthStatus {
|
|
64
|
+
/** The client holds a transport and the daemon answered. */
|
|
65
|
+
connected: boolean;
|
|
66
|
+
/** Present only when connected. */
|
|
67
|
+
daemon: {
|
|
68
|
+
base_url: string;
|
|
69
|
+
runtime_version: string;
|
|
70
|
+
api_version: string;
|
|
71
|
+
startup_phase: StartupPhaseDTO;
|
|
72
|
+
tcp: string | null;
|
|
73
|
+
} | null;
|
|
74
|
+
/** Present only when connected. */
|
|
75
|
+
model: ModelAuthReadinessDTO | null;
|
|
76
|
+
/** The one thing the application should do next, or null when ready. */
|
|
77
|
+
next_step: 'connect' | 'login' | null;
|
|
78
|
+
/** Literal text the application shows the user for `next_step`. */
|
|
79
|
+
instructions: string | null;
|
|
80
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@crouter/sdk",
|
|
3
|
+
"version": "0.3.377",
|
|
4
|
+
"description": "Typed Node and browser client for running crouter agents through the crtrd /v1 API.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"crouter-sdk": "./dist/keygen-cli.js"
|
|
10
|
+
},
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.js"
|
|
15
|
+
},
|
|
16
|
+
"./errors": {
|
|
17
|
+
"types": "./dist/error-codes.d.ts",
|
|
18
|
+
"import": "./dist/error-codes.js"
|
|
19
|
+
},
|
|
20
|
+
"./stores/postgres": {
|
|
21
|
+
"types": "./dist/stores/postgres.d.ts",
|
|
22
|
+
"import": "./dist/stores/postgres.js"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"README.md"
|
|
28
|
+
],
|
|
29
|
+
"sideEffects": false,
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsc -p tsconfig.json && chmod 755 dist/keygen-cli.js"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@crouter/api": "^0.3.377",
|
|
35
|
+
"@crouter/identity": "^0.3.377",
|
|
36
|
+
"jose": "^6.2.1"
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"pg": "^8.0.0"
|
|
40
|
+
},
|
|
41
|
+
"peerDependenciesMeta": {
|
|
42
|
+
"pg": {
|
|
43
|
+
"optional": true
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
},
|
|
49
|
+
"repository": {
|
|
50
|
+
"type": "git",
|
|
51
|
+
"url": "git+https://github.com/crouton-labs/crouter.git",
|
|
52
|
+
"directory": "packages/crouter-sdk"
|
|
53
|
+
},
|
|
54
|
+
"license": "GPL-3.0-only"
|
|
55
|
+
}
|