enigma-memory 0.1.15 → 0.1.17
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/README.md +70 -86
- package/apps/cli/bin/enigma.mjs +152 -15
- package/apps/native-host/README.md +20 -0
- package/deploy/SIMULATION.md +34 -38
- package/docs/benchmark-attestation-network.md +2 -2
- package/docs/benchmark-reproducibility.md +70 -9
- package/docs/blockchain-only-mechanisms.md +12 -0
- package/docs/browser-extension-install.md +9 -6
- package/docs/client-connectors.md +29 -55
- package/docs/demo-proof-network.md +3 -3
- package/docs/developer-ecosystem.md +207 -223
- package/docs/developer-proof-quickstart.md +3 -3
- package/docs/enigma-memory-ready-conformance.md +1 -1
- package/docs/hosted-cloud-product.md +31 -0
- package/docs/install-anywhere.md +68 -70
- package/docs/memory-benchmarks.md +21 -3
- package/docs/memory-drive-health-model.md +41 -0
- package/docs/proof-network-build-notes.md +2 -2
- package/docs/proof-network.md +90 -8
- package/docs/sdk-api.md +1 -1
- package/docs/solana-devnet-acceptance.md +1 -1
- package/docs/solana-proof-rail.md +1 -1
- package/package.json +7 -1
- package/packages/connectors/src/index.js +13 -0
- package/packages/hosted-cloud/src/index.js +470 -2
- package/packages/mcp-server/README.md +22 -0
- package/packages/mcp-server/src/index.js +1 -1
- package/packages/passport/src/index.js +730 -0
- package/packages/proof-network/src/index.js +139 -0
- package/scripts/build-benchmark-proof-release.mjs +119 -6
- package/scripts/build-cloudflare-token-policy.mjs +6 -2
- package/scripts/build-hosted-api-key-lifecycle.mjs +1 -1
- package/scripts/build-hosted-customer-lifecycle.mjs +23 -3
- package/scripts/build-installer-assets.mjs +1 -1
- package/scripts/build-production-handoff-packet.mjs +1 -1
- package/scripts/build-production-unblocker.mjs +409 -409
- package/scripts/build-production-workplan.mjs +3 -1
- package/scripts/build-proof-network-packet.mjs +1 -1
- package/scripts/check.mjs +3 -1
- package/scripts/cloudflare-ops.mjs +35 -0
- package/scripts/collect-hosted-backend-live-evidence.mjs +44 -2
- package/scripts/run-memory-benchmarks.mjs +5 -0
- package/scripts/run-standard-memory-benchmarks.mjs +287 -5
- package/scripts/stage-cloudflare-pages-artifact.mjs +145 -0
package/README.md
CHANGED
|
@@ -2,112 +2,48 @@
|
|
|
2
2
|
|
|
3
3
|
Enigma Memory is a local-first AI Memory Passport. It gives you one canonical local memory vault, receipt-backed proof for Enigma-controlled events, and MCP connections into assistants such as Claude Desktop, Cursor, Kimi Code, and generic MCP clients.
|
|
4
4
|
|
|
5
|
-
Start with
|
|
5
|
+
Start with the Memory Drive loop below — one install, one connect, then health and status verification. You do not need to understand relay, gateway, mesh, browser, desktop, hosted infrastructure, or the proof network before trying Enigma locally.
|
|
6
6
|
|
|
7
7
|
The package path needs no database, provider credential, cloud credential, npm publishing token, or external account. Hosted cloud and BYOC operation are separate advanced modes that require real deployment credentials, domains, TLS, durable storage, KMS/secrets, monitoring, backups, and operator acceptance.
|
|
8
8
|
|
|
9
9
|
Enigma does not claim that a closed provider deleted internal data, that model weights forgot, or that provider-native memory disappeared. It proves facts about Enigma-controlled vault state, receipts, checkpoints, and declared boundary operations.
|
|
10
10
|
|
|
11
|
-
## First run: install and inspect the safe local plan
|
|
12
|
-
|
|
13
|
-
The top public path is the published npm package plus the first-run planner:
|
|
14
|
-
|
|
15
|
-
```sh
|
|
16
|
-
npm install -g enigma-memory
|
|
17
|
-
enigma init --dry-run
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
`enigma init --dry-run` prints a public-safe JSON plan with exact next commands. It does not write local artifacts or third-party client configs, does not require provider credentials, hosted Enigma SaaS, Solana, or a browser extension, and states `provider_credentials_required:false`, `hosted_saas_live:false`, and `raw_memory_printed:false`.
|
|
21
|
-
|
|
22
|
-
When the plan looks right, create the local Memory Passport artifacts:
|
|
23
|
-
|
|
24
|
-
```sh
|
|
25
|
-
enigma init --overwrite
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
The execute path writes only local Enigma artifacts under the workspace `.enigma` path by default. It still does not write Claude, Cursor, Kimi, or other third-party app configs unless you explicitly add `--connect`.
|
|
29
|
-
|
|
30
|
-
Public demo path, preserved for reviewers who want the isolated proof/demo bundle:
|
|
31
|
-
|
|
32
|
-
```sh
|
|
33
|
-
enigma test-drive --overwrite
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
`enigma test-drive --overwrite` needs no database, provider API key, cloud credential, npm publishing token, package registry account, or hosted Enigma account. It runs entirely on the local filesystem, does not call OpenAI, Anthropic, Cloudflare, hosted Enigma, or other external providers, and does not write Claude, Cursor, Kimi, or other third-party client configs.
|
|
37
|
-
|
|
38
|
-
By default the command writes an isolated demo under `.enigma/test-drive`. Use `--dry-run` to print the plan without writing, or `--out-dir <path>` to choose another isolated demo directory. The default bundle and proof artifacts live inside that directory.
|
|
39
|
-
|
|
40
|
-
The test-drive output is a single public-safe JSON summary. It points to the local setup artifacts, search/status output, cross-model demo report, and benchmark pointers, and it prints exact next commands for testers without printing raw private memory plaintext. The proof is bounded to the local demo: Enigma-controlled vault state, receipts, checkpoints, committed roots, exported bundle shape, and declared boundary operations. It is not evidence that hosted SaaS is live, that a closed provider deleted internal data, that model weights forgot, or that provider-native memory disappeared.
|
|
41
|
-
|
|
42
|
-
When you are ready to connect real local clients that are already installed or already configured:
|
|
43
|
-
|
|
44
|
-
```sh
|
|
45
|
-
enigma init --connect --overwrite
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
`--connect` is the explicit first-run client-config write path. With default client selection it skips missing client configs instead of creating every default client config.
|
|
49
|
-
|
|
50
|
-
## Enigma Proof Network
|
|
51
|
-
|
|
52
|
-
Enigma Proof Network is the public proof layer for AI memory: local tools can package privacy-preserving roots, refs, counts, signatures, scoped capability grants, revocations, and benchmark attestations without exposing raw memory, prompts, transcripts, completions, embeddings, tenant names, private keys, provider responses, or provider credentials.
|
|
53
|
-
|
|
54
|
-
The `enigma chain anchor|grant|revoke|attest|verify` commands are local planning and verification commands. They emit public-safe JSON with `transaction_submitted:false` and `raw_memory_on_chain:false`; they do not submit Solana transactions, deploy hosted SaaS, create accounts, or call external providers.
|
|
55
|
-
|
|
56
|
-
Start with the category narrative in [`docs/market-category-narrative.md`](docs/market-category-narrative.md), then read the technical overview in [`docs/proof-network.md`](docs/proof-network.md), use its [Solana role](docs/proof-network.md#solana-role) section for the Solana-ready anchoring boundary, and read [`docs/proof-network-faq.md`](docs/proof-network-faq.md) for claim boundaries.
|
|
57
|
-
|
|
58
11
|
## Install once, use everywhere
|
|
59
12
|
|
|
60
13
|
Prerequisites:
|
|
61
14
|
|
|
62
15
|
- Node.js `>=24`
|
|
63
|
-
- No database, package registry account, provider credential,
|
|
16
|
+
- No database, package registry account, provider credential, cloud credential, hosted Enigma account, or external account for the local setup path
|
|
64
17
|
- Git only when you choose the advanced source-checkout path
|
|
65
18
|
|
|
66
|
-
Use Enigma as
|
|
19
|
+
Use Enigma as your private Memory Drive for AI: install once, connect the AI clients you already have, then inspect drive health and passport status before remembering, retrieving, packaging, and verifying context.
|
|
67
20
|
|
|
68
21
|
```sh
|
|
69
22
|
npm install -g enigma-memory
|
|
70
|
-
enigma init
|
|
71
|
-
enigma
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
```sh
|
|
79
|
-
enigma init --client auto --dry-run
|
|
23
|
+
enigma init
|
|
24
|
+
enigma setup --client auto --connect-installed --overwrite
|
|
25
|
+
enigma drive health
|
|
26
|
+
enigma status
|
|
27
|
+
enigma remember --text-file ./memory.txt
|
|
28
|
+
enigma search --query "project context"
|
|
29
|
+
enigma context --query "project context" --optimize
|
|
30
|
+
enigma verify --export ./.enigma/export.json
|
|
80
31
|
```
|
|
81
32
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
Only when you are ready to explicitly write connector entries for installed/config-present clients:
|
|
85
|
-
|
|
86
|
-
```sh
|
|
87
|
-
enigma init --connect --overwrite
|
|
88
|
-
```
|
|
33
|
+
`enigma init` is the credential-free first run: it creates the local `.enigma` workspace, bundle, and proof artifacts and emits public-safe JSON without provider or cloud credentials. `enigma setup --client auto --connect-installed --overwrite` then configures the drive and merges the `mcpServers.enigma` entry into every installed/config-present client it detects (Claude Desktop, Cursor, Kimi Code, VS Code/Cline, Roo, OpenCode, generic MCP). It skips clients that are not installed and never creates configs from scratch; preview with `--dry-run` first if you want to see which clients will be written. The same commands run on Windows PowerShell because the npm global install adds an `enigma.cmd` shim. Neither command prints raw memory plaintext.
|
|
89
34
|
|
|
90
|
-
|
|
35
|
+
`enigma drive health` reports a SMART-style memory-drive health packet — freshness, duplicate rate, tombstone backlog, stale derived artifacts, receipt coverage, and connector health — computed from local metadata only, with no network calls or private payloads. It is part of the Memory Drive surface in this release; in any build where it is unavailable, `enigma status` and `enigma doctor` cover the same local passport counts, roots, and connector readiness.
|
|
91
36
|
|
|
92
|
-
|
|
37
|
+
`enigma status` shows passport counts, roots, owner display fields, and connector readiness. The local Enigma vault remains canonical. Provider-native memory is non-canonical and should be treated as a convenience cache only. Enigma receipts prove Enigma-controlled vault state, receipts, checkpoints, and declared boundary operations; they do not prove provider deletion, provider model forgetting, provider-native memory removal, hosted availability, ROI/savings, or compliance certification.
|
|
93
38
|
|
|
94
|
-
|
|
95
|
-
enigma remember --text-file ./memory.txt
|
|
96
|
-
enigma search --query "..."
|
|
97
|
-
enigma context --query "..." --optimize
|
|
98
|
-
enigma verify --export ./.enigma/export.json
|
|
99
|
-
enigma connect claude-desktop --dry-run
|
|
100
|
-
```
|
|
39
|
+
## Enigma Proof Network
|
|
101
40
|
|
|
102
|
-
|
|
41
|
+
Enigma Proof Network is the public proof layer for AI memory: local tools can package privacy-preserving roots, refs, counts, signatures, scoped capability grants, revocations, and benchmark attestations without exposing raw memory, prompts, transcripts, completions, embeddings, tenant names, private keys, provider responses, or provider credentials.
|
|
103
42
|
|
|
104
|
-
The
|
|
43
|
+
The `enigma chain anchor|grant|revoke|attest|verify` commands are local planning and verification commands. They emit public-safe JSON with `transaction_submitted:false` and `raw_memory_on_chain:false`; they do not submit Solana transactions, deploy hosted SaaS, create accounts, or call external providers.
|
|
105
44
|
|
|
106
|
-
|
|
45
|
+
Start with the category narrative in [`docs/market-category-narrative.md`](docs/market-category-narrative.md), then read the technical overview in [`docs/proof-network.md`](docs/proof-network.md), use its [Solana role](docs/proof-network.md#solana-role) section for the Solana-ready anchoring boundary, and read [`docs/proof-network-faq.md`](docs/proof-network-faq.md) for claim boundaries.
|
|
107
46
|
|
|
108
|
-
```sh
|
|
109
|
-
npx --yes --package enigma-memory enigma test-drive --overwrite
|
|
110
|
-
```
|
|
111
47
|
|
|
112
48
|
## Advanced/source-only path
|
|
113
49
|
|
|
@@ -214,7 +150,7 @@ Supported connector profiles are:
|
|
|
214
150
|
- `opencode`
|
|
215
151
|
- `generic-mcp`
|
|
216
152
|
|
|
217
|
-
|
|
153
|
+
The one-command path above (`enigma setup --client auto --connect-installed --overwrite`) already connects every installed/config-present client. To connect or preview a single client instead:
|
|
218
154
|
|
|
219
155
|
```sh
|
|
220
156
|
enigma connect claude-desktop --dry-run
|
|
@@ -223,11 +159,59 @@ enigma connect kimi-code --dry-run
|
|
|
223
159
|
enigma connect generic-mcp --dry-run
|
|
224
160
|
```
|
|
225
161
|
|
|
226
|
-
|
|
162
|
+
Remove `--dry-run` from `enigma connect <client>` only after you are ready for Enigma to merge the `mcpServers.enigma` entry into that specific client config. Bare `enigma setup` without `--connect-installed` or `--write-connectors` never writes third-party app configs.
|
|
227
163
|
|
|
228
|
-
|
|
164
|
+
Copy-paste MCP snippets:
|
|
165
|
+
|
|
166
|
+
Claude Desktop:
|
|
167
|
+
|
|
168
|
+
```json
|
|
169
|
+
{
|
|
170
|
+
"mcpServers": {
|
|
171
|
+
"enigma": {
|
|
172
|
+
"command": "enigma-mcp",
|
|
173
|
+
"args": [],
|
|
174
|
+
"env": {
|
|
175
|
+
"ENIGMA_BUNDLE": "/absolute/path/to/.enigma/bundle.json"
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Cursor:
|
|
183
|
+
|
|
184
|
+
```json
|
|
185
|
+
{
|
|
186
|
+
"mcpServers": {
|
|
187
|
+
"enigma": {
|
|
188
|
+
"command": "enigma-mcp",
|
|
189
|
+
"args": [],
|
|
190
|
+
"env": {
|
|
191
|
+
"ENIGMA_BUNDLE": "/absolute/path/to/.enigma/bundle.json"
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Kimi Code:
|
|
199
|
+
|
|
200
|
+
```json
|
|
201
|
+
{
|
|
202
|
+
"mcpServers": {
|
|
203
|
+
"enigma": {
|
|
204
|
+
"command": "enigma-mcp",
|
|
205
|
+
"args": [],
|
|
206
|
+
"env": {
|
|
207
|
+
"ENIGMA_BUNDLE": "/absolute/path/to/.enigma/bundle.json"
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
```
|
|
229
213
|
|
|
230
|
-
|
|
214
|
+
Generic MCP:
|
|
231
215
|
|
|
232
216
|
```json
|
|
233
217
|
{
|
package/apps/cli/bin/enigma.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { access, mkdir, readFile, readdir, stat, writeFile } from 'node:fs/promi
|
|
|
6
6
|
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
7
7
|
import { pathToFileURL } from 'node:url';
|
|
8
8
|
import { createVault, remember, recall, updateMemory, deleteMemory, exportBundle } from '../../../packages/vault/src/index.js';
|
|
9
|
-
import { createPassport, compileContextPack } from '../../../packages/passport/src/index.js';
|
|
9
|
+
import { createPassport, compileContextPack, createMemoryDriveHealthReport } from '../../../packages/passport/src/index.js';
|
|
10
10
|
import { runBoundarySimulation } from '../../../packages/boundary/src/index.js';
|
|
11
11
|
import { startStdioServer } from '../../../packages/mcp-server/src/index.js';
|
|
12
12
|
import { runMeshDemo } from '../../../packages/mesh/src/index.js';
|
|
@@ -36,11 +36,15 @@ import {
|
|
|
36
36
|
createCapabilityGrant,
|
|
37
37
|
createCapabilityRevocation,
|
|
38
38
|
createProofNetworkAnchorBatch,
|
|
39
|
+
createRegistryEntry,
|
|
40
|
+
createRegistryBatch,
|
|
39
41
|
sha256Json as proofNetworkSha256Json,
|
|
40
42
|
validateBenchmarkAttestation,
|
|
41
43
|
validateCapabilityGrant,
|
|
42
44
|
validateCapabilityRevocation,
|
|
43
45
|
validateProofNetworkAnchorBatch,
|
|
46
|
+
validateRegistryEntry,
|
|
47
|
+
validateRegistryBatch,
|
|
44
48
|
validateProofNetworkPacket,
|
|
45
49
|
} from '../../../packages/proof-network/src/index.js';
|
|
46
50
|
|
|
@@ -53,7 +57,7 @@ export const DEFAULT_GATEWAY_PORT = 8797;
|
|
|
53
57
|
const DEFAULT_QUICKSTART_MEMORY = 'Enigma quickstart demo memory: local proof bundles can be created and verified without provider or cloud credentials.';
|
|
54
58
|
const DEFAULT_CROSS_MODEL_DEMO_BUNDLE = '.enigma/cross-model-demo-bundle.json';
|
|
55
59
|
const DEFAULT_CROSS_MODEL_MEMORY = 'Enigma cross-model demo memory: a local encrypted memory can be packaged for ChatGPT, Claude, Kimi, Cursor, and a local LLM without provider credentials.';
|
|
56
|
-
const DEFAULT_SETUP_CLIENTS = Object.freeze(['generic-mcp', 'claude-desktop', 'cursor', 'kimi-code']);
|
|
60
|
+
const DEFAULT_SETUP_CLIENTS = Object.freeze(['generic-mcp', 'claude-desktop', 'cursor', 'kimi-code', 'vscode-cline']);
|
|
57
61
|
const SETUP_CLAIM_BOUNDARIES = Object.freeze({
|
|
58
62
|
local_only: true,
|
|
59
63
|
provider_credentials_required: false,
|
|
@@ -1043,15 +1047,32 @@ function publicSetupError(error, rawDisplays, publicDisplays) {
|
|
|
1043
1047
|
return new Error(message);
|
|
1044
1048
|
}
|
|
1045
1049
|
|
|
1050
|
+
function oneCommandInstallConnect(bundleDisplay = DEFAULT_BUNDLE, outDirDisplay = dirname(bundleDisplay)) {
|
|
1051
|
+
const parts = ['npm install -g enigma-memory && enigma setup'];
|
|
1052
|
+
if (bundleDisplay !== DEFAULT_BUNDLE) parts.push(`--bundle ${commandPath(bundleDisplay)}`);
|
|
1053
|
+
if (outDirDisplay !== dirname(bundleDisplay)) parts.push(`--out-dir ${commandPath(outDirDisplay)}`);
|
|
1054
|
+
const base = parts.join(' ');
|
|
1055
|
+
return {
|
|
1056
|
+
installed_clients: `${base} --client auto --connect-installed --overwrite`,
|
|
1057
|
+
claude_desktop: `${base} --client claude-desktop --write-connectors --overwrite`,
|
|
1058
|
+
cursor: `${base} --client cursor --write-connectors --overwrite`,
|
|
1059
|
+
kimi_code: `${base} --client kimi-code --write-connectors --overwrite`,
|
|
1060
|
+
vscode_cline: `${base} --client vscode-cline --write-connectors --overwrite`,
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1046
1064
|
function setupNextCommands(bundleInput, exportDisplay, clients, writeConnectors) {
|
|
1047
1065
|
const primaryClient = clients[0] ?? DEFAULT_SETUP_CLIENTS[0];
|
|
1066
|
+
const bundle = commandPath(bundleInput);
|
|
1048
1067
|
const commands = [
|
|
1049
|
-
`enigma
|
|
1050
|
-
`enigma
|
|
1051
|
-
`enigma
|
|
1068
|
+
`enigma status --bundle ${bundle}`,
|
|
1069
|
+
`enigma drive health --bundle ${bundle}`,
|
|
1070
|
+
`enigma remember --bundle ${bundle} --text-file ./memory.txt`,
|
|
1071
|
+
`enigma search --bundle ${bundle} --query "project context"`,
|
|
1072
|
+
`enigma context --bundle ${bundle} --query "project context"`,
|
|
1052
1073
|
`enigma verify --export ${commandPath(exportDisplay)}`,
|
|
1053
1074
|
];
|
|
1054
|
-
if (!writeConnectors) commands.push(`enigma connect ${primaryClient} --bundle ${
|
|
1075
|
+
if (!writeConnectors) commands.push(`enigma connect ${primaryClient} --bundle ${bundle} --dry-run`);
|
|
1055
1076
|
return commands;
|
|
1056
1077
|
}
|
|
1057
1078
|
|
|
@@ -1072,10 +1093,13 @@ function initNextCommands({ dryRun, bundleDisplay, outDirDisplay, exportDisplay,
|
|
|
1072
1093
|
|
|
1073
1094
|
function doctorNextCommands(bundleDisplay, client) {
|
|
1074
1095
|
const clientId = client ?? DEFAULT_SETUP_CLIENTS[0];
|
|
1096
|
+
const bundle = commandPath(bundleDisplay);
|
|
1075
1097
|
return [
|
|
1076
|
-
`enigma
|
|
1077
|
-
`enigma
|
|
1078
|
-
`enigma
|
|
1098
|
+
`enigma status --bundle ${bundle}`,
|
|
1099
|
+
`enigma drive health --bundle ${bundle}`,
|
|
1100
|
+
`enigma setup --bundle ${bundle}`,
|
|
1101
|
+
`enigma doctor --bundle ${bundle} --client ${clientId}`,
|
|
1102
|
+
`enigma connect ${clientId} --bundle ${bundle}`,
|
|
1079
1103
|
];
|
|
1080
1104
|
}
|
|
1081
1105
|
|
|
@@ -1297,6 +1321,7 @@ export async function setupCommand(flags, io) {
|
|
|
1297
1321
|
client_selection: publicSetupClientSelection(selection),
|
|
1298
1322
|
connector_write_skips: connectorWriteSkips(connectors),
|
|
1299
1323
|
connectors,
|
|
1324
|
+
one_command_install_connect: oneCommandInstallConnect(displays.bundle, publicPathDisplay(outDirInput, 'out-dir')),
|
|
1300
1325
|
mcp_config_snippets: Object.fromEntries(connectors.map((connector) => [connector.client_id, connector.mcp_config_snippet])),
|
|
1301
1326
|
connect_plans: Object.fromEntries(connectors.map((connector) => [connector.client_id, connector.connect_plan])),
|
|
1302
1327
|
next_commands: setupNextCommands(displays.bundle, displays.export, clients, connectorWritesRequested && (!connectInstalled || anyConnectorWritePerformed)),
|
|
@@ -1675,6 +1700,27 @@ async function statusCommand(flags, io) {
|
|
|
1675
1700
|
print(passportStatusReport({ bundlePath, stored, vault, passport }), io);
|
|
1676
1701
|
return 0;
|
|
1677
1702
|
}
|
|
1703
|
+
async function readOptionalJsonInput(flags, names) {
|
|
1704
|
+
const path = getFlag(flags, names);
|
|
1705
|
+
if (!path) return null;
|
|
1706
|
+
if (!(await fileExists(path))) return null;
|
|
1707
|
+
return readJson(path);
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
async function driveHealthCommand(flags, io) {
|
|
1711
|
+
const bundlePath = resolve(String(getFlag(flags, ['bundle', 'file'], DEFAULT_BUNDLE)));
|
|
1712
|
+
const { vault, passport } = await loadState(bundlePath);
|
|
1713
|
+
const benchmarkSummary = await readOptionalJsonInput(flags, ['benchmark-summary', 'benchmarkSummary']);
|
|
1714
|
+
const connectorSummary = await readOptionalJsonInput(flags, ['connector-summary', 'connectorSummary']);
|
|
1715
|
+
const replicas = await readOptionalJsonInput(flags, ['replicas']);
|
|
1716
|
+
const latestAnchorBatchRef = getFlag(flags, ['latest-anchor-batch-ref', 'latestAnchorBatchRef']);
|
|
1717
|
+
const now = getFlag(flags, ['now'], '2026-06-25T00:00:00.000Z');
|
|
1718
|
+
const report = createMemoryDriveHealthReport({ vault, passport, benchmarkSummary, connectorSummary, replicas, latestAnchorBatchRef, now });
|
|
1719
|
+
const outPath = getFlag(flags, ['out']);
|
|
1720
|
+
if (outPath) await writeJson(outPath, report);
|
|
1721
|
+
print(report, io);
|
|
1722
|
+
return 0;
|
|
1723
|
+
}
|
|
1678
1724
|
|
|
1679
1725
|
function testDrivePathDisplay(outDirInput, name) {
|
|
1680
1726
|
return isAbsolute(outDirInput) ? join(outDirInput, name) : quickstartPathDisplay(outDirInput, name);
|
|
@@ -1731,6 +1777,7 @@ function testDriveNextCommands(bundleDisplay, crossModelReportDisplay) {
|
|
|
1731
1777
|
const quotedReport = commandPath(crossModelReportDisplay);
|
|
1732
1778
|
return [
|
|
1733
1779
|
`enigma status --bundle ${quotedBundle}`,
|
|
1780
|
+
`enigma drive health --bundle ${quotedBundle}`,
|
|
1734
1781
|
`enigma search --bundle ${quotedBundle} --query "local proof bundle"`,
|
|
1735
1782
|
`enigma demo cross-model --bundle ${quotedBundle} --out ${quotedReport}`,
|
|
1736
1783
|
'enigma setup --overwrite',
|
|
@@ -1745,6 +1792,7 @@ function testDriveFlowCommands({ bundleDisplay, outDirInput, crossModelReportDis
|
|
|
1745
1792
|
return [
|
|
1746
1793
|
`enigma quickstart --bundle ${quotedBundle} --out-dir ${quotedOutDir}${overwriteSuffix}`,
|
|
1747
1794
|
`enigma status --bundle ${quotedBundle}`,
|
|
1795
|
+
`enigma drive health --bundle ${quotedBundle}`,
|
|
1748
1796
|
`enigma search --bundle ${quotedBundle} --query "local proof bundle"`,
|
|
1749
1797
|
`enigma demo cross-model --bundle ${quotedBundle} --out ${quotedReport}`,
|
|
1750
1798
|
];
|
|
@@ -1938,7 +1986,7 @@ export async function testDriveCommand(flags, io) {
|
|
|
1938
1986
|
out_dir: outDirInput,
|
|
1939
1987
|
bundle: bundleInput,
|
|
1940
1988
|
install_command: `npm install -g ${packageJson.name ?? 'enigma-memory'}`,
|
|
1941
|
-
release_target: '0.1.
|
|
1989
|
+
release_target: '0.1.17',
|
|
1942
1990
|
artifacts_written: !dryRun,
|
|
1943
1991
|
client_configs_written: false,
|
|
1944
1992
|
client_config_write_required: false,
|
|
@@ -2122,6 +2170,7 @@ export async function installCommand(flags, io) {
|
|
|
2122
2170
|
mcp_command: connectorOptions(flags).mcpCommand ?? 'enigma-mcp',
|
|
2123
2171
|
clients: profiles,
|
|
2124
2172
|
mcp_config_snippets: snippets,
|
|
2173
|
+
one_command_install_connect: oneCommandInstallConnect(bundlePath, dirname(bundlePath)),
|
|
2125
2174
|
out: out && out !== true ? resolve(String(out)) : undefined,
|
|
2126
2175
|
}, io);
|
|
2127
2176
|
return 0;
|
|
@@ -2389,6 +2438,8 @@ function chainArtifactValidator(artifact) {
|
|
|
2389
2438
|
if (schema === 'enigma.proof_network.capability_revocation.v1') return [schema, validateCapabilityRevocation];
|
|
2390
2439
|
if (schema === 'enigma.proof_network.benchmark_attestation.v1') return [schema, validateBenchmarkAttestation];
|
|
2391
2440
|
if (schema === 'enigma.proof_network.packet.v1') return [schema, validateProofNetworkPacket];
|
|
2441
|
+
if (schema === 'enigma.proof_network.registry_entry.v1') return [schema, validateRegistryEntry];
|
|
2442
|
+
if (schema === 'enigma.proof_network.registry_batch.v1') return [schema, validateRegistryBatch];
|
|
2392
2443
|
throw new Error(schema ? 'Unsupported proof-network artifact schema.' : 'Unsupported proof-network artifact schema: missing.');
|
|
2393
2444
|
}
|
|
2394
2445
|
|
|
@@ -2445,11 +2496,11 @@ async function readSolanaKeypair(path) {
|
|
|
2445
2496
|
} catch {
|
|
2446
2497
|
throw new Error('Unable to read a valid Solana --keypair JSON array.');
|
|
2447
2498
|
}
|
|
2448
|
-
if (!Array.isArray(parsed) || parsed.length
|
|
2499
|
+
if (!Array.isArray(parsed) || parsed.length !== 64) throw new Error('Solana --keypair must be a JSON array of 64 secret-key bytes.');
|
|
2449
2500
|
const bytes = new Uint8Array(parsed.length);
|
|
2450
2501
|
for (let i = 0; i < parsed.length; i += 1) {
|
|
2451
2502
|
const value = parsed[i];
|
|
2452
|
-
if (!Number.isInteger(value) || value < 0 || value > 255) throw new Error('Solana --keypair must be a JSON array of secret-key bytes.');
|
|
2503
|
+
if (!Number.isInteger(value) || value < 0 || value > 255) throw new Error('Solana --keypair must be a JSON array of 64 secret-key bytes.');
|
|
2453
2504
|
bytes[i] = value;
|
|
2454
2505
|
}
|
|
2455
2506
|
return bytes;
|
|
@@ -2686,6 +2737,60 @@ export async function chainVerifyCommand(flags, io, positionalFile = undefined)
|
|
|
2686
2737
|
return result.ok === true ? 0 : 1;
|
|
2687
2738
|
}
|
|
2688
2739
|
|
|
2740
|
+
export async function chainRegisterCommand(flags, io) {
|
|
2741
|
+
const entryType = requireFlag(flags, ['entry-type', 'entryType', 'type'], 'entry-type');
|
|
2742
|
+
const artifactHashFlag = getFlag(flags, ['artifact-hash', 'artifactHash', 'digest-ref', 'digestRef', 'artifact-ref', 'artifactRef']);
|
|
2743
|
+
const artifactFile = getFlag(flags, ['artifact-file', 'artifactFile']);
|
|
2744
|
+
const resolvedArtifactHash = artifactHashFlag || (artifactFile ? await sha256PublicFile(resolve(String(artifactFile))) : undefined);
|
|
2745
|
+
if (!resolvedArtifactHash) throw new Error('Missing required --artifact-hash or --artifact-file.');
|
|
2746
|
+
const digestRefs = flagValues(flags, ['digest-ref', 'digest-refs', 'digestRef', 'digestRefs', 'root', 'roots']);
|
|
2747
|
+
const entry = createRegistryEntry({
|
|
2748
|
+
entry_type: entryType,
|
|
2749
|
+
artifact_hash: resolvedArtifactHash,
|
|
2750
|
+
artifact_schema_ref: requireFlag(flags, ['artifact-schema-ref', 'artifactSchemaRef', 'schema-ref', 'schemaRef'], 'artifact-schema-ref'),
|
|
2751
|
+
digest_refs: digestRefs.length ? digestRefs : resolvedArtifactHash,
|
|
2752
|
+
signer_refs: flagValues(flags, ['signer', 'signers', 'signer-ref', 'signerRef', 'signer-refs', 'signerRefs', 'attestor', 'attestor-ref', 'attestorRef']),
|
|
2753
|
+
registry_ref: getFlag(flags, ['registry-ref', 'registryRef', 'marketplace-ref', 'marketplaceRef']),
|
|
2754
|
+
entry_ref: getFlag(flags, ['entry-ref', 'entryRef']),
|
|
2755
|
+
entry_count: getFlag(flags, ['entry-count', 'entryCount', 'count']),
|
|
2756
|
+
created_at: getFlag(flags, ['created-at', 'createdAt', 'registered-at', 'registeredAt']),
|
|
2757
|
+
transaction_submitted: false,
|
|
2758
|
+
raw_memory_on_chain: false,
|
|
2759
|
+
});
|
|
2760
|
+
assertChainArtifact(validateRegistryEntry, entry);
|
|
2761
|
+
return chainWriteOrPrint(flags, io, entry, {
|
|
2762
|
+
artifact_type: entry.schema,
|
|
2763
|
+
registry_entry_id: entry.registry_entry_id,
|
|
2764
|
+
registry_entry_hash: entry.registry_entry_hash ?? proofNetworkSha256Json(entry),
|
|
2765
|
+
});
|
|
2766
|
+
}
|
|
2767
|
+
|
|
2768
|
+
export async function chainRegistryCommand(flags, io, positionalFile = undefined) {
|
|
2769
|
+
const entryPaths = flagValues(flags, ['entry', 'entries', 'entry-file', 'entryFile']);
|
|
2770
|
+
const positionalEntry = positionalFile && !String(positionalFile).startsWith('--') ? positionalFile : undefined;
|
|
2771
|
+
const paths = entryPaths.length ? entryPaths : (positionalEntry ? [positionalEntry] : []);
|
|
2772
|
+
if (paths.length === 0) throw new Error('Missing required --entry <registry-entry.json>.');
|
|
2773
|
+
const entries = [];
|
|
2774
|
+
for (const entryPath of paths) {
|
|
2775
|
+
const entry = await readJson(resolve(entryPath));
|
|
2776
|
+
assertNoPrivateProofPayload(entry);
|
|
2777
|
+
entries.push(entry);
|
|
2778
|
+
}
|
|
2779
|
+
const batch = createRegistryBatch({
|
|
2780
|
+
entries,
|
|
2781
|
+
registry_ref: getFlag(flags, ['registry-ref', 'registryRef', 'marketplace-ref', 'marketplaceRef']),
|
|
2782
|
+
created_at: getFlag(flags, ['created-at', 'createdAt']),
|
|
2783
|
+
transaction_submitted: false,
|
|
2784
|
+
raw_memory_on_chain: false,
|
|
2785
|
+
});
|
|
2786
|
+
assertChainArtifact(validateRegistryBatch, batch);
|
|
2787
|
+
return chainWriteOrPrint(flags, io, batch, {
|
|
2788
|
+
artifact_type: batch.schema,
|
|
2789
|
+
registry_batch_id: batch.registry_batch_id,
|
|
2790
|
+
registry_batch_hash: batch.registry_batch_hash ?? proofNetworkSha256Json(batch),
|
|
2791
|
+
});
|
|
2792
|
+
}
|
|
2793
|
+
|
|
2689
2794
|
|
|
2690
2795
|
export async function meterEventCommand(flags, io) {
|
|
2691
2796
|
const event = createUsageEvent({
|
|
@@ -2911,6 +3016,9 @@ function usage() {
|
|
|
2911
3016
|
'chain attest',
|
|
2912
3017
|
'chain verify',
|
|
2913
3018
|
'chain submit-solana',
|
|
3019
|
+
'chain register',
|
|
3020
|
+
'chain registry',
|
|
3021
|
+
'drive health',
|
|
2914
3022
|
],
|
|
2915
3023
|
connector_options: {
|
|
2916
3024
|
'--bundle <path>': 'Absolute local Enigma vault bundle path rendered as ENIGMA_BUNDLE.',
|
|
@@ -2955,6 +3063,15 @@ function usage() {
|
|
|
2955
3063
|
'--dry-run': 'Plan setup without writing local artifacts or client configs.',
|
|
2956
3064
|
'--write-connectors': 'Also write selected client MCP config files. Defaults to false.',
|
|
2957
3065
|
},
|
|
3066
|
+
install_options: {
|
|
3067
|
+
'one-command installed clients': 'npm install -g enigma-memory && enigma setup --client auto --connect-installed --overwrite',
|
|
3068
|
+
'one-command Claude Desktop': 'npm install -g enigma-memory && enigma setup --client claude-desktop --write-connectors --overwrite',
|
|
3069
|
+
'one-command Cursor': 'npm install -g enigma-memory && enigma setup --client cursor --write-connectors --overwrite',
|
|
3070
|
+
'one-command Kimi Code': 'npm install -g enigma-memory && enigma setup --client kimi-code --write-connectors --overwrite',
|
|
3071
|
+
'one-command VS Code Cline': 'npm install -g enigma-memory && enigma setup --client vscode-cline --write-connectors --overwrite',
|
|
3072
|
+
'--client <id>': 'Limit generated MCP snippets to one supported client.',
|
|
3073
|
+
'--out <path>': 'Write generated MCP snippets to a JSON file for review without hand-editing client config JSON.',
|
|
3074
|
+
},
|
|
2958
3075
|
quickstart_options: {
|
|
2959
3076
|
'--bundle <path>': 'Bundle JSON to create. Defaults to .enigma/bundle.json.',
|
|
2960
3077
|
'--out-dir <path>': 'Directory for context-pack.json, export.json, and verify-report.json. Defaults to the bundle directory.',
|
|
@@ -3022,9 +3139,26 @@ function usage() {
|
|
|
3022
3139
|
revoke: 'enigma chain revoke --grant-hash <sha256:...> --reason <public-reason-code> [--revocation-ref <public-ref>] [--out <file>]',
|
|
3023
3140
|
attest: 'enigma chain attest (--report-hash <sha256:...> | --report-file <report.json>) --dataset-ref <sha256:...> --runner-ref <public-runner-ref> --package-ref <public-package-ref> [--score name=value] [--out <file>]',
|
|
3024
3141
|
verify: 'enigma chain verify --file <proof-artifact.json>',
|
|
3025
|
-
|
|
3142
|
+
register: 'enigma chain register --entry-type <anchor_batch|benchmark_attestation|connector_conformance|health_report|operator_receipt|settlement_job> (--artifact-hash <sha256:...> | --artifact-file <artifact.json>) --artifact-schema-ref <schema-id> [--digest-ref <sha256:...>] [--signer <public-ref>] [--registry-ref <public-ref>] [--entry-ref <public-ref>] [--entry-count <n>] [--out <file>]',
|
|
3143
|
+
registry: 'enigma chain registry --entry <registry-entry.json> [--entry <registry-entry.json>] [--registry-ref <public-ref>] [--out <file>]',
|
|
3144
|
+
submit_solana: 'enigma chain submit-solana --file <proof-artifact.json> --cluster <devnet|testnet|mainnet-beta|localnet> [--rpc <url>] [--execute --keypair <solana-cli-64-byte-keypair.json>]',
|
|
3026
3145
|
boundary: 'Proof Network chain commands default to local planning and dry-run validation. submit-solana only submits a Solana Memo transaction when --execute is passed; it carries compact public-safe commitment/ref JSON, never raw memory or artifact bodies.',
|
|
3027
3146
|
},
|
|
3147
|
+
memory_drive_health: {
|
|
3148
|
+
command: 'enigma drive health --bundle <path> [--now <iso>] [--benchmark-summary <path>] [--connector-summary <path>] [--replicas <path>] [--latest-anchor-batch-ref <ref>] [--out <file>]',
|
|
3149
|
+
schema: 'enigma.memory_drive_health_report.v1',
|
|
3150
|
+
output_shape: 'SMART-style report: overall_status/overall_score, ten metrics (freshness, duplicate_rate, tombstone_risk, stale_derived_artifacts, retrieval_hit_rate, token_reduction, leakage_scan, receipt_coverage, connector_health, sync_fork_risk), each with status/score/observed/thresholds/evidence_refs/recommended_actions, plus roots, privacy_boundaries, claim_boundaries, and a conservative proof_network_ready block.',
|
|
3151
|
+
options: {
|
|
3152
|
+
'--bundle <path>': 'Local Enigma vault bundle to inspect. Defaults to .enigma/bundle.json.',
|
|
3153
|
+
'--now <iso>': 'ISO-8601 timestamp used for age calculations. Defaults to a deterministic timestamp.',
|
|
3154
|
+
'--benchmark-summary <path>': 'Optional JSON file with public-safe retrieval probes (probe_count, top_k, hit_at_k, exact_coverage, abstention_correctness). Omit to default gracefully.',
|
|
3155
|
+
'--connector-summary <path>': 'Optional JSON file with public-safe connector health (connector_count, healthy_connector_count, lagging_connector_count, error_rate_24h, cursor_gap_count). Omit to default gracefully.',
|
|
3156
|
+
'--replicas <path>': 'Optional JSON file of replica root reports for sync fork risk.',
|
|
3157
|
+
'--latest-anchor-batch-ref <ref>': 'Optional public-safe proof-network anchor batch ref for receipt coverage.',
|
|
3158
|
+
'--out <path>': 'Write the JSON report to a file in addition to stdout.',
|
|
3159
|
+
},
|
|
3160
|
+
boundary: 'Computed locally from public-safe counters, roots, receipt metadata, tombstones, and derived/context-pack refs only. No network or chain calls; transaction_submitted and raw_memory_on_chain are always false. It is local operational evidence, not provider-deletion, model-forgetting, compliance, or live-chain-settlement proof.',
|
|
3161
|
+
},
|
|
3028
3162
|
relay_gateway_options: {
|
|
3029
3163
|
'--host <host>': 'Bind host. Defaults to 127.0.0.1.',
|
|
3030
3164
|
'--port <port>': `Bind port. Defaults to ${DEFAULT_RELAY_PORT} for relay and ${DEFAULT_GATEWAY_PORT} for gateway.`,
|
|
@@ -3051,10 +3185,10 @@ export async function main(argv = process.argv.slice(2), io = { stdout: process.
|
|
|
3051
3185
|
print(usage(), io);
|
|
3052
3186
|
return 0;
|
|
3053
3187
|
}
|
|
3054
|
-
const twoPartCommands = ['boundary', 'mcp', 'mesh', 'enterprise', 'capsule', 'relay', 'gateway', 'connect', 'disconnect', 'import', 'native-host', 'meter', 'settlement', 'chain', 'demo', 'passport'];
|
|
3188
|
+
const twoPartCommands = ['boundary', 'mcp', 'mesh', 'enterprise', 'capsule', 'relay', 'gateway', 'connect', 'disconnect', 'import', 'native-host', 'meter', 'settlement', 'chain', 'demo', 'passport', 'drive'];
|
|
3055
3189
|
const flags = parseArgs(twoPartCommands.includes(command) ? argv.slice(2) : argv.slice(1));
|
|
3056
3190
|
const positionalFile = optionalPositional(argv[2]);
|
|
3057
|
-
if ((command === 'chain' && (!subcommand || subcommand === '--help' || subcommand === '-h' || flags.has('help'))) || ((flags.has('help') || argv.includes('-h')) && (command === 'init' || command === 'setup' || command === 'test-drive' || command === 'search' || command === 'status' || (command === 'passport' && subcommand === 'status') || ((command === 'relay' || command === 'gateway') && (subcommand === 'serve' || subcommand === 'demo')) || (command === 'native-host' && (subcommand === 'manifest' || subcommand === 'install-plan')) || (command === 'demo' && subcommand === 'cross-model')))) {
|
|
3191
|
+
if ((command === 'chain' && (!subcommand || subcommand === '--help' || subcommand === '-h' || flags.has('help'))) || ((flags.has('help') || argv.includes('-h')) && (command === 'init' || command === 'setup' || command === 'test-drive' || command === 'search' || command === 'status' || (command === 'passport' && subcommand === 'status') || ((command === 'relay' || command === 'gateway') && (subcommand === 'serve' || subcommand === 'demo')) || (command === 'native-host' && (subcommand === 'manifest' || subcommand === 'install-plan')) || (command === 'demo' && subcommand === 'cross-model') || (command === 'drive' && subcommand === 'health')))) {
|
|
3058
3192
|
print(usage(), io);
|
|
3059
3193
|
return 0;
|
|
3060
3194
|
}
|
|
@@ -3076,6 +3210,7 @@ export async function main(argv = process.argv.slice(2), io = { stdout: process.
|
|
|
3076
3210
|
if (command === 'search') return await searchCommand(flags, io);
|
|
3077
3211
|
if (command === 'status') return await statusCommand(flags, io);
|
|
3078
3212
|
if (command === 'passport' && subcommand === 'status') return await statusCommand(flags, io);
|
|
3213
|
+
if (command === 'drive' && subcommand === 'health') return await driveHealthCommand(flags, io);
|
|
3079
3214
|
if (command === 'export') return await exportCommand(flags, io);
|
|
3080
3215
|
if (command === 'import') return await importCommand(subcommand, flags, io, positionalFile);
|
|
3081
3216
|
if (command === 'capsule' && subcommand === 'export') return await capsuleExportCommand(flags, io, positionalFile);
|
|
@@ -3102,6 +3237,8 @@ export async function main(argv = process.argv.slice(2), io = { stdout: process.
|
|
|
3102
3237
|
if (command === 'chain' && subcommand === 'attest') return await chainAttestCommand(flags, io);
|
|
3103
3238
|
if (command === 'chain' && subcommand === 'verify') return await chainVerifyCommand(flags, io, positionalFile);
|
|
3104
3239
|
if (command === 'chain' && subcommand === 'submit-solana') return await chainSubmitSolanaCommand(flags, io, positionalFile);
|
|
3240
|
+
if (command === 'chain' && subcommand === 'register') return await chainRegisterCommand(flags, io);
|
|
3241
|
+
if (command === 'chain' && subcommand === 'registry') return await chainRegistryCommand(flags, io, positionalFile);
|
|
3105
3242
|
if (command === 'native-host' && subcommand === 'install-plan') return await nativeHostInstallPlanCommand(flags, io);
|
|
3106
3243
|
if (command === 'mesh' && subcommand === 'demo') return await meshDemoCommand(flags, io);
|
|
3107
3244
|
if (command === 'enterprise' && subcommand === 'demo') return await enterpriseDemoCommand(flags, io);
|
|
@@ -55,6 +55,26 @@ The CLI manifest generator and install-plan preview are the recommended path bec
|
|
|
55
55
|
|
|
56
56
|
4. If your browser does not inherit the `ENIGMA_BUNDLE` user environment, create a small local wrapper that sets `ENIGMA_BUNDLE` and then execs the absolute `enigma-native-host` path. Point the manifest `path` to that wrapper. Keep the wrapper owner-writable only.
|
|
57
57
|
|
|
58
|
+
|
|
59
|
+
## Fast MCP client setup
|
|
60
|
+
|
|
61
|
+
For Claude Desktop, Cursor, Kimi Code, and VS Code Cline, avoid hand-editing MCP JSON. After installing the package, use the CLI to create the local bundle and merge only the Enigma MCP server entry into the selected client config:
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
npm install -g enigma-memory && enigma setup --client claude-desktop --write-connectors --overwrite
|
|
65
|
+
npm install -g enigma-memory && enigma setup --client cursor --write-connectors --overwrite
|
|
66
|
+
npm install -g enigma-memory && enigma setup --client kimi-code --write-connectors --overwrite
|
|
67
|
+
npm install -g enigma-memory && enigma setup --client vscode-cline --write-connectors --overwrite
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
To connect only clients whose config files already exist, use:
|
|
71
|
+
|
|
72
|
+
```sh
|
|
73
|
+
npm install -g enigma-memory && enigma setup --client auto --connect-installed --overwrite
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The generated MCP entry uses `enigma-mcp` with `ENIGMA_BUNDLE` pointing at the local bundle. Existing sibling MCP servers are preserved; changed configs are backed up by `enigma connect`.
|
|
77
|
+
|
|
58
78
|
## Generate a manifest
|
|
59
79
|
|
|
60
80
|
Use the CLI generator instead of hand-editing JSON:
|