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.
- package/AGENTS.md +42 -0
- package/ARCHITECTURE.md +126 -0
- package/CHANGELOG.md +35 -0
- package/CODE_OF_CONDUCT.md +29 -0
- package/CONTRIBUTING.md +48 -0
- package/LICENSE +21 -0
- package/README.md +247 -0
- package/SECURITY.md +117 -0
- package/dist/backend.d.ts +9 -0
- package/dist/backend.d.ts.map +1 -0
- package/dist/backend.js +60 -0
- package/dist/backend.js.map +1 -0
- package/dist/cli.d.ts +34 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1040 -0
- package/dist/cli.js.map +1 -0
- package/dist/client.d.ts +168 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +1031 -0
- package/dist/client.js.map +1 -0
- package/dist/config.d.ts +33 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +153 -0
- package/dist/config.js.map +1 -0
- package/dist/credentials.d.ts +36 -0
- package/dist/credentials.d.ts.map +1 -0
- package/dist/credentials.js +127 -0
- package/dist/credentials.js.map +1 -0
- package/dist/errors.d.ts +9 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +60 -0
- package/dist/errors.js.map +1 -0
- package/dist/fs-utils.d.ts +7 -0
- package/dist/fs-utils.d.ts.map +1 -0
- package/dist/fs-utils.js +68 -0
- package/dist/fs-utils.js.map +1 -0
- package/dist/import.d.ts +655 -0
- package/dist/import.d.ts.map +1 -0
- package/dist/import.js +226 -0
- package/dist/import.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/index.d.ts +14 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +698 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp-server.d.ts +3 -0
- package/dist/mcp-server.d.ts.map +1 -0
- package/dist/mcp-server.js +45 -0
- package/dist/mcp-server.js.map +1 -0
- package/dist/oauth.d.ts +27 -0
- package/dist/oauth.d.ts.map +1 -0
- package/dist/oauth.js +201 -0
- package/dist/oauth.js.map +1 -0
- package/dist/ports/projections.d.ts +6 -0
- package/dist/ports/projections.d.ts.map +1 -0
- package/dist/ports/projections.js +2 -0
- package/dist/ports/projections.js.map +1 -0
- package/dist/ports/repository.d.ts +31 -0
- package/dist/ports/repository.d.ts.map +1 -0
- package/dist/ports/repository.js +2 -0
- package/dist/ports/repository.js.map +1 -0
- package/dist/projections.d.ts +24 -0
- package/dist/projections.d.ts.map +1 -0
- package/dist/projections.js +531 -0
- package/dist/projections.js.map +1 -0
- package/dist/remote.d.ts +120 -0
- package/dist/remote.d.ts.map +1 -0
- package/dist/remote.js +286 -0
- package/dist/remote.js.map +1 -0
- package/dist/schemas.d.ts +901 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +424 -0
- package/dist/schemas.js.map +1 -0
- package/dist/service.d.ts +126 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +2 -0
- package/dist/service.js.map +1 -0
- package/dist/skill-install.d.ts +34 -0
- package/dist/skill-install.d.ts.map +1 -0
- package/dist/skill-install.js +256 -0
- package/dist/skill-install.js.map +1 -0
- package/dist/storage.d.ts +94 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +1229 -0
- package/dist/storage.js.map +1 -0
- package/dist/types.d.ts +442 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +14 -0
- package/dist/version.js.map +1 -0
- package/docs/cli.md +180 -0
- package/docs/examples.md +70 -0
- package/docs/mcp.md +96 -0
- package/docs/recovery.md +45 -0
- package/docs/releasing.md +61 -0
- package/docs/skill.md +93 -0
- package/docs/storage-format.md +83 -0
- package/openapi/synomem-v1.yaml +674 -0
- package/package.json +100 -0
- package/skills/synomem/SKILL.md +71 -0
- package/skills/synomem/agents/openai.yaml +8 -0
- package/skills/synomem/references/examples.md +28 -0
- package/src/backend.ts +91 -0
- package/src/cli.ts +1623 -0
- package/src/client.ts +1261 -0
- package/src/config.ts +170 -0
- package/src/credentials.ts +194 -0
- package/src/errors.ts +64 -0
- package/src/fs-utils.ts +85 -0
- package/src/import.ts +284 -0
- package/src/index.ts +61 -0
- package/src/mcp/index.ts +973 -0
- package/src/mcp-server.ts +46 -0
- package/src/oauth.ts +252 -0
- package/src/ports/projections.ts +6 -0
- package/src/ports/repository.ts +67 -0
- package/src/projections.ts +624 -0
- package/src/remote.ts +516 -0
- package/src/schemas.ts +457 -0
- package/src/service.ts +140 -0
- package/src/skill-install.ts +332 -0
- package/src/storage.ts +1603 -0
- package/src/types.ts +458 -0
- package/src/version.ts +16 -0
package/src/cli.ts
ADDED
|
@@ -0,0 +1,1623 @@
|
|
|
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, type CredentialStore } from './credentials.js';
|
|
9
|
+
import { asSynomemError, SynomemError, type SynomemErrorCode } from './errors.js';
|
|
10
|
+
import { atomicWriteFile } from './fs-utils.js';
|
|
11
|
+
import { startMcpServer } from './mcp/index.js';
|
|
12
|
+
import { loginWithOAuth, StoredCredentialProvider, type OAuthLoginOptions } from './oauth.js';
|
|
13
|
+
import { RemoteSynomemService } from './remote.js';
|
|
14
|
+
import {
|
|
15
|
+
createLocalImportBundle,
|
|
16
|
+
RemoteImportClient,
|
|
17
|
+
type ImportBundle,
|
|
18
|
+
type ImportPreview,
|
|
19
|
+
type ImportResult,
|
|
20
|
+
} from './import.js';
|
|
21
|
+
import {
|
|
22
|
+
formatSkillResult,
|
|
23
|
+
installSkill,
|
|
24
|
+
skillRuntimeNames,
|
|
25
|
+
skillStatus,
|
|
26
|
+
uninstallSkill,
|
|
27
|
+
type SkillRuntime,
|
|
28
|
+
} from './skill-install.js';
|
|
29
|
+
import type {
|
|
30
|
+
ActorIdentity,
|
|
31
|
+
EvidenceReference,
|
|
32
|
+
KudosListInput,
|
|
33
|
+
KudosRecord,
|
|
34
|
+
KudosSummary,
|
|
35
|
+
ItemListInput,
|
|
36
|
+
ItemSummary,
|
|
37
|
+
TodoDue,
|
|
38
|
+
} from './types.js';
|
|
39
|
+
import { packageVersion } from './version.js';
|
|
40
|
+
import type { SynomemService, SynomemServiceFactory } from './service.js';
|
|
41
|
+
|
|
42
|
+
export interface CliIo {
|
|
43
|
+
stdout: (text: string) => void;
|
|
44
|
+
stderr: (text: string) => void;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface CliDependencies {
|
|
48
|
+
credentialStore?: CredentialStore;
|
|
49
|
+
oauthLogin?: (options: OAuthLoginOptions) => Promise<void>;
|
|
50
|
+
env?: NodeJS.ProcessEnv;
|
|
51
|
+
verifyRemoteCredential?: (options: {
|
|
52
|
+
baseUrl: string;
|
|
53
|
+
workspaceId: string;
|
|
54
|
+
actor: ActorIdentity;
|
|
55
|
+
reference: string;
|
|
56
|
+
credentialStore: CredentialStore;
|
|
57
|
+
}) => Promise<void>;
|
|
58
|
+
createImportBundle?: (home: string) => Promise<ImportBundle>;
|
|
59
|
+
remoteImport?: (options: {
|
|
60
|
+
baseUrl: string;
|
|
61
|
+
workspaceId: string;
|
|
62
|
+
actor: ActorIdentity;
|
|
63
|
+
bundle: ImportBundle;
|
|
64
|
+
planId?: string;
|
|
65
|
+
}) => Promise<ImportPreview | ImportResult>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const defaultIo: CliIo = {
|
|
69
|
+
stdout: (text) => process.stdout.write(text),
|
|
70
|
+
stderr: (text) => process.stderr.write(text),
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const cliExitCodes = new WeakMap<Command, number>();
|
|
74
|
+
|
|
75
|
+
function collect(value: string, previous: string[]): string[] {
|
|
76
|
+
return [...previous, value];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function skillRuntimes(values: string[]): SkillRuntime[] | undefined {
|
|
80
|
+
const normalized = values.map((value) => (value === 'grokbot' ? 'grok' : value));
|
|
81
|
+
const invalid = normalized.find(
|
|
82
|
+
(value) => value !== 'all' && !skillRuntimeNames.some((runtime) => runtime === value),
|
|
83
|
+
);
|
|
84
|
+
if (invalid) {
|
|
85
|
+
throw new SynomemError('INVALID_INPUT', `Unsupported skill runtime: ${invalid}.`);
|
|
86
|
+
}
|
|
87
|
+
if (!normalized.length || normalized.includes('all')) return undefined;
|
|
88
|
+
return normalized as SkillRuntime[];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const skillRuntimeHelp = `${skillRuntimeNames.join(', ')}, grokbot (alias for grok), or all`;
|
|
92
|
+
|
|
93
|
+
function parseEvidence(value: string): EvidenceReference {
|
|
94
|
+
const separator = value.indexOf(':');
|
|
95
|
+
if (separator < 1 || separator === value.length - 1) {
|
|
96
|
+
throw new SynomemError('INVALID_INPUT', 'Evidence must use kind:value syntax.');
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
kind: value.slice(0, separator) as EvidenceReference['kind'],
|
|
100
|
+
value: value.slice(separator + 1),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function actor(kind: string, id: string, displayName?: string): ActorIdentity {
|
|
105
|
+
return {
|
|
106
|
+
kind: kind as ActorIdentity['kind'],
|
|
107
|
+
id,
|
|
108
|
+
...(displayName ? { displayName } : {}),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function todoDue(options: {
|
|
113
|
+
dueDate?: string;
|
|
114
|
+
dueAt?: string;
|
|
115
|
+
timeZone?: string;
|
|
116
|
+
}): TodoDue | undefined {
|
|
117
|
+
if (options.dueDate && options.dueAt)
|
|
118
|
+
throw new SynomemError('INVALID_INPUT', 'Use due-date or due-at, not both.');
|
|
119
|
+
if (options.dueDate) return { kind: 'date', date: options.dueDate };
|
|
120
|
+
if (options.dueAt) {
|
|
121
|
+
if (!options.timeZone)
|
|
122
|
+
throw new SynomemError('INVALID_INPUT', '--time-zone is required with --due-at.');
|
|
123
|
+
return { kind: 'datetime', datetime: options.dueAt, timeZone: options.timeZone };
|
|
124
|
+
}
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function exitCode(code: SynomemErrorCode): number {
|
|
129
|
+
if (code.endsWith('_NOT_FOUND')) return 3;
|
|
130
|
+
if (
|
|
131
|
+
code.endsWith('_FORBIDDEN') ||
|
|
132
|
+
code === 'READ_ONLY' ||
|
|
133
|
+
code === 'AUTH_REQUIRED' ||
|
|
134
|
+
code === 'AUTH_FORBIDDEN'
|
|
135
|
+
)
|
|
136
|
+
return 4;
|
|
137
|
+
if (code.startsWith('DATABASE_') || code === 'UNSUPPORTED_SCHEMA' || code === 'UNSUPPORTED_EVENT')
|
|
138
|
+
return 5;
|
|
139
|
+
if (code.startsWith('REMOTE_') || code === 'RATE_LIMITED') return 5;
|
|
140
|
+
if (code === 'INTERNAL_ERROR') return 1;
|
|
141
|
+
return 2;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function lineForSummary(record: KudosSummary): string {
|
|
145
|
+
const state =
|
|
146
|
+
record.revocationStatus === 'revoked'
|
|
147
|
+
? 'revoked'
|
|
148
|
+
: record.status === 'acknowledged'
|
|
149
|
+
? 'acknowledged'
|
|
150
|
+
: 'new';
|
|
151
|
+
return `${record.id} ${record.createdAt.slice(0, 10)} ${record.recipientAgentId} [${state}] ${record.title}`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function lineForItem(item: ItemSummary): string {
|
|
155
|
+
return `${item.id} ${item.createdAt.slice(0, 10)} ${item.kind.padEnd(5)} [${item.status}] ${item.title}`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function itemListInput(options: Record<string, string | string[]>): ItemListInput {
|
|
159
|
+
return {
|
|
160
|
+
...(Array.isArray(options.kind) && options.kind.length
|
|
161
|
+
? { kinds: options.kind as ItemListInput['kinds'] }
|
|
162
|
+
: {}),
|
|
163
|
+
...(typeof options.participant === 'string' ? { participantAgentId: options.participant } : {}),
|
|
164
|
+
...(typeof options.actor === 'string' ? { actorId: options.actor } : {}),
|
|
165
|
+
...(typeof options.status === 'string' ? { status: options.status } : {}),
|
|
166
|
+
...(typeof options.tag === 'string' ? { tag: options.tag } : {}),
|
|
167
|
+
...(typeof options.visibility === 'string'
|
|
168
|
+
? { visibility: options.visibility as ItemListInput['visibility'] }
|
|
169
|
+
: {}),
|
|
170
|
+
...(typeof options.cursor === 'string' ? { cursor: options.cursor } : {}),
|
|
171
|
+
limit: Number(options.limit ?? 10),
|
|
172
|
+
offset: Number(options.offset ?? 0),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function showRecord(record: KudosRecord): string {
|
|
177
|
+
const event = record.event;
|
|
178
|
+
const evidence = event.evidence?.map((item) => ` - ${item.kind}: ${item.value}`).join('\n');
|
|
179
|
+
return [
|
|
180
|
+
event.title,
|
|
181
|
+
`ID: ${event.id}`,
|
|
182
|
+
`Recipient: ${event.recipientDisplayName} (${event.recipientAgentId})`,
|
|
183
|
+
`From: ${event.actor.displayName ?? event.actor.id} (${event.actor.kind}:${event.actor.id})`,
|
|
184
|
+
`Date: ${event.createdAt}`,
|
|
185
|
+
`Visibility: ${event.visibility}`,
|
|
186
|
+
`Status: ${record.status}`,
|
|
187
|
+
`Revocation: ${record.revocationStatus}`,
|
|
188
|
+
event.tags?.length ? `Tags: ${event.tags.join(', ')}` : undefined,
|
|
189
|
+
'',
|
|
190
|
+
event.reason,
|
|
191
|
+
evidence ? `\nEvidence:\n${evidence}` : undefined,
|
|
192
|
+
record.acknowledgment?.note ? `\nAcknowledgment: ${record.acknowledgment.note}` : undefined,
|
|
193
|
+
record.revocation ? `\nRevoked: ${record.revocation.reason}` : undefined,
|
|
194
|
+
]
|
|
195
|
+
.filter((value) => value !== undefined)
|
|
196
|
+
.join('\n');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function output(io: CliIo, json: boolean, value: unknown, human: string): void {
|
|
200
|
+
io.stdout(json ? `${JSON.stringify(value, null, 2)}\n` : `${human}\n`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function globals(command: Command): { home?: string; json: boolean } {
|
|
204
|
+
return command.optsWithGlobals<{ home?: string; json: boolean }>();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function withService<T>(
|
|
208
|
+
serviceFactory: SynomemServiceFactory,
|
|
209
|
+
home: string | undefined,
|
|
210
|
+
configuredActor: ActorIdentity,
|
|
211
|
+
operation: (client: SynomemService) => Promise<T>,
|
|
212
|
+
): Promise<T> {
|
|
213
|
+
const client = serviceFactory({ ...(home ? { home } : {}), actor: configuredActor });
|
|
214
|
+
await client.init();
|
|
215
|
+
try {
|
|
216
|
+
return await operation(client);
|
|
217
|
+
} finally {
|
|
218
|
+
await client.close();
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function addListOptions(command: Command): Command {
|
|
223
|
+
return command
|
|
224
|
+
.option('--recipient <agent>')
|
|
225
|
+
.option('--actor <id>')
|
|
226
|
+
.option('--actor-kind <kind>', 'human, agent, or system')
|
|
227
|
+
.option('--tag <tag>')
|
|
228
|
+
.option('--status <status>', 'acknowledged or unacknowledged')
|
|
229
|
+
.option('--visibility <visibility>', 'private, local, or public')
|
|
230
|
+
.addOption(
|
|
231
|
+
new Option('--revoked <state>').choices(['include', 'only', 'exclude']).default('include'),
|
|
232
|
+
)
|
|
233
|
+
.option('--from-date <iso>')
|
|
234
|
+
.option('--to-date <iso>')
|
|
235
|
+
.option('--limit <number>', 'maximum results (default 10, maximum 50)', '10')
|
|
236
|
+
.option('--cursor <cursor>', 'opaque cursor returned by the previous page')
|
|
237
|
+
.option('--offset <number>', 'pagination offset', '0');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function listInput(options: Record<string, string>): KudosListInput {
|
|
241
|
+
return {
|
|
242
|
+
...(options.recipient ? { recipientAgentId: options.recipient } : {}),
|
|
243
|
+
...(options.actor ? { actorId: options.actor } : {}),
|
|
244
|
+
...(options.actorKind ? { actorKind: options.actorKind as KudosListInput['actorKind'] } : {}),
|
|
245
|
+
...(options.tag ? { tag: options.tag } : {}),
|
|
246
|
+
...(options.status ? { status: options.status as KudosListInput['status'] } : {}),
|
|
247
|
+
...(options.visibility
|
|
248
|
+
? { visibility: options.visibility as KudosListInput['visibility'] }
|
|
249
|
+
: {}),
|
|
250
|
+
...(options.revoked === 'only' ? { revoked: true } : {}),
|
|
251
|
+
...(options.revoked === 'exclude' ? { revoked: false } : {}),
|
|
252
|
+
...(options.fromDate ? { from: options.fromDate } : {}),
|
|
253
|
+
...(options.toDate ? { to: options.toDate } : {}),
|
|
254
|
+
...(options.cursor ? { cursor: options.cursor } : {}),
|
|
255
|
+
limit: Number(options.limit),
|
|
256
|
+
offset: Number(options.offset),
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function createCli(
|
|
261
|
+
io: CliIo = defaultIo,
|
|
262
|
+
serviceFactory: SynomemServiceFactory = configuredServiceFactory,
|
|
263
|
+
dependencies: CliDependencies = {},
|
|
264
|
+
): Command {
|
|
265
|
+
const env = dependencies.env ?? process.env;
|
|
266
|
+
const credentialStore = dependencies.credentialStore ?? new OsCredentialStore();
|
|
267
|
+
const oauthLogin = dependencies.oauthLogin ?? loginWithOAuth;
|
|
268
|
+
const verifyRemoteCredential =
|
|
269
|
+
dependencies.verifyRemoteCredential ??
|
|
270
|
+
(async (options) => {
|
|
271
|
+
const remote = new RemoteSynomemService({
|
|
272
|
+
baseUrl: options.baseUrl,
|
|
273
|
+
workspaceId: options.workspaceId,
|
|
274
|
+
expectedActor: options.actor,
|
|
275
|
+
credentialProvider: new StoredCredentialProvider(
|
|
276
|
+
options.reference,
|
|
277
|
+
options.credentialStore,
|
|
278
|
+
{},
|
|
279
|
+
),
|
|
280
|
+
});
|
|
281
|
+
try {
|
|
282
|
+
await remote.init();
|
|
283
|
+
} finally {
|
|
284
|
+
await remote.close();
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
const program = new Command();
|
|
289
|
+
const withClient = <T>(
|
|
290
|
+
home: string | undefined,
|
|
291
|
+
configuredActor: ActorIdentity,
|
|
292
|
+
operation: (client: SynomemService) => Promise<T>,
|
|
293
|
+
) => withService(serviceFactory, home, configuredActor, operation);
|
|
294
|
+
cliExitCodes.set(program, 0);
|
|
295
|
+
program
|
|
296
|
+
.name('synomem')
|
|
297
|
+
.description(
|
|
298
|
+
'Local-first communication, memory, recognition, and task infrastructure for agents',
|
|
299
|
+
)
|
|
300
|
+
.version(packageVersion())
|
|
301
|
+
.option('--home <path>', 'storage root (defaults to SYNOMEM_HOME or ~/.agents)')
|
|
302
|
+
.option('--json', 'emit stable machine-readable JSON', false)
|
|
303
|
+
.showSuggestionAfterError()
|
|
304
|
+
.configureOutput({ writeOut: io.stdout, writeErr: io.stderr });
|
|
305
|
+
|
|
306
|
+
const remoteCommand = program.command('remote').description('Administer a remote workspace');
|
|
307
|
+
remoteCommand
|
|
308
|
+
.command('import')
|
|
309
|
+
.description('Preview or confirm a one-way import from a local Synomem home')
|
|
310
|
+
.requiredOption('--from-home <path>', 'source local Synomem home')
|
|
311
|
+
.requiredOption('--actor-id <id>', 'bound human administrator actor ID')
|
|
312
|
+
.option('--actor-name <name>', 'expected administrator display name')
|
|
313
|
+
.option('--preview', 'validate and return a short-lived import plan')
|
|
314
|
+
.option('--confirm <plan-id>', 'commit the exact bundle authorized by a preview')
|
|
315
|
+
.action(
|
|
316
|
+
async (
|
|
317
|
+
options: {
|
|
318
|
+
fromHome: string;
|
|
319
|
+
actorId: string;
|
|
320
|
+
actorName?: string;
|
|
321
|
+
preview?: boolean;
|
|
322
|
+
confirm?: string;
|
|
323
|
+
},
|
|
324
|
+
command: Command,
|
|
325
|
+
) => {
|
|
326
|
+
const global = globals(command);
|
|
327
|
+
if (Boolean(options.preview) === Boolean(options.confirm)) {
|
|
328
|
+
throw new SynomemError('INVALID_INPUT', 'Choose exactly one of --preview or --confirm.');
|
|
329
|
+
}
|
|
330
|
+
const config = readSynomemConfig(global.home, env);
|
|
331
|
+
if (config?.backend.kind !== 'remote') {
|
|
332
|
+
throw new SynomemError('INVALID_INPUT', 'Select a remote backend before importing.');
|
|
333
|
+
}
|
|
334
|
+
const backend = config.backend;
|
|
335
|
+
const administrator = actor('human', options.actorId, options.actorName);
|
|
336
|
+
const bundle = await (dependencies.createImportBundle ?? createLocalImportBundle)(
|
|
337
|
+
options.fromHome,
|
|
338
|
+
);
|
|
339
|
+
const result = dependencies.remoteImport
|
|
340
|
+
? await dependencies.remoteImport({
|
|
341
|
+
baseUrl: backend.baseUrl,
|
|
342
|
+
workspaceId: backend.workspaceId,
|
|
343
|
+
actor: administrator,
|
|
344
|
+
bundle,
|
|
345
|
+
...(options.confirm ? { planId: options.confirm } : {}),
|
|
346
|
+
})
|
|
347
|
+
: await (async () => {
|
|
348
|
+
const credentialProvider = env.SYNOMEM_ACCESS_TOKEN
|
|
349
|
+
? { getAccessToken: async () => env.SYNOMEM_ACCESS_TOKEN }
|
|
350
|
+
: new StoredCredentialProvider(
|
|
351
|
+
credentialReference(backend.baseUrl, backend.workspaceId, administrator),
|
|
352
|
+
credentialStore,
|
|
353
|
+
env,
|
|
354
|
+
);
|
|
355
|
+
const importer = new RemoteImportClient({
|
|
356
|
+
baseUrl: backend.baseUrl,
|
|
357
|
+
workspaceId: backend.workspaceId,
|
|
358
|
+
credentialProvider,
|
|
359
|
+
});
|
|
360
|
+
return options.confirm
|
|
361
|
+
? await importer.confirm(bundle, options.confirm)
|
|
362
|
+
: await importer.preview(bundle);
|
|
363
|
+
})();
|
|
364
|
+
const human =
|
|
365
|
+
'planId' in result
|
|
366
|
+
? [
|
|
367
|
+
`Import preview: ${result.events} events and ${result.profiles} agent profiles.`,
|
|
368
|
+
`Source: ${result.sourceWorkspaceId}`,
|
|
369
|
+
`Target: ${result.targetWorkspaceId}`,
|
|
370
|
+
`Checksum: ${result.checksum}`,
|
|
371
|
+
`Plan expires: ${result.expiresAt}`,
|
|
372
|
+
`Plan ID: ${result.planId}`,
|
|
373
|
+
'Nothing was imported. Re-run with --confirm <plan-id> to commit this exact snapshot.',
|
|
374
|
+
].join('\n')
|
|
375
|
+
: `Imported ${result.events} events and ${result.profiles} agent profiles into ${result.targetWorkspaceId}.`;
|
|
376
|
+
output(io, global.json, result, human);
|
|
377
|
+
},
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
program
|
|
381
|
+
.command('init')
|
|
382
|
+
.description('Initialize the local Synomem database')
|
|
383
|
+
.action(async (_options, command: Command) => {
|
|
384
|
+
const options = globals(command);
|
|
385
|
+
const persisted = readSynomemConfig(options.home);
|
|
386
|
+
if (persisted?.backend.kind === 'remote') {
|
|
387
|
+
throw new SynomemError('INVALID_INPUT', 'The init command requires a local backend.');
|
|
388
|
+
}
|
|
389
|
+
await withClient(options.home, actor('system', 'cli'), async (client) => {
|
|
390
|
+
const info = await client.info();
|
|
391
|
+
if (info.backend !== 'local') {
|
|
392
|
+
throw new SynomemError('INVALID_INPUT', 'The init command requires a local backend.');
|
|
393
|
+
}
|
|
394
|
+
output(
|
|
395
|
+
io,
|
|
396
|
+
options.json,
|
|
397
|
+
{ home: info.home, database: info.databasePath },
|
|
398
|
+
`Initialized Synomem at ${info.home}`,
|
|
399
|
+
);
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
const backendCommand = program
|
|
404
|
+
.command('backend')
|
|
405
|
+
.description('Inspect or select the canonical backend');
|
|
406
|
+
backendCommand
|
|
407
|
+
.command('show')
|
|
408
|
+
.description('Show backend selection without connecting')
|
|
409
|
+
.action((_options, command: Command) => {
|
|
410
|
+
const global = globals(command);
|
|
411
|
+
const config = readSynomemConfig(global.home);
|
|
412
|
+
const backend = config?.backend ?? { kind: 'local' as const };
|
|
413
|
+
const human =
|
|
414
|
+
backend.kind === 'local'
|
|
415
|
+
? `Backend: local${config ? `\nWorkspace: ${config.workspaceId}` : ' (not initialized)'}`
|
|
416
|
+
: `Backend: remote\nURL: ${backend.baseUrl}\nWorkspace: ${backend.workspaceId}`;
|
|
417
|
+
output(io, global.json, { backend, initialized: config !== undefined }, human);
|
|
418
|
+
});
|
|
419
|
+
backendCommand
|
|
420
|
+
.command('use')
|
|
421
|
+
.description('Select local or remote canonical state')
|
|
422
|
+
.argument('<kind>', 'local or remote')
|
|
423
|
+
.option('--url <url>', 'remote HTTPS origin')
|
|
424
|
+
.option('--workspace <id>', 'remote workspace ID')
|
|
425
|
+
.action((kind: string, options: { url?: string; workspace?: string }, command: Command) => {
|
|
426
|
+
const global = globals(command);
|
|
427
|
+
if (kind !== 'local' && kind !== 'remote') {
|
|
428
|
+
throw new SynomemError('INVALID_INPUT', 'Backend kind must be local or remote.');
|
|
429
|
+
}
|
|
430
|
+
if (kind === 'remote' && (!options.url || !options.workspace)) {
|
|
431
|
+
throw new SynomemError(
|
|
432
|
+
'INVALID_INPUT',
|
|
433
|
+
'Remote backend selection requires --url and --workspace.',
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
const config = writeSynomemBackend(
|
|
437
|
+
kind === 'local'
|
|
438
|
+
? { kind: 'local' }
|
|
439
|
+
: { kind: 'remote', baseUrl: options.url!, workspaceId: options.workspace! },
|
|
440
|
+
global.home,
|
|
441
|
+
);
|
|
442
|
+
output(
|
|
443
|
+
io,
|
|
444
|
+
global.json,
|
|
445
|
+
{ backend: config.backend },
|
|
446
|
+
`Selected ${config.backend.kind} Synomem backend.`,
|
|
447
|
+
);
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
const authCommand = program.command('auth').description('Inspect remote authentication');
|
|
451
|
+
authCommand
|
|
452
|
+
.command('status')
|
|
453
|
+
.description('Report token availability without printing it')
|
|
454
|
+
.option('--actor-id <id>', 'bound actor ID (or SYNOMEM_ACTOR_ID)')
|
|
455
|
+
.option('--actor-kind <kind>', 'human, agent, or system', 'agent')
|
|
456
|
+
.action(async (options: { actorId?: string; actorKind: string }, command: Command) => {
|
|
457
|
+
const global = globals(command);
|
|
458
|
+
if (env.SYNOMEM_ACCESS_TOKEN) {
|
|
459
|
+
output(
|
|
460
|
+
io,
|
|
461
|
+
global.json,
|
|
462
|
+
{ authenticated: true, source: 'environment' },
|
|
463
|
+
'Remote authentication token is available from SYNOMEM_ACCESS_TOKEN.',
|
|
464
|
+
);
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
const config = readSynomemConfig(global.home, env);
|
|
468
|
+
if (config?.backend.kind !== 'remote') {
|
|
469
|
+
throw new SynomemError(
|
|
470
|
+
'INVALID_INPUT',
|
|
471
|
+
'Select a remote backend before checking authentication.',
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
const actorId = options.actorId ?? env.SYNOMEM_ACTOR_ID;
|
|
475
|
+
if (!actorId) throw new SynomemError('INVALID_INPUT', 'Specify --actor-id.');
|
|
476
|
+
const reference = credentialReference(
|
|
477
|
+
config.backend.baseUrl,
|
|
478
|
+
config.backend.workspaceId,
|
|
479
|
+
actor(options.actorKind, actorId),
|
|
480
|
+
);
|
|
481
|
+
const available = Boolean(await credentialStore.get(reference));
|
|
482
|
+
output(
|
|
483
|
+
io,
|
|
484
|
+
global.json,
|
|
485
|
+
{ authenticated: available, source: available ? 'os-credential-store' : undefined },
|
|
486
|
+
available
|
|
487
|
+
? 'A remote credential is available in the operating-system credential store.'
|
|
488
|
+
: 'Remote authentication is not configured.',
|
|
489
|
+
);
|
|
490
|
+
});
|
|
491
|
+
authCommand
|
|
492
|
+
.command('login')
|
|
493
|
+
.description('Authorize this actor with OAuth 2.1 authorization code and PKCE')
|
|
494
|
+
.requiredOption('--actor-id <id>', 'bound actor ID')
|
|
495
|
+
.option('--actor-kind <kind>', 'human, agent, or system', 'agent')
|
|
496
|
+
.option('--actor-name <name>', 'expected actor display name')
|
|
497
|
+
.option('--client-id <id>', 'registered public OAuth client ID (or SYNOMEM_OAUTH_CLIENT_ID)')
|
|
498
|
+
.option('--scope <scope>', 'requested OAuth scopes')
|
|
499
|
+
.option('--callback-port <port>', 'loopback callback port', '43817')
|
|
500
|
+
.action(
|
|
501
|
+
async (
|
|
502
|
+
options: {
|
|
503
|
+
actorId: string;
|
|
504
|
+
actorKind: string;
|
|
505
|
+
actorName?: string;
|
|
506
|
+
clientId?: string;
|
|
507
|
+
scope?: string;
|
|
508
|
+
callbackPort: string;
|
|
509
|
+
},
|
|
510
|
+
command: Command,
|
|
511
|
+
) => {
|
|
512
|
+
const global = globals(command);
|
|
513
|
+
const config = readSynomemConfig(global.home, env);
|
|
514
|
+
if (config?.backend.kind !== 'remote') {
|
|
515
|
+
throw new SynomemError('INVALID_INPUT', 'Select a remote backend before login.');
|
|
516
|
+
}
|
|
517
|
+
const configuredActor = actor(options.actorKind, options.actorId, options.actorName);
|
|
518
|
+
const clientId = options.clientId ?? env.SYNOMEM_OAUTH_CLIENT_ID;
|
|
519
|
+
if (!clientId) throw new SynomemError('INVALID_INPUT', 'Specify --client-id.');
|
|
520
|
+
const callbackPort = Number(options.callbackPort);
|
|
521
|
+
if (!Number.isSafeInteger(callbackPort) || callbackPort < 1 || callbackPort > 65_535) {
|
|
522
|
+
throw new SynomemError('INVALID_INPUT', '--callback-port must be from 1 through 65535.');
|
|
523
|
+
}
|
|
524
|
+
const reference = credentialReference(
|
|
525
|
+
config.backend.baseUrl,
|
|
526
|
+
config.backend.workspaceId,
|
|
527
|
+
configuredActor,
|
|
528
|
+
);
|
|
529
|
+
await oauthLogin({
|
|
530
|
+
baseUrl: config.backend.baseUrl,
|
|
531
|
+
clientId,
|
|
532
|
+
credentialReference: reference,
|
|
533
|
+
credentialStore,
|
|
534
|
+
callbackPort,
|
|
535
|
+
...(options.scope ? { scope: options.scope } : {}),
|
|
536
|
+
});
|
|
537
|
+
try {
|
|
538
|
+
await verifyRemoteCredential({
|
|
539
|
+
baseUrl: config.backend.baseUrl,
|
|
540
|
+
workspaceId: config.backend.workspaceId,
|
|
541
|
+
actor: configuredActor,
|
|
542
|
+
reference,
|
|
543
|
+
credentialStore,
|
|
544
|
+
});
|
|
545
|
+
} catch (error) {
|
|
546
|
+
await credentialStore.delete(reference);
|
|
547
|
+
throw error;
|
|
548
|
+
}
|
|
549
|
+
output(
|
|
550
|
+
io,
|
|
551
|
+
global.json,
|
|
552
|
+
{ authenticated: true, source: 'os-credential-store', actor: configuredActor },
|
|
553
|
+
`Authorized ${configuredActor.kind}:${configuredActor.id}; the credential is stored by the operating system.`,
|
|
554
|
+
);
|
|
555
|
+
},
|
|
556
|
+
);
|
|
557
|
+
authCommand
|
|
558
|
+
.command('logout')
|
|
559
|
+
.description('Remove the stored OAuth credential for one actor')
|
|
560
|
+
.requiredOption('--actor-id <id>', 'bound actor ID')
|
|
561
|
+
.option('--actor-kind <kind>', 'human, agent, or system', 'agent')
|
|
562
|
+
.action(async (options: { actorId: string; actorKind: string }, command: Command) => {
|
|
563
|
+
const global = globals(command);
|
|
564
|
+
const config = readSynomemConfig(global.home, env);
|
|
565
|
+
if (config?.backend.kind !== 'remote') {
|
|
566
|
+
throw new SynomemError('INVALID_INPUT', 'Select a remote backend before logout.');
|
|
567
|
+
}
|
|
568
|
+
const configuredActor = actor(options.actorKind, options.actorId);
|
|
569
|
+
const removed = await credentialStore.delete(
|
|
570
|
+
credentialReference(config.backend.baseUrl, config.backend.workspaceId, configuredActor),
|
|
571
|
+
);
|
|
572
|
+
output(
|
|
573
|
+
io,
|
|
574
|
+
global.json,
|
|
575
|
+
{ authenticated: false, removed },
|
|
576
|
+
removed
|
|
577
|
+
? 'Removed the stored Synomem credential.'
|
|
578
|
+
: 'No stored Synomem credential existed.',
|
|
579
|
+
);
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
const agentCommand = program
|
|
583
|
+
.command('agent')
|
|
584
|
+
.description('Create and inspect stable agent identities');
|
|
585
|
+
agentCommand
|
|
586
|
+
.command('create <id>')
|
|
587
|
+
.description('Create a stable agent profile')
|
|
588
|
+
.requiredOption('--name <display-name>', 'display name')
|
|
589
|
+
.option('--alias <id>', 'alias (repeatable)', collect, [])
|
|
590
|
+
.option('--description <text>')
|
|
591
|
+
.action(
|
|
592
|
+
async (
|
|
593
|
+
id: string,
|
|
594
|
+
options: { name: string; alias: string[]; description?: string },
|
|
595
|
+
command: Command,
|
|
596
|
+
) => {
|
|
597
|
+
const global = globals(command);
|
|
598
|
+
const profile = await withClient(global.home, actor('system', 'cli'), (client) =>
|
|
599
|
+
client.agents.create({
|
|
600
|
+
id,
|
|
601
|
+
displayName: options.name,
|
|
602
|
+
...(options.alias.length ? { aliases: options.alias } : {}),
|
|
603
|
+
...(options.description ? { description: options.description } : {}),
|
|
604
|
+
}),
|
|
605
|
+
);
|
|
606
|
+
output(io, global.json, profile, `Created ${profile.displayName} (${profile.id})`);
|
|
607
|
+
},
|
|
608
|
+
);
|
|
609
|
+
|
|
610
|
+
const skillCommand = program
|
|
611
|
+
.command('skill')
|
|
612
|
+
.description('Install and maintain the packaged agent skill');
|
|
613
|
+
|
|
614
|
+
skillCommand
|
|
615
|
+
.command('install')
|
|
616
|
+
.description('Plan or install the skill for detected agent runtimes')
|
|
617
|
+
.option('--runtime <runtime>', `${skillRuntimeHelp} (repeatable)`, collect, [])
|
|
618
|
+
.option('--yes', 'apply the displayed plan', false)
|
|
619
|
+
.option('--force', 'replace a conflicting synomem directory', false)
|
|
620
|
+
.option('--link', 'symlink to the packaged skill instead of copying it', false)
|
|
621
|
+
.option('--actor-id <id>', 'print actor-bound MCP registration commands')
|
|
622
|
+
.option('--actor-name <name>', 'display name used in MCP registration commands')
|
|
623
|
+
.action(
|
|
624
|
+
(
|
|
625
|
+
options: {
|
|
626
|
+
runtime: string[];
|
|
627
|
+
yes: boolean;
|
|
628
|
+
force: boolean;
|
|
629
|
+
link: boolean;
|
|
630
|
+
actorId?: string;
|
|
631
|
+
actorName?: string;
|
|
632
|
+
},
|
|
633
|
+
command: Command,
|
|
634
|
+
) => {
|
|
635
|
+
const global = globals(command);
|
|
636
|
+
const result = installSkill({
|
|
637
|
+
runtimes: skillRuntimes(options.runtime),
|
|
638
|
+
apply: options.yes,
|
|
639
|
+
force: options.force,
|
|
640
|
+
link: options.link,
|
|
641
|
+
actorId: options.actorId,
|
|
642
|
+
actorName: options.actorName,
|
|
643
|
+
});
|
|
644
|
+
output(io, global.json, result, formatSkillResult(result, 'install'));
|
|
645
|
+
},
|
|
646
|
+
);
|
|
647
|
+
|
|
648
|
+
skillCommand
|
|
649
|
+
.command('status')
|
|
650
|
+
.description('Show installed, stale, missing, or conflicting skill copies')
|
|
651
|
+
.option('--runtime <runtime>', `${skillRuntimeHelp} (repeatable)`, collect, [])
|
|
652
|
+
.option('--actor-id <id>', 'print actor-bound MCP registration commands')
|
|
653
|
+
.option('--actor-name <name>', 'display name used in MCP registration commands')
|
|
654
|
+
.action(
|
|
655
|
+
(options: { runtime: string[]; actorId?: string; actorName?: string }, command: Command) => {
|
|
656
|
+
const global = globals(command);
|
|
657
|
+
const result = skillStatus({
|
|
658
|
+
runtimes: skillRuntimes(options.runtime),
|
|
659
|
+
actorId: options.actorId,
|
|
660
|
+
actorName: options.actorName,
|
|
661
|
+
});
|
|
662
|
+
output(io, global.json, result, formatSkillResult(result, 'status'));
|
|
663
|
+
},
|
|
664
|
+
);
|
|
665
|
+
|
|
666
|
+
skillCommand
|
|
667
|
+
.command('uninstall')
|
|
668
|
+
.description('Plan or remove Synomem-owned skill installations')
|
|
669
|
+
.option('--runtime <runtime>', `${skillRuntimeHelp} (repeatable)`, collect, [])
|
|
670
|
+
.option('--yes', 'apply the displayed plan', false)
|
|
671
|
+
.option('--force', 'remove a conflicting synomem directory', false)
|
|
672
|
+
.action((options: { runtime: string[]; yes: boolean; force: boolean }, command: Command) => {
|
|
673
|
+
const global = globals(command);
|
|
674
|
+
const result = uninstallSkill({
|
|
675
|
+
runtimes: skillRuntimes(options.runtime),
|
|
676
|
+
apply: options.yes,
|
|
677
|
+
force: options.force,
|
|
678
|
+
});
|
|
679
|
+
output(io, global.json, result, formatSkillResult(result, 'uninstall'));
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
agentCommand
|
|
683
|
+
.command('list')
|
|
684
|
+
.description('List known agent identities')
|
|
685
|
+
.action(async (_options, command: Command) => {
|
|
686
|
+
const global = globals(command);
|
|
687
|
+
const agents = await withClient(global.home, actor('system', 'cli'), (client) =>
|
|
688
|
+
client.agents.list(),
|
|
689
|
+
);
|
|
690
|
+
const human = agents.length
|
|
691
|
+
? agents
|
|
692
|
+
.map(
|
|
693
|
+
(profile) =>
|
|
694
|
+
`${profile.id} ${profile.displayName}${profile.aliases?.length ? ` aliases: ${profile.aliases.join(', ')}` : ''}`,
|
|
695
|
+
)
|
|
696
|
+
.join('\n')
|
|
697
|
+
: 'No agents configured.';
|
|
698
|
+
output(io, global.json, { agents }, human);
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
agentCommand
|
|
702
|
+
.command('show <id>')
|
|
703
|
+
.description('Show one agent profile, resolving aliases')
|
|
704
|
+
.action(async (id: string, _options, command: Command) => {
|
|
705
|
+
const global = globals(command);
|
|
706
|
+
const profile = await withClient(global.home, actor('system', 'cli'), (client) =>
|
|
707
|
+
client.agents.get(id),
|
|
708
|
+
);
|
|
709
|
+
output(
|
|
710
|
+
io,
|
|
711
|
+
global.json,
|
|
712
|
+
profile,
|
|
713
|
+
`${profile.displayName} (${profile.id})\n${profile.description ?? 'No description.'}`,
|
|
714
|
+
);
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
agentCommand
|
|
718
|
+
.command('update <id>')
|
|
719
|
+
.description('Update an agent profile without rewriting history')
|
|
720
|
+
.option('--name <display-name>')
|
|
721
|
+
.option('--alias <id>', 'replace aliases (repeatable)', collect, [])
|
|
722
|
+
.option('--clear-aliases', 'remove every alias', false)
|
|
723
|
+
.option('--description <text>')
|
|
724
|
+
.action(
|
|
725
|
+
async (
|
|
726
|
+
id: string,
|
|
727
|
+
options: { name?: string; alias: string[]; clearAliases: boolean; description?: string },
|
|
728
|
+
command: Command,
|
|
729
|
+
) => {
|
|
730
|
+
const global = globals(command);
|
|
731
|
+
const hasAliases = options.clearAliases || options.alias.length > 0;
|
|
732
|
+
const profile = await withClient(global.home, actor('system', 'cli'), (client) =>
|
|
733
|
+
client.agents.update(id, {
|
|
734
|
+
...(options.name ? { displayName: options.name } : {}),
|
|
735
|
+
...(hasAliases ? { aliases: options.clearAliases ? [] : options.alias } : {}),
|
|
736
|
+
...(options.description !== undefined ? { description: options.description } : {}),
|
|
737
|
+
}),
|
|
738
|
+
);
|
|
739
|
+
output(io, global.json, profile, `Updated ${profile.displayName} (${profile.id})`);
|
|
740
|
+
},
|
|
741
|
+
);
|
|
742
|
+
|
|
743
|
+
const kudosCommand = program.command('kudos').description('Give and manage agent recognition');
|
|
744
|
+
|
|
745
|
+
kudosCommand
|
|
746
|
+
.command('give <recipient>')
|
|
747
|
+
.description('Give specific, evidence-based kudos to an agent')
|
|
748
|
+
.requiredOption('--from <actor-id>', 'stable ID of the giver')
|
|
749
|
+
.requiredOption('--actor-kind <kind>', 'human, agent, or system')
|
|
750
|
+
.option('--actor-name <display-name>')
|
|
751
|
+
.requiredOption('--title <title>')
|
|
752
|
+
.requiredOption('--reason <reason>')
|
|
753
|
+
.option('--tag <tag>', 'tag (repeatable)', collect, [])
|
|
754
|
+
.option('--evidence <kind:value>', 'sanitized evidence (repeatable)', collect, [])
|
|
755
|
+
.option('--visibility <visibility>', 'private, local, or public', 'workspace')
|
|
756
|
+
.option('--idempotency-key <key>')
|
|
757
|
+
.action(
|
|
758
|
+
async (
|
|
759
|
+
recipient: string,
|
|
760
|
+
options: {
|
|
761
|
+
from: string;
|
|
762
|
+
actorKind: string;
|
|
763
|
+
actorName?: string;
|
|
764
|
+
title: string;
|
|
765
|
+
reason: string;
|
|
766
|
+
tag: string[];
|
|
767
|
+
evidence: string[];
|
|
768
|
+
visibility: 'private' | 'workspace' | 'public';
|
|
769
|
+
idempotencyKey?: string;
|
|
770
|
+
},
|
|
771
|
+
command: Command,
|
|
772
|
+
) => {
|
|
773
|
+
const global = globals(command);
|
|
774
|
+
const result = await withClient(
|
|
775
|
+
global.home,
|
|
776
|
+
actor(options.actorKind, options.from, options.actorName),
|
|
777
|
+
(client) =>
|
|
778
|
+
client.kudos.give({
|
|
779
|
+
recipientAgentId: recipient,
|
|
780
|
+
title: options.title,
|
|
781
|
+
reason: options.reason,
|
|
782
|
+
visibility: options.visibility,
|
|
783
|
+
...(options.tag.length ? { tags: options.tag } : {}),
|
|
784
|
+
...(options.evidence.length ? { evidence: options.evidence.map(parseEvidence) } : {}),
|
|
785
|
+
...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
|
|
786
|
+
}),
|
|
787
|
+
);
|
|
788
|
+
const event = result.record.event;
|
|
789
|
+
output(
|
|
790
|
+
io,
|
|
791
|
+
global.json,
|
|
792
|
+
result,
|
|
793
|
+
`${result.deduplicated ? 'Found existing' : 'Created'} kudos for ${event.recipientDisplayName}\nTitle: ${event.title}\nDate: ${event.createdAt}\nID: ${event.id}`,
|
|
794
|
+
);
|
|
795
|
+
},
|
|
796
|
+
);
|
|
797
|
+
|
|
798
|
+
program
|
|
799
|
+
.command('inbox [agent]')
|
|
800
|
+
.description('Show pending kudos, memos, and todos for an agent')
|
|
801
|
+
.option('--as <agent-id>', 'defaults to the positional agent')
|
|
802
|
+
.option('--limit <number>', 'maximum results (default 10, maximum 50)', '10')
|
|
803
|
+
.option('--cursor <cursor>', 'opaque cursor returned by the previous page')
|
|
804
|
+
.action(
|
|
805
|
+
async (
|
|
806
|
+
agentId: string | undefined,
|
|
807
|
+
options: { as?: string; limit: string; cursor?: string },
|
|
808
|
+
command: Command,
|
|
809
|
+
) => {
|
|
810
|
+
const global = globals(command);
|
|
811
|
+
const recipient = agentId ?? options.as;
|
|
812
|
+
if (!recipient) throw new SynomemError('INVALID_INPUT', 'Specify an agent inbox.');
|
|
813
|
+
const page = await withClient(
|
|
814
|
+
global.home,
|
|
815
|
+
actor('agent', options.as ?? recipient),
|
|
816
|
+
(client) =>
|
|
817
|
+
client.items.list({
|
|
818
|
+
participantAgentId: recipient,
|
|
819
|
+
pending: true,
|
|
820
|
+
limit: Number(options.limit),
|
|
821
|
+
...(options.cursor ? { cursor: options.cursor } : {}),
|
|
822
|
+
}),
|
|
823
|
+
);
|
|
824
|
+
output(
|
|
825
|
+
io,
|
|
826
|
+
global.json,
|
|
827
|
+
page,
|
|
828
|
+
page.items.length
|
|
829
|
+
? `${page.items.map(lineForItem).join('\n')}${page.hasMore ? `\nNext cursor: ${page.nextCursor}` : ''}`
|
|
830
|
+
: 'Inbox is clear.',
|
|
831
|
+
);
|
|
832
|
+
},
|
|
833
|
+
);
|
|
834
|
+
|
|
835
|
+
addListOptions(kudosCommand.command('list').description('List and filter kudos')).action(
|
|
836
|
+
async (options: Record<string, string>, command: Command) => {
|
|
837
|
+
const global = globals(command);
|
|
838
|
+
const page = await withClient(global.home, actor('human', 'local-cli'), (client) =>
|
|
839
|
+
client.kudos.list(listInput(options)),
|
|
840
|
+
);
|
|
841
|
+
output(
|
|
842
|
+
io,
|
|
843
|
+
global.json,
|
|
844
|
+
page,
|
|
845
|
+
page.items.length
|
|
846
|
+
? `${page.items.map(lineForSummary).join('\n')}${page.hasMore ? `\nNext cursor: ${page.nextCursor}` : ''}`
|
|
847
|
+
: 'No kudos found.',
|
|
848
|
+
);
|
|
849
|
+
},
|
|
850
|
+
);
|
|
851
|
+
|
|
852
|
+
program
|
|
853
|
+
.command('list')
|
|
854
|
+
.description('List compact summaries across all record types')
|
|
855
|
+
.option('--kind <kind>', 'kudos, memo, note, or todo (repeatable)', collect, [])
|
|
856
|
+
.option('--participant <agent>')
|
|
857
|
+
.option('--actor <id>')
|
|
858
|
+
.option('--tag <tag>')
|
|
859
|
+
.option('--status <status>')
|
|
860
|
+
.option('--visibility <visibility>', 'private, workspace, or public')
|
|
861
|
+
.option('--limit <number>', 'maximum results (default 10, maximum 50)', '10')
|
|
862
|
+
.option('--cursor <cursor>')
|
|
863
|
+
.option('--offset <number>', 'deprecated offset', '0')
|
|
864
|
+
.action(async (options: Record<string, string | string[]>, command: Command) => {
|
|
865
|
+
const global = globals(command);
|
|
866
|
+
const page = await withClient(global.home, actor('human', 'local-cli'), (client) =>
|
|
867
|
+
client.items.list(itemListInput(options)),
|
|
868
|
+
);
|
|
869
|
+
output(
|
|
870
|
+
io,
|
|
871
|
+
global.json,
|
|
872
|
+
page,
|
|
873
|
+
page.items.length
|
|
874
|
+
? `${page.items.map(lineForItem).join('\n')}${page.hasMore ? `\nNext cursor: ${page.nextCursor}` : ''}`
|
|
875
|
+
: 'No items found.',
|
|
876
|
+
);
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
program
|
|
880
|
+
.command('changes')
|
|
881
|
+
.description('List compact changes across all record types after an opaque watermark')
|
|
882
|
+
.option('--kind <kind>', 'filter by kind (repeatable)', collect, [])
|
|
883
|
+
.option('--after <watermark>', 'watermark or change cursor from a previous response')
|
|
884
|
+
.option('--limit <number>', 'maximum changes (default 20, maximum 100)', '20')
|
|
885
|
+
.action(
|
|
886
|
+
async (options: { after?: string; limit: string; kind: string[] }, command: Command) => {
|
|
887
|
+
const global = globals(command);
|
|
888
|
+
const page = await withClient(global.home, actor('human', 'local-cli'), (client) =>
|
|
889
|
+
client.items.changes({
|
|
890
|
+
limit: Number(options.limit),
|
|
891
|
+
...(options.kind.length ? { kinds: options.kind as ItemListInput['kinds'] } : {}),
|
|
892
|
+
...(options.after ? { after: options.after } : {}),
|
|
893
|
+
}),
|
|
894
|
+
);
|
|
895
|
+
const human = page.items.length
|
|
896
|
+
? `${page.items
|
|
897
|
+
.map(
|
|
898
|
+
(change) =>
|
|
899
|
+
`${change.sequence} ${change.createdAt} ${change.type} ${change.itemId ?? '-'}`,
|
|
900
|
+
)
|
|
901
|
+
.join('\n')}\nWatermark: ${page.nextCursor}`
|
|
902
|
+
: `No new changes. Watermark: ${page.watermark}`;
|
|
903
|
+
output(io, global.json, page, human);
|
|
904
|
+
},
|
|
905
|
+
);
|
|
906
|
+
|
|
907
|
+
const memoCommand = program.command('memo').description('Send and manage durable messages');
|
|
908
|
+
memoCommand
|
|
909
|
+
.command('send <recipient>')
|
|
910
|
+
.requiredOption('--from <actor-id>')
|
|
911
|
+
.option('--actor-kind <kind>', 'human, agent, or system', 'agent')
|
|
912
|
+
.option('--actor-name <name>')
|
|
913
|
+
.requiredOption('--subject <subject>')
|
|
914
|
+
.requiredOption('--body <body>')
|
|
915
|
+
.option('--tag <tag>', 'tag (repeatable)', collect, [])
|
|
916
|
+
.option('--visibility <visibility>', 'private, workspace, or public', 'workspace')
|
|
917
|
+
.option('--idempotency-key <key>')
|
|
918
|
+
.action(
|
|
919
|
+
async (
|
|
920
|
+
recipient: string,
|
|
921
|
+
options: {
|
|
922
|
+
from: string;
|
|
923
|
+
actorKind: string;
|
|
924
|
+
actorName?: string;
|
|
925
|
+
subject: string;
|
|
926
|
+
body: string;
|
|
927
|
+
tag: string[];
|
|
928
|
+
visibility: 'private' | 'workspace' | 'public';
|
|
929
|
+
idempotencyKey?: string;
|
|
930
|
+
},
|
|
931
|
+
command: Command,
|
|
932
|
+
) => {
|
|
933
|
+
const global = globals(command);
|
|
934
|
+
const result = await withClient(
|
|
935
|
+
global.home,
|
|
936
|
+
actor(options.actorKind, options.from, options.actorName),
|
|
937
|
+
(client) =>
|
|
938
|
+
client.memos.send({
|
|
939
|
+
recipientAgentId: recipient,
|
|
940
|
+
subject: options.subject,
|
|
941
|
+
body: options.body,
|
|
942
|
+
visibility: options.visibility,
|
|
943
|
+
...(options.tag.length ? { tags: options.tag } : {}),
|
|
944
|
+
...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
|
|
945
|
+
}),
|
|
946
|
+
);
|
|
947
|
+
output(
|
|
948
|
+
io,
|
|
949
|
+
global.json,
|
|
950
|
+
result,
|
|
951
|
+
`${result.deduplicated ? 'Found existing' : 'Sent'} memo to ${result.record.event.recipientDisplayName}\nSubject: ${result.record.event.subject}\nID: ${result.record.event.id}`,
|
|
952
|
+
);
|
|
953
|
+
},
|
|
954
|
+
);
|
|
955
|
+
memoCommand
|
|
956
|
+
.command('list')
|
|
957
|
+
.option('--participant <agent>')
|
|
958
|
+
.option('--status <status>')
|
|
959
|
+
.option('--limit <number>', 'maximum results', '10')
|
|
960
|
+
.option('--cursor <cursor>')
|
|
961
|
+
.action(
|
|
962
|
+
async (
|
|
963
|
+
options: { participant?: string; status?: string; limit: string; cursor?: string },
|
|
964
|
+
command: Command,
|
|
965
|
+
) => {
|
|
966
|
+
const global = globals(command);
|
|
967
|
+
const page = await withClient(global.home, actor('human', 'local-cli'), (client) =>
|
|
968
|
+
client.memos.list({
|
|
969
|
+
...(options.participant ? { participantAgentId: options.participant } : {}),
|
|
970
|
+
...(options.status ? { status: options.status } : {}),
|
|
971
|
+
limit: Number(options.limit),
|
|
972
|
+
...(options.cursor ? { cursor: options.cursor } : {}),
|
|
973
|
+
}),
|
|
974
|
+
);
|
|
975
|
+
output(io, global.json, page, page.items.map(lineForItem).join('\n') || 'No memos found.');
|
|
976
|
+
},
|
|
977
|
+
);
|
|
978
|
+
memoCommand.command('show <memo-id>').action(async (id: string, _options, command: Command) => {
|
|
979
|
+
const global = globals(command);
|
|
980
|
+
const record = await withClient(global.home, actor('human', 'local-cli'), (client) =>
|
|
981
|
+
client.memos.get(id),
|
|
982
|
+
);
|
|
983
|
+
output(
|
|
984
|
+
io,
|
|
985
|
+
global.json,
|
|
986
|
+
record,
|
|
987
|
+
`${record.event.subject}\nID: ${record.event.id}\nStatus: ${record.status}\n\n${record.event.body}`,
|
|
988
|
+
);
|
|
989
|
+
});
|
|
990
|
+
for (const operation of ['read', 'archive'] as const) {
|
|
991
|
+
memoCommand
|
|
992
|
+
.command(`${operation} <memo-id>`)
|
|
993
|
+
.requiredOption('--as <agent-id>')
|
|
994
|
+
.option('--actor-kind <kind>', 'agent or human', 'agent')
|
|
995
|
+
.option('--idempotency-key <key>')
|
|
996
|
+
.action(
|
|
997
|
+
async (
|
|
998
|
+
id: string,
|
|
999
|
+
options: { as: string; actorKind: string; idempotencyKey?: string },
|
|
1000
|
+
command: Command,
|
|
1001
|
+
) => {
|
|
1002
|
+
const global = globals(command);
|
|
1003
|
+
const record = await withClient(
|
|
1004
|
+
global.home,
|
|
1005
|
+
actor(options.actorKind, options.as),
|
|
1006
|
+
(client) =>
|
|
1007
|
+
client.memos[operation]({
|
|
1008
|
+
memoId: id,
|
|
1009
|
+
...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
|
|
1010
|
+
}),
|
|
1011
|
+
);
|
|
1012
|
+
output(io, global.json, record, `Memo ${id} is ${record.status}.`);
|
|
1013
|
+
},
|
|
1014
|
+
);
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
const noteCommand = program
|
|
1018
|
+
.command('note')
|
|
1019
|
+
.description('Retain and revise agent-owned knowledge');
|
|
1020
|
+
noteCommand
|
|
1021
|
+
.command('create')
|
|
1022
|
+
.requiredOption('--as <actor-id>')
|
|
1023
|
+
.option('--actor-kind <kind>', 'agent or human', 'agent')
|
|
1024
|
+
.option('--owner <agent-id>')
|
|
1025
|
+
.requiredOption('--title <title>')
|
|
1026
|
+
.requiredOption('--body <body>')
|
|
1027
|
+
.option('--tag <tag>', 'tag (repeatable)', collect, [])
|
|
1028
|
+
.option('--idempotency-key <key>')
|
|
1029
|
+
.action(
|
|
1030
|
+
async (
|
|
1031
|
+
options: {
|
|
1032
|
+
as: string;
|
|
1033
|
+
actorKind: string;
|
|
1034
|
+
owner?: string;
|
|
1035
|
+
title: string;
|
|
1036
|
+
body: string;
|
|
1037
|
+
tag: string[];
|
|
1038
|
+
idempotencyKey?: string;
|
|
1039
|
+
},
|
|
1040
|
+
command: Command,
|
|
1041
|
+
) => {
|
|
1042
|
+
const global = globals(command);
|
|
1043
|
+
const result = await withClient(
|
|
1044
|
+
global.home,
|
|
1045
|
+
actor(options.actorKind, options.as),
|
|
1046
|
+
(client) =>
|
|
1047
|
+
client.notes.create({
|
|
1048
|
+
...(options.owner ? { ownerAgentId: options.owner } : {}),
|
|
1049
|
+
title: options.title,
|
|
1050
|
+
body: options.body,
|
|
1051
|
+
...(options.tag.length ? { tags: options.tag } : {}),
|
|
1052
|
+
...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
|
|
1053
|
+
}),
|
|
1054
|
+
);
|
|
1055
|
+
output(
|
|
1056
|
+
io,
|
|
1057
|
+
global.json,
|
|
1058
|
+
result,
|
|
1059
|
+
`${result.deduplicated ? 'Found existing' : 'Created'} note\nTitle: ${result.record.current.title}\nID: ${result.record.event.id}`,
|
|
1060
|
+
);
|
|
1061
|
+
},
|
|
1062
|
+
);
|
|
1063
|
+
noteCommand
|
|
1064
|
+
.command('list')
|
|
1065
|
+
.option('--owner <agent>')
|
|
1066
|
+
.option('--status <status>')
|
|
1067
|
+
.option('--limit <number>', 'maximum results', '10')
|
|
1068
|
+
.action(
|
|
1069
|
+
async (options: { owner?: string; status?: string; limit: string }, command: Command) => {
|
|
1070
|
+
const global = globals(command);
|
|
1071
|
+
const page = await withClient(global.home, actor('human', 'local-cli'), (client) =>
|
|
1072
|
+
client.notes.list({
|
|
1073
|
+
...(options.owner ? { participantAgentId: options.owner } : {}),
|
|
1074
|
+
...(options.status ? { status: options.status } : {}),
|
|
1075
|
+
limit: Number(options.limit),
|
|
1076
|
+
}),
|
|
1077
|
+
);
|
|
1078
|
+
output(io, global.json, page, page.items.map(lineForItem).join('\n') || 'No notes found.');
|
|
1079
|
+
},
|
|
1080
|
+
);
|
|
1081
|
+
noteCommand.command('show <note-id>').action(async (id: string, _options, command: Command) => {
|
|
1082
|
+
const global = globals(command);
|
|
1083
|
+
const record = await withClient(global.home, actor('human', 'local-cli'), (client) =>
|
|
1084
|
+
client.notes.get(id),
|
|
1085
|
+
);
|
|
1086
|
+
output(
|
|
1087
|
+
io,
|
|
1088
|
+
global.json,
|
|
1089
|
+
record,
|
|
1090
|
+
`${record.current.title}\nID: ${record.event.id}\nVersion: ${record.current.version}\n\n${record.current.body}`,
|
|
1091
|
+
);
|
|
1092
|
+
});
|
|
1093
|
+
noteCommand
|
|
1094
|
+
.command('revise <note-id>')
|
|
1095
|
+
.requiredOption('--as <actor-id>')
|
|
1096
|
+
.option('--actor-kind <kind>', 'agent or human', 'agent')
|
|
1097
|
+
.requiredOption('--expected-version <number>')
|
|
1098
|
+
.option('--title <title>')
|
|
1099
|
+
.option('--body <body>')
|
|
1100
|
+
.option('--tag <tag>', 'replace tags', collect, [])
|
|
1101
|
+
.option('--idempotency-key <key>')
|
|
1102
|
+
.action(
|
|
1103
|
+
async (
|
|
1104
|
+
id: string,
|
|
1105
|
+
options: {
|
|
1106
|
+
as: string;
|
|
1107
|
+
actorKind: string;
|
|
1108
|
+
expectedVersion: string;
|
|
1109
|
+
title?: string;
|
|
1110
|
+
body?: string;
|
|
1111
|
+
tag: string[];
|
|
1112
|
+
idempotencyKey?: string;
|
|
1113
|
+
},
|
|
1114
|
+
command: Command,
|
|
1115
|
+
) => {
|
|
1116
|
+
const global = globals(command);
|
|
1117
|
+
const record = await withClient(
|
|
1118
|
+
global.home,
|
|
1119
|
+
actor(options.actorKind, options.as),
|
|
1120
|
+
(client) =>
|
|
1121
|
+
client.notes.revise({
|
|
1122
|
+
noteId: id,
|
|
1123
|
+
expectedVersion: Number(options.expectedVersion),
|
|
1124
|
+
...(options.title ? { title: options.title } : {}),
|
|
1125
|
+
...(options.body ? { body: options.body } : {}),
|
|
1126
|
+
...(options.tag.length ? { tags: options.tag } : {}),
|
|
1127
|
+
...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
|
|
1128
|
+
}),
|
|
1129
|
+
);
|
|
1130
|
+
output(io, global.json, record, `Revised note ${id} to version ${record.current.version}.`);
|
|
1131
|
+
},
|
|
1132
|
+
);
|
|
1133
|
+
noteCommand
|
|
1134
|
+
.command('archive <note-id>')
|
|
1135
|
+
.requiredOption('--as <actor-id>')
|
|
1136
|
+
.option('--actor-kind <kind>', 'agent or human', 'agent')
|
|
1137
|
+
.option('--idempotency-key <key>')
|
|
1138
|
+
.action(
|
|
1139
|
+
async (
|
|
1140
|
+
id: string,
|
|
1141
|
+
options: { as: string; actorKind: string; idempotencyKey?: string },
|
|
1142
|
+
command: Command,
|
|
1143
|
+
) => {
|
|
1144
|
+
const global = globals(command);
|
|
1145
|
+
const record = await withClient(
|
|
1146
|
+
global.home,
|
|
1147
|
+
actor(options.actorKind, options.as),
|
|
1148
|
+
(client) =>
|
|
1149
|
+
client.notes.archive({
|
|
1150
|
+
noteId: id,
|
|
1151
|
+
...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
|
|
1152
|
+
}),
|
|
1153
|
+
);
|
|
1154
|
+
output(io, global.json, record, `Archived note ${id}.`);
|
|
1155
|
+
},
|
|
1156
|
+
);
|
|
1157
|
+
|
|
1158
|
+
const todoCommand = program.command('todo').description('Create and manage agent todos');
|
|
1159
|
+
todoCommand
|
|
1160
|
+
.command('create <assignee>')
|
|
1161
|
+
.requiredOption('--from <actor-id>')
|
|
1162
|
+
.option('--actor-kind <kind>', 'human, agent, or system', 'agent')
|
|
1163
|
+
.requiredOption('--title <title>')
|
|
1164
|
+
.option('--description <text>')
|
|
1165
|
+
.option('--priority <number>', '1 highest, 4 lowest', '3')
|
|
1166
|
+
.option('--due-date <date>')
|
|
1167
|
+
.option('--due-at <datetime>')
|
|
1168
|
+
.option('--time-zone <iana-zone>')
|
|
1169
|
+
.option('--tag <tag>', 'tag (repeatable)', collect, [])
|
|
1170
|
+
.option('--visibility <visibility>', 'private, workspace, or public', 'workspace')
|
|
1171
|
+
.option('--idempotency-key <key>')
|
|
1172
|
+
.action(
|
|
1173
|
+
async (
|
|
1174
|
+
assignee: string,
|
|
1175
|
+
options: {
|
|
1176
|
+
from: string;
|
|
1177
|
+
actorKind: string;
|
|
1178
|
+
title: string;
|
|
1179
|
+
description?: string;
|
|
1180
|
+
priority: string;
|
|
1181
|
+
dueDate?: string;
|
|
1182
|
+
dueAt?: string;
|
|
1183
|
+
timeZone?: string;
|
|
1184
|
+
tag: string[];
|
|
1185
|
+
visibility: 'private' | 'workspace' | 'public';
|
|
1186
|
+
idempotencyKey?: string;
|
|
1187
|
+
},
|
|
1188
|
+
command: Command,
|
|
1189
|
+
) => {
|
|
1190
|
+
const global = globals(command);
|
|
1191
|
+
const result = await withClient(
|
|
1192
|
+
global.home,
|
|
1193
|
+
actor(options.actorKind, options.from),
|
|
1194
|
+
(client) =>
|
|
1195
|
+
client.todos.create({
|
|
1196
|
+
assigneeAgentId: assignee,
|
|
1197
|
+
title: options.title,
|
|
1198
|
+
...(options.description ? { description: options.description } : {}),
|
|
1199
|
+
priority: Number(options.priority) as 1 | 2 | 3 | 4,
|
|
1200
|
+
...((due) => (due ? { due } : {}))(todoDue(options)),
|
|
1201
|
+
...(options.tag.length ? { tags: options.tag } : {}),
|
|
1202
|
+
visibility: options.visibility,
|
|
1203
|
+
...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
|
|
1204
|
+
}),
|
|
1205
|
+
);
|
|
1206
|
+
output(
|
|
1207
|
+
io,
|
|
1208
|
+
global.json,
|
|
1209
|
+
result,
|
|
1210
|
+
`${result.deduplicated ? 'Found existing' : 'Created'} todo for ${result.record.event.assigneeDisplayName}\nTitle: ${result.record.current.title}\nID: ${result.record.event.id}`,
|
|
1211
|
+
);
|
|
1212
|
+
},
|
|
1213
|
+
);
|
|
1214
|
+
todoCommand
|
|
1215
|
+
.command('list')
|
|
1216
|
+
.option('--assignee <agent>')
|
|
1217
|
+
.option('--status <status>')
|
|
1218
|
+
.option('--limit <number>', 'maximum results', '10')
|
|
1219
|
+
.action(
|
|
1220
|
+
async (options: { assignee?: string; status?: string; limit: string }, command: Command) => {
|
|
1221
|
+
const global = globals(command);
|
|
1222
|
+
const page = await withClient(global.home, actor('human', 'local-cli'), (client) =>
|
|
1223
|
+
client.todos.list({
|
|
1224
|
+
...(options.assignee ? { participantAgentId: options.assignee } : {}),
|
|
1225
|
+
...(options.status ? { status: options.status } : {}),
|
|
1226
|
+
limit: Number(options.limit),
|
|
1227
|
+
}),
|
|
1228
|
+
);
|
|
1229
|
+
output(io, global.json, page, page.items.map(lineForItem).join('\n') || 'No todos found.');
|
|
1230
|
+
},
|
|
1231
|
+
);
|
|
1232
|
+
todoCommand.command('show <todo-id>').action(async (id: string, _options, command: Command) => {
|
|
1233
|
+
const global = globals(command);
|
|
1234
|
+
const record = await withClient(global.home, actor('human', 'local-cli'), (client) =>
|
|
1235
|
+
client.todos.get(id),
|
|
1236
|
+
);
|
|
1237
|
+
output(
|
|
1238
|
+
io,
|
|
1239
|
+
global.json,
|
|
1240
|
+
record,
|
|
1241
|
+
`${record.current.title}\nID: ${record.event.id}\nStatus: ${record.status}\nVersion: ${record.current.version}`,
|
|
1242
|
+
);
|
|
1243
|
+
});
|
|
1244
|
+
todoCommand
|
|
1245
|
+
.command('update <todo-id>')
|
|
1246
|
+
.requiredOption('--as <actor-id>')
|
|
1247
|
+
.option('--actor-kind <kind>', 'agent or human', 'agent')
|
|
1248
|
+
.requiredOption('--expected-version <number>')
|
|
1249
|
+
.option('--title <title>')
|
|
1250
|
+
.option('--description <text>')
|
|
1251
|
+
.option('--priority <number>')
|
|
1252
|
+
.option('--due-date <date>')
|
|
1253
|
+
.option('--due-at <datetime>')
|
|
1254
|
+
.option('--time-zone <iana-zone>')
|
|
1255
|
+
.option('--clear-due')
|
|
1256
|
+
.option('--visibility <visibility>')
|
|
1257
|
+
.option('--idempotency-key <key>')
|
|
1258
|
+
.action(
|
|
1259
|
+
async (
|
|
1260
|
+
id: string,
|
|
1261
|
+
options: {
|
|
1262
|
+
as: string;
|
|
1263
|
+
actorKind: string;
|
|
1264
|
+
expectedVersion: string;
|
|
1265
|
+
title?: string;
|
|
1266
|
+
description?: string;
|
|
1267
|
+
priority?: string;
|
|
1268
|
+
dueDate?: string;
|
|
1269
|
+
dueAt?: string;
|
|
1270
|
+
timeZone?: string;
|
|
1271
|
+
clearDue?: boolean;
|
|
1272
|
+
visibility?: 'private' | 'workspace' | 'public';
|
|
1273
|
+
idempotencyKey?: string;
|
|
1274
|
+
},
|
|
1275
|
+
command: Command,
|
|
1276
|
+
) => {
|
|
1277
|
+
const global = globals(command);
|
|
1278
|
+
const parsedDue = todoDue(options);
|
|
1279
|
+
const record = await withClient(
|
|
1280
|
+
global.home,
|
|
1281
|
+
actor(options.actorKind, options.as),
|
|
1282
|
+
(client) =>
|
|
1283
|
+
client.todos.update({
|
|
1284
|
+
todoId: id,
|
|
1285
|
+
expectedVersion: Number(options.expectedVersion),
|
|
1286
|
+
...(options.title ? { title: options.title } : {}),
|
|
1287
|
+
...(options.description !== undefined ? { description: options.description } : {}),
|
|
1288
|
+
...(options.priority ? { priority: Number(options.priority) as 1 | 2 | 3 | 4 } : {}),
|
|
1289
|
+
...(options.clearDue ? { due: null } : parsedDue ? { due: parsedDue } : {}),
|
|
1290
|
+
...(options.visibility ? { visibility: options.visibility } : {}),
|
|
1291
|
+
...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
|
|
1292
|
+
}),
|
|
1293
|
+
);
|
|
1294
|
+
output(io, global.json, record, `Updated todo ${id} to version ${record.current.version}.`);
|
|
1295
|
+
},
|
|
1296
|
+
);
|
|
1297
|
+
for (const operation of ['accept', 'reject', 'complete', 'reopen', 'cancel'] as const) {
|
|
1298
|
+
todoCommand
|
|
1299
|
+
.command(`${operation} <todo-id>`)
|
|
1300
|
+
.requiredOption('--as <actor-id>')
|
|
1301
|
+
.option('--actor-kind <kind>', 'agent or human', 'agent')
|
|
1302
|
+
.option('--note <text>')
|
|
1303
|
+
.option('--reason <text>')
|
|
1304
|
+
.option('--idempotency-key <key>')
|
|
1305
|
+
.action(
|
|
1306
|
+
async (
|
|
1307
|
+
id: string,
|
|
1308
|
+
options: {
|
|
1309
|
+
as: string;
|
|
1310
|
+
actorKind: string;
|
|
1311
|
+
note?: string;
|
|
1312
|
+
reason?: string;
|
|
1313
|
+
idempotencyKey?: string;
|
|
1314
|
+
},
|
|
1315
|
+
command: Command,
|
|
1316
|
+
) => {
|
|
1317
|
+
const global = globals(command);
|
|
1318
|
+
const record = await withClient(
|
|
1319
|
+
global.home,
|
|
1320
|
+
actor(options.actorKind, options.as),
|
|
1321
|
+
(client) =>
|
|
1322
|
+
operation === 'accept'
|
|
1323
|
+
? client.todos.accept({
|
|
1324
|
+
todoId: id,
|
|
1325
|
+
...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
|
|
1326
|
+
})
|
|
1327
|
+
: operation === 'reject'
|
|
1328
|
+
? client.todos.reject({
|
|
1329
|
+
todoId: id,
|
|
1330
|
+
...(options.reason ? { reason: options.reason } : {}),
|
|
1331
|
+
...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
|
|
1332
|
+
})
|
|
1333
|
+
: operation === 'complete'
|
|
1334
|
+
? client.todos.complete({
|
|
1335
|
+
todoId: id,
|
|
1336
|
+
...(options.note ? { note: options.note } : {}),
|
|
1337
|
+
...(options.idempotencyKey
|
|
1338
|
+
? { idempotencyKey: options.idempotencyKey }
|
|
1339
|
+
: {}),
|
|
1340
|
+
})
|
|
1341
|
+
: operation === 'cancel'
|
|
1342
|
+
? client.todos.cancel({
|
|
1343
|
+
todoId: id,
|
|
1344
|
+
...(options.reason ? { reason: options.reason } : {}),
|
|
1345
|
+
...(options.idempotencyKey
|
|
1346
|
+
? { idempotencyKey: options.idempotencyKey }
|
|
1347
|
+
: {}),
|
|
1348
|
+
})
|
|
1349
|
+
: client.todos.reopen({
|
|
1350
|
+
todoId: id,
|
|
1351
|
+
...(options.idempotencyKey
|
|
1352
|
+
? { idempotencyKey: options.idempotencyKey }
|
|
1353
|
+
: {}),
|
|
1354
|
+
}),
|
|
1355
|
+
);
|
|
1356
|
+
output(io, global.json, record, `Todo ${id} is ${record.status}.`);
|
|
1357
|
+
},
|
|
1358
|
+
);
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
kudosCommand
|
|
1362
|
+
.command('show <kudos-id>')
|
|
1363
|
+
.description('Show one kudos item and its current state')
|
|
1364
|
+
.action(async (id: string, _options, command: Command) => {
|
|
1365
|
+
const global = globals(command);
|
|
1366
|
+
const record = await withClient(global.home, actor('system', 'cli'), (client) =>
|
|
1367
|
+
client.kudos.get(id),
|
|
1368
|
+
);
|
|
1369
|
+
output(io, global.json, record, showRecord(record));
|
|
1370
|
+
});
|
|
1371
|
+
|
|
1372
|
+
kudosCommand
|
|
1373
|
+
.command('acknowledge <kudos-id>')
|
|
1374
|
+
.description('Record that a recipient reviewed kudos')
|
|
1375
|
+
.requiredOption('--as <agent-id>', 'recipient agent identity')
|
|
1376
|
+
.option('--actor-kind <kind>', 'agent, human, or system', 'agent')
|
|
1377
|
+
.option('--name <display-name>')
|
|
1378
|
+
.option('--note <text>')
|
|
1379
|
+
.action(
|
|
1380
|
+
async (
|
|
1381
|
+
id: string,
|
|
1382
|
+
options: { as: string; actorKind: string; name?: string; note?: string },
|
|
1383
|
+
command: Command,
|
|
1384
|
+
) => {
|
|
1385
|
+
const global = globals(command);
|
|
1386
|
+
const record = await withClient(
|
|
1387
|
+
global.home,
|
|
1388
|
+
actor(options.actorKind, options.as, options.name),
|
|
1389
|
+
(client) =>
|
|
1390
|
+
client.kudos.acknowledge({
|
|
1391
|
+
kudosId: id,
|
|
1392
|
+
...(options.note ? { note: options.note } : {}),
|
|
1393
|
+
}),
|
|
1394
|
+
);
|
|
1395
|
+
output(io, global.json, record, `Acknowledged ${id} as ${options.as}.`);
|
|
1396
|
+
},
|
|
1397
|
+
);
|
|
1398
|
+
|
|
1399
|
+
kudosCommand
|
|
1400
|
+
.command('revoke <kudos-id>')
|
|
1401
|
+
.description('Record a revocation while preserving history')
|
|
1402
|
+
.requiredOption('--as <actor-id>')
|
|
1403
|
+
.option('--actor-kind <kind>', 'human, agent, or system', 'human')
|
|
1404
|
+
.requiredOption('--reason <reason>')
|
|
1405
|
+
.option('--administrative', 'mark as an administrative revocation', false)
|
|
1406
|
+
.action(
|
|
1407
|
+
async (
|
|
1408
|
+
id: string,
|
|
1409
|
+
options: { as: string; actorKind: string; reason: string; administrative: boolean },
|
|
1410
|
+
command: Command,
|
|
1411
|
+
) => {
|
|
1412
|
+
const global = globals(command);
|
|
1413
|
+
const record = await withClient(
|
|
1414
|
+
global.home,
|
|
1415
|
+
actor(options.actorKind, options.as),
|
|
1416
|
+
(client) =>
|
|
1417
|
+
client.kudos.revoke({
|
|
1418
|
+
kudosId: id,
|
|
1419
|
+
reason: options.reason,
|
|
1420
|
+
administrative: options.administrative,
|
|
1421
|
+
}),
|
|
1422
|
+
);
|
|
1423
|
+
output(io, global.json, record, `Revoked ${id}; the audit trail was preserved.`);
|
|
1424
|
+
},
|
|
1425
|
+
);
|
|
1426
|
+
|
|
1427
|
+
kudosCommand
|
|
1428
|
+
.command('wins [agent]')
|
|
1429
|
+
.description('Print the generated WINS.md path or content')
|
|
1430
|
+
.option('--open', 'open WINS.md in the system GUI', false)
|
|
1431
|
+
.option('--print', 'print Markdown content', false)
|
|
1432
|
+
.action(
|
|
1433
|
+
async (
|
|
1434
|
+
agentId: string | undefined,
|
|
1435
|
+
options: { open: boolean; print: boolean },
|
|
1436
|
+
command: Command,
|
|
1437
|
+
) => {
|
|
1438
|
+
const global = globals(command);
|
|
1439
|
+
if (!agentId) throw new SynomemError('INVALID_INPUT', 'Specify an agent.');
|
|
1440
|
+
const details = await withClient(global.home, actor('system', 'cli'), async (client) => {
|
|
1441
|
+
const profile = await client.agents.get(agentId);
|
|
1442
|
+
const info = await client.info();
|
|
1443
|
+
if (info.backend !== 'local') {
|
|
1444
|
+
throw new SynomemError(
|
|
1445
|
+
'INVALID_INPUT',
|
|
1446
|
+
'Generated WINS.md files are available only with the local backend.',
|
|
1447
|
+
);
|
|
1448
|
+
}
|
|
1449
|
+
const capabilities = await client.capabilities();
|
|
1450
|
+
const path = join(info.home, profile.id, 'WINS.md');
|
|
1451
|
+
if (!existsSync(path)) {
|
|
1452
|
+
const hint = capabilities.projections.writeWinsMarkdown
|
|
1453
|
+
? 'Run `synomem rebuild` to generate it.'
|
|
1454
|
+
: 'Enable projection.writeWinsMarkdown and run `synomem rebuild`.';
|
|
1455
|
+
throw new SynomemError(
|
|
1456
|
+
'INVALID_INPUT',
|
|
1457
|
+
`No generated WINS.md exists for ${profile.id}. ${hint}`,
|
|
1458
|
+
);
|
|
1459
|
+
}
|
|
1460
|
+
return { profile, path, content: readFileSync(path, 'utf8') };
|
|
1461
|
+
});
|
|
1462
|
+
if (options.open) {
|
|
1463
|
+
const commandName =
|
|
1464
|
+
process.platform === 'darwin'
|
|
1465
|
+
? 'open'
|
|
1466
|
+
: process.platform === 'win32'
|
|
1467
|
+
? 'cmd'
|
|
1468
|
+
: 'xdg-open';
|
|
1469
|
+
const args =
|
|
1470
|
+
process.platform === 'win32' ? ['/c', 'start', '', details.path] : [details.path];
|
|
1471
|
+
spawn(commandName, args, { detached: true, stdio: 'ignore' }).unref();
|
|
1472
|
+
}
|
|
1473
|
+
output(io, global.json, details, options.print ? details.content.trimEnd() : details.path);
|
|
1474
|
+
},
|
|
1475
|
+
);
|
|
1476
|
+
|
|
1477
|
+
addListOptions(
|
|
1478
|
+
kudosCommand.command('stats').description('Show aggregate kudos statistics'),
|
|
1479
|
+
).action(async (options: Record<string, string>, command: Command) => {
|
|
1480
|
+
const global = globals(command);
|
|
1481
|
+
const stats = await withClient(global.home, actor('system', 'cli'), (client) =>
|
|
1482
|
+
client.stats(listInput(options)),
|
|
1483
|
+
);
|
|
1484
|
+
output(
|
|
1485
|
+
io,
|
|
1486
|
+
global.json,
|
|
1487
|
+
stats,
|
|
1488
|
+
`Total: ${stats.total}\nActive: ${stats.active}\nAcknowledged: ${stats.acknowledged}\nRevoked: ${stats.revoked}`,
|
|
1489
|
+
);
|
|
1490
|
+
});
|
|
1491
|
+
|
|
1492
|
+
program
|
|
1493
|
+
.command('rebuild')
|
|
1494
|
+
.description('Regenerate current-state and filesystem projections from canonical events')
|
|
1495
|
+
.action(async (_options, command: Command) => {
|
|
1496
|
+
const global = globals(command);
|
|
1497
|
+
const result = await withClient(global.home, actor('system', 'cli'), (client) =>
|
|
1498
|
+
client.rebuild(),
|
|
1499
|
+
);
|
|
1500
|
+
output(
|
|
1501
|
+
io,
|
|
1502
|
+
global.json,
|
|
1503
|
+
result,
|
|
1504
|
+
`Rebuilt ${result.generated.length} file(s); removed ${result.removed.length} stale file(s).`,
|
|
1505
|
+
);
|
|
1506
|
+
});
|
|
1507
|
+
|
|
1508
|
+
program
|
|
1509
|
+
.command('backup <destination>')
|
|
1510
|
+
.description('Create a transactionally consistent SQLite backup')
|
|
1511
|
+
.action(async (destination: string, _options, command: Command) => {
|
|
1512
|
+
const global = globals(command);
|
|
1513
|
+
const path = await withClient(global.home, actor('system', 'cli'), (client) => {
|
|
1514
|
+
if (!client.backup) {
|
|
1515
|
+
throw new SynomemError(
|
|
1516
|
+
'INVALID_INPUT',
|
|
1517
|
+
'Filesystem backup is available only with the local backend.',
|
|
1518
|
+
);
|
|
1519
|
+
}
|
|
1520
|
+
return client.backup(destination);
|
|
1521
|
+
});
|
|
1522
|
+
output(io, global.json, { path }, `Created backup at ${path}`);
|
|
1523
|
+
});
|
|
1524
|
+
|
|
1525
|
+
program
|
|
1526
|
+
.command('export')
|
|
1527
|
+
.description('Export canonical events for portability')
|
|
1528
|
+
.addOption(
|
|
1529
|
+
new Option('--format <format>').choices(['json', 'jsonl', 'markdown']).default('json'),
|
|
1530
|
+
)
|
|
1531
|
+
.option('--output <path>', 'write to an explicit destination instead of stdout')
|
|
1532
|
+
.action(
|
|
1533
|
+
async (
|
|
1534
|
+
options: { format: 'json' | 'jsonl' | 'markdown'; output?: string },
|
|
1535
|
+
command: Command,
|
|
1536
|
+
) => {
|
|
1537
|
+
const global = globals(command);
|
|
1538
|
+
const content = await withClient(global.home, actor('system', 'cli'), (client) =>
|
|
1539
|
+
client.export(options.format),
|
|
1540
|
+
);
|
|
1541
|
+
if (options.output) {
|
|
1542
|
+
const destination = resolve(options.output);
|
|
1543
|
+
atomicWriteFile(destination, content, 0o600);
|
|
1544
|
+
output(
|
|
1545
|
+
io,
|
|
1546
|
+
global.json,
|
|
1547
|
+
{ path: destination, format: options.format },
|
|
1548
|
+
`Exported ${options.format} to ${destination}`,
|
|
1549
|
+
);
|
|
1550
|
+
} else {
|
|
1551
|
+
io.stdout(content);
|
|
1552
|
+
}
|
|
1553
|
+
},
|
|
1554
|
+
);
|
|
1555
|
+
|
|
1556
|
+
program
|
|
1557
|
+
.command('doctor')
|
|
1558
|
+
.description('Run safe diagnostics')
|
|
1559
|
+
.action(async (_options, command: Command) => {
|
|
1560
|
+
const global = globals(command);
|
|
1561
|
+
const result = await withClient(global.home, actor('system', 'cli'), (client) =>
|
|
1562
|
+
client.doctor(),
|
|
1563
|
+
);
|
|
1564
|
+
const human = result.diagnostics
|
|
1565
|
+
.map((item) => `${item.level.toUpperCase().padEnd(7)} ${item.code}: ${item.message}`)
|
|
1566
|
+
.join('\n');
|
|
1567
|
+
output(io, global.json, result, human);
|
|
1568
|
+
if (!result.healthy) cliExitCodes.set(program, 5);
|
|
1569
|
+
});
|
|
1570
|
+
|
|
1571
|
+
program
|
|
1572
|
+
.command('mcp')
|
|
1573
|
+
.description('Run the actor-bound MCP server over stdio')
|
|
1574
|
+
.requiredOption('--actor-id <id>')
|
|
1575
|
+
.requiredOption('--actor-kind <kind>', 'human, agent, or system')
|
|
1576
|
+
.option('--actor-name <display-name>')
|
|
1577
|
+
.action(
|
|
1578
|
+
async (
|
|
1579
|
+
options: { actorId: string; actorKind: string; actorName?: string },
|
|
1580
|
+
command: Command,
|
|
1581
|
+
) => {
|
|
1582
|
+
const global = globals(command);
|
|
1583
|
+
await startMcpServer({
|
|
1584
|
+
...(global.home ? { home: global.home } : {}),
|
|
1585
|
+
actor: actor(options.actorKind, options.actorId, options.actorName),
|
|
1586
|
+
});
|
|
1587
|
+
},
|
|
1588
|
+
);
|
|
1589
|
+
|
|
1590
|
+
return program;
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
export async function runCli(
|
|
1594
|
+
argv = process.argv,
|
|
1595
|
+
io: CliIo = defaultIo,
|
|
1596
|
+
serviceFactory: SynomemServiceFactory = configuredServiceFactory,
|
|
1597
|
+
dependencies: CliDependencies = {},
|
|
1598
|
+
): Promise<number> {
|
|
1599
|
+
const program = createCli(io, serviceFactory, dependencies);
|
|
1600
|
+
program.exitOverride();
|
|
1601
|
+
try {
|
|
1602
|
+
await program.parseAsync(argv);
|
|
1603
|
+
return cliExitCodes.get(program) ?? 0;
|
|
1604
|
+
} catch (error) {
|
|
1605
|
+
if (error instanceof CommanderError && error.exitCode === 0) return 0;
|
|
1606
|
+
if (error instanceof CommanderError) {
|
|
1607
|
+
if (!error.message.startsWith('error:')) io.stderr(`${error.message}\n`);
|
|
1608
|
+
return 2;
|
|
1609
|
+
}
|
|
1610
|
+
const kudosError = asSynomemError(error);
|
|
1611
|
+
const json = argv.includes('--json');
|
|
1612
|
+
io.stderr(
|
|
1613
|
+
json
|
|
1614
|
+
? `${JSON.stringify({ ok: false, error: { code: kudosError.code, message: kudosError.message } })}\n`
|
|
1615
|
+
: `Error [${kudosError.code}]: ${kudosError.message}\n`,
|
|
1616
|
+
);
|
|
1617
|
+
return exitCode(kudosError.code);
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
|
|
1622
|
+
process.exitCode = await runCli();
|
|
1623
|
+
}
|