synomem 0.3.0 → 0.5.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 (91) hide show
  1. package/CHANGELOG.md +82 -1
  2. package/README.md +54 -14
  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 +9 -0
  7. package/dist/cli.d.ts.map +1 -1
  8. package/dist/cli.js +451 -24
  9. package/dist/cli.js.map +1 -1
  10. package/dist/client.d.ts +34 -2
  11. package/dist/client.d.ts.map +1 -1
  12. package/dist/client.js +162 -11
  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 +193 -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 +9 -1
  28. package/dist/credentials.js.map +1 -1
  29. package/dist/discover.d.ts +48 -0
  30. package/dist/discover.d.ts.map +1 -0
  31. package/dist/discover.js +106 -0
  32. package/dist/discover.js.map +1 -0
  33. package/dist/import.d.ts +58 -43
  34. package/dist/import.d.ts.map +1 -1
  35. package/dist/index.d.ts +4 -1
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +2 -0
  38. package/dist/index.js.map +1 -1
  39. package/dist/mcp/index.d.ts.map +1 -1
  40. package/dist/mcp/index.js +15 -6
  41. package/dist/mcp/index.js.map +1 -1
  42. package/dist/oauth.d.ts.map +1 -1
  43. package/dist/oauth.js +8 -1
  44. package/dist/oauth.js.map +1 -1
  45. package/dist/projections.d.ts.map +1 -1
  46. package/dist/projections.js +19 -11
  47. package/dist/projections.js.map +1 -1
  48. package/dist/prompt.d.ts +28 -0
  49. package/dist/prompt.d.ts.map +1 -0
  50. package/dist/prompt.js +72 -0
  51. package/dist/prompt.js.map +1 -0
  52. package/dist/remote.d.ts +4 -0
  53. package/dist/remote.d.ts.map +1 -1
  54. package/dist/remote.js +4 -0
  55. package/dist/remote.js.map +1 -1
  56. package/dist/schemas.d.ts +101 -60
  57. package/dist/schemas.d.ts.map +1 -1
  58. package/dist/schemas.js +57 -5
  59. package/dist/schemas.js.map +1 -1
  60. package/dist/service.d.ts +6 -1
  61. package/dist/service.d.ts.map +1 -1
  62. package/dist/storage.d.ts +19 -1
  63. package/dist/storage.d.ts.map +1 -1
  64. package/dist/storage.js +61 -14
  65. package/dist/storage.js.map +1 -1
  66. package/dist/types.d.ts +50 -2
  67. package/dist/types.d.ts.map +1 -1
  68. package/docs/cli.md +68 -4
  69. package/docs/examples.md +1 -1
  70. package/docs/mcp.md +1 -1
  71. package/docs/skill.md +1 -1
  72. package/docs/storage-format.md +1 -1
  73. package/package.json +8 -8
  74. package/src/backend.ts +8 -2
  75. package/src/cli.ts +597 -31
  76. package/src/client.ts +176 -12
  77. package/src/cloud.ts +19 -0
  78. package/src/config.ts +8 -1
  79. package/src/configure.ts +249 -0
  80. package/src/credentials.ts +26 -3
  81. package/src/discover.ts +155 -0
  82. package/src/index.ts +9 -1
  83. package/src/mcp/index.ts +19 -5
  84. package/src/oauth.ts +8 -1
  85. package/src/projections.ts +21 -11
  86. package/src/prompt.ts +88 -0
  87. package/src/remote.ts +24 -0
  88. package/src/schemas.ts +60 -5
  89. package/src/service.ts +12 -0
  90. package/src/storage.ts +79 -13
  91. package/src/types.ts +45 -2
package/src/storage.ts CHANGED
@@ -255,6 +255,21 @@ CREATE TABLE post_acknowledgments (
255
255
  CREATE INDEX post_acknowledgments_post ON post_acknowledgments(post_id);
256
256
  `;
257
257
 
258
+ const migrationV7 = `
259
+ -- Opaque canonical IDs, with the handle as a separate mutable name.
260
+ --
261
+ -- Existing agents keep their name-shaped ID and take it as their handle too.
262
+ -- Rewriting the actor ID inside stored events to tidy the format would be
263
+ -- exactly the rewrite an append-only log exists to prevent, so history stays
264
+ -- as written and only NEW agents get a generated opaque ID.
265
+ ALTER TABLE agents ADD COLUMN handle TEXT;
266
+ UPDATE agents SET handle = id WHERE handle IS NULL;
267
+ CREATE UNIQUE INDEX agents_handle ON agents(handle);
268
+
269
+ -- Archived agents keep their records and stop being able to act.
270
+ ALTER TABLE agents ADD COLUMN status TEXT NOT NULL DEFAULT 'active';
271
+ `;
272
+
258
273
  const migrationV3 = `
259
274
  DROP TRIGGER IF EXISTS events_append_only_update;
260
275
  DROP TRIGGER IF EXISTS events_append_only_delete;
@@ -430,7 +445,8 @@ export class SynomemStorage implements SynomemRepository {
430
445
 
431
446
  constructor(options: StorageOptions) {
432
447
  this.home = resolve(options.home);
433
- this.storageDirectory = join(this.home, 'synomem');
448
+ // The home is the storage directory; see configLocation in backend.ts.
449
+ this.storageDirectory = this.home;
434
450
  this.databasePath = join(this.storageDirectory, 'synomem.sqlite3');
435
451
  this.configPath = join(this.storageDirectory, 'config.json');
436
452
  this.readOnly = options.readOnly;
@@ -518,7 +534,7 @@ export class SynomemStorage implements SynomemRepository {
518
534
  const version = Number(
519
535
  (db.prepare('PRAGMA user_version').get() as { user_version: number }).user_version,
520
536
  );
521
- if (version > 6) {
537
+ if (version > 7) {
522
538
  throw new SynomemError(
523
539
  'UNSUPPORTED_SCHEMA',
524
540
  `Database schema version ${version} is newer than this package supports.`,
@@ -598,14 +614,26 @@ export class SynomemStorage implements SynomemRepository {
598
614
  db.exec('PRAGMA user_version = 6');
599
615
  });
600
616
  }
617
+ const afterV6 = Number(
618
+ (db.prepare('PRAGMA user_version').get() as { user_version: number }).user_version,
619
+ );
620
+ if (afterV6 === 6) {
621
+ this.transactionSync(() => {
622
+ db.exec(migrationV7);
623
+ db.prepare(
624
+ 'INSERT OR IGNORE INTO schema_migrations(version, applied_at) VALUES (?, ?)',
625
+ ).run(7, new Date().toISOString());
626
+ db.exec('PRAGMA user_version = 7');
627
+ });
628
+ }
601
629
  }
602
630
 
603
631
  private assertSchemaSupported(): void {
604
632
  const version = Number(
605
633
  (this.db().prepare('PRAGMA user_version').get() as { user_version: number }).user_version,
606
634
  );
607
- if (version !== 6) {
608
- if (version >= 1 && version <= 5) {
635
+ if (version !== 7) {
636
+ if (version >= 1 && version <= 6) {
609
637
  throw new SynomemError(
610
638
  'UNSUPPORTED_SCHEMA',
611
639
  `Database schema version ${version} requires migration. Open this home once with readOnly: false, then retry the read-only client.`,
@@ -1530,12 +1558,23 @@ export class SynomemStorage implements SynomemRepository {
1530
1558
  return { schemaVersion, appliedVersions };
1531
1559
  }
1532
1560
 
1561
+ /**
1562
+ * Aliases that also name an agent directly.
1563
+ *
1564
+ * Matches against the HANDLE as well as the canonical ID. With opaque IDs an
1565
+ * alias can no longer accidentally equal one, but it can easily equal another
1566
+ * agent's handle — which is the collision that actually makes a lookup
1567
+ * ambiguous now.
1568
+ */
1533
1569
  aliasIdentityConflicts(): Array<{ alias: string; agentId: string }> {
1534
1570
  return this.db()
1535
1571
  .prepare(
1536
1572
  `SELECT x.alias, x.agent_id AS agentId
1537
- FROM aliases x JOIN agents a ON a.id = x.alias
1538
- ORDER BY x.alias`,
1573
+ FROM aliases x
1574
+ JOIN agents a ON lower(a.id) = x.normalized_alias
1575
+ OR lower(a.handle) = x.normalized_alias
1576
+ WHERE a.id != x.agent_id
1577
+ ORDER BY x.alias`,
1539
1578
  )
1540
1579
  .all() as unknown as Array<{ alias: string; agentId: string }>;
1541
1580
  }
@@ -1727,10 +1766,13 @@ export class SynomemStorage implements SynomemRepository {
1727
1766
  const parsed = profileSchema.parse(profile);
1728
1767
  this.db()
1729
1768
  .prepare(
1730
- 'INSERT INTO agents(id, display_name, profile_json, created_at, updated_at) VALUES (?, ?, ?, ?, ?)',
1769
+ `INSERT INTO agents(id, handle, status, display_name, profile_json, created_at, updated_at)
1770
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
1731
1771
  )
1732
1772
  .run(
1733
1773
  parsed.id,
1774
+ parsed.handle,
1775
+ parsed.status,
1734
1776
  parsed.displayName,
1735
1777
  JSON.stringify(parsed),
1736
1778
  parsed.createdAt,
@@ -1742,8 +1784,18 @@ export class SynomemStorage implements SynomemRepository {
1742
1784
  updateAgent(profile: AgentProfile, updatedAt: string): void {
1743
1785
  const parsed = profileSchema.parse(profile);
1744
1786
  this.db()
1745
- .prepare('UPDATE agents SET display_name = ?, profile_json = ?, updated_at = ? WHERE id = ?')
1746
- .run(parsed.displayName, JSON.stringify(parsed), updatedAt, parsed.id);
1787
+ .prepare(
1788
+ `UPDATE agents SET handle = ?, status = ?, display_name = ?, profile_json = ?,
1789
+ updated_at = ? WHERE id = ?`,
1790
+ )
1791
+ .run(
1792
+ parsed.handle,
1793
+ parsed.status,
1794
+ parsed.displayName,
1795
+ JSON.stringify(parsed),
1796
+ updatedAt,
1797
+ parsed.id,
1798
+ );
1747
1799
  this.db().prepare('DELETE FROM aliases WHERE agent_id = ?').run(parsed.id);
1748
1800
  this.insertAliases(parsed.id, parsed.aliases ?? []);
1749
1801
  }
@@ -1770,10 +1822,10 @@ export class SynomemStorage implements SynomemRepository {
1770
1822
  `SELECT DISTINCT a.profile_json
1771
1823
  FROM agents a
1772
1824
  LEFT JOIN aliases x ON x.agent_id = a.id
1773
- WHERE lower(a.id) = ? OR x.normalized_alias = ?
1825
+ WHERE lower(a.id) = ? OR lower(a.handle) = ? OR x.normalized_alias = ?
1774
1826
  ORDER BY a.id ASC`,
1775
1827
  )
1776
- .all(normalized, normalized) as unknown as ProfileRow[];
1828
+ .all(normalized, normalized, normalized) as unknown as ProfileRow[];
1777
1829
  const candidates = rows.map((row) => profileSchema.parse(JSON.parse(row.profile_json)));
1778
1830
  return candidates.length === 1 ? { match: candidates[0]!, candidates } : { candidates };
1779
1831
  }
@@ -1964,11 +2016,16 @@ export class SynomemStorage implements SynomemRepository {
1964
2016
  });
1965
2017
  }
1966
2018
 
1967
- replaceAgentProjectionManifest(agentId: string, paths: string[], generatedAt: string): void {
2019
+ /**
2020
+ * @param directory the agent's projection directory name, which is its
2021
+ * handle rather than its canonical ID: these rows are keyed by the path on
2022
+ * disk, and projections are named for people to read.
2023
+ */
2024
+ replaceAgentProjectionManifest(directory: string, paths: string[], generatedAt: string): void {
1968
2025
  this.transactionSync(() => {
1969
2026
  this.db()
1970
2027
  .prepare('DELETE FROM projection_manifest WHERE path LIKE ? OR path LIKE ?')
1971
- .run(`${agentId}/%`, `${agentId}\\%`);
2028
+ .run(`${directory}/%`, `${directory}\\%`);
1972
2029
  const insert = this.db().prepare(
1973
2030
  'INSERT INTO projection_manifest(path, generated_at) VALUES (?, ?)',
1974
2031
  );
@@ -1984,6 +2041,15 @@ export class SynomemStorage implements SynomemRepository {
1984
2041
  ).map((row) => row.path);
1985
2042
  }
1986
2043
 
2044
+ /** The manifest with the time each path was written, newest first. */
2045
+ projectionManifestEntries(): { path: string; generatedAt: string }[] {
2046
+ return (
2047
+ this.db()
2048
+ .prepare('SELECT path, generated_at FROM projection_manifest ORDER BY generated_at DESC')
2049
+ .all() as unknown as { path: string; generated_at: string }[]
2050
+ ).map((row) => ({ path: row.path, generatedAt: row.generated_at }));
2051
+ }
2052
+
1987
2053
  integrityCheck(): string[] {
1988
2054
  const rows = this.db().prepare('PRAGMA integrity_check').all() as unknown as Record<
1989
2055
  string,
package/src/types.ts CHANGED
@@ -28,10 +28,22 @@ export interface EvidenceReference {
28
28
  }
29
29
 
30
30
  export interface AgentProfile {
31
+ /**
32
+ * Canonical, opaque, immutable. Every event references this, so it can never
33
+ * change — which is exactly why the handle exists separately.
34
+ */
31
35
  id: string;
36
+ /** The human-friendly name, unique in the workspace and safe to rename. */
37
+ handle: string;
32
38
  displayName: string;
33
39
  aliases?: string[];
34
40
  description?: string;
41
+ /**
42
+ * Archived agents keep their history and stop being able to act. Events
43
+ * reference the actor permanently, so deletion would leave history pointing
44
+ * at nothing.
45
+ */
46
+ status: 'active' | 'archived';
35
47
  createdAt: string;
36
48
  metadata?: Record<string, JsonValue>;
37
49
  }
@@ -684,13 +696,15 @@ export interface BindRuntimeInput {
684
696
  }
685
697
 
686
698
  export interface CreateAgentInput {
687
- id: string;
699
+ /** The handle. The canonical ID is generated, never supplied. */
700
+ handle: string;
688
701
  displayName: string;
689
702
  aliases?: string[];
690
703
  description?: string;
691
704
  metadata?: Record<string, JsonValue>;
692
705
  }
693
706
  export interface UpdateAgentInput {
707
+ handle?: string;
694
708
  displayName?: string;
695
709
  aliases?: string[];
696
710
  description?: string;
@@ -706,7 +720,15 @@ export interface KudosStats {
706
720
  byTag: Record<string, number>;
707
721
  }
708
722
  export interface Diagnostic {
709
- level: 'ok' | 'warning' | 'error';
723
+ /**
724
+ * `skipped` is not a failure.
725
+ *
726
+ * A check the caller lacks permission to run says so and leaves the overall
727
+ * result healthy. Failing the whole diagnostic because an ordinary agent
728
+ * cannot read workspace administration would make `doctor` useless to the
729
+ * callers who need it most.
730
+ */
731
+ level: 'ok' | 'warning' | 'error' | 'skipped';
710
732
  code: string;
711
733
  message: string;
712
734
  path?: string;
@@ -715,6 +737,27 @@ export interface DoctorResult {
715
737
  healthy: boolean;
716
738
  diagnostics: Diagnostic[];
717
739
  }
740
+ /**
741
+ * What the projected files on disk look like next to what they should be.
742
+ *
743
+ * Projections are derived, never canonical, so this reports drift rather than
744
+ * damage: `missing` and `unexpected` are both repaired by a rebuild, and
745
+ * neither means an event was lost.
746
+ */
747
+ export interface ProjectionStatus {
748
+ /** Where projected files live. Absent on a backend that projects nothing. */
749
+ directory?: string;
750
+ settings: SynomemConfig['projection'];
751
+ /** True when the manifest matches what a rebuild would produce. */
752
+ current: boolean;
753
+ /** Recorded by the manifest, not the filesystem; absent before any rebuild. */
754
+ lastRebuiltAt?: string;
755
+ counts: { expected: number; manifest: number; missing: number; unexpected: number };
756
+ /** Expected but not on disk. Capped, because a large workspace has many. */
757
+ missing: string[];
758
+ /** On disk and in the manifest, but no longer expected. Capped likewise. */
759
+ unexpected: string[];
760
+ }
718
761
 
719
762
  export type SynomemBackendConfig =
720
763
  { kind: 'local' } | { kind: 'remote'; baseUrl: string; workspaceId: string };