@pellux/goodvibes-daemon-sdk 0.38.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/artifact-upload.d.ts.map +1 -1
  2. package/dist/artifact-upload.js +53 -2
  3. package/dist/context.d.ts +17 -1
  4. package/dist/context.d.ts.map +1 -1
  5. package/dist/control-routes.d.ts.map +1 -1
  6. package/dist/control-routes.js +12 -3
  7. package/dist/index.d.ts +2 -2
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/integration-route-types.d.ts +62 -0
  10. package/dist/integration-route-types.d.ts.map +1 -1
  11. package/dist/integration-routes.d.ts.map +1 -1
  12. package/dist/integration-routes.js +45 -0
  13. package/dist/memory-record-body.d.ts +22 -0
  14. package/dist/memory-record-body.d.ts.map +1 -0
  15. package/dist/memory-record-body.js +110 -0
  16. package/dist/operator.js +15 -0
  17. package/dist/runtime-route-types.d.ts +46 -3
  18. package/dist/runtime-route-types.d.ts.map +1 -1
  19. package/dist/runtime-session-lifecycle-routes.d.ts +14 -0
  20. package/dist/runtime-session-lifecycle-routes.d.ts.map +1 -0
  21. package/dist/runtime-session-lifecycle-routes.js +68 -0
  22. package/dist/runtime-session-register.d.ts +9 -0
  23. package/dist/runtime-session-register.d.ts.map +1 -0
  24. package/dist/runtime-session-register.js +56 -0
  25. package/dist/runtime-session-routes.d.ts +40 -8
  26. package/dist/runtime-session-routes.d.ts.map +1 -1
  27. package/dist/runtime-session-routes.js +100 -65
  28. package/dist/sessions.d.ts.map +1 -1
  29. package/dist/sessions.js +16 -5
  30. package/dist/system-route-types.d.ts +48 -0
  31. package/dist/system-route-types.d.ts.map +1 -1
  32. package/dist/system-routes.d.ts.map +1 -1
  33. package/dist/system-routes.js +59 -7
  34. package/package.json +3 -3
@@ -1,16 +1,33 @@
1
+ import { handleRegisterSharedSession } from './runtime-session-register.js';
2
+ import { handleDeleteSharedSession, handleGetSharedSession, handleSharedSessionDetach, handleSharedSessionLifecycle, } from './runtime-session-lifecycle-routes.js';
1
3
  import { withAdmin } from './auth-helpers.js';
2
4
  import { randomUUID } from 'node:crypto';
3
5
  import { jsonErrorResponse } from './error-response.js';
6
+ import { SDKErrorCodes } from '@pellux/goodvibes-errors';
4
7
  import { createRouteBodySchema, createRouteBodySchemaRegistry, isJsonRecord, readBoundedPositiveInteger, readOptionalStringField, readStringArrayField, } from './route-helpers.js';
5
8
  const DEFAULT_LIST_LIMIT = 100;
6
9
  const MAX_LIST_LIMIT = 500;
7
10
  const MAX_SESSION_TOOL_NAMES = 64;
8
- const SHARED_SESSION_KINDS = new Set(['tui', 'companion-task', 'companion-chat']);
11
+ export const SHARED_SESSION_KINDS = new Set(['tui', 'agent', 'webui', 'companion-task', 'companion-chat', 'automation']);
9
12
  const SHARED_SESSION_STATUSES = new Set(['active', 'closed']);
10
13
  function readBoundedLimit(url, key = 'limit') {
11
14
  return readBoundedPositiveInteger(url.searchParams.get(key), DEFAULT_LIST_LIMIT, MAX_LIST_LIMIT);
12
15
  }
13
- function toSharedSessionRecordResponse(sessionId, session, options = {}) {
16
+ /** Runs a submit/steer/follow-up broker call, converting its closed-session
17
+ * guard throw ({ code: SDKErrorCodes.SESSION_CLOSED, status: 409 }, thrown
18
+ * before any mutation) into the same structured 409 other routes return. */
19
+ async function callOrSessionClosed(fn) {
20
+ try {
21
+ return await fn();
22
+ }
23
+ catch (err) {
24
+ const closed = err;
25
+ if (closed.code !== SDKErrorCodes.SESSION_CLOSED)
26
+ throw err;
27
+ return jsonErrorResponse({ error: 'Session is closed', code: SDKErrorCodes.SESSION_CLOSED }, { status: closed.status ?? 409 });
28
+ }
29
+ }
30
+ export function toSharedSessionRecordResponse(sessionId, session, options = {}) {
14
31
  const record = isJsonRecord(session) ? session : {};
15
32
  const id = readNonEmptyString(record.id) ?? sessionId;
16
33
  const now = Date.now();
@@ -24,9 +41,12 @@ function toSharedSessionRecordResponse(sessionId, session, options = {}) {
24
41
  const activeAgentId = readNonEmptyString(record.activeAgentId);
25
42
  const lastAgentId = readNonEmptyString(record.lastAgentId);
26
43
  const lastError = readNonEmptyString(record.lastError);
44
+ const messageCount = Math.max(readFiniteNumber(record.messageCount) ?? 0, options.messageCount ?? 0);
45
+ const retained = readFiniteNumber(record.retainedMessageCount);
27
46
  return {
28
47
  id,
29
48
  kind,
49
+ project: readNonEmptyString(record.project) ?? 'unknown',
30
50
  title: readNonEmptyString(record.title) ?? `Session ${id}`,
31
51
  status,
32
52
  createdAt,
@@ -34,7 +54,8 @@ function toSharedSessionRecordResponse(sessionId, session, options = {}) {
34
54
  ...(lastMessageAt !== undefined ? { lastMessageAt } : {}),
35
55
  ...(closedAt !== undefined ? { closedAt } : {}),
36
56
  lastActivityAt,
37
- messageCount: Math.max(readFiniteNumber(record.messageCount) ?? 0, options.messageCount ?? 0),
57
+ messageCount,
58
+ ...(retained !== undefined && retained < messageCount ? { retainedMessageCount: retained } : {}),
38
59
  pendingInputCount: Math.max(readFiniteNumber(record.pendingInputCount) ?? 0, options.pendingInputCount ?? 0),
39
60
  routeIds: readStringArray(record.routeIds),
40
61
  surfaceKinds: readStringArray(record.surfaceKinds),
@@ -97,16 +118,20 @@ function readParticipants(value) {
97
118
  export function createDaemonRuntimeSessionRouteHandlers(context) {
98
119
  return {
99
120
  createSharedSession: async (request) => withAdmin(context, request, () => handleCreateSharedSession(context, request)),
121
+ registerSharedSession: async (request) => withAdmin(context, request, () => handleRegisterSharedSession(context, request)),
100
122
  postTask: async (request) => withAdmin(context, request, () => handlePostTask(context, request)),
101
123
  getSharedSession: async (sessionId) => handleGetSharedSession(context, sessionId),
102
124
  closeSharedSession: (sessionId, request) => withAdmin(context, request, () => handleSharedSessionLifecycle(context, sessionId, 'close')),
103
125
  reopenSharedSession: (sessionId, request) => withAdmin(context, request, () => handleSharedSessionLifecycle(context, sessionId, 'reopen')),
126
+ detachSharedSession: (sessionId, request) => withAdmin(context, request, () => handleSharedSessionDetach(context, sessionId, request)),
127
+ deleteSharedSession: (sessionId, request) => withAdmin(context, request, () => handleDeleteSharedSession(context, sessionId)),
104
128
  getSharedSessionMessages: async (sessionId, url) => handleGetSharedSessionMessages(context, sessionId, url),
105
129
  getSharedSessionInputs: async (sessionId, url) => handleGetSharedSessionInputs(context, sessionId, url),
106
130
  postSharedSessionMessage: (sessionId, request) => withAdmin(context, request, () => handlePostSharedSessionMessage(context, sessionId, request)),
107
131
  postSharedSessionSteer: (sessionId, request) => withAdmin(context, request, () => handlePostSharedSessionSteer(context, sessionId, request)),
108
132
  postSharedSessionFollowUp: (sessionId, request) => withAdmin(context, request, () => handlePostSharedSessionFollowUp(context, sessionId, request)),
109
133
  cancelSharedSessionInput: (sessionId, inputId, request) => withAdmin(context, request, () => handleCancelSharedSessionInput(context, sessionId, inputId)),
134
+ deliverSharedSessionInput: (sessionId, inputId, request) => withAdmin(context, request, () => handleDeliverSharedSessionInput(context, sessionId, inputId, request)),
110
135
  getRuntimeTask: (taskId) => handleGetRuntimeTask(context, taskId),
111
136
  runtimeTaskAction: (taskId, action, request) => withAdmin(context, request, () => handleRuntimeTaskAction(context, taskId, action, request)),
112
137
  getTaskStatus: (agentId) => handleGetTaskStatus(context, agentId),
@@ -169,7 +194,7 @@ async function handlePostTask(context, req) {
169
194
  return input;
170
195
  const wantsSharedSession = typeof body.sessionId === 'string' || typeof body.routeId === 'string' || typeof body.surfaceKind === 'string';
171
196
  if (wantsSharedSession) {
172
- const submission = await context.sessionBroker.submitMessage({
197
+ const submission = await callOrSessionClosed(() => context.sessionBroker.submitMessage({
173
198
  sessionId: typeof body.sessionId === 'string' ? body.sessionId : undefined,
174
199
  routeId: typeof body.routeId === 'string' ? body.routeId : undefined,
175
200
  surfaceKind: typeof body.surfaceKind === 'string' ? body.surfaceKind : 'web',
@@ -182,7 +207,9 @@ async function handlePostTask(context, req) {
182
207
  body: input.task,
183
208
  metadata: typeof body.metadata === 'object' && body.metadata !== null ? body.metadata : {},
184
209
  ...(input.routing ? { routing: input.routing } : {}),
185
- });
210
+ }));
211
+ if (submission instanceof Response)
212
+ return submission;
186
213
  if (submission.mode === 'continued-live') {
187
214
  return context.recordApiResponse(req, '/task', Response.json({
188
215
  acknowledged: true,
@@ -258,27 +285,6 @@ async function handlePostTask(context, req) {
258
285
  tools: spawnResult.tools,
259
286
  }, { status: 202 }));
260
287
  }
261
- async function handleGetSharedSession(context, sessionId) {
262
- await context.sessionBroker.start();
263
- const session = context.sessionBroker.getSession(sessionId);
264
- if (!session) {
265
- return jsonErrorResponse({ error: 'Unknown shared session' }, { status: 404 });
266
- }
267
- const messages = context.sessionBroker.getMessages(sessionId, 100);
268
- return Response.json({
269
- session: toSharedSessionRecordResponse(sessionId, session, { messageCount: messages.length }),
270
- messages,
271
- });
272
- }
273
- async function handleSharedSessionLifecycle(context, sessionId, action) {
274
- await context.sessionBroker.start();
275
- const session = action === 'close'
276
- ? await context.sessionBroker.closeSession(sessionId)
277
- : await context.sessionBroker.reopenSession(sessionId);
278
- return session
279
- ? Response.json({ session: toSharedSessionRecordResponse(sessionId, session, { status: action === 'close' ? 'closed' : 'active' }) })
280
- : jsonErrorResponse({ error: 'Unknown shared session' }, { status: 404 });
281
- }
282
288
  async function handleGetSharedSessionMessages(context, sessionId, url) {
283
289
  await context.sessionBroker.start();
284
290
  const session = context.sessionBroker.getSession(sessionId);
@@ -299,23 +305,54 @@ async function handleGetSharedSessionInputs(context, sessionId, url) {
299
305
  return jsonErrorResponse({ error: 'Unknown shared session' }, { status: 404 });
300
306
  }
301
307
  const limit = readBoundedLimit(url);
302
- const inputs = context.sessionBroker.getInputs(sessionId, limit);
308
+ // Collection cursor for live surfaces: filter by state (e.g. 'queued') and/or a
309
+ // `since` createdAt cursor (exclusive) so a surface drains only inputs it has
310
+ // not collected yet. Absent params preserve the legacy "last N inputs" behavior.
311
+ const stateParam = readSessionInputState(url.searchParams.get('state'));
312
+ const sinceParam = readPositiveInt(url.searchParams.get('since'));
313
+ const inputs = (stateParam !== undefined || sinceParam !== undefined)
314
+ ? context.sessionBroker.getInputsSince(sessionId, {
315
+ ...(stateParam !== undefined ? { state: stateParam } : {}),
316
+ ...(sinceParam !== undefined ? { since: sinceParam } : {}),
317
+ limit,
318
+ })
319
+ : context.sessionBroker.getInputs(sessionId, limit);
303
320
  return Response.json({
304
321
  session: toSharedSessionRecordResponse(sessionId, session, { pendingInputCount: inputs.length }),
305
322
  inputs,
306
323
  });
307
324
  }
308
- /**
309
- * Handle POST /api/sessions/:sessionId/messages.
310
- *
311
- * Accepts `{body}` in the request payload and returns 400 when it is absent or empty.
312
- */
325
+ const SHARED_SESSION_INPUT_STATES = new Set([
326
+ 'queued', 'delivered', 'spawned', 'completed', 'cancelled', 'failed', 'rejected',
327
+ ]);
328
+ function readSessionInputState(value) {
329
+ return value !== null && SHARED_SESSION_INPUT_STATES.has(value) ? value : undefined;
330
+ }
331
+ function readPositiveInt(value) {
332
+ if (value === null)
333
+ return undefined;
334
+ const parsed = Number(value);
335
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : undefined;
336
+ }
337
+ /** Handle POST /api/sessions/:sessionId/inputs/:inputId/deliver — a live surface reports
338
+ * collection (`{consumed:true}` = completed, else delivered); optional body, null/bare = consumed:false. */
339
+ async function handleDeliverSharedSessionInput(context, sessionId, inputId, req) {
340
+ const body = await context.parseOptionalJsonBody(req);
341
+ if (body instanceof Response)
342
+ return body;
343
+ const consumed = body?.consumed === true;
344
+ const input = await context.sessionBroker.markInputDelivered(sessionId, inputId, { consumed });
345
+ if (!input) {
346
+ return jsonErrorResponse({ error: 'Unknown shared session input' }, { status: 404 });
347
+ }
348
+ return context.recordApiResponse(req, `/api/sessions/${sessionId}/inputs/${inputId}/deliver`, Response.json({ input }, { status: 200 }));
349
+ }
350
+ /** Handle POST /api/sessions/:sessionId/messages. Accepts `{body}`; 400 when absent/empty. */
313
351
  async function handlePostSharedSessionMessage(context, sessionId, req) {
314
352
  const body = await context.parseJsonBody(req);
315
353
  if (body instanceof Response)
316
354
  return body;
317
- // Validate kind field. Ordinary session messages default to conversation
318
- // routing; callers must explicitly opt into kind='task' for agent/WRFC work.
355
+ // Ordinary session messages default to conversation routing; callers must opt into kind='task' for agent/WRFC work.
319
356
  const kind = body.kind === undefined ? 'message' : body.kind;
320
357
  if (kind !== 'task' && kind !== 'message' && kind !== 'followup') {
321
358
  return jsonErrorResponse({ error: `Invalid kind '${String(kind)}'. Accepted values: 'task' | 'message' | 'followup'`, code: 'INVALID_KIND' }, { status: 400 });
@@ -327,7 +364,9 @@ async function handlePostSharedSessionMessage(context, sessionId, req) {
327
364
  const input = buildSharedSessionMessageInput(sessionId, body, message);
328
365
  // kind='followup' — always queues/spawns a follow-up turn via followUpMessage()
329
366
  if (kind === 'followup') {
330
- const followUpSubmission = await context.sessionBroker.followUpMessage(input);
367
+ const followUpSubmission = await callOrSessionClosed(() => context.sessionBroker.followUpMessage(input));
368
+ if (followUpSubmission instanceof Response)
369
+ return followUpSubmission;
331
370
  return await respondToSessionSubmission(context, req, followUpSubmission, message, `/api/sessions/${sessionId}/messages`, 'DaemonServer.handlePostSharedSessionMessage.followup', {
332
371
  context: `shared-session:${followUpSubmission.session.id}`,
333
372
  });
@@ -337,24 +376,22 @@ async function handlePostSharedSessionMessage(context, sessionId, req) {
337
376
  if (kind === 'message') {
338
377
  return handleCompanionMessageKind(context, sessionId, req, input);
339
378
  }
340
- const submission = await context.sessionBroker.submitMessage(input);
379
+ const submission = await callOrSessionClosed(() => context.sessionBroker.submitMessage(input));
380
+ if (submission instanceof Response)
381
+ return submission;
341
382
  return await respondToSessionSubmission(context, req, submission, message, `/api/sessions/${sessionId}/messages`, 'DaemonServer.handlePostSharedSessionMessage', {
342
383
  context: `shared-session:${submission.session.id}`,
343
384
  });
344
385
  }
345
- /**
346
- * Handles kind='message' companion main-chat messages.
347
- *
348
- * Short-circuits before sessionBroker.submitMessage() so conversation messages
349
- * are routed to the existing session instead of spawning continuation work.
350
- */
386
+ /** Handles kind='message' companion main-chat messages — short-circuits before
387
+ * sessionBroker.submitMessage() so conversation messages route to the existing session instead of spawning continuation work. */
351
388
  async function handleCompanionMessageKind(context, sessionId, req, input) {
352
389
  const session = context.sessionBroker.getSession(sessionId);
353
390
  if (!session) {
354
391
  return jsonErrorResponse({ error: 'Unknown shared session', code: 'SESSION_NOT_FOUND' }, { status: 404 });
355
392
  }
356
393
  if (session.status === 'closed') {
357
- return jsonErrorResponse({ error: 'Session is closed', code: 'SESSION_CLOSED' }, { status: 409 });
394
+ return jsonErrorResponse({ error: 'Session is closed', code: SDKErrorCodes.SESSION_CLOSED }, { status: 409 });
358
395
  }
359
396
  const messageId = `companion-${randomUUID()}`;
360
397
  const timestamp = Date.now();
@@ -368,9 +405,8 @@ async function handleCompanionMessageKind(context, sessionId, req, input) {
368
405
  timestamp,
369
406
  ...(metadata ? { metadata } : {}),
370
407
  });
371
- // Notify in-process subscribers via the conversation follow-up event. The
372
- // runtime subscriber turns this persisted message into a normal conversation
373
- // turn whose events stream to both local and remote clients.
408
+ // Notify in-process subscribers via the conversation follow-up event (the
409
+ // runtime subscriber turns it into a normal conversation turn that streams out).
374
410
  context.publishConversationFollowup(sessionId, {
375
411
  messageId,
376
412
  body: input.body,
@@ -378,8 +414,7 @@ async function handleCompanionMessageKind(context, sessionId, req, input) {
378
414
  timestamp,
379
415
  ...(metadata ? { metadata } : {}),
380
416
  });
381
- // The { routedTo: 'conversation' } shape signals the companion app that the message
382
- // was received and persisted (isConversationRouteResult check).
417
+ // { routedTo: 'conversation' } signals the companion app the message was persisted.
383
418
  return context.recordApiResponse(req, `/api/sessions/${sessionId}/messages`, Response.json({
384
419
  messageId,
385
420
  routedTo: 'conversation',
@@ -408,11 +443,8 @@ function buildCompanionMessageMetadata(input) {
408
443
  metadata.displayName = input.displayName;
409
444
  return Object.keys(metadata).length > 0 ? metadata : undefined;
410
445
  }
411
- /**
412
- * Handle GET /api/sessions/:id/events creates a session-scoped SSE stream
413
- * for the companion app to receive turn events (STREAM_DELTA, TURN_COMPLETED,
414
- * etc.) and agent events from the shared session.
415
- */
446
+ /** Handle GET /api/sessions/:id/events — a session-scoped SSE stream for turn events
447
+ * (STREAM_DELTA, TURN_COMPLETED, etc.) and agent events. */
416
448
  async function handleGetSharedSessionEvents(context, sessionId, req) {
417
449
  await context.sessionBroker.start();
418
450
  const session = context.sessionBroker.getSession(sessionId);
@@ -431,10 +463,12 @@ async function handlePostSharedSessionSteer(context, sessionId, req) {
431
463
  if (!message) {
432
464
  return jsonErrorResponse({ error: 'Missing shared session steer body' }, { status: 400 });
433
465
  }
434
- const submission = await context.sessionBroker.steerMessage({
466
+ const submission = await callOrSessionClosed(() => context.sessionBroker.steerMessage({
435
467
  ...buildSharedSessionMessageInput(sessionId, body, message),
436
468
  ...(body.allowSpawnFallback === true ? { allowSpawnFallback: true } : {}),
437
- });
469
+ }));
470
+ if (submission instanceof Response)
471
+ return submission;
438
472
  return await respondToSessionSubmission(context, req, submission, message, `/api/sessions/${sessionId}/steer`, 'DaemonServer.handlePostSharedSessionSteer', {
439
473
  context: `shared-session:${submission.session.id}`,
440
474
  });
@@ -447,7 +481,9 @@ async function handlePostSharedSessionFollowUp(context, sessionId, req) {
447
481
  if (!message) {
448
482
  return jsonErrorResponse({ error: 'Missing shared session follow-up body' }, { status: 400 });
449
483
  }
450
- const submission = await context.sessionBroker.followUpMessage(buildSharedSessionMessageInput(sessionId, body, message));
484
+ const submission = await callOrSessionClosed(() => context.sessionBroker.followUpMessage(buildSharedSessionMessageInput(sessionId, body, message)));
485
+ if (submission instanceof Response)
486
+ return submission;
451
487
  return await respondToSessionSubmission(context, req, submission, message, `/api/sessions/${sessionId}/follow-up`, 'DaemonServer.handlePostSharedSessionFollowUp', {
452
488
  context: `shared-session:${submission.session.id}`,
453
489
  });
@@ -477,14 +513,8 @@ function handleGetRuntimeTask(context, taskId) {
477
513
  return Response.json({ task });
478
514
  }
479
515
  /**
480
- * Extract the message body from an incoming POST body.
481
- *
482
- * Accepts the canonical `body` field from the request envelope.
483
- * Returns an empty string when the field is absent or not a string; the
484
- * caller must check for an empty result and return 400.
485
- *
486
- * @param body - Parsed JSON body from the request.
487
- * @returns Trimmed message string, or '' if none of the accepted fields are present.
516
+ * Extract the canonical `body` field from an incoming POST envelope. Returns ''
517
+ * when absent or non-string; the caller must check for '' and return 400.
488
518
  */
489
519
  export function readSharedSessionMessageBody(body) {
490
520
  return typeof body.body === 'string' ? body.body.trim() : '';
@@ -551,7 +581,12 @@ function readHelperModel(value) {
551
581
  : undefined;
552
582
  }
553
583
  async function respondToSessionSubmission(context, req, submission, taskText, path, logLabel, spawnOptions = {}) {
554
- if (submission.mode === 'continued-live' || submission.mode === 'queued-follow-up') {
584
+ if (submission.mode === 'continued-live' ||
585
+ submission.mode === 'queued-follow-up' ||
586
+ submission.mode === 'queued-for-surface') {
587
+ // queued-for-surface: the input is queued for a live registered surface to
588
+ // collect (sessions.inputs.list) and deliver (sessions.inputs.deliver). No
589
+ // daemon executor is spawned — the surface owns turn execution.
555
590
  return context.recordApiResponse(req, path, Response.json({
556
591
  session: toSharedSessionRecordResponse(submission.session.id, submission.session),
557
592
  message: submission.userMessage ?? null,
@@ -1 +1 @@
1
- {"version":3,"file":"sessions.d.ts","sourceRoot":"","sources":["../src/sessions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,cAAc,CAAC;AAE/D,wBAAsB,qBAAqB,CACzC,GAAG,EAAE,OAAO,EACZ,QAAQ,EAAE,0BAA0B,GACnC,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAwC1B"}
1
+ {"version":3,"file":"sessions.d.ts","sourceRoot":"","sources":["../src/sessions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,cAAc,CAAC;AAE/D,wBAAsB,qBAAqB,CACzC,GAAG,EAAE,OAAO,EACZ,QAAQ,EAAE,0BAA0B,GACnC,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAgD1B"}
package/dist/sessions.js CHANGED
@@ -4,16 +4,23 @@ export async function dispatchSessionRoutes(req, handlers) {
4
4
  const method = req.method;
5
5
  if (pathname === '/api/sessions' && method === 'GET')
6
6
  return handlers.getIntegrationSessions();
7
+ if (pathname === '/api/sessions/register' && method === 'POST')
8
+ return handlers.registerSharedSession(req);
7
9
  if (pathname === '/api/sessions' && method === 'POST')
8
10
  return handlers.createSharedSession(req);
9
11
  const sharedSessionMatch = pathname.match(/^\/api\/sessions\/([^/]+)$/);
10
12
  if (sharedSessionMatch && method === 'GET')
11
13
  return handlers.getSharedSession(sharedSessionMatch[1]);
12
- const sharedSessionCloseMatch = pathname.match(/^\/api\/sessions\/([^/]+)\/(close|reopen)$/);
13
- if (sharedSessionCloseMatch && method === 'POST') {
14
- return sharedSessionCloseMatch[2] === 'close'
15
- ? handlers.closeSharedSession(sharedSessionCloseMatch[1], req)
16
- : handlers.reopenSharedSession(sharedSessionCloseMatch[1], req);
14
+ if (sharedSessionMatch && method === 'DELETE')
15
+ return handlers.deleteSharedSession(sharedSessionMatch[1], req);
16
+ const sharedSessionLifecycleMatch = pathname.match(/^\/api\/sessions\/([^/]+)\/(close|reopen|detach)$/);
17
+ if (sharedSessionLifecycleMatch && method === 'POST') {
18
+ const [, sessionId, action] = sharedSessionLifecycleMatch;
19
+ if (action === 'close')
20
+ return handlers.closeSharedSession(sessionId, req);
21
+ if (action === 'reopen')
22
+ return handlers.reopenSharedSession(sessionId, req);
23
+ return handlers.detachSharedSession(sessionId, req);
17
24
  }
18
25
  const sharedSessionMessagesMatch = pathname.match(/^\/api\/sessions\/([^/]+)\/messages$/);
19
26
  if (sharedSessionMessagesMatch && method === 'GET')
@@ -33,6 +40,10 @@ export async function dispatchSessionRoutes(req, handlers) {
33
40
  if (sharedSessionCancelInputMatch && method === 'POST') {
34
41
  return handlers.cancelSharedSessionInput(sharedSessionCancelInputMatch[1], sharedSessionCancelInputMatch[2], req);
35
42
  }
43
+ const sharedSessionDeliverInputMatch = pathname.match(/^\/api\/sessions\/([^/]+)\/inputs\/([^/]+)\/deliver$/);
44
+ if (sharedSessionDeliverInputMatch && method === 'POST') {
45
+ return handlers.deliverSharedSessionInput(sharedSessionDeliverInputMatch[1], sharedSessionDeliverInputMatch[2], req);
46
+ }
36
47
  const sharedSessionEventsMatch = pathname.match(/^\/api\/sessions\/([^/]+)\/events$/);
37
48
  if (sharedSessionEventsMatch && method === 'GET')
38
49
  return handlers.getSharedSessionEvents(sharedSessionEventsMatch[1], req);
@@ -101,6 +101,13 @@ export interface ApprovalBrokerLike {
101
101
  readonly actor: string;
102
102
  readonly actorSurface: string;
103
103
  readonly note?: string | undefined;
104
+ /**
105
+ * Optional per-hunk selection (edit-tool approvals only). The broker
106
+ * filters the approval's own edit list to these indices server-side. An
107
+ * out-of-range index or a non-edit approval throws a 400-tagged error the
108
+ * route layer converts to an HTTP 400.
109
+ */
110
+ readonly selectedHunks?: readonly number[] | undefined;
104
111
  }): Promise<unknown | null>;
105
112
  }
106
113
  export interface WorkspaceSwapManagerLike {
@@ -120,9 +127,50 @@ export interface WorkspaceSwapManagerLike {
120
127
  reason: string;
121
128
  }>;
122
129
  }
130
+ /**
131
+ * Secret-free status metadata for a single credential held in the daemon's
132
+ * shared store (or overridden by an environment variable). This is the ONLY
133
+ * shape the credential-read wire method (`credentials.get`) ever returns — the
134
+ * plaintext secret value never crosses the wire. `usable` reflects a real
135
+ * in-process resolution attempt, so a configured-but-unresolvable reference
136
+ * (e.g. a broken `op://` ref) reports `configured: true, usable: false`.
137
+ */
138
+ export interface CredentialStatusRecord {
139
+ readonly key: string;
140
+ /** A value exists in the shared store (or, for a named probe, in env). */
141
+ readonly configured: boolean;
142
+ /** The value/reference resolved to non-empty plaintext in-process. */
143
+ readonly usable: boolean;
144
+ /** Where the value was found: 'env' | 'user-secure' | 'project-secure' | … */
145
+ readonly source: string;
146
+ /** 'user' | 'project' | 'env'. */
147
+ readonly scope: string;
148
+ /** Backed by an encrypted store (vs plaintext). */
149
+ readonly secure: boolean;
150
+ /** A same-named environment variable overrides the stored value. */
151
+ readonly overriddenByEnv: boolean;
152
+ /** External-reference provider, when the stored value is a secret ref. */
153
+ readonly refSource?: string | undefined;
154
+ }
155
+ /**
156
+ * Reads credential STATUS (never plaintext) from the daemon's shared secret
157
+ * store. Enumeration (`list`) is over stored keys only — never `process.env` —
158
+ * so it cannot leak the names of unrelated environment variables. A named probe
159
+ * (`get`) may consult env for that one caller-named key.
160
+ */
161
+ export interface CredentialStatusProviderLike {
162
+ list(): Promise<readonly CredentialStatusRecord[]>;
163
+ get(key: string): Promise<CredentialStatusRecord | null>;
164
+ }
123
165
  export interface DaemonSystemRouteContext {
124
166
  readonly approvalBroker: ApprovalBrokerLike;
125
167
  readonly configManager: ConfigManagerLike;
168
+ /**
169
+ * Secret-free credential-status source for `credentials.get`. Null when the
170
+ * daemon host wires no shared secret store — the handler then reports an
171
+ * honest 503 rather than a false "no credentials".
172
+ */
173
+ readonly credentialStatus: CredentialStatusProviderLike | null;
126
174
  readonly integrationHelpers: IntegrationApprovalSnapshotSourceLike | null;
127
175
  readonly inspectInboundTls: (surface: 'controlPlane' | 'httpListener') => unknown;
128
176
  readonly inspectOutboundTls: () => unknown;
@@ -1 +1 @@
1
- {"version":3,"file":"system-route-types.d.ts","sourceRoot":"","sources":["../src/system-route-types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"system-route-types.d.ts","sourceRoot":"","sources":["../src/system-route-types.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"system-routes.d.ts","sourceRoot":"","sources":["../src/system-routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,cAAc,CAAC;AAU9D,OAAO,KAAK,EAMV,wBAAwB,EAEzB,MAAM,yBAAyB,CAAC;AA4CjC,wBAAgB,+BAA+B,CAC7C,OAAO,EAAE,wBAAwB,GAChC,yBAAyB,CA0J3B"}
1
+ {"version":3,"file":"system-routes.d.ts","sourceRoot":"","sources":["../src/system-routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,cAAc,CAAC;AAU9D,OAAO,KAAK,EAMV,wBAAwB,EAEzB,MAAM,yBAAyB,CAAC;AA4CjC,wBAAgB,+BAA+B,CAC7C,OAAO,EAAE,wBAAwB,GAChC,yBAAyB,CAkL3B"}
@@ -182,6 +182,28 @@ export function createDaemonSystemRouteHandlers(context) {
182
182
  return admin;
183
183
  return Response.json(context.configManager.getAll());
184
184
  },
185
+ getCredentials: async (req) => {
186
+ const admin = context.requireAdmin(req);
187
+ if (admin)
188
+ return admin;
189
+ if (!context.credentialStatus) {
190
+ // Honest degraded state: no shared secret store is wired, so we cannot
191
+ // truthfully report credential status. Never fabricate an empty "no
192
+ // credentials" answer that a caller would read as "nothing configured".
193
+ return jsonErrorResponse({ error: 'Shared credential store unavailable', code: 'CREDENTIAL_STORE_UNAVAILABLE' }, { status: 503 });
194
+ }
195
+ const key = new URL(req.url).searchParams.get('key');
196
+ if (key !== null) {
197
+ const trimmed = key.trim();
198
+ if (!trimmed) {
199
+ return jsonErrorResponse({ error: 'Missing or invalid key' }, { status: 400 });
200
+ }
201
+ const record = await context.credentialStatus.get(trimmed);
202
+ return Response.json({ available: true, credentials: record ? [record] : [] });
203
+ }
204
+ const credentials = await context.credentialStatus.list();
205
+ return Response.json({ available: true, credentials });
206
+ },
185
207
  postConfig: async (req) => {
186
208
  const admin = context.requireAdmin(req);
187
209
  if (admin)
@@ -366,14 +388,44 @@ async function handleApprovalAction(context, approvalId, action, req) {
366
388
  ? context.recordApiResponse(req, `/api/approvals/${approvalId}/${action}`, Response.json({ approval }))
367
389
  : context.recordApiResponse(req, `/api/approvals/${approvalId}/${action}`, jsonErrorResponse({ error: 'Unknown approval' }, { status: 404 }));
368
390
  }
369
- const approval = await context.approvalBroker.resolveApproval(approvalId, {
370
- approved: action === 'approve',
371
- remember: typeof payload.remember === 'boolean' ? payload.remember : false,
372
- actor,
373
- actorSurface: 'web',
374
- note,
375
- });
391
+ const selectedHunks = action === 'approve' ? readSelectedHunks(payload.selectedHunks) : undefined;
392
+ if (selectedHunks instanceof Response) {
393
+ return context.recordApiResponse(req, `/api/approvals/${approvalId}/${action}`, selectedHunks);
394
+ }
395
+ let approval;
396
+ try {
397
+ approval = await context.approvalBroker.resolveApproval(approvalId, {
398
+ approved: action === 'approve',
399
+ remember: typeof payload.remember === 'boolean' ? payload.remember : false,
400
+ actor,
401
+ actorSurface: 'web',
402
+ note,
403
+ ...(selectedHunks !== undefined ? { selectedHunks } : {}),
404
+ });
405
+ }
406
+ catch (error) {
407
+ // The broker throws a 400-tagged error for an out-of-range or non-edit
408
+ // per-hunk selection. Surface it as an honest HTTP 400, not a 500.
409
+ const status = typeof error?.status === 'number' ? error.status : 500;
410
+ const message = error instanceof Error ? error.message : 'Approval resolution failed.';
411
+ return context.recordApiResponse(req, `/api/approvals/${approvalId}/${action}`, jsonErrorResponse({ error: message }, { status }));
412
+ }
376
413
  return approval
377
414
  ? context.recordApiResponse(req, `/api/approvals/${approvalId}/${action}`, Response.json({ approval }))
378
415
  : context.recordApiResponse(req, `/api/approvals/${approvalId}/${action}`, jsonErrorResponse({ error: 'Unknown approval' }, { status: 404 }));
379
416
  }
417
+ /**
418
+ * Read an optional selectedHunks array off the request payload. Returns the
419
+ * validated number[] when present and well-formed, undefined when absent, or a
420
+ * 400 Response when present but malformed (non-array, or an entry that is not a
421
+ * finite integer). Range validation against the specific approval's hunk count
422
+ * is the broker's job (it owns the pending edit list).
423
+ */
424
+ function readSelectedHunks(value) {
425
+ if (value === undefined || value === null)
426
+ return undefined;
427
+ if (!Array.isArray(value) || !value.every((entry) => Number.isInteger(entry))) {
428
+ return jsonErrorResponse({ error: 'selectedHunks must be an array of integer hunk indices.' }, { status: 400 });
429
+ }
430
+ return value;
431
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pellux/goodvibes-daemon-sdk",
3
- "version": "0.38.0",
3
+ "version": "1.1.0",
4
4
  "engines": {
5
5
  "bun": "1.3.10",
6
6
  "node": ">=22.0.0"
@@ -157,8 +157,8 @@
157
157
  "control-plane"
158
158
  ],
159
159
  "dependencies": {
160
- "@pellux/goodvibes-contracts": "0.38.0",
161
- "@pellux/goodvibes-errors": "0.38.0"
160
+ "@pellux/goodvibes-contracts": "1.1.0",
161
+ "@pellux/goodvibes-errors": "1.1.0"
162
162
  },
163
163
  "publishConfig": {
164
164
  "access": "public"