synomem 0.5.3 → 0.6.1
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/CHANGELOG.md +43 -0
- package/README.md +30 -12
- package/dist/cli.d.ts +5 -3
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +245 -47
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +10 -0
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +4 -0
- package/dist/config.js.map +1 -1
- package/dist/configure.d.ts.map +1 -1
- package/dist/configure.js +5 -4
- package/dist/configure.js.map +1 -1
- package/dist/discover.d.ts +9 -11
- package/dist/discover.d.ts.map +1 -1
- package/dist/discover.js +14 -15
- package/dist/discover.js.map +1 -1
- package/dist/import.d.ts +4 -4
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +46 -0
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp-server.js +26 -3
- package/dist/mcp-server.js.map +1 -1
- package/dist/ports/projections.d.ts +8 -0
- package/dist/ports/projections.d.ts.map +1 -1
- package/dist/project.d.ts +51 -0
- package/dist/project.d.ts.map +1 -0
- package/dist/project.js +143 -0
- package/dist/project.js.map +1 -0
- package/dist/projections.d.ts +14 -0
- package/dist/projections.d.ts.map +1 -1
- package/dist/projections.js +36 -1
- package/dist/projections.js.map +1 -1
- package/dist/remote.d.ts.map +1 -1
- package/dist/remote.js +9 -0
- package/dist/remote.js.map +1 -1
- package/dist/schemas.d.ts +18 -4
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +29 -2
- package/dist/schemas.js.map +1 -1
- package/dist/service.d.ts +1 -0
- package/dist/service.d.ts.map +1 -1
- package/dist/storage.d.ts +13 -0
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +24 -0
- package/dist/storage.js.map +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/workspaces.d.ts +41 -0
- package/dist/workspaces.d.ts.map +1 -0
- package/dist/workspaces.js +96 -0
- package/dist/workspaces.js.map +1 -0
- package/package.json +1 -1
- package/src/cli.ts +305 -50
- package/src/client.ts +10 -0
- package/src/config.ts +4 -0
- package/src/configure.ts +5 -4
- package/src/discover.ts +18 -19
- package/src/index.ts +22 -1
- package/src/mcp/index.ts +62 -0
- package/src/mcp-server.ts +32 -5
- package/src/ports/projections.ts +9 -0
- package/src/project.ts +168 -0
- package/src/projections.ts +38 -1
- package/src/remote.ts +9 -0
- package/src/schemas.ts +44 -12
- package/src/service.ts +1 -0
- package/src/storage.ts +28 -0
- package/src/types.ts +1 -0
- package/src/workspaces.ts +107 -0
package/src/cli.ts
CHANGED
|
@@ -21,8 +21,19 @@ import {
|
|
|
21
21
|
type ConfigPlan,
|
|
22
22
|
type CredentialStoreChoice,
|
|
23
23
|
} from './configure.js';
|
|
24
|
-
import {
|
|
25
|
-
|
|
24
|
+
import {
|
|
25
|
+
discoverAccessKeyWorkspaces,
|
|
26
|
+
discoverOrganizations,
|
|
27
|
+
workspaceChoices,
|
|
28
|
+
type DiscoveredWorkspace,
|
|
29
|
+
} from './discover.js';
|
|
30
|
+
import { DEFAULT_WORKSPACE, listLocalWorkspaces, localWorkspaceHome } from './workspaces.js';
|
|
31
|
+
import {
|
|
32
|
+
findProjectSelection,
|
|
33
|
+
resolveWorkspaceSelection,
|
|
34
|
+
writeProjectSelection,
|
|
35
|
+
} from './project.js';
|
|
36
|
+
import { defaultPromptIo, select, type PromptIo } from './prompt.js';
|
|
26
37
|
import { credentialReference, OsCredentialStore, type CredentialStore } from './credentials.js';
|
|
27
38
|
import { asSynomemError, SynomemError, type SynomemErrorCode } from './errors.js';
|
|
28
39
|
import { atomicWriteFile } from './fs-utils.js';
|
|
@@ -78,12 +89,12 @@ export interface CliDependencies {
|
|
|
78
89
|
}) => Promise<void>;
|
|
79
90
|
/*
|
|
80
91
|
* Injected so setup can be tested without a network. The default asks the
|
|
81
|
-
* service which
|
|
92
|
+
* service which workspaces an access key can reach.
|
|
82
93
|
*/
|
|
83
|
-
|
|
94
|
+
discoverAccessKeyWorkspaces?: (options: {
|
|
84
95
|
baseUrl: string;
|
|
85
96
|
accessToken: string;
|
|
86
|
-
}) => Promise<{
|
|
97
|
+
}) => Promise<{ organizationId: string; workspaces: DiscoveredWorkspace[] }>;
|
|
87
98
|
createImportBundle?: (home: string) => Promise<ImportBundle>;
|
|
88
99
|
remoteImport?: (options: {
|
|
89
100
|
baseUrl: string;
|
|
@@ -148,10 +159,18 @@ function defaultActor(
|
|
|
148
159
|
env: NodeJS.ProcessEnv,
|
|
149
160
|
fallbackKind: string,
|
|
150
161
|
fallbackId: string,
|
|
162
|
+
/** `--actor`, which outranks the environment: it is said on this invocation. */
|
|
163
|
+
override?: string,
|
|
151
164
|
): ActorIdentity {
|
|
152
|
-
const id = env.SYNOMEM_ACTOR_ID?.trim();
|
|
165
|
+
const id = override?.trim() || env.SYNOMEM_ACTOR_ID?.trim();
|
|
153
166
|
if (!id) return actor(fallbackKind, fallbackId);
|
|
154
|
-
|
|
167
|
+
const kind = override?.trim()
|
|
168
|
+
? // An explicit --actor names an agent unless told otherwise; the historical
|
|
169
|
+
// fallbacks here are `system`/`cli`, which is not what somebody means when
|
|
170
|
+
// they name one.
|
|
171
|
+
env.SYNOMEM_ACTOR_KIND?.trim() || 'agent'
|
|
172
|
+
: env.SYNOMEM_ACTOR_KIND?.trim() || fallbackKind;
|
|
173
|
+
return actor(kind, id, env.SYNOMEM_ACTOR_NAME?.trim());
|
|
155
174
|
}
|
|
156
175
|
|
|
157
176
|
function taskDue(options: {
|
|
@@ -245,10 +264,30 @@ function output(io: CliIo, json: boolean, value: unknown, human: string): void {
|
|
|
245
264
|
io.stdout(json ? `${JSON.stringify(value, null, 2)}\n` : `${human}\n`);
|
|
246
265
|
}
|
|
247
266
|
|
|
248
|
-
|
|
249
|
-
|
|
267
|
+
/**
|
|
268
|
+
* The resolved global options for a command.
|
|
269
|
+
*
|
|
270
|
+
* `--workspace` is turned into a home HERE, before any service exists, which is
|
|
271
|
+
* the whole reason it costs nothing downstream: a local workspace is a separate
|
|
272
|
+
* database in its own home, and choosing one is choosing a home. Nothing in the
|
|
273
|
+
* domain, the commands, or the MCP tools learns that a workspace was selected.
|
|
274
|
+
*
|
|
275
|
+
* On a remote backend the name means a hosted workspace instead, which
|
|
276
|
+
* `backend use remote --workspace` already handles; passing both here would be
|
|
277
|
+
* two different answers to the same question, so it is refused.
|
|
278
|
+
*/
|
|
279
|
+
/** `parent child`, so a subcommand name cannot be confused with another's. */
|
|
280
|
+
function commandPath(command: Command): string {
|
|
281
|
+
const parent = command.parent?.name();
|
|
282
|
+
return parent && parent !== 'synomem' ? `${parent} ${command.name()}` : command.name();
|
|
250
283
|
}
|
|
251
284
|
|
|
285
|
+
/**
|
|
286
|
+
* Commands where `--workspace` names a HOSTED workspace being configured,
|
|
287
|
+
* rather than a local one to act in.
|
|
288
|
+
*/
|
|
289
|
+
const CONFIGURES_BACKEND = new Set(['config init', 'backend use', 'remote import']);
|
|
290
|
+
|
|
252
291
|
async function withService<T>(
|
|
253
292
|
serviceFactory: SynomemServiceFactory,
|
|
254
293
|
home: string | undefined,
|
|
@@ -302,15 +341,91 @@ function listInput(options: Record<string, string>): KudosListInput {
|
|
|
302
341
|
};
|
|
303
342
|
}
|
|
304
343
|
|
|
344
|
+
/**
|
|
345
|
+
* The value `--actor` should supply to commands that name an actor.
|
|
346
|
+
*
|
|
347
|
+
* Read from argv directly, before the commands are built, because Commander
|
|
348
|
+
* evaluates option defaults at DECLARATION time: a `--as` declared without one
|
|
349
|
+
* is required, and a `--as` declared with one is already satisfied. Supplying
|
|
350
|
+
* it here is a single change point instead of a fallback threaded through
|
|
351
|
+
* twenty action bodies, and `--as` still wins when both are given because an
|
|
352
|
+
* explicitly passed option overrides its default.
|
|
353
|
+
*/
|
|
354
|
+
function actorDefault(argv: string[], env: NodeJS.ProcessEnv): string | undefined {
|
|
355
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
356
|
+
const argument = argv[index]!;
|
|
357
|
+
if (argument === '--actor') return argv[index + 1]?.trim() || undefined;
|
|
358
|
+
if (argument.startsWith('--actor='))
|
|
359
|
+
return argument.slice('--actor='.length).trim() || undefined;
|
|
360
|
+
}
|
|
361
|
+
if (env.SYNOMEM_ACTOR_ID?.trim()) return env.SYNOMEM_ACTOR_ID.trim();
|
|
362
|
+
/*
|
|
363
|
+
* Last, the project's own binding. `workspace use --as` exists so a
|
|
364
|
+
* repository can settle both questions once — which workspace, and as whom —
|
|
365
|
+
* and a command run there needs neither flag afterwards.
|
|
366
|
+
*/
|
|
367
|
+
try {
|
|
368
|
+
return findProjectSelection()?.actor;
|
|
369
|
+
} catch {
|
|
370
|
+
// A malformed project file is reported by the resolver when the command
|
|
371
|
+
// actually runs, with the path in the message. Failing here would turn it
|
|
372
|
+
// into an error before any command had been parsed.
|
|
373
|
+
return undefined;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
305
377
|
export function createCli(
|
|
306
378
|
io: CliIo = defaultIo,
|
|
307
379
|
serviceFactory: SynomemServiceFactory = configuredServiceFactory,
|
|
308
380
|
dependencies: CliDependencies = {},
|
|
381
|
+
argv: string[] = process.argv,
|
|
309
382
|
): Command {
|
|
310
383
|
const env = dependencies.env ?? process.env;
|
|
384
|
+
const actingDefault = actorDefault(argv, env);
|
|
385
|
+
|
|
386
|
+
const globals = (
|
|
387
|
+
command: Command,
|
|
388
|
+
): { home?: string; json: boolean; actor?: string; workspace?: string } => {
|
|
389
|
+
const options = command.optsWithGlobals<{
|
|
390
|
+
home?: string;
|
|
391
|
+
json: boolean;
|
|
392
|
+
workspace?: string;
|
|
393
|
+
actor?: string;
|
|
394
|
+
}>();
|
|
395
|
+
if (CONFIGURES_BACKEND.has(commandPath(command))) return options;
|
|
396
|
+
/*
|
|
397
|
+
* Resolution runs even with no `--workspace`, because a project's
|
|
398
|
+
* `.synomem/config.json` selects one without anybody passing a flag — that is
|
|
399
|
+
* the whole point of it. `--home` still wins outright: it names a home
|
|
400
|
+
* directly rather than a workspace within one.
|
|
401
|
+
*/
|
|
402
|
+
if (!options.home) {
|
|
403
|
+
const selection = resolveWorkspaceSelection({
|
|
404
|
+
...(options.workspace ? { flag: options.workspace } : {}),
|
|
405
|
+
...(options.actor ? { actorFlag: options.actor } : {}),
|
|
406
|
+
env,
|
|
407
|
+
});
|
|
408
|
+
return { ...options, home: selection.home, actor: selection.actor ?? options.actor };
|
|
409
|
+
}
|
|
410
|
+
if (!options.workspace) return options;
|
|
411
|
+
/*
|
|
412
|
+
* One flag, one meaning — "which workspace" — resolved differently by the
|
|
413
|
+
* handful of commands that CONFIGURE a backend rather than act inside one.
|
|
414
|
+
* For those, the value is a hosted workspace ID to be written to the config,
|
|
415
|
+
* so it is passed through raw and they read `workspace`. Everywhere else it
|
|
416
|
+
* names a local workspace, which is a home.
|
|
417
|
+
*
|
|
418
|
+
* These commands used to declare their own `--workspace`, which does not
|
|
419
|
+
* work: Commander gives a duplicated long flag to the parent, so the
|
|
420
|
+
* subcommand never received it at all.
|
|
421
|
+
*/
|
|
422
|
+
return { ...options, home: localWorkspaceHome(options.workspace, options.home) };
|
|
423
|
+
};
|
|
424
|
+
|
|
311
425
|
const credentialStore = dependencies.credentialStore ?? new OsCredentialStore();
|
|
312
426
|
const oauthLogin = dependencies.oauthLogin ?? loginWithOAuth;
|
|
313
|
-
const
|
|
427
|
+
const discoverWorkspaces =
|
|
428
|
+
dependencies.discoverAccessKeyWorkspaces ?? discoverAccessKeyWorkspaces;
|
|
314
429
|
const promptIo = dependencies.promptIo ?? defaultPromptIo();
|
|
315
430
|
const verifyRemoteCredential =
|
|
316
431
|
dependencies.verifyRemoteCredential ??
|
|
@@ -346,10 +461,113 @@ export function createCli(
|
|
|
346
461
|
)
|
|
347
462
|
.version(packageVersion())
|
|
348
463
|
.option('--home <path>', 'storage root (defaults to SYNOMEM_HOME or ~/.synomem)')
|
|
464
|
+
// A local workspace is its own database under the root, so this selects a
|
|
465
|
+
// home. On a remote backend the hosted workspace is chosen by
|
|
466
|
+
// `backend use remote --workspace` instead.
|
|
467
|
+
.option('--workspace <name>', 'local workspace to act in (see `synomem workspace list`)')
|
|
468
|
+
.option('--actor <id>', 'act as this agent (overrides SYNOMEM_ACTOR_ID)')
|
|
349
469
|
.option('--json', 'emit stable machine-readable JSON', false)
|
|
350
470
|
.showSuggestionAfterError()
|
|
351
471
|
.configureOutput({ writeOut: io.stdout, writeErr: io.stderr });
|
|
352
472
|
|
|
473
|
+
/*
|
|
474
|
+
* Local workspaces.
|
|
475
|
+
*
|
|
476
|
+
* Each is a separate database in its own home, which is what makes the
|
|
477
|
+
* isolation real: SQLite has no row-level security, so a shared file would
|
|
478
|
+
* rest on every query remembering to filter, with nothing to catch a miss.
|
|
479
|
+
* Separate files mean cross-workspace leakage is not something anybody can
|
|
480
|
+
* write by accident.
|
|
481
|
+
*/
|
|
482
|
+
const workspaceCommand = program
|
|
483
|
+
.command('workspace')
|
|
484
|
+
.description('Work in a separate local store, isolated from the others');
|
|
485
|
+
|
|
486
|
+
workspaceCommand
|
|
487
|
+
.command('list')
|
|
488
|
+
.description('List the local workspaces on this machine')
|
|
489
|
+
.action((_options, command: Command) => {
|
|
490
|
+
const global = globals(command);
|
|
491
|
+
// Read from disk, so nothing is listed that does not exist.
|
|
492
|
+
const workspaces = listLocalWorkspaces(global.home);
|
|
493
|
+
// Which one is in effect here, and what decided it — a flag, the
|
|
494
|
+
// environment, a project file, or nothing.
|
|
495
|
+
const selection = resolveWorkspaceSelection({ env });
|
|
496
|
+
const human = workspaces
|
|
497
|
+
.map(
|
|
498
|
+
(workspace) =>
|
|
499
|
+
`${workspace.name === DEFAULT_WORKSPACE ? '*' : ' '} ${workspace.name.padEnd(24)} ${
|
|
500
|
+
workspace.initialized ? workspace.home : `${workspace.home} (not initialized)`
|
|
501
|
+
}`,
|
|
502
|
+
)
|
|
503
|
+
.join('\n');
|
|
504
|
+
output(
|
|
505
|
+
io,
|
|
506
|
+
global.json,
|
|
507
|
+
{ workspaces, active: selection.workspace ?? DEFAULT_WORKSPACE, source: selection.source },
|
|
508
|
+
[
|
|
509
|
+
human,
|
|
510
|
+
'',
|
|
511
|
+
`Acting in: ${selection.workspace ?? DEFAULT_WORKSPACE} (from ${selection.source})`,
|
|
512
|
+
'',
|
|
513
|
+
'Bind a directory with `synomem workspace use <name>`, or pass --workspace once.',
|
|
514
|
+
'A local workspace is a separate store on this machine; a hosted workspace is shared,',
|
|
515
|
+
'and is selected with `backend use remote --workspace`.',
|
|
516
|
+
].join('\n'),
|
|
517
|
+
);
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
workspaceCommand
|
|
521
|
+
.command('use <name>')
|
|
522
|
+
.description('Bind this directory to a workspace, for every session opened here')
|
|
523
|
+
.option('--as <actor-id>', 'also always write as this agent', actingDefault)
|
|
524
|
+
.action((name: string, options: { as?: string }, command: Command) => {
|
|
525
|
+
const global = globals(command);
|
|
526
|
+
// Validated by resolving it, so a name that could never work is refused
|
|
527
|
+
// before a file claiming it is written.
|
|
528
|
+
const home = localWorkspaceHome(name, undefined);
|
|
529
|
+
const path = writeProjectSelection(process.cwd(), {
|
|
530
|
+
workspace: name,
|
|
531
|
+
...(options.as ? { actor: options.as } : {}),
|
|
532
|
+
});
|
|
533
|
+
output(
|
|
534
|
+
io,
|
|
535
|
+
global.json,
|
|
536
|
+
{ path, workspace: name, home, ...(options.as ? { actor: options.as } : {}) },
|
|
537
|
+
[
|
|
538
|
+
`Wrote ${path}`,
|
|
539
|
+
'',
|
|
540
|
+
`Every Synomem command and MCP server started in this directory now acts in ${name}${
|
|
541
|
+
options.as ? ` as ${options.as}` : ''
|
|
542
|
+
}, with no flag.`,
|
|
543
|
+
`Records live in ${home} — nothing is stored in this directory.`,
|
|
544
|
+
'',
|
|
545
|
+
'Commit it to share the choice with the repository, or ignore it to keep it yours.',
|
|
546
|
+
].join('\n'),
|
|
547
|
+
);
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
workspaceCommand
|
|
551
|
+
.command('create <name>')
|
|
552
|
+
.description('Create a local workspace and initialize its store')
|
|
553
|
+
.action(async (name: string, _options, command: Command) => {
|
|
554
|
+
const global = globals(command);
|
|
555
|
+
const home = localWorkspaceHome(name, global.home);
|
|
556
|
+
if (readSynomemConfig(home)) {
|
|
557
|
+
throw new SynomemError('INVALID_INPUT', `Workspace already exists: ${name}`);
|
|
558
|
+
}
|
|
559
|
+
writeSynomemBackend({ kind: 'local' }, home);
|
|
560
|
+
// Opening it once creates the database, so `list` does not report a
|
|
561
|
+
// workspace that exists in name only.
|
|
562
|
+
await withClient(home, defaultActor(env, 'system', 'cli'), async () => undefined);
|
|
563
|
+
output(
|
|
564
|
+
io,
|
|
565
|
+
global.json,
|
|
566
|
+
{ name, home },
|
|
567
|
+
`Created workspace ${name} at ${home}.\nAct in it with --workspace ${name}.`,
|
|
568
|
+
);
|
|
569
|
+
});
|
|
570
|
+
|
|
353
571
|
const remoteCommand = program.command('remote').description('Administer a remote workspace');
|
|
354
572
|
|
|
355
573
|
/*
|
|
@@ -511,18 +729,54 @@ export function createCli(
|
|
|
511
729
|
const serviceUrl = plan.serviceUrl ?? cloudApiUrl(env);
|
|
512
730
|
|
|
513
731
|
/*
|
|
514
|
-
*
|
|
515
|
-
*
|
|
516
|
-
*
|
|
517
|
-
*
|
|
518
|
-
*
|
|
519
|
-
* network round trip to configure a machine.
|
|
732
|
+
* A member-owned access key reaches every workspace in its organization,
|
|
733
|
+
* never just one, so the workspace is discovered and then chosen — not
|
|
734
|
+
* typed from memory, and not assumed. An explicit --workspace still wins
|
|
735
|
+
* regardless: automation should not depend on a network round trip, and a
|
|
736
|
+
* person who already knows which one they want should not be asked again.
|
|
520
737
|
*/
|
|
521
738
|
let workspaceId = plan.workspaceId;
|
|
522
739
|
if (plan.backend === 'remote' && !workspaceId && token) {
|
|
523
|
-
const
|
|
524
|
-
|
|
525
|
-
|
|
740
|
+
const discovered = await discoverWorkspaces({ baseUrl: serviceUrl, accessToken: token });
|
|
741
|
+
if (discovered.workspaces.length === 0) {
|
|
742
|
+
throw new SynomemError(
|
|
743
|
+
'INVALID_INPUT',
|
|
744
|
+
[
|
|
745
|
+
"This access key's organization has no workspaces yet.",
|
|
746
|
+
'Create one in the Synomem portal, then re-run this command — or pass',
|
|
747
|
+
'--workspace <workspace-id> once one exists.',
|
|
748
|
+
].join('\n'),
|
|
749
|
+
);
|
|
750
|
+
} else if (discovered.workspaces.length === 1) {
|
|
751
|
+
workspaceId = discovered.workspaces[0]!.id;
|
|
752
|
+
io.stdout(
|
|
753
|
+
`Using this key's only workspace: ${discovered.workspaces[0]!.displayName} (${workspaceId}).\n`,
|
|
754
|
+
);
|
|
755
|
+
} else if (promptIo.interactive) {
|
|
756
|
+
workspaceId = await select(
|
|
757
|
+
promptIo,
|
|
758
|
+
'Which workspace should this machine use?',
|
|
759
|
+
discovered.workspaces.map((workspace) => ({
|
|
760
|
+
value: workspace.id,
|
|
761
|
+
label: workspace.displayName,
|
|
762
|
+
detail: workspace.id,
|
|
763
|
+
})),
|
|
764
|
+
);
|
|
765
|
+
} else {
|
|
766
|
+
throw new SynomemError(
|
|
767
|
+
'INVALID_INPUT',
|
|
768
|
+
[
|
|
769
|
+
'This access key can reach more than one workspace, so a non-interactive',
|
|
770
|
+
'setup needs to be told which one:',
|
|
771
|
+
'',
|
|
772
|
+
...discovered.workspaces.map(
|
|
773
|
+
(workspace) => ` ${workspace.id} ${workspace.displayName}`,
|
|
774
|
+
),
|
|
775
|
+
'',
|
|
776
|
+
'Re-run with --workspace <workspace-id>.',
|
|
777
|
+
].join('\n'),
|
|
778
|
+
);
|
|
779
|
+
}
|
|
526
780
|
}
|
|
527
781
|
if (plan.backend === 'remote' && !workspaceId) {
|
|
528
782
|
/*
|
|
@@ -631,7 +885,6 @@ export function createCli(
|
|
|
631
885
|
.description('Configure Synomem without prompting')
|
|
632
886
|
.option('--backend <kind>', 'local or remote')
|
|
633
887
|
.option('--auth <method>', 'browser or access-key')
|
|
634
|
-
.option('--workspace <id>', 'remote workspace ID')
|
|
635
888
|
.option('--credential-store <where>', 'auto, keychain, file, or environment', 'auto')
|
|
636
889
|
// The token is read from stdin, never taken as an argument: an argument is
|
|
637
890
|
// kept by the shell history and visible in the process list.
|
|
@@ -642,7 +895,6 @@ export function createCli(
|
|
|
642
895
|
options: {
|
|
643
896
|
backend?: string;
|
|
644
897
|
auth?: string;
|
|
645
|
-
workspace?: string;
|
|
646
898
|
credentialStore: string;
|
|
647
899
|
accessTokenStdin: boolean;
|
|
648
900
|
yes: boolean;
|
|
@@ -654,12 +906,12 @@ export function createCli(
|
|
|
654
906
|
throw new SynomemError('INVALID_INPUT', 'Pass --backend local or --backend remote.');
|
|
655
907
|
}
|
|
656
908
|
const backend: BackendChoice = options.backend;
|
|
657
|
-
// An access key
|
|
658
|
-
// required when there is no key to ask.
|
|
659
|
-
if (backend === 'remote' && !
|
|
909
|
+
// An access key can be asked which workspaces it reaches, so
|
|
910
|
+
// --workspace is only required when there is no key to ask.
|
|
911
|
+
if (backend === 'remote' && !global.workspace && !options.accessTokenStdin) {
|
|
660
912
|
throw new SynomemError(
|
|
661
913
|
'INVALID_INPUT',
|
|
662
|
-
'Remote setup requires --workspace, or --access-token-stdin so the key can
|
|
914
|
+
'Remote setup requires --workspace, or --access-token-stdin so the key can be asked.',
|
|
663
915
|
);
|
|
664
916
|
}
|
|
665
917
|
const token = options.accessTokenStdin ? await readAccessToken(promptIo) : undefined;
|
|
@@ -676,7 +928,7 @@ export function createCli(
|
|
|
676
928
|
? {
|
|
677
929
|
serviceUrl: cloudApiUrl(env),
|
|
678
930
|
auth: (options.auth as AuthChoice | undefined) ?? 'access-key',
|
|
679
|
-
workspaceId:
|
|
931
|
+
workspaceId: global.workspace,
|
|
680
932
|
credentialStore: options.credentialStore as CredentialStoreChoice,
|
|
681
933
|
}
|
|
682
934
|
: {}),
|
|
@@ -818,13 +1070,12 @@ export function createCli(
|
|
|
818
1070
|
// never ask for a service address, because a person has no way to tell a
|
|
819
1071
|
// real one from a phished one.
|
|
820
1072
|
.option('--url <url>', 'internal: alternate HTTPS origin')
|
|
821
|
-
.
|
|
822
|
-
.action((kind: string, options: { url?: string; workspace?: string }, command: Command) => {
|
|
1073
|
+
.action((kind: string, options: { url?: string }, command: Command) => {
|
|
823
1074
|
const global = globals(command);
|
|
824
1075
|
if (kind !== 'local' && kind !== 'remote') {
|
|
825
1076
|
throw new SynomemError('INVALID_INPUT', 'Backend kind must be local or remote.');
|
|
826
1077
|
}
|
|
827
|
-
if (kind === 'remote' && !
|
|
1078
|
+
if (kind === 'remote' && !global.workspace) {
|
|
828
1079
|
throw new SynomemError('INVALID_INPUT', 'Remote backend selection requires --workspace.');
|
|
829
1080
|
}
|
|
830
1081
|
const config = writeSynomemBackend(
|
|
@@ -833,7 +1084,7 @@ export function createCli(
|
|
|
833
1084
|
: {
|
|
834
1085
|
kind: 'remote',
|
|
835
1086
|
baseUrl: options.url ?? cloudApiUrl(env),
|
|
836
|
-
workspaceId:
|
|
1087
|
+
workspaceId: global.workspace!,
|
|
837
1088
|
},
|
|
838
1089
|
global.home,
|
|
839
1090
|
);
|
|
@@ -1493,7 +1744,7 @@ export function createCli(
|
|
|
1493
1744
|
postCommand
|
|
1494
1745
|
.command('create')
|
|
1495
1746
|
.description('Publish a post the whole workspace can read')
|
|
1496
|
-
.requiredOption('--as <actor-id>')
|
|
1747
|
+
.requiredOption('--as <actor-id>', 'actor to act as (defaults to --actor)', actingDefault)
|
|
1497
1748
|
.option('--actor-kind <kind>', 'human, agent, or system', 'agent')
|
|
1498
1749
|
.requiredOption('--title <title>')
|
|
1499
1750
|
.requiredOption('--body <body>')
|
|
@@ -1530,7 +1781,7 @@ export function createCli(
|
|
|
1530
1781
|
postCommand
|
|
1531
1782
|
.command('list')
|
|
1532
1783
|
.description('List posts in this workspace')
|
|
1533
|
-
.requiredOption('--as <actor-id>')
|
|
1784
|
+
.requiredOption('--as <actor-id>', 'actor to act as (defaults to --actor)', actingDefault)
|
|
1534
1785
|
.option('--actor-kind <kind>', 'human, agent, or system', 'agent')
|
|
1535
1786
|
.option('--limit <n>', 'default 10, maximum 50')
|
|
1536
1787
|
.action(
|
|
@@ -1549,7 +1800,7 @@ export function createCli(
|
|
|
1549
1800
|
postCommand
|
|
1550
1801
|
.command('show <post-id>')
|
|
1551
1802
|
.description('Show one post with its acknowledgements')
|
|
1552
|
-
.requiredOption('--as <actor-id>')
|
|
1803
|
+
.requiredOption('--as <actor-id>', 'actor to act as (defaults to --actor)', actingDefault)
|
|
1553
1804
|
.option('--actor-kind <kind>', 'human, agent, or system', 'agent')
|
|
1554
1805
|
.action(
|
|
1555
1806
|
async (postId: string, options: { as: string; actorKind: string }, command: Command) => {
|
|
@@ -1576,7 +1827,7 @@ export function createCli(
|
|
|
1576
1827
|
postCommand
|
|
1577
1828
|
.command('acknowledge <post-id>')
|
|
1578
1829
|
.description('Say you have seen a post')
|
|
1579
|
-
.requiredOption('--as <actor-id>')
|
|
1830
|
+
.requiredOption('--as <actor-id>', 'actor to act as (defaults to --actor)', actingDefault)
|
|
1580
1831
|
.option('--actor-kind <kind>', 'human, agent, or system', 'agent')
|
|
1581
1832
|
.option('--note <text>', 'optional context for the author')
|
|
1582
1833
|
.action(
|
|
@@ -1602,7 +1853,7 @@ export function createCli(
|
|
|
1602
1853
|
postCommand
|
|
1603
1854
|
.command('roster <post-id>')
|
|
1604
1855
|
.description('Who has acknowledged a post, and who has not')
|
|
1605
|
-
.requiredOption('--as <actor-id>')
|
|
1856
|
+
.requiredOption('--as <actor-id>', 'actor to act as (defaults to --actor)', actingDefault)
|
|
1606
1857
|
.option('--actor-kind <kind>', 'human, agent, or system', 'agent')
|
|
1607
1858
|
.action(
|
|
1608
1859
|
async (postId: string, options: { as: string; actorKind: string }, command: Command) => {
|
|
@@ -1634,7 +1885,7 @@ export function createCli(
|
|
|
1634
1885
|
postCommand
|
|
1635
1886
|
.command('archive <post-id>')
|
|
1636
1887
|
.description('Archive a post you wrote')
|
|
1637
|
-
.requiredOption('--as <actor-id>')
|
|
1888
|
+
.requiredOption('--as <actor-id>', 'actor to act as (defaults to --actor)', actingDefault)
|
|
1638
1889
|
.option('--actor-kind <kind>', 'human, agent, or system', 'agent')
|
|
1639
1890
|
.option('--reason <text>')
|
|
1640
1891
|
.action(
|
|
@@ -1659,7 +1910,11 @@ export function createCli(
|
|
|
1659
1910
|
kudosCommand
|
|
1660
1911
|
.command('give <recipient>')
|
|
1661
1912
|
.description('Give specific, evidence-based kudos to an agent')
|
|
1662
|
-
.requiredOption(
|
|
1913
|
+
.requiredOption(
|
|
1914
|
+
'--from <actor-id>',
|
|
1915
|
+
'stable ID of the giver (defaults to --actor)',
|
|
1916
|
+
actingDefault,
|
|
1917
|
+
)
|
|
1663
1918
|
.requiredOption('--actor-kind <kind>', 'human, agent, or system')
|
|
1664
1919
|
.option('--actor-name <display-name>')
|
|
1665
1920
|
.requiredOption('--title <title>')
|
|
@@ -1828,7 +2083,7 @@ export function createCli(
|
|
|
1828
2083
|
const memoCommand = program.command('memo').description('Send and manage durable messages');
|
|
1829
2084
|
memoCommand
|
|
1830
2085
|
.command('send <recipient>')
|
|
1831
|
-
.requiredOption('--from <actor-id>')
|
|
2086
|
+
.requiredOption('--from <actor-id>', 'actor to act as (defaults to --actor)', actingDefault)
|
|
1832
2087
|
.option('--actor-kind <kind>', 'human, agent, or system', 'agent')
|
|
1833
2088
|
.option('--actor-name <name>')
|
|
1834
2089
|
.requiredOption('--subject <subject>')
|
|
@@ -1945,7 +2200,7 @@ export function createCli(
|
|
|
1945
2200
|
.description('Retain and revise agent-owned knowledge');
|
|
1946
2201
|
noteCommand
|
|
1947
2202
|
.command('create')
|
|
1948
|
-
.requiredOption('--as <actor-id>')
|
|
2203
|
+
.requiredOption('--as <actor-id>', 'actor to act as (defaults to --actor)', actingDefault)
|
|
1949
2204
|
.option('--actor-kind <kind>', 'agent or human', 'agent')
|
|
1950
2205
|
.option('--owner <agent-id>')
|
|
1951
2206
|
.requiredOption('--title <title>')
|
|
@@ -2023,7 +2278,7 @@ export function createCli(
|
|
|
2023
2278
|
});
|
|
2024
2279
|
noteCommand
|
|
2025
2280
|
.command('revise <note-id>')
|
|
2026
|
-
.requiredOption('--as <actor-id>')
|
|
2281
|
+
.requiredOption('--as <actor-id>', 'actor to act as (defaults to --actor)', actingDefault)
|
|
2027
2282
|
.option('--actor-kind <kind>', 'agent or human', 'agent')
|
|
2028
2283
|
.requiredOption('--expected-version <number>')
|
|
2029
2284
|
.option('--title <title>')
|
|
@@ -2063,7 +2318,7 @@ export function createCli(
|
|
|
2063
2318
|
);
|
|
2064
2319
|
noteCommand
|
|
2065
2320
|
.command('archive <note-id>')
|
|
2066
|
-
.requiredOption('--as <actor-id>')
|
|
2321
|
+
.requiredOption('--as <actor-id>', 'actor to act as (defaults to --actor)', actingDefault)
|
|
2067
2322
|
.option('--actor-kind <kind>', 'agent or human', 'agent')
|
|
2068
2323
|
.option('--idempotency-key <key>')
|
|
2069
2324
|
.action(
|
|
@@ -2091,7 +2346,7 @@ export function createCli(
|
|
|
2091
2346
|
.description('Create and manage your own private reminders');
|
|
2092
2347
|
todoCommand
|
|
2093
2348
|
.command('create')
|
|
2094
|
-
.requiredOption('--as <actor-id>')
|
|
2349
|
+
.requiredOption('--as <actor-id>', 'actor to act as (defaults to --actor)', actingDefault)
|
|
2095
2350
|
.option('--actor-kind <kind>', 'human, agent, or system', 'agent')
|
|
2096
2351
|
.requiredOption('--title <title>')
|
|
2097
2352
|
.option('--details <text>', 'private working detail')
|
|
@@ -2141,7 +2396,7 @@ export function createCli(
|
|
|
2141
2396
|
);
|
|
2142
2397
|
todoCommand
|
|
2143
2398
|
.command('list')
|
|
2144
|
-
.requiredOption('--as <actor-id>')
|
|
2399
|
+
.requiredOption('--as <actor-id>', 'actor to act as (defaults to --actor)', actingDefault)
|
|
2145
2400
|
.option('--actor-kind <kind>', 'human, agent, or system', 'agent')
|
|
2146
2401
|
.option('--status <status>')
|
|
2147
2402
|
.option('--limit <number>', 'maximum results', '10')
|
|
@@ -2171,7 +2426,7 @@ export function createCli(
|
|
|
2171
2426
|
);
|
|
2172
2427
|
todoCommand
|
|
2173
2428
|
.command('show <todo-id>')
|
|
2174
|
-
.requiredOption('--as <actor-id>')
|
|
2429
|
+
.requiredOption('--as <actor-id>', 'actor to act as (defaults to --actor)', actingDefault)
|
|
2175
2430
|
.option('--actor-kind <kind>', 'human, agent, or system', 'agent')
|
|
2176
2431
|
.action(async (id: string, options: { as: string; actorKind: string }, command: Command) => {
|
|
2177
2432
|
const global = globals(command);
|
|
@@ -2188,7 +2443,7 @@ export function createCli(
|
|
|
2188
2443
|
for (const operation of ['complete', 'reopen', 'cancel', 'archive'] as const) {
|
|
2189
2444
|
todoCommand
|
|
2190
2445
|
.command(`${operation} <todo-id>`)
|
|
2191
|
-
.requiredOption('--as <actor-id>')
|
|
2446
|
+
.requiredOption('--as <actor-id>', 'actor to act as (defaults to --actor)', actingDefault)
|
|
2192
2447
|
.option('--actor-kind <kind>', 'human, agent, or system', 'agent')
|
|
2193
2448
|
.option('--note <text>')
|
|
2194
2449
|
.option('--reason <text>')
|
|
@@ -2244,7 +2499,7 @@ export function createCli(
|
|
|
2244
2499
|
const taskCommand = program.command('task').description('Create and manage agent tasks');
|
|
2245
2500
|
taskCommand
|
|
2246
2501
|
.command('create <assignee>')
|
|
2247
|
-
.requiredOption('--from <actor-id>')
|
|
2502
|
+
.requiredOption('--from <actor-id>', 'actor to act as (defaults to --actor)', actingDefault)
|
|
2248
2503
|
.option('--actor-kind <kind>', 'human, agent, or system', 'agent')
|
|
2249
2504
|
.requiredOption('--title <title>')
|
|
2250
2505
|
.option('--description <text>')
|
|
@@ -2334,7 +2589,7 @@ export function createCli(
|
|
|
2334
2589
|
});
|
|
2335
2590
|
taskCommand
|
|
2336
2591
|
.command('update <task-id>')
|
|
2337
|
-
.requiredOption('--as <actor-id>')
|
|
2592
|
+
.requiredOption('--as <actor-id>', 'actor to act as (defaults to --actor)', actingDefault)
|
|
2338
2593
|
.option('--actor-kind <kind>', 'agent or human', 'agent')
|
|
2339
2594
|
.requiredOption('--expected-version <number>')
|
|
2340
2595
|
.option('--title <title>')
|
|
@@ -2388,7 +2643,7 @@ export function createCli(
|
|
|
2388
2643
|
for (const operation of ['accept', 'reject', 'complete', 'reopen', 'cancel'] as const) {
|
|
2389
2644
|
const command_ = taskCommand
|
|
2390
2645
|
.command(`${operation} <task-id>`)
|
|
2391
|
-
.requiredOption('--as <actor-id>')
|
|
2646
|
+
.requiredOption('--as <actor-id>', 'actor to act as (defaults to --actor)', actingDefault)
|
|
2392
2647
|
.option('--actor-kind <kind>', 'agent or human', 'agent')
|
|
2393
2648
|
.option('--note <text>')
|
|
2394
2649
|
.option('--reason <text>')
|
|
@@ -2497,7 +2752,7 @@ export function createCli(
|
|
|
2497
2752
|
kudosCommand
|
|
2498
2753
|
.command('revoke <kudos-id>')
|
|
2499
2754
|
.description('Record a revocation while preserving history')
|
|
2500
|
-
.requiredOption('--as <actor-id>')
|
|
2755
|
+
.requiredOption('--as <actor-id>', 'actor to act as (defaults to --actor)', actingDefault)
|
|
2501
2756
|
.option('--actor-kind <kind>', 'human, agent, or system', 'human')
|
|
2502
2757
|
.requiredOption('--reason <reason>')
|
|
2503
2758
|
.option('--administrative', 'mark as an administrative revocation', false)
|
|
@@ -2727,7 +2982,7 @@ export async function runCli(
|
|
|
2727
2982
|
serviceFactory: SynomemServiceFactory = configuredServiceFactory,
|
|
2728
2983
|
dependencies: CliDependencies = {},
|
|
2729
2984
|
): Promise<number> {
|
|
2730
|
-
const program = createCli(io, serviceFactory, dependencies);
|
|
2985
|
+
const program = createCli(io, serviceFactory, dependencies, argv);
|
|
2731
2986
|
program.exitOverride();
|
|
2732
2987
|
try {
|
|
2733
2988
|
await program.parseAsync(argv);
|
package/src/client.ts
CHANGED
|
@@ -435,6 +435,15 @@ export class SynomemCore implements SynomemDomainService {
|
|
|
435
435
|
await this.repository.updateAgent(updated, event.createdAt);
|
|
436
436
|
await this.repository.insertEvent(event);
|
|
437
437
|
});
|
|
438
|
+
/*
|
|
439
|
+
* Projections are named by handle, so a rename has to move the directory
|
|
440
|
+
* before it is regenerated. Otherwise the generated files appear under the
|
|
441
|
+
* new handle and `NOTES.md` -- which belongs to the reader and is never
|
|
442
|
+
* deleted -- is left stranded under the old one.
|
|
443
|
+
*/
|
|
444
|
+
if (existing.handle !== updated.handle && this.projectionWriter.renameAgentDirectory) {
|
|
445
|
+
await this.projectionWriter.renameAgentDirectory(existing.handle, updated.handle);
|
|
446
|
+
}
|
|
438
447
|
await this.projectionWriter.syncAgent(updated.id);
|
|
439
448
|
return updated;
|
|
440
449
|
}
|
|
@@ -1915,6 +1924,7 @@ export class SynomemClient extends SynomemCore implements SynomemService {
|
|
|
1915
1924
|
binding: { workspaceId: this.storage.config.workspaceId, actor: this.actor },
|
|
1916
1925
|
administration: {
|
|
1917
1926
|
agentCreationViaMcp: this.storage.config.allowAgentCreationViaMcp,
|
|
1927
|
+
agentArchiveViaMcp: this.storage.config.allowAgentArchiveViaMcp,
|
|
1918
1928
|
rebuildViaMcp: this.storage.config.allowRebuildViaMcp,
|
|
1919
1929
|
},
|
|
1920
1930
|
projections: { ...this.storage.config.projection },
|
package/src/config.ts
CHANGED
|
@@ -12,6 +12,7 @@ export const defaultConfig: SynomemConfig = {
|
|
|
12
12
|
allowSelfAwards: false,
|
|
13
13
|
allowCrossAgentTasks: true,
|
|
14
14
|
allowAgentCreationViaMcp: false,
|
|
15
|
+
allowAgentArchiveViaMcp: false,
|
|
15
16
|
allowRebuildViaMcp: false,
|
|
16
17
|
includePrivateInStats: false,
|
|
17
18
|
projection: {
|
|
@@ -55,6 +56,7 @@ const policySchema = z.object({
|
|
|
55
56
|
allowSelfAwards: z.boolean(),
|
|
56
57
|
allowCrossAgentTasks: z.boolean(),
|
|
57
58
|
allowAgentCreationViaMcp: z.boolean(),
|
|
59
|
+
allowAgentArchiveViaMcp: z.boolean(),
|
|
58
60
|
allowRebuildViaMcp: z.boolean(),
|
|
59
61
|
includePrivateInStats: z.boolean(),
|
|
60
62
|
projection: z.object({
|
|
@@ -156,6 +158,7 @@ function environmentConfig(env: NodeJS.ProcessEnv): SynomemConfigOverrides {
|
|
|
156
158
|
const allowSelfAwards = optionalBoolean(env, 'SYNOMEM_ALLOW_SELF_AWARDS');
|
|
157
159
|
const allowCrossAgentTasks = optionalBoolean(env, 'SYNOMEM_ALLOW_CROSS_AGENT_TODOS');
|
|
158
160
|
const allowAgentCreationViaMcp = optionalBoolean(env, 'SYNOMEM_ALLOW_AGENT_CREATION_VIA_MCP');
|
|
161
|
+
const allowAgentArchiveViaMcp = optionalBoolean(env, 'SYNOMEM_ALLOW_AGENT_ARCHIVE_VIA_MCP');
|
|
159
162
|
const allowRebuildViaMcp = optionalBoolean(env, 'SYNOMEM_ALLOW_REBUILD_VIA_MCP');
|
|
160
163
|
const includePrivateInStats = optionalBoolean(env, 'SYNOMEM_INCLUDE_PRIVATE_IN_STATS');
|
|
161
164
|
const projection = {
|
|
@@ -170,6 +173,7 @@ function environmentConfig(env: NodeJS.ProcessEnv): SynomemConfigOverrides {
|
|
|
170
173
|
...(allowSelfAwards !== undefined ? { allowSelfAwards } : {}),
|
|
171
174
|
...(allowCrossAgentTasks !== undefined ? { allowCrossAgentTasks } : {}),
|
|
172
175
|
...(allowAgentCreationViaMcp !== undefined ? { allowAgentCreationViaMcp } : {}),
|
|
176
|
+
...(allowAgentArchiveViaMcp !== undefined ? { allowAgentArchiveViaMcp } : {}),
|
|
173
177
|
...(allowRebuildViaMcp !== undefined ? { allowRebuildViaMcp } : {}),
|
|
174
178
|
...(includePrivateInStats !== undefined ? { includePrivateInStats } : {}),
|
|
175
179
|
...(Object.keys(projection).length ? { projection } : {}),
|