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/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';
@@ -109,11 +109,27 @@ function actor(kind: string, id: string, displayName?: string): ActorIdentity {
109
109
  };
110
110
  }
111
111
 
112
- function todoDue(options: {
112
+ /**
113
+ * The acting identity for commands that do not take an explicit actor.
114
+ * Local storage accepts any actor, so these fall back to the historical CLI defaults; a remote
115
+ * backend binds the credential to one actor, so SYNOMEM_ACTOR_ID/KIND/NAME must be able to
116
+ * override them or those commands cannot authenticate.
117
+ */
118
+ function defaultActor(
119
+ env: NodeJS.ProcessEnv,
120
+ fallbackKind: string,
121
+ fallbackId: string,
122
+ ): ActorIdentity {
123
+ const id = env.SYNOMEM_ACTOR_ID?.trim();
124
+ if (!id) return actor(fallbackKind, fallbackId);
125
+ return actor(env.SYNOMEM_ACTOR_KIND?.trim() || fallbackKind, id, env.SYNOMEM_ACTOR_NAME?.trim());
126
+ }
127
+
128
+ function taskDue(options: {
113
129
  dueDate?: string;
114
130
  dueAt?: string;
115
131
  timeZone?: string;
116
- }): TodoDue | undefined {
132
+ }): TaskDue | undefined {
117
133
  if (options.dueDate && options.dueAt)
118
134
  throw new SynomemError('INVALID_INPUT', 'Use due-date or due-at, not both.');
119
135
  if (options.dueDate) return { kind: 'date', date: options.dueDate };
@@ -386,7 +402,7 @@ export function createCli(
386
402
  if (persisted?.backend.kind === 'remote') {
387
403
  throw new SynomemError('INVALID_INPUT', 'The init command requires a local backend.');
388
404
  }
389
- await withClient(options.home, actor('system', 'cli'), async (client) => {
405
+ await withClient(options.home, defaultActor(env, 'system', 'cli'), async (client) => {
390
406
  const info = await client.info();
391
407
  if (info.backend !== 'local') {
392
408
  throw new SynomemError('INVALID_INPUT', 'The init command requires a local backend.');
@@ -595,13 +611,16 @@ export function createCli(
595
611
  command: Command,
596
612
  ) => {
597
613
  const global = globals(command);
598
- const profile = await withClient(global.home, actor('system', 'cli'), (client) =>
599
- client.agents.create({
600
- id,
601
- displayName: options.name,
602
- ...(options.alias.length ? { aliases: options.alias } : {}),
603
- ...(options.description ? { description: options.description } : {}),
604
- }),
614
+ const profile = await withClient(
615
+ global.home,
616
+ defaultActor(env, 'system', 'cli'),
617
+ (client) =>
618
+ client.agents.create({
619
+ id,
620
+ displayName: options.name,
621
+ ...(options.alias.length ? { aliases: options.alias } : {}),
622
+ ...(options.description ? { description: options.description } : {}),
623
+ }),
605
624
  );
606
625
  output(io, global.json, profile, `Created ${profile.displayName} (${profile.id})`);
607
626
  },
@@ -618,29 +637,73 @@ export function createCli(
618
637
  .option('--yes', 'apply the displayed plan', false)
619
638
  .option('--force', 'replace a conflicting synomem directory', false)
620
639
  .option('--link', 'symlink to the packaged skill instead of copying it', false)
621
- .option('--actor-id <id>', 'print actor-bound MCP registration commands')
622
- .option('--actor-name <name>', 'display name used in MCP registration commands')
640
+ .option('--agent <id-or-alias>', 'bind this installation to an agent')
623
641
  .action(
624
- (
642
+ async (
625
643
  options: {
626
644
  runtime: string[];
627
645
  yes: boolean;
628
646
  force: boolean;
629
647
  link: boolean;
630
- actorId?: string;
631
- actorName?: string;
648
+ agent?: string;
632
649
  },
633
650
  command: Command,
634
651
  ) => {
635
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
+
636
683
  const result = installSkill({
637
- runtimes: skillRuntimes(options.runtime),
684
+ ...(runtimes ? { runtimes } : {}),
638
685
  apply: options.yes,
639
686
  force: options.force,
640
687
  link: options.link,
641
- actorId: options.actorId,
642
- actorName: options.actorName,
688
+ ...(agentId ? { agentId } : {}),
643
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
+
644
707
  output(io, global.json, result, formatSkillResult(result, 'install'));
645
708
  },
646
709
  );
@@ -649,19 +712,15 @@ export function createCli(
649
712
  .command('status')
650
713
  .description('Show installed, stale, missing, or conflicting skill copies')
651
714
  .option('--runtime <runtime>', `${skillRuntimeHelp} (repeatable)`, collect, [])
652
- .option('--actor-id <id>', 'print actor-bound MCP registration commands')
653
- .option('--actor-name <name>', 'display name used in MCP registration commands')
654
- .action(
655
- (options: { runtime: string[]; actorId?: string; actorName?: string }, command: Command) => {
656
- const global = globals(command);
657
- const result = skillStatus({
658
- runtimes: skillRuntimes(options.runtime),
659
- actorId: options.actorId,
660
- actorName: options.actorName,
661
- });
662
- output(io, global.json, result, formatSkillResult(result, 'status'));
663
- },
664
- );
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
+ });
665
724
 
666
725
  skillCommand
667
726
  .command('uninstall')
@@ -684,7 +743,7 @@ export function createCli(
684
743
  .description('List known agent identities')
685
744
  .action(async (_options, command: Command) => {
686
745
  const global = globals(command);
687
- const agents = await withClient(global.home, actor('system', 'cli'), (client) =>
746
+ const agents = await withClient(global.home, defaultActor(env, 'system', 'cli'), (client) =>
688
747
  client.agents.list(),
689
748
  );
690
749
  const human = agents.length
@@ -703,7 +762,7 @@ export function createCli(
703
762
  .description('Show one agent profile, resolving aliases')
704
763
  .action(async (id: string, _options, command: Command) => {
705
764
  const global = globals(command);
706
- const profile = await withClient(global.home, actor('system', 'cli'), (client) =>
765
+ const profile = await withClient(global.home, defaultActor(env, 'system', 'cli'), (client) =>
707
766
  client.agents.get(id),
708
767
  );
709
768
  output(
@@ -729,17 +788,141 @@ export function createCli(
729
788
  ) => {
730
789
  const global = globals(command);
731
790
  const hasAliases = options.clearAliases || options.alias.length > 0;
732
- const profile = await withClient(global.home, actor('system', 'cli'), (client) =>
733
- client.agents.update(id, {
734
- ...(options.name ? { displayName: options.name } : {}),
735
- ...(hasAliases ? { aliases: options.clearAliases ? [] : options.alias } : {}),
736
- ...(options.description !== undefined ? { description: options.description } : {}),
737
- }),
791
+ const profile = await withClient(
792
+ global.home,
793
+ defaultActor(env, 'system', 'cli'),
794
+ (client) =>
795
+ client.agents.update(id, {
796
+ ...(options.name ? { displayName: options.name } : {}),
797
+ ...(hasAliases ? { aliases: options.clearAliases ? [] : options.alias } : {}),
798
+ ...(options.description !== undefined ? { description: options.description } : {}),
799
+ }),
738
800
  );
739
801
  output(io, global.json, profile, `Updated ${profile.displayName} (${profile.id})`);
740
802
  },
741
803
  );
742
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
+
743
926
  const kudosCommand = program.command('kudos').description('Give and manage agent recognition');
744
927
 
745
928
  kudosCommand
@@ -797,7 +980,7 @@ export function createCli(
797
980
 
798
981
  program
799
982
  .command('inbox [agent]')
800
- .description('Show pending kudos, memos, and todos for an agent')
983
+ .description('Show pending kudos, memos, and tasks for an agent')
801
984
  .option('--as <agent-id>', 'defaults to the positional agent')
802
985
  .option('--limit <number>', 'maximum results (default 10, maximum 50)', '10')
803
986
  .option('--cursor <cursor>', 'opaque cursor returned by the previous page')
@@ -835,8 +1018,10 @@ export function createCli(
835
1018
  addListOptions(kudosCommand.command('list').description('List and filter kudos')).action(
836
1019
  async (options: Record<string, string>, command: Command) => {
837
1020
  const global = globals(command);
838
- const page = await withClient(global.home, actor('human', 'local-cli'), (client) =>
839
- client.kudos.list(listInput(options)),
1021
+ const page = await withClient(
1022
+ global.home,
1023
+ defaultActor(env, 'human', 'local-cli'),
1024
+ (client) => client.kudos.list(listInput(options)),
840
1025
  );
841
1026
  output(
842
1027
  io,
@@ -852,7 +1037,7 @@ export function createCli(
852
1037
  program
853
1038
  .command('list')
854
1039
  .description('List compact summaries across all record types')
855
- .option('--kind <kind>', 'kudos, memo, note, or todo (repeatable)', collect, [])
1040
+ .option('--kind <kind>', 'kudos, memo, note, or task (repeatable)', collect, [])
856
1041
  .option('--participant <agent>')
857
1042
  .option('--actor <id>')
858
1043
  .option('--tag <tag>')
@@ -863,8 +1048,10 @@ export function createCli(
863
1048
  .option('--offset <number>', 'deprecated offset', '0')
864
1049
  .action(async (options: Record<string, string | string[]>, command: Command) => {
865
1050
  const global = globals(command);
866
- const page = await withClient(global.home, actor('human', 'local-cli'), (client) =>
867
- client.items.list(itemListInput(options)),
1051
+ const page = await withClient(
1052
+ global.home,
1053
+ defaultActor(env, 'human', 'local-cli'),
1054
+ (client) => client.items.list(itemListInput(options)),
868
1055
  );
869
1056
  output(
870
1057
  io,
@@ -885,12 +1072,15 @@ export function createCli(
885
1072
  .action(
886
1073
  async (options: { after?: string; limit: string; kind: string[] }, command: Command) => {
887
1074
  const global = globals(command);
888
- const page = await withClient(global.home, actor('human', 'local-cli'), (client) =>
889
- client.items.changes({
890
- limit: Number(options.limit),
891
- ...(options.kind.length ? { kinds: options.kind as ItemListInput['kinds'] } : {}),
892
- ...(options.after ? { after: options.after } : {}),
893
- }),
1075
+ const page = await withClient(
1076
+ global.home,
1077
+ defaultActor(env, 'human', 'local-cli'),
1078
+ (client) =>
1079
+ client.items.changes({
1080
+ limit: Number(options.limit),
1081
+ ...(options.kind.length ? { kinds: options.kind as ItemListInput['kinds'] } : {}),
1082
+ ...(options.after ? { after: options.after } : {}),
1083
+ }),
894
1084
  );
895
1085
  const human = page.items.length
896
1086
  ? `${page.items
@@ -964,21 +1154,26 @@ export function createCli(
964
1154
  command: Command,
965
1155
  ) => {
966
1156
  const global = globals(command);
967
- const page = await withClient(global.home, actor('human', 'local-cli'), (client) =>
968
- client.memos.list({
969
- ...(options.participant ? { participantAgentId: options.participant } : {}),
970
- ...(options.status ? { status: options.status } : {}),
971
- limit: Number(options.limit),
972
- ...(options.cursor ? { cursor: options.cursor } : {}),
973
- }),
1157
+ const page = await withClient(
1158
+ global.home,
1159
+ defaultActor(env, 'human', 'local-cli'),
1160
+ (client) =>
1161
+ client.memos.list({
1162
+ ...(options.participant ? { participantAgentId: options.participant } : {}),
1163
+ ...(options.status ? { status: options.status } : {}),
1164
+ limit: Number(options.limit),
1165
+ ...(options.cursor ? { cursor: options.cursor } : {}),
1166
+ }),
974
1167
  );
975
1168
  output(io, global.json, page, page.items.map(lineForItem).join('\n') || 'No memos found.');
976
1169
  },
977
1170
  );
978
1171
  memoCommand.command('show <memo-id>').action(async (id: string, _options, command: Command) => {
979
1172
  const global = globals(command);
980
- const record = await withClient(global.home, actor('human', 'local-cli'), (client) =>
981
- client.memos.get(id),
1173
+ const record = await withClient(
1174
+ global.home,
1175
+ defaultActor(env, 'human', 'local-cli'),
1176
+ (client) => client.memos.get(id),
982
1177
  );
983
1178
  output(
984
1179
  io,
@@ -1068,20 +1263,25 @@ export function createCli(
1068
1263
  .action(
1069
1264
  async (options: { owner?: string; status?: string; limit: string }, command: Command) => {
1070
1265
  const global = globals(command);
1071
- const page = await withClient(global.home, actor('human', 'local-cli'), (client) =>
1072
- client.notes.list({
1073
- ...(options.owner ? { participantAgentId: options.owner } : {}),
1074
- ...(options.status ? { status: options.status } : {}),
1075
- limit: Number(options.limit),
1076
- }),
1266
+ const page = await withClient(
1267
+ global.home,
1268
+ defaultActor(env, 'human', 'local-cli'),
1269
+ (client) =>
1270
+ client.notes.list({
1271
+ ...(options.owner ? { participantAgentId: options.owner } : {}),
1272
+ ...(options.status ? { status: options.status } : {}),
1273
+ limit: Number(options.limit),
1274
+ }),
1077
1275
  );
1078
1276
  output(io, global.json, page, page.items.map(lineForItem).join('\n') || 'No notes found.');
1079
1277
  },
1080
1278
  );
1081
1279
  noteCommand.command('show <note-id>').action(async (id: string, _options, command: Command) => {
1082
1280
  const global = globals(command);
1083
- const record = await withClient(global.home, actor('human', 'local-cli'), (client) =>
1084
- client.notes.get(id),
1281
+ const record = await withClient(
1282
+ global.home,
1283
+ defaultActor(env, 'human', 'local-cli'),
1284
+ (client) => client.notes.get(id),
1085
1285
  );
1086
1286
  output(
1087
1287
  io,
@@ -1155,8 +1355,163 @@ export function createCli(
1155
1355
  },
1156
1356
  );
1157
1357
 
1158
- const todoCommand = program.command('todo').description('Create and manage agent todos');
1358
+ const todoCommand = program
1359
+ .command('todo')
1360
+ .description('Create and manage your own private reminders');
1159
1361
  todoCommand
1362
+ .command('create')
1363
+ .requiredOption('--as <actor-id>')
1364
+ .option('--actor-kind <kind>', 'human, agent, or system', 'agent')
1365
+ .requiredOption('--title <title>')
1366
+ .option('--details <text>', 'private working detail')
1367
+ .option('--priority <number>', '1 highest, 4 lowest', '3')
1368
+ .option('--due-date <date>')
1369
+ .option('--due-at <datetime>')
1370
+ .option('--time-zone <iana-zone>')
1371
+ .option('--tag <tag>', 'tag (repeatable)', collect, [])
1372
+ .option('--idempotency-key <key>')
1373
+ .action(
1374
+ async (
1375
+ options: {
1376
+ as: string;
1377
+ actorKind: string;
1378
+ title: string;
1379
+ details?: string;
1380
+ priority: string;
1381
+ dueDate?: string;
1382
+ dueAt?: string;
1383
+ timeZone?: string;
1384
+ tag: string[];
1385
+ idempotencyKey?: string;
1386
+ },
1387
+ command: Command,
1388
+ ) => {
1389
+ const global = globals(command);
1390
+ const result = await withClient(
1391
+ global.home,
1392
+ actor(options.actorKind, options.as),
1393
+ (client) =>
1394
+ client.todos.create({
1395
+ title: options.title,
1396
+ ...(options.details ? { details: options.details } : {}),
1397
+ priority: Number(options.priority) as 1 | 2 | 3 | 4,
1398
+ ...((due) => (due ? { due } : {}))(taskDue(options)),
1399
+ ...(options.tag.length ? { tags: options.tag } : {}),
1400
+ ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
1401
+ }),
1402
+ );
1403
+ output(
1404
+ io,
1405
+ global.json,
1406
+ result,
1407
+ `${result.deduplicated ? 'Found existing' : 'Created'} private todo\nTitle: ${result.record.current.title}\nID: ${result.record.event.id}`,
1408
+ );
1409
+ },
1410
+ );
1411
+ todoCommand
1412
+ .command('list')
1413
+ .requiredOption('--as <actor-id>')
1414
+ .option('--actor-kind <kind>', 'human, agent, or system', 'agent')
1415
+ .option('--status <status>')
1416
+ .option('--limit <number>', 'maximum results', '10')
1417
+ .action(
1418
+ async (
1419
+ options: { as: string; actorKind: string; status?: string; limit: string },
1420
+ command: Command,
1421
+ ) => {
1422
+ const global = globals(command);
1423
+ const page = await withClient(global.home, actor(options.actorKind, options.as), (client) =>
1424
+ client.todos.list({
1425
+ ...(options.status ? { status: options.status } : {}),
1426
+ limit: Number(options.limit),
1427
+ }),
1428
+ );
1429
+ output(
1430
+ io,
1431
+ global.json,
1432
+ page,
1433
+ page.items.length
1434
+ ? page.items
1435
+ .map((item) => `${item.id} ${item.status.padEnd(9)} ${item.title}`)
1436
+ .join('\n')
1437
+ : 'No todos.',
1438
+ );
1439
+ },
1440
+ );
1441
+ todoCommand
1442
+ .command('show <todo-id>')
1443
+ .requiredOption('--as <actor-id>')
1444
+ .option('--actor-kind <kind>', 'human, agent, or system', 'agent')
1445
+ .action(async (id: string, options: { as: string; actorKind: string }, command: Command) => {
1446
+ const global = globals(command);
1447
+ const record = await withClient(global.home, actor(options.actorKind, options.as), (client) =>
1448
+ client.todos.get(id),
1449
+ );
1450
+ output(
1451
+ io,
1452
+ global.json,
1453
+ record,
1454
+ `${record.current.title}\nStatus: ${record.status}\nPriority: ${record.current.priority}\nVersion: ${record.current.version}${record.current.details ? `\n\n${record.current.details}` : ''}`,
1455
+ );
1456
+ });
1457
+ for (const operation of ['complete', 'reopen', 'cancel', 'archive'] as const) {
1458
+ todoCommand
1459
+ .command(`${operation} <todo-id>`)
1460
+ .requiredOption('--as <actor-id>')
1461
+ .option('--actor-kind <kind>', 'human, agent, or system', 'agent')
1462
+ .option('--note <text>')
1463
+ .option('--reason <text>')
1464
+ .option('--idempotency-key <key>')
1465
+ .action(
1466
+ async (
1467
+ id: string,
1468
+ options: {
1469
+ as: string;
1470
+ actorKind: string;
1471
+ note?: string;
1472
+ reason?: string;
1473
+ idempotencyKey?: string;
1474
+ },
1475
+ command: Command,
1476
+ ) => {
1477
+ const global = globals(command);
1478
+ const record = await withClient(
1479
+ global.home,
1480
+ actor(options.actorKind, options.as),
1481
+ (client) =>
1482
+ operation === 'complete'
1483
+ ? client.todos.complete({
1484
+ todoId: id,
1485
+ ...(options.note ? { note: options.note } : {}),
1486
+ ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
1487
+ })
1488
+ : operation === 'cancel'
1489
+ ? client.todos.cancel({
1490
+ todoId: id,
1491
+ ...(options.reason ? { reason: options.reason } : {}),
1492
+ ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
1493
+ })
1494
+ : operation === 'archive'
1495
+ ? client.todos.archive({
1496
+ todoId: id,
1497
+ ...(options.idempotencyKey
1498
+ ? { idempotencyKey: options.idempotencyKey }
1499
+ : {}),
1500
+ })
1501
+ : client.todos.reopen({
1502
+ todoId: id,
1503
+ ...(options.idempotencyKey
1504
+ ? { idempotencyKey: options.idempotencyKey }
1505
+ : {}),
1506
+ }),
1507
+ );
1508
+ output(io, global.json, record, `Todo ${id} is now ${record.status}.`);
1509
+ },
1510
+ );
1511
+ }
1512
+
1513
+ const taskCommand = program.command('task').description('Create and manage agent tasks');
1514
+ taskCommand
1160
1515
  .command('create <assignee>')
1161
1516
  .requiredOption('--from <actor-id>')
1162
1517
  .option('--actor-kind <kind>', 'human, agent, or system', 'agent')
@@ -1192,12 +1547,12 @@ export function createCli(
1192
1547
  global.home,
1193
1548
  actor(options.actorKind, options.from),
1194
1549
  (client) =>
1195
- client.todos.create({
1550
+ client.tasks.create({
1196
1551
  assigneeAgentId: assignee,
1197
1552
  title: options.title,
1198
1553
  ...(options.description ? { description: options.description } : {}),
1199
1554
  priority: Number(options.priority) as 1 | 2 | 3 | 4,
1200
- ...((due) => (due ? { due } : {}))(todoDue(options)),
1555
+ ...((due) => (due ? { due } : {}))(taskDue(options)),
1201
1556
  ...(options.tag.length ? { tags: options.tag } : {}),
1202
1557
  visibility: options.visibility,
1203
1558
  ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
@@ -1207,11 +1562,11 @@ export function createCli(
1207
1562
  io,
1208
1563
  global.json,
1209
1564
  result,
1210
- `${result.deduplicated ? 'Found existing' : 'Created'} todo for ${result.record.event.assigneeDisplayName}\nTitle: ${result.record.current.title}\nID: ${result.record.event.id}`,
1565
+ `${result.deduplicated ? 'Found existing' : 'Created'} task for ${result.record.event.assigneeDisplayName}\nTitle: ${result.record.current.title}\nID: ${result.record.event.id}`,
1211
1566
  );
1212
1567
  },
1213
1568
  );
1214
- todoCommand
1569
+ taskCommand
1215
1570
  .command('list')
1216
1571
  .option('--assignee <agent>')
1217
1572
  .option('--status <status>')
@@ -1219,20 +1574,25 @@ export function createCli(
1219
1574
  .action(
1220
1575
  async (options: { assignee?: string; status?: string; limit: string }, command: Command) => {
1221
1576
  const global = globals(command);
1222
- const page = await withClient(global.home, actor('human', 'local-cli'), (client) =>
1223
- client.todos.list({
1224
- ...(options.assignee ? { participantAgentId: options.assignee } : {}),
1225
- ...(options.status ? { status: options.status } : {}),
1226
- limit: Number(options.limit),
1227
- }),
1577
+ const page = await withClient(
1578
+ global.home,
1579
+ defaultActor(env, 'human', 'local-cli'),
1580
+ (client) =>
1581
+ client.tasks.list({
1582
+ ...(options.assignee ? { participantAgentId: options.assignee } : {}),
1583
+ ...(options.status ? { status: options.status } : {}),
1584
+ limit: Number(options.limit),
1585
+ }),
1228
1586
  );
1229
- output(io, global.json, page, page.items.map(lineForItem).join('\n') || 'No todos found.');
1587
+ output(io, global.json, page, page.items.map(lineForItem).join('\n') || 'No tasks found.');
1230
1588
  },
1231
1589
  );
1232
- todoCommand.command('show <todo-id>').action(async (id: string, _options, command: Command) => {
1590
+ taskCommand.command('show <task-id>').action(async (id: string, _options, command: Command) => {
1233
1591
  const global = globals(command);
1234
- const record = await withClient(global.home, actor('human', 'local-cli'), (client) =>
1235
- client.todos.get(id),
1592
+ const record = await withClient(
1593
+ global.home,
1594
+ defaultActor(env, 'human', 'local-cli'),
1595
+ (client) => client.tasks.get(id),
1236
1596
  );
1237
1597
  output(
1238
1598
  io,
@@ -1241,8 +1601,8 @@ export function createCli(
1241
1601
  `${record.current.title}\nID: ${record.event.id}\nStatus: ${record.status}\nVersion: ${record.current.version}`,
1242
1602
  );
1243
1603
  });
1244
- todoCommand
1245
- .command('update <todo-id>')
1604
+ taskCommand
1605
+ .command('update <task-id>')
1246
1606
  .requiredOption('--as <actor-id>')
1247
1607
  .option('--actor-kind <kind>', 'agent or human', 'agent')
1248
1608
  .requiredOption('--expected-version <number>')
@@ -1275,13 +1635,13 @@ export function createCli(
1275
1635
  command: Command,
1276
1636
  ) => {
1277
1637
  const global = globals(command);
1278
- const parsedDue = todoDue(options);
1638
+ const parsedDue = taskDue(options);
1279
1639
  const record = await withClient(
1280
1640
  global.home,
1281
1641
  actor(options.actorKind, options.as),
1282
1642
  (client) =>
1283
- client.todos.update({
1284
- todoId: id,
1643
+ client.tasks.update({
1644
+ taskId: id,
1285
1645
  expectedVersion: Number(options.expectedVersion),
1286
1646
  ...(options.title ? { title: options.title } : {}),
1287
1647
  ...(options.description !== undefined ? { description: options.description } : {}),
@@ -1291,71 +1651,78 @@ export function createCli(
1291
1651
  ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
1292
1652
  }),
1293
1653
  );
1294
- output(io, global.json, record, `Updated todo ${id} to version ${record.current.version}.`);
1654
+ output(io, global.json, record, `Updated task ${id} to version ${record.current.version}.`);
1295
1655
  },
1296
1656
  );
1297
1657
  for (const operation of ['accept', 'reject', 'complete', 'reopen', 'cancel'] as const) {
1298
- todoCommand
1299
- .command(`${operation} <todo-id>`)
1658
+ const command_ = taskCommand
1659
+ .command(`${operation} <task-id>`)
1300
1660
  .requiredOption('--as <actor-id>')
1301
1661
  .option('--actor-kind <kind>', 'agent or human', 'agent')
1302
1662
  .option('--note <text>')
1303
1663
  .option('--reason <text>')
1304
- .option('--idempotency-key <key>')
1305
- .action(
1306
- async (
1307
- id: string,
1308
- options: {
1309
- as: string;
1310
- actorKind: string;
1311
- note?: string;
1312
- reason?: string;
1313
- idempotencyKey?: string;
1314
- },
1315
- command: Command,
1316
- ) => {
1317
- const global = globals(command);
1318
- const record = await withClient(
1319
- global.home,
1320
- actor(options.actorKind, options.as),
1321
- (client) =>
1322
- operation === 'accept'
1323
- ? client.todos.accept({
1324
- todoId: id,
1664
+ .option('--idempotency-key <key>');
1665
+ // Rejecting requires saying why; accepting may. Marked required at the
1666
+ // parser so the CLI refuses before touching the store.
1667
+ if (operation === 'reject') {
1668
+ command_.requiredOption('--response <text>', 'why the task is being refused');
1669
+ } else if (operation === 'accept') {
1670
+ command_.option('--response <text>', 'conditions, timing, or partial capability');
1671
+ }
1672
+ command_.action(
1673
+ async (
1674
+ id: string,
1675
+ options: {
1676
+ as: string;
1677
+ actorKind: string;
1678
+ note?: string;
1679
+ reason?: string;
1680
+ response?: string;
1681
+ idempotencyKey?: string;
1682
+ },
1683
+ command: Command,
1684
+ ) => {
1685
+ const global = globals(command);
1686
+ const record = await withClient(
1687
+ global.home,
1688
+ actor(options.actorKind, options.as),
1689
+ (client) =>
1690
+ operation === 'accept'
1691
+ ? client.tasks.accept({
1692
+ taskId: id,
1693
+ ...(options.response ? { response: options.response } : {}),
1694
+ ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
1695
+ })
1696
+ : operation === 'reject'
1697
+ ? client.tasks.reject({
1698
+ taskId: id,
1699
+ response: options.response ?? options.reason ?? '',
1325
1700
  ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
1326
1701
  })
1327
- : operation === 'reject'
1328
- ? client.todos.reject({
1329
- todoId: id,
1330
- ...(options.reason ? { reason: options.reason } : {}),
1702
+ : operation === 'complete'
1703
+ ? client.tasks.complete({
1704
+ taskId: id,
1705
+ ...(options.note ? { note: options.note } : {}),
1331
1706
  ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
1332
1707
  })
1333
- : operation === 'complete'
1334
- ? client.todos.complete({
1335
- todoId: id,
1336
- ...(options.note ? { note: options.note } : {}),
1708
+ : operation === 'cancel'
1709
+ ? client.tasks.cancel({
1710
+ taskId: id,
1711
+ ...(options.reason ? { reason: options.reason } : {}),
1337
1712
  ...(options.idempotencyKey
1338
1713
  ? { idempotencyKey: options.idempotencyKey }
1339
1714
  : {}),
1340
1715
  })
1341
- : operation === 'cancel'
1342
- ? client.todos.cancel({
1343
- todoId: id,
1344
- ...(options.reason ? { reason: options.reason } : {}),
1345
- ...(options.idempotencyKey
1346
- ? { idempotencyKey: options.idempotencyKey }
1347
- : {}),
1348
- })
1349
- : client.todos.reopen({
1350
- todoId: id,
1351
- ...(options.idempotencyKey
1352
- ? { idempotencyKey: options.idempotencyKey }
1353
- : {}),
1354
- }),
1355
- );
1356
- output(io, global.json, record, `Todo ${id} is ${record.status}.`);
1357
- },
1358
- );
1716
+ : client.tasks.reopen({
1717
+ taskId: id,
1718
+ ...(options.idempotencyKey
1719
+ ? { idempotencyKey: options.idempotencyKey }
1720
+ : {}),
1721
+ }),
1722
+ );
1723
+ output(io, global.json, record, `Task ${id} is ${record.status}.`);
1724
+ },
1725
+ );
1359
1726
  }
1360
1727
 
1361
1728
  kudosCommand
@@ -1363,7 +1730,7 @@ export function createCli(
1363
1730
  .description('Show one kudos item and its current state')
1364
1731
  .action(async (id: string, _options, command: Command) => {
1365
1732
  const global = globals(command);
1366
- const record = await withClient(global.home, actor('system', 'cli'), (client) =>
1733
+ const record = await withClient(global.home, defaultActor(env, 'system', 'cli'), (client) =>
1367
1734
  client.kudos.get(id),
1368
1735
  );
1369
1736
  output(io, global.json, record, showRecord(record));
@@ -1437,28 +1804,32 @@ export function createCli(
1437
1804
  ) => {
1438
1805
  const global = globals(command);
1439
1806
  if (!agentId) throw new SynomemError('INVALID_INPUT', 'Specify an agent.');
1440
- const details = await withClient(global.home, actor('system', 'cli'), async (client) => {
1441
- const profile = await client.agents.get(agentId);
1442
- const info = await client.info();
1443
- if (info.backend !== 'local') {
1444
- throw new SynomemError(
1445
- 'INVALID_INPUT',
1446
- 'Generated WINS.md files are available only with the local backend.',
1447
- );
1448
- }
1449
- const capabilities = await client.capabilities();
1450
- const path = join(info.home, profile.id, 'WINS.md');
1451
- if (!existsSync(path)) {
1452
- const hint = capabilities.projections.writeWinsMarkdown
1453
- ? 'Run `synomem rebuild` to generate it.'
1454
- : 'Enable projection.writeWinsMarkdown and run `synomem rebuild`.';
1455
- throw new SynomemError(
1456
- 'INVALID_INPUT',
1457
- `No generated WINS.md exists for ${profile.id}. ${hint}`,
1458
- );
1459
- }
1460
- return { profile, path, content: readFileSync(path, 'utf8') };
1461
- });
1807
+ const details = await withClient(
1808
+ global.home,
1809
+ defaultActor(env, 'system', 'cli'),
1810
+ async (client) => {
1811
+ const profile = await client.agents.get(agentId);
1812
+ const info = await client.info();
1813
+ if (info.backend !== 'local') {
1814
+ throw new SynomemError(
1815
+ 'INVALID_INPUT',
1816
+ 'Generated WINS.md files are available only with the local backend.',
1817
+ );
1818
+ }
1819
+ const capabilities = await client.capabilities();
1820
+ const path = join(info.home, profile.id, 'WINS.md');
1821
+ if (!existsSync(path)) {
1822
+ const hint = capabilities.projections.writeWinsMarkdown
1823
+ ? 'Run `synomem rebuild` to generate it.'
1824
+ : 'Enable projection.writeWinsMarkdown and run `synomem rebuild`.';
1825
+ throw new SynomemError(
1826
+ 'INVALID_INPUT',
1827
+ `No generated WINS.md exists for ${profile.id}. ${hint}`,
1828
+ );
1829
+ }
1830
+ return { profile, path, content: readFileSync(path, 'utf8') };
1831
+ },
1832
+ );
1462
1833
  if (options.open) {
1463
1834
  const commandName =
1464
1835
  process.platform === 'darwin'
@@ -1478,7 +1849,7 @@ export function createCli(
1478
1849
  kudosCommand.command('stats').description('Show aggregate kudos statistics'),
1479
1850
  ).action(async (options: Record<string, string>, command: Command) => {
1480
1851
  const global = globals(command);
1481
- const stats = await withClient(global.home, actor('system', 'cli'), (client) =>
1852
+ const stats = await withClient(global.home, defaultActor(env, 'system', 'cli'), (client) =>
1482
1853
  client.stats(listInput(options)),
1483
1854
  );
1484
1855
  output(
@@ -1494,7 +1865,7 @@ export function createCli(
1494
1865
  .description('Regenerate current-state and filesystem projections from canonical events')
1495
1866
  .action(async (_options, command: Command) => {
1496
1867
  const global = globals(command);
1497
- const result = await withClient(global.home, actor('system', 'cli'), (client) =>
1868
+ const result = await withClient(global.home, defaultActor(env, 'system', 'cli'), (client) =>
1498
1869
  client.rebuild(),
1499
1870
  );
1500
1871
  output(
@@ -1510,7 +1881,7 @@ export function createCli(
1510
1881
  .description('Create a transactionally consistent SQLite backup')
1511
1882
  .action(async (destination: string, _options, command: Command) => {
1512
1883
  const global = globals(command);
1513
- const path = await withClient(global.home, actor('system', 'cli'), (client) => {
1884
+ const path = await withClient(global.home, defaultActor(env, 'system', 'cli'), (client) => {
1514
1885
  if (!client.backup) {
1515
1886
  throw new SynomemError(
1516
1887
  'INVALID_INPUT',
@@ -1535,8 +1906,10 @@ export function createCli(
1535
1906
  command: Command,
1536
1907
  ) => {
1537
1908
  const global = globals(command);
1538
- const content = await withClient(global.home, actor('system', 'cli'), (client) =>
1539
- client.export(options.format),
1909
+ const content = await withClient(
1910
+ global.home,
1911
+ defaultActor(env, 'system', 'cli'),
1912
+ (client) => client.export(options.format),
1540
1913
  );
1541
1914
  if (options.output) {
1542
1915
  const destination = resolve(options.output);
@@ -1558,7 +1931,7 @@ export function createCli(
1558
1931
  .description('Run safe diagnostics')
1559
1932
  .action(async (_options, command: Command) => {
1560
1933
  const global = globals(command);
1561
- const result = await withClient(global.home, actor('system', 'cli'), (client) =>
1934
+ const result = await withClient(global.home, defaultActor(env, 'system', 'cli'), (client) =>
1562
1935
  client.doctor(),
1563
1936
  );
1564
1937
  const human = result.diagnostics
@@ -1571,18 +1944,44 @@ export function createCli(
1571
1944
  program
1572
1945
  .command('mcp')
1573
1946
  .description('Run the actor-bound MCP server over stdio')
1574
- .requiredOption('--actor-id <id>')
1575
- .requiredOption('--actor-kind <kind>', 'human, agent, or system')
1576
- .option('--actor-name <display-name>')
1947
+ .option('--agent-id <id>', 'bound agent, whose identity is read from Synomem')
1948
+ .option('--actor-id <id>', 'bound non-agent actor ID')
1949
+ .option('--actor-kind <kind>', 'human or system')
1950
+ .option('--actor-name <display-name>', 'display name for a non-agent actor')
1577
1951
  .action(
1578
1952
  async (
1579
- options: { actorId: string; actorKind: string; actorName?: string },
1953
+ options: { agentId?: string; actorId?: string; actorKind?: string; actorName?: string },
1580
1954
  command: Command,
1581
1955
  ) => {
1582
1956
  const global = globals(command);
1957
+ // An agent's name comes from its profile, never from the command line:
1958
+ // the name is written into every event the session appends, and a
1959
+ // harness must not be able to sign another agent's name to work.
1960
+ const bound = options.agentId
1961
+ ? await withClient(global.home, defaultActor(env, 'system', 'cli'), async (client) => {
1962
+ const resolution = await client.agents.resolve(options.agentId!);
1963
+ if (!resolution.match) {
1964
+ throw new SynomemError(
1965
+ 'AGENT_NOT_FOUND',
1966
+ resolution.candidates.length
1967
+ ? `"${options.agentId}" matches ${resolution.candidates.length} agents: ${resolution.candidates
1968
+ .map((candidate) => candidate.id)
1969
+ .join(', ')}. Name one of them.`
1970
+ : `Unknown agent: ${options.agentId}`,
1971
+ );
1972
+ }
1973
+ return actor('agent', resolution.match.id, resolution.match.displayName);
1974
+ })
1975
+ : undefined;
1976
+ if (!bound && !(options.actorId && options.actorKind)) {
1977
+ throw new SynomemError(
1978
+ 'INVALID_INPUT',
1979
+ 'Specify --agent-id, or --actor-id with --actor-kind for a non-agent actor.',
1980
+ );
1981
+ }
1583
1982
  await startMcpServer({
1584
1983
  ...(global.home ? { home: global.home } : {}),
1585
- actor: actor(options.actorKind, options.actorId, options.actorName),
1984
+ actor: bound ?? actor(options.actorKind!, options.actorId!, options.actorName),
1586
1985
  });
1587
1986
  },
1588
1987
  );