synomem 0.1.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 (130) hide show
  1. package/AGENTS.md +42 -0
  2. package/ARCHITECTURE.md +126 -0
  3. package/CHANGELOG.md +35 -0
  4. package/CODE_OF_CONDUCT.md +29 -0
  5. package/CONTRIBUTING.md +48 -0
  6. package/LICENSE +21 -0
  7. package/README.md +247 -0
  8. package/SECURITY.md +117 -0
  9. package/dist/backend.d.ts +9 -0
  10. package/dist/backend.d.ts.map +1 -0
  11. package/dist/backend.js +60 -0
  12. package/dist/backend.js.map +1 -0
  13. package/dist/cli.d.ts +34 -0
  14. package/dist/cli.d.ts.map +1 -0
  15. package/dist/cli.js +1040 -0
  16. package/dist/cli.js.map +1 -0
  17. package/dist/client.d.ts +168 -0
  18. package/dist/client.d.ts.map +1 -0
  19. package/dist/client.js +1031 -0
  20. package/dist/client.js.map +1 -0
  21. package/dist/config.d.ts +33 -0
  22. package/dist/config.d.ts.map +1 -0
  23. package/dist/config.js +153 -0
  24. package/dist/config.js.map +1 -0
  25. package/dist/credentials.d.ts +36 -0
  26. package/dist/credentials.d.ts.map +1 -0
  27. package/dist/credentials.js +127 -0
  28. package/dist/credentials.js.map +1 -0
  29. package/dist/errors.d.ts +9 -0
  30. package/dist/errors.d.ts.map +1 -0
  31. package/dist/errors.js +60 -0
  32. package/dist/errors.js.map +1 -0
  33. package/dist/fs-utils.d.ts +7 -0
  34. package/dist/fs-utils.d.ts.map +1 -0
  35. package/dist/fs-utils.js +68 -0
  36. package/dist/fs-utils.js.map +1 -0
  37. package/dist/import.d.ts +655 -0
  38. package/dist/import.d.ts.map +1 -0
  39. package/dist/import.js +226 -0
  40. package/dist/import.js.map +1 -0
  41. package/dist/index.d.ts +20 -0
  42. package/dist/index.d.ts.map +1 -0
  43. package/dist/index.js +11 -0
  44. package/dist/index.js.map +1 -0
  45. package/dist/mcp/index.d.ts +14 -0
  46. package/dist/mcp/index.d.ts.map +1 -0
  47. package/dist/mcp/index.js +698 -0
  48. package/dist/mcp/index.js.map +1 -0
  49. package/dist/mcp-server.d.ts +3 -0
  50. package/dist/mcp-server.d.ts.map +1 -0
  51. package/dist/mcp-server.js +45 -0
  52. package/dist/mcp-server.js.map +1 -0
  53. package/dist/oauth.d.ts +27 -0
  54. package/dist/oauth.d.ts.map +1 -0
  55. package/dist/oauth.js +201 -0
  56. package/dist/oauth.js.map +1 -0
  57. package/dist/ports/projections.d.ts +6 -0
  58. package/dist/ports/projections.d.ts.map +1 -0
  59. package/dist/ports/projections.js +2 -0
  60. package/dist/ports/projections.js.map +1 -0
  61. package/dist/ports/repository.d.ts +31 -0
  62. package/dist/ports/repository.d.ts.map +1 -0
  63. package/dist/ports/repository.js +2 -0
  64. package/dist/ports/repository.js.map +1 -0
  65. package/dist/projections.d.ts +24 -0
  66. package/dist/projections.d.ts.map +1 -0
  67. package/dist/projections.js +531 -0
  68. package/dist/projections.js.map +1 -0
  69. package/dist/remote.d.ts +120 -0
  70. package/dist/remote.d.ts.map +1 -0
  71. package/dist/remote.js +286 -0
  72. package/dist/remote.js.map +1 -0
  73. package/dist/schemas.d.ts +901 -0
  74. package/dist/schemas.d.ts.map +1 -0
  75. package/dist/schemas.js +424 -0
  76. package/dist/schemas.js.map +1 -0
  77. package/dist/service.d.ts +126 -0
  78. package/dist/service.d.ts.map +1 -0
  79. package/dist/service.js +2 -0
  80. package/dist/service.js.map +1 -0
  81. package/dist/skill-install.d.ts +34 -0
  82. package/dist/skill-install.d.ts.map +1 -0
  83. package/dist/skill-install.js +256 -0
  84. package/dist/skill-install.js.map +1 -0
  85. package/dist/storage.d.ts +94 -0
  86. package/dist/storage.d.ts.map +1 -0
  87. package/dist/storage.js +1229 -0
  88. package/dist/storage.js.map +1 -0
  89. package/dist/types.d.ts +442 -0
  90. package/dist/types.d.ts.map +1 -0
  91. package/dist/types.js +2 -0
  92. package/dist/types.js.map +1 -0
  93. package/dist/version.d.ts +3 -0
  94. package/dist/version.d.ts.map +1 -0
  95. package/dist/version.js +14 -0
  96. package/dist/version.js.map +1 -0
  97. package/docs/cli.md +180 -0
  98. package/docs/examples.md +70 -0
  99. package/docs/mcp.md +96 -0
  100. package/docs/recovery.md +45 -0
  101. package/docs/releasing.md +61 -0
  102. package/docs/skill.md +93 -0
  103. package/docs/storage-format.md +83 -0
  104. package/openapi/synomem-v1.yaml +674 -0
  105. package/package.json +100 -0
  106. package/skills/synomem/SKILL.md +71 -0
  107. package/skills/synomem/agents/openai.yaml +8 -0
  108. package/skills/synomem/references/examples.md +28 -0
  109. package/src/backend.ts +91 -0
  110. package/src/cli.ts +1623 -0
  111. package/src/client.ts +1261 -0
  112. package/src/config.ts +170 -0
  113. package/src/credentials.ts +194 -0
  114. package/src/errors.ts +64 -0
  115. package/src/fs-utils.ts +85 -0
  116. package/src/import.ts +284 -0
  117. package/src/index.ts +61 -0
  118. package/src/mcp/index.ts +973 -0
  119. package/src/mcp-server.ts +46 -0
  120. package/src/oauth.ts +252 -0
  121. package/src/ports/projections.ts +6 -0
  122. package/src/ports/repository.ts +67 -0
  123. package/src/projections.ts +624 -0
  124. package/src/remote.ts +516 -0
  125. package/src/schemas.ts +457 -0
  126. package/src/service.ts +140 -0
  127. package/src/skill-install.ts +332 -0
  128. package/src/storage.ts +1603 -0
  129. package/src/types.ts +458 -0
  130. package/src/version.ts +16 -0
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgs } from 'node:util';
3
+ import { actorSchema } from './schemas.js';
4
+ import { startMcpServer } from './mcp/index.js';
5
+ import { packageVersion } from './version.js';
6
+
7
+ const version = packageVersion();
8
+
9
+ const { values } = parseArgs({
10
+ options: {
11
+ home: { type: 'string' },
12
+ 'actor-id': { type: 'string' },
13
+ 'actor-kind': { type: 'string' },
14
+ 'actor-name': { type: 'string' },
15
+ help: { type: 'boolean', short: 'h' },
16
+ version: { type: 'boolean', short: 'v' },
17
+ },
18
+ });
19
+
20
+ if (values.help) {
21
+ process.stdout.write(`synomem-mcp ${version}
22
+
23
+ Actor-bound Synomem MCP server (stdio transport)
24
+
25
+ Options:
26
+ --home <path> Storage root
27
+ --actor-id <id> Bound actor ID (or SYNOMEM_ACTOR_ID)
28
+ --actor-kind <kind> human, agent, or system (or SYNOMEM_ACTOR_KIND)
29
+ --actor-name <name> Optional display name (or SYNOMEM_ACTOR_NAME)
30
+ -h, --help Show help
31
+ -v, --version Show version
32
+ `);
33
+ } else if (values.version) {
34
+ process.stdout.write(`${version}\n`);
35
+ } else {
36
+ const actor = actorSchema.parse({
37
+ id: values['actor-id'] ?? process.env.SYNOMEM_ACTOR_ID,
38
+ kind: values['actor-kind'] ?? process.env.SYNOMEM_ACTOR_KIND,
39
+ displayName: values['actor-name'] ?? process.env.SYNOMEM_ACTOR_NAME,
40
+ });
41
+
42
+ await startMcpServer({
43
+ actor,
44
+ ...(values.home ? { home: values.home } : {}),
45
+ });
46
+ }
package/src/oauth.ts ADDED
@@ -0,0 +1,252 @@
1
+ import { createServer } from 'node:http';
2
+ import { spawn } from 'node:child_process';
3
+ import { randomBytes } from 'node:crypto';
4
+ import {
5
+ discoverOAuthServerInfo,
6
+ startAuthorization,
7
+ } from '@modelcontextprotocol/sdk/client/auth.js';
8
+ import { SynomemError } from './errors.js';
9
+ import type { CredentialStore, StoredOAuthCredential } from './credentials.js';
10
+ import type { SynomemCredentialProvider } from './remote.js';
11
+
12
+ const defaultScope = 'synomem:read synomem:write offline_access';
13
+
14
+ interface TokenResponse {
15
+ access_token: string;
16
+ refresh_token?: string;
17
+ expires_in?: number;
18
+ scope?: string;
19
+ }
20
+
21
+ function secureEndpoint(value: string, label: string): URL {
22
+ const url = new URL(value);
23
+ if (url.protocol !== 'https:') {
24
+ throw new SynomemError('AUTH_REQUIRED', `${label} must use HTTPS.`);
25
+ }
26
+ return url;
27
+ }
28
+
29
+ async function tokenRequest(
30
+ endpoint: string,
31
+ parameters: URLSearchParams,
32
+ fetchImplementation: typeof fetch,
33
+ signal?: AbortSignal,
34
+ ): Promise<TokenResponse> {
35
+ let response: Response;
36
+ try {
37
+ response = await fetchImplementation(secureEndpoint(endpoint, 'OAuth token endpoint'), {
38
+ method: 'POST',
39
+ redirect: 'error',
40
+ headers: { 'content-type': 'application/x-www-form-urlencoded', accept: 'application/json' },
41
+ body: parameters,
42
+ ...(signal ? { signal } : {}),
43
+ });
44
+ } catch (error) {
45
+ if (error instanceof SynomemError) throw error;
46
+ throw new SynomemError('REMOTE_UNAVAILABLE', 'The OAuth token endpoint is unavailable.');
47
+ }
48
+ const text = await response.text();
49
+ if (Buffer.byteLength(text) > 64 * 1024) {
50
+ throw new SynomemError('REMOTE_PROTOCOL', 'OAuth token response exceeded the safe limit.');
51
+ }
52
+ let parsed: Partial<TokenResponse>;
53
+ try {
54
+ parsed = JSON.parse(text) as Partial<TokenResponse>;
55
+ } catch {
56
+ throw new SynomemError('AUTH_REQUIRED', 'The OAuth token response was invalid.');
57
+ }
58
+ if (!response.ok || typeof parsed.access_token !== 'string') {
59
+ throw new SynomemError('AUTH_REQUIRED', 'OAuth authorization was rejected.');
60
+ }
61
+ return parsed as TokenResponse;
62
+ }
63
+
64
+ function launchBrowser(url: URL): void {
65
+ const command = process.platform === 'darwin' ? 'open' : 'xdg-open';
66
+ if (process.platform !== 'darwin' && process.platform !== 'linux') {
67
+ throw new SynomemError('CONFIG_INVALID', `Open this URL in a browser: ${url.href}`);
68
+ }
69
+ const child = spawn(command, [url.href], { detached: true, stdio: 'ignore' });
70
+ child.once('error', () => undefined);
71
+ child.unref();
72
+ }
73
+
74
+ async function authorizationCode(
75
+ authorizationUrl: URL,
76
+ state: string,
77
+ callbackPort: number,
78
+ openBrowser: (url: URL) => void,
79
+ timeoutMs: number,
80
+ ): Promise<string> {
81
+ return await new Promise((resolve, reject) => {
82
+ const server = createServer((request, response) => {
83
+ const url = new URL(request.url ?? '/', `http://127.0.0.1:${callbackPort}`);
84
+ if (url.pathname !== '/callback') {
85
+ response.writeHead(404).end('Not found');
86
+ return;
87
+ }
88
+ const code = url.searchParams.get('code');
89
+ const returnedState = url.searchParams.get('state');
90
+ const oauthError = url.searchParams.get('error');
91
+ if (oauthError || !code || returnedState !== state) {
92
+ response.writeHead(400, { 'content-type': 'text/plain; charset=utf-8' });
93
+ response.end('Synomem authorization failed. Return to your terminal.');
94
+ finish(new SynomemError('AUTH_REQUIRED', 'OAuth callback validation failed.'));
95
+ return;
96
+ }
97
+ response.writeHead(200, {
98
+ 'content-type': 'text/plain; charset=utf-8',
99
+ 'cache-control': 'no-store',
100
+ });
101
+ response.end('Synomem authorization complete. You may close this window.');
102
+ finish(undefined, code);
103
+ });
104
+ const timer = setTimeout(
105
+ () => finish(new SynomemError('AUTH_REQUIRED', 'OAuth login timed out.')),
106
+ timeoutMs,
107
+ );
108
+ const finish = (error?: Error, code?: string) => {
109
+ clearTimeout(timer);
110
+ server.close();
111
+ if (error) reject(error);
112
+ else if (code) resolve(code);
113
+ };
114
+ server.once('error', (error) => finish(error));
115
+ server.listen(callbackPort, '127.0.0.1', () => {
116
+ try {
117
+ openBrowser(authorizationUrl);
118
+ } catch (error) {
119
+ finish(error instanceof Error ? error : new Error(String(error)));
120
+ }
121
+ });
122
+ });
123
+ }
124
+
125
+ export interface OAuthLoginOptions {
126
+ baseUrl: string;
127
+ clientId: string;
128
+ credentialReference: string;
129
+ credentialStore: CredentialStore;
130
+ scope?: string;
131
+ callbackPort?: number;
132
+ fetch?: typeof fetch;
133
+ openBrowser?: (url: URL) => void;
134
+ timeoutMs?: number;
135
+ }
136
+
137
+ export async function loginWithOAuth(options: OAuthLoginOptions): Promise<void> {
138
+ const fetchImplementation = options.fetch ?? fetch;
139
+ const mcpUrl = new URL('/mcp', options.baseUrl);
140
+ const discovered = await discoverOAuthServerInfo(mcpUrl, { fetchFn: fetchImplementation });
141
+ const metadata = discovered.authorizationServerMetadata;
142
+ if (!metadata?.authorization_endpoint || !metadata.token_endpoint) {
143
+ throw new SynomemError('AUTH_REQUIRED', 'OAuth server discovery is incomplete.');
144
+ }
145
+ secureEndpoint(metadata.authorization_endpoint, 'OAuth authorization endpoint');
146
+ secureEndpoint(metadata.token_endpoint, 'OAuth token endpoint');
147
+ if (!metadata.code_challenge_methods_supported?.includes('S256')) {
148
+ throw new SynomemError('AUTH_REQUIRED', 'The OAuth server must support PKCE S256.');
149
+ }
150
+ const callbackPort = options.callbackPort ?? 43_817;
151
+ const redirectUrl = new URL(`http://127.0.0.1:${callbackPort}/callback`);
152
+ const state = randomBytes(32).toString('base64url');
153
+ const resource = new URL(discovered.resourceMetadata?.resource ?? mcpUrl.href);
154
+ const scope = options.scope ?? defaultScope;
155
+ const started = await startAuthorization(discovered.authorizationServerUrl, {
156
+ metadata,
157
+ clientInformation: { client_id: options.clientId },
158
+ redirectUrl,
159
+ scope,
160
+ state,
161
+ resource,
162
+ });
163
+ const code = await authorizationCode(
164
+ started.authorizationUrl,
165
+ state,
166
+ callbackPort,
167
+ options.openBrowser ?? launchBrowser,
168
+ options.timeoutMs ?? 5 * 60_000,
169
+ );
170
+ const tokens = await tokenRequest(
171
+ metadata.token_endpoint,
172
+ new URLSearchParams({
173
+ grant_type: 'authorization_code',
174
+ client_id: options.clientId,
175
+ code,
176
+ code_verifier: started.codeVerifier,
177
+ redirect_uri: redirectUrl.href,
178
+ resource: resource.href,
179
+ }),
180
+ fetchImplementation,
181
+ );
182
+ await options.credentialStore.set(options.credentialReference, {
183
+ accessToken: tokens.access_token,
184
+ ...(tokens.refresh_token ? { refreshToken: tokens.refresh_token } : {}),
185
+ ...(tokens.expires_in
186
+ ? { expiresAt: Date.now() + Math.max(0, tokens.expires_in - 30) * 1_000 }
187
+ : {}),
188
+ tokenEndpoint: metadata.token_endpoint,
189
+ clientId: options.clientId,
190
+ resource: resource.href,
191
+ scope: tokens.scope ?? scope,
192
+ });
193
+ }
194
+
195
+ export class StoredCredentialProvider implements SynomemCredentialProvider {
196
+ private refresh?: Promise<string | undefined>;
197
+ private credential?: StoredOAuthCredential;
198
+ private loaded = false;
199
+
200
+ constructor(
201
+ private readonly reference: string,
202
+ private readonly store: CredentialStore,
203
+ private readonly env: NodeJS.ProcessEnv = process.env,
204
+ private readonly fetchImplementation: typeof fetch = fetch,
205
+ ) {}
206
+
207
+ async getAccessToken(signal?: AbortSignal): Promise<string | undefined> {
208
+ if (this.env.SYNOMEM_ACCESS_TOKEN) return this.env.SYNOMEM_ACCESS_TOKEN;
209
+ if (!this.loaded) {
210
+ this.credential = await this.store.get(this.reference);
211
+ this.loaded = true;
212
+ }
213
+ const credential = this.credential;
214
+ if (!credential) return undefined;
215
+ if (!credential.expiresAt || credential.expiresAt > Date.now()) return credential.accessToken;
216
+ if (!credential.refreshToken) return undefined;
217
+ this.refresh ??= this.refreshCredential(credential, signal).finally(() => {
218
+ this.refresh = undefined;
219
+ });
220
+ return await this.refresh;
221
+ }
222
+
223
+ private async refreshCredential(
224
+ credential: StoredOAuthCredential,
225
+ signal?: AbortSignal,
226
+ ): Promise<string> {
227
+ const tokens = await tokenRequest(
228
+ credential.tokenEndpoint,
229
+ new URLSearchParams({
230
+ grant_type: 'refresh_token',
231
+ client_id: credential.clientId,
232
+ refresh_token: credential.refreshToken!,
233
+ resource: credential.resource,
234
+ scope: credential.scope,
235
+ }),
236
+ this.fetchImplementation,
237
+ signal,
238
+ );
239
+ const updated: StoredOAuthCredential = {
240
+ ...credential,
241
+ accessToken: tokens.access_token,
242
+ refreshToken: tokens.refresh_token ?? credential.refreshToken,
243
+ ...(tokens.expires_in
244
+ ? { expiresAt: Date.now() + Math.max(0, tokens.expires_in - 30) * 1_000 }
245
+ : { expiresAt: undefined }),
246
+ scope: tokens.scope ?? credential.scope,
247
+ };
248
+ await this.store.set(this.reference, updated);
249
+ this.credential = updated;
250
+ return updated.accessToken;
251
+ }
252
+ }
@@ -0,0 +1,6 @@
1
+ import type { ProjectionRebuildResult } from '../service.js';
2
+ import type { Awaitable } from './repository.js';
3
+
4
+ export interface ProjectionWriter {
5
+ syncAgent(agentId: string): Awaitable<ProjectionRebuildResult>;
6
+ }
@@ -0,0 +1,67 @@
1
+ import type {
2
+ ActorIdentity,
3
+ AgentProfile,
4
+ ChangePage,
5
+ ItemListInput,
6
+ ItemSummary,
7
+ KudosListInput,
8
+ KudosSummary,
9
+ SynomemConfig,
10
+ SynomemEvent,
11
+ Page,
12
+ RecordKind,
13
+ } from '../types.js';
14
+
15
+ /**
16
+ * Operations required by authoritative domain behavior.
17
+ *
18
+ * This is the first extraction seam around the synchronous SQLite implementation. A later R2 step
19
+ * replaces callback transactions with asynchronous atomic commit operations suitable for both
20
+ * local SQLite and remote service implementations.
21
+ */
22
+ export interface SynomemRepository {
23
+ readonly config: SynomemConfig;
24
+
25
+ init(): Awaitable<void>;
26
+ close(): Awaitable<void>;
27
+ assertEventCompatibility(): Awaitable<void>;
28
+ transaction<T>(operation: () => Awaitable<T>): Promise<T>;
29
+
30
+ insertEvent(event: SynomemEvent): Awaitable<void>;
31
+ nextAggregateVersion(aggregateId: string): Awaitable<number>;
32
+ getEventByIdempotency(
33
+ actorKind: ActorIdentity['kind'],
34
+ actorId: string,
35
+ key: string,
36
+ ): Awaitable<SynomemEvent | undefined>;
37
+ getReadableSynomemEvents(kudosId: string): Awaitable<SynomemEvent[]>;
38
+ getReadableItemEvents(id: string): Awaitable<SynomemEvent[]>;
39
+
40
+ insertAgent(profile: AgentProfile): Awaitable<void>;
41
+ updateAgent(profile: AgentProfile, updatedAt: string): Awaitable<void>;
42
+ getAgent(idOrAlias: string): Awaitable<AgentProfile | undefined>;
43
+ listAgents(): Awaitable<AgentProfile[]>;
44
+
45
+ listKudosSummaries(
46
+ input: Required<Pick<KudosListInput, 'limit' | 'offset'>> & KudosListInput,
47
+ viewer: ActorIdentity,
48
+ ): Awaitable<Page<KudosSummary>>;
49
+ listKudosChanges(
50
+ after: string | undefined,
51
+ limit: number,
52
+ viewer: ActorIdentity,
53
+ ): Awaitable<ChangePage>;
54
+ listItemSummaries(
55
+ input: Required<Pick<ItemListInput, 'limit' | 'offset'>> & ItemListInput,
56
+ viewer: ActorIdentity,
57
+ ): Awaitable<Page<ItemSummary>>;
58
+ listItemChanges(
59
+ after: string | undefined,
60
+ limit: number,
61
+ viewer: ActorIdentity,
62
+ kinds?: RecordKind[],
63
+ ): Awaitable<ChangePage>;
64
+ getItemSummary(id: string): Awaitable<ItemSummary | undefined>;
65
+ }
66
+
67
+ export type Awaitable<T> = T | Promise<T>;