synomem 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +42 -1
- package/README.md +50 -10
- 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 +31 -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 +1 -1
- package/src/cli.ts +228 -20
- package/src/client.ts +41 -1
- package/src/configure.ts +30 -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,48 @@
|
|
|
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.0] - 2026-09-07
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- **`synomem backend status`** — connects to the selected backend and reports
|
|
11
|
+
what answered. `backend show` still reads the configuration file and connects
|
|
12
|
+
to nothing; a person debugging a broken setup needs that, and a person
|
|
13
|
+
confirming a working one needs a connection to have been made.
|
|
14
|
+
- **`synomem projection status`** — whether the generated files match the
|
|
15
|
+
canonical events, when they were last rebuilt, and which ones drifted. The
|
|
16
|
+
comparison is against Synomem's own manifest, so a file somebody added to the
|
|
17
|
+
projection tree by hand is not reported as drift.
|
|
18
|
+
- **`synomem remote workspaces`** — the organizations and workspaces a
|
|
19
|
+
credential can reach, with the IDs `backend use remote --workspace` takes.
|
|
20
|
+
- `agent runtime list` with no agent named lists every agent that runs
|
|
21
|
+
anywhere. Requiring the agent meant already knowing the answer to the
|
|
22
|
+
question being asked.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- **Remote setup no longer asks for a workspace ID.** An installation access
|
|
27
|
+
key is bound to exactly one workspace, so the service is asked which one
|
|
28
|
+
rather than the person; `config init --backend remote --access-token-stdin`
|
|
29
|
+
needs no `--workspace`. Typing `ws-04psqx2rkt8ttft7a1t2z69r97` from memory
|
|
30
|
+
was never something a person could do.
|
|
31
|
+
- The Windows credential-store choice is the restricted file rather than
|
|
32
|
+
Credential Manager, which the credential layer does not implement. Offering a
|
|
33
|
+
store that cannot read its own credential back fails on first use, after
|
|
34
|
+
setup has already claimed the credential was safe.
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
|
|
38
|
+
- `doctor` reported every workspace's projections as stale once any agent had a
|
|
39
|
+
generated ID: the expected-path list was built from canonical IDs while the
|
|
40
|
+
files, the manifest and the cleanup all used handles.
|
|
41
|
+
- `doctor`'s agent-directory symbolic-link check inspected a path built from the
|
|
42
|
+
canonical ID, so it examined a directory that does not exist and passed on a
|
|
43
|
+
workspace whose agent directory really had been replaced with a link.
|
|
44
|
+
|
|
45
|
+
## [0.4.0] - 2026-09-07
|
|
46
|
+
|
|
47
|
+
Published as 0.3.0 and 0.4.0 on the same day; the entries below cover both.
|
|
7
48
|
|
|
8
49
|
### Changed
|
|
9
50
|
|
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:
|
|
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:
|
|
53
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
|
|
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>')
|