@orkestrel/toolbox 0.0.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/LICENSE +21 -0
- package/README.md +53 -0
- package/dist/src/core/index.cjs +3375 -0
- package/dist/src/core/index.cjs.map +1 -0
- package/dist/src/core/index.d.cts +2651 -0
- package/dist/src/core/index.d.ts +2651 -0
- package/dist/src/core/index.js +3269 -0
- package/dist/src/core/index.js.map +1 -0
- package/dist/src/server/index.cjs +267 -0
- package/dist/src/server/index.cjs.map +1 -0
- package/dist/src/server/index.d.cts +167 -0
- package/dist/src/server/index.d.ts +167 -0
- package/dist/src/server/index.js +262 -0
- package/dist/src/server/index.js.map +1 -0
- package/package.json +105 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { HEADER_TOKEN, defaultTimer, isAnswerPayload, serializeExpire, serializePending } from "@orkestrel/terminal";
|
|
2
|
+
import { ContentTooLargeError, DEFAULT_BODY_LIMIT, collectRequestBody, openStream } from "@orkestrel/server";
|
|
3
|
+
//#region src/server/constants.ts
|
|
4
|
+
/**
|
|
5
|
+
* The default `:name`-templated path {@link import('./factories.js').createTerminalRoutes}
|
|
6
|
+
* mounts its GET (SSE) + POST (answer) routes under.
|
|
7
|
+
*/
|
|
8
|
+
var TERMINAL_ROUTES_PATH = "/terminals/:name";
|
|
9
|
+
/**
|
|
10
|
+
* The default SSE keepalive interval (in milliseconds)
|
|
11
|
+
* {@link import('./factories.js').createTerminalRoutes} arms per open connection — a `: `
|
|
12
|
+
* comment ping a conforming SSE parser ignores, keeping intermediary proxies from timing out an
|
|
13
|
+
* otherwise-idle stream.
|
|
14
|
+
*/
|
|
15
|
+
var TERMINAL_KEEPALIVE_MS = 15e3;
|
|
16
|
+
//#endregion
|
|
17
|
+
//#region src/server/routes/TerminalConnection.ts
|
|
18
|
+
/**
|
|
19
|
+
* Own one terminal SSE connection's replay, subscriptions, keepalive, and teardown.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* import { TerminalConnection } from '@orkestrel/toolbox/server'
|
|
24
|
+
*
|
|
25
|
+
* const connection = new TerminalConnection(manager, name, request, stream, accepts, timer, 15_000)
|
|
26
|
+
* const response = connection.open()
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
var TerminalConnection = class {
|
|
30
|
+
#manager;
|
|
31
|
+
#name;
|
|
32
|
+
#request;
|
|
33
|
+
#stream;
|
|
34
|
+
#accepts;
|
|
35
|
+
#timer;
|
|
36
|
+
#keepalive;
|
|
37
|
+
#presented;
|
|
38
|
+
#cancel;
|
|
39
|
+
#destroyHandler;
|
|
40
|
+
#pendingHandler;
|
|
41
|
+
#expireHandler;
|
|
42
|
+
#tickHandler;
|
|
43
|
+
/**
|
|
44
|
+
* Create a terminal stream connection.
|
|
45
|
+
*
|
|
46
|
+
* @param manager - Terminal manager supplying pending prompts and lifecycle events
|
|
47
|
+
* @param name - Terminal endpoint streamed by this connection
|
|
48
|
+
* @param request - Request whose abort signal owns the connection lifetime
|
|
49
|
+
* @param stream - Open SSE stream
|
|
50
|
+
* @param accepts - Presented-token validator
|
|
51
|
+
* @param timer - Keepalive timer implementation
|
|
52
|
+
* @param keepalive - Keepalive interval in milliseconds
|
|
53
|
+
*/
|
|
54
|
+
constructor(manager, name, request, stream, accepts, timer, keepalive) {
|
|
55
|
+
this.#manager = manager;
|
|
56
|
+
this.#name = name;
|
|
57
|
+
this.#request = request;
|
|
58
|
+
this.#stream = stream;
|
|
59
|
+
this.#accepts = accepts;
|
|
60
|
+
this.#timer = timer;
|
|
61
|
+
this.#keepalive = keepalive;
|
|
62
|
+
this.#presented = request.headers.get(HEADER_TOKEN) ?? void 0;
|
|
63
|
+
this.#destroyHandler = this.#destroy.bind(this);
|
|
64
|
+
this.#pendingHandler = this.#pending.bind(this);
|
|
65
|
+
this.#expireHandler = this.#expire.bind(this);
|
|
66
|
+
this.#tickHandler = this.#tick.bind(this);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Open the connection by replaying pending prompts, subscribing, and arming keepalive handling.
|
|
70
|
+
*
|
|
71
|
+
* @returns The SSE response
|
|
72
|
+
*/
|
|
73
|
+
open() {
|
|
74
|
+
if (this.#stream.closed || this.#request.signal.aborted) {
|
|
75
|
+
this.#destroy();
|
|
76
|
+
return this.#stream.response;
|
|
77
|
+
}
|
|
78
|
+
if (this.#cancel !== void 0) return this.#stream.response;
|
|
79
|
+
for (const prompt of this.#manager.pending(this.#name)) this.#write(serializePending(prompt));
|
|
80
|
+
this.#manager.emitter.on("pending", this.#pendingHandler);
|
|
81
|
+
this.#manager.emitter.on("expire", this.#expireHandler);
|
|
82
|
+
this.#cancel = this.#timer(this.#tickHandler, this.#keepalive);
|
|
83
|
+
this.#request.signal.addEventListener("abort", this.#destroyHandler);
|
|
84
|
+
return this.#stream.response;
|
|
85
|
+
}
|
|
86
|
+
#write(wire) {
|
|
87
|
+
this.#stream.write({
|
|
88
|
+
event: wire.event,
|
|
89
|
+
data: wire.data,
|
|
90
|
+
...wire.id === void 0 ? {} : { id: wire.id }
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
#pending(prompt) {
|
|
94
|
+
if (prompt.to !== this.#name) return;
|
|
95
|
+
if (this.#stream.closed) {
|
|
96
|
+
this.#destroy();
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
this.#write(serializePending(prompt));
|
|
100
|
+
}
|
|
101
|
+
#expire(to, id) {
|
|
102
|
+
if (to !== this.#name) return;
|
|
103
|
+
if (this.#stream.closed) {
|
|
104
|
+
this.#destroy();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
this.#write(serializeExpire(id));
|
|
108
|
+
}
|
|
109
|
+
#tick() {
|
|
110
|
+
if (this.#stream.closed || !this.#accepted()) {
|
|
111
|
+
this.#destroy();
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
this.#stream.comment("");
|
|
115
|
+
this.#cancel = this.#timer(this.#tickHandler, this.#keepalive);
|
|
116
|
+
}
|
|
117
|
+
#accepted() {
|
|
118
|
+
try {
|
|
119
|
+
return this.#accepts(this.#presented);
|
|
120
|
+
} catch {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
#destroy() {
|
|
125
|
+
const cancel = this.#cancel;
|
|
126
|
+
this.#cancel = void 0;
|
|
127
|
+
cancel?.();
|
|
128
|
+
this.#manager.emitter.off("pending", this.#pendingHandler);
|
|
129
|
+
this.#manager.emitter.off("expire", this.#expireHandler);
|
|
130
|
+
this.#request.signal.removeEventListener("abort", this.#destroyHandler);
|
|
131
|
+
this.#stream.end();
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
//#endregion
|
|
135
|
+
//#region src/server/routes/TerminalRoutes.ts
|
|
136
|
+
/**
|
|
137
|
+
* Build and serve the terminal manager's GET stream and POST answer routes.
|
|
138
|
+
*
|
|
139
|
+
* @example
|
|
140
|
+
* ```ts
|
|
141
|
+
* import { TerminalRoutes } from '@orkestrel/toolbox/server'
|
|
142
|
+
*
|
|
143
|
+
* const routes = new TerminalRoutes(manager).routes()
|
|
144
|
+
* ```
|
|
145
|
+
*/
|
|
146
|
+
var TerminalRoutes = class {
|
|
147
|
+
#manager;
|
|
148
|
+
#path;
|
|
149
|
+
#token;
|
|
150
|
+
#keepalive;
|
|
151
|
+
#timer;
|
|
152
|
+
#limit;
|
|
153
|
+
#accepts;
|
|
154
|
+
#get;
|
|
155
|
+
#post;
|
|
156
|
+
/**
|
|
157
|
+
* Create a terminal route owner.
|
|
158
|
+
*
|
|
159
|
+
* @param manager - Terminal manager bridged onto HTTP
|
|
160
|
+
* @param options - Shared route, authorization, keepalive, timer, and body-limit options
|
|
161
|
+
*/
|
|
162
|
+
constructor(manager, options) {
|
|
163
|
+
this.#manager = manager;
|
|
164
|
+
this.#path = options?.path ?? "/terminals/:name";
|
|
165
|
+
this.#token = options?.token;
|
|
166
|
+
this.#keepalive = options?.keepalive ?? 15e3;
|
|
167
|
+
this.#timer = options?.timer ?? defaultTimer;
|
|
168
|
+
const limit = options?.limit;
|
|
169
|
+
this.#limit = limit === void 0 || !Number.isFinite(limit) ? DEFAULT_BODY_LIMIT : Math.max(0, Math.floor(limit));
|
|
170
|
+
this.#accepts = this.#valid.bind(this);
|
|
171
|
+
this.#get = this.#handleGet.bind(this);
|
|
172
|
+
this.#post = this.#handlePost.bind(this);
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Project the bound GET and POST route records.
|
|
176
|
+
*
|
|
177
|
+
* @returns The GET stream route followed by the POST answer route
|
|
178
|
+
*/
|
|
179
|
+
routes() {
|
|
180
|
+
return [{
|
|
181
|
+
method: "GET",
|
|
182
|
+
path: this.#path,
|
|
183
|
+
handler: this.#get
|
|
184
|
+
}, {
|
|
185
|
+
method: "POST",
|
|
186
|
+
path: this.#path,
|
|
187
|
+
handler: this.#post
|
|
188
|
+
}];
|
|
189
|
+
}
|
|
190
|
+
#valid(presented) {
|
|
191
|
+
if (this.#token === void 0) return true;
|
|
192
|
+
try {
|
|
193
|
+
return typeof this.#token === "function" ? this.#token(presented) : presented === this.#token;
|
|
194
|
+
} catch {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
#authorized(request) {
|
|
199
|
+
return this.#valid(request.headers.get(HEADER_TOKEN) ?? void 0);
|
|
200
|
+
}
|
|
201
|
+
#handleGet(request, context) {
|
|
202
|
+
if (!this.#authorized(request)) return new Response(null, { status: 401 });
|
|
203
|
+
const name = context.params.name;
|
|
204
|
+
if (name === void 0 || this.#manager.terminal(name) === void 0) return new Response(null, { status: 404 });
|
|
205
|
+
return new TerminalConnection(this.#manager, name, request, openStream(), this.#accepts, this.#timer, this.#keepalive).open();
|
|
206
|
+
}
|
|
207
|
+
async #handlePost(request, context) {
|
|
208
|
+
if (!this.#authorized(request)) return new Response(null, { status: 401 });
|
|
209
|
+
const name = context.params.name;
|
|
210
|
+
if (name === void 0 || this.#manager.terminal(name) === void 0) return new Response(null, { status: 404 });
|
|
211
|
+
let bytes;
|
|
212
|
+
try {
|
|
213
|
+
bytes = await collectRequestBody(request, Math.max(1, this.#limit));
|
|
214
|
+
} catch (error) {
|
|
215
|
+
if (error instanceof ContentTooLargeError) return new Response(null, { status: 413 });
|
|
216
|
+
throw error;
|
|
217
|
+
}
|
|
218
|
+
if (bytes.byteLength > 0 && bytes.byteLength > this.#limit) return new Response(null, { status: 413 });
|
|
219
|
+
let body;
|
|
220
|
+
try {
|
|
221
|
+
body = JSON.parse(new TextDecoder().decode(bytes));
|
|
222
|
+
} catch {
|
|
223
|
+
return new Response(null, { status: 400 });
|
|
224
|
+
}
|
|
225
|
+
if (!isAnswerPayload(body)) return new Response(null, { status: 422 });
|
|
226
|
+
const result = this.#manager.answer(name, body.id, body.value);
|
|
227
|
+
if (result.success) return new Response(null, { status: 204 });
|
|
228
|
+
if (result.error === "terminal") return new Response(result.error, { status: 404 });
|
|
229
|
+
return new Response(result.error, { status: 422 });
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
//#endregion
|
|
233
|
+
//#region src/server/factories.ts
|
|
234
|
+
/**
|
|
235
|
+
* Build the GET SSE stream and POST answer routes that bridge a terminal manager onto the wire.
|
|
236
|
+
*
|
|
237
|
+
* @remarks
|
|
238
|
+
* Both routes share the configured `:name` path and optional token gate. The GET route replays
|
|
239
|
+
* pending prompts, forwards live pending/expire events, and owns abort/keepalive teardown. The
|
|
240
|
+
* POST route bounds the request body before parsing and maps answer outcomes to HTTP statuses.
|
|
241
|
+
*
|
|
242
|
+
* @param manager - The terminal manager whose endpoints are bridged
|
|
243
|
+
* @param options - Route path, token, keepalive, timer, and body-limit options
|
|
244
|
+
* @returns The GET route followed by the POST route
|
|
245
|
+
*
|
|
246
|
+
* @example
|
|
247
|
+
* ```ts
|
|
248
|
+
* import { createTerminalRoutes } from '@src/server'
|
|
249
|
+
* import { createTerminalManager } from '@orkestrel/terminal'
|
|
250
|
+
*
|
|
251
|
+
* const manager = createTerminalManager()
|
|
252
|
+
* manager.add('assistant')
|
|
253
|
+
* const routes = createTerminalRoutes(manager, { token: 'secret' })
|
|
254
|
+
* ```
|
|
255
|
+
*/
|
|
256
|
+
function createTerminalRoutes(manager, options) {
|
|
257
|
+
return new TerminalRoutes(manager, options).routes();
|
|
258
|
+
}
|
|
259
|
+
//#endregion
|
|
260
|
+
export { TERMINAL_KEEPALIVE_MS, TERMINAL_ROUTES_PATH, TerminalConnection, TerminalRoutes, createTerminalRoutes };
|
|
261
|
+
|
|
262
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":["#manager","#name","#request","#stream","#accepts","#timer","#keepalive","#presented","#destroyHandler","#pendingHandler","#expireHandler","#tickHandler","#destroy","#pending","#expire","#tick","#cancel","#write","#accepted","#manager","#path","#token","#keepalive","#timer","#limit","#accepts","#get","#post","#valid","#handleGet","#handlePost","#authorized"],"sources":["../../../src/server/constants.ts","../../../src/server/routes/TerminalConnection.ts","../../../src/server/routes/TerminalRoutes.ts","../../../src/server/factories.ts"],"sourcesContent":["// Server-package constants — UPPER_SNAKE, `Object.freeze`d where structural, every member\n// exported (AGENTS §5).\n\n/**\n * The default `:name`-templated path {@link import('./factories.js').createTerminalRoutes}\n * mounts its GET (SSE) + POST (answer) routes under.\n */\nexport const TERMINAL_ROUTES_PATH = '/terminals/:name'\n\n/**\n * The default SSE keepalive interval (in milliseconds)\n * {@link import('./factories.js').createTerminalRoutes} arms per open connection — a `: `\n * comment ping a conforming SSE parser ignores, keeping intermediary proxies from timing out an\n * otherwise-idle stream.\n */\nexport const TERMINAL_KEEPALIVE_MS = 15_000\n","import type {\n\tPendingPrompt,\n\tTerminalManagerInterface,\n\tTimerCancel,\n\tTimerHandler,\n\tWireEvent,\n} from '@orkestrel/terminal'\nimport type { StreamInterface } from '@orkestrel/server'\nimport { HEADER_TOKEN, serializeExpire, serializePending } from '@orkestrel/terminal'\n\n/**\n * Own one terminal SSE connection's replay, subscriptions, keepalive, and teardown.\n *\n * @example\n * ```ts\n * import { TerminalConnection } from '@orkestrel/toolbox/server'\n *\n * const connection = new TerminalConnection(manager, name, request, stream, accepts, timer, 15_000)\n * const response = connection.open()\n * ```\n */\nexport class TerminalConnection {\n\treadonly #manager: TerminalManagerInterface\n\treadonly #name: string\n\treadonly #request: Request\n\treadonly #stream: StreamInterface\n\treadonly #accepts: (presented: string | undefined) => boolean\n\treadonly #timer: TimerHandler\n\treadonly #keepalive: number\n\treadonly #presented: string | undefined\n\t#cancel: TimerCancel | undefined\n\treadonly #destroyHandler: () => void\n\treadonly #pendingHandler: (prompt: PendingPrompt) => void\n\treadonly #expireHandler: (to: string, id: string) => void\n\treadonly #tickHandler: () => void\n\n\t/**\n\t * Create a terminal stream connection.\n\t *\n\t * @param manager - Terminal manager supplying pending prompts and lifecycle events\n\t * @param name - Terminal endpoint streamed by this connection\n\t * @param request - Request whose abort signal owns the connection lifetime\n\t * @param stream - Open SSE stream\n\t * @param accepts - Presented-token validator\n\t * @param timer - Keepalive timer implementation\n\t * @param keepalive - Keepalive interval in milliseconds\n\t */\n\tconstructor(\n\t\tmanager: TerminalManagerInterface,\n\t\tname: string,\n\t\trequest: Request,\n\t\tstream: StreamInterface,\n\t\taccepts: (presented: string | undefined) => boolean,\n\t\ttimer: TimerHandler,\n\t\tkeepalive: number,\n\t) {\n\t\tthis.#manager = manager\n\t\tthis.#name = name\n\t\tthis.#request = request\n\t\tthis.#stream = stream\n\t\tthis.#accepts = accepts\n\t\tthis.#timer = timer\n\t\tthis.#keepalive = keepalive\n\t\tthis.#presented = request.headers.get(HEADER_TOKEN) ?? undefined\n\t\tthis.#destroyHandler = this.#destroy.bind(this)\n\t\tthis.#pendingHandler = this.#pending.bind(this)\n\t\tthis.#expireHandler = this.#expire.bind(this)\n\t\tthis.#tickHandler = this.#tick.bind(this)\n\t}\n\n\t/**\n\t * Open the connection by replaying pending prompts, subscribing, and arming keepalive handling.\n\t *\n\t * @returns The SSE response\n\t */\n\topen(): Response {\n\t\tif (this.#stream.closed || this.#request.signal.aborted) {\n\t\t\tthis.#destroy()\n\t\t\treturn this.#stream.response\n\t\t}\n\t\tif (this.#cancel !== undefined) return this.#stream.response\n\t\tfor (const prompt of this.#manager.pending(this.#name)) {\n\t\t\tthis.#write(serializePending(prompt))\n\t\t}\n\t\tthis.#manager.emitter.on('pending', this.#pendingHandler)\n\t\tthis.#manager.emitter.on('expire', this.#expireHandler)\n\t\tthis.#cancel = this.#timer(this.#tickHandler, this.#keepalive)\n\t\tthis.#request.signal.addEventListener('abort', this.#destroyHandler)\n\t\treturn this.#stream.response\n\t}\n\n\t#write(wire: WireEvent): void {\n\t\tthis.#stream.write({\n\t\t\tevent: wire.event,\n\t\t\tdata: wire.data,\n\t\t\t...(wire.id === undefined ? {} : { id: wire.id }),\n\t\t})\n\t}\n\n\t#pending(prompt: PendingPrompt): void {\n\t\tif (prompt.to !== this.#name) return\n\t\tif (this.#stream.closed) {\n\t\t\tthis.#destroy()\n\t\t\treturn\n\t\t}\n\t\tthis.#write(serializePending(prompt))\n\t}\n\n\t#expire(to: string, id: string): void {\n\t\tif (to !== this.#name) return\n\t\tif (this.#stream.closed) {\n\t\t\tthis.#destroy()\n\t\t\treturn\n\t\t}\n\t\tthis.#write(serializeExpire(id))\n\t}\n\n\t#tick(): void {\n\t\tif (this.#stream.closed || !this.#accepted()) {\n\t\t\tthis.#destroy()\n\t\t\treturn\n\t\t}\n\t\tthis.#stream.comment('')\n\t\tthis.#cancel = this.#timer(this.#tickHandler, this.#keepalive)\n\t}\n\n\t#accepted(): boolean {\n\t\ttry {\n\t\t\treturn this.#accepts(this.#presented)\n\t\t} catch {\n\t\t\treturn false\n\t\t}\n\t}\n\n\t#destroy(): void {\n\t\tconst cancel = this.#cancel\n\t\tthis.#cancel = undefined\n\t\tcancel?.()\n\t\tthis.#manager.emitter.off('pending', this.#pendingHandler)\n\t\tthis.#manager.emitter.off('expire', this.#expireHandler)\n\t\tthis.#request.signal.removeEventListener('abort', this.#destroyHandler)\n\t\tthis.#stream.end()\n\t}\n}\n","import type { TerminalManagerInterface, TimerHandler } from '@orkestrel/terminal'\nimport type {\n\tTerminalRoute,\n\tTerminalRouteContext,\n\tTerminalRoutesOptions,\n\tTerminalToken,\n} from '../types.js'\nimport { defaultTimer, HEADER_TOKEN, isAnswerPayload } from '@orkestrel/terminal'\nimport {\n\tcollectRequestBody,\n\tContentTooLargeError,\n\tDEFAULT_BODY_LIMIT,\n\topenStream,\n} from '@orkestrel/server'\nimport { TERMINAL_KEEPALIVE_MS, TERMINAL_ROUTES_PATH } from '../constants.js'\nimport { TerminalConnection } from './TerminalConnection.js'\n\n/**\n * Build and serve the terminal manager's GET stream and POST answer routes.\n *\n * @example\n * ```ts\n * import { TerminalRoutes } from '@orkestrel/toolbox/server'\n *\n * const routes = new TerminalRoutes(manager).routes()\n * ```\n */\nexport class TerminalRoutes {\n\treadonly #manager: TerminalManagerInterface\n\treadonly #path: string\n\treadonly #token: TerminalToken | undefined\n\treadonly #keepalive: number\n\treadonly #timer: TimerHandler\n\treadonly #limit: number\n\treadonly #accepts: (presented: string | undefined) => boolean\n\treadonly #get: TerminalRoute['handler']\n\treadonly #post: TerminalRoute['handler']\n\n\t/**\n\t * Create a terminal route owner.\n\t *\n\t * @param manager - Terminal manager bridged onto HTTP\n\t * @param options - Shared route, authorization, keepalive, timer, and body-limit options\n\t */\n\tconstructor(manager: TerminalManagerInterface, options?: TerminalRoutesOptions) {\n\t\tthis.#manager = manager\n\t\tthis.#path = options?.path ?? TERMINAL_ROUTES_PATH\n\t\tthis.#token = options?.token\n\t\tthis.#keepalive = options?.keepalive ?? TERMINAL_KEEPALIVE_MS\n\t\tthis.#timer = options?.timer ?? defaultTimer\n\t\tconst limit = options?.limit\n\t\tthis.#limit =\n\t\t\tlimit === undefined || !Number.isFinite(limit)\n\t\t\t\t? DEFAULT_BODY_LIMIT\n\t\t\t\t: Math.max(0, Math.floor(limit))\n\t\tthis.#accepts = this.#valid.bind(this)\n\t\tthis.#get = this.#handleGet.bind(this)\n\t\tthis.#post = this.#handlePost.bind(this)\n\t}\n\n\t/**\n\t * Project the bound GET and POST route records.\n\t *\n\t * @returns The GET stream route followed by the POST answer route\n\t */\n\troutes(): readonly TerminalRoute[] {\n\t\treturn [\n\t\t\t{ method: 'GET', path: this.#path, handler: this.#get },\n\t\t\t{ method: 'POST', path: this.#path, handler: this.#post },\n\t\t]\n\t}\n\n\t#valid(presented: string | undefined): boolean {\n\t\tif (this.#token === undefined) return true\n\t\ttry {\n\t\t\treturn typeof this.#token === 'function' ? this.#token(presented) : presented === this.#token\n\t\t} catch {\n\t\t\treturn false\n\t\t}\n\t}\n\n\t#authorized(request: Request): boolean {\n\t\treturn this.#valid(request.headers.get(HEADER_TOKEN) ?? undefined)\n\t}\n\n\t#handleGet(request: Request, context: TerminalRouteContext): Response {\n\t\tif (!this.#authorized(request)) return new Response(null, { status: 401 })\n\t\tconst name = context.params.name\n\t\tif (name === undefined || this.#manager.terminal(name) === undefined) {\n\t\t\treturn new Response(null, { status: 404 })\n\t\t}\n\t\tconst connection = new TerminalConnection(\n\t\t\tthis.#manager,\n\t\t\tname,\n\t\t\trequest,\n\t\t\topenStream(),\n\t\t\tthis.#accepts,\n\t\t\tthis.#timer,\n\t\t\tthis.#keepalive,\n\t\t)\n\t\treturn connection.open()\n\t}\n\n\tasync #handlePost(request: Request, context: TerminalRouteContext): Promise<Response> {\n\t\tif (!this.#authorized(request)) return new Response(null, { status: 401 })\n\t\tconst name = context.params.name\n\t\tif (name === undefined || this.#manager.terminal(name) === undefined) {\n\t\t\treturn new Response(null, { status: 404 })\n\t\t}\n\t\tlet bytes: Uint8Array\n\t\ttry {\n\t\t\tbytes = await collectRequestBody(request, Math.max(1, this.#limit))\n\t\t} catch (error) {\n\t\t\tif (error instanceof ContentTooLargeError) return new Response(null, { status: 413 })\n\t\t\tthrow error\n\t\t}\n\t\tif (bytes.byteLength > 0 && bytes.byteLength > this.#limit) {\n\t\t\treturn new Response(null, { status: 413 })\n\t\t}\n\n\t\tlet body: unknown\n\t\ttry {\n\t\t\tbody = JSON.parse(new TextDecoder().decode(bytes))\n\t\t} catch {\n\t\t\treturn new Response(null, { status: 400 })\n\t\t}\n\t\tif (!isAnswerPayload(body)) return new Response(null, { status: 422 })\n\n\t\tconst result = this.#manager.answer(name, body.id, body.value)\n\t\tif (result.success) return new Response(null, { status: 204 })\n\t\tif (result.error === 'terminal') return new Response(result.error, { status: 404 })\n\t\treturn new Response(result.error, { status: 422 })\n\t}\n}\n","import type { TerminalManagerInterface } from '@orkestrel/terminal'\nimport type { TerminalRoute, TerminalRoutesOptions } from './types.js'\nimport { TerminalRoutes } from './routes/TerminalRoutes.js'\n\n/**\n * Build the GET SSE stream and POST answer routes that bridge a terminal manager onto the wire.\n *\n * @remarks\n * Both routes share the configured `:name` path and optional token gate. The GET route replays\n * pending prompts, forwards live pending/expire events, and owns abort/keepalive teardown. The\n * POST route bounds the request body before parsing and maps answer outcomes to HTTP statuses.\n *\n * @param manager - The terminal manager whose endpoints are bridged\n * @param options - Route path, token, keepalive, timer, and body-limit options\n * @returns The GET route followed by the POST route\n *\n * @example\n * ```ts\n * import { createTerminalRoutes } from '@src/server'\n * import { createTerminalManager } from '@orkestrel/terminal'\n *\n * const manager = createTerminalManager()\n * manager.add('assistant')\n * const routes = createTerminalRoutes(manager, { token: 'secret' })\n * ```\n */\nexport function createTerminalRoutes(\n\tmanager: TerminalManagerInterface,\n\toptions?: TerminalRoutesOptions,\n): readonly TerminalRoute[] {\n\treturn new TerminalRoutes(manager, options).routes()\n}\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@orkestrel/toolbox",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Concrete LLM-callable tools for the @orkestrel line — workflow authoring, workspace editing, sub-agent delegation, and more, over the @orkestrel/tool runtime, with pluggable stores. Part of the @orkestrel line.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"agent",
|
|
7
|
+
"llm",
|
|
8
|
+
"tool",
|
|
9
|
+
"toolbox",
|
|
10
|
+
"typescript",
|
|
11
|
+
"workflow",
|
|
12
|
+
"workspace"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/orkestrel/toolbox#readme",
|
|
15
|
+
"bugs": "https://github.com/orkestrel/toolbox/issues",
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/orkestrel/toolbox.git"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist/src",
|
|
23
|
+
"README.md"
|
|
24
|
+
],
|
|
25
|
+
"type": "module",
|
|
26
|
+
"sideEffects": false,
|
|
27
|
+
"main": "./dist/src/core/index.cjs",
|
|
28
|
+
"module": "./dist/src/core/index.js",
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"import": {
|
|
32
|
+
"types": "./dist/src/core/index.d.ts",
|
|
33
|
+
"default": "./dist/src/core/index.js"
|
|
34
|
+
},
|
|
35
|
+
"require": {
|
|
36
|
+
"types": "./dist/src/core/index.d.cts",
|
|
37
|
+
"default": "./dist/src/core/index.cjs"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"./server": {
|
|
41
|
+
"import": {
|
|
42
|
+
"types": "./dist/src/server/index.d.ts",
|
|
43
|
+
"default": "./dist/src/server/index.js"
|
|
44
|
+
},
|
|
45
|
+
"require": {
|
|
46
|
+
"types": "./dist/src/server/index.d.cts",
|
|
47
|
+
"default": "./dist/src/server/index.cjs"
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"./package.json": "./package.json"
|
|
51
|
+
},
|
|
52
|
+
"publishConfig": {
|
|
53
|
+
"access": "public"
|
|
54
|
+
},
|
|
55
|
+
"scripts": {
|
|
56
|
+
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
57
|
+
"copy": "node -e \"const fs=require('node:fs'),p=require('node:path'),a=process.argv[1],b=process.argv[2];fs.mkdirSync(p.dirname(b),{recursive:true});fs.cpSync(a,b,{force:true});console.log('Copied: '+a+' to '+b)\"",
|
|
58
|
+
"scaffold": "scaffold",
|
|
59
|
+
"lint": "oxlint --config .oxlintrc.json --fix --deny-warnings .",
|
|
60
|
+
"check": "tsc --noEmit --project tsconfig.json && npm run check:src",
|
|
61
|
+
"check:src": "npm run check:src:core && npm run check:src:server",
|
|
62
|
+
"check:src:core": "tsc --noEmit -p configs/src/tsconfig.core.json",
|
|
63
|
+
"check:src:server": "tsc --noEmit -p configs/src/tsconfig.server.json",
|
|
64
|
+
"format": "oxfmt --config .oxfmtrc.json --write .",
|
|
65
|
+
"format:check": "oxfmt --config .oxfmtrc.json --check .",
|
|
66
|
+
"lint:check": "oxlint --config .oxlintrc.json --deny-warnings .",
|
|
67
|
+
"test": "npm run test:src && npm run test:policy && npm run test:guides",
|
|
68
|
+
"test:src": "vitest run --config vite.config.ts --no-cache --reporter=dot --project src:core --project src:server",
|
|
69
|
+
"test:src:core": "vitest run --config vite.config.ts --no-cache --reporter=dot --project src:core",
|
|
70
|
+
"test:src:server": "vitest run --config vite.config.ts --no-cache --reporter=dot --project src:server",
|
|
71
|
+
"test:policy": "vitest run --config vite.config.ts --no-cache --reporter=dot --project policy",
|
|
72
|
+
"test:guides": "vitest run --config vite.config.ts --reporter=dot --project guides",
|
|
73
|
+
"build": "npm run clean && npm run build:src",
|
|
74
|
+
"build:src": "npm run build:src:core && npm run build:src:server",
|
|
75
|
+
"build:src:core": "vite build --config configs/src/vite.core.config.ts && npm run copy dist/src/core/index.d.ts dist/src/core/index.d.cts",
|
|
76
|
+
"build:src:server": "vite build --config configs/src/vite.server.config.ts && npm run copy dist/src/server/index.d.ts dist/src/server/index.d.cts",
|
|
77
|
+
"prepublishOnly": "npm run format:check && npm run lint:check && npm run check && npm run build && npm test"
|
|
78
|
+
},
|
|
79
|
+
"dependencies": {
|
|
80
|
+
"@orkestrel/agent": "^0.0.11",
|
|
81
|
+
"@orkestrel/contract": "^0.0.9",
|
|
82
|
+
"@orkestrel/database": "^0.0.6",
|
|
83
|
+
"@orkestrel/relation": "^0.0.6",
|
|
84
|
+
"@orkestrel/server": "^0.0.7",
|
|
85
|
+
"@orkestrel/terminal": "^0.0.5",
|
|
86
|
+
"@orkestrel/tool": "^0.0.7",
|
|
87
|
+
"@orkestrel/workflow": "^0.0.7"
|
|
88
|
+
},
|
|
89
|
+
"devDependencies": {
|
|
90
|
+
"@microsoft/api-extractor": "^7.58.12",
|
|
91
|
+
"@orkestrel/guide": "^0.0.8",
|
|
92
|
+
"@orkestrel/scaffold": "^0.0.12",
|
|
93
|
+
"@types/node": "^26.1.2",
|
|
94
|
+
"@vitest/browser-playwright": "^4.1.10",
|
|
95
|
+
"oxfmt": "^0.61.0",
|
|
96
|
+
"oxlint": "^1.76.0",
|
|
97
|
+
"typescript": "^6.0.3",
|
|
98
|
+
"vite": "^8.2.0",
|
|
99
|
+
"vite-plugin-dts": "^5.0.3",
|
|
100
|
+
"vitest": "^4.1.10"
|
|
101
|
+
},
|
|
102
|
+
"engines": {
|
|
103
|
+
"node": ">=22.12.0"
|
|
104
|
+
}
|
|
105
|
+
}
|