mnemonad-cli 0.1.1 → 0.3.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/README.md +173 -19
- package/bin/mnemonad.js +164 -11
- package/lib/bridgeCodec.js +12 -0
- package/lib/chainClient.js +28 -7
- package/lib/commands/buildIndex.js +171 -0
- package/lib/commands/burn.js +59 -0
- package/lib/commands/compact.js +7 -2
- package/lib/commands/diff.js +25 -6
- package/lib/commands/index.js +3 -0
- package/lib/commands/info.js +42 -2
- package/lib/commands/pull.js +13 -6
- package/lib/commands/push.js +61 -7
- package/lib/commands/search.js +91 -0
- package/lib/commands/shared.js +137 -11
- package/lib/commands/watch.js +66 -12
- package/lib/passkeyBridge.js +203 -0
- package/lib/search/Indexer.js +204 -0
- package/lib/search/Searcher.js +68 -0
- package/lib/search/VectorIndex.js +225 -0
- package/lib/search/browser/BrowserSearcher.js +131 -0
- package/lib/search/browser/WasmIndexReader.js +93 -0
- package/lib/search/browser.js +13 -0
- package/lib/search/chunking/ChunkingStrategy.js +23 -0
- package/lib/search/chunking/TextWindowChunkingStrategy.js +69 -0
- package/lib/search/embeddings/EmbeddingProvider.js +40 -0
- package/lib/search/embeddings/StaticEmbeddingProvider.js +180 -0
- package/lib/search/embeddings/modelFiles.browser.js +28 -0
- package/lib/search/embeddings/modelFiles.node.js +41 -0
- package/lib/search/embeddings/modelFiles.shared.js +44 -0
- package/lib/search/index.js +18 -0
- package/lib/search/providers.js +153 -0
- package/lib/search/schema.js +103 -0
- package/mnemonad.config.js +19 -5
- package/package.json +18 -3
package/README.md
CHANGED
|
@@ -15,16 +15,30 @@ signer's own wallet key.
|
|
|
15
15
|
### From npm (recommended)
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
-
npm install -g mnemonad-cli
|
|
18
|
+
npm install -g mnemonad-cli@latest
|
|
19
19
|
mnemonad --help
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
To pick up a new release later
|
|
22
|
+
To pick up a new release later, prefer reinstalling `@latest` over `npm update -g` — `update`
|
|
23
|
+
has known quirks where it doesn't reliably bump an already-installed global package even when
|
|
24
|
+
a newer version is published:
|
|
23
25
|
|
|
24
26
|
```bash
|
|
25
|
-
npm
|
|
27
|
+
npm install -g mnemonad-cli@latest
|
|
28
|
+
mnemonad --help # banner shows the version — confirm it actually moved
|
|
26
29
|
```
|
|
27
30
|
|
|
31
|
+
If it still reports the old version, you likely have two installs shadowing each other on
|
|
32
|
+
`PATH` (common with nvm, after switching Node versions):
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
which mnemonad
|
|
36
|
+
npm ls -g mnemonad-cli
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
If `which mnemonad` doesn't resolve into the `npm root -g` you just installed into, remove
|
|
40
|
+
the stale one — `npm uninstall -g mnemonad-cli`, then reinstall.
|
|
41
|
+
|
|
28
42
|
### From this repo (for development)
|
|
29
43
|
|
|
30
44
|
This package lives in the monorepo alongside `mnemonad` and `monadsync`, linked through a
|
|
@@ -62,15 +76,24 @@ mnemonad info [stream-id] [path] [options] Show chain/wallet info, or full s
|
|
|
62
76
|
mnemonad diff <stream-id> <path> [options] List files added/modified/deleted locally vs the on-chain version
|
|
63
77
|
mnemonad watch <stream-id> <path> [options] Watch folder: auto-push on changes, auto-pull on remote updates
|
|
64
78
|
mnemonad compact <stream-id> <path> [options] Truncate old history and push folder as a fresh snapshot
|
|
79
|
+
mnemonad burn <stream-id> --yes [options] Permanently destroy a stream — irreversible
|
|
80
|
+
mnemonad index <path> [options] Build/update a local vector-search index (optional)
|
|
81
|
+
mnemonad search <path> <query> [options] Search a folder's vector index
|
|
65
82
|
```
|
|
66
83
|
|
|
67
84
|
`<path>` is always required for any command that touches a folder — there is no need to
|
|
68
85
|
`cd` into the folder first, but there's also no falling back to the current working
|
|
69
86
|
directory: an omitted or mistyped path fails fast with an error rather than silently
|
|
70
|
-
operating on wherever the CLI happened to be run from. The
|
|
71
|
-
|
|
72
|
-
chain/wallet). A stream id is recognized as a
|
|
73
|
-
|
|
87
|
+
operating on wherever the CLI happened to be run from. The commands that can run with no
|
|
88
|
+
path at all are `mnemonad info` with no stream id either (it just checks your configured
|
|
89
|
+
chain/wallet) and `mnemonad burn`, which never takes one. A stream id is recognized as a
|
|
90
|
+
bare decimal number (`123`), a
|
|
91
|
+
`0x`-prefixed hex string, or the network-qualified form the
|
|
92
|
+
[explorer dApp](https://mnemonad.vercel.app)'s own URLs use —
|
|
93
|
+
`testnet-0x648f18…` — pasted straight out of a `/stream/<id>` link. That form also sets
|
|
94
|
+
`--chain` to match; pass `--chain` too only if it agrees, or the command refuses rather than
|
|
95
|
+
guess which one you meant. Every command's own output always prints the bare id in hex
|
|
96
|
+
(`0x` + the full 32-byte token id), matching the explorer's own display.
|
|
74
97
|
|
|
75
98
|
Every command prints the installed CLI version as its first line of output.
|
|
76
99
|
|
|
@@ -81,9 +104,11 @@ Every command prints the installed CLI version as its first line of output.
|
|
|
81
104
|
| `--chain <name>` | Chain: `testnet`, `mainnet`, `local` (default: `testnet`) |
|
|
82
105
|
| `--rpc-url <url>` | Override the resolved RPC endpoint (e.g. a local Hardhat node) |
|
|
83
106
|
| `--contract-address <addr>` | Registry address to use instead of the chain's known deployment (required with `--chain local`) |
|
|
84
|
-
| `--gateway-url <url>` | IPFS gateway for reading externally-offloaded items (default:
|
|
107
|
+
| `--gateway-url <url>` | IPFS gateway for reading externally-offloaded items (default: this project's Pinata dedicated gateway, or `MNEMONAD_IPFS_GATEWAY` env var) |
|
|
85
108
|
| `--key <privkey>` | Monad private key (or set `MNEMONAD_KEY` env var) |
|
|
86
109
|
| `--phrase <mnemonic>` | Mnemonic phrase instead of a raw key |
|
|
110
|
+
| `--passkey` | Sign with a passkey instead: opens the explorer in your browser to sign in with WebAuthn, then relays signing requests to that tab for this command. Interactive only — not for scripts/CI. Cannot be combined with `--key`/`--phrase` |
|
|
111
|
+
| `--auth-origin <url>` | Explorer deployment `--passkey` opens (default: this project's explorer deployment, or `MNEMONAD_AUTH_ORIGIN` env var) |
|
|
87
112
|
| `--version <n>` | Version to restore or compare against (`pull`, `diff`, `info`; default: latest) |
|
|
88
113
|
| `--exclude <p1,p2>` | Extra exclude patterns (comma-separated) |
|
|
89
114
|
| `--no-compress` | Disable gzip compression |
|
|
@@ -91,12 +116,20 @@ Every command prints the installed CLI version as its first line of output.
|
|
|
91
116
|
| `--encrypt-with <method>` | How to key it: `password` (needs `--password`), or `wallet` — the `--key`/`--phrase` account signs for it, and only that account can ever decrypt it. Implied by `--password` |
|
|
92
117
|
| `--password <pw>` | Password to encrypt a new stream with, or decrypt an existing one |
|
|
93
118
|
| `--pinata-jwt <jwt>` | Pinata JWT for IPFS offload of large files (or `PINATA_JWT` env var) |
|
|
94
|
-
| `--manifest` | Write/use `.mnemonad` manifest for faster change detection |
|
|
119
|
+
| `--manifest` | Write/use `.mnemonad` manifest for faster change detection. With no stream-id given, every command recovers it from an existing `.mnemonad` in the folder instead of requiring it explicitly |
|
|
95
120
|
| `--force-snapshot` | Push a full snapshot regardless of prior history (repairs a corrupt stream) |
|
|
121
|
+
| `--index` | `push`/`watch`: update the folder's search index before every push (takes `--db`/`--model`/`--chunk-size`/`--chunk-overlap`); if indexing fails, nothing is pushed |
|
|
96
122
|
| `--poll-interval <s>` | `watch`: seconds between remote version checks (default: `2`) |
|
|
97
123
|
| `--debounce <ms>` | `watch`: quiet period in ms before pushing after a local change (default: `1000`) |
|
|
98
124
|
| `--push-only` | `watch`: disable auto-pull |
|
|
99
125
|
| `--pull-only` | `watch`: disable auto-push |
|
|
126
|
+
| `--yes` | Required by `burn` — explicit opt-in for an irreversible action, never assumed |
|
|
127
|
+
| `--db <name>` | `index`/`search`: vector-index filename (default: `search_index.db`) |
|
|
128
|
+
| `--model <name>` | `index` / `--index`: embedding model — `potion` (built in, the default for a new index) or `minilm` (needs `npm install -g mnemonad-search-transformers`), or a full model id. Without it, an existing index keeps its own model; `search` always uses the index's model |
|
|
129
|
+
| `--rebuild` | `index` / `--index`: start the index over — the way to switch models (the next push uploads the whole index again) |
|
|
130
|
+
| `--chunk-size <n>` | `index`: target characters per chunk (default: `1000`) |
|
|
131
|
+
| `--chunk-overlap <n>` | `index`: characters of overlap between chunks (default: `150`) |
|
|
132
|
+
| `--limit <n>`, `-k <n>` | `search`: number of results (default: `5`) |
|
|
100
133
|
| `--help` | Show help |
|
|
101
134
|
|
|
102
135
|
### Authentication
|
|
@@ -117,6 +150,21 @@ mnemonad push ~/my-data
|
|
|
117
150
|
A mnemonic works instead of a raw key: `--phrase "word1 word2 ..."`. `--key` and
|
|
118
151
|
`MNEMONAD_KEY` both take precedence over `--phrase`.
|
|
119
152
|
|
|
153
|
+
Or sign in with a passkey instead of any key at all:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
mnemonad push ~/my-data --passkey
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
This opens the explorer's `/cli-auth` page in your default browser to complete a WebAuthn
|
|
160
|
+
sign-in — no key ever touches this machine's disk or environment, only signatures relayed
|
|
161
|
+
back for the duration of that one command. It needs a passkey already registered through the
|
|
162
|
+
explorer (`--passkey` only signs in, it doesn't create one), a browser to complete the
|
|
163
|
+
prompt in, and someone available to click through it — so it's a good fit for a human running
|
|
164
|
+
a command by hand, not for scripts or CI (use `--key`/`MNEMONAD_KEY` there instead). See
|
|
165
|
+
[`docs/wallet/passkey-accounts.md`](../docs/wallet/passkey-accounts.md) for how the account
|
|
166
|
+
is derived and what this flow does and doesn't send anywhere.
|
|
167
|
+
|
|
120
168
|
`pull`, `info` and `diff` work without a key for public (unencrypted) streams. A key is
|
|
121
169
|
required for encrypted streams that use wallet-based encryption (not needed for
|
|
122
170
|
password-based encryption — see below), and for any `push`.
|
|
@@ -131,13 +179,19 @@ chain.
|
|
|
131
179
|
```bash
|
|
132
180
|
mnemonad push ~/my-data
|
|
133
181
|
|
|
134
|
-
# prints the new stream id, e.g.:
|
|
135
|
-
# created:
|
|
182
|
+
# prints the new stream id in hex, e.g.:
|
|
183
|
+
# created: 0x68d9736257f0d2da3c848666a4b36c4b9f18eda7e4dfba655599c47560d8a4b5
|
|
136
184
|
# version 1 pushed (full snapshot, gzip compressed)
|
|
185
|
+
|
|
186
|
+
# examples below use `123` as a stand-in for whatever id you actually got back —
|
|
187
|
+
# decimal or hex both work as input, see "Usage" above
|
|
137
188
|
```
|
|
138
189
|
|
|
139
190
|
By default a new stream is public (unencrypted). Encrypting one takes a method — the CLI
|
|
140
|
-
never picks for you, since both are reachable from the same invocation
|
|
191
|
+
never picks for you, since both are reachable from the same invocation. See
|
|
192
|
+
[`../docs/encryption/encryption.md`](../docs/encryption/encryption.md) (and its
|
|
193
|
+
[password](../docs/encryption/password.md)/[signature](../docs/encryption/signature.md)
|
|
194
|
+
pages) for how each actually works and the trade-off each makes:
|
|
141
195
|
|
|
142
196
|
```bash
|
|
143
197
|
# Keyed by a password: anyone who knows it can read the stream, from any machine.
|
|
@@ -187,7 +241,7 @@ mnemonad info 123 ~/my-data
|
|
|
187
241
|
chain: testnet
|
|
188
242
|
your wallet: 0x06bc6420b37a4898424429dcfa236f0065e12279
|
|
189
243
|
|
|
190
|
-
stream:
|
|
244
|
+
stream: 0x68d9736257f0d2da3c848666a4b36c4b9f18eda7e4dfba655599c47560d8a4b5
|
|
191
245
|
owner: 0x06bc6420b37a4898424429dcfa236f0065e12279 (you)
|
|
192
246
|
encrypted: no
|
|
193
247
|
versions: 3
|
|
@@ -228,6 +282,23 @@ mnemonad push 123 ~/my-data --manifest
|
|
|
228
282
|
# subsequent pushes are skipped when nothing has changed locally
|
|
229
283
|
```
|
|
230
284
|
|
|
285
|
+
`--manifest` writes `.mnemonad` into the folder, recording the stream id, version and file
|
|
286
|
+
hashes. With it, later commands on the same folder don't need the id typed again:
|
|
287
|
+
|
|
288
|
+
```bash
|
|
289
|
+
mnemonad push ~/my-data --manifest # no id — read from .mnemonad
|
|
290
|
+
mnemonad pull ~/my-data --manifest # same
|
|
291
|
+
mnemonad diff ~/my-data --manifest # same
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
If no id is given and there's no `.mnemonad` yet (or `--manifest` wasn't passed), the usual
|
|
295
|
+
`stream-id is required` error is thrown.
|
|
296
|
+
|
|
297
|
+
`info` is the one exception that doesn't require `--manifest` to react to a manifest: being
|
|
298
|
+
read-only, it always peeks at `.mnemonad` in `<path>` and reports what it finds — a note that
|
|
299
|
+
one's available (with its id) if you left `--manifest` off, or a note if the id you gave
|
|
300
|
+
doesn't match the one recorded in the folder's own manifest.
|
|
301
|
+
|
|
231
302
|
### Watch a folder (auto push + pull)
|
|
232
303
|
|
|
233
304
|
```bash
|
|
@@ -261,6 +332,23 @@ just a faster-replay optimization.
|
|
|
261
332
|
mnemonad compact 123 ~/my-data
|
|
262
333
|
```
|
|
263
334
|
|
|
335
|
+
### Burn a stream
|
|
336
|
+
|
|
337
|
+
Permanently destroys the stream: the ERC-721 token and its metadata are gone, and — because
|
|
338
|
+
ids are deterministic — re-creating a stream with the same id afterward does **not** recover
|
|
339
|
+
the old data; the contract deliberately marks a burned id as permanently used. Deployed
|
|
340
|
+
CODE-tier item bytes physically survive on-chain regardless — this removes the stream as a
|
|
341
|
+
usable, readable thing, not necessarily every trace of its bytes. Requires **`--yes`**: run
|
|
342
|
+
without it first to see what would be destroyed (owner, version count, on-chain size) before
|
|
343
|
+
committing.
|
|
344
|
+
|
|
345
|
+
```bash
|
|
346
|
+
mnemonad burn 123
|
|
347
|
+
# prints stream/chain/versions/size, then refuses to proceed without --yes
|
|
348
|
+
|
|
349
|
+
mnemonad burn 123 --yes
|
|
350
|
+
```
|
|
351
|
+
|
|
264
352
|
### Repair a corrupt stream with a force snapshot
|
|
265
353
|
|
|
266
354
|
If a diff patch was pushed against a stale base (e.g. a race condition in `watch`),
|
|
@@ -272,6 +360,63 @@ mnemonad push 123 ~/my-data --force-snapshot
|
|
|
272
360
|
# restore() will recover from this snapshot, skipping any corrupt diffs before it
|
|
273
361
|
```
|
|
274
362
|
|
|
363
|
+
### Vector search (optional)
|
|
364
|
+
|
|
365
|
+
Builds a local, plain (non-dot-prefixed) `search_index.db` at the folder root, using a local
|
|
366
|
+
embedding model — no API key, nothing sent anywhere. Because it's a plain file, it rides
|
|
367
|
+
through the next `push` like anything else in the folder, so anyone who later `pull`s the
|
|
368
|
+
stream gets a ready-to-search index with no re-embedding of their own.
|
|
369
|
+
|
|
370
|
+
```bash
|
|
371
|
+
mnemonad index ~/my-data
|
|
372
|
+
# embeds every changed text file since the last run (skips unchanged ones); binary files
|
|
373
|
+
# are skipped entirely
|
|
374
|
+
|
|
375
|
+
mnemonad search ~/my-data "how does chunking work"
|
|
376
|
+
# 1. chunks.txt (chunk 0, distance 0.5838)
|
|
377
|
+
# one byte slips inward the boundaries hold their ground only one chunk moves
|
|
378
|
+
|
|
379
|
+
mnemonad push ~/my-data
|
|
380
|
+
# search_index.db is included automatically — it's just a file in the folder
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
The built-in model is `minishlab/potion-base-8M`: static embeddings in plain JS, no native
|
|
384
|
+
code, a 31 MB download on first use, and a whole folder embedded in milliseconds. For better
|
|
385
|
+
results on paraphrased queries, MiniLM is available as an optional extension (about 450 MB
|
|
386
|
+
installed, most of it ONNX Runtime):
|
|
387
|
+
|
|
388
|
+
```bash
|
|
389
|
+
npm install -g mnemonad-search-transformers
|
|
390
|
+
mnemonad index ~/my-data --model minilm
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
An index keeps the model it was built with, and `search` always uses it. To switch an existing
|
|
394
|
+
index to another model, add `--rebuild`. See `docs/search.md` in the repository for how the two
|
|
395
|
+
compare on this project's own data.
|
|
396
|
+
|
|
397
|
+
`push` can also keep the index current for you: `--index` runs the same update right before pushing,
|
|
398
|
+
with the push's own `--exclude`, so the index always covers exactly what the push uploads.
|
|
399
|
+
If indexing fails (the first-time model download, say), nothing is pushed:
|
|
400
|
+
|
|
401
|
+
```bash
|
|
402
|
+
mnemonad push ~/my-data --index
|
|
403
|
+
# updating search index...
|
|
404
|
+
# index: 1 file(s) re-embedded, 0 removed (3 chunks)
|
|
405
|
+
|
|
406
|
+
mnemonad watch <stream-id> ~/my-data --index
|
|
407
|
+
# re-indexes before every automatic push; the model loads once for the whole session
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
A plain `push` of a folder that has an index checks it first. It doesn't load the model and
|
|
411
|
+
doesn't touch the file. If files changed since the index was built, it says so and suggests
|
|
412
|
+
`--index`, because a stale index would keep returning the old text's passages.
|
|
413
|
+
|
|
414
|
+
See [`../docs/search.md`](../docs/search.md) for the whole process, and why the index stays
|
|
415
|
+
diff-friendly on repeated pushes.
|
|
416
|
+
|
|
417
|
+
A pushed index also turns on a search box in the explorer's file browser for that stream.
|
|
418
|
+
The search runs in the browser on the same file and gives the same ranking as `mnemonad search`.
|
|
419
|
+
|
|
275
420
|
## End-to-end workflow
|
|
276
421
|
|
|
277
422
|
Create a stream, verify what was stored, then roll back to an earlier version:
|
|
@@ -281,8 +426,10 @@ export MNEMONAD_KEY=0xabc123...
|
|
|
281
426
|
|
|
282
427
|
# 1. first push — creates the stream
|
|
283
428
|
mnemonad push ~/my-data
|
|
284
|
-
# created:
|
|
429
|
+
# created: 0x68d9736257f0d2da3c848666a4b36c4b9f18eda7e4dfba655599c47560d8a4b5
|
|
285
430
|
# version 1 pushed (full snapshot, gzip compressed)
|
|
431
|
+
# (the rest of this walkthrough keeps using 123 as a stand-in for that id — both
|
|
432
|
+
# forms work as input, see "Usage" above)
|
|
286
433
|
|
|
287
434
|
# 2. verify: restore into a scratch folder and compare against the original
|
|
288
435
|
mnemonad pull 123 ~/verify-tmp
|
|
@@ -318,10 +465,13 @@ mnemonad pull 123 ~/my-data-v1 --version 1
|
|
|
318
465
|
the on-chain version.
|
|
319
466
|
5. **watch** — combines push and pull in a loop. A filesystem watcher triggers a debounced
|
|
320
467
|
push on local changes. A poll interval checks the remote stream for new versions and
|
|
321
|
-
pulls them if found. Push and pull never run concurrently.
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
468
|
+
pulls them if found. Push and pull never run concurrently. The contract *does* check an
|
|
469
|
+
`expectedLength` atomically on every write, so two writers can never land at the same
|
|
470
|
+
index — one just reverts. What it can't check is content: it stores opaque bytes, with no
|
|
471
|
+
way to verify a diff patch is actually consistent with the real previous item. `watch`
|
|
472
|
+
re-checks the remote length and resyncs before pushing specifically to narrow that
|
|
473
|
+
separate window — a best-effort narrowing, not a hard guarantee, since only an atomic
|
|
474
|
+
on-chain check (which it already has, for the index) could close it outright.
|
|
325
475
|
6. **compact** — drops all existing history on-chain and pushes the current folder as a
|
|
326
476
|
single fresh full snapshot, speeding up future replay after many incremental syncs.
|
|
327
477
|
|
|
@@ -329,7 +479,8 @@ mnemonad pull 123 ~/my-data-v1 --version 1
|
|
|
329
479
|
|
|
330
480
|
The following are always excluded from snapshots: `node_modules`, `.git`, `.env`,
|
|
331
481
|
`.DS_Store`, `.mnemonad`, `.claude`, `pnpm-lock.yaml`, `package-lock.json`. Add more with
|
|
332
|
-
`--exclude`.
|
|
482
|
+
`--exclude`. `search_index.db` (see "Vector search" above) is deliberately **not** on this
|
|
483
|
+
list — it's a plain file, meant to be pushed like any other.
|
|
333
484
|
|
|
334
485
|
## Dependencies
|
|
335
486
|
|
|
@@ -337,4 +488,7 @@ The following are always excluded from snapshots: `node_modules`, `.git`, `.env`
|
|
|
337
488
|
|---|---|
|
|
338
489
|
| `mnemonad` | On-chain stream primitive (this repo's `js/`) |
|
|
339
490
|
| `monadsync` | Folder-diff / snapshot layer on top of `mnemonad` (this repo's `monadsync/`) |
|
|
491
|
+
| `better-sqlite3`, `@sqliteai/sqlite-vector` | The search index: SQLite with vector search (`lib/search/`) |
|
|
492
|
+
| `@huggingface/tokenizers` | Tokenizer for the built-in embedding model (pure JS) |
|
|
493
|
+
| `mnemonad-search-transformers` | *Not a dependency* — the optional MiniLM extension, installed separately and found at runtime |
|
|
340
494
|
| `viem` | Monad client / key management |
|
package/bin/mnemonad.js
CHANGED
|
@@ -17,7 +17,9 @@ import { readFileSync } from 'node:fs';
|
|
|
17
17
|
import { fileURLToPath } from 'node:url';
|
|
18
18
|
import { dirname, join } from 'node:path';
|
|
19
19
|
|
|
20
|
-
import { push, pull, info, diff, watch, compact } from '../lib/commands/index.js';
|
|
20
|
+
import { push, pull, info, diff, watch, compact, burn, buildIndex, search } from '../lib/commands/index.js';
|
|
21
|
+
import { parseQualifiedStreamId } from '../lib/commands/shared.js';
|
|
22
|
+
import { closePasskeyBridge } from '../lib/passkeyBridge.js';
|
|
21
23
|
import defaultConfig from '../mnemonad.config.js';
|
|
22
24
|
|
|
23
25
|
const PKG_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
@@ -39,7 +41,7 @@ const CONFIG = defaultConfig || {};
|
|
|
39
41
|
// Same default the explorer dApp uses (explorer/src/mnemonad/presignProvider.js) — a
|
|
40
42
|
// public read gateway so pulling/diffing/inspecting a stream with IPFS-offloaded items
|
|
41
43
|
// works out of the box, with no setup, for anyone who didn't push it themselves.
|
|
42
|
-
const DEFAULT_IPFS_GATEWAY = CONFIG.gatewayUrl || 'https://
|
|
44
|
+
const DEFAULT_IPFS_GATEWAY = CONFIG.gatewayUrl || 'https://azure-casual-firefly-850.mypinata.cloud/ipfs/';
|
|
43
45
|
|
|
44
46
|
// The deployed presign server (presign-server/) this project runs by default — see its own
|
|
45
47
|
// README for what it does. Pushing an item too big to fit on-chain uses this automatically
|
|
@@ -47,6 +49,10 @@ const DEFAULT_IPFS_GATEWAY = CONFIG.gatewayUrl || 'https://gateway.pinata.cloud/
|
|
|
47
49
|
// alternative for anyone who'd rather supply their own.
|
|
48
50
|
const DEFAULT_PRESIGN_URL = CONFIG.presignUrl || null;
|
|
49
51
|
|
|
52
|
+
// See mnemonad.config.js's own comment on why this has to be a real deployed domain, not a
|
|
53
|
+
// placeholder — --passkey opens it to run the WebAuthn ceremony there.
|
|
54
|
+
const DEFAULT_AUTH_ORIGIN = CONFIG.authOrigin || 'https://mnemonad.vercel.app';
|
|
55
|
+
|
|
50
56
|
const USAGE = `mnemonad v${VERSION} — sync local folders to a versioned, diffed stream on Monad
|
|
51
57
|
|
|
52
58
|
Usage:
|
|
@@ -56,10 +62,18 @@ Usage:
|
|
|
56
62
|
mnemonad diff <stream-id> <path> [options] List local files added/modified/deleted vs the on-chain version
|
|
57
63
|
mnemonad watch <stream-id> <path> [options] Watch folder and auto-push/pull
|
|
58
64
|
mnemonad compact <stream-id> <path> [options] Truncate old history, push a fresh snapshot
|
|
65
|
+
mnemonad burn <stream-id> --yes [options] Permanently destroy a stream (irreversible)
|
|
66
|
+
mnemonad index <path> [options] Build/update a local vector-search index
|
|
67
|
+
for the folder (optional; see below)
|
|
68
|
+
mnemonad search <path> <query> [options] Search a folder's vector index
|
|
59
69
|
|
|
60
70
|
<path> is always required wherever a command touches a folder — the CLI never assumes the
|
|
61
|
-
current directory. The
|
|
62
|
-
|
|
71
|
+
current directory. The exceptions are \`mnemonad info\` with no stream-id (just checks your
|
|
72
|
+
configured chain/wallet) and \`mnemonad burn\`, which takes no folder at all.
|
|
73
|
+
|
|
74
|
+
<stream-id> also accepts the network-qualified form shown in the explorer's URLs —
|
|
75
|
+
testnet-0x648f18… — paste it straight out of a /stream/<id> link. It sets --chain to match;
|
|
76
|
+
pass --chain too only if it agrees, or the command refuses rather than guess which one you meant.
|
|
63
77
|
|
|
64
78
|
Options:
|
|
65
79
|
--chain <name> Chain: testnet, mainnet, local (default: testnet)
|
|
@@ -67,11 +81,17 @@ Options:
|
|
|
67
81
|
--contract-address <addr> Registry address to use instead of the chain's known deployment
|
|
68
82
|
(required with --chain local)
|
|
69
83
|
--gateway-url <url> IPFS gateway for reading externally-offloaded items
|
|
70
|
-
(default:
|
|
84
|
+
(default: this project's Pinata dedicated gateway, or MNEMONAD_IPFS_GATEWAY env var)
|
|
71
85
|
--presign-url <url> Presign server for paid IPFS uploads with no Pinata credential
|
|
72
86
|
(default: the deployed server in mnemonad.config.js, or MNEMONAD_PRESIGN_URL env var)
|
|
73
87
|
--key <privkey> Monad private key (or set MNEMONAD_KEY env var)
|
|
74
88
|
--phrase <mnemonic> Mnemonic phrase
|
|
89
|
+
--passkey Sign with a passkey instead of --key/--phrase: opens the explorer
|
|
90
|
+
in your browser to sign in with WebAuthn, then relays signing
|
|
91
|
+
requests to that tab for this command. Interactive only — not for
|
|
92
|
+
scripts/CI. See docs/wallet/passkey-accounts.md
|
|
93
|
+
--auth-origin <url> Explorer deployment --passkey opens (default: this project's
|
|
94
|
+
explorer deployment, or MNEMONAD_AUTH_ORIGIN env var)
|
|
75
95
|
--version <n> Version to restore/compare (pull, diff, info; default: latest)
|
|
76
96
|
--exclude <p1,p2> Extra exclude patterns (comma-separated)
|
|
77
97
|
--no-compress Disable gzip compression
|
|
@@ -80,13 +100,39 @@ Options:
|
|
|
80
100
|
signs for it). Implied by --password; required otherwise
|
|
81
101
|
--password <pw> Password to encrypt a new stream with, or decrypt an existing one
|
|
82
102
|
--pinata-jwt <jwt> Pinata JWT for IPFS offload of large files (or PINATA_JWT env var)
|
|
83
|
-
--manifest Write/use .mnemonad manifest for faster change detection
|
|
103
|
+
--manifest Write/use .mnemonad manifest for faster change detection.
|
|
104
|
+
With no stream-id given, every command recovers it from an
|
|
105
|
+
existing .mnemonad in <path> instead of requiring it explicitly
|
|
84
106
|
--force-snapshot Push a full snapshot regardless of prior history (repairs corrupt streams)
|
|
107
|
+
--index push/watch: update the folder's search index (search_index.db)
|
|
108
|
+
before every push, so the stream is searchable right after a pull.
|
|
109
|
+
Takes --db/--model/--chunk-size/--chunk-overlap like \`index\`
|
|
85
110
|
--poll-interval <s> Watch: seconds between remote checks (default: 2)
|
|
86
111
|
--debounce <ms> Watch: ms quiet period before pushing after a change (default: 1000)
|
|
87
112
|
--push-only Watch: disable auto-pull
|
|
88
113
|
--pull-only Watch: disable auto-push
|
|
114
|
+
--yes Required by burn — explicit opt-in for an irreversible action
|
|
115
|
+
--db <name> index/search: vector-index filename (default: search_index.db)
|
|
116
|
+
--model <name> index / --index: embedding model. \`potion\` (built in, the default for
|
|
117
|
+
a new index) or \`minilm\` (better on paraphrased queries; needs the
|
|
118
|
+
optional extension: npm install -g mnemonad-search-transformers),
|
|
119
|
+
or a full model id. Without it, an existing index keeps its own
|
|
120
|
+
model. \`search\` always uses the index's own model. All local —
|
|
121
|
+
no API key, nothing sent anywhere
|
|
122
|
+
--rebuild index / --index: start the index over (the way to switch models);
|
|
123
|
+
the next push uploads the whole index again
|
|
124
|
+
--chunk-size <n> index: target characters per chunk (default: 1000)
|
|
125
|
+
--chunk-overlap <n> index: characters of overlap between chunks (default: 150)
|
|
126
|
+
--limit, -k <n> search: number of results (default: 5)
|
|
89
127
|
--help Show this help
|
|
128
|
+
|
|
129
|
+
index/search are optional and independent of push/pull — a folder with no search_index.db
|
|
130
|
+
just has no search capability. \`index\` writes a plain (non-dot-prefixed) file at the
|
|
131
|
+
folder's root, so it rides through the next \`push\` like any other file: whoever pulls the
|
|
132
|
+
stream gets the index already built, with no re-embedding of their own. \`push --index\` and
|
|
133
|
+
\`watch --index\` run \`index\` for you before each push, and a plain \`push\` of a folder whose
|
|
134
|
+
index has fallen behind its files says so. See docs/search.md in the repository for how the
|
|
135
|
+
index works and stays diff-friendly on repeated pushes.
|
|
90
136
|
`;
|
|
91
137
|
|
|
92
138
|
function parseArgs(argv) {
|
|
@@ -103,6 +149,8 @@ function parseArgs(argv) {
|
|
|
103
149
|
presignUrl: process.env.MNEMONAD_PRESIGN_URL || DEFAULT_PRESIGN_URL,
|
|
104
150
|
key: null,
|
|
105
151
|
phrase: null,
|
|
152
|
+
passkey: false,
|
|
153
|
+
authOrigin: process.env.MNEMONAD_AUTH_ORIGIN || DEFAULT_AUTH_ORIGIN,
|
|
106
154
|
version: null,
|
|
107
155
|
exclude: null,
|
|
108
156
|
compress: 'gzip',
|
|
@@ -111,11 +159,20 @@ function parseArgs(argv) {
|
|
|
111
159
|
password: null,
|
|
112
160
|
pinataJwt: process.env.PINATA_JWT || null,
|
|
113
161
|
manifest: false,
|
|
162
|
+
index: false,
|
|
163
|
+
rebuild: false,
|
|
114
164
|
forceSnapshot: false,
|
|
115
165
|
pollInterval: 2,
|
|
116
166
|
debounce: 1000,
|
|
117
167
|
pushOnly: false,
|
|
118
168
|
pullOnly: false,
|
|
169
|
+
yes: false,
|
|
170
|
+
query: null,
|
|
171
|
+
searchDb: null,
|
|
172
|
+
model: null,
|
|
173
|
+
chunkSize: null,
|
|
174
|
+
chunkOverlap: null,
|
|
175
|
+
limit: null,
|
|
119
176
|
};
|
|
120
177
|
|
|
121
178
|
const raw = argv.slice(2);
|
|
@@ -134,27 +191,58 @@ function parseArgs(argv) {
|
|
|
134
191
|
// command except a bare `info`) fails fast with a clear error instead of silently
|
|
135
192
|
// operating on whatever directory the CLI happened to be run from.
|
|
136
193
|
// A stream id is a decimal bigint or a 0x-prefixed hex string; anything else is a path.
|
|
137
|
-
|
|
194
|
+
//
|
|
195
|
+
// Also accepted: the network-qualified form the explorer's own URLs use —
|
|
196
|
+
// `<network>-0x<hex>`, e.g. `testnet-0x648f18…` — since that's what an agent naturally
|
|
197
|
+
// has in hand after pulling an id straight out of a /stream/<id> link. A qualified id
|
|
198
|
+
// names the chain the same way the URL does; parseQualifiedStreamId (shared.js, same
|
|
199
|
+
// split the explorer's own parseStreamRouteId does) records which one (chainFromId,
|
|
200
|
+
// below) so it can settle --chain once flags are parsed too.
|
|
201
|
+
const isStreamId = (s) => /^\d+$/.test(s) || /^0x[0-9a-fA-F]+$/.test(s) || !!parseQualifiedStreamId(s);
|
|
202
|
+
|
|
203
|
+
let chainFromId = null;
|
|
204
|
+
// Strips a recognized `<network>-` prefix and records the network it named, so every
|
|
205
|
+
// downstream command only ever sees the bare id it already knows how to parse
|
|
206
|
+
// (formatStreamId, `new Mnemonad({id})`, etc. never learn this form exists).
|
|
207
|
+
const takeStreamId = (arg) => {
|
|
208
|
+
const qualified = parseQualifiedStreamId(arg);
|
|
209
|
+
if (!qualified) return arg;
|
|
210
|
+
chainFromId = qualified.network;
|
|
211
|
+
return qualified.id;
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
// `-k` is the one short flag — it has to end the positionals too, or `search`'s
|
|
215
|
+
// join-the-rest query below swallows it (`search ./f slow and steady -k 3` once searched
|
|
216
|
+
// for "slow and steady -k 3").
|
|
217
|
+
const isFlag = (arg) => arg.startsWith('--') || arg === '-k';
|
|
138
218
|
const positionals = [];
|
|
139
|
-
while (i < raw.length && !raw[i]
|
|
219
|
+
while (i < raw.length && !isFlag(raw[i])) {
|
|
140
220
|
positionals.push(raw[i]);
|
|
141
221
|
i++;
|
|
142
222
|
}
|
|
143
|
-
if (
|
|
144
|
-
|
|
223
|
+
if (args.command === 'search') {
|
|
224
|
+
// <path> <query...> — everything after the path is the query, joined back with spaces
|
|
225
|
+
// so an unquoted multi-word query (`mnemonad search ./docs how does chunking work`)
|
|
226
|
+
// works the same as a quoted one. Never a stream id — `search` doesn't take one.
|
|
227
|
+
args.path = positionals[0] || null;
|
|
228
|
+
args.query = positionals.length > 1 ? positionals.slice(1).join(' ') : null;
|
|
229
|
+
} else if (positionals.length === 2) {
|
|
230
|
+
args.streamId = takeStreamId(positionals[0]);
|
|
145
231
|
args.path = positionals[1];
|
|
146
232
|
} else if (positionals.length === 1) {
|
|
147
233
|
if (isStreamId(positionals[0])) {
|
|
148
|
-
args.streamId = positionals[0];
|
|
234
|
+
args.streamId = takeStreamId(positionals[0]);
|
|
149
235
|
} else {
|
|
150
236
|
args.path = positionals[0];
|
|
151
237
|
}
|
|
152
238
|
}
|
|
153
239
|
|
|
240
|
+
let sawChainFlag = false;
|
|
154
241
|
while (i < raw.length) {
|
|
155
242
|
const flag = raw[i];
|
|
156
243
|
if (flag === '--chain' && i + 1 < raw.length) {
|
|
157
244
|
args.chain = raw[++i];
|
|
245
|
+
sawChainFlag = true;
|
|
158
246
|
} else if (flag === '--rpc-url' && i + 1 < raw.length) {
|
|
159
247
|
args.rpcUrl = raw[++i];
|
|
160
248
|
} else if (flag === '--contract-address' && i + 1 < raw.length) {
|
|
@@ -167,6 +255,10 @@ function parseArgs(argv) {
|
|
|
167
255
|
args.key = raw[++i];
|
|
168
256
|
} else if (flag === '--phrase' && i + 1 < raw.length) {
|
|
169
257
|
args.phrase = raw[++i];
|
|
258
|
+
} else if (flag === '--passkey') {
|
|
259
|
+
args.passkey = true;
|
|
260
|
+
} else if (flag === '--auth-origin' && i + 1 < raw.length) {
|
|
261
|
+
args.authOrigin = raw[++i];
|
|
170
262
|
} else if (flag === '--version' && i + 1 < raw.length) {
|
|
171
263
|
args.version = raw[++i];
|
|
172
264
|
} else if (flag === '--exclude' && i + 1 < raw.length) {
|
|
@@ -183,6 +275,10 @@ function parseArgs(argv) {
|
|
|
183
275
|
args.pinataJwt = raw[++i];
|
|
184
276
|
} else if (flag === '--manifest') {
|
|
185
277
|
args.manifest = true;
|
|
278
|
+
} else if (flag === '--index') {
|
|
279
|
+
args.index = true;
|
|
280
|
+
} else if (flag === '--rebuild') {
|
|
281
|
+
args.rebuild = true;
|
|
186
282
|
} else if (flag === '--force-snapshot') {
|
|
187
283
|
args.forceSnapshot = true;
|
|
188
284
|
} else if (flag === '--poll-interval' && i + 1 < raw.length) {
|
|
@@ -193,10 +289,34 @@ function parseArgs(argv) {
|
|
|
193
289
|
args.pushOnly = true;
|
|
194
290
|
} else if (flag === '--pull-only') {
|
|
195
291
|
args.pullOnly = true;
|
|
292
|
+
} else if (flag === '--yes') {
|
|
293
|
+
args.yes = true;
|
|
294
|
+
} else if (flag === '--db' && i + 1 < raw.length) {
|
|
295
|
+
args.searchDb = raw[++i];
|
|
296
|
+
} else if (flag === '--model' && i + 1 < raw.length) {
|
|
297
|
+
args.model = raw[++i];
|
|
298
|
+
} else if (flag === '--chunk-size' && i + 1 < raw.length) {
|
|
299
|
+
args.chunkSize = Number(raw[++i]);
|
|
300
|
+
} else if (flag === '--chunk-overlap' && i + 1 < raw.length) {
|
|
301
|
+
args.chunkOverlap = Number(raw[++i]);
|
|
302
|
+
} else if ((flag === '--limit' || flag === '-k') && i + 1 < raw.length) {
|
|
303
|
+
args.limit = Number(raw[++i]);
|
|
196
304
|
}
|
|
197
305
|
i++;
|
|
198
306
|
}
|
|
199
307
|
|
|
308
|
+
if (chainFromId) {
|
|
309
|
+
if (sawChainFlag && args.chain !== chainFromId) {
|
|
310
|
+
console.error(
|
|
311
|
+
`Error: the stream id names network '${chainFromId}', but --chain '${args.chain}' was\n` +
|
|
312
|
+
` also given — they disagree. Drop --chain to use the id's network, or pass\n` +
|
|
313
|
+
` --chain ${chainFromId} to match it.`
|
|
314
|
+
);
|
|
315
|
+
_realExit(1);
|
|
316
|
+
}
|
|
317
|
+
args.chain = chainFromId;
|
|
318
|
+
}
|
|
319
|
+
|
|
200
320
|
return args;
|
|
201
321
|
}
|
|
202
322
|
|
|
@@ -208,6 +328,22 @@ console.log(`mnemonad v${VERSION}`);
|
|
|
208
328
|
// anything in the RPC/fetch stack unref's its own I/O (see the process.exit note above).
|
|
209
329
|
const keepAlive = setInterval(() => {}, 60_000);
|
|
210
330
|
|
|
331
|
+
// --passkey's browser tab shows a result line when the command finishes (see
|
|
332
|
+
// closePasskeyBridge()) rather than a bare "done" — commands don't return a summary
|
|
333
|
+
// explicitly, so this captures their own last console.log line instead, the same thing this
|
|
334
|
+
// terminal already sees. Only wired up when --passkey is in play; every other run's
|
|
335
|
+
// console.log is untouched.
|
|
336
|
+
const logLines = [];
|
|
337
|
+
const originalConsoleLog = console.log;
|
|
338
|
+
if (args.passkey) {
|
|
339
|
+
console.log = (...parts) => {
|
|
340
|
+
originalConsoleLog(...parts);
|
|
341
|
+
logLines.push(parts.join(' '));
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
let succeeded = true;
|
|
345
|
+
let errorMessage = null;
|
|
346
|
+
|
|
211
347
|
try {
|
|
212
348
|
if (args.command === 'push') {
|
|
213
349
|
await push(args);
|
|
@@ -221,6 +357,12 @@ try {
|
|
|
221
357
|
await watch(args);
|
|
222
358
|
} else if (args.command === 'compact') {
|
|
223
359
|
await compact(args);
|
|
360
|
+
} else if (args.command === 'burn') {
|
|
361
|
+
await burn(args);
|
|
362
|
+
} else if (args.command === 'index') {
|
|
363
|
+
await buildIndex(args);
|
|
364
|
+
} else if (args.command === 'search') {
|
|
365
|
+
await search(args);
|
|
224
366
|
} else {
|
|
225
367
|
console.error('Unknown command:', args.command);
|
|
226
368
|
console.log(USAGE);
|
|
@@ -229,15 +371,26 @@ try {
|
|
|
229
371
|
} catch (err) {
|
|
230
372
|
if (err._isProcessExit) {
|
|
231
373
|
process.exitCode = err._exitCode;
|
|
374
|
+
succeeded = err._exitCode === 0;
|
|
232
375
|
} else if (err._isUserError) {
|
|
233
376
|
// Expected, actionable failure — message only, no stack trace.
|
|
234
377
|
console.error('Error:', err.message);
|
|
235
378
|
process.exitCode = 1;
|
|
379
|
+
succeeded = false;
|
|
236
380
|
} else {
|
|
237
381
|
console.error('Error:', err.message);
|
|
238
382
|
if (err.stack) console.error(err.stack);
|
|
239
383
|
process.exitCode = 1;
|
|
384
|
+
succeeded = false;
|
|
240
385
|
}
|
|
386
|
+
errorMessage = err.message;
|
|
241
387
|
} finally {
|
|
388
|
+
console.log = originalConsoleLog;
|
|
389
|
+
// No-op when --passkey was never used — see passkeyBridge.js. Runs whether the command
|
|
390
|
+
// succeeded or failed, so a browser tab left open on a failed push doesn't linger.
|
|
391
|
+
closePasskeyBridge({
|
|
392
|
+
ok: succeeded,
|
|
393
|
+
summary: succeeded ? (logLines.filter(Boolean).pop() || 'Done.') : (errorMessage || 'Failed.'),
|
|
394
|
+
});
|
|
242
395
|
clearInterval(keepAlive);
|
|
243
396
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON can't carry BigInt, and viem's transaction-request objects use BigInt for gas/value/
|
|
3
|
+
* fee fields — so a transaction going out over `--passkey`'s loopback WebSocket bridge
|
|
4
|
+
* (passkeyBridge.js) to the browser tab is wrapped through this first.
|
|
5
|
+
*
|
|
6
|
+
* Must produce exactly what explorer/src/wallet/bridgeCodec.js's `decodeFromBridge` expects
|
|
7
|
+
* — the two sides are never bundled together (separate packages), so keep them in sync by
|
|
8
|
+
* hand if either changes.
|
|
9
|
+
*/
|
|
10
|
+
export function encodeForBridge(value) {
|
|
11
|
+
return JSON.parse(JSON.stringify(value, (_key, v) => (typeof v === 'bigint' ? { __bigint__: v.toString() } : v)));
|
|
12
|
+
}
|