cccc-sdk 0.1.4 → 0.4.3

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/dist/client.js CHANGED
@@ -1,8 +1,7 @@
1
1
  /**
2
2
  * CCCC SDK client
3
3
  */
4
- import { DaemonAPIError, IncompatibleDaemonError, ErrorCodes, } from './errors.js';
5
- import { isStreamEvent } from './types.js';
4
+ import { DaemonAPIError, IncompatibleDaemonError, } from './errors.js';
6
5
  import { discoverEndpoint, callDaemon, openEventsStream, readLines, } from './transport.js';
7
6
  /**
8
7
  * Client for communicating with the CCCC daemon over IPC (Unix socket or TCP).
@@ -51,23 +50,22 @@ export class CCCCClient {
51
50
  args: args ?? {},
52
51
  };
53
52
  const response = await callDaemon(this._endpoint, request, this._timeoutMs);
54
- if (!response.ok) {
55
- throw new DaemonAPIError(response.error?.code ?? 'error', response.error?.message ?? 'daemon error', response.error?.details ?? {}, response);
53
+ if (!response.ok && response.error) {
54
+ throw new DaemonAPIError(response.error.code ?? 'error', response.error.message ?? 'daemon error', response.error.details ?? {}, response);
56
55
  }
57
56
  return response;
58
57
  }
59
58
  /**
60
59
  * Send an IPC request and return only the result payload.
61
- * @typeParam T - Expected result type (defaults to `Record<string, unknown>`).
62
60
  * @param op - The IPC operation name.
63
61
  * @param args - Operation arguments.
64
- * @returns The `result` field from the daemon response, cast to `T`.
62
+ * @returns The `result` field from the daemon response (empty object if absent).
65
63
  * @throws {DaemonAPIError} If the daemon returns an error.
66
64
  * @throws {DaemonUnavailableError} If the connection fails.
67
65
  */
68
66
  async call(op, args) {
69
67
  const response = await this.callRaw(op, args);
70
- return (response.result ?? {});
68
+ return response.result ?? {};
71
69
  }
72
70
  /**
73
71
  * Assert that the connected daemon meets the caller's compatibility requirements.
@@ -79,8 +77,12 @@ export class CCCCClient {
79
77
  */
80
78
  async assertCompatible(options = {}) {
81
79
  const pingResult = await this.ping();
82
- const ipcV = pingResult.ipc_v ?? 0;
83
- const capabilities = pingResult.capabilities ?? {};
80
+ const rawIpcV = pingResult['ipc_v'];
81
+ const ipcV = typeof rawIpcV === 'number' ? rawIpcV : 0;
82
+ const rawCaps = pingResult['capabilities'];
83
+ const capabilities = (rawCaps !== null && typeof rawCaps === 'object' && !Array.isArray(rawCaps))
84
+ ? rawCaps
85
+ : {};
84
86
  // Check IPC version
85
87
  const requiredV = options.requireIpcV ?? 1;
86
88
  if (ipcV < requiredV) {
@@ -101,7 +103,7 @@ export class CCCCClient {
101
103
  await this.callRaw(op, {});
102
104
  }
103
105
  catch (e) {
104
- if (e instanceof DaemonAPIError && e.code === ErrorCodes.UNKNOWN_OP) {
106
+ if (e instanceof DaemonAPIError && e.code === 'unknown_op') {
105
107
  throw new IncompatibleDaemonError(`Operation not supported: ${op}`);
106
108
  }
107
109
  // Other errors (e.g. missing_group_id) imply the operation exists.
@@ -205,7 +207,7 @@ export class CCCCClient {
205
207
  async groupAutomationManage(options) {
206
208
  const actions = options.actions;
207
209
  if (actions.length === 0) {
208
- throw new DaemonAPIError(ErrorCodes.INVALID_ARGS, 'groupAutomationManage requires a non-empty actions array', {});
210
+ throw new DaemonAPIError('invalid_args', 'groupAutomationManage requires a non-empty actions array', {});
209
211
  }
210
212
  const args = {
211
213
  group_id: options.groupId,
@@ -274,6 +276,8 @@ export class CCCCClient {
274
276
  command: options.command,
275
277
  env: options.env,
276
278
  env_private: options.envPrivate,
279
+ capability_autoload: options.capabilityAutoload,
280
+ profile_id: options.profileId,
277
281
  default_scope_key: options.defaultScopeKey,
278
282
  submit: options.submit,
279
283
  };
@@ -291,12 +295,17 @@ export class CCCCClient {
291
295
  * Update actor
292
296
  */
293
297
  async actorUpdate(options) {
294
- return this.call('actor_update', {
298
+ const args = {
295
299
  group_id: options.groupId,
296
300
  actor_id: options.actorId,
297
- patch: options.patch,
301
+ patch: options.patch ?? {},
298
302
  by: options.by ?? 'user',
299
- });
303
+ };
304
+ if (options.profileId != null)
305
+ args['profile_id'] = options.profileId;
306
+ if (options.profileAction != null)
307
+ args['profile_action'] = options.profileAction;
308
+ return this.call('actor_update', args);
300
309
  }
301
310
  /**
302
311
  * Remove actor
@@ -305,25 +314,10 @@ export class CCCCClient {
305
314
  return this.call('actor_remove', { group_id: groupId, actor_id: actorId, by });
306
315
  }
307
316
  /**
308
- * Start actor.
309
- * @param groupId - Group ID.
310
- * @param actorId - Actor ID.
311
- * @param options - Optional settings. Pass `{ idempotent: true }` to suppress errors when the actor is already running.
312
- * @returns The daemon result.
313
- * @throws {DaemonAPIError} On error (unless idempotent and actor already running).
317
+ * Start actor
314
318
  */
315
- async actorStart(groupId, actorId, options) {
316
- const by = typeof options === 'string' ? options : (options?.by ?? 'user');
317
- const idempotent = typeof options === 'object' && options?.idempotent === true;
318
- try {
319
- return await this.call('actor_start', { group_id: groupId, actor_id: actorId, by });
320
- }
321
- catch (e) {
322
- if (idempotent && e instanceof DaemonAPIError && e.code === ErrorCodes.CONFLICT) {
323
- return {};
324
- }
325
- throw e;
326
- }
319
+ async actorStart(groupId, actorId, by = 'user') {
320
+ return this.call('actor_start', { group_id: groupId, actor_id: actorId, by });
327
321
  }
328
322
  /**
329
323
  * Stop actor
@@ -359,13 +353,299 @@ export class CCCCClient {
359
353
  args['unset'] = options.unset;
360
354
  return this.call('actor_env_private_update', args);
361
355
  }
356
+ /**
357
+ * List global actor profiles.
358
+ */
359
+ async actorProfileList(by = 'user') {
360
+ return this.call('actor_profile_list', { by });
361
+ }
362
+ /**
363
+ * Get one actor profile and current usage.
364
+ */
365
+ async actorProfileGet(profileId, by = 'user') {
366
+ return this.call('actor_profile_get', { profile_id: profileId, by });
367
+ }
368
+ /**
369
+ * Create/update one actor profile.
370
+ */
371
+ async actorProfileUpsert(options) {
372
+ const profile = { ...options.profile };
373
+ if ('capabilityDefaults' in profile) {
374
+ const rawDefaults = profile['capabilityDefaults'];
375
+ delete profile['capabilityDefaults'];
376
+ if (rawDefaults != null && typeof rawDefaults === 'object' && !Array.isArray(rawDefaults)) {
377
+ const defaults = rawDefaults;
378
+ profile['capability_defaults'] = {
379
+ ...(defaults['autoloadCapabilities'] !== undefined
380
+ ? { autoload_capabilities: defaults['autoloadCapabilities'] }
381
+ : {}),
382
+ ...(defaults['defaultScope'] !== undefined
383
+ ? { default_scope: defaults['defaultScope'] }
384
+ : {}),
385
+ ...(defaults['sessionTtlSeconds'] !== undefined
386
+ ? { session_ttl_seconds: defaults['sessionTtlSeconds'] }
387
+ : {}),
388
+ };
389
+ }
390
+ else {
391
+ profile['capability_defaults'] = rawDefaults;
392
+ }
393
+ }
394
+ const args = {
395
+ profile,
396
+ by: options.by ?? 'user',
397
+ };
398
+ if (options.expectedRevision !== undefined)
399
+ args['expected_revision'] = options.expectedRevision;
400
+ return this.call('actor_profile_upsert', args);
401
+ }
402
+ /**
403
+ * Delete one actor profile (rejected when still in use).
404
+ */
405
+ async actorProfileDelete(profileId, by = 'user', forceDetach = false) {
406
+ return this.call('actor_profile_delete', { profile_id: profileId, by, force_detach: forceDetach });
407
+ }
408
+ /**
409
+ * List profile-level secret keys and masked previews.
410
+ */
411
+ async actorProfileSecretKeys(profileId, by = 'user') {
412
+ return this.call('actor_profile_secret_keys', { profile_id: profileId, by });
413
+ }
414
+ /**
415
+ * Update profile-level private env.
416
+ */
417
+ async actorProfileSecretUpdate(options) {
418
+ const args = {
419
+ profile_id: options.profileId,
420
+ by: options.by ?? 'user',
421
+ clear: options.clear ?? false,
422
+ };
423
+ if (options.set)
424
+ args['set'] = options.set;
425
+ if (options.unset)
426
+ args['unset'] = options.unset;
427
+ return this.call('actor_profile_secret_update', args);
428
+ }
429
+ /**
430
+ * Copy one actor's runtime env (public + private) into a profile's private env.
431
+ */
432
+ async actorProfileSecretCopyFromActor(options) {
433
+ return this.call('actor_profile_secret_copy_from_actor', {
434
+ profile_id: options.profileId,
435
+ group_id: options.groupId,
436
+ actor_id: options.actorId,
437
+ by: options.by ?? 'user',
438
+ });
439
+ }
440
+ /**
441
+ * Copy one profile's secrets into another profile.
442
+ */
443
+ async actorProfileSecretCopyFromProfile(options) {
444
+ return this.call('actor_profile_secret_copy_from_profile', {
445
+ profile_id: options.profileId,
446
+ source_profile_id: options.sourceProfileId,
447
+ by: options.by ?? 'user',
448
+ });
449
+ }
450
+ // ============================================================
451
+ // Convenience methods: capabilities
452
+ // ============================================================
453
+ /**
454
+ * Read the global capability overview snapshot.
455
+ */
456
+ async capabilityOverview(options = {}) {
457
+ const args = {};
458
+ if (options.query)
459
+ args['query'] = options.query;
460
+ if (options.limit !== undefined)
461
+ args['limit'] = options.limit;
462
+ if (options.includeIndexed !== undefined)
463
+ args['include_indexed'] = options.includeIndexed;
464
+ return this.call('capability_overview', args);
465
+ }
466
+ /**
467
+ * Search the capability registry for one group/caller scope.
468
+ */
469
+ async capabilitySearch(options) {
470
+ const args = {
471
+ group_id: options.groupId,
472
+ by: options.by ?? 'user',
473
+ };
474
+ if (options.actorId)
475
+ args['actor_id'] = options.actorId;
476
+ if (options.query)
477
+ args['query'] = options.query;
478
+ if (options.kind !== undefined)
479
+ args['kind'] = options.kind;
480
+ if (options.sourceId)
481
+ args['source_id'] = options.sourceId;
482
+ if (options.trustTier)
483
+ args['trust_tier'] = options.trustTier;
484
+ if (options.qualificationStatus !== undefined)
485
+ args['qualification_status'] = options.qualificationStatus;
486
+ if (options.includeExternal !== undefined)
487
+ args['include_external'] = options.includeExternal;
488
+ if (options.limit !== undefined)
489
+ args['limit'] = options.limit;
490
+ return this.call('capability_search', args);
491
+ }
492
+ /**
493
+ * Enable or disable a capability.
494
+ */
495
+ async capabilityEnable(options) {
496
+ const args = {
497
+ group_id: options.groupId,
498
+ capability_id: options.capabilityId,
499
+ scope: options.scope ?? 'session',
500
+ enabled: options.enabled ?? true,
501
+ cleanup: options.cleanup ?? false,
502
+ by: options.by ?? 'user',
503
+ };
504
+ if (options.reason)
505
+ args['reason'] = options.reason;
506
+ if (options.ttlSeconds !== undefined)
507
+ args['ttl_seconds'] = options.ttlSeconds;
508
+ if (options.actorId)
509
+ args['actor_id'] = options.actorId;
510
+ return this.call('capability_enable', args);
511
+ }
512
+ /**
513
+ * Block or unblock a capability.
514
+ */
515
+ async capabilityBlock(options) {
516
+ const args = {
517
+ group_id: options.groupId,
518
+ capability_id: options.capabilityId,
519
+ scope: options.scope ?? 'group',
520
+ blocked: options.blocked ?? true,
521
+ by: options.by ?? 'user',
522
+ };
523
+ if (options.ttlSeconds !== undefined)
524
+ args['ttl_seconds'] = options.ttlSeconds;
525
+ if (options.reason)
526
+ args['reason'] = options.reason;
527
+ if (options.actorId)
528
+ args['actor_id'] = options.actorId;
529
+ return this.call('capability_block', args);
530
+ }
531
+ /**
532
+ * Read effective capability exposure for one caller scope.
533
+ */
534
+ async capabilityState(options) {
535
+ const args = {
536
+ group_id: options.groupId,
537
+ by: options.by ?? 'user',
538
+ };
539
+ if (options.actorId)
540
+ args['actor_id'] = options.actorId;
541
+ return this.call('capability_state', args);
542
+ }
543
+ /**
544
+ * Read capability allowlist default, overlay, and effective snapshots.
545
+ */
546
+ async capabilityAllowlistGet(options = {}) {
547
+ return this.call('capability_allowlist_get', {
548
+ by: options.by ?? 'user',
549
+ });
550
+ }
551
+ /**
552
+ * Dry-run capability allowlist overlay validation without persistence.
553
+ */
554
+ async capabilityAllowlistValidate(options = {}) {
555
+ const args = {
556
+ mode: options.mode ?? 'patch',
557
+ };
558
+ if (options.patch !== undefined)
559
+ args['patch'] = options.patch;
560
+ if (options.overlay !== undefined)
561
+ args['overlay'] = options.overlay;
562
+ return this.call('capability_allowlist_validate', args);
563
+ }
564
+ /**
565
+ * Persist capability allowlist overlay with optional optimistic concurrency.
566
+ */
567
+ async capabilityAllowlistUpdate(options = {}) {
568
+ const args = {
569
+ by: options.by ?? 'user',
570
+ mode: options.mode ?? 'patch',
571
+ };
572
+ if (options.expectedRevision)
573
+ args['expected_revision'] = options.expectedRevision;
574
+ if (options.patch !== undefined)
575
+ args['patch'] = options.patch;
576
+ if (options.overlay !== undefined)
577
+ args['overlay'] = options.overlay;
578
+ return this.call('capability_allowlist_update', args);
579
+ }
580
+ /**
581
+ * Reset capability allowlist overlay to empty/default state.
582
+ */
583
+ async capabilityAllowlistReset(options = {}) {
584
+ return this.call('capability_allowlist_reset', {
585
+ by: options.by ?? 'user',
586
+ });
587
+ }
588
+ /**
589
+ * Import one structured capability record, with optional readiness probe.
590
+ */
591
+ async capabilityImport(options) {
592
+ const args = {
593
+ group_id: options.groupId,
594
+ record: options.record,
595
+ by: options.by ?? 'user',
596
+ dry_run: options.dryRun ?? false,
597
+ };
598
+ if (options.actorId)
599
+ args['actor_id'] = options.actorId;
600
+ if (options.probe !== undefined)
601
+ args['probe'] = options.probe;
602
+ if (options.enableAfterImport !== undefined)
603
+ args['enable_after_import'] = options.enableAfterImport;
604
+ if (options.scope)
605
+ args['scope'] = options.scope;
606
+ if (options.ttlSeconds !== undefined)
607
+ args['ttl_seconds'] = options.ttlSeconds;
608
+ if (options.reason)
609
+ args['reason'] = options.reason;
610
+ return this.call('capability_import', args);
611
+ }
612
+ /**
613
+ * Uninstall a capability from the target group scope.
614
+ */
615
+ async capabilityUninstall(options) {
616
+ const args = {
617
+ group_id: options.groupId,
618
+ capability_id: options.capabilityId,
619
+ by: options.by ?? 'user',
620
+ };
621
+ if (options.reason)
622
+ args['reason'] = options.reason;
623
+ if (options.actorId)
624
+ args['actor_id'] = options.actorId;
625
+ return this.call('capability_uninstall', args);
626
+ }
627
+ /**
628
+ * Call one enabled dynamic capability tool through daemon IPC.
629
+ */
630
+ async capabilityToolCall(options) {
631
+ const args = {
632
+ group_id: options.groupId,
633
+ tool_name: options.toolName,
634
+ by: options.by ?? 'user',
635
+ };
636
+ if (options.arguments)
637
+ args['arguments'] = options.arguments;
638
+ if (options.actorId)
639
+ args['actor_id'] = options.actorId;
640
+ return this.call('capability_tool_call', args);
641
+ }
362
642
  // ============================================================
363
643
  // Convenience methods: messaging
364
644
  // ============================================================
365
645
  /**
366
646
  * Send a chat message to a group.
367
647
  * @param options - Message content, recipients, and priority.
368
- * @returns The created event and optional ack event.
648
+ * @returns The daemon result (includes event id).
369
649
  * @throws {DaemonAPIError} On invalid group, missing permissions, etc.
370
650
  */
371
651
  async send(options) {
@@ -383,8 +663,7 @@ export class CCCCClient {
383
663
  return this.call('send', args);
384
664
  }
385
665
  /**
386
- * Send message across groups.
387
- * @returns The created event and optional ack event.
666
+ * Send message across groups
388
667
  */
389
668
  async sendCrossGroup(options) {
390
669
  const args = {
@@ -400,8 +679,7 @@ export class CCCCClient {
400
679
  return this.call('send_cross_group', args);
401
680
  }
402
681
  /**
403
- * Reply to a message.
404
- * @returns The created event and optional ack event.
682
+ * Reply message
405
683
  */
406
684
  async reply(options) {
407
685
  const args = {
@@ -416,55 +694,6 @@ export class CCCCClient {
416
694
  args['to'] = options.to;
417
695
  return this.call('reply', args);
418
696
  }
419
- /**
420
- * Send a message and wait for a reply to it.
421
- * Opens the event stream **before** sending to avoid race conditions,
422
- * then yields control to the stream until a matching reply arrives.
423
- *
424
- * @param options - Send options plus `listenAs` (the actor to listen as) and optional `waitTimeoutMs`.
425
- * @returns The reply event (a `chat.message` whose `reply_to` matches the sent event ID).
426
- * @throws {DaemonAPIError} On send/stream errors.
427
- * @throws {Error} If the timeout elapses or the signal is aborted before a reply arrives.
428
- */
429
- async sendAndWaitForReply(options) {
430
- const waitTimeout = options.waitTimeoutMs ?? 60000;
431
- const deadline = Date.now() + waitTimeout;
432
- // Open stream FIRST (with sinceTs = now) to avoid missing replies
433
- // that arrive between send() returning and stream connecting.
434
- const stream = this.eventsStream({
435
- groupId: options.groupId,
436
- by: options.listenAs,
437
- kinds: ['chat.message'],
438
- sinceTs: new Date().toISOString(),
439
- signal: options.signal,
440
- });
441
- // Now send the message
442
- const sendResult = await this.send(options);
443
- const sentEventId = sendResult.event.id;
444
- try {
445
- for await (const item of stream) {
446
- if (options.signal?.aborted) {
447
- throw new Error('sendAndWaitForReply aborted');
448
- }
449
- if (Date.now() > deadline) {
450
- throw new Error(`sendAndWaitForReply timed out after ${waitTimeout}ms`);
451
- }
452
- if (isStreamEvent(item)) {
453
- const evt = item.event;
454
- if (evt.kind === 'chat.message') {
455
- const data = evt.data;
456
- if (data['reply_to'] === sentEventId) {
457
- return evt;
458
- }
459
- }
460
- }
461
- }
462
- }
463
- finally {
464
- stream.return(undefined);
465
- }
466
- throw new Error('sendAndWaitForReply: stream ended without reply');
467
- }
468
697
  /**
469
698
  * Acknowledge chat message
470
699
  */
@@ -476,56 +705,6 @@ export class CCCCClient {
476
705
  by: by ?? actorId,
477
706
  });
478
707
  }
479
- /**
480
- * Send a file as a chat attachment.
481
- * @param options - File path (relative to scope root), optional caption and recipients.
482
- * @returns The created event and optional ack event.
483
- */
484
- async fileSend(options) {
485
- const args = {
486
- group_id: options.groupId,
487
- path: options.path,
488
- by: options.by ?? 'user',
489
- priority: options.priority ?? 'normal',
490
- reply_required: options.replyRequired ?? false,
491
- };
492
- if (options.text)
493
- args['text'] = options.text;
494
- if (options.to)
495
- args['to'] = options.to;
496
- return this.call('file_send', args);
497
- }
498
- /**
499
- * Get recent chat messages from the ledger.
500
- * @param options - Group ID, limit, and max chars.
501
- * @returns The ledger tail result.
502
- */
503
- async ledgerTail(options) {
504
- const args = {
505
- group_id: options.groupId,
506
- by: options.by ?? 'user',
507
- };
508
- if (options.limit !== undefined)
509
- args['limit'] = options.limit;
510
- if (options.maxChars !== undefined)
511
- args['max_chars'] = options.maxChars;
512
- return this.call('ledger_tail', args);
513
- }
514
- /**
515
- * Get recent terminal output from an actor.
516
- * @param options - Group ID, actor ID, and line count.
517
- * @returns The terminal tail result.
518
- */
519
- async terminalTail(options) {
520
- const args = {
521
- group_id: options.groupId,
522
- actor_id: options.actorId,
523
- by: options.by ?? 'user',
524
- };
525
- if (options.lines !== undefined)
526
- args['lines'] = options.lines;
527
- return this.call('terminal_tail', args);
528
- }
529
708
  // ============================================================
530
709
  // Convenience methods: inbox
531
710
  // ============================================================
@@ -598,20 +777,219 @@ export class CCCCClient {
598
777
  });
599
778
  }
600
779
  // ============================================================
780
+ // Convenience methods: Group Space
781
+ // ============================================================
782
+ /**
783
+ * Read Group Space provider and binding status.
784
+ */
785
+ async groupSpaceStatus(options) {
786
+ return this.call('group_space_status', {
787
+ group_id: options.groupId,
788
+ provider: options.provider ?? 'notebooklm',
789
+ });
790
+ }
791
+ /**
792
+ * List available remote spaces for binding.
793
+ */
794
+ async groupSpaceSpaces(options) {
795
+ return this.call('group_space_spaces', {
796
+ group_id: options.groupId,
797
+ provider: options.provider ?? 'notebooklm',
798
+ });
799
+ }
800
+ /**
801
+ * Read the provider capability matrix for a group.
802
+ */
803
+ async groupSpaceCapabilities(options) {
804
+ return this.call('group_space_capabilities', {
805
+ group_id: options.groupId,
806
+ provider: options.provider ?? 'notebooklm',
807
+ });
808
+ }
809
+ /**
810
+ * Bind or unbind one Group Space lane.
811
+ */
812
+ async groupSpaceBind(options) {
813
+ const args = {
814
+ group_id: options.groupId,
815
+ provider: options.provider ?? 'notebooklm',
816
+ lane: options.lane,
817
+ action: options.action ?? 'bind',
818
+ by: options.by ?? 'user',
819
+ };
820
+ if (options.remoteSpaceId)
821
+ args['remote_space_id'] = options.remoteSpaceId;
822
+ return this.call('group_space_bind', args);
823
+ }
824
+ /**
825
+ * Enqueue one Group Space ingest action.
826
+ */
827
+ async groupSpaceIngest(options) {
828
+ const args = {
829
+ group_id: options.groupId,
830
+ provider: options.provider ?? 'notebooklm',
831
+ lane: options.lane,
832
+ kind: options.kind ?? 'context_sync',
833
+ by: options.by ?? 'user',
834
+ };
835
+ if (options.payload)
836
+ args['payload'] = options.payload;
837
+ if (options.idempotencyKey)
838
+ args['idempotency_key'] = options.idempotencyKey;
839
+ return this.call('group_space_ingest', args);
840
+ }
841
+ /**
842
+ * Query Group Space knowledge for one lane.
843
+ */
844
+ async groupSpaceQuery(options) {
845
+ const args = {
846
+ group_id: options.groupId,
847
+ provider: options.provider ?? 'notebooklm',
848
+ lane: options.lane,
849
+ query: options.query,
850
+ };
851
+ if (options.options)
852
+ args['options'] = options.options;
853
+ return this.call('group_space_query', args);
854
+ }
855
+ /**
856
+ * Manage remote sources in the bound Group Space lane.
857
+ */
858
+ async groupSpaceSources(options) {
859
+ const args = {
860
+ group_id: options.groupId,
861
+ provider: options.provider ?? 'notebooklm',
862
+ lane: options.lane,
863
+ action: options.action ?? 'list',
864
+ by: options.by ?? 'user',
865
+ };
866
+ if (options.sourceId)
867
+ args['source_id'] = options.sourceId;
868
+ if (options.newTitle)
869
+ args['new_title'] = options.newTitle;
870
+ return this.call('group_space_sources', args);
871
+ }
872
+ /**
873
+ * List, generate, or download Group Space artifacts.
874
+ */
875
+ async groupSpaceArtifact(options) {
876
+ const args = {
877
+ group_id: options.groupId,
878
+ provider: options.provider ?? 'notebooklm',
879
+ lane: options.lane,
880
+ action: options.action ?? 'list',
881
+ by: options.by ?? 'user',
882
+ };
883
+ if (options.kind)
884
+ args['kind'] = options.kind;
885
+ if (options.options)
886
+ args['options'] = options.options;
887
+ if (options.wait !== undefined)
888
+ args['wait'] = options.wait;
889
+ if (options.saveToSpace !== undefined)
890
+ args['save_to_space'] = options.saveToSpace;
891
+ if (options.outputPath)
892
+ args['output_path'] = options.outputPath;
893
+ if (options.outputFormat)
894
+ args['output_format'] = options.outputFormat;
895
+ if (options.artifactId)
896
+ args['artifact_id'] = options.artifactId;
897
+ if (options.timeoutSeconds !== undefined)
898
+ args['timeout_seconds'] = options.timeoutSeconds;
899
+ if (options.initialInterval !== undefined)
900
+ args['initial_interval'] = options.initialInterval;
901
+ if (options.maxInterval !== undefined)
902
+ args['max_interval'] = options.maxInterval;
903
+ return this.call('group_space_artifact', args);
904
+ }
905
+ /**
906
+ * List or manage Group Space jobs.
907
+ */
908
+ async groupSpaceJobs(options) {
909
+ const args = {
910
+ group_id: options.groupId,
911
+ provider: options.provider ?? 'notebooklm',
912
+ lane: options.lane,
913
+ action: options.action ?? 'list',
914
+ by: options.by ?? 'user',
915
+ };
916
+ if (options.jobId)
917
+ args['job_id'] = options.jobId;
918
+ if (options.state)
919
+ args['state'] = options.state;
920
+ if (options.limit !== undefined)
921
+ args['limit'] = options.limit;
922
+ return this.call('group_space_jobs', args);
923
+ }
924
+ /**
925
+ * Read or run Group Space synchronization for one lane.
926
+ */
927
+ async groupSpaceSync(options) {
928
+ return this.call('group_space_sync', {
929
+ group_id: options.groupId,
930
+ provider: options.provider ?? 'notebooklm',
931
+ lane: options.lane,
932
+ action: options.action ?? 'status',
933
+ force: options.force ?? false,
934
+ by: options.by ?? 'user',
935
+ });
936
+ }
937
+ /**
938
+ * Read provider credential status.
939
+ */
940
+ async groupSpaceProviderCredentialStatus(options = {}) {
941
+ return this.call('group_space_provider_credential_status', {
942
+ provider: options.provider ?? 'notebooklm',
943
+ by: options.by ?? 'user',
944
+ });
945
+ }
946
+ /**
947
+ * Update provider credentials.
948
+ */
949
+ async groupSpaceProviderCredentialUpdate(options = {}) {
950
+ const args = {
951
+ provider: options.provider ?? 'notebooklm',
952
+ by: options.by ?? 'user',
953
+ clear: options.clear ?? false,
954
+ };
955
+ if (options.authJson)
956
+ args['auth_json'] = options.authJson;
957
+ return this.call('group_space_provider_credential_update', args);
958
+ }
959
+ /**
960
+ * Run provider health check.
961
+ */
962
+ async groupSpaceProviderHealthCheck(options = {}) {
963
+ return this.call('group_space_provider_health_check', {
964
+ provider: options.provider ?? 'notebooklm',
965
+ by: options.by ?? 'user',
966
+ });
967
+ }
968
+ /**
969
+ * Control provider auth flow.
970
+ */
971
+ async groupSpaceProviderAuth(options = {}) {
972
+ const args = {
973
+ provider: options.provider ?? 'notebooklm',
974
+ action: options.action ?? 'status',
975
+ by: options.by ?? 'user',
976
+ };
977
+ if (options.timeoutSeconds !== undefined)
978
+ args['timeout_seconds'] = options.timeoutSeconds;
979
+ return this.call('group_space_provider_auth', args);
980
+ }
981
+ // ============================================================
601
982
  // Event stream
602
983
  // ============================================================
603
984
  /**
604
985
  * Subscribe to the group event stream (Server-Sent Events style, long-lived connection).
605
986
  * Yields {@link EventStreamItem} objects as they arrive. The socket is destroyed
606
987
  * when the generator is returned or thrown.
607
- * @param options - Group ID, event filters, optional since cursor, and optional AbortSignal.
988
+ * @param options - Group ID, event filters, and optional since cursor.
608
989
  * @yields {EventStreamItem} Each event or heartbeat from the stream.
609
990
  * @throws {DaemonAPIError} If the handshake fails.
610
991
  */
611
992
  async *eventsStream(options) {
612
- // Check abort before connecting
613
- if (options.signal?.aborted)
614
- return;
615
993
  const args = {
616
994
  group_id: options.groupId,
617
995
  by: options.by ?? 'user',
@@ -637,37 +1015,20 @@ export class CCCCClient {
637
1015
  socket.destroy();
638
1016
  throw new DaemonAPIError(handshake.error?.code ?? 'unknown', handshake.error?.message ?? 'Handshake failed', handshake.error?.details, handshake);
639
1017
  }
640
- // Wire up AbortSignal to destroy the socket
641
- const onAbort = () => { socket.destroy(); };
642
- options.signal?.addEventListener('abort', onAbort, { once: true });
643
1018
  try {
644
1019
  for await (const line of readLines(socket, initialBuffer)) {
645
- if (options.signal?.aborted)
646
- return;
647
- let parsed;
648
1020
  try {
649
- parsed = JSON.parse(line);
650
- }
651
- catch {
652
- // Non-JSON line from daemon; surface as an error so callers know
653
- // something unexpected happened instead of silently losing data.
654
- throw new DaemonAPIError('invalid_stream_data', `Unparseable event stream line: ${line.slice(0, 200)}`, {});
655
- }
656
- if (parsed !== null && typeof parsed === 'object') {
657
- const obj = parsed;
658
- // Daemon may send error objects in the stream (ok: false).
659
- if ('ok' in obj && obj['ok'] === false) {
660
- const err = obj['error'];
661
- throw new DaemonAPIError(err?.['code'] ?? 'stream_error', err?.['message'] ?? 'Daemon sent error in event stream', err?.['details'] ?? {}, obj);
662
- }
663
- if ('t' in obj) {
1021
+ const parsed = JSON.parse(line);
1022
+ if (parsed !== null && typeof parsed === 'object' && 't' in parsed) {
664
1023
  yield parsed;
665
1024
  }
666
1025
  }
1026
+ catch {
1027
+ // Skip invalid JSON lines.
1028
+ }
667
1029
  }
668
1030
  }
669
1031
  finally {
670
- options.signal?.removeEventListener('abort', onAbort);
671
1032
  socket.destroy();
672
1033
  }
673
1034
  }