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
package/dist/cli.js ADDED
@@ -0,0 +1,1040 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, readFileSync, realpathSync } from 'node:fs';
3
+ import { spawn } from 'node:child_process';
4
+ import { join, resolve } from 'node:path';
5
+ import { pathToFileURL } from 'node:url';
6
+ import { Command, CommanderError, Option } from 'commander';
7
+ import { configuredServiceFactory, readSynomemConfig, writeSynomemBackend } from './backend.js';
8
+ import { credentialReference, OsCredentialStore } from './credentials.js';
9
+ import { asSynomemError, SynomemError } from './errors.js';
10
+ import { atomicWriteFile } from './fs-utils.js';
11
+ import { startMcpServer } from './mcp/index.js';
12
+ import { loginWithOAuth, StoredCredentialProvider } from './oauth.js';
13
+ import { RemoteSynomemService } from './remote.js';
14
+ import { createLocalImportBundle, RemoteImportClient, } from './import.js';
15
+ import { formatSkillResult, installSkill, skillRuntimeNames, skillStatus, uninstallSkill, } from './skill-install.js';
16
+ import { packageVersion } from './version.js';
17
+ const defaultIo = {
18
+ stdout: (text) => process.stdout.write(text),
19
+ stderr: (text) => process.stderr.write(text),
20
+ };
21
+ const cliExitCodes = new WeakMap();
22
+ function collect(value, previous) {
23
+ return [...previous, value];
24
+ }
25
+ function skillRuntimes(values) {
26
+ const normalized = values.map((value) => (value === 'grokbot' ? 'grok' : value));
27
+ const invalid = normalized.find((value) => value !== 'all' && !skillRuntimeNames.some((runtime) => runtime === value));
28
+ if (invalid) {
29
+ throw new SynomemError('INVALID_INPUT', `Unsupported skill runtime: ${invalid}.`);
30
+ }
31
+ if (!normalized.length || normalized.includes('all'))
32
+ return undefined;
33
+ return normalized;
34
+ }
35
+ const skillRuntimeHelp = `${skillRuntimeNames.join(', ')}, grokbot (alias for grok), or all`;
36
+ function parseEvidence(value) {
37
+ const separator = value.indexOf(':');
38
+ if (separator < 1 || separator === value.length - 1) {
39
+ throw new SynomemError('INVALID_INPUT', 'Evidence must use kind:value syntax.');
40
+ }
41
+ return {
42
+ kind: value.slice(0, separator),
43
+ value: value.slice(separator + 1),
44
+ };
45
+ }
46
+ function actor(kind, id, displayName) {
47
+ return {
48
+ kind: kind,
49
+ id,
50
+ ...(displayName ? { displayName } : {}),
51
+ };
52
+ }
53
+ function todoDue(options) {
54
+ if (options.dueDate && options.dueAt)
55
+ throw new SynomemError('INVALID_INPUT', 'Use due-date or due-at, not both.');
56
+ if (options.dueDate)
57
+ return { kind: 'date', date: options.dueDate };
58
+ if (options.dueAt) {
59
+ if (!options.timeZone)
60
+ throw new SynomemError('INVALID_INPUT', '--time-zone is required with --due-at.');
61
+ return { kind: 'datetime', datetime: options.dueAt, timeZone: options.timeZone };
62
+ }
63
+ return undefined;
64
+ }
65
+ function exitCode(code) {
66
+ if (code.endsWith('_NOT_FOUND'))
67
+ return 3;
68
+ if (code.endsWith('_FORBIDDEN') ||
69
+ code === 'READ_ONLY' ||
70
+ code === 'AUTH_REQUIRED' ||
71
+ code === 'AUTH_FORBIDDEN')
72
+ return 4;
73
+ if (code.startsWith('DATABASE_') || code === 'UNSUPPORTED_SCHEMA' || code === 'UNSUPPORTED_EVENT')
74
+ return 5;
75
+ if (code.startsWith('REMOTE_') || code === 'RATE_LIMITED')
76
+ return 5;
77
+ if (code === 'INTERNAL_ERROR')
78
+ return 1;
79
+ return 2;
80
+ }
81
+ function lineForSummary(record) {
82
+ const state = record.revocationStatus === 'revoked'
83
+ ? 'revoked'
84
+ : record.status === 'acknowledged'
85
+ ? 'acknowledged'
86
+ : 'new';
87
+ return `${record.id} ${record.createdAt.slice(0, 10)} ${record.recipientAgentId} [${state}] ${record.title}`;
88
+ }
89
+ function lineForItem(item) {
90
+ return `${item.id} ${item.createdAt.slice(0, 10)} ${item.kind.padEnd(5)} [${item.status}] ${item.title}`;
91
+ }
92
+ function itemListInput(options) {
93
+ return {
94
+ ...(Array.isArray(options.kind) && options.kind.length
95
+ ? { kinds: options.kind }
96
+ : {}),
97
+ ...(typeof options.participant === 'string' ? { participantAgentId: options.participant } : {}),
98
+ ...(typeof options.actor === 'string' ? { actorId: options.actor } : {}),
99
+ ...(typeof options.status === 'string' ? { status: options.status } : {}),
100
+ ...(typeof options.tag === 'string' ? { tag: options.tag } : {}),
101
+ ...(typeof options.visibility === 'string'
102
+ ? { visibility: options.visibility }
103
+ : {}),
104
+ ...(typeof options.cursor === 'string' ? { cursor: options.cursor } : {}),
105
+ limit: Number(options.limit ?? 10),
106
+ offset: Number(options.offset ?? 0),
107
+ };
108
+ }
109
+ function showRecord(record) {
110
+ const event = record.event;
111
+ const evidence = event.evidence?.map((item) => ` - ${item.kind}: ${item.value}`).join('\n');
112
+ return [
113
+ event.title,
114
+ `ID: ${event.id}`,
115
+ `Recipient: ${event.recipientDisplayName} (${event.recipientAgentId})`,
116
+ `From: ${event.actor.displayName ?? event.actor.id} (${event.actor.kind}:${event.actor.id})`,
117
+ `Date: ${event.createdAt}`,
118
+ `Visibility: ${event.visibility}`,
119
+ `Status: ${record.status}`,
120
+ `Revocation: ${record.revocationStatus}`,
121
+ event.tags?.length ? `Tags: ${event.tags.join(', ')}` : undefined,
122
+ '',
123
+ event.reason,
124
+ evidence ? `\nEvidence:\n${evidence}` : undefined,
125
+ record.acknowledgment?.note ? `\nAcknowledgment: ${record.acknowledgment.note}` : undefined,
126
+ record.revocation ? `\nRevoked: ${record.revocation.reason}` : undefined,
127
+ ]
128
+ .filter((value) => value !== undefined)
129
+ .join('\n');
130
+ }
131
+ function output(io, json, value, human) {
132
+ io.stdout(json ? `${JSON.stringify(value, null, 2)}\n` : `${human}\n`);
133
+ }
134
+ function globals(command) {
135
+ return command.optsWithGlobals();
136
+ }
137
+ async function withService(serviceFactory, home, configuredActor, operation) {
138
+ const client = serviceFactory({ ...(home ? { home } : {}), actor: configuredActor });
139
+ await client.init();
140
+ try {
141
+ return await operation(client);
142
+ }
143
+ finally {
144
+ await client.close();
145
+ }
146
+ }
147
+ function addListOptions(command) {
148
+ return command
149
+ .option('--recipient <agent>')
150
+ .option('--actor <id>')
151
+ .option('--actor-kind <kind>', 'human, agent, or system')
152
+ .option('--tag <tag>')
153
+ .option('--status <status>', 'acknowledged or unacknowledged')
154
+ .option('--visibility <visibility>', 'private, local, or public')
155
+ .addOption(new Option('--revoked <state>').choices(['include', 'only', 'exclude']).default('include'))
156
+ .option('--from-date <iso>')
157
+ .option('--to-date <iso>')
158
+ .option('--limit <number>', 'maximum results (default 10, maximum 50)', '10')
159
+ .option('--cursor <cursor>', 'opaque cursor returned by the previous page')
160
+ .option('--offset <number>', 'pagination offset', '0');
161
+ }
162
+ function listInput(options) {
163
+ return {
164
+ ...(options.recipient ? { recipientAgentId: options.recipient } : {}),
165
+ ...(options.actor ? { actorId: options.actor } : {}),
166
+ ...(options.actorKind ? { actorKind: options.actorKind } : {}),
167
+ ...(options.tag ? { tag: options.tag } : {}),
168
+ ...(options.status ? { status: options.status } : {}),
169
+ ...(options.visibility
170
+ ? { visibility: options.visibility }
171
+ : {}),
172
+ ...(options.revoked === 'only' ? { revoked: true } : {}),
173
+ ...(options.revoked === 'exclude' ? { revoked: false } : {}),
174
+ ...(options.fromDate ? { from: options.fromDate } : {}),
175
+ ...(options.toDate ? { to: options.toDate } : {}),
176
+ ...(options.cursor ? { cursor: options.cursor } : {}),
177
+ limit: Number(options.limit),
178
+ offset: Number(options.offset),
179
+ };
180
+ }
181
+ export function createCli(io = defaultIo, serviceFactory = configuredServiceFactory, dependencies = {}) {
182
+ const env = dependencies.env ?? process.env;
183
+ const credentialStore = dependencies.credentialStore ?? new OsCredentialStore();
184
+ const oauthLogin = dependencies.oauthLogin ?? loginWithOAuth;
185
+ const verifyRemoteCredential = dependencies.verifyRemoteCredential ??
186
+ (async (options) => {
187
+ const remote = new RemoteSynomemService({
188
+ baseUrl: options.baseUrl,
189
+ workspaceId: options.workspaceId,
190
+ expectedActor: options.actor,
191
+ credentialProvider: new StoredCredentialProvider(options.reference, options.credentialStore, {}),
192
+ });
193
+ try {
194
+ await remote.init();
195
+ }
196
+ finally {
197
+ await remote.close();
198
+ }
199
+ });
200
+ const program = new Command();
201
+ const withClient = (home, configuredActor, operation) => withService(serviceFactory, home, configuredActor, operation);
202
+ cliExitCodes.set(program, 0);
203
+ program
204
+ .name('synomem')
205
+ .description('Local-first communication, memory, recognition, and task infrastructure for agents')
206
+ .version(packageVersion())
207
+ .option('--home <path>', 'storage root (defaults to SYNOMEM_HOME or ~/.agents)')
208
+ .option('--json', 'emit stable machine-readable JSON', false)
209
+ .showSuggestionAfterError()
210
+ .configureOutput({ writeOut: io.stdout, writeErr: io.stderr });
211
+ const remoteCommand = program.command('remote').description('Administer a remote workspace');
212
+ remoteCommand
213
+ .command('import')
214
+ .description('Preview or confirm a one-way import from a local Synomem home')
215
+ .requiredOption('--from-home <path>', 'source local Synomem home')
216
+ .requiredOption('--actor-id <id>', 'bound human administrator actor ID')
217
+ .option('--actor-name <name>', 'expected administrator display name')
218
+ .option('--preview', 'validate and return a short-lived import plan')
219
+ .option('--confirm <plan-id>', 'commit the exact bundle authorized by a preview')
220
+ .action(async (options, command) => {
221
+ const global = globals(command);
222
+ if (Boolean(options.preview) === Boolean(options.confirm)) {
223
+ throw new SynomemError('INVALID_INPUT', 'Choose exactly one of --preview or --confirm.');
224
+ }
225
+ const config = readSynomemConfig(global.home, env);
226
+ if (config?.backend.kind !== 'remote') {
227
+ throw new SynomemError('INVALID_INPUT', 'Select a remote backend before importing.');
228
+ }
229
+ const backend = config.backend;
230
+ const administrator = actor('human', options.actorId, options.actorName);
231
+ const bundle = await (dependencies.createImportBundle ?? createLocalImportBundle)(options.fromHome);
232
+ const result = dependencies.remoteImport
233
+ ? await dependencies.remoteImport({
234
+ baseUrl: backend.baseUrl,
235
+ workspaceId: backend.workspaceId,
236
+ actor: administrator,
237
+ bundle,
238
+ ...(options.confirm ? { planId: options.confirm } : {}),
239
+ })
240
+ : await (async () => {
241
+ const credentialProvider = env.SYNOMEM_ACCESS_TOKEN
242
+ ? { getAccessToken: async () => env.SYNOMEM_ACCESS_TOKEN }
243
+ : new StoredCredentialProvider(credentialReference(backend.baseUrl, backend.workspaceId, administrator), credentialStore, env);
244
+ const importer = new RemoteImportClient({
245
+ baseUrl: backend.baseUrl,
246
+ workspaceId: backend.workspaceId,
247
+ credentialProvider,
248
+ });
249
+ return options.confirm
250
+ ? await importer.confirm(bundle, options.confirm)
251
+ : await importer.preview(bundle);
252
+ })();
253
+ const human = 'planId' in result
254
+ ? [
255
+ `Import preview: ${result.events} events and ${result.profiles} agent profiles.`,
256
+ `Source: ${result.sourceWorkspaceId}`,
257
+ `Target: ${result.targetWorkspaceId}`,
258
+ `Checksum: ${result.checksum}`,
259
+ `Plan expires: ${result.expiresAt}`,
260
+ `Plan ID: ${result.planId}`,
261
+ 'Nothing was imported. Re-run with --confirm <plan-id> to commit this exact snapshot.',
262
+ ].join('\n')
263
+ : `Imported ${result.events} events and ${result.profiles} agent profiles into ${result.targetWorkspaceId}.`;
264
+ output(io, global.json, result, human);
265
+ });
266
+ program
267
+ .command('init')
268
+ .description('Initialize the local Synomem database')
269
+ .action(async (_options, command) => {
270
+ const options = globals(command);
271
+ const persisted = readSynomemConfig(options.home);
272
+ if (persisted?.backend.kind === 'remote') {
273
+ throw new SynomemError('INVALID_INPUT', 'The init command requires a local backend.');
274
+ }
275
+ await withClient(options.home, actor('system', 'cli'), async (client) => {
276
+ const info = await client.info();
277
+ if (info.backend !== 'local') {
278
+ throw new SynomemError('INVALID_INPUT', 'The init command requires a local backend.');
279
+ }
280
+ output(io, options.json, { home: info.home, database: info.databasePath }, `Initialized Synomem at ${info.home}`);
281
+ });
282
+ });
283
+ const backendCommand = program
284
+ .command('backend')
285
+ .description('Inspect or select the canonical backend');
286
+ backendCommand
287
+ .command('show')
288
+ .description('Show backend selection without connecting')
289
+ .action((_options, command) => {
290
+ const global = globals(command);
291
+ const config = readSynomemConfig(global.home);
292
+ const backend = config?.backend ?? { kind: 'local' };
293
+ const human = backend.kind === 'local'
294
+ ? `Backend: local${config ? `\nWorkspace: ${config.workspaceId}` : ' (not initialized)'}`
295
+ : `Backend: remote\nURL: ${backend.baseUrl}\nWorkspace: ${backend.workspaceId}`;
296
+ output(io, global.json, { backend, initialized: config !== undefined }, human);
297
+ });
298
+ backendCommand
299
+ .command('use')
300
+ .description('Select local or remote canonical state')
301
+ .argument('<kind>', 'local or remote')
302
+ .option('--url <url>', 'remote HTTPS origin')
303
+ .option('--workspace <id>', 'remote workspace ID')
304
+ .action((kind, options, command) => {
305
+ const global = globals(command);
306
+ if (kind !== 'local' && kind !== 'remote') {
307
+ throw new SynomemError('INVALID_INPUT', 'Backend kind must be local or remote.');
308
+ }
309
+ if (kind === 'remote' && (!options.url || !options.workspace)) {
310
+ throw new SynomemError('INVALID_INPUT', 'Remote backend selection requires --url and --workspace.');
311
+ }
312
+ const config = writeSynomemBackend(kind === 'local'
313
+ ? { kind: 'local' }
314
+ : { kind: 'remote', baseUrl: options.url, workspaceId: options.workspace }, global.home);
315
+ output(io, global.json, { backend: config.backend }, `Selected ${config.backend.kind} Synomem backend.`);
316
+ });
317
+ const authCommand = program.command('auth').description('Inspect remote authentication');
318
+ authCommand
319
+ .command('status')
320
+ .description('Report token availability without printing it')
321
+ .option('--actor-id <id>', 'bound actor ID (or SYNOMEM_ACTOR_ID)')
322
+ .option('--actor-kind <kind>', 'human, agent, or system', 'agent')
323
+ .action(async (options, command) => {
324
+ const global = globals(command);
325
+ if (env.SYNOMEM_ACCESS_TOKEN) {
326
+ output(io, global.json, { authenticated: true, source: 'environment' }, 'Remote authentication token is available from SYNOMEM_ACCESS_TOKEN.');
327
+ return;
328
+ }
329
+ const config = readSynomemConfig(global.home, env);
330
+ if (config?.backend.kind !== 'remote') {
331
+ throw new SynomemError('INVALID_INPUT', 'Select a remote backend before checking authentication.');
332
+ }
333
+ const actorId = options.actorId ?? env.SYNOMEM_ACTOR_ID;
334
+ if (!actorId)
335
+ throw new SynomemError('INVALID_INPUT', 'Specify --actor-id.');
336
+ const reference = credentialReference(config.backend.baseUrl, config.backend.workspaceId, actor(options.actorKind, actorId));
337
+ const available = Boolean(await credentialStore.get(reference));
338
+ output(io, global.json, { authenticated: available, source: available ? 'os-credential-store' : undefined }, available
339
+ ? 'A remote credential is available in the operating-system credential store.'
340
+ : 'Remote authentication is not configured.');
341
+ });
342
+ authCommand
343
+ .command('login')
344
+ .description('Authorize this actor with OAuth 2.1 authorization code and PKCE')
345
+ .requiredOption('--actor-id <id>', 'bound actor ID')
346
+ .option('--actor-kind <kind>', 'human, agent, or system', 'agent')
347
+ .option('--actor-name <name>', 'expected actor display name')
348
+ .option('--client-id <id>', 'registered public OAuth client ID (or SYNOMEM_OAUTH_CLIENT_ID)')
349
+ .option('--scope <scope>', 'requested OAuth scopes')
350
+ .option('--callback-port <port>', 'loopback callback port', '43817')
351
+ .action(async (options, command) => {
352
+ const global = globals(command);
353
+ const config = readSynomemConfig(global.home, env);
354
+ if (config?.backend.kind !== 'remote') {
355
+ throw new SynomemError('INVALID_INPUT', 'Select a remote backend before login.');
356
+ }
357
+ const configuredActor = actor(options.actorKind, options.actorId, options.actorName);
358
+ const clientId = options.clientId ?? env.SYNOMEM_OAUTH_CLIENT_ID;
359
+ if (!clientId)
360
+ throw new SynomemError('INVALID_INPUT', 'Specify --client-id.');
361
+ const callbackPort = Number(options.callbackPort);
362
+ if (!Number.isSafeInteger(callbackPort) || callbackPort < 1 || callbackPort > 65_535) {
363
+ throw new SynomemError('INVALID_INPUT', '--callback-port must be from 1 through 65535.');
364
+ }
365
+ const reference = credentialReference(config.backend.baseUrl, config.backend.workspaceId, configuredActor);
366
+ await oauthLogin({
367
+ baseUrl: config.backend.baseUrl,
368
+ clientId,
369
+ credentialReference: reference,
370
+ credentialStore,
371
+ callbackPort,
372
+ ...(options.scope ? { scope: options.scope } : {}),
373
+ });
374
+ try {
375
+ await verifyRemoteCredential({
376
+ baseUrl: config.backend.baseUrl,
377
+ workspaceId: config.backend.workspaceId,
378
+ actor: configuredActor,
379
+ reference,
380
+ credentialStore,
381
+ });
382
+ }
383
+ catch (error) {
384
+ await credentialStore.delete(reference);
385
+ throw error;
386
+ }
387
+ output(io, global.json, { authenticated: true, source: 'os-credential-store', actor: configuredActor }, `Authorized ${configuredActor.kind}:${configuredActor.id}; the credential is stored by the operating system.`);
388
+ });
389
+ authCommand
390
+ .command('logout')
391
+ .description('Remove the stored OAuth credential for one actor')
392
+ .requiredOption('--actor-id <id>', 'bound actor ID')
393
+ .option('--actor-kind <kind>', 'human, agent, or system', 'agent')
394
+ .action(async (options, command) => {
395
+ const global = globals(command);
396
+ const config = readSynomemConfig(global.home, env);
397
+ if (config?.backend.kind !== 'remote') {
398
+ throw new SynomemError('INVALID_INPUT', 'Select a remote backend before logout.');
399
+ }
400
+ const configuredActor = actor(options.actorKind, options.actorId);
401
+ const removed = await credentialStore.delete(credentialReference(config.backend.baseUrl, config.backend.workspaceId, configuredActor));
402
+ output(io, global.json, { authenticated: false, removed }, removed
403
+ ? 'Removed the stored Synomem credential.'
404
+ : 'No stored Synomem credential existed.');
405
+ });
406
+ const agentCommand = program
407
+ .command('agent')
408
+ .description('Create and inspect stable agent identities');
409
+ agentCommand
410
+ .command('create <id>')
411
+ .description('Create a stable agent profile')
412
+ .requiredOption('--name <display-name>', 'display name')
413
+ .option('--alias <id>', 'alias (repeatable)', collect, [])
414
+ .option('--description <text>')
415
+ .action(async (id, options, command) => {
416
+ const global = globals(command);
417
+ const profile = await withClient(global.home, actor('system', 'cli'), (client) => client.agents.create({
418
+ id,
419
+ displayName: options.name,
420
+ ...(options.alias.length ? { aliases: options.alias } : {}),
421
+ ...(options.description ? { description: options.description } : {}),
422
+ }));
423
+ output(io, global.json, profile, `Created ${profile.displayName} (${profile.id})`);
424
+ });
425
+ const skillCommand = program
426
+ .command('skill')
427
+ .description('Install and maintain the packaged agent skill');
428
+ skillCommand
429
+ .command('install')
430
+ .description('Plan or install the skill for detected agent runtimes')
431
+ .option('--runtime <runtime>', `${skillRuntimeHelp} (repeatable)`, collect, [])
432
+ .option('--yes', 'apply the displayed plan', false)
433
+ .option('--force', 'replace a conflicting synomem directory', false)
434
+ .option('--link', 'symlink to the packaged skill instead of copying it', false)
435
+ .option('--actor-id <id>', 'print actor-bound MCP registration commands')
436
+ .option('--actor-name <name>', 'display name used in MCP registration commands')
437
+ .action((options, command) => {
438
+ const global = globals(command);
439
+ const result = installSkill({
440
+ runtimes: skillRuntimes(options.runtime),
441
+ apply: options.yes,
442
+ force: options.force,
443
+ link: options.link,
444
+ actorId: options.actorId,
445
+ actorName: options.actorName,
446
+ });
447
+ output(io, global.json, result, formatSkillResult(result, 'install'));
448
+ });
449
+ skillCommand
450
+ .command('status')
451
+ .description('Show installed, stale, missing, or conflicting skill copies')
452
+ .option('--runtime <runtime>', `${skillRuntimeHelp} (repeatable)`, collect, [])
453
+ .option('--actor-id <id>', 'print actor-bound MCP registration commands')
454
+ .option('--actor-name <name>', 'display name used in MCP registration commands')
455
+ .action((options, command) => {
456
+ const global = globals(command);
457
+ const result = skillStatus({
458
+ runtimes: skillRuntimes(options.runtime),
459
+ actorId: options.actorId,
460
+ actorName: options.actorName,
461
+ });
462
+ output(io, global.json, result, formatSkillResult(result, 'status'));
463
+ });
464
+ skillCommand
465
+ .command('uninstall')
466
+ .description('Plan or remove Synomem-owned skill installations')
467
+ .option('--runtime <runtime>', `${skillRuntimeHelp} (repeatable)`, collect, [])
468
+ .option('--yes', 'apply the displayed plan', false)
469
+ .option('--force', 'remove a conflicting synomem directory', false)
470
+ .action((options, command) => {
471
+ const global = globals(command);
472
+ const result = uninstallSkill({
473
+ runtimes: skillRuntimes(options.runtime),
474
+ apply: options.yes,
475
+ force: options.force,
476
+ });
477
+ output(io, global.json, result, formatSkillResult(result, 'uninstall'));
478
+ });
479
+ agentCommand
480
+ .command('list')
481
+ .description('List known agent identities')
482
+ .action(async (_options, command) => {
483
+ const global = globals(command);
484
+ const agents = await withClient(global.home, actor('system', 'cli'), (client) => client.agents.list());
485
+ const human = agents.length
486
+ ? agents
487
+ .map((profile) => `${profile.id} ${profile.displayName}${profile.aliases?.length ? ` aliases: ${profile.aliases.join(', ')}` : ''}`)
488
+ .join('\n')
489
+ : 'No agents configured.';
490
+ output(io, global.json, { agents }, human);
491
+ });
492
+ agentCommand
493
+ .command('show <id>')
494
+ .description('Show one agent profile, resolving aliases')
495
+ .action(async (id, _options, command) => {
496
+ const global = globals(command);
497
+ const profile = await withClient(global.home, actor('system', 'cli'), (client) => client.agents.get(id));
498
+ output(io, global.json, profile, `${profile.displayName} (${profile.id})\n${profile.description ?? 'No description.'}`);
499
+ });
500
+ agentCommand
501
+ .command('update <id>')
502
+ .description('Update an agent profile without rewriting history')
503
+ .option('--name <display-name>')
504
+ .option('--alias <id>', 'replace aliases (repeatable)', collect, [])
505
+ .option('--clear-aliases', 'remove every alias', false)
506
+ .option('--description <text>')
507
+ .action(async (id, options, command) => {
508
+ const global = globals(command);
509
+ const hasAliases = options.clearAliases || options.alias.length > 0;
510
+ const profile = await withClient(global.home, actor('system', 'cli'), (client) => client.agents.update(id, {
511
+ ...(options.name ? { displayName: options.name } : {}),
512
+ ...(hasAliases ? { aliases: options.clearAliases ? [] : options.alias } : {}),
513
+ ...(options.description !== undefined ? { description: options.description } : {}),
514
+ }));
515
+ output(io, global.json, profile, `Updated ${profile.displayName} (${profile.id})`);
516
+ });
517
+ const kudosCommand = program.command('kudos').description('Give and manage agent recognition');
518
+ kudosCommand
519
+ .command('give <recipient>')
520
+ .description('Give specific, evidence-based kudos to an agent')
521
+ .requiredOption('--from <actor-id>', 'stable ID of the giver')
522
+ .requiredOption('--actor-kind <kind>', 'human, agent, or system')
523
+ .option('--actor-name <display-name>')
524
+ .requiredOption('--title <title>')
525
+ .requiredOption('--reason <reason>')
526
+ .option('--tag <tag>', 'tag (repeatable)', collect, [])
527
+ .option('--evidence <kind:value>', 'sanitized evidence (repeatable)', collect, [])
528
+ .option('--visibility <visibility>', 'private, local, or public', 'workspace')
529
+ .option('--idempotency-key <key>')
530
+ .action(async (recipient, options, command) => {
531
+ const global = globals(command);
532
+ const result = await withClient(global.home, actor(options.actorKind, options.from, options.actorName), (client) => client.kudos.give({
533
+ recipientAgentId: recipient,
534
+ title: options.title,
535
+ reason: options.reason,
536
+ visibility: options.visibility,
537
+ ...(options.tag.length ? { tags: options.tag } : {}),
538
+ ...(options.evidence.length ? { evidence: options.evidence.map(parseEvidence) } : {}),
539
+ ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
540
+ }));
541
+ const event = result.record.event;
542
+ output(io, global.json, result, `${result.deduplicated ? 'Found existing' : 'Created'} kudos for ${event.recipientDisplayName}\nTitle: ${event.title}\nDate: ${event.createdAt}\nID: ${event.id}`);
543
+ });
544
+ program
545
+ .command('inbox [agent]')
546
+ .description('Show pending kudos, memos, and todos for an agent')
547
+ .option('--as <agent-id>', 'defaults to the positional agent')
548
+ .option('--limit <number>', 'maximum results (default 10, maximum 50)', '10')
549
+ .option('--cursor <cursor>', 'opaque cursor returned by the previous page')
550
+ .action(async (agentId, options, command) => {
551
+ const global = globals(command);
552
+ const recipient = agentId ?? options.as;
553
+ if (!recipient)
554
+ throw new SynomemError('INVALID_INPUT', 'Specify an agent inbox.');
555
+ const page = await withClient(global.home, actor('agent', options.as ?? recipient), (client) => client.items.list({
556
+ participantAgentId: recipient,
557
+ pending: true,
558
+ limit: Number(options.limit),
559
+ ...(options.cursor ? { cursor: options.cursor } : {}),
560
+ }));
561
+ output(io, global.json, page, page.items.length
562
+ ? `${page.items.map(lineForItem).join('\n')}${page.hasMore ? `\nNext cursor: ${page.nextCursor}` : ''}`
563
+ : 'Inbox is clear.');
564
+ });
565
+ addListOptions(kudosCommand.command('list').description('List and filter kudos')).action(async (options, command) => {
566
+ const global = globals(command);
567
+ const page = await withClient(global.home, actor('human', 'local-cli'), (client) => client.kudos.list(listInput(options)));
568
+ output(io, global.json, page, page.items.length
569
+ ? `${page.items.map(lineForSummary).join('\n')}${page.hasMore ? `\nNext cursor: ${page.nextCursor}` : ''}`
570
+ : 'No kudos found.');
571
+ });
572
+ program
573
+ .command('list')
574
+ .description('List compact summaries across all record types')
575
+ .option('--kind <kind>', 'kudos, memo, note, or todo (repeatable)', collect, [])
576
+ .option('--participant <agent>')
577
+ .option('--actor <id>')
578
+ .option('--tag <tag>')
579
+ .option('--status <status>')
580
+ .option('--visibility <visibility>', 'private, workspace, or public')
581
+ .option('--limit <number>', 'maximum results (default 10, maximum 50)', '10')
582
+ .option('--cursor <cursor>')
583
+ .option('--offset <number>', 'deprecated offset', '0')
584
+ .action(async (options, command) => {
585
+ const global = globals(command);
586
+ const page = await withClient(global.home, actor('human', 'local-cli'), (client) => client.items.list(itemListInput(options)));
587
+ output(io, global.json, page, page.items.length
588
+ ? `${page.items.map(lineForItem).join('\n')}${page.hasMore ? `\nNext cursor: ${page.nextCursor}` : ''}`
589
+ : 'No items found.');
590
+ });
591
+ program
592
+ .command('changes')
593
+ .description('List compact changes across all record types after an opaque watermark')
594
+ .option('--kind <kind>', 'filter by kind (repeatable)', collect, [])
595
+ .option('--after <watermark>', 'watermark or change cursor from a previous response')
596
+ .option('--limit <number>', 'maximum changes (default 20, maximum 100)', '20')
597
+ .action(async (options, command) => {
598
+ const global = globals(command);
599
+ const page = await withClient(global.home, actor('human', 'local-cli'), (client) => client.items.changes({
600
+ limit: Number(options.limit),
601
+ ...(options.kind.length ? { kinds: options.kind } : {}),
602
+ ...(options.after ? { after: options.after } : {}),
603
+ }));
604
+ const human = page.items.length
605
+ ? `${page.items
606
+ .map((change) => `${change.sequence} ${change.createdAt} ${change.type} ${change.itemId ?? '-'}`)
607
+ .join('\n')}\nWatermark: ${page.nextCursor}`
608
+ : `No new changes. Watermark: ${page.watermark}`;
609
+ output(io, global.json, page, human);
610
+ });
611
+ const memoCommand = program.command('memo').description('Send and manage durable messages');
612
+ memoCommand
613
+ .command('send <recipient>')
614
+ .requiredOption('--from <actor-id>')
615
+ .option('--actor-kind <kind>', 'human, agent, or system', 'agent')
616
+ .option('--actor-name <name>')
617
+ .requiredOption('--subject <subject>')
618
+ .requiredOption('--body <body>')
619
+ .option('--tag <tag>', 'tag (repeatable)', collect, [])
620
+ .option('--visibility <visibility>', 'private, workspace, or public', 'workspace')
621
+ .option('--idempotency-key <key>')
622
+ .action(async (recipient, options, command) => {
623
+ const global = globals(command);
624
+ const result = await withClient(global.home, actor(options.actorKind, options.from, options.actorName), (client) => client.memos.send({
625
+ recipientAgentId: recipient,
626
+ subject: options.subject,
627
+ body: options.body,
628
+ visibility: options.visibility,
629
+ ...(options.tag.length ? { tags: options.tag } : {}),
630
+ ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
631
+ }));
632
+ output(io, global.json, result, `${result.deduplicated ? 'Found existing' : 'Sent'} memo to ${result.record.event.recipientDisplayName}\nSubject: ${result.record.event.subject}\nID: ${result.record.event.id}`);
633
+ });
634
+ memoCommand
635
+ .command('list')
636
+ .option('--participant <agent>')
637
+ .option('--status <status>')
638
+ .option('--limit <number>', 'maximum results', '10')
639
+ .option('--cursor <cursor>')
640
+ .action(async (options, command) => {
641
+ const global = globals(command);
642
+ const page = await withClient(global.home, actor('human', 'local-cli'), (client) => client.memos.list({
643
+ ...(options.participant ? { participantAgentId: options.participant } : {}),
644
+ ...(options.status ? { status: options.status } : {}),
645
+ limit: Number(options.limit),
646
+ ...(options.cursor ? { cursor: options.cursor } : {}),
647
+ }));
648
+ output(io, global.json, page, page.items.map(lineForItem).join('\n') || 'No memos found.');
649
+ });
650
+ memoCommand.command('show <memo-id>').action(async (id, _options, command) => {
651
+ const global = globals(command);
652
+ const record = await withClient(global.home, actor('human', 'local-cli'), (client) => client.memos.get(id));
653
+ output(io, global.json, record, `${record.event.subject}\nID: ${record.event.id}\nStatus: ${record.status}\n\n${record.event.body}`);
654
+ });
655
+ for (const operation of ['read', 'archive']) {
656
+ memoCommand
657
+ .command(`${operation} <memo-id>`)
658
+ .requiredOption('--as <agent-id>')
659
+ .option('--actor-kind <kind>', 'agent or human', 'agent')
660
+ .option('--idempotency-key <key>')
661
+ .action(async (id, options, command) => {
662
+ const global = globals(command);
663
+ const record = await withClient(global.home, actor(options.actorKind, options.as), (client) => client.memos[operation]({
664
+ memoId: id,
665
+ ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
666
+ }));
667
+ output(io, global.json, record, `Memo ${id} is ${record.status}.`);
668
+ });
669
+ }
670
+ const noteCommand = program
671
+ .command('note')
672
+ .description('Retain and revise agent-owned knowledge');
673
+ noteCommand
674
+ .command('create')
675
+ .requiredOption('--as <actor-id>')
676
+ .option('--actor-kind <kind>', 'agent or human', 'agent')
677
+ .option('--owner <agent-id>')
678
+ .requiredOption('--title <title>')
679
+ .requiredOption('--body <body>')
680
+ .option('--tag <tag>', 'tag (repeatable)', collect, [])
681
+ .option('--idempotency-key <key>')
682
+ .action(async (options, command) => {
683
+ const global = globals(command);
684
+ const result = await withClient(global.home, actor(options.actorKind, options.as), (client) => client.notes.create({
685
+ ...(options.owner ? { ownerAgentId: options.owner } : {}),
686
+ title: options.title,
687
+ body: options.body,
688
+ ...(options.tag.length ? { tags: options.tag } : {}),
689
+ ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
690
+ }));
691
+ output(io, global.json, result, `${result.deduplicated ? 'Found existing' : 'Created'} note\nTitle: ${result.record.current.title}\nID: ${result.record.event.id}`);
692
+ });
693
+ noteCommand
694
+ .command('list')
695
+ .option('--owner <agent>')
696
+ .option('--status <status>')
697
+ .option('--limit <number>', 'maximum results', '10')
698
+ .action(async (options, command) => {
699
+ const global = globals(command);
700
+ const page = await withClient(global.home, actor('human', 'local-cli'), (client) => client.notes.list({
701
+ ...(options.owner ? { participantAgentId: options.owner } : {}),
702
+ ...(options.status ? { status: options.status } : {}),
703
+ limit: Number(options.limit),
704
+ }));
705
+ output(io, global.json, page, page.items.map(lineForItem).join('\n') || 'No notes found.');
706
+ });
707
+ noteCommand.command('show <note-id>').action(async (id, _options, command) => {
708
+ const global = globals(command);
709
+ const record = await withClient(global.home, actor('human', 'local-cli'), (client) => client.notes.get(id));
710
+ output(io, global.json, record, `${record.current.title}\nID: ${record.event.id}\nVersion: ${record.current.version}\n\n${record.current.body}`);
711
+ });
712
+ noteCommand
713
+ .command('revise <note-id>')
714
+ .requiredOption('--as <actor-id>')
715
+ .option('--actor-kind <kind>', 'agent or human', 'agent')
716
+ .requiredOption('--expected-version <number>')
717
+ .option('--title <title>')
718
+ .option('--body <body>')
719
+ .option('--tag <tag>', 'replace tags', collect, [])
720
+ .option('--idempotency-key <key>')
721
+ .action(async (id, options, command) => {
722
+ const global = globals(command);
723
+ const record = await withClient(global.home, actor(options.actorKind, options.as), (client) => client.notes.revise({
724
+ noteId: id,
725
+ expectedVersion: Number(options.expectedVersion),
726
+ ...(options.title ? { title: options.title } : {}),
727
+ ...(options.body ? { body: options.body } : {}),
728
+ ...(options.tag.length ? { tags: options.tag } : {}),
729
+ ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
730
+ }));
731
+ output(io, global.json, record, `Revised note ${id} to version ${record.current.version}.`);
732
+ });
733
+ noteCommand
734
+ .command('archive <note-id>')
735
+ .requiredOption('--as <actor-id>')
736
+ .option('--actor-kind <kind>', 'agent or human', 'agent')
737
+ .option('--idempotency-key <key>')
738
+ .action(async (id, options, command) => {
739
+ const global = globals(command);
740
+ const record = await withClient(global.home, actor(options.actorKind, options.as), (client) => client.notes.archive({
741
+ noteId: id,
742
+ ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
743
+ }));
744
+ output(io, global.json, record, `Archived note ${id}.`);
745
+ });
746
+ const todoCommand = program.command('todo').description('Create and manage agent todos');
747
+ todoCommand
748
+ .command('create <assignee>')
749
+ .requiredOption('--from <actor-id>')
750
+ .option('--actor-kind <kind>', 'human, agent, or system', 'agent')
751
+ .requiredOption('--title <title>')
752
+ .option('--description <text>')
753
+ .option('--priority <number>', '1 highest, 4 lowest', '3')
754
+ .option('--due-date <date>')
755
+ .option('--due-at <datetime>')
756
+ .option('--time-zone <iana-zone>')
757
+ .option('--tag <tag>', 'tag (repeatable)', collect, [])
758
+ .option('--visibility <visibility>', 'private, workspace, or public', 'workspace')
759
+ .option('--idempotency-key <key>')
760
+ .action(async (assignee, options, command) => {
761
+ const global = globals(command);
762
+ const result = await withClient(global.home, actor(options.actorKind, options.from), (client) => client.todos.create({
763
+ assigneeAgentId: assignee,
764
+ title: options.title,
765
+ ...(options.description ? { description: options.description } : {}),
766
+ priority: Number(options.priority),
767
+ ...((due) => (due ? { due } : {}))(todoDue(options)),
768
+ ...(options.tag.length ? { tags: options.tag } : {}),
769
+ visibility: options.visibility,
770
+ ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
771
+ }));
772
+ output(io, global.json, result, `${result.deduplicated ? 'Found existing' : 'Created'} todo for ${result.record.event.assigneeDisplayName}\nTitle: ${result.record.current.title}\nID: ${result.record.event.id}`);
773
+ });
774
+ todoCommand
775
+ .command('list')
776
+ .option('--assignee <agent>')
777
+ .option('--status <status>')
778
+ .option('--limit <number>', 'maximum results', '10')
779
+ .action(async (options, command) => {
780
+ const global = globals(command);
781
+ const page = await withClient(global.home, actor('human', 'local-cli'), (client) => client.todos.list({
782
+ ...(options.assignee ? { participantAgentId: options.assignee } : {}),
783
+ ...(options.status ? { status: options.status } : {}),
784
+ limit: Number(options.limit),
785
+ }));
786
+ output(io, global.json, page, page.items.map(lineForItem).join('\n') || 'No todos found.');
787
+ });
788
+ todoCommand.command('show <todo-id>').action(async (id, _options, command) => {
789
+ const global = globals(command);
790
+ const record = await withClient(global.home, actor('human', 'local-cli'), (client) => client.todos.get(id));
791
+ output(io, global.json, record, `${record.current.title}\nID: ${record.event.id}\nStatus: ${record.status}\nVersion: ${record.current.version}`);
792
+ });
793
+ todoCommand
794
+ .command('update <todo-id>')
795
+ .requiredOption('--as <actor-id>')
796
+ .option('--actor-kind <kind>', 'agent or human', 'agent')
797
+ .requiredOption('--expected-version <number>')
798
+ .option('--title <title>')
799
+ .option('--description <text>')
800
+ .option('--priority <number>')
801
+ .option('--due-date <date>')
802
+ .option('--due-at <datetime>')
803
+ .option('--time-zone <iana-zone>')
804
+ .option('--clear-due')
805
+ .option('--visibility <visibility>')
806
+ .option('--idempotency-key <key>')
807
+ .action(async (id, options, command) => {
808
+ const global = globals(command);
809
+ const parsedDue = todoDue(options);
810
+ const record = await withClient(global.home, actor(options.actorKind, options.as), (client) => client.todos.update({
811
+ todoId: id,
812
+ expectedVersion: Number(options.expectedVersion),
813
+ ...(options.title ? { title: options.title } : {}),
814
+ ...(options.description !== undefined ? { description: options.description } : {}),
815
+ ...(options.priority ? { priority: Number(options.priority) } : {}),
816
+ ...(options.clearDue ? { due: null } : parsedDue ? { due: parsedDue } : {}),
817
+ ...(options.visibility ? { visibility: options.visibility } : {}),
818
+ ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
819
+ }));
820
+ output(io, global.json, record, `Updated todo ${id} to version ${record.current.version}.`);
821
+ });
822
+ for (const operation of ['accept', 'reject', 'complete', 'reopen', 'cancel']) {
823
+ todoCommand
824
+ .command(`${operation} <todo-id>`)
825
+ .requiredOption('--as <actor-id>')
826
+ .option('--actor-kind <kind>', 'agent or human', 'agent')
827
+ .option('--note <text>')
828
+ .option('--reason <text>')
829
+ .option('--idempotency-key <key>')
830
+ .action(async (id, options, command) => {
831
+ const global = globals(command);
832
+ const record = await withClient(global.home, actor(options.actorKind, options.as), (client) => operation === 'accept'
833
+ ? client.todos.accept({
834
+ todoId: id,
835
+ ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
836
+ })
837
+ : operation === 'reject'
838
+ ? client.todos.reject({
839
+ todoId: id,
840
+ ...(options.reason ? { reason: options.reason } : {}),
841
+ ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
842
+ })
843
+ : operation === 'complete'
844
+ ? client.todos.complete({
845
+ todoId: id,
846
+ ...(options.note ? { note: options.note } : {}),
847
+ ...(options.idempotencyKey
848
+ ? { idempotencyKey: options.idempotencyKey }
849
+ : {}),
850
+ })
851
+ : operation === 'cancel'
852
+ ? client.todos.cancel({
853
+ todoId: id,
854
+ ...(options.reason ? { reason: options.reason } : {}),
855
+ ...(options.idempotencyKey
856
+ ? { idempotencyKey: options.idempotencyKey }
857
+ : {}),
858
+ })
859
+ : client.todos.reopen({
860
+ todoId: id,
861
+ ...(options.idempotencyKey
862
+ ? { idempotencyKey: options.idempotencyKey }
863
+ : {}),
864
+ }));
865
+ output(io, global.json, record, `Todo ${id} is ${record.status}.`);
866
+ });
867
+ }
868
+ kudosCommand
869
+ .command('show <kudos-id>')
870
+ .description('Show one kudos item and its current state')
871
+ .action(async (id, _options, command) => {
872
+ const global = globals(command);
873
+ const record = await withClient(global.home, actor('system', 'cli'), (client) => client.kudos.get(id));
874
+ output(io, global.json, record, showRecord(record));
875
+ });
876
+ kudosCommand
877
+ .command('acknowledge <kudos-id>')
878
+ .description('Record that a recipient reviewed kudos')
879
+ .requiredOption('--as <agent-id>', 'recipient agent identity')
880
+ .option('--actor-kind <kind>', 'agent, human, or system', 'agent')
881
+ .option('--name <display-name>')
882
+ .option('--note <text>')
883
+ .action(async (id, options, command) => {
884
+ const global = globals(command);
885
+ const record = await withClient(global.home, actor(options.actorKind, options.as, options.name), (client) => client.kudos.acknowledge({
886
+ kudosId: id,
887
+ ...(options.note ? { note: options.note } : {}),
888
+ }));
889
+ output(io, global.json, record, `Acknowledged ${id} as ${options.as}.`);
890
+ });
891
+ kudosCommand
892
+ .command('revoke <kudos-id>')
893
+ .description('Record a revocation while preserving history')
894
+ .requiredOption('--as <actor-id>')
895
+ .option('--actor-kind <kind>', 'human, agent, or system', 'human')
896
+ .requiredOption('--reason <reason>')
897
+ .option('--administrative', 'mark as an administrative revocation', false)
898
+ .action(async (id, options, command) => {
899
+ const global = globals(command);
900
+ const record = await withClient(global.home, actor(options.actorKind, options.as), (client) => client.kudos.revoke({
901
+ kudosId: id,
902
+ reason: options.reason,
903
+ administrative: options.administrative,
904
+ }));
905
+ output(io, global.json, record, `Revoked ${id}; the audit trail was preserved.`);
906
+ });
907
+ kudosCommand
908
+ .command('wins [agent]')
909
+ .description('Print the generated WINS.md path or content')
910
+ .option('--open', 'open WINS.md in the system GUI', false)
911
+ .option('--print', 'print Markdown content', false)
912
+ .action(async (agentId, options, command) => {
913
+ const global = globals(command);
914
+ if (!agentId)
915
+ throw new SynomemError('INVALID_INPUT', 'Specify an agent.');
916
+ const details = await withClient(global.home, actor('system', 'cli'), async (client) => {
917
+ const profile = await client.agents.get(agentId);
918
+ const info = await client.info();
919
+ if (info.backend !== 'local') {
920
+ throw new SynomemError('INVALID_INPUT', 'Generated WINS.md files are available only with the local backend.');
921
+ }
922
+ const capabilities = await client.capabilities();
923
+ const path = join(info.home, profile.id, 'WINS.md');
924
+ if (!existsSync(path)) {
925
+ const hint = capabilities.projections.writeWinsMarkdown
926
+ ? 'Run `synomem rebuild` to generate it.'
927
+ : 'Enable projection.writeWinsMarkdown and run `synomem rebuild`.';
928
+ throw new SynomemError('INVALID_INPUT', `No generated WINS.md exists for ${profile.id}. ${hint}`);
929
+ }
930
+ return { profile, path, content: readFileSync(path, 'utf8') };
931
+ });
932
+ if (options.open) {
933
+ const commandName = process.platform === 'darwin'
934
+ ? 'open'
935
+ : process.platform === 'win32'
936
+ ? 'cmd'
937
+ : 'xdg-open';
938
+ const args = process.platform === 'win32' ? ['/c', 'start', '', details.path] : [details.path];
939
+ spawn(commandName, args, { detached: true, stdio: 'ignore' }).unref();
940
+ }
941
+ output(io, global.json, details, options.print ? details.content.trimEnd() : details.path);
942
+ });
943
+ addListOptions(kudosCommand.command('stats').description('Show aggregate kudos statistics')).action(async (options, command) => {
944
+ const global = globals(command);
945
+ const stats = await withClient(global.home, actor('system', 'cli'), (client) => client.stats(listInput(options)));
946
+ output(io, global.json, stats, `Total: ${stats.total}\nActive: ${stats.active}\nAcknowledged: ${stats.acknowledged}\nRevoked: ${stats.revoked}`);
947
+ });
948
+ program
949
+ .command('rebuild')
950
+ .description('Regenerate current-state and filesystem projections from canonical events')
951
+ .action(async (_options, command) => {
952
+ const global = globals(command);
953
+ const result = await withClient(global.home, actor('system', 'cli'), (client) => client.rebuild());
954
+ output(io, global.json, result, `Rebuilt ${result.generated.length} file(s); removed ${result.removed.length} stale file(s).`);
955
+ });
956
+ program
957
+ .command('backup <destination>')
958
+ .description('Create a transactionally consistent SQLite backup')
959
+ .action(async (destination, _options, command) => {
960
+ const global = globals(command);
961
+ const path = await withClient(global.home, actor('system', 'cli'), (client) => {
962
+ if (!client.backup) {
963
+ throw new SynomemError('INVALID_INPUT', 'Filesystem backup is available only with the local backend.');
964
+ }
965
+ return client.backup(destination);
966
+ });
967
+ output(io, global.json, { path }, `Created backup at ${path}`);
968
+ });
969
+ program
970
+ .command('export')
971
+ .description('Export canonical events for portability')
972
+ .addOption(new Option('--format <format>').choices(['json', 'jsonl', 'markdown']).default('json'))
973
+ .option('--output <path>', 'write to an explicit destination instead of stdout')
974
+ .action(async (options, command) => {
975
+ const global = globals(command);
976
+ const content = await withClient(global.home, actor('system', 'cli'), (client) => client.export(options.format));
977
+ if (options.output) {
978
+ const destination = resolve(options.output);
979
+ atomicWriteFile(destination, content, 0o600);
980
+ output(io, global.json, { path: destination, format: options.format }, `Exported ${options.format} to ${destination}`);
981
+ }
982
+ else {
983
+ io.stdout(content);
984
+ }
985
+ });
986
+ program
987
+ .command('doctor')
988
+ .description('Run safe diagnostics')
989
+ .action(async (_options, command) => {
990
+ const global = globals(command);
991
+ const result = await withClient(global.home, actor('system', 'cli'), (client) => client.doctor());
992
+ const human = result.diagnostics
993
+ .map((item) => `${item.level.toUpperCase().padEnd(7)} ${item.code}: ${item.message}`)
994
+ .join('\n');
995
+ output(io, global.json, result, human);
996
+ if (!result.healthy)
997
+ cliExitCodes.set(program, 5);
998
+ });
999
+ program
1000
+ .command('mcp')
1001
+ .description('Run the actor-bound MCP server over stdio')
1002
+ .requiredOption('--actor-id <id>')
1003
+ .requiredOption('--actor-kind <kind>', 'human, agent, or system')
1004
+ .option('--actor-name <display-name>')
1005
+ .action(async (options, command) => {
1006
+ const global = globals(command);
1007
+ await startMcpServer({
1008
+ ...(global.home ? { home: global.home } : {}),
1009
+ actor: actor(options.actorKind, options.actorId, options.actorName),
1010
+ });
1011
+ });
1012
+ return program;
1013
+ }
1014
+ export async function runCli(argv = process.argv, io = defaultIo, serviceFactory = configuredServiceFactory, dependencies = {}) {
1015
+ const program = createCli(io, serviceFactory, dependencies);
1016
+ program.exitOverride();
1017
+ try {
1018
+ await program.parseAsync(argv);
1019
+ return cliExitCodes.get(program) ?? 0;
1020
+ }
1021
+ catch (error) {
1022
+ if (error instanceof CommanderError && error.exitCode === 0)
1023
+ return 0;
1024
+ if (error instanceof CommanderError) {
1025
+ if (!error.message.startsWith('error:'))
1026
+ io.stderr(`${error.message}\n`);
1027
+ return 2;
1028
+ }
1029
+ const kudosError = asSynomemError(error);
1030
+ const json = argv.includes('--json');
1031
+ io.stderr(json
1032
+ ? `${JSON.stringify({ ok: false, error: { code: kudosError.code, message: kudosError.message } })}\n`
1033
+ : `Error [${kudosError.code}]: ${kudosError.message}\n`);
1034
+ return exitCode(kudosError.code);
1035
+ }
1036
+ }
1037
+ if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
1038
+ process.exitCode = await runCli();
1039
+ }
1040
+ //# sourceMappingURL=cli.js.map