synomem 0.2.0 → 0.4.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/CHANGELOG.md +49 -0
- package/README.md +4 -4
- package/dist/backend.d.ts.map +1 -1
- package/dist/backend.js +8 -2
- package/dist/backend.js.map +1 -1
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +379 -16
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts +68 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +283 -12
- package/dist/client.js.map +1 -1
- package/dist/cloud.d.ts +16 -0
- package/dist/cloud.d.ts.map +1 -0
- package/dist/cloud.js +19 -0
- package/dist/cloud.js.map +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +8 -1
- package/dist/config.js.map +1 -1
- package/dist/configure.d.ts +55 -0
- package/dist/configure.d.ts.map +1 -0
- package/dist/configure.js +177 -0
- package/dist/configure.js.map +1 -0
- package/dist/credentials.d.ts +15 -2
- package/dist/credentials.d.ts.map +1 -1
- package/dist/credentials.js.map +1 -1
- package/dist/import.d.ts +202 -38
- package/dist/import.d.ts.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +72 -6
- package/dist/mcp/index.js.map +1 -1
- package/dist/oauth.d.ts.map +1 -1
- package/dist/oauth.js +8 -1
- package/dist/oauth.js.map +1 -1
- package/dist/ports/repository.d.ts +4 -1
- package/dist/ports/repository.d.ts.map +1 -1
- package/dist/projections.d.ts +9 -1
- package/dist/projections.d.ts.map +1 -1
- package/dist/projections.js +75 -4
- package/dist/projections.js.map +1 -1
- package/dist/prompt.d.ts +28 -0
- package/dist/prompt.d.ts.map +1 -0
- package/dist/prompt.js +72 -0
- package/dist/prompt.js.map +1 -0
- package/dist/remote.d.ts +30 -1
- package/dist/remote.d.ts.map +1 -1
- package/dist/remote.js +14 -0
- package/dist/remote.js.map +1 -1
- package/dist/schemas.d.ts +270 -55
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +120 -6
- package/dist/schemas.js.map +1 -1
- package/dist/service.d.ts +30 -1
- package/dist/service.d.ts.map +1 -1
- package/dist/storage.d.ts +25 -2
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +168 -15
- package/dist/storage.js.map +1 -1
- package/dist/types.d.ts +123 -4
- package/dist/types.d.ts.map +1 -1
- package/docs/cli.md +1 -1
- package/docs/examples.md +1 -1
- package/docs/mcp.md +1 -1
- package/docs/skill.md +1 -1
- package/docs/storage-format.md +1 -1
- package/package.json +8 -8
- package/src/backend.ts +8 -2
- package/src/cli.ts +543 -19
- package/src/client.ts +331 -11
- package/src/cloud.ts +19 -0
- package/src/config.ts +8 -1
- package/src/configure.ts +233 -0
- package/src/credentials.ts +17 -2
- package/src/index.ts +7 -1
- package/src/mcp/index.ts +95 -5
- package/src/oauth.ts +8 -1
- package/src/ports/repository.ts +5 -0
- package/src/projections.ts +74 -4
- package/src/prompt.ts +88 -0
- package/src/remote.ts +72 -0
- package/src/schemas.ts +125 -6
- package/src/service.ts +30 -0
- package/src/storage.ts +217 -14
- package/src/types.ts +129 -3
package/src/cli.ts
CHANGED
|
@@ -1,10 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
2
|
+
import { existsSync, readFileSync, realpathSync, rmSync } from 'node:fs';
|
|
3
3
|
import { spawn } from 'node:child_process';
|
|
4
4
|
import { join, resolve } from 'node:path';
|
|
5
5
|
import { pathToFileURL } from 'node:url';
|
|
6
6
|
import { Command, CommanderError, Option } from 'commander';
|
|
7
7
|
import { configuredServiceFactory, readSynomemConfig, writeSynomemBackend } from './backend.js';
|
|
8
|
+
import { cloudApiUrl } from './cloud.js';
|
|
9
|
+
import { resolveHome } from './config.js';
|
|
10
|
+
import {
|
|
11
|
+
assertInteractive,
|
|
12
|
+
confirmPlan,
|
|
13
|
+
credentialFingerprint,
|
|
14
|
+
credentialStoreChoices,
|
|
15
|
+
environmentInstructions,
|
|
16
|
+
readAccessToken,
|
|
17
|
+
runConfigWizard,
|
|
18
|
+
writeCredentialFile,
|
|
19
|
+
type AuthChoice,
|
|
20
|
+
type BackendChoice,
|
|
21
|
+
type ConfigPlan,
|
|
22
|
+
type CredentialStoreChoice,
|
|
23
|
+
} from './configure.js';
|
|
24
|
+
import { defaultPromptIo, type PromptIo } from './prompt.js';
|
|
8
25
|
import { credentialReference, OsCredentialStore, type CredentialStore } from './credentials.js';
|
|
9
26
|
import { asSynomemError, SynomemError, type SynomemErrorCode } from './errors.js';
|
|
10
27
|
import { atomicWriteFile } from './fs-utils.js';
|
|
@@ -45,6 +62,8 @@ export interface CliIo {
|
|
|
45
62
|
}
|
|
46
63
|
|
|
47
64
|
export interface CliDependencies {
|
|
65
|
+
/** Injected so the wizard can be driven by a test without a terminal. */
|
|
66
|
+
promptIo?: PromptIo;
|
|
48
67
|
credentialStore?: CredentialStore;
|
|
49
68
|
oauthLogin?: (options: OAuthLoginOptions) => Promise<void>;
|
|
50
69
|
env?: NodeJS.ProcessEnv;
|
|
@@ -281,6 +300,7 @@ export function createCli(
|
|
|
281
300
|
const env = dependencies.env ?? process.env;
|
|
282
301
|
const credentialStore = dependencies.credentialStore ?? new OsCredentialStore();
|
|
283
302
|
const oauthLogin = dependencies.oauthLogin ?? loginWithOAuth;
|
|
303
|
+
const promptIo = dependencies.promptIo ?? defaultPromptIo();
|
|
284
304
|
const verifyRemoteCredential =
|
|
285
305
|
dependencies.verifyRemoteCredential ??
|
|
286
306
|
(async (options) => {
|
|
@@ -314,7 +334,7 @@ export function createCli(
|
|
|
314
334
|
'Local-first communication, memory, recognition, and task infrastructure for agents',
|
|
315
335
|
)
|
|
316
336
|
.version(packageVersion())
|
|
317
|
-
.option('--home <path>', 'storage root (defaults to SYNOMEM_HOME or ~/.
|
|
337
|
+
.option('--home <path>', 'storage root (defaults to SYNOMEM_HOME or ~/.synomem)')
|
|
318
338
|
.option('--json', 'emit stable machine-readable JSON', false)
|
|
319
339
|
.showSuggestionAfterError()
|
|
320
340
|
.configureOutput({ writeOut: io.stdout, writeErr: io.stderr });
|
|
@@ -416,6 +436,263 @@ export function createCli(
|
|
|
416
436
|
});
|
|
417
437
|
});
|
|
418
438
|
|
|
439
|
+
/*
|
|
440
|
+
* `synomem config` is the canonical entry point. `configure` and `setup` are
|
|
441
|
+
* accepted because people reach for them, and a setup program that rejects
|
|
442
|
+
* the word somebody guessed is needlessly unhelpful.
|
|
443
|
+
*/
|
|
444
|
+
const configCommand = program
|
|
445
|
+
.command('config')
|
|
446
|
+
.aliases(['configure', 'setup'])
|
|
447
|
+
.description('Set up Synomem, interactively or deterministically');
|
|
448
|
+
|
|
449
|
+
const applyPlan = async (
|
|
450
|
+
plan: ConfigPlan,
|
|
451
|
+
token: string | undefined,
|
|
452
|
+
global: { home?: string; json: boolean },
|
|
453
|
+
): Promise<void> => {
|
|
454
|
+
const home = plan.home;
|
|
455
|
+
const config = writeSynomemBackend(
|
|
456
|
+
plan.backend === 'local'
|
|
457
|
+
? { kind: 'local' }
|
|
458
|
+
: {
|
|
459
|
+
kind: 'remote',
|
|
460
|
+
baseUrl: plan.serviceUrl ?? cloudApiUrl(env),
|
|
461
|
+
workspaceId: plan.workspaceId!,
|
|
462
|
+
},
|
|
463
|
+
home,
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
let credentialLocation: string | undefined;
|
|
467
|
+
if (token) {
|
|
468
|
+
if (plan.credentialStore === 'environment') {
|
|
469
|
+
io.stdout(`${environmentInstructions(token)}\n`);
|
|
470
|
+
credentialLocation = 'environment';
|
|
471
|
+
} else if (plan.credentialStore === 'file') {
|
|
472
|
+
credentialLocation = writeCredentialFile(home, token);
|
|
473
|
+
} else {
|
|
474
|
+
// The platform store is the default, and a failure falls back to the
|
|
475
|
+
// restricted file rather than leaving the credential nowhere.
|
|
476
|
+
try {
|
|
477
|
+
await credentialStore.set(`synomem:${plan.workspaceId}`, {
|
|
478
|
+
kind: 'installation-key',
|
|
479
|
+
accessToken: token,
|
|
480
|
+
});
|
|
481
|
+
credentialLocation = 'platform credential store';
|
|
482
|
+
} catch {
|
|
483
|
+
credentialLocation = writeCredentialFile(home, token);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Diagnostics run before success is claimed: a configuration that cannot
|
|
489
|
+
// open its own database is not a finished setup.
|
|
490
|
+
const diagnostics = await withClient(home, defaultActor(env, 'system', 'cli'), (client) =>
|
|
491
|
+
client.doctor(),
|
|
492
|
+
);
|
|
493
|
+
|
|
494
|
+
output(
|
|
495
|
+
io,
|
|
496
|
+
global.json,
|
|
497
|
+
{
|
|
498
|
+
backend: config.backend,
|
|
499
|
+
home,
|
|
500
|
+
...(credentialLocation ? { credentialSource: credentialLocation } : {}),
|
|
501
|
+
...(token ? { credential: credentialFingerprint(token) } : {}),
|
|
502
|
+
healthy: diagnostics.healthy,
|
|
503
|
+
},
|
|
504
|
+
[
|
|
505
|
+
'',
|
|
506
|
+
'Synomem is ready.',
|
|
507
|
+
'',
|
|
508
|
+
` Backend: ${config.backend.kind === 'local' ? 'Local SQLite' : 'Synomem Cloud'}`,
|
|
509
|
+
` Home: ${home}`,
|
|
510
|
+
...(config.backend.kind === 'remote'
|
|
511
|
+
? [` Service: ${config.backend.baseUrl}`, ` Workspace: ${config.backend.workspaceId}`]
|
|
512
|
+
: []),
|
|
513
|
+
...(credentialLocation ? [` Credential: ${credentialLocation}`] : []),
|
|
514
|
+
` Database: ${diagnostics.healthy ? 'Healthy' : 'Needs attention — run synomem doctor'}`,
|
|
515
|
+
].join('\n'),
|
|
516
|
+
);
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
configCommand.action(async (_options, command: Command) => {
|
|
520
|
+
const global = globals(command);
|
|
521
|
+
assertInteractive(promptIo);
|
|
522
|
+
const plan = await runConfigWizard(promptIo, {
|
|
523
|
+
...(global.home ? { home: global.home } : {}),
|
|
524
|
+
env,
|
|
525
|
+
});
|
|
526
|
+
const token = plan.auth === 'access-key' ? await readAccessToken(promptIo) : undefined;
|
|
527
|
+
if (!(await confirmPlan(promptIo, plan))) {
|
|
528
|
+
output(io, global.json, { applied: false }, 'Nothing was changed.');
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
await applyPlan(plan, token, global);
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
configCommand
|
|
535
|
+
.command('init')
|
|
536
|
+
.description('Configure Synomem without prompting')
|
|
537
|
+
.option('--backend <kind>', 'local or remote')
|
|
538
|
+
.option('--auth <method>', 'browser or access-key')
|
|
539
|
+
.option('--workspace <id>', 'remote workspace ID')
|
|
540
|
+
.option('--credential-store <where>', 'auto, keychain, file, or environment', 'auto')
|
|
541
|
+
// The token is read from stdin, never taken as an argument: an argument is
|
|
542
|
+
// kept by the shell history and visible in the process list.
|
|
543
|
+
.option('--access-token-stdin', 'read the installation access key from stdin', false)
|
|
544
|
+
.option('--yes', 'apply without confirming', false)
|
|
545
|
+
.action(
|
|
546
|
+
async (
|
|
547
|
+
options: {
|
|
548
|
+
backend?: string;
|
|
549
|
+
auth?: string;
|
|
550
|
+
workspace?: string;
|
|
551
|
+
credentialStore: string;
|
|
552
|
+
accessTokenStdin: boolean;
|
|
553
|
+
yes: boolean;
|
|
554
|
+
},
|
|
555
|
+
command: Command,
|
|
556
|
+
) => {
|
|
557
|
+
const global = globals(command);
|
|
558
|
+
if (options.backend !== 'local' && options.backend !== 'remote') {
|
|
559
|
+
throw new SynomemError('INVALID_INPUT', 'Pass --backend local or --backend remote.');
|
|
560
|
+
}
|
|
561
|
+
const backend: BackendChoice = options.backend;
|
|
562
|
+
if (backend === 'remote' && !options.workspace) {
|
|
563
|
+
throw new SynomemError('INVALID_INPUT', 'Remote setup requires --workspace.');
|
|
564
|
+
}
|
|
565
|
+
const token = options.accessTokenStdin ? await readAccessToken(promptIo) : undefined;
|
|
566
|
+
if (backend === 'remote' && options.auth === 'access-key' && !token) {
|
|
567
|
+
throw new SynomemError(
|
|
568
|
+
'INVALID_INPUT',
|
|
569
|
+
'Access-key setup requires --access-token-stdin so the key is not passed as an argument.',
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
const plan: ConfigPlan = {
|
|
573
|
+
backend,
|
|
574
|
+
home: resolveHome(global.home),
|
|
575
|
+
...(backend === 'remote'
|
|
576
|
+
? {
|
|
577
|
+
serviceUrl: cloudApiUrl(env),
|
|
578
|
+
auth: (options.auth as AuthChoice | undefined) ?? 'access-key',
|
|
579
|
+
workspaceId: options.workspace,
|
|
580
|
+
credentialStore: options.credentialStore as CredentialStoreChoice,
|
|
581
|
+
}
|
|
582
|
+
: {}),
|
|
583
|
+
};
|
|
584
|
+
if (!options.yes) {
|
|
585
|
+
throw new SynomemError('INVALID_INPUT', 'Re-run with --yes to apply this configuration.');
|
|
586
|
+
}
|
|
587
|
+
await applyPlan(plan, token, global);
|
|
588
|
+
},
|
|
589
|
+
);
|
|
590
|
+
|
|
591
|
+
configCommand
|
|
592
|
+
.command('show')
|
|
593
|
+
.description('Show the current configuration without revealing secrets')
|
|
594
|
+
.action(async (_options, command: Command) => {
|
|
595
|
+
const global = globals(command);
|
|
596
|
+
const home = resolveHome(global.home);
|
|
597
|
+
const config = readSynomemConfig(global.home, env);
|
|
598
|
+
const backend = config?.backend ?? { kind: 'local' as const };
|
|
599
|
+
const credentialSource = env.SYNOMEM_ACCESS_TOKEN
|
|
600
|
+
? 'environment (SYNOMEM_ACCESS_TOKEN)'
|
|
601
|
+
: existsSync(join(home, 'credentials', 'installation.json'))
|
|
602
|
+
? 'restricted file'
|
|
603
|
+
: 'platform credential store or none';
|
|
604
|
+
output(
|
|
605
|
+
io,
|
|
606
|
+
global.json,
|
|
607
|
+
// Never the secret itself, only where it comes from.
|
|
608
|
+
{ backend, home, credentialSource, stores: credentialStoreChoices().map((c) => c.value) },
|
|
609
|
+
[
|
|
610
|
+
`Backend: ${backend.kind === 'local' ? 'Local SQLite' : 'Synomem Cloud'}`,
|
|
611
|
+
`Home: ${home}`,
|
|
612
|
+
...(backend.kind === 'remote'
|
|
613
|
+
? [`Service: ${backend.baseUrl}`, `Workspace: ${backend.workspaceId}`]
|
|
614
|
+
: []),
|
|
615
|
+
`Credential: ${credentialSource}`,
|
|
616
|
+
].join('\n'),
|
|
617
|
+
);
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
program
|
|
621
|
+
.command('reset')
|
|
622
|
+
.description('Remove Synomem configuration, database and credentials')
|
|
623
|
+
// Integrations are opt-in because they live in other tools' directories.
|
|
624
|
+
// Removing somebody's harness configuration as a side effect of resetting
|
|
625
|
+
// Synomem would be a surprise with no undo.
|
|
626
|
+
.option('--integrations', 'also remove installed skills and MCP registrations', false)
|
|
627
|
+
.option('--yes', 'apply the displayed plan', false)
|
|
628
|
+
.action(async (options: { integrations: boolean; yes: boolean }, command: Command) => {
|
|
629
|
+
const global = globals(command);
|
|
630
|
+
const home = resolveHome(global.home);
|
|
631
|
+
|
|
632
|
+
/*
|
|
633
|
+
* Every target is an exact path, listed before anything is touched. No
|
|
634
|
+
* recursive delete is ever derived from a variable that might be empty:
|
|
635
|
+
* a reset that computes `rm -rf $HOME/` from an unset home is the
|
|
636
|
+
* failure this shape exists to make impossible.
|
|
637
|
+
*/
|
|
638
|
+
const targets = [
|
|
639
|
+
join(home, 'config.json'),
|
|
640
|
+
join(home, 'synomem.sqlite3'),
|
|
641
|
+
join(home, 'synomem.sqlite3-wal'),
|
|
642
|
+
join(home, 'synomem.sqlite3-shm'),
|
|
643
|
+
join(home, 'credentials', 'installation.json'),
|
|
644
|
+
].filter((path) => existsSync(path));
|
|
645
|
+
|
|
646
|
+
const skillPlan = options.integrations ? uninstallSkill({ apply: false }) : undefined;
|
|
647
|
+
const skillTargets =
|
|
648
|
+
skillPlan?.locations
|
|
649
|
+
// Installed Synomem-owned copies only; an unowned directory at
|
|
650
|
+
// the same path is not ours to remove.
|
|
651
|
+
.filter((location) => location.state === 'current' || location.state === 'stale')
|
|
652
|
+
.map((location) => location.target) ?? [];
|
|
653
|
+
|
|
654
|
+
if (!options.yes) {
|
|
655
|
+
output(
|
|
656
|
+
io,
|
|
657
|
+
global.json,
|
|
658
|
+
{ targets, skillTargets, applied: false },
|
|
659
|
+
[
|
|
660
|
+
'This will remove:',
|
|
661
|
+
...(targets.length ? targets.map((path) => ` ${path}`) : [' (nothing found)']),
|
|
662
|
+
...(skillTargets.length ? ['', 'And these Synomem-owned skills:'] : []),
|
|
663
|
+
...skillTargets.map((path) => ` ${path}`),
|
|
664
|
+
'',
|
|
665
|
+
...(options.integrations
|
|
666
|
+
? []
|
|
667
|
+
: ['Installed skills and MCP registrations are left alone.', '']),
|
|
668
|
+
'Run with --yes to continue.',
|
|
669
|
+
].join('\n'),
|
|
670
|
+
);
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
const removed: string[] = [];
|
|
675
|
+
for (const path of targets) {
|
|
676
|
+
rmSync(path, { force: true });
|
|
677
|
+
removed.push(path);
|
|
678
|
+
}
|
|
679
|
+
// Only ownership-stamped Synomem skills are removed, which uninstall
|
|
680
|
+
// already enforces — an unowned directory at the same path is left.
|
|
681
|
+
const skillResult = options.integrations ? uninstallSkill({ apply: true }) : undefined;
|
|
682
|
+
|
|
683
|
+
output(
|
|
684
|
+
io,
|
|
685
|
+
global.json,
|
|
686
|
+
{ removed, skills: skillResult?.locations ?? [] },
|
|
687
|
+
[
|
|
688
|
+
`Removed ${removed.length} file(s).`,
|
|
689
|
+
...(skillResult
|
|
690
|
+
? [`Skill locations processed: ${skillResult.locations.length}.`]
|
|
691
|
+
: ['Installed skills and MCP registrations were left alone.']),
|
|
692
|
+
].join('\n'),
|
|
693
|
+
);
|
|
694
|
+
});
|
|
695
|
+
|
|
419
696
|
const backendCommand = program
|
|
420
697
|
.command('backend')
|
|
421
698
|
.description('Inspect or select the canonical backend');
|
|
@@ -436,23 +713,28 @@ export function createCli(
|
|
|
436
713
|
.command('use')
|
|
437
714
|
.description('Select local or remote canonical state')
|
|
438
715
|
.argument('<kind>', 'local or remote')
|
|
439
|
-
|
|
716
|
+
// --url is for development and private deployments. It stays out of the
|
|
717
|
+
// README, the public docs and the packaged skill: public onboarding must
|
|
718
|
+
// never ask for a service address, because a person has no way to tell a
|
|
719
|
+
// real one from a phished one.
|
|
720
|
+
.option('--url <url>', 'internal: alternate HTTPS origin')
|
|
440
721
|
.option('--workspace <id>', 'remote workspace ID')
|
|
441
722
|
.action((kind: string, options: { url?: string; workspace?: string }, command: Command) => {
|
|
442
723
|
const global = globals(command);
|
|
443
724
|
if (kind !== 'local' && kind !== 'remote') {
|
|
444
725
|
throw new SynomemError('INVALID_INPUT', 'Backend kind must be local or remote.');
|
|
445
726
|
}
|
|
446
|
-
if (kind === 'remote' &&
|
|
447
|
-
throw new SynomemError(
|
|
448
|
-
'INVALID_INPUT',
|
|
449
|
-
'Remote backend selection requires --url and --workspace.',
|
|
450
|
-
);
|
|
727
|
+
if (kind === 'remote' && !options.workspace) {
|
|
728
|
+
throw new SynomemError('INVALID_INPUT', 'Remote backend selection requires --workspace.');
|
|
451
729
|
}
|
|
452
730
|
const config = writeSynomemBackend(
|
|
453
731
|
kind === 'local'
|
|
454
732
|
? { kind: 'local' }
|
|
455
|
-
: {
|
|
733
|
+
: {
|
|
734
|
+
kind: 'remote',
|
|
735
|
+
baseUrl: options.url ?? cloudApiUrl(env),
|
|
736
|
+
workspaceId: options.workspace!,
|
|
737
|
+
},
|
|
456
738
|
global.home,
|
|
457
739
|
);
|
|
458
740
|
output(
|
|
@@ -599,14 +881,14 @@ export function createCli(
|
|
|
599
881
|
.command('agent')
|
|
600
882
|
.description('Create and inspect stable agent identities');
|
|
601
883
|
agentCommand
|
|
602
|
-
.command('create <
|
|
603
|
-
.description('Create
|
|
884
|
+
.command('create <handle>')
|
|
885
|
+
.description('Create an agent. The canonical ID is generated, not chosen.')
|
|
604
886
|
.requiredOption('--name <display-name>', 'display name')
|
|
605
|
-
.option('--alias <
|
|
887
|
+
.option('--alias <name>', 'alias (repeatable)', collect, [])
|
|
606
888
|
.option('--description <text>')
|
|
607
889
|
.action(
|
|
608
890
|
async (
|
|
609
|
-
|
|
891
|
+
handle: string,
|
|
610
892
|
options: { name: string; alias: string[]; description?: string },
|
|
611
893
|
command: Command,
|
|
612
894
|
) => {
|
|
@@ -616,16 +898,87 @@ export function createCli(
|
|
|
616
898
|
defaultActor(env, 'system', 'cli'),
|
|
617
899
|
(client) =>
|
|
618
900
|
client.agents.create({
|
|
619
|
-
|
|
901
|
+
handle,
|
|
620
902
|
displayName: options.name,
|
|
621
903
|
...(options.alias.length ? { aliases: options.alias } : {}),
|
|
622
904
|
...(options.description ? { description: options.description } : {}),
|
|
623
905
|
}),
|
|
624
906
|
);
|
|
625
|
-
|
|
907
|
+
// Both are printed because both matter: the handle is what people type,
|
|
908
|
+
// the ID is what every event records and what MCP registration uses.
|
|
909
|
+
output(
|
|
910
|
+
io,
|
|
911
|
+
global.json,
|
|
912
|
+
profile,
|
|
913
|
+
`Created ${profile.displayName}\n\nHandle: ${profile.handle}\nAgent ID: ${profile.id}`,
|
|
914
|
+
);
|
|
626
915
|
},
|
|
627
916
|
);
|
|
628
917
|
|
|
918
|
+
const aliasCommand = agentCommand
|
|
919
|
+
.command('alias')
|
|
920
|
+
.description('Add or remove discovery aliases without replacing the set');
|
|
921
|
+
|
|
922
|
+
aliasCommand
|
|
923
|
+
.command('add <agent> <alias...>')
|
|
924
|
+
.description('Add aliases, keeping the ones already there')
|
|
925
|
+
.action(async (agent: string, aliases: string[], _options, command: Command) => {
|
|
926
|
+
const global = globals(command);
|
|
927
|
+
const profile = await withClient(global.home, defaultActor(env, 'system', 'cli'), (client) =>
|
|
928
|
+
client.agents.addAliases(agent, aliases),
|
|
929
|
+
);
|
|
930
|
+
output(io, global.json, profile, `Aliases: ${(profile.aliases ?? []).join(', ') || 'none'}`);
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
aliasCommand
|
|
934
|
+
.command('remove <agent> <alias...>')
|
|
935
|
+
.description('Remove aliases, keeping the rest')
|
|
936
|
+
.action(async (agent: string, aliases: string[], _options, command: Command) => {
|
|
937
|
+
const global = globals(command);
|
|
938
|
+
const profile = await withClient(global.home, defaultActor(env, 'system', 'cli'), (client) =>
|
|
939
|
+
client.agents.removeAliases(agent, aliases),
|
|
940
|
+
);
|
|
941
|
+
output(io, global.json, profile, `Aliases: ${(profile.aliases ?? []).join(', ') || 'none'}`);
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
agentCommand
|
|
945
|
+
.command('rename <agent> <handle>')
|
|
946
|
+
.description('Change an agent handle. Its canonical ID never changes.')
|
|
947
|
+
.action(async (agent: string, handle: string, _options, command: Command) => {
|
|
948
|
+
const global = globals(command);
|
|
949
|
+
const profile = await withClient(global.home, defaultActor(env, 'system', 'cli'), (client) =>
|
|
950
|
+
client.agents.update(agent, { handle }),
|
|
951
|
+
);
|
|
952
|
+
output(
|
|
953
|
+
io,
|
|
954
|
+
global.json,
|
|
955
|
+
profile,
|
|
956
|
+
`Handle: ${profile.handle}\nAgent ID: ${profile.id} (unchanged)`,
|
|
957
|
+
);
|
|
958
|
+
});
|
|
959
|
+
|
|
960
|
+
agentCommand
|
|
961
|
+
.command('archive <agent>')
|
|
962
|
+
.description('Stop an agent acting, keeping its records and history')
|
|
963
|
+
.action(async (agent: string, _options, command: Command) => {
|
|
964
|
+
const global = globals(command);
|
|
965
|
+
const profile = await withClient(global.home, defaultActor(env, 'system', 'cli'), (client) =>
|
|
966
|
+
client.agents.archive(agent),
|
|
967
|
+
);
|
|
968
|
+
output(io, global.json, profile, `Archived ${profile.handle} (${profile.id})`);
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
agentCommand
|
|
972
|
+
.command('restore <agent>')
|
|
973
|
+
.description('Let an archived agent act again')
|
|
974
|
+
.action(async (agent: string, _options, command: Command) => {
|
|
975
|
+
const global = globals(command);
|
|
976
|
+
const profile = await withClient(global.home, defaultActor(env, 'system', 'cli'), (client) =>
|
|
977
|
+
client.agents.restore(agent),
|
|
978
|
+
);
|
|
979
|
+
output(io, global.json, profile, `Restored ${profile.handle} (${profile.id})`);
|
|
980
|
+
});
|
|
981
|
+
|
|
629
982
|
const skillCommand = program
|
|
630
983
|
.command('skill')
|
|
631
984
|
.description('Install and maintain the packaged agent skill');
|
|
@@ -750,7 +1103,11 @@ export function createCli(
|
|
|
750
1103
|
? agents
|
|
751
1104
|
.map(
|
|
752
1105
|
(profile) =>
|
|
753
|
-
|
|
1106
|
+
// Handle first: it is what people type. The canonical ID
|
|
1107
|
+
// follows because MCP registration needs it.
|
|
1108
|
+
`${profile.handle} ${profile.displayName}${
|
|
1109
|
+
profile.status === 'archived' ? ' [archived]' : ''
|
|
1110
|
+
}${profile.aliases?.length ? ` aliases: ${profile.aliases.join(', ')}` : ''}\n ${profile.id}`,
|
|
754
1111
|
)
|
|
755
1112
|
.join('\n')
|
|
756
1113
|
: 'No agents configured.';
|
|
@@ -769,7 +1126,7 @@ export function createCli(
|
|
|
769
1126
|
io,
|
|
770
1127
|
global.json,
|
|
771
1128
|
profile,
|
|
772
|
-
`${profile.displayName}
|
|
1129
|
+
`${profile.displayName}\n\nHandle: ${profile.handle}\nAgent ID: ${profile.id}\nStatus: ${profile.status}\n\n${profile.description ?? 'No description.'}`,
|
|
773
1130
|
);
|
|
774
1131
|
});
|
|
775
1132
|
|
|
@@ -923,6 +1280,172 @@ export function createCli(
|
|
|
923
1280
|
);
|
|
924
1281
|
});
|
|
925
1282
|
|
|
1283
|
+
const postCommand = program.command('post').description('Publish to everyone in the workspace');
|
|
1284
|
+
|
|
1285
|
+
postCommand
|
|
1286
|
+
.command('create')
|
|
1287
|
+
.description('Publish a post the whole workspace can read')
|
|
1288
|
+
.requiredOption('--as <actor-id>')
|
|
1289
|
+
.option('--actor-kind <kind>', 'human, agent, or system', 'agent')
|
|
1290
|
+
.requiredOption('--title <title>')
|
|
1291
|
+
.requiredOption('--body <body>')
|
|
1292
|
+
.option('--tag <tag>', 'repeatable', collect, [])
|
|
1293
|
+
.option('--reply-to <post-id>')
|
|
1294
|
+
.action(
|
|
1295
|
+
async (
|
|
1296
|
+
options: {
|
|
1297
|
+
as: string;
|
|
1298
|
+
actorKind: string;
|
|
1299
|
+
title: string;
|
|
1300
|
+
body: string;
|
|
1301
|
+
tag: string[];
|
|
1302
|
+
replyTo?: string;
|
|
1303
|
+
},
|
|
1304
|
+
command: Command,
|
|
1305
|
+
) => {
|
|
1306
|
+
const global = globals(command);
|
|
1307
|
+
const result = await withClient(
|
|
1308
|
+
global.home,
|
|
1309
|
+
actor(options.actorKind, options.as),
|
|
1310
|
+
(client) =>
|
|
1311
|
+
client.posts.create({
|
|
1312
|
+
title: options.title,
|
|
1313
|
+
body: options.body,
|
|
1314
|
+
...(options.tag.length ? { tags: options.tag } : {}),
|
|
1315
|
+
...(options.replyTo ? { replyTo: options.replyTo } : {}),
|
|
1316
|
+
}),
|
|
1317
|
+
);
|
|
1318
|
+
output(io, global.json, result, `Published ${result.record.event.id}`);
|
|
1319
|
+
},
|
|
1320
|
+
);
|
|
1321
|
+
|
|
1322
|
+
postCommand
|
|
1323
|
+
.command('list')
|
|
1324
|
+
.description('List posts in this workspace')
|
|
1325
|
+
.requiredOption('--as <actor-id>')
|
|
1326
|
+
.option('--actor-kind <kind>', 'human, agent, or system', 'agent')
|
|
1327
|
+
.option('--limit <n>', 'default 10, maximum 50')
|
|
1328
|
+
.action(
|
|
1329
|
+
async (options: { as: string; actorKind: string; limit?: string }, command: Command) => {
|
|
1330
|
+
const global = globals(command);
|
|
1331
|
+
const page = await withClient(global.home, actor(options.actorKind, options.as), (client) =>
|
|
1332
|
+
client.posts.list(options.limit ? { limit: Number(options.limit) } : {}),
|
|
1333
|
+
);
|
|
1334
|
+
const human = page.items.length
|
|
1335
|
+
? page.items.map((item) => `${item.id} ${item.title}`).join('\n')
|
|
1336
|
+
: 'No posts yet.';
|
|
1337
|
+
output(io, global.json, page, human);
|
|
1338
|
+
},
|
|
1339
|
+
);
|
|
1340
|
+
|
|
1341
|
+
postCommand
|
|
1342
|
+
.command('show <post-id>')
|
|
1343
|
+
.description('Show one post with its acknowledgements')
|
|
1344
|
+
.requiredOption('--as <actor-id>')
|
|
1345
|
+
.option('--actor-kind <kind>', 'human, agent, or system', 'agent')
|
|
1346
|
+
.action(
|
|
1347
|
+
async (postId: string, options: { as: string; actorKind: string }, command: Command) => {
|
|
1348
|
+
const global = globals(command);
|
|
1349
|
+
const record = await withClient(
|
|
1350
|
+
global.home,
|
|
1351
|
+
actor(options.actorKind, options.as),
|
|
1352
|
+
(client) => client.posts.get(postId),
|
|
1353
|
+
);
|
|
1354
|
+
const acks = record.acknowledgments.length
|
|
1355
|
+
? record.acknowledgments
|
|
1356
|
+
.map((entry) => ` ${entry.actor.id}${entry.note ? ` — ${entry.note}` : ''}`)
|
|
1357
|
+
.join('\n')
|
|
1358
|
+
: ' none yet';
|
|
1359
|
+
output(
|
|
1360
|
+
io,
|
|
1361
|
+
global.json,
|
|
1362
|
+
record,
|
|
1363
|
+
`${record.title}\n\n${record.body}\n\nAcknowledged by:\n${acks}`,
|
|
1364
|
+
);
|
|
1365
|
+
},
|
|
1366
|
+
);
|
|
1367
|
+
|
|
1368
|
+
postCommand
|
|
1369
|
+
.command('acknowledge <post-id>')
|
|
1370
|
+
.description('Say you have seen a post')
|
|
1371
|
+
.requiredOption('--as <actor-id>')
|
|
1372
|
+
.option('--actor-kind <kind>', 'human, agent, or system', 'agent')
|
|
1373
|
+
.option('--note <text>', 'optional context for the author')
|
|
1374
|
+
.action(
|
|
1375
|
+
async (
|
|
1376
|
+
postId: string,
|
|
1377
|
+
options: { as: string; actorKind: string; note?: string },
|
|
1378
|
+
command: Command,
|
|
1379
|
+
) => {
|
|
1380
|
+
const global = globals(command);
|
|
1381
|
+
const record = await withClient(
|
|
1382
|
+
global.home,
|
|
1383
|
+
actor(options.actorKind, options.as),
|
|
1384
|
+
(client) =>
|
|
1385
|
+
client.posts.acknowledge({
|
|
1386
|
+
postId,
|
|
1387
|
+
...(options.note ? { note: options.note } : {}),
|
|
1388
|
+
}),
|
|
1389
|
+
);
|
|
1390
|
+
output(io, global.json, record, `Acknowledged ${postId}`);
|
|
1391
|
+
},
|
|
1392
|
+
);
|
|
1393
|
+
|
|
1394
|
+
postCommand
|
|
1395
|
+
.command('roster <post-id>')
|
|
1396
|
+
.description('Who has acknowledged a post, and who has not')
|
|
1397
|
+
.requiredOption('--as <actor-id>')
|
|
1398
|
+
.option('--actor-kind <kind>', 'human, agent, or system', 'agent')
|
|
1399
|
+
.action(
|
|
1400
|
+
async (postId: string, options: { as: string; actorKind: string }, command: Command) => {
|
|
1401
|
+
const global = globals(command);
|
|
1402
|
+
const roster = await withClient(
|
|
1403
|
+
global.home,
|
|
1404
|
+
actor(options.actorKind, options.as),
|
|
1405
|
+
(client) => client.posts.roster(postId),
|
|
1406
|
+
);
|
|
1407
|
+
// "Outstanding" means no acknowledgement recorded — never that somebody
|
|
1408
|
+
// has not read it, which this cannot know.
|
|
1409
|
+
const lines = [
|
|
1410
|
+
`Acknowledged (${roster.acknowledged.length}):`,
|
|
1411
|
+
...(roster.acknowledged.length
|
|
1412
|
+
? roster.acknowledged.map((entry) => ` ${entry.actor.id}`)
|
|
1413
|
+
: [' none yet']),
|
|
1414
|
+
`No acknowledgement recorded (${roster.outstanding.length}):`,
|
|
1415
|
+
...(roster.outstanding.length
|
|
1416
|
+
? roster.outstanding.map((entry) => ` ${entry.id}`)
|
|
1417
|
+
: [' none']),
|
|
1418
|
+
];
|
|
1419
|
+
if (roster.joinedSince > 0) {
|
|
1420
|
+
lines.push(`${roster.joinedSince} agent(s) joined after this was posted.`);
|
|
1421
|
+
}
|
|
1422
|
+
output(io, global.json, roster, lines.join('\n'));
|
|
1423
|
+
},
|
|
1424
|
+
);
|
|
1425
|
+
|
|
1426
|
+
postCommand
|
|
1427
|
+
.command('archive <post-id>')
|
|
1428
|
+
.description('Archive a post you wrote')
|
|
1429
|
+
.requiredOption('--as <actor-id>')
|
|
1430
|
+
.option('--actor-kind <kind>', 'human, agent, or system', 'agent')
|
|
1431
|
+
.option('--reason <text>')
|
|
1432
|
+
.action(
|
|
1433
|
+
async (
|
|
1434
|
+
postId: string,
|
|
1435
|
+
options: { as: string; actorKind: string; reason?: string },
|
|
1436
|
+
command: Command,
|
|
1437
|
+
) => {
|
|
1438
|
+
const global = globals(command);
|
|
1439
|
+
const record = await withClient(
|
|
1440
|
+
global.home,
|
|
1441
|
+
actor(options.actorKind, options.as),
|
|
1442
|
+
(client) =>
|
|
1443
|
+
client.posts.archive({ postId, ...(options.reason ? { reason: options.reason } : {}) }),
|
|
1444
|
+
);
|
|
1445
|
+
output(io, global.json, record, `Archived ${postId}`);
|
|
1446
|
+
},
|
|
1447
|
+
);
|
|
1448
|
+
|
|
926
1449
|
const kudosCommand = program.command('kudos').description('Give and manage agent recognition');
|
|
927
1450
|
|
|
928
1451
|
kudosCommand
|
|
@@ -1817,14 +2340,15 @@ export function createCli(
|
|
|
1817
2340
|
);
|
|
1818
2341
|
}
|
|
1819
2342
|
const capabilities = await client.capabilities();
|
|
1820
|
-
|
|
2343
|
+
// Projections are written under the handle, since they exist to be read.
|
|
2344
|
+
const path = join(info.home, profile.handle, 'WINS.md');
|
|
1821
2345
|
if (!existsSync(path)) {
|
|
1822
2346
|
const hint = capabilities.projections.writeWinsMarkdown
|
|
1823
2347
|
? 'Run `synomem rebuild` to generate it.'
|
|
1824
2348
|
: 'Enable projection.writeWinsMarkdown and run `synomem rebuild`.';
|
|
1825
2349
|
throw new SynomemError(
|
|
1826
2350
|
'INVALID_INPUT',
|
|
1827
|
-
`No generated WINS.md exists for ${profile.
|
|
2351
|
+
`No generated WINS.md exists for ${profile.handle}. ${hint}`,
|
|
1828
2352
|
);
|
|
1829
2353
|
}
|
|
1830
2354
|
return { profile, path, content: readFileSync(path, 'utf8') };
|