@saluzi/saluzi-edu 0.4.1 → 0.4.2

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saluzi/saluzi-edu",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "Saluzi CLI - interactive AI coding assistant in the terminal",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -399,6 +399,91 @@ describe('V1 Session Routes', () => {
399
399
  body: JSON.stringify({ events: [{ type: 'init', data: 'starting' }] }),
400
400
  })
401
401
  expect(sessRes.status).toBe(200)
402
+ const { id } = await sessRes.json()
403
+ // Legacy raw shape — published verbatim as before (type from the event,
404
+ // direction outbound).
405
+ const events = getEventBus(id).getEventsSince(0)
406
+ expect(events).toHaveLength(1)
407
+ expect(events[0]?.type).toBe('init')
408
+ expect(events[0]?.direction).toBe('outbound')
409
+ })
410
+
411
+ test('POST /v1/sessions with CCR-wrapped system/init seed — publishes a typed system event the Web UI scan can find', async () => {
412
+ // `slz rcs one`'s headless worker seeds its pre-created session with a
413
+ // system/init so the Web UI has the worker's command/skill tables and
414
+ // capabilities before the first message spawns a --print child. The
415
+ // create route must unwrap { type: 'event', data: SDKMessage } and run
416
+ // the inner message through the standard ingest normalization —
417
+ // publishing the raw wrapper would yield an opaque 'event'-typed entry
418
+ // no consumer (loadHistory worker-info scan, live SSE handler) matches.
419
+ const sessRes = await app.request('/v1/sessions', {
420
+ method: 'POST',
421
+ headers: { ...AUTH_HEADERS, 'Content-Type': 'application/json' },
422
+ body: JSON.stringify({
423
+ events: [
424
+ {
425
+ type: 'event',
426
+ data: {
427
+ type: 'system',
428
+ subtype: 'init',
429
+ uuid: 'seed-init-uuid',
430
+ slash_commands: ['compact'],
431
+ slash_command_details: [
432
+ {
433
+ name: 'compact',
434
+ description: 'Shrink context',
435
+ type: 'local',
436
+ },
437
+ ],
438
+ skills: ['code-review'],
439
+ skill_details: [
440
+ {
441
+ name: 'code-review',
442
+ description: 'Review code',
443
+ type: 'prompt',
444
+ },
445
+ ],
446
+ remote_capabilities: {
447
+ file_search: true,
448
+ clear: true,
449
+ set_max_thinking_tokens: true,
450
+ get_context_usage: true,
451
+ stop_task: true,
452
+ set_model: true,
453
+ set_permission_mode: true,
454
+ interrupt: true,
455
+ end_session: true,
456
+ mcp_management: true,
457
+ remote_settings: true,
458
+ rewind: true,
459
+ set_effort: true,
460
+ side_question: true,
461
+ },
462
+ },
463
+ },
464
+ ],
465
+ }),
466
+ })
467
+ expect(sessRes.status).toBe(200)
468
+ const { id } = await sessRes.json()
469
+
470
+ const events = getEventBus(id).getEventsSince(0)
471
+ expect(events).toHaveLength(1)
472
+ expect(events[0]?.type).toBe('system')
473
+ expect(events[0]?.direction).toBe('inbound')
474
+ const payload = events[0]?.payload as Record<string, unknown>
475
+ expect(payload.subtype).toBe('init')
476
+ expect(payload.slash_command_details).toEqual([
477
+ { name: 'compact', description: 'Shrink context', type: 'local' },
478
+ ])
479
+ expect(payload.skill_details).toEqual([
480
+ { name: 'code-review', description: 'Review code', type: 'prompt' },
481
+ ])
482
+ expect(
483
+ (payload.remote_capabilities as Record<string, unknown>).set_model,
484
+ ).toBe(true)
485
+ // raw keeps the full SDK message for the adapter's payload.raw fallback.
486
+ expect((payload.raw as Record<string, unknown>).subtype).toBe('init')
402
487
  })
403
488
  })
404
489
 
@@ -11,6 +11,7 @@ import {
11
11
  } from '../../services/session'
12
12
  import { createWorkItem } from '../../services/work-dispatch'
13
13
  import { publishSessionEvent } from '../../services/transport'
14
+ import { ingestBridgeMessage } from '../../transport/ws-handler'
14
15
  import { loadPersistedEvents } from '../../services/event-persistence'
15
16
  import { getEventBus } from '../../transport/event-bus'
16
17
  import { touchEnvironmentActivity } from '../../services/environment'
@@ -225,7 +226,26 @@ app.post('/', acceptCliHeaders, apiKeyAuth, async c => {
225
226
  // Publish initial events if provided
226
227
  if (body.events && Array.isArray(body.events)) {
227
228
  for (const evt of body.events) {
228
- publishSessionEvent(session.id, evt.type || 'init', evt, 'outbound')
229
+ // CCR wire format { type: 'event', data: SDKMessage } — unwrap and run
230
+ // the inner SDK message through the standard ingest path (event-type
231
+ // derivation + payload normalization, e.g. system/init's
232
+ // slash_command_details / remote_capabilities preservation). Publishing
233
+ // the raw wrapper as-is yields an opaque 'event'-typed entry no
234
+ // consumer recognizes.
235
+ const data =
236
+ evt &&
237
+ typeof evt === 'object' &&
238
+ evt.type === 'event' &&
239
+ evt.data &&
240
+ typeof evt.data === 'object' &&
241
+ typeof evt.data.type === 'string'
242
+ ? evt.data
243
+ : null
244
+ if (data) {
245
+ ingestBridgeMessage(session.id, data)
246
+ } else {
247
+ publishSessionEvent(session.id, evt.type || 'init', evt, 'outbound')
248
+ }
229
249
  }
230
250
  }
231
251