synomem 0.5.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/README.md +30 -12
  3. package/dist/cli.d.ts +1 -1
  4. package/dist/cli.d.ts.map +1 -1
  5. package/dist/cli.js +205 -32
  6. package/dist/cli.js.map +1 -1
  7. package/dist/client.d.ts.map +1 -1
  8. package/dist/client.js +10 -0
  9. package/dist/client.js.map +1 -1
  10. package/dist/config.d.ts +1 -0
  11. package/dist/config.d.ts.map +1 -1
  12. package/dist/config.js +4 -0
  13. package/dist/config.js.map +1 -1
  14. package/dist/import.d.ts +4 -4
  15. package/dist/index.d.ts +4 -0
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +2 -0
  18. package/dist/index.js.map +1 -1
  19. package/dist/mcp/index.d.ts.map +1 -1
  20. package/dist/mcp/index.js +49 -3
  21. package/dist/mcp/index.js.map +1 -1
  22. package/dist/mcp-server.js +26 -3
  23. package/dist/mcp-server.js.map +1 -1
  24. package/dist/ports/projections.d.ts +8 -0
  25. package/dist/ports/projections.d.ts.map +1 -1
  26. package/dist/project.d.ts +51 -0
  27. package/dist/project.d.ts.map +1 -0
  28. package/dist/project.js +143 -0
  29. package/dist/project.js.map +1 -0
  30. package/dist/projections.d.ts +14 -0
  31. package/dist/projections.d.ts.map +1 -1
  32. package/dist/projections.js +36 -1
  33. package/dist/projections.js.map +1 -1
  34. package/dist/schemas.d.ts +18 -4
  35. package/dist/schemas.d.ts.map +1 -1
  36. package/dist/schemas.js +29 -2
  37. package/dist/schemas.js.map +1 -1
  38. package/dist/service.d.ts +1 -0
  39. package/dist/service.d.ts.map +1 -1
  40. package/dist/storage.d.ts +13 -0
  41. package/dist/storage.d.ts.map +1 -1
  42. package/dist/storage.js +24 -0
  43. package/dist/storage.js.map +1 -1
  44. package/dist/types.d.ts +1 -0
  45. package/dist/types.d.ts.map +1 -1
  46. package/dist/workspaces.d.ts +41 -0
  47. package/dist/workspaces.d.ts.map +1 -0
  48. package/dist/workspaces.js +96 -0
  49. package/dist/workspaces.js.map +1 -0
  50. package/package.json +1 -1
  51. package/skills/synomem/SKILL.md +9 -3
  52. package/skills/synomem/references/examples.md +5 -3
  53. package/src/cli.ts +245 -32
  54. package/src/client.ts +10 -0
  55. package/src/config.ts +4 -0
  56. package/src/index.ts +17 -0
  57. package/src/mcp/index.ts +65 -3
  58. package/src/mcp-server.ts +32 -5
  59. package/src/ports/projections.ts +9 -0
  60. package/src/project.ts +168 -0
  61. package/src/projections.ts +38 -1
  62. package/src/schemas.ts +44 -12
  63. package/src/service.ts +1 -0
  64. package/src/storage.ts +28 -0
  65. package/src/types.ts +1 -0
  66. package/src/workspaces.ts +107 -0
package/src/cli.ts CHANGED
@@ -22,6 +22,12 @@ import {
22
22
  type CredentialStoreChoice,
23
23
  } from './configure.js';
24
24
  import { discoverBoundWorkspace, discoverOrganizations, workspaceChoices } from './discover.js';
25
+ import { DEFAULT_WORKSPACE, listLocalWorkspaces, localWorkspaceHome } from './workspaces.js';
26
+ import {
27
+ findProjectSelection,
28
+ resolveWorkspaceSelection,
29
+ writeProjectSelection,
30
+ } from './project.js';
25
31
  import { defaultPromptIo, type PromptIo } from './prompt.js';
26
32
  import { credentialReference, OsCredentialStore, type CredentialStore } from './credentials.js';
27
33
  import { asSynomemError, SynomemError, type SynomemErrorCode } from './errors.js';
@@ -148,10 +154,18 @@ function defaultActor(
148
154
  env: NodeJS.ProcessEnv,
149
155
  fallbackKind: string,
150
156
  fallbackId: string,
157
+ /** `--actor`, which outranks the environment: it is said on this invocation. */
158
+ override?: string,
151
159
  ): ActorIdentity {
152
- const id = env.SYNOMEM_ACTOR_ID?.trim();
160
+ const id = override?.trim() || env.SYNOMEM_ACTOR_ID?.trim();
153
161
  if (!id) return actor(fallbackKind, fallbackId);
154
- return actor(env.SYNOMEM_ACTOR_KIND?.trim() || fallbackKind, id, env.SYNOMEM_ACTOR_NAME?.trim());
162
+ const kind = override?.trim()
163
+ ? // An explicit --actor names an agent unless told otherwise; the historical
164
+ // fallbacks here are `system`/`cli`, which is not what somebody means when
165
+ // they name one.
166
+ env.SYNOMEM_ACTOR_KIND?.trim() || 'agent'
167
+ : env.SYNOMEM_ACTOR_KIND?.trim() || fallbackKind;
168
+ return actor(kind, id, env.SYNOMEM_ACTOR_NAME?.trim());
155
169
  }
156
170
 
157
171
  function taskDue(options: {
@@ -245,10 +259,30 @@ function output(io: CliIo, json: boolean, value: unknown, human: string): void {
245
259
  io.stdout(json ? `${JSON.stringify(value, null, 2)}\n` : `${human}\n`);
246
260
  }
247
261
 
248
- function globals(command: Command): { home?: string; json: boolean } {
249
- return command.optsWithGlobals<{ home?: string; json: boolean }>();
262
+ /**
263
+ * The resolved global options for a command.
264
+ *
265
+ * `--workspace` is turned into a home HERE, before any service exists, which is
266
+ * the whole reason it costs nothing downstream: a local workspace is a separate
267
+ * database in its own home, and choosing one is choosing a home. Nothing in the
268
+ * domain, the commands, or the MCP tools learns that a workspace was selected.
269
+ *
270
+ * On a remote backend the name means a hosted workspace instead, which
271
+ * `backend use remote --workspace` already handles; passing both here would be
272
+ * two different answers to the same question, so it is refused.
273
+ */
274
+ /** `parent child`, so a subcommand name cannot be confused with another's. */
275
+ function commandPath(command: Command): string {
276
+ const parent = command.parent?.name();
277
+ return parent && parent !== 'synomem' ? `${parent} ${command.name()}` : command.name();
250
278
  }
251
279
 
280
+ /**
281
+ * Commands where `--workspace` names a HOSTED workspace being configured,
282
+ * rather than a local one to act in.
283
+ */
284
+ const CONFIGURES_BACKEND = new Set(['config init', 'backend use', 'remote import']);
285
+
252
286
  async function withService<T>(
253
287
  serviceFactory: SynomemServiceFactory,
254
288
  home: string | undefined,
@@ -302,12 +336,87 @@ function listInput(options: Record<string, string>): KudosListInput {
302
336
  };
303
337
  }
304
338
 
339
+ /**
340
+ * The value `--actor` should supply to commands that name an actor.
341
+ *
342
+ * Read from argv directly, before the commands are built, because Commander
343
+ * evaluates option defaults at DECLARATION time: a `--as` declared without one
344
+ * is required, and a `--as` declared with one is already satisfied. Supplying
345
+ * it here is a single change point instead of a fallback threaded through
346
+ * twenty action bodies, and `--as` still wins when both are given because an
347
+ * explicitly passed option overrides its default.
348
+ */
349
+ function actorDefault(argv: string[], env: NodeJS.ProcessEnv): string | undefined {
350
+ for (let index = 0; index < argv.length; index += 1) {
351
+ const argument = argv[index]!;
352
+ if (argument === '--actor') return argv[index + 1]?.trim() || undefined;
353
+ if (argument.startsWith('--actor='))
354
+ return argument.slice('--actor='.length).trim() || undefined;
355
+ }
356
+ if (env.SYNOMEM_ACTOR_ID?.trim()) return env.SYNOMEM_ACTOR_ID.trim();
357
+ /*
358
+ * Last, the project's own binding. `workspace use --as` exists so a
359
+ * repository can settle both questions once — which workspace, and as whom —
360
+ * and a command run there needs neither flag afterwards.
361
+ */
362
+ try {
363
+ return findProjectSelection()?.actor;
364
+ } catch {
365
+ // A malformed project file is reported by the resolver when the command
366
+ // actually runs, with the path in the message. Failing here would turn it
367
+ // into an error before any command had been parsed.
368
+ return undefined;
369
+ }
370
+ }
371
+
305
372
  export function createCli(
306
373
  io: CliIo = defaultIo,
307
374
  serviceFactory: SynomemServiceFactory = configuredServiceFactory,
308
375
  dependencies: CliDependencies = {},
376
+ argv: string[] = process.argv,
309
377
  ): Command {
310
378
  const env = dependencies.env ?? process.env;
379
+ const actingDefault = actorDefault(argv, env);
380
+
381
+ const globals = (
382
+ command: Command,
383
+ ): { home?: string; json: boolean; actor?: string; workspace?: string } => {
384
+ const options = command.optsWithGlobals<{
385
+ home?: string;
386
+ json: boolean;
387
+ workspace?: string;
388
+ actor?: string;
389
+ }>();
390
+ if (CONFIGURES_BACKEND.has(commandPath(command))) return options;
391
+ /*
392
+ * Resolution runs even with no `--workspace`, because a project's
393
+ * `.synomem/config.json` selects one without anybody passing a flag — that is
394
+ * the whole point of it. `--home` still wins outright: it names a home
395
+ * directly rather than a workspace within one.
396
+ */
397
+ if (!options.home) {
398
+ const selection = resolveWorkspaceSelection({
399
+ ...(options.workspace ? { flag: options.workspace } : {}),
400
+ ...(options.actor ? { actorFlag: options.actor } : {}),
401
+ env,
402
+ });
403
+ return { ...options, home: selection.home, actor: selection.actor ?? options.actor };
404
+ }
405
+ if (!options.workspace) return options;
406
+ /*
407
+ * One flag, one meaning — "which workspace" — resolved differently by the
408
+ * handful of commands that CONFIGURE a backend rather than act inside one.
409
+ * For those, the value is a hosted workspace ID to be written to the config,
410
+ * so it is passed through raw and they read `workspace`. Everywhere else it
411
+ * names a local workspace, which is a home.
412
+ *
413
+ * These commands used to declare their own `--workspace`, which does not
414
+ * work: Commander gives a duplicated long flag to the parent, so the
415
+ * subcommand never received it at all.
416
+ */
417
+ return { ...options, home: localWorkspaceHome(options.workspace, options.home) };
418
+ };
419
+
311
420
  const credentialStore = dependencies.credentialStore ?? new OsCredentialStore();
312
421
  const oauthLogin = dependencies.oauthLogin ?? loginWithOAuth;
313
422
  const discoverWorkspace = dependencies.discoverBoundWorkspace ?? discoverBoundWorkspace;
@@ -346,10 +455,113 @@ export function createCli(
346
455
  )
347
456
  .version(packageVersion())
348
457
  .option('--home <path>', 'storage root (defaults to SYNOMEM_HOME or ~/.synomem)')
458
+ // A local workspace is its own database under the root, so this selects a
459
+ // home. On a remote backend the hosted workspace is chosen by
460
+ // `backend use remote --workspace` instead.
461
+ .option('--workspace <name>', 'local workspace to act in (see `synomem workspace list`)')
462
+ .option('--actor <id>', 'act as this agent (overrides SYNOMEM_ACTOR_ID)')
349
463
  .option('--json', 'emit stable machine-readable JSON', false)
350
464
  .showSuggestionAfterError()
351
465
  .configureOutput({ writeOut: io.stdout, writeErr: io.stderr });
352
466
 
467
+ /*
468
+ * Local workspaces.
469
+ *
470
+ * Each is a separate database in its own home, which is what makes the
471
+ * isolation real: SQLite has no row-level security, so a shared file would
472
+ * rest on every query remembering to filter, with nothing to catch a miss.
473
+ * Separate files mean cross-workspace leakage is not something anybody can
474
+ * write by accident.
475
+ */
476
+ const workspaceCommand = program
477
+ .command('workspace')
478
+ .description('Work in a separate local store, isolated from the others');
479
+
480
+ workspaceCommand
481
+ .command('list')
482
+ .description('List the local workspaces on this machine')
483
+ .action((_options, command: Command) => {
484
+ const global = globals(command);
485
+ // Read from disk, so nothing is listed that does not exist.
486
+ const workspaces = listLocalWorkspaces(global.home);
487
+ // Which one is in effect here, and what decided it — a flag, the
488
+ // environment, a project file, or nothing.
489
+ const selection = resolveWorkspaceSelection({ env });
490
+ const human = workspaces
491
+ .map(
492
+ (workspace) =>
493
+ `${workspace.name === DEFAULT_WORKSPACE ? '*' : ' '} ${workspace.name.padEnd(24)} ${
494
+ workspace.initialized ? workspace.home : `${workspace.home} (not initialized)`
495
+ }`,
496
+ )
497
+ .join('\n');
498
+ output(
499
+ io,
500
+ global.json,
501
+ { workspaces, active: selection.workspace ?? DEFAULT_WORKSPACE, source: selection.source },
502
+ [
503
+ human,
504
+ '',
505
+ `Acting in: ${selection.workspace ?? DEFAULT_WORKSPACE} (from ${selection.source})`,
506
+ '',
507
+ 'Bind a directory with `synomem workspace use <name>`, or pass --workspace once.',
508
+ 'A local workspace is a separate store on this machine; a hosted workspace is shared,',
509
+ 'and is selected with `backend use remote --workspace`.',
510
+ ].join('\n'),
511
+ );
512
+ });
513
+
514
+ workspaceCommand
515
+ .command('use <name>')
516
+ .description('Bind this directory to a workspace, for every session opened here')
517
+ .option('--as <actor-id>', 'also always write as this agent', actingDefault)
518
+ .action((name: string, options: { as?: string }, command: Command) => {
519
+ const global = globals(command);
520
+ // Validated by resolving it, so a name that could never work is refused
521
+ // before a file claiming it is written.
522
+ const home = localWorkspaceHome(name, undefined);
523
+ const path = writeProjectSelection(process.cwd(), {
524
+ workspace: name,
525
+ ...(options.as ? { actor: options.as } : {}),
526
+ });
527
+ output(
528
+ io,
529
+ global.json,
530
+ { path, workspace: name, home, ...(options.as ? { actor: options.as } : {}) },
531
+ [
532
+ `Wrote ${path}`,
533
+ '',
534
+ `Every Synomem command and MCP server started in this directory now acts in ${name}${
535
+ options.as ? ` as ${options.as}` : ''
536
+ }, with no flag.`,
537
+ `Records live in ${home} — nothing is stored in this directory.`,
538
+ '',
539
+ 'Commit it to share the choice with the repository, or ignore it to keep it yours.',
540
+ ].join('\n'),
541
+ );
542
+ });
543
+
544
+ workspaceCommand
545
+ .command('create <name>')
546
+ .description('Create a local workspace and initialize its store')
547
+ .action(async (name: string, _options, command: Command) => {
548
+ const global = globals(command);
549
+ const home = localWorkspaceHome(name, global.home);
550
+ if (readSynomemConfig(home)) {
551
+ throw new SynomemError('INVALID_INPUT', `Workspace already exists: ${name}`);
552
+ }
553
+ writeSynomemBackend({ kind: 'local' }, home);
554
+ // Opening it once creates the database, so `list` does not report a
555
+ // workspace that exists in name only.
556
+ await withClient(home, defaultActor(env, 'system', 'cli'), async () => undefined);
557
+ output(
558
+ io,
559
+ global.json,
560
+ { name, home },
561
+ `Created workspace ${name} at ${home}.\nAct in it with --workspace ${name}.`,
562
+ );
563
+ });
564
+
353
565
  const remoteCommand = program.command('remote').description('Administer a remote workspace');
354
566
 
355
567
  /*
@@ -631,7 +843,6 @@ export function createCli(
631
843
  .description('Configure Synomem without prompting')
632
844
  .option('--backend <kind>', 'local or remote')
633
845
  .option('--auth <method>', 'browser or access-key')
634
- .option('--workspace <id>', 'remote workspace ID')
635
846
  .option('--credential-store <where>', 'auto, keychain, file, or environment', 'auto')
636
847
  // The token is read from stdin, never taken as an argument: an argument is
637
848
  // kept by the shell history and visible in the process list.
@@ -642,7 +853,6 @@ export function createCli(
642
853
  options: {
643
854
  backend?: string;
644
855
  auth?: string;
645
- workspace?: string;
646
856
  credentialStore: string;
647
857
  accessTokenStdin: boolean;
648
858
  yes: boolean;
@@ -656,7 +866,7 @@ export function createCli(
656
866
  const backend: BackendChoice = options.backend;
657
867
  // An access key names its own workspace, so --workspace is only
658
868
  // required when there is no key to ask.
659
- if (backend === 'remote' && !options.workspace && !options.accessTokenStdin) {
869
+ if (backend === 'remote' && !global.workspace && !options.accessTokenStdin) {
660
870
  throw new SynomemError(
661
871
  'INVALID_INPUT',
662
872
  'Remote setup requires --workspace, or --access-token-stdin so the key can name its own.',
@@ -676,7 +886,7 @@ export function createCli(
676
886
  ? {
677
887
  serviceUrl: cloudApiUrl(env),
678
888
  auth: (options.auth as AuthChoice | undefined) ?? 'access-key',
679
- workspaceId: options.workspace,
889
+ workspaceId: global.workspace,
680
890
  credentialStore: options.credentialStore as CredentialStoreChoice,
681
891
  }
682
892
  : {}),
@@ -818,13 +1028,12 @@ export function createCli(
818
1028
  // never ask for a service address, because a person has no way to tell a
819
1029
  // real one from a phished one.
820
1030
  .option('--url <url>', 'internal: alternate HTTPS origin')
821
- .option('--workspace <id>', 'remote workspace ID')
822
- .action((kind: string, options: { url?: string; workspace?: string }, command: Command) => {
1031
+ .action((kind: string, options: { url?: string }, command: Command) => {
823
1032
  const global = globals(command);
824
1033
  if (kind !== 'local' && kind !== 'remote') {
825
1034
  throw new SynomemError('INVALID_INPUT', 'Backend kind must be local or remote.');
826
1035
  }
827
- if (kind === 'remote' && !options.workspace) {
1036
+ if (kind === 'remote' && !global.workspace) {
828
1037
  throw new SynomemError('INVALID_INPUT', 'Remote backend selection requires --workspace.');
829
1038
  }
830
1039
  const config = writeSynomemBackend(
@@ -833,7 +1042,7 @@ export function createCli(
833
1042
  : {
834
1043
  kind: 'remote',
835
1044
  baseUrl: options.url ?? cloudApiUrl(env),
836
- workspaceId: options.workspace!,
1045
+ workspaceId: global.workspace!,
837
1046
  },
838
1047
  global.home,
839
1048
  );
@@ -1493,7 +1702,7 @@ export function createCli(
1493
1702
  postCommand
1494
1703
  .command('create')
1495
1704
  .description('Publish a post the whole workspace can read')
1496
- .requiredOption('--as <actor-id>')
1705
+ .requiredOption('--as <actor-id>', 'actor to act as (defaults to --actor)', actingDefault)
1497
1706
  .option('--actor-kind <kind>', 'human, agent, or system', 'agent')
1498
1707
  .requiredOption('--title <title>')
1499
1708
  .requiredOption('--body <body>')
@@ -1530,7 +1739,7 @@ export function createCli(
1530
1739
  postCommand
1531
1740
  .command('list')
1532
1741
  .description('List posts in this workspace')
1533
- .requiredOption('--as <actor-id>')
1742
+ .requiredOption('--as <actor-id>', 'actor to act as (defaults to --actor)', actingDefault)
1534
1743
  .option('--actor-kind <kind>', 'human, agent, or system', 'agent')
1535
1744
  .option('--limit <n>', 'default 10, maximum 50')
1536
1745
  .action(
@@ -1549,7 +1758,7 @@ export function createCli(
1549
1758
  postCommand
1550
1759
  .command('show <post-id>')
1551
1760
  .description('Show one post with its acknowledgements')
1552
- .requiredOption('--as <actor-id>')
1761
+ .requiredOption('--as <actor-id>', 'actor to act as (defaults to --actor)', actingDefault)
1553
1762
  .option('--actor-kind <kind>', 'human, agent, or system', 'agent')
1554
1763
  .action(
1555
1764
  async (postId: string, options: { as: string; actorKind: string }, command: Command) => {
@@ -1576,7 +1785,7 @@ export function createCli(
1576
1785
  postCommand
1577
1786
  .command('acknowledge <post-id>')
1578
1787
  .description('Say you have seen a post')
1579
- .requiredOption('--as <actor-id>')
1788
+ .requiredOption('--as <actor-id>', 'actor to act as (defaults to --actor)', actingDefault)
1580
1789
  .option('--actor-kind <kind>', 'human, agent, or system', 'agent')
1581
1790
  .option('--note <text>', 'optional context for the author')
1582
1791
  .action(
@@ -1602,7 +1811,7 @@ export function createCli(
1602
1811
  postCommand
1603
1812
  .command('roster <post-id>')
1604
1813
  .description('Who has acknowledged a post, and who has not')
1605
- .requiredOption('--as <actor-id>')
1814
+ .requiredOption('--as <actor-id>', 'actor to act as (defaults to --actor)', actingDefault)
1606
1815
  .option('--actor-kind <kind>', 'human, agent, or system', 'agent')
1607
1816
  .action(
1608
1817
  async (postId: string, options: { as: string; actorKind: string }, command: Command) => {
@@ -1634,7 +1843,7 @@ export function createCli(
1634
1843
  postCommand
1635
1844
  .command('archive <post-id>')
1636
1845
  .description('Archive a post you wrote')
1637
- .requiredOption('--as <actor-id>')
1846
+ .requiredOption('--as <actor-id>', 'actor to act as (defaults to --actor)', actingDefault)
1638
1847
  .option('--actor-kind <kind>', 'human, agent, or system', 'agent')
1639
1848
  .option('--reason <text>')
1640
1849
  .action(
@@ -1659,7 +1868,11 @@ export function createCli(
1659
1868
  kudosCommand
1660
1869
  .command('give <recipient>')
1661
1870
  .description('Give specific, evidence-based kudos to an agent')
1662
- .requiredOption('--from <actor-id>', 'stable ID of the giver')
1871
+ .requiredOption(
1872
+ '--from <actor-id>',
1873
+ 'stable ID of the giver (defaults to --actor)',
1874
+ actingDefault,
1875
+ )
1663
1876
  .requiredOption('--actor-kind <kind>', 'human, agent, or system')
1664
1877
  .option('--actor-name <display-name>')
1665
1878
  .requiredOption('--title <title>')
@@ -1828,7 +2041,7 @@ export function createCli(
1828
2041
  const memoCommand = program.command('memo').description('Send and manage durable messages');
1829
2042
  memoCommand
1830
2043
  .command('send <recipient>')
1831
- .requiredOption('--from <actor-id>')
2044
+ .requiredOption('--from <actor-id>', 'actor to act as (defaults to --actor)', actingDefault)
1832
2045
  .option('--actor-kind <kind>', 'human, agent, or system', 'agent')
1833
2046
  .option('--actor-name <name>')
1834
2047
  .requiredOption('--subject <subject>')
@@ -1945,7 +2158,7 @@ export function createCli(
1945
2158
  .description('Retain and revise agent-owned knowledge');
1946
2159
  noteCommand
1947
2160
  .command('create')
1948
- .requiredOption('--as <actor-id>')
2161
+ .requiredOption('--as <actor-id>', 'actor to act as (defaults to --actor)', actingDefault)
1949
2162
  .option('--actor-kind <kind>', 'agent or human', 'agent')
1950
2163
  .option('--owner <agent-id>')
1951
2164
  .requiredOption('--title <title>')
@@ -2023,7 +2236,7 @@ export function createCli(
2023
2236
  });
2024
2237
  noteCommand
2025
2238
  .command('revise <note-id>')
2026
- .requiredOption('--as <actor-id>')
2239
+ .requiredOption('--as <actor-id>', 'actor to act as (defaults to --actor)', actingDefault)
2027
2240
  .option('--actor-kind <kind>', 'agent or human', 'agent')
2028
2241
  .requiredOption('--expected-version <number>')
2029
2242
  .option('--title <title>')
@@ -2063,7 +2276,7 @@ export function createCli(
2063
2276
  );
2064
2277
  noteCommand
2065
2278
  .command('archive <note-id>')
2066
- .requiredOption('--as <actor-id>')
2279
+ .requiredOption('--as <actor-id>', 'actor to act as (defaults to --actor)', actingDefault)
2067
2280
  .option('--actor-kind <kind>', 'agent or human', 'agent')
2068
2281
  .option('--idempotency-key <key>')
2069
2282
  .action(
@@ -2091,7 +2304,7 @@ export function createCli(
2091
2304
  .description('Create and manage your own private reminders');
2092
2305
  todoCommand
2093
2306
  .command('create')
2094
- .requiredOption('--as <actor-id>')
2307
+ .requiredOption('--as <actor-id>', 'actor to act as (defaults to --actor)', actingDefault)
2095
2308
  .option('--actor-kind <kind>', 'human, agent, or system', 'agent')
2096
2309
  .requiredOption('--title <title>')
2097
2310
  .option('--details <text>', 'private working detail')
@@ -2141,7 +2354,7 @@ export function createCli(
2141
2354
  );
2142
2355
  todoCommand
2143
2356
  .command('list')
2144
- .requiredOption('--as <actor-id>')
2357
+ .requiredOption('--as <actor-id>', 'actor to act as (defaults to --actor)', actingDefault)
2145
2358
  .option('--actor-kind <kind>', 'human, agent, or system', 'agent')
2146
2359
  .option('--status <status>')
2147
2360
  .option('--limit <number>', 'maximum results', '10')
@@ -2171,7 +2384,7 @@ export function createCli(
2171
2384
  );
2172
2385
  todoCommand
2173
2386
  .command('show <todo-id>')
2174
- .requiredOption('--as <actor-id>')
2387
+ .requiredOption('--as <actor-id>', 'actor to act as (defaults to --actor)', actingDefault)
2175
2388
  .option('--actor-kind <kind>', 'human, agent, or system', 'agent')
2176
2389
  .action(async (id: string, options: { as: string; actorKind: string }, command: Command) => {
2177
2390
  const global = globals(command);
@@ -2188,7 +2401,7 @@ export function createCli(
2188
2401
  for (const operation of ['complete', 'reopen', 'cancel', 'archive'] as const) {
2189
2402
  todoCommand
2190
2403
  .command(`${operation} <todo-id>`)
2191
- .requiredOption('--as <actor-id>')
2404
+ .requiredOption('--as <actor-id>', 'actor to act as (defaults to --actor)', actingDefault)
2192
2405
  .option('--actor-kind <kind>', 'human, agent, or system', 'agent')
2193
2406
  .option('--note <text>')
2194
2407
  .option('--reason <text>')
@@ -2244,7 +2457,7 @@ export function createCli(
2244
2457
  const taskCommand = program.command('task').description('Create and manage agent tasks');
2245
2458
  taskCommand
2246
2459
  .command('create <assignee>')
2247
- .requiredOption('--from <actor-id>')
2460
+ .requiredOption('--from <actor-id>', 'actor to act as (defaults to --actor)', actingDefault)
2248
2461
  .option('--actor-kind <kind>', 'human, agent, or system', 'agent')
2249
2462
  .requiredOption('--title <title>')
2250
2463
  .option('--description <text>')
@@ -2334,7 +2547,7 @@ export function createCli(
2334
2547
  });
2335
2548
  taskCommand
2336
2549
  .command('update <task-id>')
2337
- .requiredOption('--as <actor-id>')
2550
+ .requiredOption('--as <actor-id>', 'actor to act as (defaults to --actor)', actingDefault)
2338
2551
  .option('--actor-kind <kind>', 'agent or human', 'agent')
2339
2552
  .requiredOption('--expected-version <number>')
2340
2553
  .option('--title <title>')
@@ -2388,7 +2601,7 @@ export function createCli(
2388
2601
  for (const operation of ['accept', 'reject', 'complete', 'reopen', 'cancel'] as const) {
2389
2602
  const command_ = taskCommand
2390
2603
  .command(`${operation} <task-id>`)
2391
- .requiredOption('--as <actor-id>')
2604
+ .requiredOption('--as <actor-id>', 'actor to act as (defaults to --actor)', actingDefault)
2392
2605
  .option('--actor-kind <kind>', 'agent or human', 'agent')
2393
2606
  .option('--note <text>')
2394
2607
  .option('--reason <text>')
@@ -2497,7 +2710,7 @@ export function createCli(
2497
2710
  kudosCommand
2498
2711
  .command('revoke <kudos-id>')
2499
2712
  .description('Record a revocation while preserving history')
2500
- .requiredOption('--as <actor-id>')
2713
+ .requiredOption('--as <actor-id>', 'actor to act as (defaults to --actor)', actingDefault)
2501
2714
  .option('--actor-kind <kind>', 'human, agent, or system', 'human')
2502
2715
  .requiredOption('--reason <reason>')
2503
2716
  .option('--administrative', 'mark as an administrative revocation', false)
@@ -2727,7 +2940,7 @@ export async function runCli(
2727
2940
  serviceFactory: SynomemServiceFactory = configuredServiceFactory,
2728
2941
  dependencies: CliDependencies = {},
2729
2942
  ): Promise<number> {
2730
- const program = createCli(io, serviceFactory, dependencies);
2943
+ const program = createCli(io, serviceFactory, dependencies, argv);
2731
2944
  program.exitOverride();
2732
2945
  try {
2733
2946
  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 } : {}),
package/src/index.ts CHANGED
@@ -41,6 +41,23 @@ export type {
41
41
  } from './service.js';
42
42
  export { defaultConfig, resolveHome } from './config.js';
43
43
  export { cloudApiUrl, SYNOMEM_CLOUD_API_URL } from './cloud.js';
44
+ export {
45
+ DEFAULT_WORKSPACE,
46
+ listLocalWorkspaces,
47
+ localWorkspaceHome,
48
+ WORKSPACES_DIRECTORY,
49
+ workspaceNameSchema,
50
+ } from './workspaces.js';
51
+ export type { LocalWorkspace } from './workspaces.js';
52
+ export {
53
+ findProjectSelection,
54
+ PROJECT_CONFIG_FILE,
55
+ PROJECT_DIRECTORY,
56
+ projectConfigSchema,
57
+ resolveWorkspaceSelection,
58
+ writeProjectSelection,
59
+ } from './project.js';
60
+ export type { ProjectConfig, ProjectSelection } from './project.js';
44
61
  export { discoverBoundWorkspace, discoverOrganizations, workspaceChoices } from './discover.js';
45
62
  export type { DiscoveredOrganization, DiscoveredWorkspace, DiscoveryOptions } from './discover.js';
46
63
  export { asSynomemError, errorCodes, SynomemError } from './errors.js';