synomem 0.9.5 → 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 (120) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +9 -7
  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 +337 -30
  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 -367
  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 +3 -1
  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 +13 -13
  96. package/skills/synomem/agents/openai.yaml +1 -1
  97. package/skills/synomem/references/examples.md +2 -3
  98. package/src/actor-directory.ts +87 -0
  99. package/src/backend.ts +29 -3
  100. package/src/bookmarks.ts +127 -0
  101. package/src/cli.ts +941 -546
  102. package/src/client.ts +1143 -279
  103. package/src/cursors.ts +132 -0
  104. package/src/errors.ts +2 -0
  105. package/src/index.ts +50 -0
  106. package/src/mcp/index.ts +420 -104
  107. package/src/mutation-receipts.ts +7 -0
  108. package/src/notifications.ts +483 -0
  109. package/src/participation-repository.ts +406 -0
  110. package/src/participation.ts +103 -0
  111. package/src/policy.ts +109 -0
  112. package/src/ports/repository.ts +33 -0
  113. package/src/profiles.ts +2 -1
  114. package/src/projections.ts +49 -48
  115. package/src/remote.ts +227 -37
  116. package/src/schemas.ts +116 -16
  117. package/src/search.ts +24 -0
  118. package/src/service.ts +122 -8
  119. package/src/storage.ts +1039 -699
  120. package/src/types.ts +67 -32
@@ -0,0 +1,87 @@
1
+ import { SynomemError } from './errors.js';
2
+ import { cursorFilter } from './cursors.js';
3
+ import type { CursorBinding } from './cursors.js';
4
+ import type { AddressableActor, ActorRef } from './policy.js';
5
+ import type { ActorIdentity, ItemSummary, Page } from './types.js';
6
+ export interface ActorProfileInput {
7
+ target: ActorRef;
8
+ after?: string;
9
+ limit?: number;
10
+ }
11
+ export interface ActorProfile {
12
+ actor: AddressableActor;
13
+ counts: { kudosReceived: number; usefulReceived: number };
14
+ authored: Page<ItemSummary>;
15
+ }
16
+ export interface ActorDirectoryInput {
17
+ query?: string;
18
+ kind?: 'human' | 'agent';
19
+ cursor?: string;
20
+ limit?: number;
21
+ }
22
+ export interface ActorDirectoryPage {
23
+ items: AddressableActor[];
24
+ hasMore: boolean;
25
+ nextCursor?: string;
26
+ total: number;
27
+ limit: number;
28
+ }
29
+ export function actorDirectoryBinding(
30
+ workspaceId: string,
31
+ viewer: ActorIdentity,
32
+ input: ActorDirectoryInput,
33
+ ): CursorBinding {
34
+ return {
35
+ purpose: 'actor-directory',
36
+ workspaceId,
37
+ actor: { kind: viewer.kind, id: viewer.id },
38
+ filter: cursorFilter({ query: (input.query ?? '').toLowerCase(), kind: input.kind ?? null }),
39
+ };
40
+ }
41
+ export function actorDirectoryKey(actor: Pick<AddressableActor, 'kind' | 'handle' | 'id'>): string {
42
+ return JSON.stringify([actor.kind, actor.handle, actor.id]);
43
+ }
44
+ export function parseActorDirectoryKey(key: string): [string, string, string] {
45
+ try {
46
+ const parts: unknown = JSON.parse(key);
47
+ if (
48
+ Array.isArray(parts) &&
49
+ parts.length === 3 &&
50
+ (parts[0] === 'human' || parts[0] === 'agent') &&
51
+ typeof parts[1] === 'string' &&
52
+ typeof parts[2] === 'string' &&
53
+ parts[1].length <= 100 &&
54
+ parts[2].length <= 200
55
+ )
56
+ return parts as [string, string, string];
57
+ } catch {
58
+ // A signed cursor can still be stale or malformed after a codec change.
59
+ }
60
+ throw new SynomemError('INVALID_INPUT', 'Invalid actor directory cursor.');
61
+ }
62
+ export function actorDirectoryInput(
63
+ input: ActorDirectoryInput = {},
64
+ ): Required<Pick<ActorDirectoryInput, 'query' | 'limit'>> &
65
+ Pick<ActorDirectoryInput, 'kind' | 'cursor'> {
66
+ if (
67
+ Object.keys(input).some((key) => !['query', 'kind', 'cursor', 'limit'].includes(key)) ||
68
+ (input.query !== undefined && typeof input.query !== 'string') ||
69
+ (input.kind !== undefined && !['human', 'agent'].includes(input.kind)) ||
70
+ (input.cursor !== undefined &&
71
+ (typeof input.cursor !== 'string' ||
72
+ input.cursor.length < 1 ||
73
+ input.cursor.length > 4096)) ||
74
+ (input.limit !== undefined &&
75
+ (!Number.isSafeInteger(input.limit) || input.limit < 1 || input.limit > 50))
76
+ )
77
+ throw new SynomemError('INVALID_INPUT', 'Invalid actor directory query.');
78
+ const query = (input.query ?? '').trim();
79
+ if (query.length > 100)
80
+ throw new SynomemError('INVALID_INPUT', 'Actor query is limited to 100 characters.');
81
+ return {
82
+ query,
83
+ limit: input.limit ?? 20,
84
+ ...(input.kind ? { kind: input.kind } : {}),
85
+ ...(input.cursor ? { cursor: input.cursor } : {}),
86
+ };
87
+ }
package/src/backend.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { DatabaseSync } from 'node:sqlite';
1
2
  /**
2
3
  * Local store helpers.
3
4
  *
@@ -13,7 +14,7 @@
13
14
  import { chmodSync, existsSync, lstatSync, mkdirSync } from 'node:fs';
14
15
  import { join } from 'node:path';
15
16
  import { ulid } from 'ulid';
16
- import { SynomemClient } from './client.js';
17
+ import { createLocalOwnerClient, SynomemClient } from './client.js';
17
18
  import { defaultConfig, mergeConfig, resolveHome } from './config.js';
18
19
  import { SynomemError } from './errors.js';
19
20
  import {
@@ -72,10 +73,10 @@ export function ensureLocalStore(explicitHome?: string): SynomemConfig {
72
73
  if (lstatSync(home).isSymbolicLink()) {
73
74
  throw new SynomemError('UNSAFE_PATH', 'The configured Synomem home cannot be a symbolic link.');
74
75
  }
75
- ensureDirectory(home);
76
- chmodSync(home, 0o700);
77
76
  const existing = readSynomemConfig(explicitHome, {});
78
77
  if (existing) return existing;
78
+ ensureDirectory(home);
79
+ chmodSync(home, 0o700);
79
80
  const config = mergeConfig(
80
81
  { ...defaultConfig, backend: { kind: 'local' }, workspaceId: ulid() },
81
82
  undefined,
@@ -100,3 +101,28 @@ export async function openLocalService(
100
101
  await client.init();
101
102
  return client;
102
103
  }
104
+
105
+ export async function openLocalOwnerService(home?: string): Promise<SynomemClient> {
106
+ ensureLocalStore(home);
107
+ const client = createLocalOwnerClient({ ...(home ? { home } : {}), actor: LOCAL_OPERATOR });
108
+ await client.init();
109
+ return client;
110
+ }
111
+
112
+ /** Filesystem-owner recovery path: no migration, normalization or writes. */
113
+ export function exportRawLocalEvents(home?: string, format: 'json' | 'jsonl' = 'jsonl'): string {
114
+ const root = resolveHome(home);
115
+ const databasePath = join(root, 'synomem.sqlite3');
116
+ assertNoSymlinkEscape(root, databasePath);
117
+ const database = new DatabaseSync(databasePath, { readOnly: true });
118
+ try {
119
+ const rows = database.prepare('SELECT payload FROM events ORDER BY rowid').all() as Array<{
120
+ payload: string;
121
+ }>;
122
+ return format === 'jsonl'
123
+ ? rows.map((row) => row.payload).join('\n') + (rows.length ? '\n' : '')
124
+ : `[${rows.map((row) => row.payload).join(',\n')}]\n`;
125
+ } finally {
126
+ database.close();
127
+ }
128
+ }
@@ -0,0 +1,127 @@
1
+ import type { ActorIdentity, ItemSummary } from './types.js';
2
+ import type { ActorRef } from './policy.js';
3
+ import type { NotificationSql } from './notifications.js';
4
+ import { cursorFilter } from './cursors.js';
5
+ export interface BookmarkInput {
6
+ after?: string;
7
+ limit?: number;
8
+ }
9
+ export interface BookmarkPage {
10
+ items: Array<{ root: Pick<ItemSummary, 'id' | 'kind' | 'title'>; savedAt: string }>;
11
+ hasMore: boolean;
12
+ nextCursor?: string;
13
+ watermark: string;
14
+ }
15
+ export interface BookmarkRepository {
16
+ set(rootId: string, actor: ActorRef, present: boolean): Promise<void>;
17
+ has(rootId: string, actor: ActorRef): Promise<boolean>;
18
+ list(input: BookmarkInput, actor: ActorIdentity): Promise<BookmarkPage>;
19
+ }
20
+ export const bookmarkTables = `
21
+ CREATE TABLE bookmark_sequences(workspace_id TEXT PRIMARY KEY,last_sequence BIGINT NOT NULL);
22
+ CREATE TABLE bookmarks(workspace_id TEXT NOT NULL,actor_kind TEXT NOT NULL,actor_id TEXT NOT NULL,root_id TEXT NOT NULL,saved_at TEXT NOT NULL,saved_sequence BIGINT NOT NULL,PRIMARY KEY(workspace_id,actor_kind,actor_id,root_id));
23
+ CREATE INDEX bookmarks_actor_sequence ON bookmarks(workspace_id,actor_kind,actor_id,saved_sequence);
24
+ `;
25
+ export class SqlBookmarkRepository implements BookmarkRepository {
26
+ constructor(private readonly sql: NotificationSql) {}
27
+ async has(rootId: string, actor: ActorRef): Promise<boolean> {
28
+ return (
29
+ (
30
+ await this.sql.query(
31
+ 'SELECT 1 FROM bookmarks WHERE workspace_id=? AND actor_kind=? AND actor_id=? AND root_id=?',
32
+ [this.sql.workspaceId, actor.kind, actor.id, rootId],
33
+ )
34
+ ).length > 0
35
+ );
36
+ }
37
+ async set(rootId: string, actor: ActorRef, present: boolean): Promise<void> {
38
+ if (!present) {
39
+ await this.sql.execute(
40
+ 'DELETE FROM bookmarks WHERE workspace_id=? AND actor_kind=? AND actor_id=? AND root_id=?',
41
+ [this.sql.workspaceId, actor.kind, actor.id, rootId],
42
+ );
43
+ return;
44
+ }
45
+ if (await this.has(rootId, actor)) return;
46
+ const counter = (
47
+ await this.sql.query(
48
+ 'INSERT INTO bookmark_sequences(workspace_id,last_sequence) VALUES(?,1) ON CONFLICT(workspace_id) DO UPDATE SET last_sequence=bookmark_sequences.last_sequence+1 RETURNING last_sequence',
49
+ [this.sql.workspaceId],
50
+ )
51
+ )[0]!;
52
+ await this.sql.execute(
53
+ 'INSERT INTO bookmarks(workspace_id,actor_kind,actor_id,root_id,saved_at,saved_sequence) VALUES(?,?,?,?,?,?) ON CONFLICT(workspace_id,actor_kind,actor_id,root_id) DO NOTHING',
54
+ [
55
+ this.sql.workspaceId,
56
+ actor.kind,
57
+ actor.id,
58
+ rootId,
59
+ new Date().toISOString(),
60
+ String(counter.last_sequence),
61
+ ],
62
+ );
63
+ }
64
+ async list(input: BookmarkInput, actor: ActorIdentity): Promise<BookmarkPage> {
65
+ const maximum = String(
66
+ (
67
+ await this.sql.query(
68
+ 'SELECT COALESCE((SELECT last_sequence FROM bookmark_sequences WHERE workspace_id=?),0) AS max',
69
+ [this.sql.workspaceId],
70
+ )
71
+ )[0]!.max,
72
+ );
73
+ const binding = {
74
+ purpose: 'bookmarks',
75
+ workspaceId: this.sql.workspaceId,
76
+ actor,
77
+ filter: cursorFilter({}),
78
+ };
79
+ const codec = this.sql.codec();
80
+ const position = input.after ? codec.decode(input.after, binding, maximum) : undefined;
81
+ const high = position?.watermark ?? maximum;
82
+ const policy = this.sql.visibility(actor);
83
+ const limit = input.limit ?? 20;
84
+ const rows = await this.sql.query(
85
+ `SELECT b.saved_sequence,b.saved_at,i.item_id,i.kind,i.title FROM bookmarks b JOIN items_current i ON i.workspace_id=b.workspace_id AND i.item_id=b.root_id WHERE b.workspace_id=? AND b.actor_kind=? AND b.actor_id=? AND b.saved_sequence>? AND b.saved_sequence<=? AND (${policy.sql}) ORDER BY b.saved_sequence LIMIT ?`,
86
+ [
87
+ this.sql.workspaceId,
88
+ actor.kind,
89
+ actor.id,
90
+ position?.sequence ?? '0',
91
+ high,
92
+ ...policy.values,
93
+ limit + 1,
94
+ ],
95
+ );
96
+ const items: BookmarkPage['items'] = [];
97
+ let bytes = 2048;
98
+ for (const row of rows.slice(0, limit)) {
99
+ const entry = {
100
+ root: {
101
+ id: String(row.item_id),
102
+ kind: String(row.kind) as ItemSummary['kind'],
103
+ title: String(row.title),
104
+ },
105
+ savedAt: String(row.saved_at),
106
+ };
107
+ const size = Buffer.byteLength(JSON.stringify(entry));
108
+ if (items.length && bytes + size > 24576) break;
109
+ items.push(entry);
110
+ bytes += size;
111
+ }
112
+ const hasMore = rows.length > items.length;
113
+ return {
114
+ items,
115
+ hasMore,
116
+ ...(hasMore
117
+ ? {
118
+ nextCursor: codec.encode(binding, {
119
+ sequence: String(rows[items.length - 1]!.saved_sequence),
120
+ watermark: high,
121
+ }),
122
+ }
123
+ : {}),
124
+ watermark: codec.encode(binding, { sequence: high, watermark: high }),
125
+ };
126
+ }
127
+ }