synomem 0.2.0 → 0.4.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 (89) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +4 -4
  3. package/dist/backend.d.ts.map +1 -1
  4. package/dist/backend.js +8 -2
  5. package/dist/backend.js.map +1 -1
  6. package/dist/cli.d.ts +3 -0
  7. package/dist/cli.d.ts.map +1 -1
  8. package/dist/cli.js +379 -16
  9. package/dist/cli.js.map +1 -1
  10. package/dist/client.d.ts +68 -2
  11. package/dist/client.d.ts.map +1 -1
  12. package/dist/client.js +283 -12
  13. package/dist/client.js.map +1 -1
  14. package/dist/cloud.d.ts +16 -0
  15. package/dist/cloud.d.ts.map +1 -0
  16. package/dist/cloud.js +19 -0
  17. package/dist/cloud.js.map +1 -0
  18. package/dist/config.d.ts.map +1 -1
  19. package/dist/config.js +8 -1
  20. package/dist/config.js.map +1 -1
  21. package/dist/configure.d.ts +55 -0
  22. package/dist/configure.d.ts.map +1 -0
  23. package/dist/configure.js +177 -0
  24. package/dist/configure.js.map +1 -0
  25. package/dist/credentials.d.ts +15 -2
  26. package/dist/credentials.d.ts.map +1 -1
  27. package/dist/credentials.js.map +1 -1
  28. package/dist/import.d.ts +202 -38
  29. package/dist/import.d.ts.map +1 -1
  30. package/dist/index.d.ts +2 -1
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +1 -0
  33. package/dist/index.js.map +1 -1
  34. package/dist/mcp/index.d.ts.map +1 -1
  35. package/dist/mcp/index.js +72 -6
  36. package/dist/mcp/index.js.map +1 -1
  37. package/dist/oauth.d.ts.map +1 -1
  38. package/dist/oauth.js +8 -1
  39. package/dist/oauth.js.map +1 -1
  40. package/dist/ports/repository.d.ts +4 -1
  41. package/dist/ports/repository.d.ts.map +1 -1
  42. package/dist/projections.d.ts +9 -1
  43. package/dist/projections.d.ts.map +1 -1
  44. package/dist/projections.js +75 -4
  45. package/dist/projections.js.map +1 -1
  46. package/dist/prompt.d.ts +28 -0
  47. package/dist/prompt.d.ts.map +1 -0
  48. package/dist/prompt.js +72 -0
  49. package/dist/prompt.js.map +1 -0
  50. package/dist/remote.d.ts +30 -1
  51. package/dist/remote.d.ts.map +1 -1
  52. package/dist/remote.js +14 -0
  53. package/dist/remote.js.map +1 -1
  54. package/dist/schemas.d.ts +270 -55
  55. package/dist/schemas.d.ts.map +1 -1
  56. package/dist/schemas.js +120 -6
  57. package/dist/schemas.js.map +1 -1
  58. package/dist/service.d.ts +30 -1
  59. package/dist/service.d.ts.map +1 -1
  60. package/dist/storage.d.ts +25 -2
  61. package/dist/storage.d.ts.map +1 -1
  62. package/dist/storage.js +168 -15
  63. package/dist/storage.js.map +1 -1
  64. package/dist/types.d.ts +123 -4
  65. package/dist/types.d.ts.map +1 -1
  66. package/docs/cli.md +1 -1
  67. package/docs/examples.md +1 -1
  68. package/docs/mcp.md +1 -1
  69. package/docs/skill.md +1 -1
  70. package/docs/storage-format.md +1 -1
  71. package/package.json +8 -8
  72. package/src/backend.ts +8 -2
  73. package/src/cli.ts +543 -19
  74. package/src/client.ts +331 -11
  75. package/src/cloud.ts +19 -0
  76. package/src/config.ts +8 -1
  77. package/src/configure.ts +233 -0
  78. package/src/credentials.ts +17 -2
  79. package/src/index.ts +7 -1
  80. package/src/mcp/index.ts +95 -5
  81. package/src/oauth.ts +8 -1
  82. package/src/ports/repository.ts +5 -0
  83. package/src/projections.ts +74 -4
  84. package/src/prompt.ts +88 -0
  85. package/src/remote.ts +72 -0
  86. package/src/schemas.ts +125 -6
  87. package/src/service.ts +30 -0
  88. package/src/storage.ts +217 -14
  89. package/src/types.ts +129 -3
package/src/client.ts CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  escapeMarkdown,
9
9
  memoRecordsFromEvents,
10
10
  noteRecordsFromEvents,
11
+ postRecordsFromEvents,
11
12
  ProjectionManager,
12
13
  recordsFromEvents,
13
14
  taskRecordsFromEvents,
@@ -18,6 +19,8 @@ import {
18
19
  agentLookupSchema,
19
20
  bindRuntimeSchema,
20
21
  createAgentSchema,
22
+ createPostSchema,
23
+ updatePostSchema,
21
24
  createNoteSchema,
22
25
  createTaskSchema,
23
26
  createTodoSchema,
@@ -49,6 +52,10 @@ import type {
49
52
  AgentRuntimeBinding,
50
53
  BindRuntimeInput,
51
54
  CreateAgentInput,
55
+ CreatePostInput,
56
+ PostRecord,
57
+ PostRoster,
58
+ UpdatePostInput,
52
59
  Diagnostic,
53
60
  DoctorResult,
54
61
  GiveKudosInput,
@@ -99,7 +106,11 @@ export interface SynomemCoreOptions {
99
106
  }
100
107
 
101
108
  export class SynomemCore implements SynomemDomainService {
102
- readonly actor: ActorIdentity;
109
+ /**
110
+ * Mutable because an agent actor is resolved to its canonical identity on
111
+ * init: callers name a handle, events record the opaque ID.
112
+ */
113
+ actor: ActorIdentity;
103
114
  private readonly repository: SynomemRepository;
104
115
  private readonly projectionWriter: ProjectionWriter;
105
116
  private readonly clock: () => Date;
@@ -114,6 +125,11 @@ export class SynomemCore implements SynomemDomainService {
114
125
  get: (idOrAlias: string) => this.getAgent(idOrAlias),
115
126
  list: () => this.listAgents(),
116
127
  resolve: (query: string) => this.resolveAgent(query),
128
+ archive: (idOrAlias: string) => this.setAgentStatus(idOrAlias, 'archived'),
129
+ restore: (idOrAlias: string) => this.setAgentStatus(idOrAlias, 'active'),
130
+ addAliases: (idOrAlias: string, aliases: string[]) => this.addAgentAliases(idOrAlias, aliases),
131
+ removeAliases: (idOrAlias: string, aliases: string[]) =>
132
+ this.removeAgentAliases(idOrAlias, aliases),
117
133
  directory: () => this.agentDirectory(),
118
134
  bindings: (idOrAlias: string) => this.listRuntimeBindings(idOrAlias),
119
135
  bindRuntime: (input: BindRuntimeInput) => this.bindRuntime(input),
@@ -139,6 +155,27 @@ export class SynomemCore implements SynomemDomainService {
139
155
  archive: (input: { memoId: string; idempotencyKey?: string }) => this.archiveMemo(input),
140
156
  };
141
157
 
158
+ readonly posts = {
159
+ create: (input: CreatePostInput) => this.createPost(input),
160
+ list: (input: Omit<ItemListInput, 'kinds'> = {}) =>
161
+ this.listItems({ ...input, kinds: ['post'] }),
162
+ get: (id: string) => this.getPost(id),
163
+ update: (input: UpdatePostInput) => this.updatePost(input),
164
+ archive: (input: { postId: string; reason?: string; idempotencyKey?: string }) =>
165
+ this.archivePost(input),
166
+ /*
167
+ * Acknowledging is an explicit call and always speaks for the caller alone.
168
+ * There is no bulk form and no acknowledge-on-behalf-of: an acknowledgement
169
+ * is one actor saying "I have seen this", and reading a post must never
170
+ * append one, or the roster stops meaning anything.
171
+ */
172
+ acknowledge: (input: { postId: string; note?: string; idempotencyKey?: string }) =>
173
+ this.acknowledgePost(input),
174
+ withdrawAcknowledgment: (input: { postId: string; reason?: string }) =>
175
+ this.withdrawPostAcknowledgment(input),
176
+ roster: (postId: string) => this.postRoster(postId),
177
+ };
178
+
142
179
  readonly notes = {
143
180
  create: (input: CreateNoteInput) => this.createNote(input),
144
181
  list: (input: Omit<ItemListInput, 'kinds'> = {}) =>
@@ -243,6 +280,30 @@ export class SynomemCore implements SynomemDomainService {
243
280
  if (this.initialized) return;
244
281
  await this.repository.init();
245
282
  this.initialized = true;
283
+
284
+ /*
285
+ * An agent actor is resolved to its canonical identity here.
286
+ *
287
+ * Callers name a handle because that is what people and harnesses know,
288
+ * but every event must record the opaque ID — otherwise renaming a handle
289
+ * would orphan the history written under the old one. The display name
290
+ * comes from the profile for the same reason a harness cannot assert it on
291
+ * the command line: the stored record is the authority, not the argument.
292
+ *
293
+ * An unresolvable name is left as given rather than rejected, so a system
294
+ * actor can still create the agent that does not exist yet. Writing as an
295
+ * unknown agent is refused later by the checks that already exist.
296
+ */
297
+ if (this.actor.kind === 'agent') {
298
+ const resolved = await this.repository.resolveAgent(this.actor.id);
299
+ if (resolved.match) {
300
+ this.actor = {
301
+ kind: 'agent',
302
+ id: resolved.match.id,
303
+ ...(resolved.match.displayName ? { displayName: resolved.match.displayName } : {}),
304
+ };
305
+ }
306
+ }
246
307
  }
247
308
 
248
309
  async close(): Promise<void> {
@@ -286,20 +347,29 @@ export class SynomemCore implements SynomemDomainService {
286
347
  this.checkAbort();
287
348
  await this.repository.assertEventCompatibility();
288
349
  const parsed = this.validate(() => createAgentSchema.parse(input));
289
- if (await this.repository.getAgent(parsed.id)) {
290
- throw new SynomemError('AGENT_EXISTS', `Agent or alias already exists: ${parsed.id}`);
350
+ if (await this.repository.getAgent(parsed.handle)) {
351
+ throw new SynomemError('AGENT_EXISTS', `Agent or alias already exists: ${parsed.handle}`);
291
352
  }
292
353
  const aliases = [...new Set(parsed.aliases ?? [])].sort();
293
- if (aliases.includes(parsed.id)) {
294
- throw new SynomemError('ALIAS_CONFLICT', 'An agent cannot use its own ID as an alias.');
354
+ if (aliases.includes(parsed.handle)) {
355
+ throw new SynomemError('ALIAS_CONFLICT', 'An agent cannot use its own handle as an alias.');
295
356
  }
296
357
  for (const alias of aliases) {
297
358
  if (await this.repository.getAgent(alias)) {
298
359
  throw new SynomemError('ALIAS_CONFLICT', `Alias already belongs to an agent: ${alias}`);
299
360
  }
300
361
  }
362
+ /*
363
+ * The canonical ID is generated here and never supplied by the caller.
364
+ * Every event references it permanently, so it has to be free of meaning:
365
+ * a caller that could choose it could choose one that collides with an
366
+ * archived agent's history, and a meaningful ID becomes a handle nobody can
367
+ * rename.
368
+ */
301
369
  const profile: AgentProfile = {
302
- id: parsed.id,
370
+ id: this.nextId(),
371
+ handle: parsed.handle,
372
+ status: 'active',
303
373
  displayName: parsed.displayName,
304
374
  ...(aliases.length ? { aliases } : {}),
305
375
  ...(parsed.description !== undefined ? { description: parsed.description } : {}),
@@ -327,8 +397,20 @@ export class SynomemCore implements SynomemDomainService {
327
397
  const existing = await this.repository.getAgent(idOrAlias);
328
398
  if (!existing) throw new SynomemError('AGENT_NOT_FOUND', `Unknown agent: ${idOrAlias}`);
329
399
  const aliases = parsed.aliases ? [...new Set(parsed.aliases)].sort() : existing.aliases;
330
- if (aliases?.includes(existing.id)) {
331
- throw new SynomemError('ALIAS_CONFLICT', 'An agent cannot use its own ID as an alias.');
400
+ const handle = parsed.handle ?? existing.handle;
401
+ if (aliases?.includes(handle)) {
402
+ throw new SynomemError('ALIAS_CONFLICT', 'An agent cannot use its own handle as an alias.');
403
+ }
404
+ // Renaming the handle is allowed and is why the canonical ID exists, but a
405
+ // handle another agent already answers to is still refused.
406
+ if (parsed.handle && parsed.handle !== existing.handle) {
407
+ const owner = await this.repository.getAgent(parsed.handle);
408
+ if (owner && owner.id !== existing.id) {
409
+ throw new SynomemError(
410
+ 'ALIAS_CONFLICT',
411
+ `Handle already belongs to ${owner.id}: ${parsed.handle}`,
412
+ );
413
+ }
332
414
  }
333
415
  for (const alias of aliases ?? []) {
334
416
  const owner = await this.repository.getAgent(alias);
@@ -356,6 +438,52 @@ export class SynomemCore implements SynomemDomainService {
356
438
  return updated;
357
439
  }
358
440
 
441
+ /**
442
+ * Archiving stops an agent acting without erasing it.
443
+ *
444
+ * Events reference the actor permanently, so deleting an agent would leave
445
+ * history pointing at nothing. Archived agents keep their records and their
446
+ * handle, and can be restored.
447
+ */
448
+ private async setAgentStatus(
449
+ idOrAlias: string,
450
+ status: 'active' | 'archived',
451
+ ): Promise<AgentProfile> {
452
+ this.checkAbort();
453
+ await this.repository.assertEventCompatibility();
454
+ this.validate(() => agentLookupSchema.parse(idOrAlias));
455
+ const existing = await this.repository.getAgent(idOrAlias);
456
+ if (!existing) throw new SynomemError('AGENT_NOT_FOUND', `Unknown agent: ${idOrAlias}`);
457
+ if (existing.status === status) return existing;
458
+ const updated: AgentProfile = { ...existing, status };
459
+ await this.repository.transaction(async () => {
460
+ const event: SynomemEvent = {
461
+ ...this.eventBase(existing.id, await this.repository.nextAggregateVersion(existing.id)),
462
+ type: 'agent.updated',
463
+ agentId: existing.id,
464
+ changes: { status },
465
+ };
466
+ await this.repository.updateAgent(updated, event.createdAt);
467
+ await this.repository.insertEvent(event);
468
+ });
469
+ await this.projectionWriter.syncAgent(updated.id);
470
+ return updated;
471
+ }
472
+
473
+ /** Adds aliases without disturbing the ones already there. */
474
+ private async addAgentAliases(idOrAlias: string, add: string[]): Promise<AgentProfile> {
475
+ const existing = await this.getAgent(idOrAlias);
476
+ const merged = [...new Set([...(existing.aliases ?? []), ...add])].sort();
477
+ return await this.updateAgent(existing.id, { aliases: merged });
478
+ }
479
+
480
+ private async removeAgentAliases(idOrAlias: string, remove: string[]): Promise<AgentProfile> {
481
+ const existing = await this.getAgent(idOrAlias);
482
+ const drop = new Set(remove.map((alias) => alias.trim().toLowerCase()));
483
+ const kept = (existing.aliases ?? []).filter((alias) => !drop.has(alias));
484
+ return await this.updateAgent(existing.id, { aliases: kept });
485
+ }
486
+
359
487
  private async getAgent(idOrAlias: string): Promise<AgentProfile> {
360
488
  this.checkAbort();
361
489
  this.validate(() => agentLookupSchema.parse(idOrAlias));
@@ -757,6 +885,174 @@ export class SynomemCore implements SynomemDomainService {
757
885
  return await this.getMemoRecord(input.memoId);
758
886
  }
759
887
 
888
+ private async getPostRecord(id: string): Promise<PostRecord> {
889
+ await this.requireVisibleItem(id, 'post');
890
+ const record = postRecordsFromEvents(await this.repository.getReadableItemEvents(id))[0];
891
+ if (!record) throw new SynomemError('ITEM_NOT_FOUND', `Unknown post: ${id}`);
892
+ return record;
893
+ }
894
+
895
+ private async getPost(id: string): Promise<PostRecord> {
896
+ this.checkAbort();
897
+ return await this.getPostRecord(id);
898
+ }
899
+
900
+ private async createPost(input: CreatePostInput): Promise<{
901
+ record: PostRecord;
902
+ created: boolean;
903
+ deduplicated: boolean;
904
+ }> {
905
+ this.checkAbort();
906
+ await this.repository.assertEventCompatibility();
907
+ const parsed = this.validate(() => createPostSchema.parse(input));
908
+
909
+ // A reply inherits its parent's workspace by construction, and cannot name
910
+ // a different target — there is no target to name.
911
+ if (parsed.replyTo) await this.requireVisibleItem(parsed.replyTo, 'post');
912
+
913
+ const outcome = await this.repository.transaction(async () => {
914
+ const prior = await this.priorMutation(parsed.idempotencyKey, 'post.created');
915
+ if (prior?.type === 'post.created') return { id: prior.id, created: false };
916
+ const id = this.nextId();
917
+ const event: SynomemEvent = {
918
+ ...this.eventBase(id, 1, id),
919
+ type: 'post.created',
920
+ title: parsed.title,
921
+ body: parsed.body,
922
+ tags: [...new Set(parsed.tags ?? [])].sort(),
923
+ ...(parsed.replyTo ? { replyTo: parsed.replyTo } : {}),
924
+ ...(parsed.idempotencyKey ? { idempotencyKey: parsed.idempotencyKey } : {}),
925
+ ...(parsed.source ? { source: parsed.source } : {}),
926
+ ...(parsed.metadata ? { metadata: parsed.metadata } : {}),
927
+ };
928
+ await this.repository.insertEvent(event);
929
+ return { id, created: true };
930
+ });
931
+ return {
932
+ record: await this.getPostRecord(outcome.id),
933
+ created: outcome.created,
934
+ deduplicated: !outcome.created,
935
+ };
936
+ }
937
+
938
+ /** Only the author edits a post. Everyone else responds to it. */
939
+ private assertPostAuthor(record: PostRecord): void {
940
+ if (this.administrative) return;
941
+ if (record.event.actor.id !== this.actor.id || record.event.actor.kind !== this.actor.kind) {
942
+ throw new SynomemError('MUTATION_FORBIDDEN', 'Only the author can change a post.');
943
+ }
944
+ }
945
+
946
+ private async updatePost(input: UpdatePostInput): Promise<PostRecord> {
947
+ this.checkAbort();
948
+ const parsed = this.validate(() => updatePostSchema.parse(input));
949
+ const record = await this.getPostRecord(parsed.postId);
950
+ this.assertPostAuthor(record);
951
+ if (record.status === 'archived') {
952
+ throw new SynomemError('MUTATION_FORBIDDEN', 'An archived post cannot be edited.');
953
+ }
954
+ if (record.version !== parsed.expectedVersion) {
955
+ throw new SynomemError(
956
+ 'REVISION_CONFLICT',
957
+ `Post ${parsed.postId} is at version ${record.version}.`,
958
+ );
959
+ }
960
+ await this.repository.transaction(async () => {
961
+ const event: SynomemEvent = {
962
+ ...this.eventBase(parsed.postId, await this.repository.nextAggregateVersion(parsed.postId)),
963
+ type: 'post.edited',
964
+ postId: parsed.postId,
965
+ title: parsed.title ?? record.title,
966
+ body: parsed.body ?? record.body,
967
+ tags: [...new Set(parsed.tags ?? record.tags ?? [])].sort(),
968
+ ...(parsed.idempotencyKey ? { idempotencyKey: parsed.idempotencyKey } : {}),
969
+ };
970
+ await this.repository.insertEvent(event);
971
+ });
972
+ return await this.getPostRecord(parsed.postId);
973
+ }
974
+
975
+ private async archivePost(input: {
976
+ postId: string;
977
+ reason?: string;
978
+ idempotencyKey?: string;
979
+ }): Promise<PostRecord> {
980
+ this.checkAbort();
981
+ const record = await this.getPostRecord(input.postId);
982
+ this.assertPostAuthor(record);
983
+ if (record.status !== 'archived') {
984
+ await this.repository.transaction(async () => {
985
+ const event: SynomemEvent = {
986
+ ...this.eventBase(input.postId, await this.repository.nextAggregateVersion(input.postId)),
987
+ type: 'post.archived',
988
+ postId: input.postId,
989
+ ...(input.reason ? { reason: input.reason } : {}),
990
+ ...(input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {}),
991
+ };
992
+ await this.repository.insertEvent(event);
993
+ });
994
+ }
995
+ return await this.getPostRecord(input.postId);
996
+ }
997
+
998
+ private async acknowledgePost(input: {
999
+ postId: string;
1000
+ note?: string;
1001
+ idempotencyKey?: string;
1002
+ }): Promise<PostRecord> {
1003
+ this.checkAbort();
1004
+ const record = await this.getPostRecord(input.postId);
1005
+ // Acknowledging twice is the same statement, so the second is a no-op
1006
+ // rather than a second row or an error.
1007
+ const already = record.acknowledgments.some(
1008
+ (entry) => entry.actor.id === this.actor.id && entry.actor.kind === this.actor.kind,
1009
+ );
1010
+ if (!already) {
1011
+ await this.repository.transaction(async () => {
1012
+ const event: SynomemEvent = {
1013
+ ...this.eventBase(input.postId, await this.repository.nextAggregateVersion(input.postId)),
1014
+ type: 'post.acknowledged',
1015
+ postId: input.postId,
1016
+ ...(input.note ? { note: input.note } : {}),
1017
+ ...(input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {}),
1018
+ };
1019
+ await this.repository.insertEvent(event);
1020
+ });
1021
+ }
1022
+ return await this.getPostRecord(input.postId);
1023
+ }
1024
+
1025
+ private async withdrawPostAcknowledgment(input: {
1026
+ postId: string;
1027
+ reason?: string;
1028
+ }): Promise<PostRecord> {
1029
+ this.checkAbort();
1030
+ const record = await this.getPostRecord(input.postId);
1031
+ const mine = record.acknowledgments.some(
1032
+ (entry) => entry.actor.id === this.actor.id && entry.actor.kind === this.actor.kind,
1033
+ );
1034
+ if (mine) {
1035
+ await this.repository.transaction(async () => {
1036
+ const event: SynomemEvent = {
1037
+ ...this.eventBase(input.postId, await this.repository.nextAggregateVersion(input.postId)),
1038
+ type: 'post.acknowledgment.withdrawn',
1039
+ postId: input.postId,
1040
+ ...(input.reason ? { reason: input.reason } : {}),
1041
+ };
1042
+ await this.repository.insertEvent(event);
1043
+ });
1044
+ }
1045
+ return await this.getPostRecord(input.postId);
1046
+ }
1047
+
1048
+ private async postRoster(postId: string): Promise<PostRoster> {
1049
+ this.checkAbort();
1050
+ await this.requireVisibleItem(postId, 'post');
1051
+ const roster = await this.repository.postRoster(postId);
1052
+ if (!roster) throw new SynomemError('ITEM_NOT_FOUND', `Unknown post: ${postId}`);
1053
+ return roster;
1054
+ }
1055
+
760
1056
  private async getNoteRecord(id: string): Promise<NoteRecord> {
761
1057
  await this.requireVisibleItem(id, 'note');
762
1058
  const record = noteRecordsFromEvents(await this.repository.getReadableItemEvents(id))[0];
@@ -1245,10 +1541,34 @@ export class SynomemCore implements SynomemDomainService {
1245
1541
  return await this.getTodoRecord(input.todoId);
1246
1542
  }
1247
1543
 
1544
+ /**
1545
+ * Turns an agent name in a filter into the canonical ID the records hold.
1546
+ *
1547
+ * Callers filter by the name they know — a handle or an alias — while every
1548
+ * record stores the opaque ID. Without this the filter silently matches
1549
+ * nothing, which reads as "there is nothing here" rather than "that name
1550
+ * means something else now".
1551
+ *
1552
+ * An unresolvable name is passed through unchanged so it can match a legacy
1553
+ * name-shaped ID rather than being swallowed.
1554
+ */
1555
+ private async canonicalAgentId(name: string | undefined): Promise<string | undefined> {
1556
+ if (!name) return name;
1557
+ const resolved = await this.repository.resolveAgent(name);
1558
+ return resolved.match?.id ?? name;
1559
+ }
1560
+
1248
1561
  private async listItems(input: ItemListInput): Promise<Page<ItemSummary>> {
1249
1562
  this.checkAbort();
1250
1563
  const parsed = this.validate(() => itemListInputSchema.parse(input));
1251
- return await this.repository.listItemSummaries(parsed, this.actor);
1564
+ const resolved = {
1565
+ ...parsed,
1566
+ ...(parsed.participantAgentId
1567
+ ? { participantAgentId: await this.canonicalAgentId(parsed.participantAgentId) }
1568
+ : {}),
1569
+ ...(parsed.actorId ? { actorId: await this.canonicalAgentId(parsed.actorId) } : {}),
1570
+ };
1571
+ return await this.repository.listItemSummaries(resolved, this.actor);
1252
1572
  }
1253
1573
  private async listItemChanges(input: ChangesInput): Promise<ChangePage> {
1254
1574
  this.checkAbort();
@@ -1343,8 +1663,8 @@ export class SynomemCore implements SynomemDomainService {
1343
1663
  * check that silently lags the migration runner reports a healthy database as
1344
1664
  * broken.
1345
1665
  */
1346
- const CURRENT_SCHEMA_VERSION = 5;
1347
- const EXPECTED_APPLIED_MIGRATIONS = [1, 2, 3, 4, 5];
1666
+ const CURRENT_SCHEMA_VERSION = 7;
1667
+ const EXPECTED_APPLIED_MIGRATIONS = [1, 2, 3, 4, 5, 6, 7];
1348
1668
 
1349
1669
  export class SynomemClient extends SynomemCore implements SynomemService {
1350
1670
  readonly home: string;
package/src/cloud.ts ADDED
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Synomem Cloud, as a constant rather than a question.
3
+ *
4
+ * Public onboarding must never ask for a service URL. A person setting up
5
+ * Synomem has no way to know whether an address they were given is the real
6
+ * one, and a prompt that accepts any origin is a prompt that can be phished.
7
+ * The hosted service therefore has one address, compiled in.
8
+ *
9
+ * `SYNOMEM_API_URL` remains for development and private deployments. It is
10
+ * deliberately undocumented in the README, the public docs, the packaged skill
11
+ * and ordinary help output — a private deployment is configured by whoever runs
12
+ * it, not discovered by an ordinary user.
13
+ */
14
+ export const SYNOMEM_CLOUD_API_URL = 'https://api.synomem.ai';
15
+
16
+ export function cloudApiUrl(env: NodeJS.ProcessEnv = process.env): string {
17
+ const override = env.SYNOMEM_API_URL?.trim();
18
+ return override || SYNOMEM_CLOUD_API_URL;
19
+ }
package/src/config.ts CHANGED
@@ -73,7 +73,14 @@ export const configSchema = policySchema.extend({
73
73
  });
74
74
 
75
75
  export function resolveHome(explicitHome?: string): string {
76
- const candidate = explicitHome ?? process.env.SYNOMEM_HOME ?? resolve(homedir(), '.agents');
76
+ /*
77
+ * `~/.synomem`, resolved as an exact directory.
78
+ *
79
+ * No detection of or migration from `~/.agents`: the project is greenfield,
80
+ * and code that quietly moves somebody's database is worse than a clear
81
+ * message telling them where the new home is.
82
+ */
83
+ const candidate = explicitHome ?? process.env.SYNOMEM_HOME ?? resolve(homedir(), '.synomem');
77
84
  if (candidate.includes('\0')) throw new SynomemError('UNSAFE_PATH', 'Storage home contains NUL.');
78
85
  return resolve(candidate);
79
86
  }