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.
Files changed (89) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +4 -4
  3. package/dist/backend.d.ts.map +1 -1
  4. package/dist/backend.js +8 -2
  5. package/dist/backend.js.map +1 -1
  6. package/dist/cli.d.ts +3 -0
  7. package/dist/cli.d.ts.map +1 -1
  8. package/dist/cli.js +379 -16
  9. package/dist/cli.js.map +1 -1
  10. package/dist/client.d.ts +68 -2
  11. package/dist/client.d.ts.map +1 -1
  12. package/dist/client.js +283 -12
  13. package/dist/client.js.map +1 -1
  14. package/dist/cloud.d.ts +16 -0
  15. package/dist/cloud.d.ts.map +1 -0
  16. package/dist/cloud.js +19 -0
  17. package/dist/cloud.js.map +1 -0
  18. package/dist/config.d.ts.map +1 -1
  19. package/dist/config.js +8 -1
  20. package/dist/config.js.map +1 -1
  21. package/dist/configure.d.ts +55 -0
  22. package/dist/configure.d.ts.map +1 -0
  23. package/dist/configure.js +177 -0
  24. package/dist/configure.js.map +1 -0
  25. package/dist/credentials.d.ts +15 -2
  26. package/dist/credentials.d.ts.map +1 -1
  27. package/dist/credentials.js.map +1 -1
  28. package/dist/import.d.ts +202 -38
  29. package/dist/import.d.ts.map +1 -1
  30. package/dist/index.d.ts +2 -1
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +1 -0
  33. package/dist/index.js.map +1 -1
  34. package/dist/mcp/index.d.ts.map +1 -1
  35. package/dist/mcp/index.js +72 -6
  36. package/dist/mcp/index.js.map +1 -1
  37. package/dist/oauth.d.ts.map +1 -1
  38. package/dist/oauth.js +8 -1
  39. package/dist/oauth.js.map +1 -1
  40. package/dist/ports/repository.d.ts +4 -1
  41. package/dist/ports/repository.d.ts.map +1 -1
  42. package/dist/projections.d.ts +9 -1
  43. package/dist/projections.d.ts.map +1 -1
  44. package/dist/projections.js +75 -4
  45. package/dist/projections.js.map +1 -1
  46. package/dist/prompt.d.ts +28 -0
  47. package/dist/prompt.d.ts.map +1 -0
  48. package/dist/prompt.js +72 -0
  49. package/dist/prompt.js.map +1 -0
  50. package/dist/remote.d.ts +30 -1
  51. package/dist/remote.d.ts.map +1 -1
  52. package/dist/remote.js +14 -0
  53. package/dist/remote.js.map +1 -1
  54. package/dist/schemas.d.ts +270 -55
  55. package/dist/schemas.d.ts.map +1 -1
  56. package/dist/schemas.js +120 -6
  57. package/dist/schemas.js.map +1 -1
  58. package/dist/service.d.ts +30 -1
  59. package/dist/service.d.ts.map +1 -1
  60. package/dist/storage.d.ts +25 -2
  61. package/dist/storage.d.ts.map +1 -1
  62. package/dist/storage.js +168 -15
  63. package/dist/storage.js.map +1 -1
  64. package/dist/types.d.ts +123 -4
  65. package/dist/types.d.ts.map +1 -1
  66. package/docs/cli.md +1 -1
  67. package/docs/examples.md +1 -1
  68. package/docs/mcp.md +1 -1
  69. package/docs/skill.md +1 -1
  70. package/docs/storage-format.md +1 -1
  71. package/package.json +8 -8
  72. package/src/backend.ts +8 -2
  73. package/src/cli.ts +543 -19
  74. package/src/client.ts +331 -11
  75. package/src/cloud.ts +19 -0
  76. package/src/config.ts +8 -1
  77. package/src/configure.ts +233 -0
  78. package/src/credentials.ts +17 -2
  79. package/src/index.ts +7 -1
  80. package/src/mcp/index.ts +95 -5
  81. package/src/oauth.ts +8 -1
  82. package/src/ports/repository.ts +5 -0
  83. package/src/projections.ts +74 -4
  84. package/src/prompt.ts +88 -0
  85. package/src/remote.ts +72 -0
  86. package/src/schemas.ts +125 -6
  87. package/src/service.ts +30 -0
  88. package/src/storage.ts +217 -14
  89. package/src/types.ts +129 -3
package/CHANGELOG.md CHANGED
@@ -7,6 +7,55 @@ All notable changes will be documented here. The format follows
7
7
 
8
8
  ### Changed
9
9
 
10
+ - **Agents have an opaque canonical ID and a separate handle.** `agent create
11
+ <handle>` generates a ULID; the handle is what people type and can be renamed
12
+ with `agent rename` without orphaning the events written under the old name.
13
+ Events, rosters and statistics key on the canonical ID.
14
+ - The home IS the storage directory. It was `<home>/synomem`, which produced
15
+ `~/.synomem/synomem` once the home moved — a path the layout rules out.
16
+ - The default home is `~/.synomem`. There is no detection of or migration from
17
+ `~/.agents`, per the greenfield policy.
18
+ - Projection directories are named by handle, since they exist to be read.
19
+
20
+ ### Added
21
+
22
+ - **`synomem config`** — the onboarding wizard, with `configure` and `setup` as
23
+ aliases. It refuses to run without a terminal and names the deterministic
24
+ flags instead of blocking forever on a pipe.
25
+ - **`synomem config init`** — the non-interactive equivalent, including
26
+ `--access-token-stdin` so an installation key is never passed as an argument
27
+ where the shell history and process list would keep it.
28
+ - **`synomem config show`** — reports where a credential comes from, never the
29
+ credential.
30
+ - **`synomem reset`** — lists every exact target before removing anything.
31
+ Installed skills and MCP registrations are left alone unless
32
+ `--integrations` is passed.
33
+ - Synomem Cloud is implicit: `backend use remote` needs only `--workspace`.
34
+ `--url` remains for private deployments and stays out of the public docs.
35
+ - A discriminated credential model, so an installation key is no longer treated
36
+ as a refreshable OAuth credential.
37
+ - `agent rename`, `agent archive`, `agent restore`, and `agent alias add` /
38
+ `alias remove`, which add and remove aliases without replacing the set.
39
+ - The MCP server reports the canonical actor rather than the requested name, so
40
+ a misconfigured runtime cannot appear to act as somebody it is not.
41
+
42
+ ### Schema
43
+
44
+ - Version 7 adds `agents.handle` (backfilled from the ID, uniquely indexed) and
45
+ `agents.status`. Existing agents keep their name-shaped ID; only new agents
46
+ get a generated one.
47
+
48
+ ### Added
49
+
50
+ - Posts: publication to everyone in a workspace, with per-actor acknowledgements
51
+ so an author can see who has responded. A post has no recipient and no
52
+ assignee — work for one actor is a memo or a task.
53
+ - `post roster` answers who has acknowledged and who has not, counting agents
54
+ created after the post separately rather than listing them as outstanding.
55
+ - Schema version 6 adds `post_acknowledgments`.
56
+
57
+ ### Changed
58
+
10
59
  - The MCP server binds to an agent with `--agent-id`, reading the display name and actor kind from
11
60
  the agent's profile. `synomem skill install --agent <id-or-alias>` resolves the agent before
12
61
  writing anything, records a runtime binding for each runtime it installs, and generates a
package/README.md CHANGED
@@ -51,7 +51,7 @@ and opens no network listener.
51
51
  ```bash
52
52
  npm install --global synomem
53
53
 
54
- export SYNOMEM_HOME="$(mktemp -d)/.agents"
54
+ export SYNOMEM_HOME="$(mktemp -d)/.synomem"
55
55
  synomem init
56
56
  synomem agent create codex --name "Codex"
57
57
  synomem agent create gracie --name "Gracie"
@@ -93,12 +93,12 @@ terminal-capable agent. The Synomem package contains the portable
93
93
  installer for the six named local harnesses.
94
94
 
95
95
  ```text
96
- Set up Synomem for this agent and runtime. Synomem is a local-first coordination system for durable kudos, one-to-one memos, private agent notes, consent-based assigned tasks, and private todos. It uses an append-only SQLite database under ~/.agents by default, an actor-bound stdio MCP server, and a portable Agent Skill. Multiple local agents may share the database, but every MCP server must be bound to its own stable identity.
96
+ Set up Synomem for this agent and runtime. Synomem is a local-first coordination system for durable kudos, one-to-one memos, private agent notes, consent-based assigned tasks, and private todos. It uses an append-only SQLite database under ~/.synomem by default, an actor-bound stdio MCP server, and a portable Agent Skill. Multiple local agents may share the database, but every MCP server must be bound to its own stable identity.
97
97
 
98
98
  Work autonomously through the safe, reversible steps below. Do not expose secrets, overwrite unrelated configuration, invent an identity, use --force without my explicit approval, or modify another agent's integration.
99
99
 
100
100
  1. Verify Node.js 22.13+ and npm are available. Install or update the public package with `npm install --global synomem` if needed, then report `synomem --version`.
101
- 2. Preserve an existing `SYNOMEM_HOME`; otherwise use the default ~/.agents. Run `synomem init`, then `synomem doctor`. Never point tests or experiments at another Synomem home.
101
+ 2. Preserve an existing `SYNOMEM_HOME`; otherwise use the default ~/.synomem. Run `synomem init`, then `synomem doctor`. Never point tests or experiments at another Synomem home.
102
102
  3. Run `synomem agent list`. Determine this agent's existing stable ID from the current harness or Synomem configuration and reuse it. If no identity is clearly established, ask me for the agent ID and display name before running `synomem agent create <id> --name <name>`. Never silently merge or rename identities.
103
103
  4. Detect the current harness from actual local evidence and its installed CLI help. Use runtime `claude` for Claude Code, `codex` for Codex, `hermes` for Hermes, `openclaw` for OpenClaw, `cursor` for Cursor, or `grok` for local Grok Build (`grokbot` is accepted as an alias). Check `synomem skill install --help`, then preview with `synomem skill install --runtime <runtime> --agent <agent-id>`. Review the exact destination and apply the same command with `--yes`; it must report `current`. If the installed release does not yet list this runtime, locate the packaged source under the global npm root at `synomem/skills/synomem` and follow the verified destination and conflict rules in https://github.com/Coaden/synomem/blob/main/docs/skill.md instead. Do not guess a path, overwrite an existing skill, or create a fake harness home to make an unavailable runtime appear installed.
104
104
  5. Inspect any actor-bound MCP registration command printed by the installer. Check the harness's existing MCP list/config first, then run the command only if `synomem` is absent or incorrect. Do not create duplicates. Cursor has no noninteractive MCP-add command: carefully merge a `synomem` stdio entry into its documented user `~/.cursor/mcp.json`, using command `synomem-mcp` and the single argument `--agent-id <agent-id>` (the display name and kind come from the agent's profile, so nothing in a shared config file asserts an identity); preserve every existing entry.
@@ -202,7 +202,7 @@ are `claude`, `codex`, `hermes`, `openclaw`, `cursor`, and `grok`; `grokbot` ali
202
202
  ## Storage
203
203
 
204
204
  ```text
205
- ~/.agents/
205
+ ~/.synomem/
206
206
  ├── synomem/
207
207
  │ ├── config.json
208
208
  │ └── synomem.sqlite3
@@ -1 +1 @@
1
- {"version":3,"file":"backend.d.ts","sourceRoot":"","sources":["../src/backend.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAS5C,OAAO,EAAiC,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAGlF,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAC1D,OAAO,KAAK,EAEV,oBAAoB,EACpB,oBAAoB,EACpB,aAAa,EACd,MAAM,YAAY,CAAC;AAYpB,wBAAgB,iBAAiB,CAC/B,YAAY,CAAC,EAAE,MAAM,EACrB,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,aAAa,GAAG,SAAS,CAQ3B;AAED,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,oBAAoB,EAC7B,YAAY,CAAC,EAAE,MAAM,GACpB,aAAa,CAef;AAED,wBAAgB,uBAAuB,CACrC,OAAO,GAAE,oBAAyB,EAClC,GAAG,GAAE,MAAM,CAAC,UAAwB,wCAmBrC;AAED,eAAO,MAAM,wBAAwB,EAAE,qBACL,CAAC"}
1
+ {"version":3,"file":"backend.d.ts","sourceRoot":"","sources":["../src/backend.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAS5C,OAAO,EAAiC,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAGlF,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAC1D,OAAO,KAAK,EAEV,oBAAoB,EACpB,oBAAoB,EACpB,aAAa,EACd,MAAM,YAAY,CAAC;AAkBpB,wBAAgB,iBAAiB,CAC/B,YAAY,CAAC,EAAE,MAAM,EACrB,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,aAAa,GAAG,SAAS,CAQ3B;AAED,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,oBAAoB,EAC7B,YAAY,CAAC,EAAE,MAAM,GACpB,aAAa,CAef;AAED,wBAAgB,uBAAuB,CACrC,OAAO,GAAE,oBAAyB,EAClC,GAAG,GAAE,MAAM,CAAC,UAAwB,wCAmBrC;AAED,eAAO,MAAM,wBAAwB,EAAE,qBACL,CAAC"}
package/dist/backend.js CHANGED
@@ -9,9 +9,15 @@ import { environmentCredentialProvider, RemoteSynomemService } from './remote.js
9
9
  import { credentialReference, OsCredentialStore } from './credentials.js';
10
10
  import { StoredCredentialProvider } from './oauth.js';
11
11
  function configLocation(explicitHome) {
12
+ /*
13
+ * The home IS the storage directory. It used to be `<home>/synomem`, which
14
+ * made sense while the default home was `~/.agents` and Synomem was one
15
+ * tenant inside it. Now that the home is `~/.synomem`, that nesting produces
16
+ * `~/.synomem/synomem` — a path the layout explicitly rules out, and one that
17
+ * makes every documented path wrong by a level.
18
+ */
12
19
  const home = resolveHome(explicitHome);
13
- const storageDirectory = join(home, 'synomem');
14
- return { home, storageDirectory, configPath: join(storageDirectory, 'config.json') };
20
+ return { home, storageDirectory: home, configPath: join(home, 'config.json') };
15
21
  }
16
22
  export function readSynomemConfig(explicitHome, env = process.env) {
17
23
  const { home, storageDirectory, configPath } = configLocation(explicitHome);
@@ -1 +1 @@
1
- {"version":3,"file":"backend.js","sourceRoot":"","sources":["../src/backend.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"backend.js","sourceRoot":"","sources":["../src/backend.ts"],"names":[],"mappings":"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"}
package/dist/cli.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from 'commander';
3
+ import { type PromptIo } from './prompt.js';
3
4
  import { type CredentialStore } from './credentials.js';
4
5
  import { type OAuthLoginOptions } from './oauth.js';
5
6
  import { type ImportBundle, type ImportPreview, type ImportResult } from './import.js';
@@ -10,6 +11,8 @@ export interface CliIo {
10
11
  stderr: (text: string) => void;
11
12
  }
12
13
  export interface CliDependencies {
14
+ /** Injected so the wizard can be driven by a test without a terminal. */
15
+ promptIo?: PromptIo;
13
16
  credentialStore?: CredentialStore;
14
17
  oauthLogin?: (options: OAuthLoginOptions) => Promise<void>;
15
18
  env?: NodeJS.ProcessEnv;
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAKA,OAAO,EAAE,OAAO,EAA0B,MAAM,WAAW,CAAC;AAE5D,OAAO,EAA0C,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAIhG,OAAO,EAA4C,KAAK,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAE9F,OAAO,EAGL,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,KAAK,YAAY,EAClB,MAAM,aAAa,CAAC;AASrB,OAAO,KAAK,EACV,aAAa,EAQd,MAAM,YAAY,CAAC;AAEpB,OAAO,KAAK,EAAkB,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAE1E,MAAM,WAAW,KAAK;IACpB,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CAChC;AAED,MAAM,WAAW,eAAe;IAC9B,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,iBAAiB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3D,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,sBAAsB,CAAC,EAAE,CAAC,OAAO,EAAE;QACjC,OAAO,EAAE,MAAM,CAAC;QAChB,WAAW,EAAE,MAAM,CAAC;QACpB,KAAK,EAAE,aAAa,CAAC;QACrB,SAAS,EAAE,MAAM,CAAC;QAClB,eAAe,EAAE,eAAe,CAAC;KAClC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACpB,kBAAkB,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,YAAY,CAAC,CAAC;IAC7D,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE;QACvB,OAAO,EAAE,MAAM,CAAC;QAChB,WAAW,EAAE,MAAM,CAAC;QACpB,KAAK,EAAE,aAAa,CAAC;QACrB,MAAM,EAAE,YAAY,CAAC;QACrB,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,KAAK,OAAO,CAAC,aAAa,GAAG,YAAY,CAAC,CAAC;CAC7C;AAkND,wBAAgB,SAAS,CACvB,EAAE,GAAE,KAAiB,EACrB,cAAc,GAAE,qBAAgD,EAChE,YAAY,GAAE,eAAoB,GACjC,OAAO,CA8qDT;AAED,wBAAsB,MAAM,CAC1B,IAAI,WAAe,EACnB,EAAE,GAAE,KAAiB,EACrB,cAAc,GAAE,qBAAgD,EAChE,YAAY,GAAE,eAAoB,GACjC,OAAO,CAAC,MAAM,CAAC,CAqBjB"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAKA,OAAO,EAAE,OAAO,EAA0B,MAAM,WAAW,CAAC;AAkB5D,OAAO,EAAmB,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC7D,OAAO,EAA0C,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAIhG,OAAO,EAA4C,KAAK,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAE9F,OAAO,EAGL,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,KAAK,YAAY,EAClB,MAAM,aAAa,CAAC;AASrB,OAAO,KAAK,EACV,aAAa,EAQd,MAAM,YAAY,CAAC;AAEpB,OAAO,KAAK,EAAkB,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAE1E,MAAM,WAAW,KAAK;IACpB,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CAChC;AAED,MAAM,WAAW,eAAe;IAC9B,yEAAyE;IACzE,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,iBAAiB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3D,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,sBAAsB,CAAC,EAAE,CAAC,OAAO,EAAE;QACjC,OAAO,EAAE,MAAM,CAAC;QAChB,WAAW,EAAE,MAAM,CAAC;QACpB,KAAK,EAAE,aAAa,CAAC;QACrB,SAAS,EAAE,MAAM,CAAC;QAClB,eAAe,EAAE,eAAe,CAAC;KAClC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACpB,kBAAkB,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,YAAY,CAAC,CAAC;IAC7D,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE;QACvB,OAAO,EAAE,MAAM,CAAC;QAChB,WAAW,EAAE,MAAM,CAAC;QACpB,KAAK,EAAE,aAAa,CAAC;QACrB,MAAM,EAAE,YAAY,CAAC;QACrB,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,KAAK,OAAO,CAAC,aAAa,GAAG,YAAY,CAAC,CAAC;CAC7C;AAkND,wBAAgB,SAAS,CACvB,EAAE,GAAE,KAAiB,EACrB,cAAc,GAAE,qBAAgD,EAChE,YAAY,GAAE,eAAoB,GACjC,OAAO,CAuqET;AAED,wBAAsB,MAAM,CAC1B,IAAI,WAAe,EACnB,EAAE,GAAE,KAAiB,EACrB,cAAc,GAAE,qBAAgD,EAChE,YAAY,GAAE,eAAoB,GACjC,OAAO,CAAC,MAAM,CAAC,CAqBjB"}
package/dist/cli.js CHANGED
@@ -1,10 +1,14 @@
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 { assertInteractive, confirmPlan, credentialFingerprint, credentialStoreChoices, environmentInstructions, readAccessToken, runConfigWizard, writeCredentialFile, } from './configure.js';
11
+ import { defaultPromptIo } from './prompt.js';
8
12
  import { credentialReference, OsCredentialStore } from './credentials.js';
9
13
  import { asSynomemError, SynomemError } from './errors.js';
10
14
  import { atomicWriteFile } from './fs-utils.js';
@@ -194,6 +198,7 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
194
198
  const env = dependencies.env ?? process.env;
195
199
  const credentialStore = dependencies.credentialStore ?? new OsCredentialStore();
196
200
  const oauthLogin = dependencies.oauthLogin ?? loginWithOAuth;
201
+ const promptIo = dependencies.promptIo ?? defaultPromptIo();
197
202
  const verifyRemoteCredential = dependencies.verifyRemoteCredential ??
198
203
  (async (options) => {
199
204
  const remote = new RemoteSynomemService({
@@ -216,7 +221,7 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
216
221
  .name('synomem')
217
222
  .description('Local-first communication, memory, recognition, and task infrastructure for agents')
218
223
  .version(packageVersion())
219
- .option('--home <path>', 'storage root (defaults to SYNOMEM_HOME or ~/.agents)')
224
+ .option('--home <path>', 'storage root (defaults to SYNOMEM_HOME or ~/.synomem)')
220
225
  .option('--json', 'emit stable machine-readable JSON', false)
221
226
  .showSuggestionAfterError()
222
227
  .configureOutput({ writeOut: io.stdout, writeErr: io.stderr });
@@ -292,6 +297,208 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
292
297
  output(io, options.json, { home: info.home, database: info.databasePath }, `Initialized Synomem at ${info.home}`);
293
298
  });
294
299
  });
300
+ /*
301
+ * `synomem config` is the canonical entry point. `configure` and `setup` are
302
+ * accepted because people reach for them, and a setup program that rejects
303
+ * the word somebody guessed is needlessly unhelpful.
304
+ */
305
+ const configCommand = program
306
+ .command('config')
307
+ .aliases(['configure', 'setup'])
308
+ .description('Set up Synomem, interactively or deterministically');
309
+ const applyPlan = async (plan, token, global) => {
310
+ const home = plan.home;
311
+ const config = writeSynomemBackend(plan.backend === 'local'
312
+ ? { kind: 'local' }
313
+ : {
314
+ kind: 'remote',
315
+ baseUrl: plan.serviceUrl ?? cloudApiUrl(env),
316
+ workspaceId: plan.workspaceId,
317
+ }, home);
318
+ let credentialLocation;
319
+ if (token) {
320
+ if (plan.credentialStore === 'environment') {
321
+ io.stdout(`${environmentInstructions(token)}\n`);
322
+ credentialLocation = 'environment';
323
+ }
324
+ else if (plan.credentialStore === 'file') {
325
+ credentialLocation = writeCredentialFile(home, token);
326
+ }
327
+ else {
328
+ // The platform store is the default, and a failure falls back to the
329
+ // restricted file rather than leaving the credential nowhere.
330
+ try {
331
+ await credentialStore.set(`synomem:${plan.workspaceId}`, {
332
+ kind: 'installation-key',
333
+ accessToken: token,
334
+ });
335
+ credentialLocation = 'platform credential store';
336
+ }
337
+ catch {
338
+ credentialLocation = writeCredentialFile(home, token);
339
+ }
340
+ }
341
+ }
342
+ // Diagnostics run before success is claimed: a configuration that cannot
343
+ // open its own database is not a finished setup.
344
+ const diagnostics = await withClient(home, defaultActor(env, 'system', 'cli'), (client) => client.doctor());
345
+ output(io, global.json, {
346
+ backend: config.backend,
347
+ home,
348
+ ...(credentialLocation ? { credentialSource: credentialLocation } : {}),
349
+ ...(token ? { credential: credentialFingerprint(token) } : {}),
350
+ healthy: diagnostics.healthy,
351
+ }, [
352
+ '',
353
+ 'Synomem is ready.',
354
+ '',
355
+ ` Backend: ${config.backend.kind === 'local' ? 'Local SQLite' : 'Synomem Cloud'}`,
356
+ ` Home: ${home}`,
357
+ ...(config.backend.kind === 'remote'
358
+ ? [` Service: ${config.backend.baseUrl}`, ` Workspace: ${config.backend.workspaceId}`]
359
+ : []),
360
+ ...(credentialLocation ? [` Credential: ${credentialLocation}`] : []),
361
+ ` Database: ${diagnostics.healthy ? 'Healthy' : 'Needs attention — run synomem doctor'}`,
362
+ ].join('\n'));
363
+ };
364
+ configCommand.action(async (_options, command) => {
365
+ const global = globals(command);
366
+ assertInteractive(promptIo);
367
+ const plan = await runConfigWizard(promptIo, {
368
+ ...(global.home ? { home: global.home } : {}),
369
+ env,
370
+ });
371
+ const token = plan.auth === 'access-key' ? await readAccessToken(promptIo) : undefined;
372
+ if (!(await confirmPlan(promptIo, plan))) {
373
+ output(io, global.json, { applied: false }, 'Nothing was changed.');
374
+ return;
375
+ }
376
+ await applyPlan(plan, token, global);
377
+ });
378
+ configCommand
379
+ .command('init')
380
+ .description('Configure Synomem without prompting')
381
+ .option('--backend <kind>', 'local or remote')
382
+ .option('--auth <method>', 'browser or access-key')
383
+ .option('--workspace <id>', 'remote workspace ID')
384
+ .option('--credential-store <where>', 'auto, keychain, file, or environment', 'auto')
385
+ // The token is read from stdin, never taken as an argument: an argument is
386
+ // kept by the shell history and visible in the process list.
387
+ .option('--access-token-stdin', 'read the installation access key from stdin', false)
388
+ .option('--yes', 'apply without confirming', false)
389
+ .action(async (options, command) => {
390
+ const global = globals(command);
391
+ if (options.backend !== 'local' && options.backend !== 'remote') {
392
+ throw new SynomemError('INVALID_INPUT', 'Pass --backend local or --backend remote.');
393
+ }
394
+ const backend = options.backend;
395
+ if (backend === 'remote' && !options.workspace) {
396
+ throw new SynomemError('INVALID_INPUT', 'Remote setup requires --workspace.');
397
+ }
398
+ const token = options.accessTokenStdin ? await readAccessToken(promptIo) : undefined;
399
+ if (backend === 'remote' && options.auth === 'access-key' && !token) {
400
+ throw new SynomemError('INVALID_INPUT', 'Access-key setup requires --access-token-stdin so the key is not passed as an argument.');
401
+ }
402
+ const plan = {
403
+ backend,
404
+ home: resolveHome(global.home),
405
+ ...(backend === 'remote'
406
+ ? {
407
+ serviceUrl: cloudApiUrl(env),
408
+ auth: options.auth ?? 'access-key',
409
+ workspaceId: options.workspace,
410
+ credentialStore: options.credentialStore,
411
+ }
412
+ : {}),
413
+ };
414
+ if (!options.yes) {
415
+ throw new SynomemError('INVALID_INPUT', 'Re-run with --yes to apply this configuration.');
416
+ }
417
+ await applyPlan(plan, token, global);
418
+ });
419
+ configCommand
420
+ .command('show')
421
+ .description('Show the current configuration without revealing secrets')
422
+ .action(async (_options, command) => {
423
+ const global = globals(command);
424
+ const home = resolveHome(global.home);
425
+ const config = readSynomemConfig(global.home, env);
426
+ const backend = config?.backend ?? { kind: 'local' };
427
+ const credentialSource = env.SYNOMEM_ACCESS_TOKEN
428
+ ? 'environment (SYNOMEM_ACCESS_TOKEN)'
429
+ : existsSync(join(home, 'credentials', 'installation.json'))
430
+ ? 'restricted file'
431
+ : 'platform credential store or none';
432
+ output(io, global.json,
433
+ // Never the secret itself, only where it comes from.
434
+ { backend, home, credentialSource, stores: credentialStoreChoices().map((c) => c.value) }, [
435
+ `Backend: ${backend.kind === 'local' ? 'Local SQLite' : 'Synomem Cloud'}`,
436
+ `Home: ${home}`,
437
+ ...(backend.kind === 'remote'
438
+ ? [`Service: ${backend.baseUrl}`, `Workspace: ${backend.workspaceId}`]
439
+ : []),
440
+ `Credential: ${credentialSource}`,
441
+ ].join('\n'));
442
+ });
443
+ program
444
+ .command('reset')
445
+ .description('Remove Synomem configuration, database and credentials')
446
+ // Integrations are opt-in because they live in other tools' directories.
447
+ // Removing somebody's harness configuration as a side effect of resetting
448
+ // Synomem would be a surprise with no undo.
449
+ .option('--integrations', 'also remove installed skills and MCP registrations', false)
450
+ .option('--yes', 'apply the displayed plan', false)
451
+ .action(async (options, command) => {
452
+ const global = globals(command);
453
+ const home = resolveHome(global.home);
454
+ /*
455
+ * Every target is an exact path, listed before anything is touched. No
456
+ * recursive delete is ever derived from a variable that might be empty:
457
+ * a reset that computes `rm -rf $HOME/` from an unset home is the
458
+ * failure this shape exists to make impossible.
459
+ */
460
+ const targets = [
461
+ join(home, 'config.json'),
462
+ join(home, 'synomem.sqlite3'),
463
+ join(home, 'synomem.sqlite3-wal'),
464
+ join(home, 'synomem.sqlite3-shm'),
465
+ join(home, 'credentials', 'installation.json'),
466
+ ].filter((path) => existsSync(path));
467
+ const skillPlan = options.integrations ? uninstallSkill({ apply: false }) : undefined;
468
+ const skillTargets = skillPlan?.locations
469
+ // Installed Synomem-owned copies only; an unowned directory at
470
+ // the same path is not ours to remove.
471
+ .filter((location) => location.state === 'current' || location.state === 'stale')
472
+ .map((location) => location.target) ?? [];
473
+ if (!options.yes) {
474
+ output(io, global.json, { targets, skillTargets, applied: false }, [
475
+ 'This will remove:',
476
+ ...(targets.length ? targets.map((path) => ` ${path}`) : [' (nothing found)']),
477
+ ...(skillTargets.length ? ['', 'And these Synomem-owned skills:'] : []),
478
+ ...skillTargets.map((path) => ` ${path}`),
479
+ '',
480
+ ...(options.integrations
481
+ ? []
482
+ : ['Installed skills and MCP registrations are left alone.', '']),
483
+ 'Run with --yes to continue.',
484
+ ].join('\n'));
485
+ return;
486
+ }
487
+ const removed = [];
488
+ for (const path of targets) {
489
+ rmSync(path, { force: true });
490
+ removed.push(path);
491
+ }
492
+ // Only ownership-stamped Synomem skills are removed, which uninstall
493
+ // already enforces — an unowned directory at the same path is left.
494
+ const skillResult = options.integrations ? uninstallSkill({ apply: true }) : undefined;
495
+ output(io, global.json, { removed, skills: skillResult?.locations ?? [] }, [
496
+ `Removed ${removed.length} file(s).`,
497
+ ...(skillResult
498
+ ? [`Skill locations processed: ${skillResult.locations.length}.`]
499
+ : ['Installed skills and MCP registrations were left alone.']),
500
+ ].join('\n'));
501
+ });
295
502
  const backendCommand = program
296
503
  .command('backend')
297
504
  .description('Inspect or select the canonical backend');
@@ -311,19 +518,27 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
311
518
  .command('use')
312
519
  .description('Select local or remote canonical state')
313
520
  .argument('<kind>', 'local or remote')
314
- .option('--url <url>', 'remote HTTPS origin')
521
+ // --url is for development and private deployments. It stays out of the
522
+ // README, the public docs and the packaged skill: public onboarding must
523
+ // never ask for a service address, because a person has no way to tell a
524
+ // real one from a phished one.
525
+ .option('--url <url>', 'internal: alternate HTTPS origin')
315
526
  .option('--workspace <id>', 'remote workspace ID')
316
527
  .action((kind, options, command) => {
317
528
  const global = globals(command);
318
529
  if (kind !== 'local' && kind !== 'remote') {
319
530
  throw new SynomemError('INVALID_INPUT', 'Backend kind must be local or remote.');
320
531
  }
321
- if (kind === 'remote' && (!options.url || !options.workspace)) {
322
- throw new SynomemError('INVALID_INPUT', 'Remote backend selection requires --url and --workspace.');
532
+ if (kind === 'remote' && !options.workspace) {
533
+ throw new SynomemError('INVALID_INPUT', 'Remote backend selection requires --workspace.');
323
534
  }
324
535
  const config = writeSynomemBackend(kind === 'local'
325
536
  ? { kind: 'local' }
326
- : { kind: 'remote', baseUrl: options.url, workspaceId: options.workspace }, global.home);
537
+ : {
538
+ kind: 'remote',
539
+ baseUrl: options.url ?? cloudApiUrl(env),
540
+ workspaceId: options.workspace,
541
+ }, global.home);
327
542
  output(io, global.json, { backend: config.backend }, `Selected ${config.backend.kind} Synomem backend.`);
328
543
  });
329
544
  const authCommand = program.command('auth').description('Inspect remote authentication');
@@ -419,20 +634,65 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
419
634
  .command('agent')
420
635
  .description('Create and inspect stable agent identities');
421
636
  agentCommand
422
- .command('create <id>')
423
- .description('Create a stable agent profile')
637
+ .command('create <handle>')
638
+ .description('Create an agent. The canonical ID is generated, not chosen.')
424
639
  .requiredOption('--name <display-name>', 'display name')
425
- .option('--alias <id>', 'alias (repeatable)', collect, [])
640
+ .option('--alias <name>', 'alias (repeatable)', collect, [])
426
641
  .option('--description <text>')
427
- .action(async (id, options, command) => {
642
+ .action(async (handle, options, command) => {
428
643
  const global = globals(command);
429
644
  const profile = await withClient(global.home, defaultActor(env, 'system', 'cli'), (client) => client.agents.create({
430
- id,
645
+ handle,
431
646
  displayName: options.name,
432
647
  ...(options.alias.length ? { aliases: options.alias } : {}),
433
648
  ...(options.description ? { description: options.description } : {}),
434
649
  }));
435
- output(io, global.json, profile, `Created ${profile.displayName} (${profile.id})`);
650
+ // Both are printed because both matter: the handle is what people type,
651
+ // the ID is what every event records and what MCP registration uses.
652
+ output(io, global.json, profile, `Created ${profile.displayName}\n\nHandle: ${profile.handle}\nAgent ID: ${profile.id}`);
653
+ });
654
+ const aliasCommand = agentCommand
655
+ .command('alias')
656
+ .description('Add or remove discovery aliases without replacing the set');
657
+ aliasCommand
658
+ .command('add <agent> <alias...>')
659
+ .description('Add aliases, keeping the ones already there')
660
+ .action(async (agent, aliases, _options, command) => {
661
+ const global = globals(command);
662
+ const profile = await withClient(global.home, defaultActor(env, 'system', 'cli'), (client) => client.agents.addAliases(agent, aliases));
663
+ output(io, global.json, profile, `Aliases: ${(profile.aliases ?? []).join(', ') || 'none'}`);
664
+ });
665
+ aliasCommand
666
+ .command('remove <agent> <alias...>')
667
+ .description('Remove aliases, keeping the rest')
668
+ .action(async (agent, aliases, _options, command) => {
669
+ const global = globals(command);
670
+ const profile = await withClient(global.home, defaultActor(env, 'system', 'cli'), (client) => client.agents.removeAliases(agent, aliases));
671
+ output(io, global.json, profile, `Aliases: ${(profile.aliases ?? []).join(', ') || 'none'}`);
672
+ });
673
+ agentCommand
674
+ .command('rename <agent> <handle>')
675
+ .description('Change an agent handle. Its canonical ID never changes.')
676
+ .action(async (agent, handle, _options, command) => {
677
+ const global = globals(command);
678
+ const profile = await withClient(global.home, defaultActor(env, 'system', 'cli'), (client) => client.agents.update(agent, { handle }));
679
+ output(io, global.json, profile, `Handle: ${profile.handle}\nAgent ID: ${profile.id} (unchanged)`);
680
+ });
681
+ agentCommand
682
+ .command('archive <agent>')
683
+ .description('Stop an agent acting, keeping its records and history')
684
+ .action(async (agent, _options, command) => {
685
+ const global = globals(command);
686
+ const profile = await withClient(global.home, defaultActor(env, 'system', 'cli'), (client) => client.agents.archive(agent));
687
+ output(io, global.json, profile, `Archived ${profile.handle} (${profile.id})`);
688
+ });
689
+ agentCommand
690
+ .command('restore <agent>')
691
+ .description('Let an archived agent act again')
692
+ .action(async (agent, _options, command) => {
693
+ const global = globals(command);
694
+ const profile = await withClient(global.home, defaultActor(env, 'system', 'cli'), (client) => client.agents.restore(agent));
695
+ output(io, global.json, profile, `Restored ${profile.handle} (${profile.id})`);
436
696
  });
437
697
  const skillCommand = program
438
698
  .command('skill')
@@ -528,7 +788,10 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
528
788
  const agents = await withClient(global.home, defaultActor(env, 'system', 'cli'), (client) => client.agents.list());
529
789
  const human = agents.length
530
790
  ? agents
531
- .map((profile) => `${profile.id} ${profile.displayName}${profile.aliases?.length ? ` aliases: ${profile.aliases.join(', ')}` : ''}`)
791
+ .map((profile) =>
792
+ // Handle first: it is what people type. The canonical ID
793
+ // follows because MCP registration needs it.
794
+ `${profile.handle} ${profile.displayName}${profile.status === 'archived' ? ' [archived]' : ''}${profile.aliases?.length ? ` aliases: ${profile.aliases.join(', ')}` : ''}\n ${profile.id}`)
532
795
  .join('\n')
533
796
  : 'No agents configured.';
534
797
  output(io, global.json, { agents }, human);
@@ -539,7 +802,7 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
539
802
  .action(async (id, _options, command) => {
540
803
  const global = globals(command);
541
804
  const profile = await withClient(global.home, defaultActor(env, 'system', 'cli'), (client) => client.agents.get(id));
542
- output(io, global.json, profile, `${profile.displayName} (${profile.id})\n${profile.description ?? 'No description.'}`);
805
+ output(io, global.json, profile, `${profile.displayName}\n\nHandle: ${profile.handle}\nAgent ID: ${profile.id}\nStatus: ${profile.status}\n\n${profile.description ?? 'No description.'}`);
543
806
  });
544
807
  agentCommand
545
808
  .command('update <id>')
@@ -636,6 +899,105 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
636
899
  const removed = await withClient(global.home, defaultActor(env, 'system', 'cli'), (client) => client.agents.unbindRuntime(bindingId));
637
900
  output(io, global.json, { removed }, removed ? `Removed binding ${bindingId}.` : `No binding ${bindingId}.`);
638
901
  });
902
+ const postCommand = program.command('post').description('Publish to everyone in the workspace');
903
+ postCommand
904
+ .command('create')
905
+ .description('Publish a post the whole workspace can read')
906
+ .requiredOption('--as <actor-id>')
907
+ .option('--actor-kind <kind>', 'human, agent, or system', 'agent')
908
+ .requiredOption('--title <title>')
909
+ .requiredOption('--body <body>')
910
+ .option('--tag <tag>', 'repeatable', collect, [])
911
+ .option('--reply-to <post-id>')
912
+ .action(async (options, command) => {
913
+ const global = globals(command);
914
+ const result = await withClient(global.home, actor(options.actorKind, options.as), (client) => client.posts.create({
915
+ title: options.title,
916
+ body: options.body,
917
+ ...(options.tag.length ? { tags: options.tag } : {}),
918
+ ...(options.replyTo ? { replyTo: options.replyTo } : {}),
919
+ }));
920
+ output(io, global.json, result, `Published ${result.record.event.id}`);
921
+ });
922
+ postCommand
923
+ .command('list')
924
+ .description('List posts in this workspace')
925
+ .requiredOption('--as <actor-id>')
926
+ .option('--actor-kind <kind>', 'human, agent, or system', 'agent')
927
+ .option('--limit <n>', 'default 10, maximum 50')
928
+ .action(async (options, command) => {
929
+ const global = globals(command);
930
+ const page = await withClient(global.home, actor(options.actorKind, options.as), (client) => client.posts.list(options.limit ? { limit: Number(options.limit) } : {}));
931
+ const human = page.items.length
932
+ ? page.items.map((item) => `${item.id} ${item.title}`).join('\n')
933
+ : 'No posts yet.';
934
+ output(io, global.json, page, human);
935
+ });
936
+ postCommand
937
+ .command('show <post-id>')
938
+ .description('Show one post with its acknowledgements')
939
+ .requiredOption('--as <actor-id>')
940
+ .option('--actor-kind <kind>', 'human, agent, or system', 'agent')
941
+ .action(async (postId, options, command) => {
942
+ const global = globals(command);
943
+ const record = await withClient(global.home, actor(options.actorKind, options.as), (client) => client.posts.get(postId));
944
+ const acks = record.acknowledgments.length
945
+ ? record.acknowledgments
946
+ .map((entry) => ` ${entry.actor.id}${entry.note ? ` — ${entry.note}` : ''}`)
947
+ .join('\n')
948
+ : ' none yet';
949
+ output(io, global.json, record, `${record.title}\n\n${record.body}\n\nAcknowledged by:\n${acks}`);
950
+ });
951
+ postCommand
952
+ .command('acknowledge <post-id>')
953
+ .description('Say you have seen a post')
954
+ .requiredOption('--as <actor-id>')
955
+ .option('--actor-kind <kind>', 'human, agent, or system', 'agent')
956
+ .option('--note <text>', 'optional context for the author')
957
+ .action(async (postId, options, command) => {
958
+ const global = globals(command);
959
+ const record = await withClient(global.home, actor(options.actorKind, options.as), (client) => client.posts.acknowledge({
960
+ postId,
961
+ ...(options.note ? { note: options.note } : {}),
962
+ }));
963
+ output(io, global.json, record, `Acknowledged ${postId}`);
964
+ });
965
+ postCommand
966
+ .command('roster <post-id>')
967
+ .description('Who has acknowledged a post, and who has not')
968
+ .requiredOption('--as <actor-id>')
969
+ .option('--actor-kind <kind>', 'human, agent, or system', 'agent')
970
+ .action(async (postId, options, command) => {
971
+ const global = globals(command);
972
+ const roster = await withClient(global.home, actor(options.actorKind, options.as), (client) => client.posts.roster(postId));
973
+ // "Outstanding" means no acknowledgement recorded — never that somebody
974
+ // has not read it, which this cannot know.
975
+ const lines = [
976
+ `Acknowledged (${roster.acknowledged.length}):`,
977
+ ...(roster.acknowledged.length
978
+ ? roster.acknowledged.map((entry) => ` ${entry.actor.id}`)
979
+ : [' none yet']),
980
+ `No acknowledgement recorded (${roster.outstanding.length}):`,
981
+ ...(roster.outstanding.length
982
+ ? roster.outstanding.map((entry) => ` ${entry.id}`)
983
+ : [' none']),
984
+ ];
985
+ if (roster.joinedSince > 0) {
986
+ lines.push(`${roster.joinedSince} agent(s) joined after this was posted.`);
987
+ }
988
+ output(io, global.json, roster, lines.join('\n'));
989
+ });
990
+ postCommand
991
+ .command('archive <post-id>')
992
+ .description('Archive a post you wrote')
993
+ .requiredOption('--as <actor-id>')
994
+ .option('--actor-kind <kind>', 'human, agent, or system', 'agent')
995
+ .option('--reason <text>')
996
+ .action(async (postId, options, command) => {
997
+ const global = globals(command);
998
+ const record = await withClient(global.home, actor(options.actorKind, options.as), (client) => client.posts.archive({ postId, ...(options.reason ? { reason: options.reason } : {}) }));
999
+ output(io, global.json, record, `Archived ${postId}`);
1000
+ });
639
1001
  const kudosCommand = program.command('kudos').description('Give and manage agent recognition');
640
1002
  kudosCommand
641
1003
  .command('give <recipient>')
@@ -1141,12 +1503,13 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
1141
1503
  throw new SynomemError('INVALID_INPUT', 'Generated WINS.md files are available only with the local backend.');
1142
1504
  }
1143
1505
  const capabilities = await client.capabilities();
1144
- const path = join(info.home, profile.id, 'WINS.md');
1506
+ // Projections are written under the handle, since they exist to be read.
1507
+ const path = join(info.home, profile.handle, 'WINS.md');
1145
1508
  if (!existsSync(path)) {
1146
1509
  const hint = capabilities.projections.writeWinsMarkdown
1147
1510
  ? 'Run `synomem rebuild` to generate it.'
1148
1511
  : 'Enable projection.writeWinsMarkdown and run `synomem rebuild`.';
1149
- throw new SynomemError('INVALID_INPUT', `No generated WINS.md exists for ${profile.id}. ${hint}`);
1512
+ throw new SynomemError('INVALID_INPUT', `No generated WINS.md exists for ${profile.handle}. ${hint}`);
1150
1513
  }
1151
1514
  return { profile, path, content: readFileSync(path, 'utf8') };
1152
1515
  });