synomem 0.8.0 → 0.9.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 (93) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +47 -68
  3. package/dist/backend.d.ts +18 -6
  4. package/dist/backend.d.ts.map +1 -1
  5. package/dist/backend.js +55 -41
  6. package/dist/backend.js.map +1 -1
  7. package/dist/cli.d.ts +20 -33
  8. package/dist/cli.d.ts.map +1 -1
  9. package/dist/cli.js +1391 -1321
  10. package/dist/cli.js.map +1 -1
  11. package/dist/configure.d.ts +12 -46
  12. package/dist/configure.d.ts.map +1 -1
  13. package/dist/configure.js +51 -192
  14. package/dist/configure.js.map +1 -1
  15. package/dist/credentials.d.ts +73 -33
  16. package/dist/credentials.d.ts.map +1 -1
  17. package/dist/credentials.js +167 -43
  18. package/dist/credentials.js.map +1 -1
  19. package/dist/discover.d.ts +8 -35
  20. package/dist/discover.d.ts.map +1 -1
  21. package/dist/discover.js +42 -38
  22. package/dist/discover.js.map +1 -1
  23. package/dist/errors.d.ts +1 -1
  24. package/dist/errors.d.ts.map +1 -1
  25. package/dist/errors.js +4 -0
  26. package/dist/errors.js.map +1 -1
  27. package/dist/import.d.ts +3 -0
  28. package/dist/import.d.ts.map +1 -1
  29. package/dist/import.js +3 -0
  30. package/dist/import.js.map +1 -1
  31. package/dist/index.d.ts +10 -8
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +6 -5
  34. package/dist/index.js.map +1 -1
  35. package/dist/mcp/index.d.ts +18 -18
  36. package/dist/mcp/index.d.ts.map +1 -1
  37. package/dist/mcp/index.js +402 -231
  38. package/dist/mcp/index.js.map +1 -1
  39. package/dist/mcp-server.d.ts +5 -1
  40. package/dist/mcp-server.d.ts.map +1 -1
  41. package/dist/mcp-server.js +27 -105
  42. package/dist/mcp-server.js.map +1 -1
  43. package/dist/oauth.d.ts +31 -33
  44. package/dist/oauth.d.ts.map +1 -1
  45. package/dist/oauth.js +178 -125
  46. package/dist/oauth.js.map +1 -1
  47. package/dist/profiles.d.ts +243 -0
  48. package/dist/profiles.d.ts.map +1 -0
  49. package/dist/profiles.js +465 -0
  50. package/dist/profiles.js.map +1 -0
  51. package/dist/project.d.ts +8 -39
  52. package/dist/project.d.ts.map +1 -1
  53. package/dist/project.js +36 -94
  54. package/dist/project.js.map +1 -1
  55. package/dist/remote.d.ts +24 -17
  56. package/dist/remote.d.ts.map +1 -1
  57. package/dist/remote.js +54 -52
  58. package/dist/remote.js.map +1 -1
  59. package/dist/resolvers.d.ts +47 -0
  60. package/dist/resolvers.d.ts.map +1 -0
  61. package/dist/resolvers.js +255 -0
  62. package/dist/resolvers.js.map +1 -0
  63. package/dist/service.d.ts +3 -2
  64. package/dist/service.d.ts.map +1 -1
  65. package/dist/skill-install.d.ts +4 -6
  66. package/dist/skill-install.d.ts.map +1 -1
  67. package/dist/skill-install.js +13 -12
  68. package/dist/skill-install.js.map +1 -1
  69. package/dist/types.d.ts +45 -15
  70. package/dist/types.d.ts.map +1 -1
  71. package/docs/cli.md +173 -196
  72. package/docs/mcp.md +69 -65
  73. package/package.json +1 -1
  74. package/skills/synomem/SKILL.md +29 -26
  75. package/skills/synomem/references/examples.md +13 -11
  76. package/src/backend.ts +66 -64
  77. package/src/cli.ts +2131 -2222
  78. package/src/configure.ts +62 -241
  79. package/src/credentials.ts +208 -84
  80. package/src/discover.ts +53 -59
  81. package/src/errors.ts +4 -0
  82. package/src/import.ts +5 -0
  83. package/src/index.ts +15 -19
  84. package/src/mcp/index.ts +473 -277
  85. package/src/mcp-server.ts +32 -114
  86. package/src/oauth.ts +229 -130
  87. package/src/profiles.ts +644 -0
  88. package/src/project.ts +42 -108
  89. package/src/remote.ts +69 -63
  90. package/src/resolvers.ts +299 -0
  91. package/src/service.ts +2 -7
  92. package/src/skill-install.ts +17 -18
  93. package/src/types.ts +40 -15
package/src/configure.ts CHANGED
@@ -1,276 +1,97 @@
1
1
  /**
2
- * `synomem config` — the onboarding wizard, and its deterministic equivalent.
2
+ * Setup helpers shared by `connection` and `setup`.
3
3
  *
4
4
  * Two rules shape this file. Every interactive step has a non-interactive
5
- * counterpart, so an agent can configure a machine without a terminal. And the
6
- * wizard refuses to run at all when nobody is there to answer: a setup program
7
- * that blocks forever on a pipe is worse than one that says which flags it
8
- * needs.
5
+ * counterpart, so an agent can configure a machine without a terminal. And a
6
+ * secret is never accepted as a command-line argument: an argument is kept by
7
+ * the shell history and visible in the process list, so access keys arrive on
8
+ * stdin.
9
9
  */
10
- import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
11
- import { join } from 'node:path';
12
- import { cloudApiUrl } from './cloud.js';
13
10
  import { SynomemError } from './errors.js';
14
- import { resolveHome } from './config.js';
15
- import { ask, askSecret, confirm, select, type PromptIo } from './prompt.js';
16
-
17
- export type BackendChoice = 'local' | 'remote';
18
- export type AuthChoice = 'browser' | 'access-key';
19
- export type CredentialStoreChoice = 'auto' | 'keychain' | 'file' | 'environment';
20
-
21
- export interface ConfigInitOptions {
22
- backend?: BackendChoice;
23
- home?: string;
24
- auth?: AuthChoice;
25
- workspace?: string;
26
- accessToken?: string;
27
- credentialStore?: CredentialStoreChoice;
28
- yes?: boolean;
29
- }
30
-
31
- export interface ConfigPlan {
32
- backend: BackendChoice;
33
- home: string;
34
- serviceUrl?: string;
35
- auth?: AuthChoice;
36
- workspaceId?: string;
37
- credentialStore?: CredentialStoreChoice;
38
- }
11
+ import { askSecret, type PromptIo } from './prompt.js';
12
+ import type { CredentialBackendKind } from './credentials.js';
39
13
 
40
14
  /**
41
- * Where a credential can actually be kept on this platform.
15
+ * Where a credential can be kept on this platform, recommended first.
42
16
  *
43
- * Reported rather than assumed: offering macOS Keychain on Linux, or a Secret
44
- * Service that is not running, produces a setup that appears to succeed and
45
- * then cannot read its own credential back.
17
+ * Reported rather than assumed: offering the macOS Keychain on Linux, or a
18
+ * Secret Service that is not running, produces a setup that appears to succeed
19
+ * and then cannot read its own credential back.
46
20
  */
47
21
  export function credentialStoreChoices(
48
22
  platform: NodeJS.Platform = process.platform,
49
- ): Array<{ value: CredentialStoreChoice; label: string; detail?: string }> {
23
+ ): Array<{ value: CredentialBackendKind; label: string; detail?: string }> {
50
24
  const native =
51
25
  platform === 'darwin'
52
- ? { value: 'keychain' as const, label: 'macOS Keychain', detail: 'Recommended.' }
53
- : platform === 'win32'
54
- ? /*
55
- * Windows has no native option here yet, so the restricted file is
56
- * the recommendation rather than Credential Manager. Offering a
57
- * store the credential layer cannot actually read back would fail
58
- * at the first use, after the wizard had already told the person
59
- * their credential was safely stored.
60
- */
61
- {
62
- value: 'file' as const,
63
- label: 'A file in the Synomem home',
64
- // Not called "restricted" on Windows: `chmod` there only toggles
65
- // the read-only bit, so the protection is the user profile
66
- // directory's access control, not a mode of 0600.
67
- detail: 'Protected by your user profile. Recommended until Credential Manager lands.',
68
- }
69
- : {
70
- value: 'keychain' as const,
71
- label: 'Secret Service (libsecret)',
72
- detail: 'Recommended where a desktop keyring is running.',
73
- };
26
+ ? [{ value: 'keychain' as const, label: 'macOS Keychain', detail: 'Recommended.' }]
27
+ : platform === 'linux'
28
+ ? [
29
+ {
30
+ value: 'keychain' as const,
31
+ label: 'Secret Service (libsecret)',
32
+ detail: 'Recommended where a desktop keyring is running.',
33
+ },
34
+ ]
35
+ : [];
74
36
  return [
75
- native,
76
- ...(native.value === 'file'
77
- ? []
78
- : [
79
- {
80
- value: 'file' as const,
81
- label: 'A restricted file in the Synomem home',
82
- detail: 'Mode 0600. Use on headless machines with no keyring.',
83
- },
84
- ]),
37
+ ...native,
38
+ {
39
+ value: 'file',
40
+ label: 'A restricted file in the Synomem home',
41
+ detail: 'Mode 0600. Use on headless machines with no keyring.',
42
+ },
85
43
  {
86
44
  value: 'environment',
87
- label: 'Print environment-variable instructions',
88
- detail: 'Nothing is stored. Synomem never edits your shell profile.',
45
+ label: 'SYNOMEM_ACCESS_TOKEN (access keys only)',
46
+ detail: 'Nothing is stored; the key is read from the environment each run.',
89
47
  },
90
48
  ];
91
49
  }
92
50
 
93
- /** Refuses to guess when there is nobody to ask. */
94
- export function assertInteractive(io: PromptIo): void {
95
- if (io.interactive) return;
51
+ /** The default store: the OS keychain where one exists, otherwise refused. */
52
+ export function defaultCredentialBackend(
53
+ platform: NodeJS.Platform = process.platform,
54
+ ): CredentialBackendKind {
55
+ if (platform === 'darwin' || platform === 'linux') return 'keychain';
96
56
  throw new SynomemError(
97
- 'INVALID_INPUT',
98
- [
99
- 'synomem config needs an interactive terminal.',
100
- '',
101
- 'For automation, use the deterministic form instead:',
102
- '',
103
- ' synomem config init --backend local --yes',
104
- '',
105
- ' synomem config init --backend remote --auth access-key \\',
106
- ' --workspace <workspace-id> --access-token-stdin --yes',
107
- '',
108
- 'Pipe the token in rather than passing it as an argument: an argument is',
109
- 'kept by both the shell history and the process list.',
110
- ].join('\n'),
57
+ 'CONFIG_INVALID',
58
+ 'This platform has no supported credential store. Pass --store file (a mode-0600 file), or --store environment for an access key.',
111
59
  );
112
60
  }
113
61
 
114
- /**
115
- * Stores an access key in a restricted file.
116
- *
117
- * Separate from `config.json` so a configuration file can be read, copied or
118
- * pasted into an issue without carrying a secret with it.
119
- */
120
- function credentialFilePath(home: string): string {
121
- return join(home, 'credentials', 'installation.json');
62
+ export function parseCredentialBackend(
63
+ value: string | undefined,
64
+ platform: NodeJS.Platform = process.platform,
65
+ ): CredentialBackendKind {
66
+ if (value === undefined) return defaultCredentialBackend(platform);
67
+ if (value === 'keychain' || value === 'file' || value === 'environment') return value;
68
+ throw new SynomemError('INVALID_INPUT', '--store must be keychain, file, or environment.');
122
69
  }
123
70
 
124
- export function writeCredentialFile(home: string, token: string): string {
125
- const directory = join(home, 'credentials');
126
- mkdirSync(directory, { recursive: true, mode: 0o700 });
127
- chmodSync(directory, 0o700);
128
- const path = credentialFilePath(home);
129
- writeFileSync(path, `${JSON.stringify({ accessToken: token }, null, 2)}\n`, { mode: 0o600 });
130
- chmodSync(path, 0o600);
131
- return path;
71
+ /** Refuses to guess when there is nobody to ask. */
72
+ export function assertInteractive(io: PromptIo, alternative: string): void {
73
+ if (io.interactive) return;
74
+ throw new SynomemError(
75
+ 'INVALID_INPUT',
76
+ `This step needs an interactive terminal. For automation: ${alternative}`,
77
+ );
132
78
  }
133
79
 
134
- /**
135
- * Reads back what `writeCredentialFile` wrote.
136
- *
137
- * Without this, choosing the restricted-file store at `synomem config` wrote
138
- * a credential nothing ever read again: every later command still asked the
139
- * OS keychain, found nothing there, and failed with AUTH_REQUIRED even though
140
- * the key was sitting right there on disk.
141
- */
142
- export function readCredentialFile(home: string): string | undefined {
143
- const path = credentialFilePath(home);
144
- if (!existsSync(path)) return undefined;
145
- try {
146
- const parsed = JSON.parse(readFileSync(path, 'utf8')) as { accessToken?: unknown };
147
- return typeof parsed.accessToken === 'string' ? parsed.accessToken : undefined;
148
- } catch {
149
- return undefined;
80
+ /** Reads an access key from stdin (or a prompt), never from an argument. */
81
+ export async function readAccessKey(io: PromptIo): Promise<string> {
82
+ const key = await askSecret(io, 'Access key');
83
+ if (!key) throw new SynomemError('INVALID_INPUT', 'No access key was provided on stdin.');
84
+ if (!key.startsWith('syn_')) {
85
+ throw new SynomemError(
86
+ 'INVALID_INPUT',
87
+ 'That does not look like a Synomem access key (syn_…).',
88
+ );
150
89
  }
90
+ return key;
151
91
  }
152
92
 
153
93
  /** A key's identifying prefix. Never the key. */
154
94
  export function credentialFingerprint(token: string): string {
155
95
  const head = token.slice(0, 12);
156
- return `${head}${token.length > 12 ? '\u2026' : ''}`;
157
- }
158
-
159
- export function environmentInstructions(token: string): string {
160
- return [
161
- 'Set this value for the current shell:',
162
- '',
163
- ` export SYNOMEM_ACCESS_TOKEN='${token}'`,
164
- '',
165
- 'To persist it, add that to a secret-aware shell configuration or to your',
166
- 'agent runtime environment. Synomem will not edit your shell profile for',
167
- 'you: silently rewriting a dotfile is not a thing a setup program should do.',
168
- ].join('\n');
169
- }
170
-
171
- /** The interactive flow, returning the plan it settled on. */
172
- export async function runConfigWizard(
173
- io: PromptIo,
174
- options: { home?: string; env?: NodeJS.ProcessEnv } = {},
175
- ): Promise<ConfigPlan> {
176
- assertInteractive(io);
177
- const env = options.env ?? process.env;
178
-
179
- io.output.write(
180
- [
181
- '',
182
- 'Welcome to Synomem',
183
- '',
184
- 'Synomem gives agents durable notes, messages, tasks, todos, kudos,',
185
- 'and shared coordination.',
186
- '',
187
- ].join('\n'),
188
- );
189
-
190
- const backend = await select<BackendChoice>(io, 'Where should Synomem store canonical state?', [
191
- {
192
- value: 'local',
193
- label: 'Local \u2014 SQLite on this machine',
194
- detail: 'Nothing is uploaded. One implicit workspace.',
195
- },
196
- {
197
- value: 'remote',
198
- label: 'Synomem Cloud \u2014 shared across machines and agents',
199
- detail: 'Organizations, workspaces, roles and administration.',
200
- },
201
- ]);
202
-
203
- const home = await ask(io, 'Where should Synomem store its data?', resolveHome(options.home));
204
-
205
- if (backend === 'local') {
206
- return { backend, home };
207
- }
208
-
209
- const serviceUrl = cloudApiUrl(env);
210
- io.output.write(`\nConnecting to Synomem Cloud at ${serviceUrl}\n`);
211
-
212
- const auth = await select<AuthChoice>(io, 'How would you like to sign in?', [
213
- {
214
- value: 'browser',
215
- label: 'Sign in with your browser',
216
- detail: 'Opens the authorization server and returns through a loopback callback.',
217
- },
218
- {
219
- value: 'access-key',
220
- label: 'Use an access key',
221
- detail: 'Create one at https://portal.synomem.ai/access-keys/new',
222
- },
223
- ]);
224
-
225
- /*
226
- * Deliberately no workspace prompt here.
227
- *
228
- * A hosted workspace ID looks like `ws-04psqx2rkt8ttft7a1t2z69r97`, and the
229
- * credential authorized in the next step already knows the answer -- an
230
- * access key can list every workspace its organization has, and a browser
231
- * sign-in can list the ones the account belongs to across every
232
- * organization. Asking first means asking a person to go and look something
233
- * up that we are about to be told.
234
- */
235
-
236
- const credentialStore =
237
- auth === 'access-key'
238
- ? await select<CredentialStoreChoice>(
239
- io,
240
- 'Where should Synomem store this credential?',
241
- credentialStoreChoices(),
242
- )
243
- : 'auto';
244
-
245
- return { backend, home, serviceUrl, auth, credentialStore };
246
- }
247
-
248
- /** Reads an access key without ever accepting it as an argument. */
249
- export async function readAccessToken(io: PromptIo): Promise<string> {
250
- const token = await askSecret(io, 'Access key');
251
- if (!token) throw new SynomemError('INVALID_INPUT', 'No access key was provided.');
252
- return token;
253
- }
254
-
255
- export async function confirmPlan(io: PromptIo, plan: ConfigPlan): Promise<boolean> {
256
- io.output.write(
257
- [
258
- '',
259
- 'Synomem will be configured as:',
260
- '',
261
- ` Backend: ${plan.backend === 'local' ? 'Local SQLite' : 'Synomem Cloud'}`,
262
- ` Home: ${plan.home}`,
263
- ...(plan.serviceUrl ? [` Service: ${plan.serviceUrl}`] : []),
264
- ...(plan.workspaceId ? [` Workspace: ${plan.workspaceId}`] : []),
265
- ...(plan.credentialStore && plan.credentialStore !== 'auto'
266
- ? [` Credential: ${plan.credentialStore}`]
267
- : []),
268
- '',
269
- ].join('\n'),
270
- );
271
- return await confirm(io, 'Apply this?');
272
- }
273
-
274
- export function homeExists(home: string): boolean {
275
- return existsSync(home);
96
+ return `${head}${token.length > 12 ? '…' : ''}`;
276
97
  }