synomem 0.9.6 → 0.10.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 (118) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +5 -3
  3. package/dist/actor-directory.d.ts +34 -0
  4. package/dist/actor-directory.d.ts.map +1 -0
  5. package/dist/actor-directory.js +52 -0
  6. package/dist/actor-directory.js.map +1 -0
  7. package/dist/backend.d.ts +3 -0
  8. package/dist/backend.d.ts.map +1 -1
  9. package/dist/backend.js +26 -3
  10. package/dist/backend.js.map +1 -1
  11. package/dist/bookmarks.d.ts +30 -0
  12. package/dist/bookmarks.d.ts.map +1 -0
  13. package/dist/bookmarks.js +87 -0
  14. package/dist/bookmarks.js.map +1 -0
  15. package/dist/cli.d.ts.map +1 -1
  16. package/dist/cli.js +291 -61
  17. package/dist/cli.js.map +1 -1
  18. package/dist/client.d.ts +115 -4
  19. package/dist/client.d.ts.map +1 -1
  20. package/dist/client.js +854 -138
  21. package/dist/client.js.map +1 -1
  22. package/dist/cursors.d.ts +30 -0
  23. package/dist/cursors.d.ts.map +1 -0
  24. package/dist/cursors.js +101 -0
  25. package/dist/cursors.js.map +1 -0
  26. package/dist/errors.d.ts +1 -1
  27. package/dist/errors.d.ts.map +1 -1
  28. package/dist/errors.js +2 -0
  29. package/dist/errors.js.map +1 -1
  30. package/dist/import.d.ts +605 -39
  31. package/dist/import.d.ts.map +1 -1
  32. package/dist/index.d.ts +19 -1
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +11 -1
  35. package/dist/index.js.map +1 -1
  36. package/dist/mcp/index.d.ts +1 -1
  37. package/dist/mcp/index.d.ts.map +1 -1
  38. package/dist/mcp/index.js +329 -22
  39. package/dist/mcp/index.js.map +1 -1
  40. package/dist/mutation-receipts.d.ts +8 -0
  41. package/dist/mutation-receipts.d.ts.map +1 -0
  42. package/dist/mutation-receipts.js +2 -0
  43. package/dist/mutation-receipts.js.map +1 -0
  44. package/dist/notifications.d.ts +73 -0
  45. package/dist/notifications.d.ts.map +1 -0
  46. package/dist/notifications.js +309 -0
  47. package/dist/notifications.js.map +1 -0
  48. package/dist/participation-repository.d.ts +46 -0
  49. package/dist/participation-repository.d.ts.map +1 -0
  50. package/dist/participation-repository.js +296 -0
  51. package/dist/participation-repository.js.map +1 -0
  52. package/dist/participation.d.ts +97 -0
  53. package/dist/participation.d.ts.map +1 -0
  54. package/dist/participation.js +10 -0
  55. package/dist/participation.js.map +1 -0
  56. package/dist/policy.d.ts +31 -0
  57. package/dist/policy.d.ts.map +1 -0
  58. package/dist/policy.js +71 -0
  59. package/dist/policy.js.map +1 -0
  60. package/dist/ports/repository.d.ts +23 -0
  61. package/dist/ports/repository.d.ts.map +1 -1
  62. package/dist/profiles.d.ts +12 -0
  63. package/dist/profiles.d.ts.map +1 -1
  64. package/dist/profiles.js +2 -1
  65. package/dist/profiles.js.map +1 -1
  66. package/dist/projections.d.ts.map +1 -1
  67. package/dist/projections.js +29 -11
  68. package/dist/projections.js.map +1 -1
  69. package/dist/remote.d.ts +79 -0
  70. package/dist/remote.d.ts.map +1 -1
  71. package/dist/remote.js +65 -6
  72. package/dist/remote.js.map +1 -1
  73. package/dist/schemas.d.ts +746 -58
  74. package/dist/schemas.d.ts.map +1 -1
  75. package/dist/schemas.js +114 -16
  76. package/dist/schemas.js.map +1 -1
  77. package/dist/search.d.ts +25 -0
  78. package/dist/search.d.ts.map +1 -0
  79. package/dist/search.js +2 -0
  80. package/dist/search.js.map +1 -0
  81. package/dist/service.d.ts +89 -0
  82. package/dist/service.d.ts.map +1 -1
  83. package/dist/storage.d.ts +38 -3
  84. package/dist/storage.d.ts.map +1 -1
  85. package/dist/storage.js +494 -365
  86. package/dist/storage.js.map +1 -1
  87. package/dist/types.d.ts +65 -34
  88. package/dist/types.d.ts.map +1 -1
  89. package/docs/cli.md +6 -0
  90. package/docs/human-participation.md +20 -0
  91. package/docs/mcp.md +2 -0
  92. package/docs/storage-format.md +4 -15
  93. package/openapi/synomem-v1.yaml +187 -10
  94. package/package.json +2 -1
  95. package/skills/synomem/SKILL.md +2 -0
  96. package/src/actor-directory.ts +87 -0
  97. package/src/backend.ts +29 -3
  98. package/src/bookmarks.ts +127 -0
  99. package/src/cli.ts +941 -546
  100. package/src/client.ts +1143 -279
  101. package/src/cursors.ts +132 -0
  102. package/src/errors.ts +2 -0
  103. package/src/index.ts +50 -0
  104. package/src/mcp/index.ts +412 -96
  105. package/src/mutation-receipts.ts +7 -0
  106. package/src/notifications.ts +483 -0
  107. package/src/participation-repository.ts +406 -0
  108. package/src/participation.ts +103 -0
  109. package/src/policy.ts +109 -0
  110. package/src/ports/repository.ts +33 -0
  111. package/src/profiles.ts +2 -1
  112. package/src/projections.ts +49 -48
  113. package/src/remote.ts +227 -37
  114. package/src/schemas.ts +116 -16
  115. package/src/search.ts +24 -0
  116. package/src/service.ts +122 -8
  117. package/src/storage.ts +1039 -697
  118. package/src/types.ts +67 -32
package/dist/cli.js CHANGED
@@ -4,7 +4,7 @@ import { spawn } from 'node:child_process';
4
4
  import { join, resolve } from 'node:path';
5
5
  import { pathToFileURL } from 'node:url';
6
6
  import { Command, CommanderError, Option } from 'commander';
7
- import { ensureLocalStore, LOCAL_OPERATOR, openLocalService } from './backend.js';
7
+ import { ensureLocalStore, openLocalOwnerService, exportRawLocalEvents } from './backend.js';
8
8
  import { cloudApiUrl } from './cloud.js';
9
9
  import { resolveHome } from './config.js';
10
10
  import { assertInteractive, credentialFingerprint, parseCredentialBackend, readAccessKey, } from './configure.js';
@@ -126,7 +126,7 @@ function lineForSummary(record) {
126
126
  : record.status === 'acknowledged'
127
127
  ? 'acknowledged'
128
128
  : 'new';
129
- return `${record.id} ${record.createdAt.slice(0, 10)} ${record.recipientAgentId} [${state}] ${record.title}`;
129
+ return `${record.id} ${record.createdAt.slice(0, 10)} ${record.recipient?.id} [${state}] ${record.title}`;
130
130
  }
131
131
  function lineForItem(item) {
132
132
  return `${item.id} ${item.createdAt.slice(0, 10)} ${item.kind.padEnd(5)} [${item.status}] ${item.title}`;
@@ -136,7 +136,9 @@ function itemListInput(options) {
136
136
  ...(Array.isArray(options.kind) && options.kind.length
137
137
  ? { kinds: options.kind }
138
138
  : {}),
139
- ...(typeof options.participant === 'string' ? { participantAgentId: options.participant } : {}),
139
+ ...(typeof options.participant === 'string'
140
+ ? { participant: targetRef(options.participant) }
141
+ : {}),
140
142
  ...(typeof options.author === 'string' ? { actorId: options.author } : {}),
141
143
  ...(typeof options.status === 'string' ? { status: options.status } : {}),
142
144
  ...(typeof options.tag === 'string' ? { tag: options.tag } : {}),
@@ -149,13 +151,21 @@ function itemListInput(options) {
149
151
  offset: Number(options.offset ?? 0),
150
152
  };
151
153
  }
154
+ function targetRef(value) {
155
+ const match = /^(human|agent):(.+)$/.exec(value);
156
+ if (match)
157
+ return { kind: match[1], id: match[2] };
158
+ if (value.includes(':'))
159
+ throw new SynomemError('INVALID_INPUT', 'Use human:<handle> or agent:<handle>.');
160
+ return { kind: 'agent', id: value };
161
+ }
152
162
  function showRecord(record) {
153
163
  const event = record.event;
154
164
  const evidence = event.evidence?.map((item) => ` - ${item.kind}: ${item.value}`).join('\n');
155
165
  return [
156
166
  event.title,
157
167
  `ID: ${event.id}`,
158
- `Recipient: ${event.recipientDisplayName} (${event.recipientAgentId})`,
168
+ `Recipient: ${event.recipientDisplayName} (${event.recipient?.id})`,
159
169
  `From: ${event.actor.displayName ?? event.actor.id} (${event.actor.kind}:${event.actor.id})`,
160
170
  `Date: ${event.createdAt}`,
161
171
  `Visibility: ${event.visibility}`,
@@ -192,7 +202,7 @@ function addListOptions(command) {
192
202
  }
193
203
  function listInput(options) {
194
204
  return {
195
- ...(options.recipient ? { recipientAgentId: options.recipient } : {}),
205
+ ...(options.recipient ? { recipient: targetRef(options.recipient) } : {}),
196
206
  ...(options.author ? { actorId: options.author } : {}),
197
207
  ...(options.authorKind ? { actorKind: options.authorKind } : {}),
198
208
  ...(options.tag ? { tag: options.tag } : {}),
@@ -290,7 +300,7 @@ export function createCli(io = defaultIo, dependencies = {}) {
290
300
  return await withProfile(command, (service, context) => operation(service, context, undefined));
291
301
  }
292
302
  const storeHome = profile && isLocalProfile(profile) ? (profile.home ?? global.home) : global.home;
293
- const client = await openLocalService(storeHome, LOCAL_OPERATOR);
303
+ const client = await openLocalOwnerService(storeHome);
294
304
  try {
295
305
  return await operation(client, undefined, storeHome);
296
306
  }
@@ -310,6 +320,46 @@ export function createCli(io = defaultIo, dependencies = {}) {
310
320
  .option('--json', 'emit stable machine-readable JSON', false)
311
321
  .showSuggestionAfterError()
312
322
  .configureOutput({ writeOut: io.stdout, writeErr: io.stderr });
323
+ const humanCommand = program.command('human').description('Local human identities');
324
+ humanCommand
325
+ .command('register <handle>')
326
+ .requiredOption('--name <display-name>')
327
+ .action(async (handle, options, command) => {
328
+ await withManagement(command, async (service) => {
329
+ const human = await service.actors.registerHuman({
330
+ id: handle,
331
+ handle,
332
+ displayName: options.name,
333
+ });
334
+ output(io, globals(command).json, human, `Registered ${human.displayName} (human:${human.handle}).`);
335
+ });
336
+ });
337
+ program
338
+ .command('actors')
339
+ .description('Search addressable humans and agents')
340
+ .option('--query <text>', 'Search name, handle or ID')
341
+ .option('--kind <kind>', 'human or agent')
342
+ .option('--cursor <cursor>', 'Signed continuation cursor')
343
+ .option('--limit <number>', 'Maximum actors', Number, 20)
344
+ .action(async (options, command) => {
345
+ await withProfile(command, async (service) => {
346
+ const page = await service.actors.list(options);
347
+ output(io, globals(command).json, page, page.items
348
+ .map((actor) => `${actor.kind}:${actor.handle} — ${actor.displayName}`)
349
+ .join('\n') + (page.nextCursor ? `\nNext cursor: ${page.nextCursor}` : ''));
350
+ });
351
+ });
352
+ program
353
+ .command('actor-profile <target>')
354
+ .description('Visible authored records and separate kudos/useful counts')
355
+ .option('--after <cursor>', 'Continue authored record page')
356
+ .option('--limit <number>', 'Maximum records', Number, 20)
357
+ .action(async (target, options, command) => {
358
+ await withProfile(command, async (service) => {
359
+ const profile = await service.actors.profile({ target: targetRef(target), ...options });
360
+ output(io, globals(command).json, profile, `${profile.actor.displayName}: ${profile.counts.kudosReceived} kudos, ${profile.counts.usefulReceived} useful reactions`);
361
+ });
362
+ });
313
363
  /* ------------------------------------------------------------ setup */
314
364
  program
315
365
  .command('setup')
@@ -338,7 +388,7 @@ export function createCli(io = defaultIo, dependencies = {}) {
338
388
  ensureLocalStore(global.home);
339
389
  const config = store.read();
340
390
  const existing = config.profiles[profileName];
341
- const client = await openLocalService(global.home, LOCAL_OPERATOR);
391
+ const client = await openLocalOwnerService(global.home);
342
392
  let agent;
343
393
  let resumed = false;
344
394
  try {
@@ -474,7 +524,7 @@ export function createCli(io = defaultIo, dependencies = {}) {
474
524
  throw new SynomemError('INVALID_INPUT', 'A browser sign-in is stored in the keychain or a file, never the environment.');
475
525
  }
476
526
  const callbackPort = Number(options.callbackPort);
477
- if (!Number.isSafeInteger(callbackPort) || callbackPort < 1 || callbackPort > 65_535) {
527
+ if (!Number.isSafeInteger(callbackPort) || callbackPort < 1 || callbackPort > 65535) {
478
528
  throw new SynomemError('INVALID_INPUT', '--callback-port must be from 1 through 65535.');
479
529
  }
480
530
  const credential = await oauthLogin({
@@ -666,7 +716,8 @@ export function createCli(io = defaultIo, dependencies = {}) {
666
716
  .option('--context <context-id>', 'exact context id (see `connection status`)')
667
717
  .option('--agent <handle-or-id>', 'narrow by agent handle, id or display name')
668
718
  .option('--workspace <name-or-id>', 'narrow by workspace name or id')
669
- .option('--local', 'a local-store profile for an existing local agent', false)
719
+ .option('--human <handle-or-id>', 'local registered human identity')
720
+ .option('--local', 'a local-store profile for an existing human or agent', false)
670
721
  .option('--store-home <path>', 'local store home (default: the Synomem home)')
671
722
  .option('--default', 'make this the default profile', false)
672
723
  .action(async (rawName, options, command) => {
@@ -678,30 +729,36 @@ export function createCli(io = defaultIo, dependencies = {}) {
678
729
  throw new SynomemError('CONFIG_INVALID', `Profile ${name} already exists. Remove it first to change what it points at.`);
679
730
  }
680
731
  if (options.local) {
681
- if (!options.agent)
682
- throw new SynomemError('INVALID_INPUT', 'A local profile needs --agent <handle-or-id>.');
732
+ if ((!options.agent && !options.human) || (options.agent && options.human))
733
+ throw new SynomemError('INVALID_INPUT', 'A local profile needs exactly one of --human or --agent.');
683
734
  const storeHome = options.storeHome ? resolve(options.storeHome) : global.home;
684
- const client = await openLocalService(storeHome, LOCAL_OPERATOR);
735
+ const client = await openLocalOwnerService(storeHome);
685
736
  let agent;
686
737
  try {
687
- const resolution = await client.agents.resolve(options.agent);
688
- if (!resolution.match) {
689
- throw new SynomemError(resolution.candidates.length ? 'CONTEXT_AMBIGUOUS' : 'AGENT_NOT_FOUND', resolution.candidates.length
690
- ? `"${options.agent}" matches several agents: ${resolution.candidates.map((c) => `${c.handle} (${c.id})`).join(', ')}.`
691
- : `No local agent answers to "${options.agent}". Profiles never create agents: run \`synomem agent create\` first.`);
738
+ if (options.human) {
739
+ agent = await client.actors.get({ kind: 'human', id: options.human });
740
+ }
741
+ else {
742
+ const resolution = await client.agents.resolve(options.agent);
743
+ if (!resolution.match) {
744
+ throw new SynomemError(resolution.candidates.length ? 'CONTEXT_AMBIGUOUS' : 'AGENT_NOT_FOUND', resolution.candidates.length
745
+ ? `"${options.agent}" matches several agents: ${resolution.candidates.map((c) => `${c.handle} (${c.id})`).join(', ')}.`
746
+ : `No local agent answers to "${options.agent}". Profiles never create agents: run \`synomem agent create\` first.`);
747
+ }
748
+ agent = resolution.match;
692
749
  }
693
- agent = resolution.match;
694
750
  }
695
751
  finally {
696
752
  await client.close();
697
753
  }
698
754
  const profile = {
699
755
  backend: 'local',
756
+ actorKind: options.human ? 'human' : 'agent',
700
757
  ...(storeHome !== global.home ? { home: storeHome } : {}),
701
758
  actorId: agent.id,
702
759
  actorName: agent.displayName,
703
760
  contextId: localContextId(storeHome, {
704
- kind: 'agent',
761
+ kind: options.human ? 'human' : 'agent',
705
762
  id: agent.id,
706
763
  }),
707
764
  };
@@ -1090,7 +1147,11 @@ export function createCli(io = defaultIo, dependencies = {}) {
1090
1147
  ]
1091
1148
  : []),
1092
1149
  ...(skillTargets.length
1093
- ? ['', 'And these Synomem-owned skills:', ...skillTargets.map((path) => ` ${path}`)]
1150
+ ? [
1151
+ '',
1152
+ 'And these Synomem-owned skills:',
1153
+ ...skillTargets.map((path) => ` ${path}`),
1154
+ ]
1094
1155
  : []),
1095
1156
  '',
1096
1157
  'Server-side authorizations are not revoked; do that in the portal.',
@@ -1488,7 +1549,6 @@ export function createCli(io = defaultIo, dependencies = {}) {
1488
1549
  .requiredOption('--body <body>')
1489
1550
  .option('--tag <tag>', 'repeatable', collect, [])
1490
1551
  .option('--topic <id>', 'topic ID (repeatable)', collect, [])
1491
- .option('--reply-to <post-id>')
1492
1552
  .action(async (options, command) => {
1493
1553
  const global = globals(command);
1494
1554
  const result = await withProfile(command, (service) => service.posts.create({
@@ -1496,7 +1556,6 @@ export function createCli(io = defaultIo, dependencies = {}) {
1496
1556
  body: options.body,
1497
1557
  ...(options.tag.length ? { tags: options.tag } : {}),
1498
1558
  ...(options.topic.length ? { topicIds: options.topic } : {}),
1499
- ...(options.replyTo ? { replyTo: options.replyTo } : {}),
1500
1559
  }));
1501
1560
  output(io, global.json, result, `Published ${result.record.event.id}`);
1502
1561
  });
@@ -1531,7 +1590,11 @@ export function createCli(io = defaultIo, dependencies = {}) {
1531
1590
  .option('--note <text>', 'optional context for the author')
1532
1591
  .action(async (postId, options, command) => {
1533
1592
  const global = globals(command);
1534
- const record = await withProfile(command, (service) => service.posts.acknowledge({ postId, ...(options.note ? { note: options.note } : {}) }));
1593
+ const record = await withProfile(command, async (service) => service.posts.acknowledge({
1594
+ expectedVersion: (await service.posts.get(postId)).lifecycleVersion,
1595
+ postId,
1596
+ ...(options.note ? { note: options.note } : {}),
1597
+ }));
1535
1598
  output(io, global.json, record, `Acknowledged ${postId}`);
1536
1599
  });
1537
1600
  postCommand
@@ -1561,7 +1624,11 @@ export function createCli(io = defaultIo, dependencies = {}) {
1561
1624
  .option('--reason <text>')
1562
1625
  .action(async (postId, options, command) => {
1563
1626
  const global = globals(command);
1564
- const record = await withProfile(command, (service) => service.posts.archive({ postId, ...(options.reason ? { reason: options.reason } : {}) }));
1627
+ const record = await withProfile(command, async (service) => service.posts.archive({
1628
+ expectedVersion: (await service.posts.get(postId)).lifecycleVersion,
1629
+ postId,
1630
+ ...(options.reason ? { reason: options.reason } : {}),
1631
+ }));
1565
1632
  output(io, global.json, record, `Archived ${postId}`);
1566
1633
  });
1567
1634
  /* ------------------------------------------------------------ kudos */
@@ -1579,7 +1646,7 @@ export function createCli(io = defaultIo, dependencies = {}) {
1579
1646
  .action(async (recipient, options, command) => {
1580
1647
  const global = globals(command);
1581
1648
  const result = await withProfile(command, (service) => service.kudos.give({
1582
- recipientAgentId: recipient,
1649
+ recipient: targetRef(recipient),
1583
1650
  title: options.title,
1584
1651
  reason: options.reason,
1585
1652
  visibility: options.visibility,
@@ -1612,7 +1679,11 @@ export function createCli(io = defaultIo, dependencies = {}) {
1612
1679
  .option('--note <text>')
1613
1680
  .action(async (id, options, command) => {
1614
1681
  const global = globals(command);
1615
- const record = await withProfile(command, (service) => service.kudos.acknowledge({ kudosId: id, ...(options.note ? { note: options.note } : {}) }));
1682
+ const record = await withProfile(command, async (service) => service.kudos.acknowledge({
1683
+ expectedVersion: (await service.kudos.get(id)).lifecycleVersion,
1684
+ kudosId: id,
1685
+ ...(options.note ? { note: options.note } : {}),
1686
+ }));
1616
1687
  output(io, global.json, record, `Acknowledged ${id}.`);
1617
1688
  });
1618
1689
  kudosCommand
@@ -1622,7 +1693,8 @@ export function createCli(io = defaultIo, dependencies = {}) {
1622
1693
  .option('--administrative', 'mark as an administrative revocation', false)
1623
1694
  .action(async (id, options, command) => {
1624
1695
  const global = globals(command);
1625
- const record = await withProfile(command, (service) => service.kudos.revoke({
1696
+ const record = await withProfile(command, async (service) => service.kudos.revoke({
1697
+ expectedVersion: (await service.kudos.get(id)).lifecycleVersion,
1626
1698
  kudosId: id,
1627
1699
  reason: options.reason,
1628
1700
  administrative: options.administrative,
@@ -1675,28 +1747,30 @@ export function createCli(io = defaultIo, dependencies = {}) {
1675
1747
  });
1676
1748
  /* ------------------------------------------------------------ cross-kind */
1677
1749
  program
1678
- .command('inbox [agent]')
1679
- .description("Show pending kudos, memos, and tasks (the profile's own agent by default)")
1680
- .option('--limit <number>', 'maximum results (default 10, maximum 50)', '10')
1681
- .option('--cursor <cursor>', 'opaque cursor returned by the previous page')
1682
- .action(async (agentId, options, command) => {
1683
- const global = globals(command);
1684
- const page = await withProfile(command, (service, context) => {
1685
- const recipient = agentId ?? (context.actor.kind === 'agent' ? context.actor.id : undefined);
1686
- if (!recipient) {
1687
- throw new SynomemError('INVALID_INPUT', 'This profile is not an agent; name the agent whose inbox to show.');
1688
- }
1689
- return service.items.list({
1690
- participantAgentId: recipient,
1691
- pending: true,
1692
- limit: Number(options.limit),
1693
- ...(options.cursor ? { cursor: options.cursor } : {}),
1694
- });
1695
- });
1696
- output(io, global.json, page, page.items.length
1697
- ? `${page.items.map(lineForItem).join('\n')}${page.hasMore ? `\nNext cursor: ${page.nextCursor}` : ''}`
1750
+ .command('inbox')
1751
+ .description('Read notifications belonging to the profile effective actor')
1752
+ .option('--limit <number>', 'Maximum results', '20')
1753
+ .option('--after <cursor>', 'Signed continuation cursor')
1754
+ .option('--view <view>', 'all, unread, action_required or default', 'default')
1755
+ .action(async (options, command) => {
1756
+ const page = await withProfile(command, (service) => service.notifications.list({
1757
+ limit: Number(options.limit),
1758
+ view: options.view,
1759
+ ...(options.after ? { after: options.after } : {}),
1760
+ }));
1761
+ output(io, globals(command).json, page, page.items.length
1762
+ ? page.items
1763
+ .map((item) => `${item.read ? 'Read' : 'Unread'} ${item.root?.kind ?? 'record'}: ${item.root?.title ?? item.rootId} [${item.id}]`)
1764
+ .join('\n')
1698
1765
  : 'Inbox is clear.');
1699
1766
  });
1767
+ program
1768
+ .command('notification-read <notification-id>')
1769
+ .description('Mark your notification read without performing a lifecycle action')
1770
+ .action(async (id, _options, command) => {
1771
+ await withProfile(command, (service) => service.notifications.read(id));
1772
+ output(io, globals(command).json, { updated: true }, 'Notification marked read.');
1773
+ });
1700
1774
  program
1701
1775
  .command('list')
1702
1776
  .description('List compact summaries across all record types')
@@ -1751,7 +1825,7 @@ export function createCli(io = defaultIo, dependencies = {}) {
1751
1825
  .action(async (recipient, options, command) => {
1752
1826
  const global = globals(command);
1753
1827
  const result = await withProfile(command, (service) => service.memos.send({
1754
- recipientAgentId: recipient,
1828
+ recipient: targetRef(recipient),
1755
1829
  subject: options.subject,
1756
1830
  body: options.body,
1757
1831
  visibility: options.visibility,
@@ -1771,7 +1845,7 @@ export function createCli(io = defaultIo, dependencies = {}) {
1771
1845
  .action(async (options, command) => {
1772
1846
  const global = globals(command);
1773
1847
  const page = await withProfile(command, (service) => service.memos.list({
1774
- ...(options.participant ? { participantAgentId: options.participant } : {}),
1848
+ ...(options.participant ? { participant: targetRef(options.participant) } : {}),
1775
1849
  ...(options.status ? { status: options.status } : {}),
1776
1850
  limit: Number(options.limit),
1777
1851
  ...(options.cursor ? { cursor: options.cursor } : {}),
@@ -1795,7 +1869,8 @@ export function createCli(io = defaultIo, dependencies = {}) {
1795
1869
  .option('--idempotency-key <key>')
1796
1870
  .action(async (id, options, command) => {
1797
1871
  const global = globals(command);
1798
- const record = await withProfile(command, (service) => service.memos[operation]({
1872
+ const record = await withProfile(command, async (service) => service.memos[operation]({
1873
+ expectedVersion: (await service.memos.get(id)).lifecycleVersion,
1799
1874
  memoId: id,
1800
1875
  ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
1801
1876
  }));
@@ -1818,7 +1893,7 @@ export function createCli(io = defaultIo, dependencies = {}) {
1818
1893
  .action(async (options, command) => {
1819
1894
  const global = globals(command);
1820
1895
  const result = await withProfile(command, (service) => service.notes.create({
1821
- ...(options.owner ? { ownerAgentId: options.owner } : {}),
1896
+ ...(options.owner ? { owner: targetRef(options.owner) } : {}),
1822
1897
  title: options.title,
1823
1898
  body: options.body,
1824
1899
  ...(options.tag.length ? { tags: options.tag } : {}),
@@ -1836,7 +1911,7 @@ export function createCli(io = defaultIo, dependencies = {}) {
1836
1911
  .action(async (options, command) => {
1837
1912
  const global = globals(command);
1838
1913
  const page = await withProfile(command, (service) => service.notes.list({
1839
- ...(options.owner ? { participantAgentId: options.owner } : {}),
1914
+ ...(options.owner ? { participant: targetRef(options.owner) } : {}),
1840
1915
  ...(options.status ? { status: options.status } : {}),
1841
1916
  limit: Number(options.limit),
1842
1917
  }));
@@ -1876,7 +1951,8 @@ export function createCli(io = defaultIo, dependencies = {}) {
1876
1951
  .option('--idempotency-key <key>')
1877
1952
  .action(async (id, options, command) => {
1878
1953
  const global = globals(command);
1879
- const record = await withProfile(command, (service) => service.notes.archive({
1954
+ const record = await withProfile(command, async (service) => service.notes.archive({
1955
+ expectedVersion: (await service.notes.get(id)).lifecycleVersion,
1880
1956
  noteId: id,
1881
1957
  ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
1882
1958
  }));
@@ -1946,21 +2022,31 @@ export function createCli(io = defaultIo, dependencies = {}) {
1946
2022
  .action(async (id, options, command) => {
1947
2023
  const global = globals(command);
1948
2024
  const key = options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {};
1949
- const record = await withProfile(command, (service) => operation === 'complete'
2025
+ const record = await withProfile(command, async (service) => operation === 'complete'
1950
2026
  ? service.todos.complete({
2027
+ expectedVersion: (await service.todos.get(id)).lifecycleVersion,
1951
2028
  todoId: id,
1952
2029
  ...(options.note ? { note: options.note } : {}),
1953
2030
  ...key,
1954
2031
  })
1955
2032
  : operation === 'cancel'
1956
2033
  ? service.todos.cancel({
2034
+ expectedVersion: (await service.todos.get(id)).lifecycleVersion,
1957
2035
  todoId: id,
1958
2036
  ...(options.reason ? { reason: options.reason } : {}),
1959
2037
  ...key,
1960
2038
  })
1961
2039
  : operation === 'archive'
1962
- ? service.todos.archive({ todoId: id, ...key })
1963
- : service.todos.reopen({ todoId: id, ...key }));
2040
+ ? service.todos.archive({
2041
+ expectedVersion: (await service.todos.get(id)).lifecycleVersion,
2042
+ todoId: id,
2043
+ ...key,
2044
+ })
2045
+ : service.todos.reopen({
2046
+ expectedVersion: (await service.todos.get(id)).lifecycleVersion,
2047
+ todoId: id,
2048
+ ...key,
2049
+ }));
1964
2050
  output(io, global.json, record, `Todo ${id} is now ${record.status}.`);
1965
2051
  });
1966
2052
  }
@@ -1982,7 +2068,7 @@ export function createCli(io = defaultIo, dependencies = {}) {
1982
2068
  .action(async (assignee, options, command) => {
1983
2069
  const global = globals(command);
1984
2070
  const result = await withProfile(command, (service) => service.tasks.create({
1985
- assigneeAgentId: assignee,
2071
+ assignee: targetRef(assignee),
1986
2072
  title: options.title,
1987
2073
  ...(options.description ? { description: options.description } : {}),
1988
2074
  priority: Number(options.priority),
@@ -2003,7 +2089,7 @@ export function createCli(io = defaultIo, dependencies = {}) {
2003
2089
  .action(async (options, command) => {
2004
2090
  const global = globals(command);
2005
2091
  const page = await withProfile(command, (service) => service.tasks.list({
2006
- ...(options.assignee ? { participantAgentId: options.assignee } : {}),
2092
+ ...(options.assignee ? { participant: targetRef(options.assignee) } : {}),
2007
2093
  ...(options.status ? { status: options.status } : {}),
2008
2094
  limit: Number(options.limit),
2009
2095
  }));
@@ -2045,6 +2131,22 @@ export function createCli(io = defaultIo, dependencies = {}) {
2045
2131
  }));
2046
2132
  output(io, global.json, record, `Updated task ${id} to version ${record.current.version}.`);
2047
2133
  });
2134
+ taskCommand
2135
+ .command('override <task-id>')
2136
+ .requiredOption('--decision <status>', 'open or rejected')
2137
+ .requiredOption('--reason <text>')
2138
+ .requiredOption('--expected-version <number>')
2139
+ .option('--idempotency-key <key>')
2140
+ .action(async (id, options, command) => {
2141
+ const record = await withProfile(command, (service) => service.tasks.overrideDecision({
2142
+ taskId: id,
2143
+ nextStatus: options.decision,
2144
+ reason: options.reason,
2145
+ expectedVersion: Number(options.expectedVersion),
2146
+ ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
2147
+ }));
2148
+ output(io, globals(command).json, record, `Task decision overridden: ${record.status}.`);
2149
+ });
2048
2150
  for (const operation of ['accept', 'reject', 'complete', 'reopen', 'cancel']) {
2049
2151
  const command_ = taskCommand
2050
2152
  .command(`${operation} <task-id>`)
@@ -2062,34 +2164,157 @@ export function createCli(io = defaultIo, dependencies = {}) {
2062
2164
  command_.action(async (id, options, command) => {
2063
2165
  const global = globals(command);
2064
2166
  const key = options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {};
2065
- const record = await withProfile(command, (service) => operation === 'accept'
2167
+ const record = await withProfile(command, async (service) => operation === 'accept'
2066
2168
  ? service.tasks.accept({
2169
+ expectedVersion: (await service.tasks.get(id)).lifecycleVersion,
2067
2170
  taskId: id,
2068
2171
  ...(options.response ? { response: options.response } : {}),
2069
2172
  ...key,
2070
2173
  })
2071
2174
  : operation === 'reject'
2072
2175
  ? service.tasks.reject({
2176
+ expectedVersion: (await service.tasks.get(id)).lifecycleVersion,
2073
2177
  taskId: id,
2074
2178
  response: options.response ?? options.reason ?? '',
2075
2179
  ...key,
2076
2180
  })
2077
2181
  : operation === 'complete'
2078
2182
  ? service.tasks.complete({
2183
+ expectedVersion: (await service.tasks.get(id)).lifecycleVersion,
2079
2184
  taskId: id,
2080
2185
  ...(options.note ? { note: options.note } : {}),
2081
2186
  ...key,
2082
2187
  })
2083
2188
  : operation === 'cancel'
2084
2189
  ? service.tasks.cancel({
2190
+ expectedVersion: (await service.tasks.get(id)).lifecycleVersion,
2085
2191
  taskId: id,
2086
2192
  ...(options.reason ? { reason: options.reason } : {}),
2087
2193
  ...key,
2088
2194
  })
2089
- : service.tasks.reopen({ taskId: id, ...key }));
2195
+ : service.tasks.reopen({
2196
+ expectedVersion: (await service.tasks.get(id)).lifecycleVersion,
2197
+ taskId: id,
2198
+ ...key,
2199
+ }));
2090
2200
  output(io, global.json, record, `Task ${id} is ${record.status}.`);
2091
2201
  });
2092
2202
  }
2203
+ program
2204
+ .command('search <query>')
2205
+ .option('--kind <kind>')
2206
+ .option('--after <cursor>')
2207
+ .option('--limit <number>')
2208
+ .action(async (q, options, command) => {
2209
+ const page = await withProfile(command, (service) => service.search({
2210
+ q,
2211
+ ...(options.kind ? { kinds: [options.kind] } : {}),
2212
+ ...(options.after ? { after: options.after } : {}),
2213
+ ...(options.limit ? { limit: Number(options.limit) } : {}),
2214
+ }));
2215
+ output(io, globals(command).json, page, page.items
2216
+ .map((hit) => `${hit.kind}: ${hit.title} [${hit.rootId}]
2217
+ ${hit.snippet}`)
2218
+ .join('\n') || 'No visible results.');
2219
+ });
2220
+ const bookmarkCommand = program.command('bookmark').description('Your own saved records');
2221
+ bookmarkCommand
2222
+ .command('list')
2223
+ .option('--after <cursor>')
2224
+ .option('--limit <number>')
2225
+ .action(async (options, command) => {
2226
+ const page = await withProfile(command, (service) => service.bookmarks.list({
2227
+ ...options,
2228
+ ...(options.limit ? { limit: Number(options.limit) } : {}),
2229
+ }));
2230
+ output(io, globals(command).json, page, page.items
2231
+ .map((entry) => `${entry.root.kind}: ${entry.root.title} [${entry.root.id}]`)
2232
+ .join('\n') || 'No visible bookmarks.');
2233
+ });
2234
+ for (const operation of ['set', 'remove'])
2235
+ bookmarkCommand
2236
+ .command(`${operation} <root-id>`)
2237
+ .action(async (rootId, _options, command) => {
2238
+ const result = await withProfile(command, (service) => service.bookmarks.set({ rootId, present: operation === 'set' }));
2239
+ output(io, globals(command).json, result, operation === 'set' ? 'Bookmark saved.' : 'Bookmark removed.');
2240
+ });
2241
+ const replyCommand = program
2242
+ .command('reply')
2243
+ .description('Discuss any visible record without changing its lifecycle');
2244
+ replyCommand
2245
+ .command('create <root-id>')
2246
+ .requiredOption('--body <text>', 'Immutable reply body')
2247
+ .option('--parent <reply-id>', 'Reply to a parent in this thread')
2248
+ .option('--mention <actor>', 'Mention a typed human/agent', (value, previous) => [...previous, value], [])
2249
+ .option('--idempotency-key <key>', 'Stable retry key')
2250
+ .action(async (rootId, options, command) => {
2251
+ const result = await withProfile(command, (service) => service.replies.create({
2252
+ rootId,
2253
+ body: options.body,
2254
+ ...(options.parent ? { parentId: options.parent } : {}),
2255
+ mentions: options.mention.map(targetRef),
2256
+ ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
2257
+ }));
2258
+ output(io, globals(command).json, result, `Reply ${result.id} created by ${result.author.kind}:${result.author.id}.`);
2259
+ });
2260
+ replyCommand
2261
+ .command('show <reply-id>')
2262
+ .action(async (id, _options, command) => {
2263
+ const result = await withProfile(command, (service) => service.replies.get(id));
2264
+ output(io, globals(command).json, result, result.deleted ? 'Deleted reply.' : (result.body ?? ''));
2265
+ });
2266
+ replyCommand
2267
+ .command('delete <reply-id>')
2268
+ .requiredOption('--expected-version <number>', 'Current reply version')
2269
+ .option('--reason <text>', 'Moderation reason')
2270
+ .option('--idempotency-key <key>', 'Stable retry key')
2271
+ .action(async (replyId, options, command) => {
2272
+ const result = await withProfile(command, (service) => service.replies.delete({
2273
+ replyId,
2274
+ expectedVersion: Number(options.expectedVersion),
2275
+ ...(options.reason ? { reason: options.reason } : {}),
2276
+ ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
2277
+ }));
2278
+ output(io, globals(command).json, result, 'Visible reply removed; audit history remains.');
2279
+ });
2280
+ const threadCommand = program.command('thread').description('Read and follow record timelines');
2281
+ threadCommand
2282
+ .command('show <root-id>')
2283
+ .option('--after <cursor>', 'Continue a signed snapshot')
2284
+ .option('--limit <number>', 'Page size')
2285
+ .action(async (rootId, options, command) => {
2286
+ const result = await withProfile(command, (service) => service.threads.get({
2287
+ rootId,
2288
+ ...(options.after ? { after: options.after } : {}),
2289
+ ...(options.limit ? { limit: Number(options.limit) } : {}),
2290
+ }));
2291
+ output(io, globals(command).json, result, JSON.stringify(result, null, 2));
2292
+ });
2293
+ threadCommand
2294
+ .command('subscription <root-id>')
2295
+ .option('--follow', 'Follow new replies')
2296
+ .option('--mute', 'Mute notifications')
2297
+ .action(async (rootId, options, command) => {
2298
+ await withProfile(command, (service) => service.threads.subscription({
2299
+ rootId,
2300
+ following: !!options.follow,
2301
+ muted: !!options.mute,
2302
+ }));
2303
+ output(io, globals(command).json, { updated: true }, 'Thread subscription updated.');
2304
+ });
2305
+ program
2306
+ .command('react <target-id> <code>')
2307
+ .option('--remove', 'Set absent instead of present')
2308
+ .option('--idempotency-key <key>', 'Stable retry key')
2309
+ .action(async (targetId, code, options, command) => {
2310
+ const result = await withProfile(command, (service) => service.reactions.set({
2311
+ targetId,
2312
+ code,
2313
+ present: !options.remove,
2314
+ ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
2315
+ }));
2316
+ output(io, globals(command).json, result, 'Reaction updated.');
2317
+ });
2093
2318
  /* ------------------------------------------------------------ maintenance */
2094
2319
  const projectionCommand = program
2095
2320
  .command('projection')
@@ -2146,11 +2371,16 @@ export function createCli(io = defaultIo, dependencies = {}) {
2146
2371
  program
2147
2372
  .command('export')
2148
2373
  .description('Export canonical events for portability')
2374
+ .option('--raw', 'filesystem-owner recovery: export unchanged JSON events without opening or migrating the store', false)
2149
2375
  .addOption(new Option('--format <format>').choices(['json', 'jsonl', 'markdown']).default('json'))
2150
2376
  .option('--output <path>', 'write to an explicit destination instead of stdout')
2151
2377
  .action(async (options, command) => {
2152
2378
  const global = globals(command);
2153
- const content = await withManagement(command, (service) => service.export(options.format));
2379
+ if (options.raw && options.format === 'markdown')
2380
+ throw new SynomemError('INVALID_INPUT', 'Raw recovery supports json or jsonl only.');
2381
+ const content = options.raw
2382
+ ? exportRawLocalEvents(global.home, options.format)
2383
+ : await withManagement(command, (service) => service.export(options.format));
2154
2384
  if (options.output) {
2155
2385
  const destination = resolve(options.output);
2156
2386
  atomicWriteFile(destination, content, 0o600);