synomem 0.1.1 → 0.3.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 -0
  3. package/README.md +26 -13
  4. package/dist/cli.d.ts.map +1 -1
  5. package/dist/cli.js +379 -51
  6. package/dist/cli.js.map +1 -1
  7. package/dist/client.d.ts +131 -17
  8. package/dist/client.d.ts.map +1 -1
  9. package/dist/client.js +489 -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 +356 -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 +213 -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 +21 -1
  25. package/dist/ports/repository.d.ts.map +1 -1
  26. package/dist/projections.d.ts +26 -1
  27. package/dist/projections.d.ts.map +1 -1
  28. package/dist/projections.js +208 -44
  29. package/dist/projections.js.map +1 -1
  30. package/dist/remote.d.ts +82 -9
  31. package/dist/remote.d.ts.map +1 -1
  32. package/dist/remote.js +53 -2
  33. package/dist/remote.js.map +1 -1
  34. package/dist/schemas.d.ts +476 -21
  35. package/dist/schemas.d.ts.map +1 -1
  36. package/dist/schemas.js +204 -26
  37. package/dist/schemas.js.map +1 -1
  38. package/dist/service.d.ts +82 -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 +51 -1
  45. package/dist/storage.d.ts.map +1 -1
  46. package/dist/storage.js +366 -36
  47. package/dist/storage.js.map +1 -1
  48. package/dist/types.d.ts +310 -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 +610 -95
  61. package/src/client.ts +587 -79
  62. package/src/config.ts +8 -8
  63. package/src/index.ts +11 -1
  64. package/src/mcp/index.ts +265 -30
  65. package/src/mcp-server.ts +58 -8
  66. package/src/ports/repository.ts +21 -0
  67. package/src/projections.ts +209 -44
  68. package/src/remote.ts +166 -8
  69. package/src/schemas.ts +211 -26
  70. package/src/service.ts +85 -7
  71. package/src/skill-install.ts +14 -17
  72. package/src/storage.ts +472 -34
  73. package/src/types.ts +332 -39
package/src/cli.ts CHANGED
@@ -34,7 +34,7 @@ import type {
34
34
  KudosSummary,
35
35
  ItemListInput,
36
36
  ItemSummary,
37
- TodoDue,
37
+ TaskDue,
38
38
  } from './types.js';
39
39
  import { packageVersion } from './version.js';
40
40
  import type { SynomemService, SynomemServiceFactory } from './service.js';
@@ -125,11 +125,11 @@ function defaultActor(
125
125
  return actor(env.SYNOMEM_ACTOR_KIND?.trim() || fallbackKind, id, env.SYNOMEM_ACTOR_NAME?.trim());
126
126
  }
127
127
 
128
- function todoDue(options: {
128
+ function taskDue(options: {
129
129
  dueDate?: string;
130
130
  dueAt?: string;
131
131
  timeZone?: string;
132
- }): TodoDue | undefined {
132
+ }): TaskDue | undefined {
133
133
  if (options.dueDate && options.dueAt)
134
134
  throw new SynomemError('INVALID_INPUT', 'Use due-date or due-at, not both.');
135
135
  if (options.dueDate) return { kind: 'date', date: options.dueDate };
@@ -637,29 +637,73 @@ export function createCli(
637
637
  .option('--yes', 'apply the displayed plan', false)
638
638
  .option('--force', 'replace a conflicting synomem directory', false)
639
639
  .option('--link', 'symlink to the packaged skill instead of copying it', false)
640
- .option('--actor-id <id>', 'print actor-bound MCP registration commands')
641
- .option('--actor-name <name>', 'display name used in MCP registration commands')
640
+ .option('--agent <id-or-alias>', 'bind this installation to an agent')
642
641
  .action(
643
- (
642
+ async (
644
643
  options: {
645
644
  runtime: string[];
646
645
  yes: boolean;
647
646
  force: boolean;
648
647
  link: boolean;
649
- actorId?: string;
650
- actorName?: string;
648
+ agent?: string;
651
649
  },
652
650
  command: Command,
653
651
  ) => {
654
652
  const global = globals(command);
653
+ const runtimes = skillRuntimes(options.runtime);
654
+
655
+ /*
656
+ * The agent is resolved BEFORE anything is written. An ambiguous or
657
+ * unknown name then stops the command with a name to fix, rather than
658
+ * leaving a skill installed and pointed at an agent that does not
659
+ * exist.
660
+ */
661
+ let agentId: string | undefined;
662
+ if (options.agent) {
663
+ agentId = await withClient(
664
+ global.home,
665
+ defaultActor(env, 'system', 'cli'),
666
+ async (client) => {
667
+ const resolution = await client.agents.resolve(options.agent!);
668
+ if (!resolution.match) {
669
+ throw new SynomemError(
670
+ 'AGENT_NOT_FOUND',
671
+ resolution.candidates.length
672
+ ? `"${options.agent}" matches ${resolution.candidates.length} agents: ${resolution.candidates
673
+ .map((candidate) => candidate.id)
674
+ .join(', ')}. Name one of them.`
675
+ : `Unknown agent: ${options.agent}`,
676
+ );
677
+ }
678
+ return resolution.match.id;
679
+ },
680
+ );
681
+ }
682
+
655
683
  const result = installSkill({
656
- runtimes: skillRuntimes(options.runtime),
684
+ ...(runtimes ? { runtimes } : {}),
657
685
  apply: options.yes,
658
686
  force: options.force,
659
687
  link: options.link,
660
- actorId: options.actorId,
661
- actorName: options.actorName,
688
+ ...(agentId ? { agentId } : {}),
662
689
  });
690
+
691
+ // Bindings follow what was actually installed, and only on a real run:
692
+ // a dry run must not claim a binding it did not make, and a runtime
693
+ // whose harness is not present here is not somewhere this agent runs.
694
+ if (agentId && options.yes) {
695
+ const installed = result.locations
696
+ .filter((location) => location.state !== 'unavailable')
697
+ .map((location) => location.runtime);
698
+ if (installed.length) {
699
+ await withClient(global.home, defaultActor(env, 'system', 'cli'), async (client) => {
700
+ for (const runtime of installed) {
701
+ await client.agents.bindRuntime({ agentId, runtime });
702
+ }
703
+ });
704
+ }
705
+ }
706
+
663
707
  output(io, global.json, result, formatSkillResult(result, 'install'));
664
708
  },
665
709
  );
@@ -668,19 +712,15 @@ export function createCli(
668
712
  .command('status')
669
713
  .description('Show installed, stale, missing, or conflicting skill copies')
670
714
  .option('--runtime <runtime>', `${skillRuntimeHelp} (repeatable)`, collect, [])
671
- .option('--actor-id <id>', 'print actor-bound MCP registration commands')
672
- .option('--actor-name <name>', 'display name used in MCP registration commands')
673
- .action(
674
- (options: { runtime: string[]; actorId?: string; actorName?: string }, command: Command) => {
675
- const global = globals(command);
676
- const result = skillStatus({
677
- runtimes: skillRuntimes(options.runtime),
678
- actorId: options.actorId,
679
- actorName: options.actorName,
680
- });
681
- output(io, global.json, result, formatSkillResult(result, 'status'));
682
- },
683
- );
715
+ .option('--agent <id>', 'print the registration command for this agent')
716
+ .action((options: { runtime: string[]; agent?: string }, command: Command) => {
717
+ const global = globals(command);
718
+ const result = skillStatus({
719
+ runtimes: skillRuntimes(options.runtime),
720
+ ...(options.agent ? { agentId: options.agent } : {}),
721
+ });
722
+ output(io, global.json, result, formatSkillResult(result, 'status'));
723
+ });
684
724
 
685
725
  skillCommand
686
726
  .command('uninstall')
@@ -762,6 +802,293 @@ export function createCli(
762
802
  },
763
803
  );
764
804
 
805
+ agentCommand
806
+ .command('resolve <name>')
807
+ .description('Resolve a name or alias to one agent, or list the candidates')
808
+ .action(async (name: string, _options, command: Command) => {
809
+ const global = globals(command);
810
+ const resolution = await withClient(
811
+ global.home,
812
+ defaultActor(env, 'system', 'cli'),
813
+ (client) => client.agents.resolve(name),
814
+ );
815
+ // An ambiguous name is a question, not a failure: exit zero and show the
816
+ // candidates so the caller can pick one.
817
+ const human = resolution.match
818
+ ? `${resolution.match.displayName} (${resolution.match.id})`
819
+ : resolution.candidates.length
820
+ ? `"${resolution.query}" is ambiguous. Candidates:\n${resolution.candidates
821
+ .map((profile) => ` ${profile.id} ${profile.displayName}`)
822
+ .join('\n')}`
823
+ : `No agent answers to "${resolution.query}".`;
824
+ output(io, global.json, resolution, human);
825
+ });
826
+
827
+ agentCommand
828
+ .command('directory')
829
+ .description('List agents with their runtime bindings')
830
+ .action(async (_options, command: Command) => {
831
+ const global = globals(command);
832
+ const entries = await withClient(global.home, defaultActor(env, 'system', 'cli'), (client) =>
833
+ client.agents.directory(),
834
+ );
835
+ const human = entries.length
836
+ ? entries
837
+ .map((entry) => {
838
+ const runtimes = entry.runtimeBindings.length
839
+ ? entry.runtimeBindings
840
+ .map(
841
+ (binding) =>
842
+ ` ${binding.runtime}${binding.profile ? `/${binding.profile}` : ''}` +
843
+ // Last-seen is advisory, so it is labelled as an
844
+ // observation rather than a status.
845
+ `${binding.lastSeenAt ? ` last seen ${binding.lastSeenAt}` : ' not yet seen'}`,
846
+ )
847
+ .join('\n')
848
+ : ' no runtime bindings';
849
+ return `${entry.profile.id} ${entry.profile.displayName}\n${runtimes}`;
850
+ })
851
+ .join('\n')
852
+ : 'No agents configured.';
853
+ output(io, global.json, { entries }, human);
854
+ });
855
+
856
+ const runtimeCommand = agentCommand.command('runtime').description('Record where an agent runs');
857
+
858
+ runtimeCommand
859
+ .command('bind <agent>')
860
+ .description('Bind an agent to a runtime')
861
+ .requiredOption('--runtime <name>', 'runtime family, e.g. claude-code')
862
+ .option('--profile <name>', 'named configuration within the runtime')
863
+ .option('--installation <id>', 'hosted installation this binding belongs to')
864
+ .action(
865
+ async (
866
+ agent: string,
867
+ options: { runtime: string; profile?: string; installation?: string },
868
+ command: Command,
869
+ ) => {
870
+ const global = globals(command);
871
+ const binding = await withClient(
872
+ global.home,
873
+ defaultActor(env, 'system', 'cli'),
874
+ (client) =>
875
+ client.agents.bindRuntime({
876
+ agentId: agent,
877
+ runtime: options.runtime,
878
+ ...(options.profile ? { profile: options.profile } : {}),
879
+ ...(options.installation ? { installationId: options.installation } : {}),
880
+ }),
881
+ );
882
+ output(
883
+ io,
884
+ global.json,
885
+ binding,
886
+ `Bound ${binding.agentId} to ${binding.runtime}${binding.profile ? `/${binding.profile}` : ''} (${binding.id})`,
887
+ );
888
+ },
889
+ );
890
+
891
+ runtimeCommand
892
+ .command('list <agent>')
893
+ .description('List an agent runtime bindings')
894
+ .action(async (agent: string, _options, command: Command) => {
895
+ const global = globals(command);
896
+ const bindings = await withClient(global.home, defaultActor(env, 'system', 'cli'), (client) =>
897
+ client.agents.bindings(agent),
898
+ );
899
+ const human = bindings.length
900
+ ? bindings
901
+ .map(
902
+ (binding) =>
903
+ `${binding.id} ${binding.runtime}${binding.profile ? `/${binding.profile}` : ''} bound ${binding.boundAt}`,
904
+ )
905
+ .join('\n')
906
+ : 'No runtime bindings.';
907
+ output(io, global.json, { bindings }, human);
908
+ });
909
+
910
+ runtimeCommand
911
+ .command('unbind <binding-id>')
912
+ .description('Remove a runtime binding')
913
+ .action(async (bindingId: string, _options, command: Command) => {
914
+ const global = globals(command);
915
+ const removed = await withClient(global.home, defaultActor(env, 'system', 'cli'), (client) =>
916
+ client.agents.unbindRuntime(bindingId),
917
+ );
918
+ output(
919
+ io,
920
+ global.json,
921
+ { removed },
922
+ removed ? `Removed binding ${bindingId}.` : `No binding ${bindingId}.`,
923
+ );
924
+ });
925
+
926
+ const postCommand = program.command('post').description('Publish to everyone in the workspace');
927
+
928
+ postCommand
929
+ .command('create')
930
+ .description('Publish a post the whole workspace can read')
931
+ .requiredOption('--as <actor-id>')
932
+ .option('--actor-kind <kind>', 'human, agent, or system', 'agent')
933
+ .requiredOption('--title <title>')
934
+ .requiredOption('--body <body>')
935
+ .option('--tag <tag>', 'repeatable', collect, [])
936
+ .option('--reply-to <post-id>')
937
+ .action(
938
+ async (
939
+ options: {
940
+ as: string;
941
+ actorKind: string;
942
+ title: string;
943
+ body: string;
944
+ tag: string[];
945
+ replyTo?: string;
946
+ },
947
+ command: Command,
948
+ ) => {
949
+ const global = globals(command);
950
+ const result = await withClient(
951
+ global.home,
952
+ actor(options.actorKind, options.as),
953
+ (client) =>
954
+ client.posts.create({
955
+ title: options.title,
956
+ body: options.body,
957
+ ...(options.tag.length ? { tags: options.tag } : {}),
958
+ ...(options.replyTo ? { replyTo: options.replyTo } : {}),
959
+ }),
960
+ );
961
+ output(io, global.json, result, `Published ${result.record.event.id}`);
962
+ },
963
+ );
964
+
965
+ postCommand
966
+ .command('list')
967
+ .description('List posts in this workspace')
968
+ .requiredOption('--as <actor-id>')
969
+ .option('--actor-kind <kind>', 'human, agent, or system', 'agent')
970
+ .option('--limit <n>', 'default 10, maximum 50')
971
+ .action(
972
+ async (options: { as: string; actorKind: string; limit?: string }, command: Command) => {
973
+ const global = globals(command);
974
+ const page = await withClient(global.home, actor(options.actorKind, options.as), (client) =>
975
+ client.posts.list(options.limit ? { limit: Number(options.limit) } : {}),
976
+ );
977
+ const human = page.items.length
978
+ ? page.items.map((item) => `${item.id} ${item.title}`).join('\n')
979
+ : 'No posts yet.';
980
+ output(io, global.json, page, human);
981
+ },
982
+ );
983
+
984
+ postCommand
985
+ .command('show <post-id>')
986
+ .description('Show one post with its acknowledgements')
987
+ .requiredOption('--as <actor-id>')
988
+ .option('--actor-kind <kind>', 'human, agent, or system', 'agent')
989
+ .action(
990
+ async (postId: string, options: { as: string; actorKind: string }, command: Command) => {
991
+ const global = globals(command);
992
+ const record = await withClient(
993
+ global.home,
994
+ actor(options.actorKind, options.as),
995
+ (client) => client.posts.get(postId),
996
+ );
997
+ const acks = record.acknowledgments.length
998
+ ? record.acknowledgments
999
+ .map((entry) => ` ${entry.actor.id}${entry.note ? ` — ${entry.note}` : ''}`)
1000
+ .join('\n')
1001
+ : ' none yet';
1002
+ output(
1003
+ io,
1004
+ global.json,
1005
+ record,
1006
+ `${record.title}\n\n${record.body}\n\nAcknowledged by:\n${acks}`,
1007
+ );
1008
+ },
1009
+ );
1010
+
1011
+ postCommand
1012
+ .command('acknowledge <post-id>')
1013
+ .description('Say you have seen a post')
1014
+ .requiredOption('--as <actor-id>')
1015
+ .option('--actor-kind <kind>', 'human, agent, or system', 'agent')
1016
+ .option('--note <text>', 'optional context for the author')
1017
+ .action(
1018
+ async (
1019
+ postId: string,
1020
+ options: { as: string; actorKind: string; note?: string },
1021
+ command: Command,
1022
+ ) => {
1023
+ const global = globals(command);
1024
+ const record = await withClient(
1025
+ global.home,
1026
+ actor(options.actorKind, options.as),
1027
+ (client) =>
1028
+ client.posts.acknowledge({
1029
+ postId,
1030
+ ...(options.note ? { note: options.note } : {}),
1031
+ }),
1032
+ );
1033
+ output(io, global.json, record, `Acknowledged ${postId}`);
1034
+ },
1035
+ );
1036
+
1037
+ postCommand
1038
+ .command('roster <post-id>')
1039
+ .description('Who has acknowledged a post, and who has not')
1040
+ .requiredOption('--as <actor-id>')
1041
+ .option('--actor-kind <kind>', 'human, agent, or system', 'agent')
1042
+ .action(
1043
+ async (postId: string, options: { as: string; actorKind: string }, command: Command) => {
1044
+ const global = globals(command);
1045
+ const roster = await withClient(
1046
+ global.home,
1047
+ actor(options.actorKind, options.as),
1048
+ (client) => client.posts.roster(postId),
1049
+ );
1050
+ // "Outstanding" means no acknowledgement recorded — never that somebody
1051
+ // has not read it, which this cannot know.
1052
+ const lines = [
1053
+ `Acknowledged (${roster.acknowledged.length}):`,
1054
+ ...(roster.acknowledged.length
1055
+ ? roster.acknowledged.map((entry) => ` ${entry.actor.id}`)
1056
+ : [' none yet']),
1057
+ `No acknowledgement recorded (${roster.outstanding.length}):`,
1058
+ ...(roster.outstanding.length
1059
+ ? roster.outstanding.map((entry) => ` ${entry.id}`)
1060
+ : [' none']),
1061
+ ];
1062
+ if (roster.joinedSince > 0) {
1063
+ lines.push(`${roster.joinedSince} agent(s) joined after this was posted.`);
1064
+ }
1065
+ output(io, global.json, roster, lines.join('\n'));
1066
+ },
1067
+ );
1068
+
1069
+ postCommand
1070
+ .command('archive <post-id>')
1071
+ .description('Archive a post you wrote')
1072
+ .requiredOption('--as <actor-id>')
1073
+ .option('--actor-kind <kind>', 'human, agent, or system', 'agent')
1074
+ .option('--reason <text>')
1075
+ .action(
1076
+ async (
1077
+ postId: string,
1078
+ options: { as: string; actorKind: string; reason?: string },
1079
+ command: Command,
1080
+ ) => {
1081
+ const global = globals(command);
1082
+ const record = await withClient(
1083
+ global.home,
1084
+ actor(options.actorKind, options.as),
1085
+ (client) =>
1086
+ client.posts.archive({ postId, ...(options.reason ? { reason: options.reason } : {}) }),
1087
+ );
1088
+ output(io, global.json, record, `Archived ${postId}`);
1089
+ },
1090
+ );
1091
+
765
1092
  const kudosCommand = program.command('kudos').description('Give and manage agent recognition');
766
1093
 
767
1094
  kudosCommand
@@ -819,7 +1146,7 @@ export function createCli(
819
1146
 
820
1147
  program
821
1148
  .command('inbox [agent]')
822
- .description('Show pending kudos, memos, and todos for an agent')
1149
+ .description('Show pending kudos, memos, and tasks for an agent')
823
1150
  .option('--as <agent-id>', 'defaults to the positional agent')
824
1151
  .option('--limit <number>', 'maximum results (default 10, maximum 50)', '10')
825
1152
  .option('--cursor <cursor>', 'opaque cursor returned by the previous page')
@@ -876,7 +1203,7 @@ export function createCli(
876
1203
  program
877
1204
  .command('list')
878
1205
  .description('List compact summaries across all record types')
879
- .option('--kind <kind>', 'kudos, memo, note, or todo (repeatable)', collect, [])
1206
+ .option('--kind <kind>', 'kudos, memo, note, or task (repeatable)', collect, [])
880
1207
  .option('--participant <agent>')
881
1208
  .option('--actor <id>')
882
1209
  .option('--tag <tag>')
@@ -1194,8 +1521,163 @@ export function createCli(
1194
1521
  },
1195
1522
  );
1196
1523
 
1197
- const todoCommand = program.command('todo').description('Create and manage agent todos');
1524
+ const todoCommand = program
1525
+ .command('todo')
1526
+ .description('Create and manage your own private reminders');
1527
+ todoCommand
1528
+ .command('create')
1529
+ .requiredOption('--as <actor-id>')
1530
+ .option('--actor-kind <kind>', 'human, agent, or system', 'agent')
1531
+ .requiredOption('--title <title>')
1532
+ .option('--details <text>', 'private working detail')
1533
+ .option('--priority <number>', '1 highest, 4 lowest', '3')
1534
+ .option('--due-date <date>')
1535
+ .option('--due-at <datetime>')
1536
+ .option('--time-zone <iana-zone>')
1537
+ .option('--tag <tag>', 'tag (repeatable)', collect, [])
1538
+ .option('--idempotency-key <key>')
1539
+ .action(
1540
+ async (
1541
+ options: {
1542
+ as: string;
1543
+ actorKind: string;
1544
+ title: string;
1545
+ details?: string;
1546
+ priority: string;
1547
+ dueDate?: string;
1548
+ dueAt?: string;
1549
+ timeZone?: string;
1550
+ tag: string[];
1551
+ idempotencyKey?: string;
1552
+ },
1553
+ command: Command,
1554
+ ) => {
1555
+ const global = globals(command);
1556
+ const result = await withClient(
1557
+ global.home,
1558
+ actor(options.actorKind, options.as),
1559
+ (client) =>
1560
+ client.todos.create({
1561
+ title: options.title,
1562
+ ...(options.details ? { details: options.details } : {}),
1563
+ priority: Number(options.priority) as 1 | 2 | 3 | 4,
1564
+ ...((due) => (due ? { due } : {}))(taskDue(options)),
1565
+ ...(options.tag.length ? { tags: options.tag } : {}),
1566
+ ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
1567
+ }),
1568
+ );
1569
+ output(
1570
+ io,
1571
+ global.json,
1572
+ result,
1573
+ `${result.deduplicated ? 'Found existing' : 'Created'} private todo\nTitle: ${result.record.current.title}\nID: ${result.record.event.id}`,
1574
+ );
1575
+ },
1576
+ );
1577
+ todoCommand
1578
+ .command('list')
1579
+ .requiredOption('--as <actor-id>')
1580
+ .option('--actor-kind <kind>', 'human, agent, or system', 'agent')
1581
+ .option('--status <status>')
1582
+ .option('--limit <number>', 'maximum results', '10')
1583
+ .action(
1584
+ async (
1585
+ options: { as: string; actorKind: string; status?: string; limit: string },
1586
+ command: Command,
1587
+ ) => {
1588
+ const global = globals(command);
1589
+ const page = await withClient(global.home, actor(options.actorKind, options.as), (client) =>
1590
+ client.todos.list({
1591
+ ...(options.status ? { status: options.status } : {}),
1592
+ limit: Number(options.limit),
1593
+ }),
1594
+ );
1595
+ output(
1596
+ io,
1597
+ global.json,
1598
+ page,
1599
+ page.items.length
1600
+ ? page.items
1601
+ .map((item) => `${item.id} ${item.status.padEnd(9)} ${item.title}`)
1602
+ .join('\n')
1603
+ : 'No todos.',
1604
+ );
1605
+ },
1606
+ );
1198
1607
  todoCommand
1608
+ .command('show <todo-id>')
1609
+ .requiredOption('--as <actor-id>')
1610
+ .option('--actor-kind <kind>', 'human, agent, or system', 'agent')
1611
+ .action(async (id: string, options: { as: string; actorKind: string }, command: Command) => {
1612
+ const global = globals(command);
1613
+ const record = await withClient(global.home, actor(options.actorKind, options.as), (client) =>
1614
+ client.todos.get(id),
1615
+ );
1616
+ output(
1617
+ io,
1618
+ global.json,
1619
+ record,
1620
+ `${record.current.title}\nStatus: ${record.status}\nPriority: ${record.current.priority}\nVersion: ${record.current.version}${record.current.details ? `\n\n${record.current.details}` : ''}`,
1621
+ );
1622
+ });
1623
+ for (const operation of ['complete', 'reopen', 'cancel', 'archive'] as const) {
1624
+ todoCommand
1625
+ .command(`${operation} <todo-id>`)
1626
+ .requiredOption('--as <actor-id>')
1627
+ .option('--actor-kind <kind>', 'human, agent, or system', 'agent')
1628
+ .option('--note <text>')
1629
+ .option('--reason <text>')
1630
+ .option('--idempotency-key <key>')
1631
+ .action(
1632
+ async (
1633
+ id: string,
1634
+ options: {
1635
+ as: string;
1636
+ actorKind: string;
1637
+ note?: string;
1638
+ reason?: string;
1639
+ idempotencyKey?: string;
1640
+ },
1641
+ command: Command,
1642
+ ) => {
1643
+ const global = globals(command);
1644
+ const record = await withClient(
1645
+ global.home,
1646
+ actor(options.actorKind, options.as),
1647
+ (client) =>
1648
+ operation === 'complete'
1649
+ ? client.todos.complete({
1650
+ todoId: id,
1651
+ ...(options.note ? { note: options.note } : {}),
1652
+ ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
1653
+ })
1654
+ : operation === 'cancel'
1655
+ ? client.todos.cancel({
1656
+ todoId: id,
1657
+ ...(options.reason ? { reason: options.reason } : {}),
1658
+ ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
1659
+ })
1660
+ : operation === 'archive'
1661
+ ? client.todos.archive({
1662
+ todoId: id,
1663
+ ...(options.idempotencyKey
1664
+ ? { idempotencyKey: options.idempotencyKey }
1665
+ : {}),
1666
+ })
1667
+ : client.todos.reopen({
1668
+ todoId: id,
1669
+ ...(options.idempotencyKey
1670
+ ? { idempotencyKey: options.idempotencyKey }
1671
+ : {}),
1672
+ }),
1673
+ );
1674
+ output(io, global.json, record, `Todo ${id} is now ${record.status}.`);
1675
+ },
1676
+ );
1677
+ }
1678
+
1679
+ const taskCommand = program.command('task').description('Create and manage agent tasks');
1680
+ taskCommand
1199
1681
  .command('create <assignee>')
1200
1682
  .requiredOption('--from <actor-id>')
1201
1683
  .option('--actor-kind <kind>', 'human, agent, or system', 'agent')
@@ -1231,12 +1713,12 @@ export function createCli(
1231
1713
  global.home,
1232
1714
  actor(options.actorKind, options.from),
1233
1715
  (client) =>
1234
- client.todos.create({
1716
+ client.tasks.create({
1235
1717
  assigneeAgentId: assignee,
1236
1718
  title: options.title,
1237
1719
  ...(options.description ? { description: options.description } : {}),
1238
1720
  priority: Number(options.priority) as 1 | 2 | 3 | 4,
1239
- ...((due) => (due ? { due } : {}))(todoDue(options)),
1721
+ ...((due) => (due ? { due } : {}))(taskDue(options)),
1240
1722
  ...(options.tag.length ? { tags: options.tag } : {}),
1241
1723
  visibility: options.visibility,
1242
1724
  ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
@@ -1246,11 +1728,11 @@ export function createCli(
1246
1728
  io,
1247
1729
  global.json,
1248
1730
  result,
1249
- `${result.deduplicated ? 'Found existing' : 'Created'} todo for ${result.record.event.assigneeDisplayName}\nTitle: ${result.record.current.title}\nID: ${result.record.event.id}`,
1731
+ `${result.deduplicated ? 'Found existing' : 'Created'} task for ${result.record.event.assigneeDisplayName}\nTitle: ${result.record.current.title}\nID: ${result.record.event.id}`,
1250
1732
  );
1251
1733
  },
1252
1734
  );
1253
- todoCommand
1735
+ taskCommand
1254
1736
  .command('list')
1255
1737
  .option('--assignee <agent>')
1256
1738
  .option('--status <status>')
@@ -1262,21 +1744,21 @@ export function createCli(
1262
1744
  global.home,
1263
1745
  defaultActor(env, 'human', 'local-cli'),
1264
1746
  (client) =>
1265
- client.todos.list({
1747
+ client.tasks.list({
1266
1748
  ...(options.assignee ? { participantAgentId: options.assignee } : {}),
1267
1749
  ...(options.status ? { status: options.status } : {}),
1268
1750
  limit: Number(options.limit),
1269
1751
  }),
1270
1752
  );
1271
- output(io, global.json, page, page.items.map(lineForItem).join('\n') || 'No todos found.');
1753
+ output(io, global.json, page, page.items.map(lineForItem).join('\n') || 'No tasks found.');
1272
1754
  },
1273
1755
  );
1274
- todoCommand.command('show <todo-id>').action(async (id: string, _options, command: Command) => {
1756
+ taskCommand.command('show <task-id>').action(async (id: string, _options, command: Command) => {
1275
1757
  const global = globals(command);
1276
1758
  const record = await withClient(
1277
1759
  global.home,
1278
1760
  defaultActor(env, 'human', 'local-cli'),
1279
- (client) => client.todos.get(id),
1761
+ (client) => client.tasks.get(id),
1280
1762
  );
1281
1763
  output(
1282
1764
  io,
@@ -1285,8 +1767,8 @@ export function createCli(
1285
1767
  `${record.current.title}\nID: ${record.event.id}\nStatus: ${record.status}\nVersion: ${record.current.version}`,
1286
1768
  );
1287
1769
  });
1288
- todoCommand
1289
- .command('update <todo-id>')
1770
+ taskCommand
1771
+ .command('update <task-id>')
1290
1772
  .requiredOption('--as <actor-id>')
1291
1773
  .option('--actor-kind <kind>', 'agent or human', 'agent')
1292
1774
  .requiredOption('--expected-version <number>')
@@ -1319,13 +1801,13 @@ export function createCli(
1319
1801
  command: Command,
1320
1802
  ) => {
1321
1803
  const global = globals(command);
1322
- const parsedDue = todoDue(options);
1804
+ const parsedDue = taskDue(options);
1323
1805
  const record = await withClient(
1324
1806
  global.home,
1325
1807
  actor(options.actorKind, options.as),
1326
1808
  (client) =>
1327
- client.todos.update({
1328
- todoId: id,
1809
+ client.tasks.update({
1810
+ taskId: id,
1329
1811
  expectedVersion: Number(options.expectedVersion),
1330
1812
  ...(options.title ? { title: options.title } : {}),
1331
1813
  ...(options.description !== undefined ? { description: options.description } : {}),
@@ -1335,71 +1817,78 @@ export function createCli(
1335
1817
  ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
1336
1818
  }),
1337
1819
  );
1338
- output(io, global.json, record, `Updated todo ${id} to version ${record.current.version}.`);
1820
+ output(io, global.json, record, `Updated task ${id} to version ${record.current.version}.`);
1339
1821
  },
1340
1822
  );
1341
1823
  for (const operation of ['accept', 'reject', 'complete', 'reopen', 'cancel'] as const) {
1342
- todoCommand
1343
- .command(`${operation} <todo-id>`)
1824
+ const command_ = taskCommand
1825
+ .command(`${operation} <task-id>`)
1344
1826
  .requiredOption('--as <actor-id>')
1345
1827
  .option('--actor-kind <kind>', 'agent or human', 'agent')
1346
1828
  .option('--note <text>')
1347
1829
  .option('--reason <text>')
1348
- .option('--idempotency-key <key>')
1349
- .action(
1350
- async (
1351
- id: string,
1352
- options: {
1353
- as: string;
1354
- actorKind: string;
1355
- note?: string;
1356
- reason?: string;
1357
- idempotencyKey?: string;
1358
- },
1359
- command: Command,
1360
- ) => {
1361
- const global = globals(command);
1362
- const record = await withClient(
1363
- global.home,
1364
- actor(options.actorKind, options.as),
1365
- (client) =>
1366
- operation === 'accept'
1367
- ? client.todos.accept({
1368
- todoId: id,
1830
+ .option('--idempotency-key <key>');
1831
+ // Rejecting requires saying why; accepting may. Marked required at the
1832
+ // parser so the CLI refuses before touching the store.
1833
+ if (operation === 'reject') {
1834
+ command_.requiredOption('--response <text>', 'why the task is being refused');
1835
+ } else if (operation === 'accept') {
1836
+ command_.option('--response <text>', 'conditions, timing, or partial capability');
1837
+ }
1838
+ command_.action(
1839
+ async (
1840
+ id: string,
1841
+ options: {
1842
+ as: string;
1843
+ actorKind: string;
1844
+ note?: string;
1845
+ reason?: string;
1846
+ response?: string;
1847
+ idempotencyKey?: string;
1848
+ },
1849
+ command: Command,
1850
+ ) => {
1851
+ const global = globals(command);
1852
+ const record = await withClient(
1853
+ global.home,
1854
+ actor(options.actorKind, options.as),
1855
+ (client) =>
1856
+ operation === 'accept'
1857
+ ? client.tasks.accept({
1858
+ taskId: id,
1859
+ ...(options.response ? { response: options.response } : {}),
1860
+ ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
1861
+ })
1862
+ : operation === 'reject'
1863
+ ? client.tasks.reject({
1864
+ taskId: id,
1865
+ response: options.response ?? options.reason ?? '',
1369
1866
  ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
1370
1867
  })
1371
- : operation === 'reject'
1372
- ? client.todos.reject({
1373
- todoId: id,
1374
- ...(options.reason ? { reason: options.reason } : {}),
1868
+ : operation === 'complete'
1869
+ ? client.tasks.complete({
1870
+ taskId: id,
1871
+ ...(options.note ? { note: options.note } : {}),
1375
1872
  ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
1376
1873
  })
1377
- : operation === 'complete'
1378
- ? client.todos.complete({
1379
- todoId: id,
1380
- ...(options.note ? { note: options.note } : {}),
1874
+ : operation === 'cancel'
1875
+ ? client.tasks.cancel({
1876
+ taskId: id,
1877
+ ...(options.reason ? { reason: options.reason } : {}),
1381
1878
  ...(options.idempotencyKey
1382
1879
  ? { idempotencyKey: options.idempotencyKey }
1383
1880
  : {}),
1384
1881
  })
1385
- : operation === 'cancel'
1386
- ? client.todos.cancel({
1387
- todoId: id,
1388
- ...(options.reason ? { reason: options.reason } : {}),
1389
- ...(options.idempotencyKey
1390
- ? { idempotencyKey: options.idempotencyKey }
1391
- : {}),
1392
- })
1393
- : client.todos.reopen({
1394
- todoId: id,
1395
- ...(options.idempotencyKey
1396
- ? { idempotencyKey: options.idempotencyKey }
1397
- : {}),
1398
- }),
1399
- );
1400
- output(io, global.json, record, `Todo ${id} is ${record.status}.`);
1401
- },
1402
- );
1882
+ : client.tasks.reopen({
1883
+ taskId: id,
1884
+ ...(options.idempotencyKey
1885
+ ? { idempotencyKey: options.idempotencyKey }
1886
+ : {}),
1887
+ }),
1888
+ );
1889
+ output(io, global.json, record, `Task ${id} is ${record.status}.`);
1890
+ },
1891
+ );
1403
1892
  }
1404
1893
 
1405
1894
  kudosCommand
@@ -1621,18 +2110,44 @@ export function createCli(
1621
2110
  program
1622
2111
  .command('mcp')
1623
2112
  .description('Run the actor-bound MCP server over stdio')
1624
- .requiredOption('--actor-id <id>')
1625
- .requiredOption('--actor-kind <kind>', 'human, agent, or system')
1626
- .option('--actor-name <display-name>')
2113
+ .option('--agent-id <id>', 'bound agent, whose identity is read from Synomem')
2114
+ .option('--actor-id <id>', 'bound non-agent actor ID')
2115
+ .option('--actor-kind <kind>', 'human or system')
2116
+ .option('--actor-name <display-name>', 'display name for a non-agent actor')
1627
2117
  .action(
1628
2118
  async (
1629
- options: { actorId: string; actorKind: string; actorName?: string },
2119
+ options: { agentId?: string; actorId?: string; actorKind?: string; actorName?: string },
1630
2120
  command: Command,
1631
2121
  ) => {
1632
2122
  const global = globals(command);
2123
+ // An agent's name comes from its profile, never from the command line:
2124
+ // the name is written into every event the session appends, and a
2125
+ // harness must not be able to sign another agent's name to work.
2126
+ const bound = options.agentId
2127
+ ? await withClient(global.home, defaultActor(env, 'system', 'cli'), async (client) => {
2128
+ const resolution = await client.agents.resolve(options.agentId!);
2129
+ if (!resolution.match) {
2130
+ throw new SynomemError(
2131
+ 'AGENT_NOT_FOUND',
2132
+ resolution.candidates.length
2133
+ ? `"${options.agentId}" matches ${resolution.candidates.length} agents: ${resolution.candidates
2134
+ .map((candidate) => candidate.id)
2135
+ .join(', ')}. Name one of them.`
2136
+ : `Unknown agent: ${options.agentId}`,
2137
+ );
2138
+ }
2139
+ return actor('agent', resolution.match.id, resolution.match.displayName);
2140
+ })
2141
+ : undefined;
2142
+ if (!bound && !(options.actorId && options.actorKind)) {
2143
+ throw new SynomemError(
2144
+ 'INVALID_INPUT',
2145
+ 'Specify --agent-id, or --actor-id with --actor-kind for a non-agent actor.',
2146
+ );
2147
+ }
1633
2148
  await startMcpServer({
1634
2149
  ...(global.home ? { home: global.home } : {}),
1635
- actor: actor(options.actorKind, options.actorId, options.actorName),
2150
+ actor: bound ?? actor(options.actorKind!, options.actorId!, options.actorName),
1636
2151
  });
1637
2152
  },
1638
2153
  );