@or3/intern-client 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/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@or3/intern-client",
3
+ "version": "0.1.1",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "git+https://github.com/Saluana/or3-intern.git",
7
+ "directory": "clients/typescript"
8
+ },
9
+ "type": "module",
10
+ "private": false,
11
+ "sideEffects": false,
12
+ "main": "./dist/index.js",
13
+ "types": "./src/index.ts",
14
+ "files": [
15
+ "dist",
16
+ "src"
17
+ ],
18
+ "exports": {
19
+ ".": {
20
+ "types": "./src/index.ts",
21
+ "import": "./dist/index.js",
22
+ "default": "./dist/index.js"
23
+ }
24
+ },
25
+ "scripts": {
26
+ "build": "bun build ./src/index.ts --target=node --format=esm --outfile=./dist/index.js",
27
+ "test": "bun test",
28
+ "prepack": "bun run build"
29
+ },
30
+ "publishConfig": {
31
+ "access": "public",
32
+ "provenance": true
33
+ }
34
+ }
package/src/client.ts ADDED
@@ -0,0 +1,676 @@
1
+ import {
2
+ InternClientError,
3
+ InternUnavailableError,
4
+ } from './errors';
5
+ import {
6
+ parseInternActionAcknowledgement,
7
+ parseInternApprovalDecision,
8
+ parseInternApprovalList,
9
+ parseInternArtifact,
10
+ parseInternCapabilities,
11
+ parseInternEvent,
12
+ parseInternEventList,
13
+ parseInternHealth,
14
+ parseInternPairResult,
15
+ parseInternReadiness,
16
+ parseInternRecord,
17
+ parseInternRunnerList,
18
+ parseInternSession,
19
+ parseInternSessionList,
20
+ parseInternStartedTurn,
21
+ parseInternTurn,
22
+ parseInternTurnDecision,
23
+ parseInternTurnList,
24
+ type CreateInternSessionInput,
25
+ type InternActionAcknowledgement,
26
+ type InternApprovalDecision,
27
+ type InternApprovalDecisionAction,
28
+ type InternApprovalInput,
29
+ type InternApprovalList,
30
+ type InternArtifact,
31
+ type InternCapabilities,
32
+ type InternEvent,
33
+ type InternEventList,
34
+ type InternHealth,
35
+ type InternPairInput,
36
+ type InternPairResult,
37
+ type InternReadiness,
38
+ type InternRunner,
39
+ type InternRunnerList,
40
+ type InternSession,
41
+ type InternSessionList,
42
+ type InternStartedTurn,
43
+ type InternTurn,
44
+ type InternTurnDecision,
45
+ type InternTurnDecisionAction,
46
+ type InternTurnList,
47
+ type StartInternTurnInput,
48
+ type UnknownRecord,
49
+ } from './protocol';
50
+ import type { InternSseEvent } from './sse';
51
+ import {
52
+ createInternTransport,
53
+ type InternRequestOptions,
54
+ type InternStreamOptions,
55
+ type InternTransport,
56
+ type InternTransportOptions,
57
+ } from './transport';
58
+
59
+ export type InternCallOptions = Omit<
60
+ InternRequestOptions<never>,
61
+ | 'method'
62
+ | 'body'
63
+ | 'responseType'
64
+ | 'acceptedStatuses'
65
+ | 'parse'
66
+ >;
67
+
68
+ export interface InternCapabilitiesInput {
69
+ channel?: string;
70
+ trigger?: string;
71
+ }
72
+
73
+ export interface InternTurnListInput {
74
+ limit?: number;
75
+ }
76
+
77
+ export interface InternSessionListInput {
78
+ appSessionKeyPrefix?: string;
79
+ limit?: number;
80
+ }
81
+
82
+ export interface InternEventListInput {
83
+ afterSeq?: number;
84
+ limit?: number;
85
+ }
86
+
87
+ export interface InternArtifactInput {
88
+ sessionKey: string;
89
+ offset?: number;
90
+ maxBytes?: number;
91
+ }
92
+
93
+ export interface InternApprovalListInput {
94
+ status?: string;
95
+ type?: string;
96
+ }
97
+
98
+ export interface InternTurnStreamOptions
99
+ extends Omit<
100
+ InternStreamOptions<UnknownRecord>,
101
+ 'method' | 'body' | 'resume' | 'isTerminal'
102
+ > {
103
+ afterSeq?: number;
104
+ }
105
+
106
+ export type InternTurnStreamEvent = InternSseEvent<
107
+ InternEvent | UnknownRecord
108
+ >;
109
+
110
+ export interface InternClient {
111
+ readonly transport: InternTransport;
112
+ health(options?: InternCallOptions): Promise<InternHealth>;
113
+ readiness(options?: InternCallOptions): Promise<InternReadiness>;
114
+ capabilities(
115
+ input?: InternCapabilitiesInput,
116
+ options?: InternCallOptions
117
+ ): Promise<InternCapabilities>;
118
+ appBootstrap(options?: InternCallOptions): Promise<UnknownRecord>;
119
+ listRunners(options?: InternCallOptions): Promise<InternRunnerList>;
120
+ requireRunner(
121
+ runnerId: string,
122
+ options?: InternCallOptions
123
+ ): Promise<InternRunner>;
124
+ listSessions(
125
+ input?: InternSessionListInput,
126
+ options?: InternCallOptions
127
+ ): Promise<InternSessionList>;
128
+ createSession(
129
+ input: CreateInternSessionInput,
130
+ options?: InternCallOptions
131
+ ): Promise<InternSession>;
132
+ getSession(
133
+ sessionId: string,
134
+ options?: InternCallOptions
135
+ ): Promise<InternSession>;
136
+ listTurns(
137
+ sessionId: string,
138
+ input?: InternTurnListInput,
139
+ options?: InternCallOptions
140
+ ): Promise<InternTurnList>;
141
+ startTurn(
142
+ sessionId: string,
143
+ input: StartInternTurnInput,
144
+ options?: InternCallOptions
145
+ ): Promise<InternStartedTurn>;
146
+ getTurn(
147
+ sessionId: string,
148
+ turnId: string,
149
+ options?: InternCallOptions
150
+ ): Promise<InternTurn>;
151
+ listTurnEvents(
152
+ sessionId: string,
153
+ turnId: string,
154
+ input?: InternEventListInput,
155
+ options?: InternCallOptions
156
+ ): Promise<InternEventList>;
157
+ streamTurn(
158
+ sessionId: string,
159
+ turnId: string,
160
+ options?: InternTurnStreamOptions
161
+ ): AsyncIterable<InternTurnStreamEvent>;
162
+ abortTurn(
163
+ sessionId: string,
164
+ turnId: string,
165
+ options?: InternCallOptions
166
+ ): Promise<InternActionAcknowledgement>;
167
+ decideTurn(
168
+ sessionId: string,
169
+ turnId: string,
170
+ decision: InternTurnDecisionAction,
171
+ input?: InternApprovalInput,
172
+ options?: InternCallOptions
173
+ ): Promise<InternTurnDecision>;
174
+ readArtifact(
175
+ artifactId: string,
176
+ input: InternArtifactInput,
177
+ options?: InternCallOptions
178
+ ): Promise<InternArtifact>;
179
+ listApprovals(
180
+ input?: InternApprovalListInput,
181
+ options?: InternCallOptions
182
+ ): Promise<InternApprovalList>;
183
+ decideApproval(
184
+ requestId: string | number,
185
+ decision: InternApprovalDecisionAction,
186
+ input?: InternApprovalInput,
187
+ options?: InternCallOptions
188
+ ): Promise<InternApprovalDecision>;
189
+ pair(
190
+ input: InternPairInput,
191
+ options?: InternCallOptions
192
+ ): Promise<InternPairResult>;
193
+ }
194
+
195
+ function pathId(value: string | number, label: string): string {
196
+ const normalized = String(value).trim();
197
+ if (!normalized) {
198
+ throw new InternClientError(
199
+ 'validation_failed',
200
+ `${label} is required.`
201
+ );
202
+ }
203
+ return encodeURIComponent(normalized);
204
+ }
205
+
206
+ function queryPath(
207
+ path: string,
208
+ values: Record<string, string | number | boolean | undefined>
209
+ ): string {
210
+ const query = new URLSearchParams();
211
+ for (const [key, value] of Object.entries(values)) {
212
+ if (value !== undefined && value !== '') {
213
+ query.set(key, String(value));
214
+ }
215
+ }
216
+ const encoded = query.toString();
217
+ return encoded ? `${path}?${encoded}` : path;
218
+ }
219
+
220
+ function positiveInteger(
221
+ value: number | undefined,
222
+ label: string
223
+ ): number | undefined {
224
+ if (value === undefined) return undefined;
225
+ if (!Number.isSafeInteger(value) || value <= 0) {
226
+ throw new InternClientError(
227
+ 'validation_failed',
228
+ `${label} must be a positive integer.`
229
+ );
230
+ }
231
+ return value;
232
+ }
233
+
234
+ function boundedPositiveInteger(
235
+ value: number | undefined,
236
+ label: string,
237
+ maximum: number
238
+ ): number | undefined {
239
+ const normalized = positiveInteger(value, label);
240
+ if (normalized !== undefined && normalized > maximum) {
241
+ throw new InternClientError(
242
+ 'validation_failed',
243
+ `${label} must not exceed ${maximum}.`
244
+ );
245
+ }
246
+ return normalized;
247
+ }
248
+
249
+ function appSessionKeyPrefix(
250
+ value: string | undefined
251
+ ): string | undefined {
252
+ if (value === undefined) return undefined;
253
+ const normalized = value.trim();
254
+ if (
255
+ !normalized ||
256
+ new TextEncoder().encode(normalized).byteLength > 256 ||
257
+ /[\0\r\n]/.test(normalized)
258
+ ) {
259
+ throw new InternClientError(
260
+ 'validation_failed',
261
+ 'App session key prefix is invalid.'
262
+ );
263
+ }
264
+ return normalized;
265
+ }
266
+
267
+ function nonNegativeInteger(
268
+ value: number | undefined,
269
+ label: string
270
+ ): number | undefined {
271
+ if (value === undefined) return undefined;
272
+ if (!Number.isSafeInteger(value) || value < 0) {
273
+ throw new InternClientError(
274
+ 'validation_failed',
275
+ `${label} must be a non-negative integer.`
276
+ );
277
+ }
278
+ return value;
279
+ }
280
+
281
+ function sessionPath(sessionId: string): string {
282
+ return `/internal/v1/runner-chat/sessions/${pathId(
283
+ sessionId,
284
+ 'Session ID'
285
+ )}`;
286
+ }
287
+
288
+ function turnPath(sessionId: string, turnId: string): string {
289
+ return `${sessionPath(sessionId)}/turns/${pathId(turnId, 'Turn ID')}`;
290
+ }
291
+
292
+ function parseTurnStreamEvent(
293
+ event: InternSseEvent<UnknownRecord>
294
+ ): InternTurnStreamEvent {
295
+ if (event.json === undefined) {
296
+ return event as InternTurnStreamEvent;
297
+ }
298
+ let json: InternEvent | UnknownRecord;
299
+ if (
300
+ typeof event.json.turn_id === 'string' &&
301
+ typeof event.json.seq === 'number'
302
+ ) {
303
+ json = parseInternEvent(event.json);
304
+ } else {
305
+ json = parseInternRecord(event.json, 'streamEvent');
306
+ }
307
+ return { ...event, json };
308
+ }
309
+
310
+ export function findInternRunner(
311
+ list: InternRunnerList,
312
+ runnerId: string
313
+ ): InternRunner {
314
+ const normalized = runnerId.trim();
315
+ const runner = list.runners.find((item) => item.id === normalized);
316
+ if (!runner) {
317
+ throw new InternUnavailableError(
318
+ `Runner ${normalized || '(missing)'} is not advertised by the selected host.`,
319
+ { capability: `runner:${normalized || 'unknown'}` }
320
+ );
321
+ }
322
+ return runner;
323
+ }
324
+
325
+ export function requireInternRunner(
326
+ list: InternRunnerList,
327
+ runnerId: string
328
+ ): InternRunner {
329
+ const runner = findInternRunner(list, runnerId);
330
+ if (runner.status !== 'available') {
331
+ throw new InternUnavailableError(
332
+ `Runner ${runner.id} is advertised but is not available (${runner.status || 'unknown status'}).`,
333
+ {
334
+ capability: `runner:${runner.id}`,
335
+ details: {
336
+ runnerId: runner.id,
337
+ status: runner.status,
338
+ authStatus: runner.auth_status,
339
+ },
340
+ }
341
+ );
342
+ }
343
+ return runner;
344
+ }
345
+
346
+ export function createInternClient(
347
+ options: InternTransportOptions
348
+ ): InternClient {
349
+ const transport = createInternTransport(options);
350
+
351
+ const health = (callOptions: InternCallOptions = {}) =>
352
+ transport.request('/internal/v1/health', {
353
+ ...callOptions,
354
+ method: 'GET',
355
+ parse: parseInternHealth,
356
+ });
357
+
358
+ const readiness = (callOptions: InternCallOptions = {}) =>
359
+ transport.request('/internal/v1/readiness', {
360
+ ...callOptions,
361
+ method: 'GET',
362
+ acceptedStatuses: [503],
363
+ parse: parseInternReadiness,
364
+ });
365
+
366
+ const capabilities = (
367
+ input: InternCapabilitiesInput = {},
368
+ callOptions: InternCallOptions = {}
369
+ ) =>
370
+ transport.request(
371
+ queryPath('/internal/v1/capabilities', {
372
+ channel: input.channel,
373
+ trigger: input.trigger,
374
+ }),
375
+ {
376
+ ...callOptions,
377
+ method: 'GET',
378
+ parse: parseInternCapabilities,
379
+ }
380
+ );
381
+
382
+ const appBootstrap = (callOptions: InternCallOptions = {}) =>
383
+ transport.request('/internal/v1/app/bootstrap', {
384
+ ...callOptions,
385
+ method: 'GET',
386
+ parse: (value) => parseInternRecord(value, 'appBootstrap'),
387
+ });
388
+
389
+ const listRunners = (callOptions: InternCallOptions = {}) =>
390
+ transport.request('/internal/v1/chat-runners', {
391
+ ...callOptions,
392
+ method: 'GET',
393
+ parse: parseInternRunnerList,
394
+ });
395
+
396
+ const requireRunner = async (
397
+ runnerId: string,
398
+ callOptions: InternCallOptions = {}
399
+ ) => requireInternRunner(await listRunners(callOptions), runnerId);
400
+
401
+ const listSessions = async (
402
+ input: InternSessionListInput = {},
403
+ callOptions: InternCallOptions = {}
404
+ ) =>
405
+ transport.request(
406
+ queryPath('/internal/v1/runner-chat/sessions', {
407
+ app_session_key_prefix: appSessionKeyPrefix(
408
+ input.appSessionKeyPrefix
409
+ ),
410
+ limit: boundedPositiveInteger(
411
+ input.limit,
412
+ 'Session limit',
413
+ 100
414
+ ),
415
+ }),
416
+ {
417
+ ...callOptions,
418
+ method: 'GET',
419
+ parse: parseInternSessionList,
420
+ }
421
+ );
422
+
423
+ const createSession = (
424
+ input: CreateInternSessionInput,
425
+ callOptions: InternCallOptions = {}
426
+ ) =>
427
+ transport.request('/internal/v1/runner-chat/sessions', {
428
+ ...callOptions,
429
+ method: 'POST',
430
+ body: input,
431
+ parse: parseInternSession,
432
+ });
433
+
434
+ const getSession = async (
435
+ sessionId: string,
436
+ callOptions: InternCallOptions = {}
437
+ ) =>
438
+ transport.request(sessionPath(sessionId), {
439
+ ...callOptions,
440
+ method: 'GET',
441
+ parse: parseInternSession,
442
+ });
443
+
444
+ const listTurns = async (
445
+ sessionId: string,
446
+ input: InternTurnListInput = {},
447
+ callOptions: InternCallOptions = {}
448
+ ) =>
449
+ transport.request(
450
+ queryPath(`${sessionPath(sessionId)}/turns`, {
451
+ limit: positiveInteger(input.limit, 'Turn limit'),
452
+ }),
453
+ {
454
+ ...callOptions,
455
+ method: 'GET',
456
+ parse: parseInternTurnList,
457
+ }
458
+ );
459
+
460
+ const startTurn = async (
461
+ sessionId: string,
462
+ input: StartInternTurnInput,
463
+ callOptions: InternCallOptions = {}
464
+ ) =>
465
+ transport.request(`${sessionPath(sessionId)}/turns`, {
466
+ ...callOptions,
467
+ method: 'POST',
468
+ body: input,
469
+ parse: parseInternStartedTurn,
470
+ });
471
+
472
+ const getTurn = async (
473
+ sessionId: string,
474
+ turnId: string,
475
+ callOptions: InternCallOptions = {}
476
+ ) =>
477
+ transport.request(turnPath(sessionId, turnId), {
478
+ ...callOptions,
479
+ method: 'GET',
480
+ parse: parseInternTurn,
481
+ });
482
+
483
+ const listTurnEvents = async (
484
+ sessionId: string,
485
+ turnId: string,
486
+ input: InternEventListInput = {},
487
+ callOptions: InternCallOptions = {}
488
+ ) =>
489
+ transport.request(
490
+ queryPath(`${turnPath(sessionId, turnId)}/events`, {
491
+ after_seq: nonNegativeInteger(
492
+ input.afterSeq,
493
+ 'Event cursor'
494
+ ),
495
+ limit: positiveInteger(input.limit, 'Event limit'),
496
+ }),
497
+ {
498
+ ...callOptions,
499
+ method: 'GET',
500
+ parse: parseInternEventList,
501
+ }
502
+ );
503
+
504
+ async function* streamTurn(
505
+ sessionId: string,
506
+ turnId: string,
507
+ streamOptions: InternTurnStreamOptions = {}
508
+ ): AsyncIterable<InternTurnStreamEvent> {
509
+ const { afterSeq, ...transportOptions } = streamOptions;
510
+ const stream = transport.stream<UnknownRecord>(
511
+ `${turnPath(sessionId, turnId)}/stream`,
512
+ {
513
+ ...transportOptions,
514
+ method: 'GET',
515
+ reconnect: transportOptions.reconnect ?? true,
516
+ dedupe: transportOptions.dedupe ?? true,
517
+ resume: {
518
+ initialCursor: nonNegativeInteger(
519
+ afterSeq,
520
+ 'Event cursor'
521
+ ),
522
+ queryParameter: 'after_seq',
523
+ cursorFromEvent: (event) => {
524
+ const seq = event.json?.seq;
525
+ return typeof seq === 'string' ||
526
+ typeof seq === 'number'
527
+ ? seq
528
+ : undefined;
529
+ },
530
+ },
531
+ isTerminal: (event) => event.event === 'done',
532
+ }
533
+ );
534
+ for await (const event of stream) {
535
+ yield parseTurnStreamEvent(event);
536
+ }
537
+ }
538
+
539
+ const abortTurn = async (
540
+ sessionId: string,
541
+ turnId: string,
542
+ callOptions: InternCallOptions = {}
543
+ ) =>
544
+ transport.request(`${turnPath(sessionId, turnId)}/abort`, {
545
+ ...callOptions,
546
+ method: 'POST',
547
+ parse: parseInternActionAcknowledgement,
548
+ });
549
+
550
+ const decideTurn = async (
551
+ sessionId: string,
552
+ turnId: string,
553
+ decision: InternTurnDecisionAction,
554
+ input: InternApprovalInput = {},
555
+ callOptions: InternCallOptions = {}
556
+ ) =>
557
+ transport.request(
558
+ `${turnPath(sessionId, turnId)}/${decision}`,
559
+ {
560
+ ...callOptions,
561
+ method: 'POST',
562
+ body: {
563
+ note: input.note,
564
+ allow_session: input.allow_session,
565
+ },
566
+ parse: parseInternTurnDecision,
567
+ }
568
+ );
569
+
570
+ const readArtifact = async (
571
+ artifactId: string,
572
+ input: InternArtifactInput,
573
+ callOptions: InternCallOptions = {}
574
+ ) =>
575
+ transport.request(
576
+ queryPath(
577
+ `/internal/v1/artifacts/${pathId(
578
+ artifactId,
579
+ 'Artifact ID'
580
+ )}`,
581
+ {
582
+ session_key: input.sessionKey,
583
+ offset: nonNegativeInteger(
584
+ input.offset,
585
+ 'Artifact offset'
586
+ ),
587
+ max_bytes: positiveInteger(
588
+ input.maxBytes,
589
+ 'Artifact byte limit'
590
+ ),
591
+ }
592
+ ),
593
+ {
594
+ ...callOptions,
595
+ method: 'GET',
596
+ parse: parseInternArtifact,
597
+ }
598
+ );
599
+
600
+ const listApprovals = (
601
+ input: InternApprovalListInput = {},
602
+ callOptions: InternCallOptions = {}
603
+ ) =>
604
+ transport.request(
605
+ queryPath('/internal/v1/approvals', {
606
+ status: input.status,
607
+ type: input.type,
608
+ }),
609
+ {
610
+ ...callOptions,
611
+ method: 'GET',
612
+ parse: parseInternApprovalList,
613
+ }
614
+ );
615
+
616
+ const decideApproval = async (
617
+ requestId: string | number,
618
+ decision: InternApprovalDecisionAction,
619
+ input: InternApprovalInput = {},
620
+ callOptions: InternCallOptions = {}
621
+ ) =>
622
+ transport.request(
623
+ `/internal/v1/approvals/${pathId(
624
+ requestId,
625
+ 'Approval request ID'
626
+ )}/${decision}`,
627
+ {
628
+ ...callOptions,
629
+ method: 'POST',
630
+ body:
631
+ decision === 'approve'
632
+ ? { note: input.note, allowlist: input.allowlist }
633
+ : { note: input.note },
634
+ parse: parseInternApprovalDecision,
635
+ }
636
+ );
637
+
638
+ const pair = (
639
+ input: InternPairInput,
640
+ callOptions: InternCallOptions = {}
641
+ ) =>
642
+ transport.request(
643
+ '/internal/v1/secure-connections/pairing/approve',
644
+ {
645
+ ...callOptions,
646
+ method: 'POST',
647
+ body: input,
648
+ requireAuth: false,
649
+ parse: parseInternPairResult,
650
+ }
651
+ );
652
+
653
+ return {
654
+ transport,
655
+ health,
656
+ readiness,
657
+ capabilities,
658
+ appBootstrap,
659
+ listRunners,
660
+ requireRunner,
661
+ listSessions,
662
+ createSession,
663
+ getSession,
664
+ listTurns,
665
+ startTurn,
666
+ getTurn,
667
+ listTurnEvents,
668
+ streamTurn,
669
+ abortTurn,
670
+ decideTurn,
671
+ readArtifact,
672
+ listApprovals,
673
+ decideApproval,
674
+ pair,
675
+ };
676
+ }