@openchamber/sdk 2.5.3 → 2.6.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/API.md +13 -2
- package/DOCUMENTATION.md +60 -4
- package/GUEST_SERVICES.md +187 -4
- package/README.md +2 -2
- package/dist/host.d.ts +1 -1
- package/dist/index.d.ts +6 -4
- package/dist/index.js +3 -2
- package/dist/manifest.d.ts +232 -6
- package/dist/manifest.js +110 -4
- package/dist/parse.d.ts +186 -0
- package/dist/parse.js +161 -6
- package/dist/protocol.d.ts +4 -0
- package/dist/schemas.d.ts +1 -1
- package/dist/schemas.js +1 -1
- package/dist/service-provider-schemas.d.ts +978 -0
- package/dist/service-provider-schemas.js +159 -1
- package/dist/service-providers.d.ts +422 -1
- package/dist/service-providers.js +311 -37
- package/dist/service-surface-schemas.d.ts +2 -0
- package/dist/service-surface-schemas.js +1 -0
- package/dist/service-surface.d.ts +5 -0
- package/dist/service-surface.js +5 -1
- package/dist/session-events.d.ts +161 -0
- package/dist/session-events.js +108 -0
- package/dist/ui/progress.d.ts +4 -0
- package/dist/ui/progress.js +17 -4
- package/dist/ui/style.js +23 -16
- package/dist/workspace-schemas.d.ts +1 -0
- package/dist/workspace-schemas.js +3 -1
- package/dist/workspace.d.ts +2 -0
- package/package.json +1 -1
|
@@ -143,7 +143,8 @@ export const sourceControlProviderTimeoutMs = (method) => {
|
|
|
143
143
|
/**
|
|
144
144
|
* Environment variable with the base URL of OpenChamber's callback listener
|
|
145
145
|
* (`http://127.0.0.1:<port>`). Set only for a service that provides
|
|
146
|
-
* `source-control
|
|
146
|
+
* `source-control`, `notifications` or `tracker`; `createSourceControlForge`,
|
|
147
|
+
* `createNotificationsOutbound` and `createTrackerOutbound` read it.
|
|
147
148
|
*/
|
|
148
149
|
export const OPENCHAMBER_HOST_CALLBACK_URL = 'OPENCHAMBER_HOST_CALLBACK_URL';
|
|
149
150
|
/** Route on the callback listener a service sends a forge request to. */
|
|
@@ -237,6 +238,48 @@ export class SourceControlForgeError extends Error {
|
|
|
237
238
|
this.status = status;
|
|
238
239
|
}
|
|
239
240
|
}
|
|
241
|
+
/**
|
|
242
|
+
* One request through OpenChamber's callback listener: the handle as bearer,
|
|
243
|
+
* the request as JSON, the relayed answer back. `fail` makes the role's own
|
|
244
|
+
* error for a refusal, a silent host or a malformed relay; an answer of any
|
|
245
|
+
* status from the far side, 401 included, resolves.
|
|
246
|
+
*/
|
|
247
|
+
const postOutbound = async (url, handle, request, timeoutMs, fail) => {
|
|
248
|
+
let response;
|
|
249
|
+
try {
|
|
250
|
+
response = await fetch(url, {
|
|
251
|
+
method: 'POST',
|
|
252
|
+
headers: { authorization: `Bearer ${handle}`, 'content-type': 'application/json' },
|
|
253
|
+
body: JSON.stringify(request),
|
|
254
|
+
// A little past OpenChamber's own deadline, so its answer wins.
|
|
255
|
+
signal: AbortSignal.timeout(timeoutMs + 5_000),
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
throw fail(`OpenChamber did not answer: ${error instanceof Error ? error.message : String(error)}`, 'HOST_UNAVAILABLE', 0);
|
|
260
|
+
}
|
|
261
|
+
let answer;
|
|
262
|
+
try {
|
|
263
|
+
answer = Object(await response.json());
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
throw fail('OpenChamber answered with something other than JSON', 'HOST_UNAVAILABLE', response.status);
|
|
267
|
+
}
|
|
268
|
+
if (!response.ok) {
|
|
269
|
+
const message = answer.error?.message;
|
|
270
|
+
const code = answer.error?.code;
|
|
271
|
+
throw fail(String(message) === message && message.length > 0 ? message : `OpenChamber refused the request (${response.status})`, String(code) === code && code.length > 0 ? code : 'OUTBOUND_REFUSED', response.status);
|
|
272
|
+
}
|
|
273
|
+
if (!Number.isSafeInteger(answer.status) || String(answer.body) !== answer.body) {
|
|
274
|
+
throw fail('OpenChamber relayed a malformed answer', 'HOST_UNAVAILABLE', response.status);
|
|
275
|
+
}
|
|
276
|
+
const headers = {};
|
|
277
|
+
for (const [name, text] of Object.entries(Object(answer.headers ?? {}))) {
|
|
278
|
+
if (String(text) === text)
|
|
279
|
+
headers[name] = text;
|
|
280
|
+
}
|
|
281
|
+
return { status: Number(answer.status), headers, body: answer.body };
|
|
282
|
+
};
|
|
240
283
|
/**
|
|
241
284
|
* The service's way to the forge: OpenChamber's callback listener
|
|
242
285
|
* (`OPENCHAMBER_HOST_CALLBACK_URL`), which checks the handle, keeps the request
|
|
@@ -250,41 +293,272 @@ export const createSourceControlForge = ({ env }) => {
|
|
|
250
293
|
throw new Error(`${OPENCHAMBER_HOST_CALLBACK_URL} is not set: only a service that provides "source-control" can reach a forge.`);
|
|
251
294
|
const url = `${base.replace(/\/+$/, '')}${SOURCE_CONTROL_OUTBOUND_PATH}`;
|
|
252
295
|
return {
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
296
|
+
request: (handle, request) => postOutbound(url, handle, request, SOURCE_CONTROL_OUTBOUND_TIMEOUT_MS, (message, code, status) => new SourceControlForgeError(message, code, status)),
|
|
297
|
+
};
|
|
298
|
+
};
|
|
299
|
+
/*
|
|
300
|
+
* Notifications: a service with `provides: ["notifications"]` is the delivery
|
|
301
|
+
* channel `contributes.notifications` describes, next to the desktop and the
|
|
302
|
+
* phone. OpenChamber keeps the account and its token. Once the user connects
|
|
303
|
+
* one, every notification OpenChamber decides to show goes to the service at
|
|
304
|
+
* `NOTIFICATIONS_PROVIDER_PATH` as `notify`, and the service sends it on
|
|
305
|
+
* through OpenChamber's outbound proxy (`createNotificationsOutbound`), which
|
|
306
|
+
* pins the account's origin and adds the token. The service never sees a token.
|
|
307
|
+
*/
|
|
308
|
+
/** Route on the service loopback OpenChamber posts every notifications call to. */
|
|
309
|
+
export const NOTIFICATIONS_PROVIDER_PATH = '/notifications';
|
|
310
|
+
/** The `v` of every request; a service answers a version it does not know with HTTP 400. */
|
|
311
|
+
export const NOTIFICATIONS_PROVIDER_WIRE_VERSION = 1;
|
|
312
|
+
/**
|
|
313
|
+
* `auth.verify` reads who a candidate token belongs to, `auth.probe` whether
|
|
314
|
+
* an instance is this channel's server, `notify` delivers one notification.
|
|
315
|
+
*/
|
|
316
|
+
export const NOTIFICATIONS_PROVIDER_METHODS = ['auth.verify', 'auth.probe', 'notify'];
|
|
317
|
+
/** A notification is delivered within this deadline; a slower service drops it. */
|
|
318
|
+
export const NOTIFICATIONS_PROVIDER_NOTIFY_TIMEOUT_MS = 10_000;
|
|
319
|
+
export const NOTIFICATIONS_PROVIDER_AUTH_TIMEOUT_MS = 15_000;
|
|
320
|
+
/** Bytes of an answer OpenChamber reads. */
|
|
321
|
+
export const NOTIFICATIONS_PROVIDER_RESPONSE_MAX = 16_000;
|
|
322
|
+
/** Characters of `error.message` OpenChamber reads. */
|
|
323
|
+
export const NOTIFICATIONS_PROVIDER_ERROR_MESSAGE_MAX = 2_000;
|
|
324
|
+
/** No call for this long stops the service; the next call starts it again. */
|
|
325
|
+
export const NOTIFICATIONS_PROVIDER_IDLE_MS = 10 * 60_000;
|
|
326
|
+
/** Route on the callback listener a service sends the channel's requests to. */
|
|
327
|
+
export const NOTIFICATIONS_OUTBOUND_PATH = '/notifications/outbound';
|
|
328
|
+
/** One outbound request may take this long; the call's own deadline may end it sooner. */
|
|
329
|
+
export const NOTIFICATIONS_OUTBOUND_TIMEOUT_MS = 20_000;
|
|
330
|
+
export const notificationsProviderTimeoutMs = (method) => (method === 'notify' ? NOTIFICATIONS_PROVIDER_NOTIFY_TIMEOUT_MS : NOTIFICATIONS_PROVIDER_AUTH_TIMEOUT_MS);
|
|
331
|
+
/**
|
|
332
|
+
* The kinds OpenChamber notifies about. `ready`: the agent finished; `error`:
|
|
333
|
+
* the turn failed; `question` and `permission`: the agent waits for the user;
|
|
334
|
+
* `review`: an auto-review ended; the `goal_*` kinds: a session goal settled.
|
|
335
|
+
* Which kinds reach a channel follows the user's own notification toggles.
|
|
336
|
+
*/
|
|
337
|
+
export const GUEST_NOTIFICATION_KINDS = ['ready', 'error', 'question', 'permission', 'review', 'goal_complete', 'goal_blocked', 'goal_budget'];
|
|
338
|
+
export const isNotificationsProviderMethod = (value) => (NOTIFICATIONS_PROVIDER_METHODS.some((method) => method === value));
|
|
339
|
+
/** `undefined` is an account that is neither `null` nor `{ id, login }`. */
|
|
340
|
+
const readNotificationsAccount = (account) => {
|
|
341
|
+
if (account === null)
|
|
342
|
+
return null;
|
|
343
|
+
if (Object(account) !== account || account === undefined)
|
|
344
|
+
return undefined;
|
|
345
|
+
const { id, login } = account;
|
|
346
|
+
if (String(id) !== id || id.length === 0)
|
|
347
|
+
return undefined;
|
|
348
|
+
if (login !== null && (String(login) !== login || login.length === 0))
|
|
349
|
+
return undefined;
|
|
350
|
+
return { id, login };
|
|
351
|
+
};
|
|
352
|
+
/**
|
|
353
|
+
* A service's read of the body OpenChamber posted to
|
|
354
|
+
* `NOTIFICATIONS_PROVIDER_PATH`. OpenChamber built `input`, so this checks the
|
|
355
|
+
* envelope, not every field, and the service bundle needs no schema library.
|
|
356
|
+
* `null` is a body that is not a notifications call of this wire version;
|
|
357
|
+
* answer it with HTTP 400.
|
|
358
|
+
*/
|
|
359
|
+
export const readNotificationsProviderRequest = (body) => {
|
|
360
|
+
let wire;
|
|
361
|
+
try {
|
|
362
|
+
const parsed = JSON.parse(body);
|
|
363
|
+
if (Object(parsed) !== parsed || parsed === null)
|
|
364
|
+
return null;
|
|
365
|
+
wire = parsed;
|
|
366
|
+
}
|
|
367
|
+
catch {
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
const { v, requestId, method, provider, session, input } = wire;
|
|
371
|
+
if (v !== NOTIFICATIONS_PROVIDER_WIRE_VERSION)
|
|
372
|
+
return null;
|
|
373
|
+
if (String(requestId) !== requestId || requestId.length === 0)
|
|
374
|
+
return null;
|
|
375
|
+
if (String(provider) !== provider || provider.length === 0)
|
|
376
|
+
return null;
|
|
377
|
+
if (String(method) !== method || !isNotificationsProviderMethod(method))
|
|
378
|
+
return null;
|
|
379
|
+
if (Object(input) !== input || Array.isArray(input))
|
|
380
|
+
return null;
|
|
381
|
+
if (Object(session) !== session || session === null || session === undefined)
|
|
382
|
+
return null;
|
|
383
|
+
const { instance, handle } = session;
|
|
384
|
+
if (String(instance) !== instance || instance.length === 0 || String(handle) !== handle || handle.length === 0)
|
|
385
|
+
return null;
|
|
386
|
+
const account = readNotificationsAccount(session.account);
|
|
387
|
+
if (account === undefined)
|
|
388
|
+
return null;
|
|
389
|
+
// SAFETY: `input` is a plain object parsed from JSON, so every value in it is JSON.
|
|
390
|
+
return { v: NOTIFICATIONS_PROVIDER_WIRE_VERSION, requestId, method, provider, session: { instance, handle, account }, input: input };
|
|
391
|
+
};
|
|
392
|
+
/**
|
|
393
|
+
* OpenChamber refused the request (an expired handle, a path off the
|
|
394
|
+
* instance, a body too large), could not reach the channel's server, or did
|
|
395
|
+
* not answer. `status` is the callback listener's HTTP status, `0` when it
|
|
396
|
+
* gave none; `code` is its error code (`HOST_UNAVAILABLE` when there was no
|
|
397
|
+
* usable answer).
|
|
398
|
+
*/
|
|
399
|
+
export class NotificationsOutboundError extends Error {
|
|
400
|
+
code;
|
|
401
|
+
status;
|
|
402
|
+
constructor(message, code, status) {
|
|
403
|
+
super(message);
|
|
404
|
+
this.name = 'NotificationsOutboundError';
|
|
405
|
+
this.code = code;
|
|
406
|
+
this.status = status;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* The service's way to the channel's server: OpenChamber's callback listener
|
|
411
|
+
* (`OPENCHAMBER_HOST_CALLBACK_URL`), which checks the handle, keeps the
|
|
412
|
+
* request on the account's origin and adds the token. Plain `fetch`, no
|
|
413
|
+
* dependencies. Throws `NotificationsOutboundError` when OpenChamber refuses
|
|
414
|
+
* the request; an answer of any status from the server resolves.
|
|
415
|
+
*/
|
|
416
|
+
export const createNotificationsOutbound = ({ env }) => {
|
|
417
|
+
const base = env[OPENCHAMBER_HOST_CALLBACK_URL];
|
|
418
|
+
if (!base)
|
|
419
|
+
throw new Error(`${OPENCHAMBER_HOST_CALLBACK_URL} is not set: only a service that provides "notifications" can reach a channel's server.`);
|
|
420
|
+
const url = `${base.replace(/\/+$/, '')}${NOTIFICATIONS_OUTBOUND_PATH}`;
|
|
421
|
+
return {
|
|
422
|
+
request: (handle, request) => postOutbound(url, handle, request, NOTIFICATIONS_OUTBOUND_TIMEOUT_MS, (message, code, status) => new NotificationsOutboundError(message, code, status)),
|
|
423
|
+
};
|
|
424
|
+
};
|
|
425
|
+
/*
|
|
426
|
+
* Tracker: a service with `provides: ["tracker"]` is the work item tracker
|
|
427
|
+
* `contributes.tracker` describes, next to Linear. OpenChamber keeps the
|
|
428
|
+
* accounts and their tokens (pasted or minted through OAuth), the project
|
|
429
|
+
* mapping, the session comment preference and the session records; it
|
|
430
|
+
* forwards the reads and the two writes to the service at
|
|
431
|
+
* `TRACKER_PROVIDER_PATH`. The service never sees a token: it reaches the
|
|
432
|
+
* tracker's server through OpenChamber's outbound proxy
|
|
433
|
+
* (`createTrackerOutbound`), which pins the instance origin and adds the
|
|
434
|
+
* account's credential itself. Every item answered goes to the UI in the
|
|
435
|
+
* neutral tracker shapes, so the picker, the composer, New Worktree, linked
|
|
436
|
+
* items and tracked items draw it as they draw a Linear issue.
|
|
437
|
+
*/
|
|
438
|
+
/** Route on the service loopback OpenChamber posts every tracker call to. */
|
|
439
|
+
export const TRACKER_PROVIDER_PATH = '/tracker';
|
|
440
|
+
/** The `v` of every request; a service answers a version it does not know with HTTP 400. */
|
|
441
|
+
export const TRACKER_PROVIDER_WIRE_VERSION = 1;
|
|
442
|
+
/**
|
|
443
|
+
* Every call OpenChamber forwards. `auth.probe` asks whether an instance is
|
|
444
|
+
* this tracker's server, `auth.verify` who a credential belongs to (the handle
|
|
445
|
+
* carries a pasted token or an OAuth access token); the reads answer in the
|
|
446
|
+
* neutral shapes; `items.updateState` and `comments.create` are the two
|
|
447
|
+
* writes, the only calls whose handle may send anything but `GET`.
|
|
448
|
+
*/
|
|
449
|
+
export const TRACKER_PROVIDER_METHODS = [
|
|
450
|
+
'auth.probe',
|
|
451
|
+
'auth.verify',
|
|
452
|
+
'items.list',
|
|
453
|
+
'items.get',
|
|
454
|
+
'items.updateState',
|
|
455
|
+
'items.liveSummaries',
|
|
456
|
+
'states.list',
|
|
457
|
+
'containers.list',
|
|
458
|
+
'comments.create',
|
|
459
|
+
];
|
|
460
|
+
/** The calls that change something on the tracker. */
|
|
461
|
+
export const TRACKER_PROVIDER_WRITE_METHODS = ['items.updateState', 'comments.create'];
|
|
462
|
+
export const TRACKER_PROVIDER_AUTH_TIMEOUT_MS = 15_000;
|
|
463
|
+
export const TRACKER_PROVIDER_READ_TIMEOUT_MS = 20_000;
|
|
464
|
+
export const TRACKER_PROVIDER_WRITE_TIMEOUT_MS = 30_000;
|
|
465
|
+
/** Bytes of an answer OpenChamber reads: a page of items with their labels and people. */
|
|
466
|
+
export const TRACKER_PROVIDER_RESPONSE_MAX = 2_000_000;
|
|
467
|
+
/** Characters of `error.message` OpenChamber reads. */
|
|
468
|
+
export const TRACKER_PROVIDER_ERROR_MESSAGE_MAX = 2_000;
|
|
469
|
+
/** No call for this long stops the service; the next call starts it again. */
|
|
470
|
+
export const TRACKER_PROVIDER_IDLE_MS = 10 * 60_000;
|
|
471
|
+
/** Items in one `items.list` page, children or comments of one item, states of one container. */
|
|
472
|
+
export const TRACKER_PROVIDER_LIST_MAX = 100;
|
|
473
|
+
/** Identifiers in one `items.liveSummaries` call. */
|
|
474
|
+
export const TRACKER_PROVIDER_LIVE_SUMMARIES_MAX = 50;
|
|
475
|
+
/** Containers in one `containers.list` answer. */
|
|
476
|
+
export const TRACKER_PROVIDER_CONTAINERS_MAX = 500;
|
|
477
|
+
/** Route on the callback listener a service sends the tracker's requests to. */
|
|
478
|
+
export const TRACKER_OUTBOUND_PATH = '/tracker/outbound';
|
|
479
|
+
/** One outbound request may take this long; the call's own deadline may end it sooner. */
|
|
480
|
+
export const TRACKER_OUTBOUND_TIMEOUT_MS = 20_000;
|
|
481
|
+
export const isTrackerProviderWriteMethod = (method) => (TRACKER_PROVIDER_WRITE_METHODS.some((write) => write === method));
|
|
482
|
+
/** How long OpenChamber waits for the service to answer `method`. */
|
|
483
|
+
export const trackerProviderTimeoutMs = (method) => {
|
|
484
|
+
if (method === 'auth.probe' || method === 'auth.verify')
|
|
485
|
+
return TRACKER_PROVIDER_AUTH_TIMEOUT_MS;
|
|
486
|
+
if (isTrackerProviderWriteMethod(method))
|
|
487
|
+
return TRACKER_PROVIDER_WRITE_TIMEOUT_MS;
|
|
488
|
+
return TRACKER_PROVIDER_READ_TIMEOUT_MS;
|
|
489
|
+
};
|
|
490
|
+
export const isTrackerProviderMethod = (value) => (TRACKER_PROVIDER_METHODS.some((method) => method === value));
|
|
491
|
+
/**
|
|
492
|
+
* A service's read of the body OpenChamber posted to `TRACKER_PROVIDER_PATH`.
|
|
493
|
+
* OpenChamber built `input`, so this checks the envelope, not every field, and
|
|
494
|
+
* the service bundle needs no schema library. `null` is a body that is not a
|
|
495
|
+
* tracker call of this wire version; answer it with HTTP 400.
|
|
496
|
+
*/
|
|
497
|
+
export const readTrackerProviderRequest = (body) => {
|
|
498
|
+
let wire;
|
|
499
|
+
try {
|
|
500
|
+
const parsed = JSON.parse(body);
|
|
501
|
+
if (Object(parsed) !== parsed || parsed === null)
|
|
502
|
+
return null;
|
|
503
|
+
wire = parsed;
|
|
504
|
+
}
|
|
505
|
+
catch {
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
const { v, requestId, method, provider, session, input } = wire;
|
|
509
|
+
if (v !== TRACKER_PROVIDER_WIRE_VERSION)
|
|
510
|
+
return null;
|
|
511
|
+
if (String(requestId) !== requestId || requestId.length === 0)
|
|
512
|
+
return null;
|
|
513
|
+
if (String(provider) !== provider || provider.length === 0)
|
|
514
|
+
return null;
|
|
515
|
+
if (String(method) !== method || !isTrackerProviderMethod(method))
|
|
516
|
+
return null;
|
|
517
|
+
if (Object(input) !== input || Array.isArray(input))
|
|
518
|
+
return null;
|
|
519
|
+
if (Object(session) !== session || session === null || session === undefined)
|
|
520
|
+
return null;
|
|
521
|
+
const { instance, handle } = session;
|
|
522
|
+
if (String(instance) !== instance || instance.length === 0 || String(handle) !== handle || handle.length === 0)
|
|
523
|
+
return null;
|
|
524
|
+
// The account shape is the notification channel's: `{ id, login | null }` or null.
|
|
525
|
+
const account = readNotificationsAccount(session.account);
|
|
526
|
+
if (account === undefined)
|
|
527
|
+
return null;
|
|
528
|
+
// SAFETY: `input` is a plain object parsed from JSON, so every value in it is JSON.
|
|
529
|
+
return { v: TRACKER_PROVIDER_WIRE_VERSION, requestId, method, provider, session: { instance, handle, account }, input: input };
|
|
530
|
+
};
|
|
531
|
+
/**
|
|
532
|
+
* OpenChamber refused the request (an expired handle, a path off the
|
|
533
|
+
* instance, a body too large, a write through a read's handle), could not
|
|
534
|
+
* reach the tracker's server, or did not answer. `status` is the callback
|
|
535
|
+
* listener's HTTP status, `0` when it gave none; `code` is its error code
|
|
536
|
+
* (`HOST_UNAVAILABLE` when there was no usable answer).
|
|
537
|
+
*/
|
|
538
|
+
export class TrackerOutboundError extends Error {
|
|
539
|
+
code;
|
|
540
|
+
status;
|
|
541
|
+
constructor(message, code, status) {
|
|
542
|
+
super(message);
|
|
543
|
+
this.name = 'TrackerOutboundError';
|
|
544
|
+
this.code = code;
|
|
545
|
+
this.status = status;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* The service's way to the tracker's server: OpenChamber's callback listener
|
|
550
|
+
* (`OPENCHAMBER_HOST_CALLBACK_URL`), which checks the handle, keeps the
|
|
551
|
+
* request on the account's origin and adds the credential. Plain `fetch`, no
|
|
552
|
+
* dependencies. Throws `TrackerOutboundError` when OpenChamber refuses the
|
|
553
|
+
* request; an answer of any status from the server resolves. A read's handle
|
|
554
|
+
* sends `GET` only; `items.updateState` and `comments.create` may send writes.
|
|
555
|
+
*/
|
|
556
|
+
export const createTrackerOutbound = ({ env }) => {
|
|
557
|
+
const base = env[OPENCHAMBER_HOST_CALLBACK_URL];
|
|
558
|
+
if (!base)
|
|
559
|
+
throw new Error(`${OPENCHAMBER_HOST_CALLBACK_URL} is not set: only a service that provides "tracker" can reach a tracker's server.`);
|
|
560
|
+
const url = `${base.replace(/\/+$/, '')}${TRACKER_OUTBOUND_PATH}`;
|
|
561
|
+
return {
|
|
562
|
+
request: (handle, request) => postOutbound(url, handle, request, TRACKER_OUTBOUND_TIMEOUT_MS, (message, code, status) => new TrackerOutboundError(message, code, status)),
|
|
289
563
|
};
|
|
290
564
|
};
|
|
@@ -36,6 +36,7 @@ export declare const surfaceInputEventSchema: z.ZodDiscriminatedUnion<[z.ZodObje
|
|
|
36
36
|
}>;
|
|
37
37
|
key: z.ZodString;
|
|
38
38
|
code: z.ZodString;
|
|
39
|
+
keyCode: z.ZodOptional<z.ZodNumber>;
|
|
39
40
|
modifiers: z.ZodObject<{
|
|
40
41
|
alt: z.ZodBoolean;
|
|
41
42
|
ctrl: z.ZodBoolean;
|
|
@@ -89,6 +90,7 @@ export declare const surfaceViewerMessageSchema: z.ZodDiscriminatedUnion<[z.ZodO
|
|
|
89
90
|
}>;
|
|
90
91
|
key: z.ZodString;
|
|
91
92
|
code: z.ZodString;
|
|
93
|
+
keyCode: z.ZodOptional<z.ZodNumber>;
|
|
92
94
|
modifiers: z.ZodObject<{
|
|
93
95
|
alt: z.ZodBoolean;
|
|
94
96
|
ctrl: z.ZodBoolean;
|
|
@@ -18,6 +18,7 @@ export const surfaceInputEventSchema = z.discriminatedUnion('type', [
|
|
|
18
18
|
action: z.enum(['down', 'up']),
|
|
19
19
|
key: z.string().min(1).max(64),
|
|
20
20
|
code: z.string().max(64),
|
|
21
|
+
keyCode: z.number().int().min(0).max(255).optional(),
|
|
21
22
|
modifiers: modifiersSchema,
|
|
22
23
|
}),
|
|
23
24
|
z.object({ type: z.literal('text'), text: z.string().min(1).max(SURFACE_TEXT_MAX) }),
|
|
@@ -93,6 +93,11 @@ export type SurfaceKeyEvent = {
|
|
|
93
93
|
key: string;
|
|
94
94
|
/** `KeyboardEvent.code`. */
|
|
95
95
|
code: string;
|
|
96
|
+
/**
|
|
97
|
+
* `KeyboardEvent.keyCode` as the viewer's browser computed it for the user's
|
|
98
|
+
* keyboard layout, 0 to 255. Older hosts omit it.
|
|
99
|
+
*/
|
|
100
|
+
keyCode?: number;
|
|
96
101
|
modifiers: SurfaceModifiers;
|
|
97
102
|
};
|
|
98
103
|
/** Text the user pasted or composed; type it as is. */
|
package/dist/service-surface.js
CHANGED
|
@@ -99,7 +99,11 @@ const readEvent = (value) => {
|
|
|
99
99
|
return null;
|
|
100
100
|
if (!isText(value.key) || !isText(value.code) || value.key.length > 64 || value.code.length > 64)
|
|
101
101
|
return null;
|
|
102
|
-
|
|
102
|
+
if (value.keyCode === undefined)
|
|
103
|
+
return { type: 'key', action: value.action, key: value.key, code: value.code, modifiers };
|
|
104
|
+
if (!isFiniteNumber(value.keyCode) || !Number.isInteger(value.keyCode) || value.keyCode < 0 || value.keyCode > 255)
|
|
105
|
+
return null;
|
|
106
|
+
return { type: 'key', action: value.action, key: value.key, code: value.code, keyCode: value.keyCode, modifiers };
|
|
103
107
|
}
|
|
104
108
|
return null;
|
|
105
109
|
};
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session events for an extension's service (`contributes.sessionEvents`).
|
|
3
|
+
*
|
|
4
|
+
* OpenChamber posts each event a package subscribed to as one HTTP request to
|
|
5
|
+
* the service at `SESSION_EVENTS_PATH`, from every project, whether or not a
|
|
6
|
+
* window is open. Approving the package is the subscription: the request
|
|
7
|
+
* carries identifiers only (`event`), and the `transcript` capability, asked
|
|
8
|
+
* for with `content: true`, adds what the agent said or asked (`content`).
|
|
9
|
+
* The service answers `{ ok: true }` once it has taken the event; OpenChamber
|
|
10
|
+
* reads nothing else from the answer. Which projects an extension acts on is
|
|
11
|
+
* its own business: filter on `event.project.directory`.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* `turn-finished`: the session went idle with nothing waiting (no running
|
|
15
|
+
* subagent, no open permission, question or page, no goal or auto-review
|
|
16
|
+
* step still driving it). `turn-failed`: the turn ended in an error.
|
|
17
|
+
* `permission-asked`: the agent asked for a permission the user has to
|
|
18
|
+
* answer. `question-asked`: the agent asked the user a question through a
|
|
19
|
+
* form or a page. A turn the user stopped emits nothing.
|
|
20
|
+
*/
|
|
21
|
+
export declare const GUEST_SESSION_EVENT_TYPES: readonly ['turn-finished', 'turn-failed', 'permission-asked', 'question-asked'];
|
|
22
|
+
export type GuestSessionEventType = (typeof GUEST_SESSION_EVENT_TYPES)[number];
|
|
23
|
+
/** Route on the service loopback OpenChamber posts every event to. */
|
|
24
|
+
export declare const SESSION_EVENTS_PATH = "/session-events";
|
|
25
|
+
/** The `v` of every request; a service answers a version it does not know with HTTP 400. */
|
|
26
|
+
export declare const SESSION_EVENTS_WIRE_VERSION = 1;
|
|
27
|
+
/** One delivery may take this long; a slower service drops that event. */
|
|
28
|
+
export declare const SESSION_EVENT_TIMEOUT_MS = 10000;
|
|
29
|
+
/** No event for this long stops the service; the next event starts it again. */
|
|
30
|
+
export declare const SESSION_EVENTS_IDLE_MS: number;
|
|
31
|
+
/** Characters of the agent's reply carried in `content`; a longer reply is cut and flagged. */
|
|
32
|
+
export declare const SESSION_EVENT_REPLY_MAX = 64000;
|
|
33
|
+
/** Events kept while the service starts; newer ones past this are dropped. */
|
|
34
|
+
export declare const SESSION_EVENT_PENDING_MAX = 200;
|
|
35
|
+
/** Characters of `error` OpenChamber reads from a failed answer. */
|
|
36
|
+
export declare const SESSION_EVENT_ERROR_MAX = 2000;
|
|
37
|
+
/**
|
|
38
|
+
* `contributes.sessionEvents`: which events the service wants, and whether it
|
|
39
|
+
* also wants their content. Needs `contributes.service`; the service needs no
|
|
40
|
+
* panel or background entry for this alone.
|
|
41
|
+
*/
|
|
42
|
+
export type SessionEventsContribution = {
|
|
43
|
+
/** 1 to 4 distinct types. */
|
|
44
|
+
events: GuestSessionEventType[];
|
|
45
|
+
/** Asks for the `transcript` capability: the reply, the question, the permission's detail. */
|
|
46
|
+
content?: true;
|
|
47
|
+
};
|
|
48
|
+
/** What the catalog and the approval dialog show of the subscription. */
|
|
49
|
+
export type PublicSessionEvents = {
|
|
50
|
+
events: GuestSessionEventType[];
|
|
51
|
+
content: boolean;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* An item the user linked to the session (a tracker issue, a pull request,
|
|
55
|
+
* an external link), as the session stores it: identifiers and the link,
|
|
56
|
+
* never the item's title. `id` is the stored entry id (`owner/repo#12`,
|
|
57
|
+
* `linear:OPE-367`, `link:<url>`); `kind` is the link's kind as stored
|
|
58
|
+
* (`issue`, `pull`, `external`, or a tracker's own kind); `provider` names the
|
|
59
|
+
* tracker when one owns the item and `identifier` is its key there (`OPE-367`).
|
|
60
|
+
*/
|
|
61
|
+
export type GuestSessionLinkedItem = {
|
|
62
|
+
id: string;
|
|
63
|
+
kind: string;
|
|
64
|
+
url: string;
|
|
65
|
+
provider?: string;
|
|
66
|
+
identifier?: string;
|
|
67
|
+
/** The change request or issue number on a Git host. */
|
|
68
|
+
number?: number;
|
|
69
|
+
};
|
|
70
|
+
/** The agent's final answer of the turn, cut at `SESSION_EVENT_REPLY_MAX` when `replyTruncated`. */
|
|
71
|
+
export type GuestTurnFinishedContent = {
|
|
72
|
+
type: 'turn-finished';
|
|
73
|
+
sessionTitle: string | null;
|
|
74
|
+
reply: string;
|
|
75
|
+
replyTruncated: boolean;
|
|
76
|
+
};
|
|
77
|
+
export type GuestTurnFailedContent = {
|
|
78
|
+
type: 'turn-failed';
|
|
79
|
+
sessionTitle: string | null;
|
|
80
|
+
error: {
|
|
81
|
+
type: string;
|
|
82
|
+
message: string;
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
/** What the agent wants to do: the permission's action, the resources it names, and its note. */
|
|
86
|
+
export type GuestPermissionAskedContent = {
|
|
87
|
+
type: 'permission-asked';
|
|
88
|
+
sessionTitle: string | null;
|
|
89
|
+
action: string;
|
|
90
|
+
resources: string[];
|
|
91
|
+
message: string | null;
|
|
92
|
+
};
|
|
93
|
+
export type GuestQuestionAskedContent = {
|
|
94
|
+
type: 'question-asked';
|
|
95
|
+
sessionTitle: string | null;
|
|
96
|
+
ask: {
|
|
97
|
+
kind: 'form';
|
|
98
|
+
id: string;
|
|
99
|
+
title: string;
|
|
100
|
+
fields: Array<{
|
|
101
|
+
title: string;
|
|
102
|
+
description: string | null;
|
|
103
|
+
}>;
|
|
104
|
+
} | {
|
|
105
|
+
kind: 'page';
|
|
106
|
+
id: string;
|
|
107
|
+
title: string;
|
|
108
|
+
};
|
|
109
|
+
};
|
|
110
|
+
export type GuestSessionEventContent = GuestTurnFinishedContent | GuestTurnFailedContent | GuestPermissionAskedContent | GuestQuestionAskedContent;
|
|
111
|
+
/**
|
|
112
|
+
* One event, as OpenChamber posts it. `event` is what every subscriber gets;
|
|
113
|
+
* `content` comes only with the `transcript` grant, and only on the types
|
|
114
|
+
* that have content. `project.directory` is the session's directory, `null`
|
|
115
|
+
* when OpenChamber does not know it. `session.parentId` is set for a
|
|
116
|
+
* subagent's session. `permission.requestId` names the request on a
|
|
117
|
+
* `permission-asked` event.
|
|
118
|
+
*/
|
|
119
|
+
export type GuestSessionEventRequest = {
|
|
120
|
+
v: typeof SESSION_EVENTS_WIRE_VERSION;
|
|
121
|
+
requestId: string;
|
|
122
|
+
event: {
|
|
123
|
+
/** OpenCode's event id, or one OpenChamber made for an event it synthesized. */
|
|
124
|
+
id: string;
|
|
125
|
+
type: GuestSessionEventType;
|
|
126
|
+
/** Epoch milliseconds. */
|
|
127
|
+
at: number;
|
|
128
|
+
session: {
|
|
129
|
+
id: string;
|
|
130
|
+
parentId: string | null;
|
|
131
|
+
};
|
|
132
|
+
project: {
|
|
133
|
+
directory: string | null;
|
|
134
|
+
};
|
|
135
|
+
linked: GuestSessionLinkedItem[];
|
|
136
|
+
permission?: {
|
|
137
|
+
requestId: string;
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
content?: GuestSessionEventContent;
|
|
141
|
+
};
|
|
142
|
+
/**
|
|
143
|
+
* What the service answers. `ok: false` with an `error` is a delivery that
|
|
144
|
+
* failed; OpenChamber logs the text and nothing else, so say what went wrong
|
|
145
|
+
* on the service's side, never what the event contained.
|
|
146
|
+
*/
|
|
147
|
+
export type GuestSessionEventResult = {
|
|
148
|
+
ok: true;
|
|
149
|
+
} | {
|
|
150
|
+
ok: false;
|
|
151
|
+
error: string;
|
|
152
|
+
};
|
|
153
|
+
export declare const isGuestSessionEventType: (value: string) => value is GuestSessionEventType;
|
|
154
|
+
/**
|
|
155
|
+
* A service's read of the body OpenChamber posted to `SESSION_EVENTS_PATH`.
|
|
156
|
+
* OpenChamber built the event, so this checks the envelope and the fields a
|
|
157
|
+
* service branches on, not every content field, and the service bundle needs
|
|
158
|
+
* no schema library. `null` is a body that is not a session event of this
|
|
159
|
+
* wire version; answer it with HTTP 400.
|
|
160
|
+
*/
|
|
161
|
+
export declare const readSessionEventRequest: (body: string) => GuestSessionEventRequest | null;
|