synomem 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +62 -1
- package/README.md +55 -13
- package/dist/cli.d.ts +6 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +179 -16
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts +2 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +37 -1
- package/dist/client.js.map +1 -1
- package/dist/configure.d.ts.map +1 -1
- package/dist/configure.js +34 -15
- package/dist/configure.js.map +1 -1
- package/dist/credentials.d.ts.map +1 -1
- package/dist/credentials.js +9 -1
- package/dist/credentials.js.map +1 -1
- package/dist/discover.d.ts +48 -0
- package/dist/discover.d.ts.map +1 -0
- package/dist/discover.js +106 -0
- package/dist/discover.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/projections.d.ts.map +1 -1
- package/dist/projections.js +15 -7
- package/dist/projections.js.map +1 -1
- package/dist/service.d.ts +2 -1
- package/dist/service.d.ts.map +1 -1
- package/dist/storage.d.ts +5 -0
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +6 -0
- package/dist/storage.js.map +1 -1
- package/dist/types.d.ts +26 -0
- package/dist/types.d.ts.map +1 -1
- package/docs/cli.md +67 -3
- package/package.json +3 -3
- package/src/cli.ts +228 -20
- package/src/client.ts +41 -1
- package/src/configure.ts +33 -14
- package/src/credentials.ts +9 -1
- package/src/discover.ts +155 -0
- package/src/index.ts +2 -0
- package/src/projections.ts +17 -7
- package/src/service.ts +8 -0
- package/src/storage.ts +9 -0
- package/src/types.ts +21 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,7 +3,68 @@
|
|
|
3
3
|
All notable changes will be documented here. The format follows
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and releases follow Semantic Versioning.
|
|
5
5
|
|
|
6
|
-
## [
|
|
6
|
+
## [0.5.1] - 2026-09-07
|
|
7
|
+
|
|
8
|
+
### Documentation
|
|
9
|
+
|
|
10
|
+
- The README and CLI reference were rewritten for the two backends. They still
|
|
11
|
+
described Synomem as running only on one machine, listed four record kinds
|
|
12
|
+
when there are six, and pointed at `synomem init` rather than `synomem
|
|
13
|
+
config`. The pasteable agent-setup prompt was stale in the same ways.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- CI could not install the project at all. A dependency bump moved TypeScript to
|
|
18
|
+
7, which is outside `typescript-eslint`'s peer range, so every `npm ci` failed
|
|
19
|
+
on resolution while local runs kept working against an older `node_modules`.
|
|
20
|
+
TypeScript is pinned to 6 until `typescript-eslint` supports 7.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- Commander 15.
|
|
25
|
+
|
|
26
|
+
## [0.5.0] - 2026-09-07
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- **`synomem backend status`** — connects to the selected backend and reports
|
|
31
|
+
what answered. `backend show` still reads the configuration file and connects
|
|
32
|
+
to nothing; a person debugging a broken setup needs that, and a person
|
|
33
|
+
confirming a working one needs a connection to have been made.
|
|
34
|
+
- **`synomem projection status`** — whether the generated files match the
|
|
35
|
+
canonical events, when they were last rebuilt, and which ones drifted. The
|
|
36
|
+
comparison is against Synomem's own manifest, so a file somebody added to the
|
|
37
|
+
projection tree by hand is not reported as drift.
|
|
38
|
+
- **`synomem remote workspaces`** — the organizations and workspaces a
|
|
39
|
+
credential can reach, with the IDs `backend use remote --workspace` takes.
|
|
40
|
+
- `agent runtime list` with no agent named lists every agent that runs
|
|
41
|
+
anywhere. Requiring the agent meant already knowing the answer to the
|
|
42
|
+
question being asked.
|
|
43
|
+
|
|
44
|
+
### Changed
|
|
45
|
+
|
|
46
|
+
- **Remote setup no longer asks for a workspace ID.** An installation access
|
|
47
|
+
key is bound to exactly one workspace, so the service is asked which one
|
|
48
|
+
rather than the person; `config init --backend remote --access-token-stdin`
|
|
49
|
+
needs no `--workspace`. Typing `ws-04psqx2rkt8ttft7a1t2z69r97` from memory
|
|
50
|
+
was never something a person could do.
|
|
51
|
+
- The Windows credential-store choice is the restricted file rather than
|
|
52
|
+
Credential Manager, which the credential layer does not implement. Offering a
|
|
53
|
+
store that cannot read its own credential back fails on first use, after
|
|
54
|
+
setup has already claimed the credential was safe.
|
|
55
|
+
|
|
56
|
+
### Fixed
|
|
57
|
+
|
|
58
|
+
- `doctor` reported every workspace's projections as stale once any agent had a
|
|
59
|
+
generated ID: the expected-path list was built from canonical IDs while the
|
|
60
|
+
files, the manifest and the cleanup all used handles.
|
|
61
|
+
- `doctor`'s agent-directory symbolic-link check inspected a path built from the
|
|
62
|
+
canonical ID, so it examined a directory that does not exist and passed on a
|
|
63
|
+
workspace whose agent directory really had been replaced with a link.
|
|
64
|
+
|
|
65
|
+
## [0.4.0] - 2026-09-07
|
|
66
|
+
|
|
67
|
+
Published as 0.3.0 and 0.4.0 on the same day; the entries below cover both.
|
|
7
68
|
|
|
8
69
|
### Changed
|
|
9
70
|
|
package/README.md
CHANGED
|
@@ -14,20 +14,24 @@
|
|
|
14
14
|
|
|
15
15
|
[CLI reference](docs/cli.md) · [MCP guide](docs/mcp.md) · [Storage](docs/storage-format.md) · [Security](SECURITY.md)
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
Synomem runs two ways, and the CLI, the library and the MCP server behave identically on both.
|
|
18
|
+
**Local** keeps an append-only SQLite database on this machine, needs no account, and opens no
|
|
19
|
+
network listener. **Synomem Cloud** keeps canonical state in a hosted workspace shared across
|
|
20
|
+
machines and agents, with organizations, roles and administration. `synomem config` sets up either
|
|
21
|
+
one. Nothing is synchronized between them and choosing the hosted backend never creates a shadow
|
|
22
|
+
local database. See the [CLI reference](docs/cli.md#backend-and-authentication).
|
|
22
23
|
|
|
23
24
|
</div>
|
|
24
25
|
|
|
25
|
-
Synomem gives humans and AI agents
|
|
26
|
+
Synomem gives humans and AI agents durable ways to coordinate beyond a disappearing chat:
|
|
26
27
|
|
|
27
28
|
- **Kudos** recognize a concrete contribution.
|
|
28
29
|
- **Memos** deliver a message to another agent or to one's future self.
|
|
29
30
|
- **Notes** retain agent-owned, revisable knowledge.
|
|
30
|
-
- **
|
|
31
|
+
- **Posts** announce something to everyone in the workspace, and record who has acknowledged it.
|
|
32
|
+
- **Tasks** delegate work to another agent, with their consent.
|
|
33
|
+
- **Todos** track an agent's own actions, private to them, with optional date-only or
|
|
34
|
+
timezone-aware deadlines.
|
|
31
35
|
|
|
32
36
|
## Core philosophy
|
|
33
37
|
|
|
@@ -38,9 +42,11 @@ collaboration through one auditable protocol. V1 provides that substrate locally
|
|
|
38
42
|
designed so the same agent identities and semantics can later cross machines through an explicitly
|
|
39
43
|
configured service.
|
|
40
44
|
|
|
41
|
-
One append-only
|
|
42
|
-
|
|
43
|
-
and
|
|
45
|
+
One append-only event store powers the TypeScript library, `synomem` CLI, actor-bound stdio MCP
|
|
46
|
+
server, compact change feeds, and readable Markdown projections. On the local backend that store is
|
|
47
|
+
SQLite on this machine, and nothing listens on the network. On Synomem Cloud it is a hosted
|
|
48
|
+
Postgres workspace reached over HTTPS with actor-scoped credentials. Events are never rewritten on
|
|
49
|
+
either one.
|
|
44
50
|
|
|
45
51
|
> [!IMPORTANT]
|
|
46
52
|
> Synomem is pre-1.0 software. Review the release notes before upgrading persisted storage or public
|
|
@@ -50,7 +56,34 @@ and opens no network listener.
|
|
|
50
56
|
|
|
51
57
|
```bash
|
|
52
58
|
npm install --global synomem
|
|
59
|
+
synomem config # asks where state should live, then sets it up
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`synomem config` is interactive. Its deterministic equivalents, for a machine with no terminal:
|
|
53
63
|
|
|
64
|
+
```bash
|
|
65
|
+
# Local: SQLite on this machine, no account.
|
|
66
|
+
synomem config init --backend local --yes
|
|
67
|
+
|
|
68
|
+
# Synomem Cloud with an installation access key from
|
|
69
|
+
# https://portal.synomem.ai/installations. The key names its own workspace, so
|
|
70
|
+
# there is no ID to look up -- and it is piped rather than passed as an
|
|
71
|
+
# argument, which the shell history and the process list would both keep.
|
|
72
|
+
printf '%s' "$SYNOMEM_KEY" | synomem config init \
|
|
73
|
+
--backend remote --auth access-key --access-token-stdin --yes
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Check either one at any time:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
synomem backend status # connects, and reports what answered
|
|
80
|
+
synomem projection status # local only: are the generated files current?
|
|
81
|
+
synomem doctor
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Everything below works the same on both backends.
|
|
85
|
+
|
|
86
|
+
```bash
|
|
54
87
|
export SYNOMEM_HOME="$(mktemp -d)/.synomem"
|
|
55
88
|
synomem init
|
|
56
89
|
synomem agent create codex --name "Codex"
|
|
@@ -78,8 +111,15 @@ synomem task accept <task-id> --as codex --response "Starting after the tests."
|
|
|
78
111
|
# A todo is private to the agent that wrote it; nobody else can assign one.
|
|
79
112
|
synomem todo create --as codex --title "Re-read the migration notes"
|
|
80
113
|
|
|
114
|
+
# A post is readable by everyone in the workspace, and tracks acknowledgement.
|
|
115
|
+
synomem post create --as gracie \
|
|
116
|
+
--title "Migration tonight" --body "Expect a short read-only window."
|
|
117
|
+
synomem post acknowledge <post-id> --as codex --note "Already handled."
|
|
118
|
+
synomem post roster <post-id>
|
|
119
|
+
|
|
81
120
|
synomem agent resolve Mike
|
|
82
121
|
synomem agent directory
|
|
122
|
+
synomem agent runtime list
|
|
83
123
|
synomem list
|
|
84
124
|
```
|
|
85
125
|
|
|
@@ -93,13 +133,15 @@ terminal-capable agent. The Synomem package contains the portable
|
|
|
93
133
|
installer for the six named local harnesses.
|
|
94
134
|
|
|
95
135
|
```text
|
|
96
|
-
Set up Synomem for this agent and runtime. Synomem is a
|
|
136
|
+
Set up Synomem for this agent and runtime. Synomem is a coordination system for durable kudos, one-to-one memos, private agent notes, workspace-wide posts with acknowledgement, consent-based assigned tasks, and private todos. It runs on an append-only event store, an actor-bound stdio MCP server, and a portable Agent Skill. The store is either local SQLite under ~/.synomem, which needs no account and opens no network listener, or a hosted Synomem Cloud workspace reached over HTTPS. Nothing is synchronized between the two. Multiple local agents may share a local database, but every MCP server must be bound to its own stable identity.
|
|
137
|
+
|
|
138
|
+
Each agent has an opaque canonical ID, generated at creation and never reused, and a separate handle that people type and that can be renamed later. `agent create` takes the handle. Never assert an ID yourself.
|
|
97
139
|
|
|
98
140
|
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
141
|
|
|
100
142
|
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 ~/.synomem. Run `synomem init
|
|
102
|
-
3. Run `synomem agent list`. Determine this agent's existing
|
|
143
|
+
2. Preserve an existing `SYNOMEM_HOME`; otherwise use the default ~/.synomem. Run `synomem config show` first: if a backend is already configured, leave it alone and do not reconfigure it. Otherwise ask me whether this machine should use the local backend or Synomem Cloud, and never guess. For local, run `synomem config init --backend local --yes`. For Synomem Cloud, ask me for an installation access key from https://portal.synomem.ai/installations and pipe it in — `printf '%s' "$KEY" | synomem config init --backend remote --auth access-key --access-token-stdin --yes` — because an argument is kept by both the shell history and the process list. The key names its own workspace, so do not ask me for a workspace ID. Then run `synomem backend status` and `synomem doctor`. Never point tests or experiments at another Synomem home.
|
|
144
|
+
3. Run `synomem agent list`. Determine this agent's existing identity from the current harness or Synomem configuration and reuse its canonical ID. If no identity is clearly established, ask me for the handle and display name before running `synomem agent create <handle> --name <name>`, then record the canonical ID it reports. Never silently merge or rename identities.
|
|
103
145
|
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
146
|
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.
|
|
105
147
|
6. Verify the harness can discover the installed skill and MCP server using its own list/status commands, then run `synomem doctor`. Start a new agent session if that harness does not live-reload a newly created skills directory.
|
package/dist/cli.d.ts
CHANGED
|
@@ -23,6 +23,12 @@ export interface CliDependencies {
|
|
|
23
23
|
reference: string;
|
|
24
24
|
credentialStore: CredentialStore;
|
|
25
25
|
}) => Promise<void>;
|
|
26
|
+
discoverBoundWorkspace?: (options: {
|
|
27
|
+
baseUrl: string;
|
|
28
|
+
accessToken: string;
|
|
29
|
+
}) => Promise<{
|
|
30
|
+
workspaceId: string;
|
|
31
|
+
}>;
|
|
26
32
|
createImportBundle?: (home: string) => Promise<ImportBundle>;
|
|
27
33
|
remoteImport?: (options: {
|
|
28
34
|
baseUrl: string;
|
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;
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAKA,OAAO,EAAE,OAAO,EAA0B,MAAM,WAAW,CAAC;AAmB5D,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,EASd,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;IAKpB,sBAAsB,CAAC,EAAE,CAAC,OAAO,EAAE;QACjC,OAAO,EAAE,MAAM,CAAC;QAChB,WAAW,EAAE,MAAM,CAAC;KACrB,KAAK,OAAO,CAAC;QAAE,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACvC,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,CA62ET;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
|
@@ -8,6 +8,7 @@ import { configuredServiceFactory, readSynomemConfig, writeSynomemBackend } from
|
|
|
8
8
|
import { cloudApiUrl } from './cloud.js';
|
|
9
9
|
import { resolveHome } from './config.js';
|
|
10
10
|
import { assertInteractive, confirmPlan, credentialFingerprint, credentialStoreChoices, environmentInstructions, readAccessToken, runConfigWizard, writeCredentialFile, } from './configure.js';
|
|
11
|
+
import { discoverBoundWorkspace, discoverOrganizations, workspaceChoices } from './discover.js';
|
|
11
12
|
import { defaultPromptIo } from './prompt.js';
|
|
12
13
|
import { credentialReference, OsCredentialStore } from './credentials.js';
|
|
13
14
|
import { asSynomemError, SynomemError } from './errors.js';
|
|
@@ -198,6 +199,7 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
|
|
|
198
199
|
const env = dependencies.env ?? process.env;
|
|
199
200
|
const credentialStore = dependencies.credentialStore ?? new OsCredentialStore();
|
|
200
201
|
const oauthLogin = dependencies.oauthLogin ?? loginWithOAuth;
|
|
202
|
+
const discoverWorkspace = dependencies.discoverBoundWorkspace ?? discoverBoundWorkspace;
|
|
201
203
|
const promptIo = dependencies.promptIo ?? defaultPromptIo();
|
|
202
204
|
const verifyRemoteCredential = dependencies.verifyRemoteCredential ??
|
|
203
205
|
(async (options) => {
|
|
@@ -226,6 +228,41 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
|
|
|
226
228
|
.showSuggestionAfterError()
|
|
227
229
|
.configureOutput({ writeOut: io.stdout, writeErr: io.stderr });
|
|
228
230
|
const remoteCommand = program.command('remote').description('Administer a remote workspace');
|
|
231
|
+
/*
|
|
232
|
+
* The browser counterpart to an access key naming its own workspace.
|
|
233
|
+
*
|
|
234
|
+
* A signed-in account may reach several organizations, each with several
|
|
235
|
+
* workspaces, so there is a genuine choice to make -- and no way to make it
|
|
236
|
+
* without seeing the list. Printing the IDs alongside the names is the point:
|
|
237
|
+
* the ID is what `backend use remote --workspace` takes.
|
|
238
|
+
*/
|
|
239
|
+
remoteCommand
|
|
240
|
+
.command('workspaces')
|
|
241
|
+
.description('List the organizations and workspaces this credential can reach')
|
|
242
|
+
.option('--url <url>', 'internal: alternate HTTPS origin')
|
|
243
|
+
.action(async (options, command) => {
|
|
244
|
+
const global = globals(command);
|
|
245
|
+
const config = readSynomemConfig(global.home, env);
|
|
246
|
+
const baseUrl = options.url ??
|
|
247
|
+
(config?.backend.kind === 'remote' ? config.backend.baseUrl : cloudApiUrl(env));
|
|
248
|
+
const accessToken = env.SYNOMEM_ACCESS_TOKEN;
|
|
249
|
+
if (!accessToken) {
|
|
250
|
+
throw new SynomemError('AUTH_REQUIRED', 'Set SYNOMEM_ACCESS_TOKEN, or run `synomem auth login` first.');
|
|
251
|
+
}
|
|
252
|
+
const organizations = await discoverOrganizations({ baseUrl, accessToken });
|
|
253
|
+
const choices = workspaceChoices(organizations);
|
|
254
|
+
const human = organizations.length
|
|
255
|
+
? organizations
|
|
256
|
+
.map((organization) => [
|
|
257
|
+
`${organization.displayName} (${organization.slug}) — ${organization.role}`,
|
|
258
|
+
...(organization.workspaces.length
|
|
259
|
+
? organization.workspaces.map((workspace) => ` ${workspace.id} ${workspace.displayName}`)
|
|
260
|
+
: [' (no workspaces yet)']),
|
|
261
|
+
].join('\n'))
|
|
262
|
+
.join('\n')
|
|
263
|
+
: 'This account belongs to no organizations yet.';
|
|
264
|
+
output(io, global.json, { organizations, choices }, human);
|
|
265
|
+
});
|
|
229
266
|
remoteCommand
|
|
230
267
|
.command('import')
|
|
231
268
|
.description('Preview or confirm a one-way import from a local Synomem home')
|
|
@@ -308,13 +345,45 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
|
|
|
308
345
|
.description('Set up Synomem, interactively or deterministically');
|
|
309
346
|
const applyPlan = async (plan, token, global) => {
|
|
310
347
|
const home = plan.home;
|
|
348
|
+
const serviceUrl = plan.serviceUrl ?? cloudApiUrl(env);
|
|
349
|
+
/*
|
|
350
|
+
* The workspace is discovered, not typed.
|
|
351
|
+
*
|
|
352
|
+
* An installation access key is bound to exactly one workspace, so the
|
|
353
|
+
* service can be asked which one rather than the person. An explicit
|
|
354
|
+
* --workspace still wins, because automation should not depend on a
|
|
355
|
+
* network round trip to configure a machine.
|
|
356
|
+
*/
|
|
357
|
+
let workspaceId = plan.workspaceId;
|
|
358
|
+
if (plan.backend === 'remote' && !workspaceId && token) {
|
|
359
|
+
const bound = await discoverWorkspace({ baseUrl: serviceUrl, accessToken: token });
|
|
360
|
+
workspaceId = bound.workspaceId;
|
|
361
|
+
io.stdout(`Access key is bound to workspace ${workspaceId}.\n`);
|
|
362
|
+
}
|
|
363
|
+
if (plan.backend === 'remote' && !workspaceId) {
|
|
364
|
+
/*
|
|
365
|
+
* Signing in through a browser needs an actor identity and a client ID,
|
|
366
|
+
* which is `synomem auth login`'s job. Rather than write a remote
|
|
367
|
+
* backend with no workspace -- a configuration that fails on its first
|
|
368
|
+
* real use -- say exactly what remains.
|
|
369
|
+
*/
|
|
370
|
+
output(io, global.json, { applied: false, pending: 'sign-in', home, serviceUrl }, [
|
|
371
|
+
'',
|
|
372
|
+
'Nothing was configured yet: signing in through a browser is a separate step.',
|
|
373
|
+
'',
|
|
374
|
+
'Run, with the actor this machine acts as:',
|
|
375
|
+
'',
|
|
376
|
+
' synomem auth login --actor-id <agent> --client-id <client>',
|
|
377
|
+
'',
|
|
378
|
+
'Then select the workspace it reports:',
|
|
379
|
+
'',
|
|
380
|
+
' synomem backend use remote --workspace <workspace-id>',
|
|
381
|
+
].join('\n'));
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
311
384
|
const config = writeSynomemBackend(plan.backend === 'local'
|
|
312
385
|
? { kind: 'local' }
|
|
313
|
-
: {
|
|
314
|
-
kind: 'remote',
|
|
315
|
-
baseUrl: plan.serviceUrl ?? cloudApiUrl(env),
|
|
316
|
-
workspaceId: plan.workspaceId,
|
|
317
|
-
}, home);
|
|
386
|
+
: { kind: 'remote', baseUrl: serviceUrl, workspaceId: workspaceId }, home);
|
|
318
387
|
let credentialLocation;
|
|
319
388
|
if (token) {
|
|
320
389
|
if (plan.credentialStore === 'environment') {
|
|
@@ -328,7 +397,7 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
|
|
|
328
397
|
// The platform store is the default, and a failure falls back to the
|
|
329
398
|
// restricted file rather than leaving the credential nowhere.
|
|
330
399
|
try {
|
|
331
|
-
await credentialStore.set(`synomem:${
|
|
400
|
+
await credentialStore.set(`synomem:${workspaceId}`, {
|
|
332
401
|
kind: 'installation-key',
|
|
333
402
|
accessToken: token,
|
|
334
403
|
});
|
|
@@ -392,8 +461,10 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
|
|
|
392
461
|
throw new SynomemError('INVALID_INPUT', 'Pass --backend local or --backend remote.');
|
|
393
462
|
}
|
|
394
463
|
const backend = options.backend;
|
|
395
|
-
|
|
396
|
-
|
|
464
|
+
// An access key names its own workspace, so --workspace is only
|
|
465
|
+
// required when there is no key to ask.
|
|
466
|
+
if (backend === 'remote' && !options.workspace && !options.accessTokenStdin) {
|
|
467
|
+
throw new SynomemError('INVALID_INPUT', 'Remote setup requires --workspace, or --access-token-stdin so the key can name its own.');
|
|
397
468
|
}
|
|
398
469
|
const token = options.accessTokenStdin ? await readAccessToken(promptIo) : undefined;
|
|
399
470
|
if (backend === 'remote' && options.auth === 'access-key' && !token) {
|
|
@@ -541,6 +612,80 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
|
|
|
541
612
|
}, global.home);
|
|
542
613
|
output(io, global.json, { backend: config.backend }, `Selected ${config.backend.kind} Synomem backend.`);
|
|
543
614
|
});
|
|
615
|
+
/*
|
|
616
|
+
* `show` reads the config file; `status` proves the selection actually works.
|
|
617
|
+
*
|
|
618
|
+
* The two are deliberately separate. A person debugging a broken setup needs
|
|
619
|
+
* to know what is configured even when nothing can be reached, and a person
|
|
620
|
+
* checking that a setup is live needs a connection to have been made. One
|
|
621
|
+
* command doing both would make a printed workspace ID look like a reachable
|
|
622
|
+
* workspace.
|
|
623
|
+
*/
|
|
624
|
+
backendCommand
|
|
625
|
+
.command('status')
|
|
626
|
+
.description('Connect to the selected backend and report what answered')
|
|
627
|
+
.action(async (_options, command) => {
|
|
628
|
+
const global = globals(command);
|
|
629
|
+
const config = readSynomemConfig(global.home);
|
|
630
|
+
if (!config) {
|
|
631
|
+
throw new SynomemError('CONFIG_INVALID', 'No Synomem home here yet. Run `synomem config init` first.');
|
|
632
|
+
}
|
|
633
|
+
const result = await withClient(global.home, defaultActor(env, 'system', 'cli'), async (client) => ({
|
|
634
|
+
info: await client.info(),
|
|
635
|
+
capabilities: await client.capabilities(),
|
|
636
|
+
diagnostics: (await client.doctor()).diagnostics.filter((item) => item.level === 'error' || item.level === 'warning'),
|
|
637
|
+
}));
|
|
638
|
+
const { info, capabilities, diagnostics } = result;
|
|
639
|
+
const where = info.backend === 'local'
|
|
640
|
+
? `Home: ${info.home}\nDatabase: ${info.databasePath}`
|
|
641
|
+
: `URL: ${info.baseUrl}`;
|
|
642
|
+
const problems = diagnostics.length
|
|
643
|
+
? diagnostics
|
|
644
|
+
.map((item) => `${item.level.toUpperCase()} ${item.code}: ${item.message}`)
|
|
645
|
+
.join('\n')
|
|
646
|
+
: 'No warnings or errors.';
|
|
647
|
+
const human = [
|
|
648
|
+
`Backend: ${info.backend} (reachable)`,
|
|
649
|
+
where,
|
|
650
|
+
`Workspace: ${capabilities.binding.workspaceId}`,
|
|
651
|
+
`Acting as: ${capabilities.binding.actor.kind} ${capabilities.binding.actor.id}`,
|
|
652
|
+
problems,
|
|
653
|
+
].join('\n');
|
|
654
|
+
output(io, global.json, { reachable: true, info, capabilities, diagnostics }, human);
|
|
655
|
+
if (diagnostics.some((item) => item.level === 'error'))
|
|
656
|
+
cliExitCodes.set(program, 5);
|
|
657
|
+
});
|
|
658
|
+
const projectionCommand = program
|
|
659
|
+
.command('projection')
|
|
660
|
+
.description('Inspect the generated files Synomem derives from events');
|
|
661
|
+
projectionCommand
|
|
662
|
+
.command('status')
|
|
663
|
+
.description('Report whether the generated files match the canonical events')
|
|
664
|
+
.action(async (_options, command) => {
|
|
665
|
+
const global = globals(command);
|
|
666
|
+
const status = await withClient(global.home, defaultActor(env, 'system', 'cli'), (client) => {
|
|
667
|
+
if (!client.projectionStatus) {
|
|
668
|
+
throw new SynomemError('INVALID_INPUT', 'The remote backend keeps no filesystem projections, so there is nothing to report.');
|
|
669
|
+
}
|
|
670
|
+
return client.projectionStatus();
|
|
671
|
+
});
|
|
672
|
+
const enabled = Object.entries(status.settings)
|
|
673
|
+
.filter(([, on]) => on)
|
|
674
|
+
.map(([name]) => name);
|
|
675
|
+
const lines = [
|
|
676
|
+
`Directory: ${status.directory ?? '(none)'}`,
|
|
677
|
+
`Enabled: ${enabled.length ? enabled.join(', ') : 'none'}`,
|
|
678
|
+
`Last rebuilt: ${status.lastRebuiltAt ?? 'never'}`,
|
|
679
|
+
status.current
|
|
680
|
+
? `Current: ${status.counts.manifest} generated file(s) match the events.`
|
|
681
|
+
: `Stale: ${status.counts.missing} missing, ${status.counts.unexpected} no longer expected. Run \`synomem rebuild\`.`,
|
|
682
|
+
];
|
|
683
|
+
for (const path of status.missing)
|
|
684
|
+
lines.push(` missing ${path}`);
|
|
685
|
+
for (const path of status.unexpected)
|
|
686
|
+
lines.push(` unexpected ${path}`);
|
|
687
|
+
output(io, global.json, status, lines.join('\n'));
|
|
688
|
+
});
|
|
544
689
|
const authCommand = program.command('auth').description('Inspect remote authentication');
|
|
545
690
|
authCommand
|
|
546
691
|
.command('status')
|
|
@@ -878,18 +1023,36 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
|
|
|
878
1023
|
}));
|
|
879
1024
|
output(io, global.json, binding, `Bound ${binding.agentId} to ${binding.runtime}${binding.profile ? `/${binding.profile}` : ''} (${binding.id})`);
|
|
880
1025
|
});
|
|
1026
|
+
/*
|
|
1027
|
+
* With no agent named this answers the question people actually arrive with:
|
|
1028
|
+
* "where is any of my stuff running?". Naming an agent narrows it. Requiring
|
|
1029
|
+
* the agent, as this once did, means you must already know the answer to the
|
|
1030
|
+
* question you came to ask.
|
|
1031
|
+
*/
|
|
881
1032
|
runtimeCommand
|
|
882
|
-
.command('list
|
|
883
|
-
.description('List
|
|
1033
|
+
.command('list [agent]')
|
|
1034
|
+
.description('List runtime bindings for one agent, or for every agent')
|
|
884
1035
|
.action(async (agent, _options, command) => {
|
|
885
1036
|
const global = globals(command);
|
|
886
|
-
const
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
1037
|
+
const result = await withClient(global.home, defaultActor(env, 'system', 'cli'), async (client) => {
|
|
1038
|
+
if (agent) {
|
|
1039
|
+
const profile = await client.agents.get(agent);
|
|
1040
|
+
return [{ profile, runtimeBindings: await client.agents.bindings(agent) }];
|
|
1041
|
+
}
|
|
1042
|
+
return (await client.agents.directory()).filter((entry) => entry.runtimeBindings.length > 0);
|
|
1043
|
+
});
|
|
1044
|
+
const describe = (binding) => ` ${binding.id} ${binding.runtime}${binding.profile ? `/${binding.profile}` : ''} bound ${binding.boundAt}`;
|
|
1045
|
+
const human = result.length
|
|
1046
|
+
? result
|
|
1047
|
+
.map((entry) => [
|
|
1048
|
+
`${entry.profile.handle} (${entry.profile.id})`,
|
|
1049
|
+
...entry.runtimeBindings.map(describe),
|
|
1050
|
+
].join('\n'))
|
|
890
1051
|
.join('\n')
|
|
891
|
-
:
|
|
892
|
-
|
|
1052
|
+
: agent
|
|
1053
|
+
? 'No runtime bindings.'
|
|
1054
|
+
: 'No agent in this workspace has a runtime binding.';
|
|
1055
|
+
output(io, global.json, { agents: result }, human);
|
|
893
1056
|
});
|
|
894
1057
|
runtimeCommand
|
|
895
1058
|
.command('unbind <binding-id>')
|