synomem 0.5.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +64 -0
- package/README.md +30 -12
- package/dist/cli.d.ts +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +205 -32
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +10 -0
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +4 -0
- package/dist/config.js.map +1 -1
- package/dist/import.d.ts +4 -4
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +49 -3
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp-server.js +26 -3
- package/dist/mcp-server.js.map +1 -1
- package/dist/ports/projections.d.ts +8 -0
- package/dist/ports/projections.d.ts.map +1 -1
- package/dist/project.d.ts +51 -0
- package/dist/project.d.ts.map +1 -0
- package/dist/project.js +143 -0
- package/dist/project.js.map +1 -0
- package/dist/projections.d.ts +14 -0
- package/dist/projections.d.ts.map +1 -1
- package/dist/projections.js +36 -1
- package/dist/projections.js.map +1 -1
- package/dist/schemas.d.ts +18 -4
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +29 -2
- package/dist/schemas.js.map +1 -1
- package/dist/service.d.ts +1 -0
- package/dist/service.d.ts.map +1 -1
- package/dist/storage.d.ts +13 -0
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +24 -0
- package/dist/storage.js.map +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/workspaces.d.ts +41 -0
- package/dist/workspaces.d.ts.map +1 -0
- package/dist/workspaces.js +96 -0
- package/dist/workspaces.js.map +1 -0
- package/package.json +1 -1
- package/skills/synomem/SKILL.md +9 -3
- package/skills/synomem/references/examples.md +5 -3
- package/src/cli.ts +245 -32
- package/src/client.ts +10 -0
- package/src/config.ts +4 -0
- package/src/index.ts +17 -0
- package/src/mcp/index.ts +65 -3
- package/src/mcp-server.ts +32 -5
- package/src/ports/projections.ts +9 -0
- package/src/project.ts +168 -0
- package/src/projections.ts +38 -1
- package/src/schemas.ts +44 -12
- package/src/service.ts +1 -0
- package/src/storage.ts +28 -0
- package/src/types.ts +1 -0
- package/src/workspaces.ts +107 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,70 @@
|
|
|
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
|
+
## [0.6.0] - 2026-09-07
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- Local workspace isolation is now real. A local workspace used to be a
|
|
11
|
+
`--workspace` label with nothing behind it — only `events` carried
|
|
12
|
+
`workspace_id`, reads never filtered on it, and several tables had no such
|
|
13
|
+
column at all. A local workspace is now a separate SQLite database in its
|
|
14
|
+
own home, the safer design given SQLite has no row-level security: on
|
|
15
|
+
Postgres a query that forgets to filter by workspace returns nothing
|
|
16
|
+
instead of another workspace's rows, and separate files make the local
|
|
17
|
+
equivalent of that mistake unwritable rather than merely unlikely.
|
|
18
|
+
- A directory can now be bound to a local workspace: `synomem workspace use
|
|
19
|
+
<name> [--as <actor>]` writes `.synomem/config.json`, found by walking up
|
|
20
|
+
from the working directory the same way `.git` or `.nvmrc` is. Every command
|
|
21
|
+
and every stdio MCP server started in that directory afterward resolves the
|
|
22
|
+
workspace — and, unless overridden per launch, the default actor — with no
|
|
23
|
+
flag repeated. This is what makes several harnesses opened in the same
|
|
24
|
+
repository share one workspace while each still writes as its own actor:
|
|
25
|
+
`--workspace`, `SYNOMEM_WORKSPACE`, and `--home` all still win outright, in
|
|
26
|
+
that order, over the project file, and each harness's own `--agent-id` or
|
|
27
|
+
`--actor-id` always wins over the file's default actor.
|
|
28
|
+
- `synomem_agent_archive` and `synomem_agent_restore` MCP tools, closing the
|
|
29
|
+
gap where the CLI could archive or restore an agent identity but MCP could
|
|
30
|
+
not. Gated by a new `allowAgentArchiveViaMcp` capability
|
|
31
|
+
(`SYNOMEM_ALLOW_AGENT_ARCHIVE_VIA_MCP`), off by default and mirroring
|
|
32
|
+
`agentCreationViaMcp` exactly, so runtime agents cannot silently disable
|
|
33
|
+
each other unless an operator opts in.
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- An agent created before handles existed (schema 7) made every write in its
|
|
38
|
+
workspace fail with `UNSUPPORTED_EVENT`: its stored `agent.created` event
|
|
39
|
+
carries an id and no handle, and the compatibility check that runs before
|
|
40
|
+
each write refused the whole event stream on that missing field. The reader
|
|
41
|
+
now widens to accept it — the id is the correct handle for those records —
|
|
42
|
+
rather than the append-only log being rewritten.
|
|
43
|
+
- Renaming an agent's handle left its old projection directory behind,
|
|
44
|
+
stranding `NOTES.md`, the one file in there that belongs to the reader
|
|
45
|
+
rather than to Synomem and that a rebuild will never delete. The directory
|
|
46
|
+
is now moved to the new handle before projections are regenerated, so
|
|
47
|
+
hand-written notes arrive intact instead of being stranded.
|
|
48
|
+
|
|
49
|
+
## [0.5.3] - 2026-09-07
|
|
50
|
+
|
|
51
|
+
### Documentation
|
|
52
|
+
|
|
53
|
+
- 0.5.2 said the inbox "does not include posts or todos", which implied notes
|
|
54
|
+
were in it. They are not. The inbox holds kudos, memos and tasks and nothing
|
|
55
|
+
else, because it answers one question -- what is another actor waiting on
|
|
56
|
+
this agent for -- and nobody waits on an agent's own knowledge, its own
|
|
57
|
+
reminders, or an announcement addressed to everyone. The Skill, the examples
|
|
58
|
+
and the `synomem_inbox` description now say what it contains rather than
|
|
59
|
+
listing exclusions and getting the list wrong.
|
|
60
|
+
- `synomem_list` and `synomem_get` still described four record kinds out of
|
|
61
|
+
six. `synomem_list` now also says that posts and todos are reachable only
|
|
62
|
+
through it, and that an empty inbox is not the same as nothing to look at.
|
|
63
|
+
|
|
64
|
+
### Tests
|
|
65
|
+
|
|
66
|
+
- The inbox contract is pinned by a test, since the Skill and the MCP tool
|
|
67
|
+
descriptions both state it and neither can check it. Confirmed to fail when a
|
|
68
|
+
kind is added to the `pending` filter.
|
|
69
|
+
|
|
6
70
|
## [0.5.2] - 2026-09-07
|
|
7
71
|
|
|
8
72
|
### Documentation
|
package/README.md
CHANGED
|
@@ -38,9 +38,9 @@ Synomem gives humans and AI agents durable ways to coordinate beyond a disappear
|
|
|
38
38
|
Traditional AI memory layers resemble an isolated file cabinet for one model. Synomem turns memory
|
|
39
39
|
into a shared, transactional canvas: independently operating agents can retain private knowledge,
|
|
40
40
|
deliver durable context, delegate work with consent, track commitments, and recognize good
|
|
41
|
-
collaboration through one auditable protocol.
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
collaboration through one auditable protocol. The same agent identities and semantics work on one
|
|
42
|
+
machine or across many: the local backend keeps everything on disk, and Synomem Cloud keeps it in a
|
|
43
|
+
hosted workspace reached over HTTPS.
|
|
44
44
|
|
|
45
45
|
One append-only event store powers the TypeScript library, `synomem` CLI, actor-bound stdio MCP
|
|
46
46
|
server, compact change feeds, and readable Markdown projections. On the local backend that store is
|
|
@@ -85,7 +85,7 @@ Everything below works the same on both backends.
|
|
|
85
85
|
|
|
86
86
|
```bash
|
|
87
87
|
export SYNOMEM_HOME="$(mktemp -d)/.synomem"
|
|
88
|
-
synomem init
|
|
88
|
+
synomem config init --backend local --yes
|
|
89
89
|
synomem agent create codex --name "Codex"
|
|
90
90
|
synomem agent create gracie --name "Gracie"
|
|
91
91
|
|
|
@@ -243,23 +243,41 @@ are `claude`, `codex`, `hermes`, `openclaw`, `cursor`, and `grok`; `grokbot` ali
|
|
|
243
243
|
|
|
244
244
|
## Storage
|
|
245
245
|
|
|
246
|
+
This is the local backend. On Synomem Cloud the canonical store is a hosted Postgres workspace and
|
|
247
|
+
nothing below is written to this machine.
|
|
248
|
+
|
|
246
249
|
```text
|
|
247
250
|
~/.synomem/
|
|
248
|
-
├──
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
└──
|
|
251
|
+
├── config.json
|
|
252
|
+
├── synomem.sqlite3
|
|
253
|
+
├── credentials/
|
|
254
|
+
│ └── installation.json # only when an access key is stored in a file
|
|
255
|
+
└── <handle>/ # one directory per agent, named by handle
|
|
252
256
|
├── profile.json
|
|
253
257
|
├── WINS.md
|
|
254
258
|
├── MEMORY.md
|
|
255
259
|
├── TASKS.md
|
|
256
|
-
├──
|
|
257
|
-
└──
|
|
260
|
+
├── NOTES.md
|
|
261
|
+
└── inbox/{kudos,memos,tasks}/<record-id>.md
|
|
258
262
|
```
|
|
259
263
|
|
|
264
|
+
The home IS the storage directory: `config.json` and the database sit directly in it, with no
|
|
265
|
+
nested `synomem/` level.
|
|
266
|
+
|
|
267
|
+
Agent directories are named by HANDLE, because they exist to be read. The canonical agent ID is
|
|
268
|
+
what stored events reference, so renaming an agent leaves its history untouched.
|
|
269
|
+
|
|
270
|
+
Renaming moves the whole directory, `NOTES.md` included. That file is yours rather than Synomem's,
|
|
271
|
+
so a rebuild will never delete it — which is exactly why the rename moves the directory instead of
|
|
272
|
+
regenerating it somewhere new and leaving your notes behind.
|
|
273
|
+
|
|
260
274
|
SQLite events are canonical and append-only. Markdown and current-state tables are rebuildable
|
|
261
|
-
projections
|
|
262
|
-
|
|
275
|
+
projections — run `synomem rebuild` to regenerate them, and `synomem projection status` to see
|
|
276
|
+
whether they currently match the events. Posts and todos project no files: a post belongs to the
|
|
277
|
+
whole workspace rather than to one agent's directory, and a todo is private to its owner.
|
|
278
|
+
|
|
279
|
+
`NOTES.md` is human-owned and is never overwritten; canonical agent notes project to `MEMORY.md`.
|
|
280
|
+
Each projection can be turned off individually, in which case its file is not written at all.
|
|
263
281
|
|
|
264
282
|
Override the root with `SYNOMEM_HOME`, `--home`, or the library's `home` option. Use
|
|
265
283
|
`synomem backup` for a consistent snapshot and JSON or JSONL export for recovery. Never synchronize
|
package/dist/cli.d.ts
CHANGED
|
@@ -38,6 +38,6 @@ export interface CliDependencies {
|
|
|
38
38
|
planId?: string;
|
|
39
39
|
}) => Promise<ImportPreview | ImportResult>;
|
|
40
40
|
}
|
|
41
|
-
export declare function createCli(io?: CliIo, serviceFactory?: SynomemServiceFactory, dependencies?: CliDependencies): Command;
|
|
41
|
+
export declare function createCli(io?: CliIo, serviceFactory?: SynomemServiceFactory, dependencies?: CliDependencies, argv?: string[]): Command;
|
|
42
42
|
export declare function runCli(argv?: string[], io?: CliIo, serviceFactory?: SynomemServiceFactory, dependencies?: CliDependencies): Promise<number>;
|
|
43
43
|
//# sourceMappingURL=cli.d.ts.map
|
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;AAyB5D,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;AA+QD,wBAAgB,SAAS,CACvB,EAAE,GAAE,KAAiB,EACrB,cAAc,GAAE,qBAAgD,EAChE,YAAY,GAAE,eAAoB,EAClC,IAAI,GAAE,MAAM,EAAiB,GAC5B,OAAO,CA8/ET;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
|
@@ -9,6 +9,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
11
|
import { discoverBoundWorkspace, discoverOrganizations, workspaceChoices } from './discover.js';
|
|
12
|
+
import { DEFAULT_WORKSPACE, listLocalWorkspaces, localWorkspaceHome } from './workspaces.js';
|
|
13
|
+
import { findProjectSelection, resolveWorkspaceSelection, writeProjectSelection, } from './project.js';
|
|
12
14
|
import { defaultPromptIo } from './prompt.js';
|
|
13
15
|
import { credentialReference, OsCredentialStore } from './credentials.js';
|
|
14
16
|
import { asSynomemError, SynomemError } from './errors.js';
|
|
@@ -61,11 +63,19 @@ function actor(kind, id, displayName) {
|
|
|
61
63
|
* backend binds the credential to one actor, so SYNOMEM_ACTOR_ID/KIND/NAME must be able to
|
|
62
64
|
* override them or those commands cannot authenticate.
|
|
63
65
|
*/
|
|
64
|
-
function defaultActor(env, fallbackKind, fallbackId
|
|
65
|
-
|
|
66
|
+
function defaultActor(env, fallbackKind, fallbackId,
|
|
67
|
+
/** `--actor`, which outranks the environment: it is said on this invocation. */
|
|
68
|
+
override) {
|
|
69
|
+
const id = override?.trim() || env.SYNOMEM_ACTOR_ID?.trim();
|
|
66
70
|
if (!id)
|
|
67
71
|
return actor(fallbackKind, fallbackId);
|
|
68
|
-
|
|
72
|
+
const kind = override?.trim()
|
|
73
|
+
? // An explicit --actor names an agent unless told otherwise; the historical
|
|
74
|
+
// fallbacks here are `system`/`cli`, which is not what somebody means when
|
|
75
|
+
// they name one.
|
|
76
|
+
env.SYNOMEM_ACTOR_KIND?.trim() || 'agent'
|
|
77
|
+
: env.SYNOMEM_ACTOR_KIND?.trim() || fallbackKind;
|
|
78
|
+
return actor(kind, id, env.SYNOMEM_ACTOR_NAME?.trim());
|
|
69
79
|
}
|
|
70
80
|
function taskDue(options) {
|
|
71
81
|
if (options.dueDate && options.dueAt)
|
|
@@ -148,9 +158,28 @@ function showRecord(record) {
|
|
|
148
158
|
function output(io, json, value, human) {
|
|
149
159
|
io.stdout(json ? `${JSON.stringify(value, null, 2)}\n` : `${human}\n`);
|
|
150
160
|
}
|
|
151
|
-
|
|
152
|
-
|
|
161
|
+
/**
|
|
162
|
+
* The resolved global options for a command.
|
|
163
|
+
*
|
|
164
|
+
* `--workspace` is turned into a home HERE, before any service exists, which is
|
|
165
|
+
* the whole reason it costs nothing downstream: a local workspace is a separate
|
|
166
|
+
* database in its own home, and choosing one is choosing a home. Nothing in the
|
|
167
|
+
* domain, the commands, or the MCP tools learns that a workspace was selected.
|
|
168
|
+
*
|
|
169
|
+
* On a remote backend the name means a hosted workspace instead, which
|
|
170
|
+
* `backend use remote --workspace` already handles; passing both here would be
|
|
171
|
+
* two different answers to the same question, so it is refused.
|
|
172
|
+
*/
|
|
173
|
+
/** `parent child`, so a subcommand name cannot be confused with another's. */
|
|
174
|
+
function commandPath(command) {
|
|
175
|
+
const parent = command.parent?.name();
|
|
176
|
+
return parent && parent !== 'synomem' ? `${parent} ${command.name()}` : command.name();
|
|
153
177
|
}
|
|
178
|
+
/**
|
|
179
|
+
* Commands where `--workspace` names a HOSTED workspace being configured,
|
|
180
|
+
* rather than a local one to act in.
|
|
181
|
+
*/
|
|
182
|
+
const CONFIGURES_BACKEND = new Set(['config init', 'backend use', 'remote import']);
|
|
154
183
|
async function withService(serviceFactory, home, configuredActor, operation) {
|
|
155
184
|
const client = serviceFactory({ ...(home ? { home } : {}), actor: configuredActor });
|
|
156
185
|
await client.init();
|
|
@@ -195,8 +224,77 @@ function listInput(options) {
|
|
|
195
224
|
offset: Number(options.offset),
|
|
196
225
|
};
|
|
197
226
|
}
|
|
198
|
-
|
|
227
|
+
/**
|
|
228
|
+
* The value `--actor` should supply to commands that name an actor.
|
|
229
|
+
*
|
|
230
|
+
* Read from argv directly, before the commands are built, because Commander
|
|
231
|
+
* evaluates option defaults at DECLARATION time: a `--as` declared without one
|
|
232
|
+
* is required, and a `--as` declared with one is already satisfied. Supplying
|
|
233
|
+
* it here is a single change point instead of a fallback threaded through
|
|
234
|
+
* twenty action bodies, and `--as` still wins when both are given because an
|
|
235
|
+
* explicitly passed option overrides its default.
|
|
236
|
+
*/
|
|
237
|
+
function actorDefault(argv, env) {
|
|
238
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
239
|
+
const argument = argv[index];
|
|
240
|
+
if (argument === '--actor')
|
|
241
|
+
return argv[index + 1]?.trim() || undefined;
|
|
242
|
+
if (argument.startsWith('--actor='))
|
|
243
|
+
return argument.slice('--actor='.length).trim() || undefined;
|
|
244
|
+
}
|
|
245
|
+
if (env.SYNOMEM_ACTOR_ID?.trim())
|
|
246
|
+
return env.SYNOMEM_ACTOR_ID.trim();
|
|
247
|
+
/*
|
|
248
|
+
* Last, the project's own binding. `workspace use --as` exists so a
|
|
249
|
+
* repository can settle both questions once — which workspace, and as whom —
|
|
250
|
+
* and a command run there needs neither flag afterwards.
|
|
251
|
+
*/
|
|
252
|
+
try {
|
|
253
|
+
return findProjectSelection()?.actor;
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
// A malformed project file is reported by the resolver when the command
|
|
257
|
+
// actually runs, with the path in the message. Failing here would turn it
|
|
258
|
+
// into an error before any command had been parsed.
|
|
259
|
+
return undefined;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
export function createCli(io = defaultIo, serviceFactory = configuredServiceFactory, dependencies = {}, argv = process.argv) {
|
|
199
263
|
const env = dependencies.env ?? process.env;
|
|
264
|
+
const actingDefault = actorDefault(argv, env);
|
|
265
|
+
const globals = (command) => {
|
|
266
|
+
const options = command.optsWithGlobals();
|
|
267
|
+
if (CONFIGURES_BACKEND.has(commandPath(command)))
|
|
268
|
+
return options;
|
|
269
|
+
/*
|
|
270
|
+
* Resolution runs even with no `--workspace`, because a project's
|
|
271
|
+
* `.synomem/config.json` selects one without anybody passing a flag — that is
|
|
272
|
+
* the whole point of it. `--home` still wins outright: it names a home
|
|
273
|
+
* directly rather than a workspace within one.
|
|
274
|
+
*/
|
|
275
|
+
if (!options.home) {
|
|
276
|
+
const selection = resolveWorkspaceSelection({
|
|
277
|
+
...(options.workspace ? { flag: options.workspace } : {}),
|
|
278
|
+
...(options.actor ? { actorFlag: options.actor } : {}),
|
|
279
|
+
env,
|
|
280
|
+
});
|
|
281
|
+
return { ...options, home: selection.home, actor: selection.actor ?? options.actor };
|
|
282
|
+
}
|
|
283
|
+
if (!options.workspace)
|
|
284
|
+
return options;
|
|
285
|
+
/*
|
|
286
|
+
* One flag, one meaning — "which workspace" — resolved differently by the
|
|
287
|
+
* handful of commands that CONFIGURE a backend rather than act inside one.
|
|
288
|
+
* For those, the value is a hosted workspace ID to be written to the config,
|
|
289
|
+
* so it is passed through raw and they read `workspace`. Everywhere else it
|
|
290
|
+
* names a local workspace, which is a home.
|
|
291
|
+
*
|
|
292
|
+
* These commands used to declare their own `--workspace`, which does not
|
|
293
|
+
* work: Commander gives a duplicated long flag to the parent, so the
|
|
294
|
+
* subcommand never received it at all.
|
|
295
|
+
*/
|
|
296
|
+
return { ...options, home: localWorkspaceHome(options.workspace, options.home) };
|
|
297
|
+
};
|
|
200
298
|
const credentialStore = dependencies.credentialStore ?? new OsCredentialStore();
|
|
201
299
|
const oauthLogin = dependencies.oauthLogin ?? loginWithOAuth;
|
|
202
300
|
const discoverWorkspace = dependencies.discoverBoundWorkspace ?? discoverBoundWorkspace;
|
|
@@ -224,9 +322,86 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
|
|
|
224
322
|
.description('Local-first communication, memory, recognition, and task infrastructure for agents')
|
|
225
323
|
.version(packageVersion())
|
|
226
324
|
.option('--home <path>', 'storage root (defaults to SYNOMEM_HOME or ~/.synomem)')
|
|
325
|
+
// A local workspace is its own database under the root, so this selects a
|
|
326
|
+
// home. On a remote backend the hosted workspace is chosen by
|
|
327
|
+
// `backend use remote --workspace` instead.
|
|
328
|
+
.option('--workspace <name>', 'local workspace to act in (see `synomem workspace list`)')
|
|
329
|
+
.option('--actor <id>', 'act as this agent (overrides SYNOMEM_ACTOR_ID)')
|
|
227
330
|
.option('--json', 'emit stable machine-readable JSON', false)
|
|
228
331
|
.showSuggestionAfterError()
|
|
229
332
|
.configureOutput({ writeOut: io.stdout, writeErr: io.stderr });
|
|
333
|
+
/*
|
|
334
|
+
* Local workspaces.
|
|
335
|
+
*
|
|
336
|
+
* Each is a separate database in its own home, which is what makes the
|
|
337
|
+
* isolation real: SQLite has no row-level security, so a shared file would
|
|
338
|
+
* rest on every query remembering to filter, with nothing to catch a miss.
|
|
339
|
+
* Separate files mean cross-workspace leakage is not something anybody can
|
|
340
|
+
* write by accident.
|
|
341
|
+
*/
|
|
342
|
+
const workspaceCommand = program
|
|
343
|
+
.command('workspace')
|
|
344
|
+
.description('Work in a separate local store, isolated from the others');
|
|
345
|
+
workspaceCommand
|
|
346
|
+
.command('list')
|
|
347
|
+
.description('List the local workspaces on this machine')
|
|
348
|
+
.action((_options, command) => {
|
|
349
|
+
const global = globals(command);
|
|
350
|
+
// Read from disk, so nothing is listed that does not exist.
|
|
351
|
+
const workspaces = listLocalWorkspaces(global.home);
|
|
352
|
+
// Which one is in effect here, and what decided it — a flag, the
|
|
353
|
+
// environment, a project file, or nothing.
|
|
354
|
+
const selection = resolveWorkspaceSelection({ env });
|
|
355
|
+
const human = workspaces
|
|
356
|
+
.map((workspace) => `${workspace.name === DEFAULT_WORKSPACE ? '*' : ' '} ${workspace.name.padEnd(24)} ${workspace.initialized ? workspace.home : `${workspace.home} (not initialized)`}`)
|
|
357
|
+
.join('\n');
|
|
358
|
+
output(io, global.json, { workspaces, active: selection.workspace ?? DEFAULT_WORKSPACE, source: selection.source }, [
|
|
359
|
+
human,
|
|
360
|
+
'',
|
|
361
|
+
`Acting in: ${selection.workspace ?? DEFAULT_WORKSPACE} (from ${selection.source})`,
|
|
362
|
+
'',
|
|
363
|
+
'Bind a directory with `synomem workspace use <name>`, or pass --workspace once.',
|
|
364
|
+
'A local workspace is a separate store on this machine; a hosted workspace is shared,',
|
|
365
|
+
'and is selected with `backend use remote --workspace`.',
|
|
366
|
+
].join('\n'));
|
|
367
|
+
});
|
|
368
|
+
workspaceCommand
|
|
369
|
+
.command('use <name>')
|
|
370
|
+
.description('Bind this directory to a workspace, for every session opened here')
|
|
371
|
+
.option('--as <actor-id>', 'also always write as this agent', actingDefault)
|
|
372
|
+
.action((name, options, command) => {
|
|
373
|
+
const global = globals(command);
|
|
374
|
+
// Validated by resolving it, so a name that could never work is refused
|
|
375
|
+
// before a file claiming it is written.
|
|
376
|
+
const home = localWorkspaceHome(name, undefined);
|
|
377
|
+
const path = writeProjectSelection(process.cwd(), {
|
|
378
|
+
workspace: name,
|
|
379
|
+
...(options.as ? { actor: options.as } : {}),
|
|
380
|
+
});
|
|
381
|
+
output(io, global.json, { path, workspace: name, home, ...(options.as ? { actor: options.as } : {}) }, [
|
|
382
|
+
`Wrote ${path}`,
|
|
383
|
+
'',
|
|
384
|
+
`Every Synomem command and MCP server started in this directory now acts in ${name}${options.as ? ` as ${options.as}` : ''}, with no flag.`,
|
|
385
|
+
`Records live in ${home} — nothing is stored in this directory.`,
|
|
386
|
+
'',
|
|
387
|
+
'Commit it to share the choice with the repository, or ignore it to keep it yours.',
|
|
388
|
+
].join('\n'));
|
|
389
|
+
});
|
|
390
|
+
workspaceCommand
|
|
391
|
+
.command('create <name>')
|
|
392
|
+
.description('Create a local workspace and initialize its store')
|
|
393
|
+
.action(async (name, _options, command) => {
|
|
394
|
+
const global = globals(command);
|
|
395
|
+
const home = localWorkspaceHome(name, global.home);
|
|
396
|
+
if (readSynomemConfig(home)) {
|
|
397
|
+
throw new SynomemError('INVALID_INPUT', `Workspace already exists: ${name}`);
|
|
398
|
+
}
|
|
399
|
+
writeSynomemBackend({ kind: 'local' }, home);
|
|
400
|
+
// Opening it once creates the database, so `list` does not report a
|
|
401
|
+
// workspace that exists in name only.
|
|
402
|
+
await withClient(home, defaultActor(env, 'system', 'cli'), async () => undefined);
|
|
403
|
+
output(io, global.json, { name, home }, `Created workspace ${name} at ${home}.\nAct in it with --workspace ${name}.`);
|
|
404
|
+
});
|
|
230
405
|
const remoteCommand = program.command('remote').description('Administer a remote workspace');
|
|
231
406
|
/*
|
|
232
407
|
* The browser counterpart to an access key naming its own workspace.
|
|
@@ -449,7 +624,6 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
|
|
|
449
624
|
.description('Configure Synomem without prompting')
|
|
450
625
|
.option('--backend <kind>', 'local or remote')
|
|
451
626
|
.option('--auth <method>', 'browser or access-key')
|
|
452
|
-
.option('--workspace <id>', 'remote workspace ID')
|
|
453
627
|
.option('--credential-store <where>', 'auto, keychain, file, or environment', 'auto')
|
|
454
628
|
// The token is read from stdin, never taken as an argument: an argument is
|
|
455
629
|
// kept by the shell history and visible in the process list.
|
|
@@ -463,7 +637,7 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
|
|
|
463
637
|
const backend = options.backend;
|
|
464
638
|
// An access key names its own workspace, so --workspace is only
|
|
465
639
|
// required when there is no key to ask.
|
|
466
|
-
if (backend === 'remote' && !
|
|
640
|
+
if (backend === 'remote' && !global.workspace && !options.accessTokenStdin) {
|
|
467
641
|
throw new SynomemError('INVALID_INPUT', 'Remote setup requires --workspace, or --access-token-stdin so the key can name its own.');
|
|
468
642
|
}
|
|
469
643
|
const token = options.accessTokenStdin ? await readAccessToken(promptIo) : undefined;
|
|
@@ -477,7 +651,7 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
|
|
|
477
651
|
? {
|
|
478
652
|
serviceUrl: cloudApiUrl(env),
|
|
479
653
|
auth: options.auth ?? 'access-key',
|
|
480
|
-
workspaceId:
|
|
654
|
+
workspaceId: global.workspace,
|
|
481
655
|
credentialStore: options.credentialStore,
|
|
482
656
|
}
|
|
483
657
|
: {}),
|
|
@@ -594,13 +768,12 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
|
|
|
594
768
|
// never ask for a service address, because a person has no way to tell a
|
|
595
769
|
// real one from a phished one.
|
|
596
770
|
.option('--url <url>', 'internal: alternate HTTPS origin')
|
|
597
|
-
.option('--workspace <id>', 'remote workspace ID')
|
|
598
771
|
.action((kind, options, command) => {
|
|
599
772
|
const global = globals(command);
|
|
600
773
|
if (kind !== 'local' && kind !== 'remote') {
|
|
601
774
|
throw new SynomemError('INVALID_INPUT', 'Backend kind must be local or remote.');
|
|
602
775
|
}
|
|
603
|
-
if (kind === 'remote' && !
|
|
776
|
+
if (kind === 'remote' && !global.workspace) {
|
|
604
777
|
throw new SynomemError('INVALID_INPUT', 'Remote backend selection requires --workspace.');
|
|
605
778
|
}
|
|
606
779
|
const config = writeSynomemBackend(kind === 'local'
|
|
@@ -608,7 +781,7 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
|
|
|
608
781
|
: {
|
|
609
782
|
kind: 'remote',
|
|
610
783
|
baseUrl: options.url ?? cloudApiUrl(env),
|
|
611
|
-
workspaceId:
|
|
784
|
+
workspaceId: global.workspace,
|
|
612
785
|
}, global.home);
|
|
613
786
|
output(io, global.json, { backend: config.backend }, `Selected ${config.backend.kind} Synomem backend.`);
|
|
614
787
|
});
|
|
@@ -1066,7 +1239,7 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
|
|
|
1066
1239
|
postCommand
|
|
1067
1240
|
.command('create')
|
|
1068
1241
|
.description('Publish a post the whole workspace can read')
|
|
1069
|
-
.requiredOption('--as <actor-id>')
|
|
1242
|
+
.requiredOption('--as <actor-id>', 'actor to act as (defaults to --actor)', actingDefault)
|
|
1070
1243
|
.option('--actor-kind <kind>', 'human, agent, or system', 'agent')
|
|
1071
1244
|
.requiredOption('--title <title>')
|
|
1072
1245
|
.requiredOption('--body <body>')
|
|
@@ -1085,7 +1258,7 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
|
|
|
1085
1258
|
postCommand
|
|
1086
1259
|
.command('list')
|
|
1087
1260
|
.description('List posts in this workspace')
|
|
1088
|
-
.requiredOption('--as <actor-id>')
|
|
1261
|
+
.requiredOption('--as <actor-id>', 'actor to act as (defaults to --actor)', actingDefault)
|
|
1089
1262
|
.option('--actor-kind <kind>', 'human, agent, or system', 'agent')
|
|
1090
1263
|
.option('--limit <n>', 'default 10, maximum 50')
|
|
1091
1264
|
.action(async (options, command) => {
|
|
@@ -1099,7 +1272,7 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
|
|
|
1099
1272
|
postCommand
|
|
1100
1273
|
.command('show <post-id>')
|
|
1101
1274
|
.description('Show one post with its acknowledgements')
|
|
1102
|
-
.requiredOption('--as <actor-id>')
|
|
1275
|
+
.requiredOption('--as <actor-id>', 'actor to act as (defaults to --actor)', actingDefault)
|
|
1103
1276
|
.option('--actor-kind <kind>', 'human, agent, or system', 'agent')
|
|
1104
1277
|
.action(async (postId, options, command) => {
|
|
1105
1278
|
const global = globals(command);
|
|
@@ -1114,7 +1287,7 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
|
|
|
1114
1287
|
postCommand
|
|
1115
1288
|
.command('acknowledge <post-id>')
|
|
1116
1289
|
.description('Say you have seen a post')
|
|
1117
|
-
.requiredOption('--as <actor-id>')
|
|
1290
|
+
.requiredOption('--as <actor-id>', 'actor to act as (defaults to --actor)', actingDefault)
|
|
1118
1291
|
.option('--actor-kind <kind>', 'human, agent, or system', 'agent')
|
|
1119
1292
|
.option('--note <text>', 'optional context for the author')
|
|
1120
1293
|
.action(async (postId, options, command) => {
|
|
@@ -1128,7 +1301,7 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
|
|
|
1128
1301
|
postCommand
|
|
1129
1302
|
.command('roster <post-id>')
|
|
1130
1303
|
.description('Who has acknowledged a post, and who has not')
|
|
1131
|
-
.requiredOption('--as <actor-id>')
|
|
1304
|
+
.requiredOption('--as <actor-id>', 'actor to act as (defaults to --actor)', actingDefault)
|
|
1132
1305
|
.option('--actor-kind <kind>', 'human, agent, or system', 'agent')
|
|
1133
1306
|
.action(async (postId, options, command) => {
|
|
1134
1307
|
const global = globals(command);
|
|
@@ -1153,7 +1326,7 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
|
|
|
1153
1326
|
postCommand
|
|
1154
1327
|
.command('archive <post-id>')
|
|
1155
1328
|
.description('Archive a post you wrote')
|
|
1156
|
-
.requiredOption('--as <actor-id>')
|
|
1329
|
+
.requiredOption('--as <actor-id>', 'actor to act as (defaults to --actor)', actingDefault)
|
|
1157
1330
|
.option('--actor-kind <kind>', 'human, agent, or system', 'agent')
|
|
1158
1331
|
.option('--reason <text>')
|
|
1159
1332
|
.action(async (postId, options, command) => {
|
|
@@ -1165,7 +1338,7 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
|
|
|
1165
1338
|
kudosCommand
|
|
1166
1339
|
.command('give <recipient>')
|
|
1167
1340
|
.description('Give specific, evidence-based kudos to an agent')
|
|
1168
|
-
.requiredOption('--from <actor-id>', 'stable ID of the giver')
|
|
1341
|
+
.requiredOption('--from <actor-id>', 'stable ID of the giver (defaults to --actor)', actingDefault)
|
|
1169
1342
|
.requiredOption('--actor-kind <kind>', 'human, agent, or system')
|
|
1170
1343
|
.option('--actor-name <display-name>')
|
|
1171
1344
|
.requiredOption('--title <title>')
|
|
@@ -1258,7 +1431,7 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
|
|
|
1258
1431
|
const memoCommand = program.command('memo').description('Send and manage durable messages');
|
|
1259
1432
|
memoCommand
|
|
1260
1433
|
.command('send <recipient>')
|
|
1261
|
-
.requiredOption('--from <actor-id>')
|
|
1434
|
+
.requiredOption('--from <actor-id>', 'actor to act as (defaults to --actor)', actingDefault)
|
|
1262
1435
|
.option('--actor-kind <kind>', 'human, agent, or system', 'agent')
|
|
1263
1436
|
.option('--actor-name <name>')
|
|
1264
1437
|
.requiredOption('--subject <subject>')
|
|
@@ -1319,7 +1492,7 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
|
|
|
1319
1492
|
.description('Retain and revise agent-owned knowledge');
|
|
1320
1493
|
noteCommand
|
|
1321
1494
|
.command('create')
|
|
1322
|
-
.requiredOption('--as <actor-id>')
|
|
1495
|
+
.requiredOption('--as <actor-id>', 'actor to act as (defaults to --actor)', actingDefault)
|
|
1323
1496
|
.option('--actor-kind <kind>', 'agent or human', 'agent')
|
|
1324
1497
|
.option('--owner <agent-id>')
|
|
1325
1498
|
.requiredOption('--title <title>')
|
|
@@ -1358,7 +1531,7 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
|
|
|
1358
1531
|
});
|
|
1359
1532
|
noteCommand
|
|
1360
1533
|
.command('revise <note-id>')
|
|
1361
|
-
.requiredOption('--as <actor-id>')
|
|
1534
|
+
.requiredOption('--as <actor-id>', 'actor to act as (defaults to --actor)', actingDefault)
|
|
1362
1535
|
.option('--actor-kind <kind>', 'agent or human', 'agent')
|
|
1363
1536
|
.requiredOption('--expected-version <number>')
|
|
1364
1537
|
.option('--title <title>')
|
|
@@ -1379,7 +1552,7 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
|
|
|
1379
1552
|
});
|
|
1380
1553
|
noteCommand
|
|
1381
1554
|
.command('archive <note-id>')
|
|
1382
|
-
.requiredOption('--as <actor-id>')
|
|
1555
|
+
.requiredOption('--as <actor-id>', 'actor to act as (defaults to --actor)', actingDefault)
|
|
1383
1556
|
.option('--actor-kind <kind>', 'agent or human', 'agent')
|
|
1384
1557
|
.option('--idempotency-key <key>')
|
|
1385
1558
|
.action(async (id, options, command) => {
|
|
@@ -1395,7 +1568,7 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
|
|
|
1395
1568
|
.description('Create and manage your own private reminders');
|
|
1396
1569
|
todoCommand
|
|
1397
1570
|
.command('create')
|
|
1398
|
-
.requiredOption('--as <actor-id>')
|
|
1571
|
+
.requiredOption('--as <actor-id>', 'actor to act as (defaults to --actor)', actingDefault)
|
|
1399
1572
|
.option('--actor-kind <kind>', 'human, agent, or system', 'agent')
|
|
1400
1573
|
.requiredOption('--title <title>')
|
|
1401
1574
|
.option('--details <text>', 'private working detail')
|
|
@@ -1419,7 +1592,7 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
|
|
|
1419
1592
|
});
|
|
1420
1593
|
todoCommand
|
|
1421
1594
|
.command('list')
|
|
1422
|
-
.requiredOption('--as <actor-id>')
|
|
1595
|
+
.requiredOption('--as <actor-id>', 'actor to act as (defaults to --actor)', actingDefault)
|
|
1423
1596
|
.option('--actor-kind <kind>', 'human, agent, or system', 'agent')
|
|
1424
1597
|
.option('--status <status>')
|
|
1425
1598
|
.option('--limit <number>', 'maximum results', '10')
|
|
@@ -1437,7 +1610,7 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
|
|
|
1437
1610
|
});
|
|
1438
1611
|
todoCommand
|
|
1439
1612
|
.command('show <todo-id>')
|
|
1440
|
-
.requiredOption('--as <actor-id>')
|
|
1613
|
+
.requiredOption('--as <actor-id>', 'actor to act as (defaults to --actor)', actingDefault)
|
|
1441
1614
|
.option('--actor-kind <kind>', 'human, agent, or system', 'agent')
|
|
1442
1615
|
.action(async (id, options, command) => {
|
|
1443
1616
|
const global = globals(command);
|
|
@@ -1447,7 +1620,7 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
|
|
|
1447
1620
|
for (const operation of ['complete', 'reopen', 'cancel', 'archive']) {
|
|
1448
1621
|
todoCommand
|
|
1449
1622
|
.command(`${operation} <todo-id>`)
|
|
1450
|
-
.requiredOption('--as <actor-id>')
|
|
1623
|
+
.requiredOption('--as <actor-id>', 'actor to act as (defaults to --actor)', actingDefault)
|
|
1451
1624
|
.option('--actor-kind <kind>', 'human, agent, or system', 'agent')
|
|
1452
1625
|
.option('--note <text>')
|
|
1453
1626
|
.option('--reason <text>')
|
|
@@ -1485,7 +1658,7 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
|
|
|
1485
1658
|
const taskCommand = program.command('task').description('Create and manage agent tasks');
|
|
1486
1659
|
taskCommand
|
|
1487
1660
|
.command('create <assignee>')
|
|
1488
|
-
.requiredOption('--from <actor-id>')
|
|
1661
|
+
.requiredOption('--from <actor-id>', 'actor to act as (defaults to --actor)', actingDefault)
|
|
1489
1662
|
.option('--actor-kind <kind>', 'human, agent, or system', 'agent')
|
|
1490
1663
|
.requiredOption('--title <title>')
|
|
1491
1664
|
.option('--description <text>')
|
|
@@ -1531,7 +1704,7 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
|
|
|
1531
1704
|
});
|
|
1532
1705
|
taskCommand
|
|
1533
1706
|
.command('update <task-id>')
|
|
1534
|
-
.requiredOption('--as <actor-id>')
|
|
1707
|
+
.requiredOption('--as <actor-id>', 'actor to act as (defaults to --actor)', actingDefault)
|
|
1535
1708
|
.option('--actor-kind <kind>', 'agent or human', 'agent')
|
|
1536
1709
|
.requiredOption('--expected-version <number>')
|
|
1537
1710
|
.option('--title <title>')
|
|
@@ -1561,7 +1734,7 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
|
|
|
1561
1734
|
for (const operation of ['accept', 'reject', 'complete', 'reopen', 'cancel']) {
|
|
1562
1735
|
const command_ = taskCommand
|
|
1563
1736
|
.command(`${operation} <task-id>`)
|
|
1564
|
-
.requiredOption('--as <actor-id>')
|
|
1737
|
+
.requiredOption('--as <actor-id>', 'actor to act as (defaults to --actor)', actingDefault)
|
|
1565
1738
|
.option('--actor-kind <kind>', 'agent or human', 'agent')
|
|
1566
1739
|
.option('--note <text>')
|
|
1567
1740
|
.option('--reason <text>')
|
|
@@ -1637,7 +1810,7 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
|
|
|
1637
1810
|
kudosCommand
|
|
1638
1811
|
.command('revoke <kudos-id>')
|
|
1639
1812
|
.description('Record a revocation while preserving history')
|
|
1640
|
-
.requiredOption('--as <actor-id>')
|
|
1813
|
+
.requiredOption('--as <actor-id>', 'actor to act as (defaults to --actor)', actingDefault)
|
|
1641
1814
|
.option('--actor-kind <kind>', 'human, agent, or system', 'human')
|
|
1642
1815
|
.requiredOption('--reason <reason>')
|
|
1643
1816
|
.option('--administrative', 'mark as an administrative revocation', false)
|
|
@@ -1779,7 +1952,7 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
|
|
|
1779
1952
|
return program;
|
|
1780
1953
|
}
|
|
1781
1954
|
export async function runCli(argv = process.argv, io = defaultIo, serviceFactory = configuredServiceFactory, dependencies = {}) {
|
|
1782
|
-
const program = createCli(io, serviceFactory, dependencies);
|
|
1955
|
+
const program = createCli(io, serviceFactory, dependencies, argv);
|
|
1783
1956
|
program.exitOverride();
|
|
1784
1957
|
try {
|
|
1785
1958
|
await program.parseAsync(argv);
|