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 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 update -g mnemonad-cli
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 only command that can run with
71
- no path at all is `mnemonad info` with no stream id either (it just checks your configured
72
- chain/wallet). A stream id is recognized as a bare decimal number (`123`) or a
73
- `0x`-prefixed hex string; anything else in that position is treated as a path.
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: `https://gateway.pinata.cloud/ipfs/`, or `MNEMONAD_IPFS_GATEWAY` env var) |
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: 123
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: 123
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: 123
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. Monad's registry has no
322
- "expected prior length" guard on push, so `watch` re-checks the remote length itself
323
- right before pushing and resyncs if it has moved — a best-effort narrowing of the race
324
- window, not a hard guarantee.
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://gateway.pinata.cloud/ipfs/';
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 only exception is \`mnemonad info\` with no stream-id, which just
62
- checks your configured chain/wallet and needs no folder at all.
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: https://gateway.pinata.cloud/ipfs/, or MNEMONAD_IPFS_GATEWAY env var)
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
- const isStreamId = (s) => /^\d+$/.test(s) || /^0x[0-9a-fA-F]+$/.test(s);
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].startsWith('--')) {
219
+ while (i < raw.length && !isFlag(raw[i])) {
140
220
  positionals.push(raw[i]);
141
221
  i++;
142
222
  }
143
- if (positionals.length === 2) {
144
- args.streamId = positionals[0];
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
+ }