synomem 0.1.0 → 0.2.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 (73) hide show
  1. package/ARCHITECTURE.md +2 -2
  2. package/CHANGELOG.md +37 -1
  3. package/README.md +28 -15
  4. package/dist/cli.d.ts.map +1 -1
  5. package/dist/cli.js +311 -70
  6. package/dist/cli.js.map +1 -1
  7. package/dist/client.d.ts +96 -17
  8. package/dist/client.d.ts.map +1 -1
  9. package/dist/client.js +333 -76
  10. package/dist/client.js.map +1 -1
  11. package/dist/config.d.ts +2 -2
  12. package/dist/config.js +8 -8
  13. package/dist/import.d.ts +207 -13
  14. package/dist/import.d.ts.map +1 -1
  15. package/dist/index.d.ts +2 -2
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +2 -2
  18. package/dist/index.js.map +1 -1
  19. package/dist/mcp/index.d.ts.map +1 -1
  20. package/dist/mcp/index.js +156 -31
  21. package/dist/mcp/index.js.map +1 -1
  22. package/dist/mcp-server.js +54 -8
  23. package/dist/mcp-server.js.map +1 -1
  24. package/dist/ports/repository.d.ts +18 -1
  25. package/dist/ports/repository.d.ts.map +1 -1
  26. package/dist/projections.d.ts +18 -1
  27. package/dist/projections.d.ts.map +1 -1
  28. package/dist/projections.js +137 -44
  29. package/dist/projections.js.map +1 -1
  30. package/dist/remote.d.ts +57 -9
  31. package/dist/remote.d.ts.map +1 -1
  32. package/dist/remote.js +43 -2
  33. package/dist/remote.js.map +1 -1
  34. package/dist/schemas.d.ts +302 -21
  35. package/dist/schemas.d.ts.map +1 -1
  36. package/dist/schemas.js +142 -26
  37. package/dist/schemas.js.map +1 -1
  38. package/dist/service.d.ts +57 -10
  39. package/dist/service.d.ts.map +1 -1
  40. package/dist/skill-install.d.ts +8 -2
  41. package/dist/skill-install.d.ts.map +1 -1
  42. package/dist/skill-install.js +6 -11
  43. package/dist/skill-install.js.map +1 -1
  44. package/dist/storage.d.ts +41 -1
  45. package/dist/storage.d.ts.map +1 -1
  46. package/dist/storage.js +254 -36
  47. package/dist/storage.js.map +1 -1
  48. package/dist/types.d.ts +213 -38
  49. package/dist/types.d.ts.map +1 -1
  50. package/docs/cli.md +53 -20
  51. package/docs/examples.md +4 -4
  52. package/docs/mcp.md +12 -4
  53. package/docs/skill.md +10 -7
  54. package/docs/storage-format.md +4 -4
  55. package/openapi/synomem-v1.yaml +24 -24
  56. package/package.json +1 -1
  57. package/skills/synomem/SKILL.md +24 -8
  58. package/skills/synomem/agents/openai.yaml +1 -1
  59. package/skills/synomem/references/examples.md +1 -1
  60. package/src/cli.ts +572 -173
  61. package/src/client.ts +391 -79
  62. package/src/config.ts +8 -8
  63. package/src/index.ts +11 -1
  64. package/src/mcp/index.ts +189 -30
  65. package/src/mcp-server.ts +58 -8
  66. package/src/ports/repository.ts +16 -0
  67. package/src/projections.ts +139 -44
  68. package/src/remote.ts +118 -8
  69. package/src/schemas.ts +147 -26
  70. package/src/service.ts +59 -7
  71. package/src/skill-install.ts +14 -17
  72. package/src/storage.ts +326 -34
  73. package/src/types.ts +228 -39
package/dist/cli.js CHANGED
@@ -50,7 +50,19 @@ function actor(kind, id, displayName) {
50
50
  ...(displayName ? { displayName } : {}),
51
51
  };
52
52
  }
53
- function todoDue(options) {
53
+ /**
54
+ * The acting identity for commands that do not take an explicit actor.
55
+ * Local storage accepts any actor, so these fall back to the historical CLI defaults; a remote
56
+ * backend binds the credential to one actor, so SYNOMEM_ACTOR_ID/KIND/NAME must be able to
57
+ * override them or those commands cannot authenticate.
58
+ */
59
+ function defaultActor(env, fallbackKind, fallbackId) {
60
+ const id = env.SYNOMEM_ACTOR_ID?.trim();
61
+ if (!id)
62
+ return actor(fallbackKind, fallbackId);
63
+ return actor(env.SYNOMEM_ACTOR_KIND?.trim() || fallbackKind, id, env.SYNOMEM_ACTOR_NAME?.trim());
64
+ }
65
+ function taskDue(options) {
54
66
  if (options.dueDate && options.dueAt)
55
67
  throw new SynomemError('INVALID_INPUT', 'Use due-date or due-at, not both.');
56
68
  if (options.dueDate)
@@ -272,7 +284,7 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
272
284
  if (persisted?.backend.kind === 'remote') {
273
285
  throw new SynomemError('INVALID_INPUT', 'The init command requires a local backend.');
274
286
  }
275
- await withClient(options.home, actor('system', 'cli'), async (client) => {
287
+ await withClient(options.home, defaultActor(env, 'system', 'cli'), async (client) => {
276
288
  const info = await client.info();
277
289
  if (info.backend !== 'local') {
278
290
  throw new SynomemError('INVALID_INPUT', 'The init command requires a local backend.');
@@ -414,7 +426,7 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
414
426
  .option('--description <text>')
415
427
  .action(async (id, options, command) => {
416
428
  const global = globals(command);
417
- const profile = await withClient(global.home, actor('system', 'cli'), (client) => client.agents.create({
429
+ const profile = await withClient(global.home, defaultActor(env, 'system', 'cli'), (client) => client.agents.create({
418
430
  id,
419
431
  displayName: options.name,
420
432
  ...(options.alias.length ? { aliases: options.alias } : {}),
@@ -432,32 +444,64 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
432
444
  .option('--yes', 'apply the displayed plan', false)
433
445
  .option('--force', 'replace a conflicting synomem directory', false)
434
446
  .option('--link', 'symlink to the packaged skill instead of copying it', false)
435
- .option('--actor-id <id>', 'print actor-bound MCP registration commands')
436
- .option('--actor-name <name>', 'display name used in MCP registration commands')
437
- .action((options, command) => {
447
+ .option('--agent <id-or-alias>', 'bind this installation to an agent')
448
+ .action(async (options, command) => {
438
449
  const global = globals(command);
450
+ const runtimes = skillRuntimes(options.runtime);
451
+ /*
452
+ * The agent is resolved BEFORE anything is written. An ambiguous or
453
+ * unknown name then stops the command with a name to fix, rather than
454
+ * leaving a skill installed and pointed at an agent that does not
455
+ * exist.
456
+ */
457
+ let agentId;
458
+ if (options.agent) {
459
+ agentId = await withClient(global.home, defaultActor(env, 'system', 'cli'), async (client) => {
460
+ const resolution = await client.agents.resolve(options.agent);
461
+ if (!resolution.match) {
462
+ throw new SynomemError('AGENT_NOT_FOUND', resolution.candidates.length
463
+ ? `"${options.agent}" matches ${resolution.candidates.length} agents: ${resolution.candidates
464
+ .map((candidate) => candidate.id)
465
+ .join(', ')}. Name one of them.`
466
+ : `Unknown agent: ${options.agent}`);
467
+ }
468
+ return resolution.match.id;
469
+ });
470
+ }
439
471
  const result = installSkill({
440
- runtimes: skillRuntimes(options.runtime),
472
+ ...(runtimes ? { runtimes } : {}),
441
473
  apply: options.yes,
442
474
  force: options.force,
443
475
  link: options.link,
444
- actorId: options.actorId,
445
- actorName: options.actorName,
476
+ ...(agentId ? { agentId } : {}),
446
477
  });
478
+ // Bindings follow what was actually installed, and only on a real run:
479
+ // a dry run must not claim a binding it did not make, and a runtime
480
+ // whose harness is not present here is not somewhere this agent runs.
481
+ if (agentId && options.yes) {
482
+ const installed = result.locations
483
+ .filter((location) => location.state !== 'unavailable')
484
+ .map((location) => location.runtime);
485
+ if (installed.length) {
486
+ await withClient(global.home, defaultActor(env, 'system', 'cli'), async (client) => {
487
+ for (const runtime of installed) {
488
+ await client.agents.bindRuntime({ agentId, runtime });
489
+ }
490
+ });
491
+ }
492
+ }
447
493
  output(io, global.json, result, formatSkillResult(result, 'install'));
448
494
  });
449
495
  skillCommand
450
496
  .command('status')
451
497
  .description('Show installed, stale, missing, or conflicting skill copies')
452
498
  .option('--runtime <runtime>', `${skillRuntimeHelp} (repeatable)`, collect, [])
453
- .option('--actor-id <id>', 'print actor-bound MCP registration commands')
454
- .option('--actor-name <name>', 'display name used in MCP registration commands')
499
+ .option('--agent <id>', 'print the registration command for this agent')
455
500
  .action((options, command) => {
456
501
  const global = globals(command);
457
502
  const result = skillStatus({
458
503
  runtimes: skillRuntimes(options.runtime),
459
- actorId: options.actorId,
460
- actorName: options.actorName,
504
+ ...(options.agent ? { agentId: options.agent } : {}),
461
505
  });
462
506
  output(io, global.json, result, formatSkillResult(result, 'status'));
463
507
  });
@@ -481,7 +525,7 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
481
525
  .description('List known agent identities')
482
526
  .action(async (_options, command) => {
483
527
  const global = globals(command);
484
- const agents = await withClient(global.home, actor('system', 'cli'), (client) => client.agents.list());
528
+ const agents = await withClient(global.home, defaultActor(env, 'system', 'cli'), (client) => client.agents.list());
485
529
  const human = agents.length
486
530
  ? agents
487
531
  .map((profile) => `${profile.id} ${profile.displayName}${profile.aliases?.length ? ` aliases: ${profile.aliases.join(', ')}` : ''}`)
@@ -494,7 +538,7 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
494
538
  .description('Show one agent profile, resolving aliases')
495
539
  .action(async (id, _options, command) => {
496
540
  const global = globals(command);
497
- const profile = await withClient(global.home, actor('system', 'cli'), (client) => client.agents.get(id));
541
+ const profile = await withClient(global.home, defaultActor(env, 'system', 'cli'), (client) => client.agents.get(id));
498
542
  output(io, global.json, profile, `${profile.displayName} (${profile.id})\n${profile.description ?? 'No description.'}`);
499
543
  });
500
544
  agentCommand
@@ -507,13 +551,91 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
507
551
  .action(async (id, options, command) => {
508
552
  const global = globals(command);
509
553
  const hasAliases = options.clearAliases || options.alias.length > 0;
510
- const profile = await withClient(global.home, actor('system', 'cli'), (client) => client.agents.update(id, {
554
+ const profile = await withClient(global.home, defaultActor(env, 'system', 'cli'), (client) => client.agents.update(id, {
511
555
  ...(options.name ? { displayName: options.name } : {}),
512
556
  ...(hasAliases ? { aliases: options.clearAliases ? [] : options.alias } : {}),
513
557
  ...(options.description !== undefined ? { description: options.description } : {}),
514
558
  }));
515
559
  output(io, global.json, profile, `Updated ${profile.displayName} (${profile.id})`);
516
560
  });
561
+ agentCommand
562
+ .command('resolve <name>')
563
+ .description('Resolve a name or alias to one agent, or list the candidates')
564
+ .action(async (name, _options, command) => {
565
+ const global = globals(command);
566
+ const resolution = await withClient(global.home, defaultActor(env, 'system', 'cli'), (client) => client.agents.resolve(name));
567
+ // An ambiguous name is a question, not a failure: exit zero and show the
568
+ // candidates so the caller can pick one.
569
+ const human = resolution.match
570
+ ? `${resolution.match.displayName} (${resolution.match.id})`
571
+ : resolution.candidates.length
572
+ ? `"${resolution.query}" is ambiguous. Candidates:\n${resolution.candidates
573
+ .map((profile) => ` ${profile.id} ${profile.displayName}`)
574
+ .join('\n')}`
575
+ : `No agent answers to "${resolution.query}".`;
576
+ output(io, global.json, resolution, human);
577
+ });
578
+ agentCommand
579
+ .command('directory')
580
+ .description('List agents with their runtime bindings')
581
+ .action(async (_options, command) => {
582
+ const global = globals(command);
583
+ const entries = await withClient(global.home, defaultActor(env, 'system', 'cli'), (client) => client.agents.directory());
584
+ const human = entries.length
585
+ ? entries
586
+ .map((entry) => {
587
+ const runtimes = entry.runtimeBindings.length
588
+ ? entry.runtimeBindings
589
+ .map((binding) => ` ${binding.runtime}${binding.profile ? `/${binding.profile}` : ''}` +
590
+ // Last-seen is advisory, so it is labelled as an
591
+ // observation rather than a status.
592
+ `${binding.lastSeenAt ? ` last seen ${binding.lastSeenAt}` : ' not yet seen'}`)
593
+ .join('\n')
594
+ : ' no runtime bindings';
595
+ return `${entry.profile.id} ${entry.profile.displayName}\n${runtimes}`;
596
+ })
597
+ .join('\n')
598
+ : 'No agents configured.';
599
+ output(io, global.json, { entries }, human);
600
+ });
601
+ const runtimeCommand = agentCommand.command('runtime').description('Record where an agent runs');
602
+ runtimeCommand
603
+ .command('bind <agent>')
604
+ .description('Bind an agent to a runtime')
605
+ .requiredOption('--runtime <name>', 'runtime family, e.g. claude-code')
606
+ .option('--profile <name>', 'named configuration within the runtime')
607
+ .option('--installation <id>', 'hosted installation this binding belongs to')
608
+ .action(async (agent, options, command) => {
609
+ const global = globals(command);
610
+ const binding = await withClient(global.home, defaultActor(env, 'system', 'cli'), (client) => client.agents.bindRuntime({
611
+ agentId: agent,
612
+ runtime: options.runtime,
613
+ ...(options.profile ? { profile: options.profile } : {}),
614
+ ...(options.installation ? { installationId: options.installation } : {}),
615
+ }));
616
+ output(io, global.json, binding, `Bound ${binding.agentId} to ${binding.runtime}${binding.profile ? `/${binding.profile}` : ''} (${binding.id})`);
617
+ });
618
+ runtimeCommand
619
+ .command('list <agent>')
620
+ .description('List an agent runtime bindings')
621
+ .action(async (agent, _options, command) => {
622
+ const global = globals(command);
623
+ const bindings = await withClient(global.home, defaultActor(env, 'system', 'cli'), (client) => client.agents.bindings(agent));
624
+ const human = bindings.length
625
+ ? bindings
626
+ .map((binding) => `${binding.id} ${binding.runtime}${binding.profile ? `/${binding.profile}` : ''} bound ${binding.boundAt}`)
627
+ .join('\n')
628
+ : 'No runtime bindings.';
629
+ output(io, global.json, { bindings }, human);
630
+ });
631
+ runtimeCommand
632
+ .command('unbind <binding-id>')
633
+ .description('Remove a runtime binding')
634
+ .action(async (bindingId, _options, command) => {
635
+ const global = globals(command);
636
+ const removed = await withClient(global.home, defaultActor(env, 'system', 'cli'), (client) => client.agents.unbindRuntime(bindingId));
637
+ output(io, global.json, { removed }, removed ? `Removed binding ${bindingId}.` : `No binding ${bindingId}.`);
638
+ });
517
639
  const kudosCommand = program.command('kudos').description('Give and manage agent recognition');
518
640
  kudosCommand
519
641
  .command('give <recipient>')
@@ -543,7 +665,7 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
543
665
  });
544
666
  program
545
667
  .command('inbox [agent]')
546
- .description('Show pending kudos, memos, and todos for an agent')
668
+ .description('Show pending kudos, memos, and tasks for an agent')
547
669
  .option('--as <agent-id>', 'defaults to the positional agent')
548
670
  .option('--limit <number>', 'maximum results (default 10, maximum 50)', '10')
549
671
  .option('--cursor <cursor>', 'opaque cursor returned by the previous page')
@@ -564,7 +686,7 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
564
686
  });
565
687
  addListOptions(kudosCommand.command('list').description('List and filter kudos')).action(async (options, command) => {
566
688
  const global = globals(command);
567
- const page = await withClient(global.home, actor('human', 'local-cli'), (client) => client.kudos.list(listInput(options)));
689
+ const page = await withClient(global.home, defaultActor(env, 'human', 'local-cli'), (client) => client.kudos.list(listInput(options)));
568
690
  output(io, global.json, page, page.items.length
569
691
  ? `${page.items.map(lineForSummary).join('\n')}${page.hasMore ? `\nNext cursor: ${page.nextCursor}` : ''}`
570
692
  : 'No kudos found.');
@@ -572,7 +694,7 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
572
694
  program
573
695
  .command('list')
574
696
  .description('List compact summaries across all record types')
575
- .option('--kind <kind>', 'kudos, memo, note, or todo (repeatable)', collect, [])
697
+ .option('--kind <kind>', 'kudos, memo, note, or task (repeatable)', collect, [])
576
698
  .option('--participant <agent>')
577
699
  .option('--actor <id>')
578
700
  .option('--tag <tag>')
@@ -583,7 +705,7 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
583
705
  .option('--offset <number>', 'deprecated offset', '0')
584
706
  .action(async (options, command) => {
585
707
  const global = globals(command);
586
- const page = await withClient(global.home, actor('human', 'local-cli'), (client) => client.items.list(itemListInput(options)));
708
+ const page = await withClient(global.home, defaultActor(env, 'human', 'local-cli'), (client) => client.items.list(itemListInput(options)));
587
709
  output(io, global.json, page, page.items.length
588
710
  ? `${page.items.map(lineForItem).join('\n')}${page.hasMore ? `\nNext cursor: ${page.nextCursor}` : ''}`
589
711
  : 'No items found.');
@@ -596,7 +718,7 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
596
718
  .option('--limit <number>', 'maximum changes (default 20, maximum 100)', '20')
597
719
  .action(async (options, command) => {
598
720
  const global = globals(command);
599
- const page = await withClient(global.home, actor('human', 'local-cli'), (client) => client.items.changes({
721
+ const page = await withClient(global.home, defaultActor(env, 'human', 'local-cli'), (client) => client.items.changes({
600
722
  limit: Number(options.limit),
601
723
  ...(options.kind.length ? { kinds: options.kind } : {}),
602
724
  ...(options.after ? { after: options.after } : {}),
@@ -639,7 +761,7 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
639
761
  .option('--cursor <cursor>')
640
762
  .action(async (options, command) => {
641
763
  const global = globals(command);
642
- const page = await withClient(global.home, actor('human', 'local-cli'), (client) => client.memos.list({
764
+ const page = await withClient(global.home, defaultActor(env, 'human', 'local-cli'), (client) => client.memos.list({
643
765
  ...(options.participant ? { participantAgentId: options.participant } : {}),
644
766
  ...(options.status ? { status: options.status } : {}),
645
767
  limit: Number(options.limit),
@@ -649,7 +771,7 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
649
771
  });
650
772
  memoCommand.command('show <memo-id>').action(async (id, _options, command) => {
651
773
  const global = globals(command);
652
- const record = await withClient(global.home, actor('human', 'local-cli'), (client) => client.memos.get(id));
774
+ const record = await withClient(global.home, defaultActor(env, 'human', 'local-cli'), (client) => client.memos.get(id));
653
775
  output(io, global.json, record, `${record.event.subject}\nID: ${record.event.id}\nStatus: ${record.status}\n\n${record.event.body}`);
654
776
  });
655
777
  for (const operation of ['read', 'archive']) {
@@ -697,7 +819,7 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
697
819
  .option('--limit <number>', 'maximum results', '10')
698
820
  .action(async (options, command) => {
699
821
  const global = globals(command);
700
- const page = await withClient(global.home, actor('human', 'local-cli'), (client) => client.notes.list({
822
+ const page = await withClient(global.home, defaultActor(env, 'human', 'local-cli'), (client) => client.notes.list({
701
823
  ...(options.owner ? { participantAgentId: options.owner } : {}),
702
824
  ...(options.status ? { status: options.status } : {}),
703
825
  limit: Number(options.limit),
@@ -706,7 +828,7 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
706
828
  });
707
829
  noteCommand.command('show <note-id>').action(async (id, _options, command) => {
708
830
  const global = globals(command);
709
- const record = await withClient(global.home, actor('human', 'local-cli'), (client) => client.notes.get(id));
831
+ const record = await withClient(global.home, defaultActor(env, 'human', 'local-cli'), (client) => client.notes.get(id));
710
832
  output(io, global.json, record, `${record.current.title}\nID: ${record.event.id}\nVersion: ${record.current.version}\n\n${record.current.body}`);
711
833
  });
712
834
  noteCommand
@@ -743,8 +865,100 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
743
865
  }));
744
866
  output(io, global.json, record, `Archived note ${id}.`);
745
867
  });
746
- const todoCommand = program.command('todo').description('Create and manage agent todos');
868
+ const todoCommand = program
869
+ .command('todo')
870
+ .description('Create and manage your own private reminders');
871
+ todoCommand
872
+ .command('create')
873
+ .requiredOption('--as <actor-id>')
874
+ .option('--actor-kind <kind>', 'human, agent, or system', 'agent')
875
+ .requiredOption('--title <title>')
876
+ .option('--details <text>', 'private working detail')
877
+ .option('--priority <number>', '1 highest, 4 lowest', '3')
878
+ .option('--due-date <date>')
879
+ .option('--due-at <datetime>')
880
+ .option('--time-zone <iana-zone>')
881
+ .option('--tag <tag>', 'tag (repeatable)', collect, [])
882
+ .option('--idempotency-key <key>')
883
+ .action(async (options, command) => {
884
+ const global = globals(command);
885
+ const result = await withClient(global.home, actor(options.actorKind, options.as), (client) => client.todos.create({
886
+ title: options.title,
887
+ ...(options.details ? { details: options.details } : {}),
888
+ priority: Number(options.priority),
889
+ ...((due) => (due ? { due } : {}))(taskDue(options)),
890
+ ...(options.tag.length ? { tags: options.tag } : {}),
891
+ ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
892
+ }));
893
+ output(io, global.json, result, `${result.deduplicated ? 'Found existing' : 'Created'} private todo\nTitle: ${result.record.current.title}\nID: ${result.record.event.id}`);
894
+ });
895
+ todoCommand
896
+ .command('list')
897
+ .requiredOption('--as <actor-id>')
898
+ .option('--actor-kind <kind>', 'human, agent, or system', 'agent')
899
+ .option('--status <status>')
900
+ .option('--limit <number>', 'maximum results', '10')
901
+ .action(async (options, command) => {
902
+ const global = globals(command);
903
+ const page = await withClient(global.home, actor(options.actorKind, options.as), (client) => client.todos.list({
904
+ ...(options.status ? { status: options.status } : {}),
905
+ limit: Number(options.limit),
906
+ }));
907
+ output(io, global.json, page, page.items.length
908
+ ? page.items
909
+ .map((item) => `${item.id} ${item.status.padEnd(9)} ${item.title}`)
910
+ .join('\n')
911
+ : 'No todos.');
912
+ });
747
913
  todoCommand
914
+ .command('show <todo-id>')
915
+ .requiredOption('--as <actor-id>')
916
+ .option('--actor-kind <kind>', 'human, agent, or system', 'agent')
917
+ .action(async (id, options, command) => {
918
+ const global = globals(command);
919
+ const record = await withClient(global.home, actor(options.actorKind, options.as), (client) => client.todos.get(id));
920
+ output(io, global.json, record, `${record.current.title}\nStatus: ${record.status}\nPriority: ${record.current.priority}\nVersion: ${record.current.version}${record.current.details ? `\n\n${record.current.details}` : ''}`);
921
+ });
922
+ for (const operation of ['complete', 'reopen', 'cancel', 'archive']) {
923
+ todoCommand
924
+ .command(`${operation} <todo-id>`)
925
+ .requiredOption('--as <actor-id>')
926
+ .option('--actor-kind <kind>', 'human, agent, or system', 'agent')
927
+ .option('--note <text>')
928
+ .option('--reason <text>')
929
+ .option('--idempotency-key <key>')
930
+ .action(async (id, options, command) => {
931
+ const global = globals(command);
932
+ const record = await withClient(global.home, actor(options.actorKind, options.as), (client) => operation === 'complete'
933
+ ? client.todos.complete({
934
+ todoId: id,
935
+ ...(options.note ? { note: options.note } : {}),
936
+ ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
937
+ })
938
+ : operation === 'cancel'
939
+ ? client.todos.cancel({
940
+ todoId: id,
941
+ ...(options.reason ? { reason: options.reason } : {}),
942
+ ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
943
+ })
944
+ : operation === 'archive'
945
+ ? client.todos.archive({
946
+ todoId: id,
947
+ ...(options.idempotencyKey
948
+ ? { idempotencyKey: options.idempotencyKey }
949
+ : {}),
950
+ })
951
+ : client.todos.reopen({
952
+ todoId: id,
953
+ ...(options.idempotencyKey
954
+ ? { idempotencyKey: options.idempotencyKey }
955
+ : {}),
956
+ }));
957
+ output(io, global.json, record, `Todo ${id} is now ${record.status}.`);
958
+ });
959
+ }
960
+ const taskCommand = program.command('task').description('Create and manage agent tasks');
961
+ taskCommand
748
962
  .command('create <assignee>')
749
963
  .requiredOption('--from <actor-id>')
750
964
  .option('--actor-kind <kind>', 'human, agent, or system', 'agent')
@@ -759,39 +973,39 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
759
973
  .option('--idempotency-key <key>')
760
974
  .action(async (assignee, options, command) => {
761
975
  const global = globals(command);
762
- const result = await withClient(global.home, actor(options.actorKind, options.from), (client) => client.todos.create({
976
+ const result = await withClient(global.home, actor(options.actorKind, options.from), (client) => client.tasks.create({
763
977
  assigneeAgentId: assignee,
764
978
  title: options.title,
765
979
  ...(options.description ? { description: options.description } : {}),
766
980
  priority: Number(options.priority),
767
- ...((due) => (due ? { due } : {}))(todoDue(options)),
981
+ ...((due) => (due ? { due } : {}))(taskDue(options)),
768
982
  ...(options.tag.length ? { tags: options.tag } : {}),
769
983
  visibility: options.visibility,
770
984
  ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
771
985
  }));
772
- output(io, global.json, result, `${result.deduplicated ? 'Found existing' : 'Created'} todo for ${result.record.event.assigneeDisplayName}\nTitle: ${result.record.current.title}\nID: ${result.record.event.id}`);
986
+ output(io, global.json, result, `${result.deduplicated ? 'Found existing' : 'Created'} task for ${result.record.event.assigneeDisplayName}\nTitle: ${result.record.current.title}\nID: ${result.record.event.id}`);
773
987
  });
774
- todoCommand
988
+ taskCommand
775
989
  .command('list')
776
990
  .option('--assignee <agent>')
777
991
  .option('--status <status>')
778
992
  .option('--limit <number>', 'maximum results', '10')
779
993
  .action(async (options, command) => {
780
994
  const global = globals(command);
781
- const page = await withClient(global.home, actor('human', 'local-cli'), (client) => client.todos.list({
995
+ const page = await withClient(global.home, defaultActor(env, 'human', 'local-cli'), (client) => client.tasks.list({
782
996
  ...(options.assignee ? { participantAgentId: options.assignee } : {}),
783
997
  ...(options.status ? { status: options.status } : {}),
784
998
  limit: Number(options.limit),
785
999
  }));
786
- output(io, global.json, page, page.items.map(lineForItem).join('\n') || 'No todos found.');
1000
+ output(io, global.json, page, page.items.map(lineForItem).join('\n') || 'No tasks found.');
787
1001
  });
788
- todoCommand.command('show <todo-id>').action(async (id, _options, command) => {
1002
+ taskCommand.command('show <task-id>').action(async (id, _options, command) => {
789
1003
  const global = globals(command);
790
- const record = await withClient(global.home, actor('human', 'local-cli'), (client) => client.todos.get(id));
1004
+ const record = await withClient(global.home, defaultActor(env, 'human', 'local-cli'), (client) => client.tasks.get(id));
791
1005
  output(io, global.json, record, `${record.current.title}\nID: ${record.event.id}\nStatus: ${record.status}\nVersion: ${record.current.version}`);
792
1006
  });
793
- todoCommand
794
- .command('update <todo-id>')
1007
+ taskCommand
1008
+ .command('update <task-id>')
795
1009
  .requiredOption('--as <actor-id>')
796
1010
  .option('--actor-kind <kind>', 'agent or human', 'agent')
797
1011
  .requiredOption('--expected-version <number>')
@@ -806,9 +1020,9 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
806
1020
  .option('--idempotency-key <key>')
807
1021
  .action(async (id, options, command) => {
808
1022
  const global = globals(command);
809
- const parsedDue = todoDue(options);
810
- const record = await withClient(global.home, actor(options.actorKind, options.as), (client) => client.todos.update({
811
- todoId: id,
1023
+ const parsedDue = taskDue(options);
1024
+ const record = await withClient(global.home, actor(options.actorKind, options.as), (client) => client.tasks.update({
1025
+ taskId: id,
812
1026
  expectedVersion: Number(options.expectedVersion),
813
1027
  ...(options.title ? { title: options.title } : {}),
814
1028
  ...(options.description !== undefined ? { description: options.description } : {}),
@@ -817,52 +1031,59 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
817
1031
  ...(options.visibility ? { visibility: options.visibility } : {}),
818
1032
  ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
819
1033
  }));
820
- output(io, global.json, record, `Updated todo ${id} to version ${record.current.version}.`);
1034
+ output(io, global.json, record, `Updated task ${id} to version ${record.current.version}.`);
821
1035
  });
822
1036
  for (const operation of ['accept', 'reject', 'complete', 'reopen', 'cancel']) {
823
- todoCommand
824
- .command(`${operation} <todo-id>`)
1037
+ const command_ = taskCommand
1038
+ .command(`${operation} <task-id>`)
825
1039
  .requiredOption('--as <actor-id>')
826
1040
  .option('--actor-kind <kind>', 'agent or human', 'agent')
827
1041
  .option('--note <text>')
828
1042
  .option('--reason <text>')
829
- .option('--idempotency-key <key>')
830
- .action(async (id, options, command) => {
1043
+ .option('--idempotency-key <key>');
1044
+ // Rejecting requires saying why; accepting may. Marked required at the
1045
+ // parser so the CLI refuses before touching the store.
1046
+ if (operation === 'reject') {
1047
+ command_.requiredOption('--response <text>', 'why the task is being refused');
1048
+ }
1049
+ else if (operation === 'accept') {
1050
+ command_.option('--response <text>', 'conditions, timing, or partial capability');
1051
+ }
1052
+ command_.action(async (id, options, command) => {
831
1053
  const global = globals(command);
832
1054
  const record = await withClient(global.home, actor(options.actorKind, options.as), (client) => operation === 'accept'
833
- ? client.todos.accept({
834
- todoId: id,
1055
+ ? client.tasks.accept({
1056
+ taskId: id,
1057
+ ...(options.response ? { response: options.response } : {}),
835
1058
  ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
836
1059
  })
837
1060
  : operation === 'reject'
838
- ? client.todos.reject({
839
- todoId: id,
840
- ...(options.reason ? { reason: options.reason } : {}),
1061
+ ? client.tasks.reject({
1062
+ taskId: id,
1063
+ response: options.response ?? options.reason ?? '',
841
1064
  ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
842
1065
  })
843
1066
  : operation === 'complete'
844
- ? client.todos.complete({
845
- todoId: id,
1067
+ ? client.tasks.complete({
1068
+ taskId: id,
846
1069
  ...(options.note ? { note: options.note } : {}),
847
- ...(options.idempotencyKey
848
- ? { idempotencyKey: options.idempotencyKey }
849
- : {}),
1070
+ ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
850
1071
  })
851
1072
  : operation === 'cancel'
852
- ? client.todos.cancel({
853
- todoId: id,
1073
+ ? client.tasks.cancel({
1074
+ taskId: id,
854
1075
  ...(options.reason ? { reason: options.reason } : {}),
855
1076
  ...(options.idempotencyKey
856
1077
  ? { idempotencyKey: options.idempotencyKey }
857
1078
  : {}),
858
1079
  })
859
- : client.todos.reopen({
860
- todoId: id,
1080
+ : client.tasks.reopen({
1081
+ taskId: id,
861
1082
  ...(options.idempotencyKey
862
1083
  ? { idempotencyKey: options.idempotencyKey }
863
1084
  : {}),
864
1085
  }));
865
- output(io, global.json, record, `Todo ${id} is ${record.status}.`);
1086
+ output(io, global.json, record, `Task ${id} is ${record.status}.`);
866
1087
  });
867
1088
  }
868
1089
  kudosCommand
@@ -870,7 +1091,7 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
870
1091
  .description('Show one kudos item and its current state')
871
1092
  .action(async (id, _options, command) => {
872
1093
  const global = globals(command);
873
- const record = await withClient(global.home, actor('system', 'cli'), (client) => client.kudos.get(id));
1094
+ const record = await withClient(global.home, defaultActor(env, 'system', 'cli'), (client) => client.kudos.get(id));
874
1095
  output(io, global.json, record, showRecord(record));
875
1096
  });
876
1097
  kudosCommand
@@ -913,7 +1134,7 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
913
1134
  const global = globals(command);
914
1135
  if (!agentId)
915
1136
  throw new SynomemError('INVALID_INPUT', 'Specify an agent.');
916
- const details = await withClient(global.home, actor('system', 'cli'), async (client) => {
1137
+ const details = await withClient(global.home, defaultActor(env, 'system', 'cli'), async (client) => {
917
1138
  const profile = await client.agents.get(agentId);
918
1139
  const info = await client.info();
919
1140
  if (info.backend !== 'local') {
@@ -942,7 +1163,7 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
942
1163
  });
943
1164
  addListOptions(kudosCommand.command('stats').description('Show aggregate kudos statistics')).action(async (options, command) => {
944
1165
  const global = globals(command);
945
- const stats = await withClient(global.home, actor('system', 'cli'), (client) => client.stats(listInput(options)));
1166
+ const stats = await withClient(global.home, defaultActor(env, 'system', 'cli'), (client) => client.stats(listInput(options)));
946
1167
  output(io, global.json, stats, `Total: ${stats.total}\nActive: ${stats.active}\nAcknowledged: ${stats.acknowledged}\nRevoked: ${stats.revoked}`);
947
1168
  });
948
1169
  program
@@ -950,7 +1171,7 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
950
1171
  .description('Regenerate current-state and filesystem projections from canonical events')
951
1172
  .action(async (_options, command) => {
952
1173
  const global = globals(command);
953
- const result = await withClient(global.home, actor('system', 'cli'), (client) => client.rebuild());
1174
+ const result = await withClient(global.home, defaultActor(env, 'system', 'cli'), (client) => client.rebuild());
954
1175
  output(io, global.json, result, `Rebuilt ${result.generated.length} file(s); removed ${result.removed.length} stale file(s).`);
955
1176
  });
956
1177
  program
@@ -958,7 +1179,7 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
958
1179
  .description('Create a transactionally consistent SQLite backup')
959
1180
  .action(async (destination, _options, command) => {
960
1181
  const global = globals(command);
961
- const path = await withClient(global.home, actor('system', 'cli'), (client) => {
1182
+ const path = await withClient(global.home, defaultActor(env, 'system', 'cli'), (client) => {
962
1183
  if (!client.backup) {
963
1184
  throw new SynomemError('INVALID_INPUT', 'Filesystem backup is available only with the local backend.');
964
1185
  }
@@ -973,7 +1194,7 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
973
1194
  .option('--output <path>', 'write to an explicit destination instead of stdout')
974
1195
  .action(async (options, command) => {
975
1196
  const global = globals(command);
976
- const content = await withClient(global.home, actor('system', 'cli'), (client) => client.export(options.format));
1197
+ const content = await withClient(global.home, defaultActor(env, 'system', 'cli'), (client) => client.export(options.format));
977
1198
  if (options.output) {
978
1199
  const destination = resolve(options.output);
979
1200
  atomicWriteFile(destination, content, 0o600);
@@ -988,7 +1209,7 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
988
1209
  .description('Run safe diagnostics')
989
1210
  .action(async (_options, command) => {
990
1211
  const global = globals(command);
991
- const result = await withClient(global.home, actor('system', 'cli'), (client) => client.doctor());
1212
+ const result = await withClient(global.home, defaultActor(env, 'system', 'cli'), (client) => client.doctor());
992
1213
  const human = result.diagnostics
993
1214
  .map((item) => `${item.level.toUpperCase().padEnd(7)} ${item.code}: ${item.message}`)
994
1215
  .join('\n');
@@ -999,14 +1220,34 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
999
1220
  program
1000
1221
  .command('mcp')
1001
1222
  .description('Run the actor-bound MCP server over stdio')
1002
- .requiredOption('--actor-id <id>')
1003
- .requiredOption('--actor-kind <kind>', 'human, agent, or system')
1004
- .option('--actor-name <display-name>')
1223
+ .option('--agent-id <id>', 'bound agent, whose identity is read from Synomem')
1224
+ .option('--actor-id <id>', 'bound non-agent actor ID')
1225
+ .option('--actor-kind <kind>', 'human or system')
1226
+ .option('--actor-name <display-name>', 'display name for a non-agent actor')
1005
1227
  .action(async (options, command) => {
1006
1228
  const global = globals(command);
1229
+ // An agent's name comes from its profile, never from the command line:
1230
+ // the name is written into every event the session appends, and a
1231
+ // harness must not be able to sign another agent's name to work.
1232
+ const bound = options.agentId
1233
+ ? await withClient(global.home, defaultActor(env, 'system', 'cli'), async (client) => {
1234
+ const resolution = await client.agents.resolve(options.agentId);
1235
+ if (!resolution.match) {
1236
+ throw new SynomemError('AGENT_NOT_FOUND', resolution.candidates.length
1237
+ ? `"${options.agentId}" matches ${resolution.candidates.length} agents: ${resolution.candidates
1238
+ .map((candidate) => candidate.id)
1239
+ .join(', ')}. Name one of them.`
1240
+ : `Unknown agent: ${options.agentId}`);
1241
+ }
1242
+ return actor('agent', resolution.match.id, resolution.match.displayName);
1243
+ })
1244
+ : undefined;
1245
+ if (!bound && !(options.actorId && options.actorKind)) {
1246
+ throw new SynomemError('INVALID_INPUT', 'Specify --agent-id, or --actor-id with --actor-kind for a non-agent actor.');
1247
+ }
1007
1248
  await startMcpServer({
1008
1249
  ...(global.home ? { home: global.home } : {}),
1009
- actor: actor(options.actorKind, options.actorId, options.actorName),
1250
+ actor: bound ?? actor(options.actorKind, options.actorId, options.actorName),
1010
1251
  });
1011
1252
  });
1012
1253
  return program;