mnemonad-cli 0.2.0 → 0.3.1
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 +156 -9
- package/bin/mnemonad.js +122 -5
- package/lib/bridgeCodec.js +12 -0
- package/lib/chainClient.js +21 -7
- package/lib/commands/buildIndex.js +171 -0
- package/lib/commands/burn.js +1 -1
- package/lib/commands/compact.js +3 -1
- package/lib/commands/diff.js +3 -1
- package/lib/commands/index.js +2 -0
- package/lib/commands/info.js +10 -1
- package/lib/commands/pull.js +6 -1
- package/lib/commands/push.js +152 -1
- package/lib/commands/search.js +91 -0
- package/lib/commands/shared.js +3 -2
- package/lib/commands/watch.js +60 -9
- package/lib/jobs.js +380 -0
- 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 +14 -4
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
|
|
@@ -63,6 +77,8 @@ mnemonad diff <stream-id> <path> [options] List files added/modified/deleted
|
|
|
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
|
|
65
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
|
|
66
82
|
```
|
|
67
83
|
|
|
68
84
|
`<path>` is always required for any command that touches a folder — there is no need to
|
|
@@ -88,9 +104,11 @@ Every command prints the installed CLI version as its first line of output.
|
|
|
88
104
|
| `--chain <name>` | Chain: `testnet`, `mainnet`, `local` (default: `testnet`) |
|
|
89
105
|
| `--rpc-url <url>` | Override the resolved RPC endpoint (e.g. a local Hardhat node) |
|
|
90
106
|
| `--contract-address <addr>` | Registry address to use instead of the chain's known deployment (required with `--chain local`) |
|
|
91
|
-
| `--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) |
|
|
92
108
|
| `--key <privkey>` | Monad private key (or set `MNEMONAD_KEY` env var) |
|
|
93
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) |
|
|
94
112
|
| `--version <n>` | Version to restore or compare against (`pull`, `diff`, `info`; default: latest) |
|
|
95
113
|
| `--exclude <p1,p2>` | Extra exclude patterns (comma-separated) |
|
|
96
114
|
| `--no-compress` | Disable gzip compression |
|
|
@@ -100,11 +118,19 @@ Every command prints the installed CLI version as its first line of output.
|
|
|
100
118
|
| `--pinata-jwt <jwt>` | Pinata JWT for IPFS offload of large files (or `PINATA_JWT` env var) |
|
|
101
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 |
|
|
102
120
|
| `--force-snapshot` | Push a full snapshot regardless of prior history (repairs a corrupt stream) |
|
|
121
|
+
| `--detach` | `push`: build the next version here, send it from a background process, and return right away. Existing streams only. See "Push in the background" below |
|
|
122
|
+
| `--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 |
|
|
103
123
|
| `--poll-interval <s>` | `watch`: seconds between remote version checks (default: `2`) |
|
|
104
124
|
| `--debounce <ms>` | `watch`: quiet period in ms before pushing after a local change (default: `1000`) |
|
|
105
125
|
| `--push-only` | `watch`: disable auto-pull |
|
|
106
126
|
| `--pull-only` | `watch`: disable auto-push |
|
|
107
127
|
| `--yes` | Required by `burn` — explicit opt-in for an irreversible action, never assumed |
|
|
128
|
+
| `--db <name>` | `index`/`search`: vector-index filename (default: `search_index.db`) |
|
|
129
|
+
| `--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 |
|
|
130
|
+
| `--rebuild` | `index` / `--index`: start the index over — the way to switch models (the next push uploads the whole index again) |
|
|
131
|
+
| `--chunk-size <n>` | `index`: target characters per chunk (default: `1000`) |
|
|
132
|
+
| `--chunk-overlap <n>` | `index`: characters of overlap between chunks (default: `150`) |
|
|
133
|
+
| `--limit <n>`, `-k <n>` | `search`: number of results (default: `5`) |
|
|
108
134
|
| `--help` | Show help |
|
|
109
135
|
|
|
110
136
|
### Authentication
|
|
@@ -125,6 +151,21 @@ mnemonad push ~/my-data
|
|
|
125
151
|
A mnemonic works instead of a raw key: `--phrase "word1 word2 ..."`. `--key` and
|
|
126
152
|
`MNEMONAD_KEY` both take precedence over `--phrase`.
|
|
127
153
|
|
|
154
|
+
Or sign in with a passkey instead of any key at all:
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
mnemonad push ~/my-data --passkey
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
This opens the explorer's `/cli-auth` page in your default browser to complete a WebAuthn
|
|
161
|
+
sign-in — no key ever touches this machine's disk or environment, only signatures relayed
|
|
162
|
+
back for the duration of that one command. It needs a passkey already registered through the
|
|
163
|
+
explorer (`--passkey` only signs in, it doesn't create one), a browser to complete the
|
|
164
|
+
prompt in, and someone available to click through it — so it's a good fit for a human running
|
|
165
|
+
a command by hand, not for scripts or CI (use `--key`/`MNEMONAD_KEY` there instead). See
|
|
166
|
+
[`docs/wallet/passkey-accounts.md`](../docs/wallet/passkey-accounts.md) for how the account
|
|
167
|
+
is derived and what this flow does and doesn't send anywhere.
|
|
168
|
+
|
|
128
169
|
`pull`, `info` and `diff` work without a key for public (unencrypted) streams. A key is
|
|
129
170
|
required for encrypted streams that use wallet-based encryption (not needed for
|
|
130
171
|
password-based encryption — see below), and for any `push`.
|
|
@@ -309,6 +350,48 @@ mnemonad burn 123
|
|
|
309
350
|
mnemonad burn 123 --yes
|
|
310
351
|
```
|
|
311
352
|
|
|
353
|
+
### Push in the background
|
|
354
|
+
|
|
355
|
+
A push spends most of its time waiting for the chain. `--detach` does everything that can
|
|
356
|
+
fail for your own reasons first, here and now: the owner check, unlocking, `--index`, "no
|
|
357
|
+
changes", and the balance against the estimated cost. Then it hands only the sending to a
|
|
358
|
+
background process and returns:
|
|
359
|
+
|
|
360
|
+
```bash
|
|
361
|
+
mnemonad push ~/my-data --manifest --detach
|
|
362
|
+
# version 8 prepared (diff, gzip compressed, 2.1 KB)
|
|
363
|
+
# estimated cost: 0.0041 MON
|
|
364
|
+
# stream: 0x68d9…
|
|
365
|
+
# sending in the background: job 8f3c1a2b (pid 41210)
|
|
366
|
+
# log: ~/.cache/mnemonad/jobs/8f3c1a2b/push.log
|
|
367
|
+
# check: mnemonad info ~/my-data --manifest
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
- **The version is frozen when the command runs.** Edits made after it returns go into the
|
|
371
|
+
next push, not this one.
|
|
372
|
+
- **Existing streams only.** It needs a stream id, or `--manifest` on a folder that has a
|
|
373
|
+
`.mnemonad`. Create a stream with a normal push first. `--passkey` can't be used: the browser
|
|
374
|
+
tab that signs for it closes when the command exits.
|
|
375
|
+
- **The next command waits for it.** A `push`, `pull`, `compact` or `watch` of the same stream
|
|
376
|
+
waits until the background push has finished, so nothing builds on, or restores, the version
|
|
377
|
+
before it. A push from the same wallet to another stream waits too, so the two don't race for
|
|
378
|
+
one nonce. `diff` only notes that a push is still sending.
|
|
379
|
+
- **Results:** `mnemonad info <path>` shows the last background push for the stream, and when
|
|
380
|
+
it failed, why. If one fails, the next command on that stream also prints a warning, once.
|
|
381
|
+
- **Nothing half-lands.** The background process refuses to send a version whose base is gone
|
|
382
|
+
(the stream changed after it was prepared), and the contract rejects any write that doesn't
|
|
383
|
+
land at the index it names. A failed background push leaves the stream as it was. Push again.
|
|
384
|
+
- **Secrets stay in memory.** The key, the password and a Pinata JWT are handed to the
|
|
385
|
+
background process over a pipe. They are never written to the job folder, the log, or the
|
|
386
|
+
process list.
|
|
387
|
+
- If the background process is killed mid-send, the job is reported as failed the next time a
|
|
388
|
+
command looks at it. A large push that was staged over several transactions, or paid for an
|
|
389
|
+
IPFS upload, can't be resumed. A paid but unused upload can be refunded after the gateway's
|
|
390
|
+
time limit, with the SDK's `MnemonadUploadGateway.refund()` (the CLI has no command for it).
|
|
391
|
+
|
|
392
|
+
Jobs live in `~/.cache/mnemonad/jobs/` (set `MNEMONAD_JOBS_DIR` to move them). Finished ones are
|
|
393
|
+
removed after 14 days.
|
|
394
|
+
|
|
312
395
|
### Repair a corrupt stream with a force snapshot
|
|
313
396
|
|
|
314
397
|
If a diff patch was pushed against a stale base (e.g. a race condition in `watch`),
|
|
@@ -320,6 +403,63 @@ mnemonad push 123 ~/my-data --force-snapshot
|
|
|
320
403
|
# restore() will recover from this snapshot, skipping any corrupt diffs before it
|
|
321
404
|
```
|
|
322
405
|
|
|
406
|
+
### Vector search (optional)
|
|
407
|
+
|
|
408
|
+
Builds a local, plain (non-dot-prefixed) `search_index.db` at the folder root, using a local
|
|
409
|
+
embedding model — no API key, nothing sent anywhere. Because it's a plain file, it rides
|
|
410
|
+
through the next `push` like anything else in the folder, so anyone who later `pull`s the
|
|
411
|
+
stream gets a ready-to-search index with no re-embedding of their own.
|
|
412
|
+
|
|
413
|
+
```bash
|
|
414
|
+
mnemonad index ~/my-data
|
|
415
|
+
# embeds every changed text file since the last run (skips unchanged ones); binary files
|
|
416
|
+
# are skipped entirely
|
|
417
|
+
|
|
418
|
+
mnemonad search ~/my-data "how does chunking work"
|
|
419
|
+
# 1. chunks.txt (chunk 0, distance 0.5838)
|
|
420
|
+
# one byte slips inward the boundaries hold their ground only one chunk moves
|
|
421
|
+
|
|
422
|
+
mnemonad push ~/my-data
|
|
423
|
+
# search_index.db is included automatically — it's just a file in the folder
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
The built-in model is `minishlab/potion-base-8M`: static embeddings in plain JS, no native
|
|
427
|
+
code, a 31 MB download on first use, and a whole folder embedded in milliseconds. For better
|
|
428
|
+
results on paraphrased queries, MiniLM is available as an optional extension (about 450 MB
|
|
429
|
+
installed, most of it ONNX Runtime):
|
|
430
|
+
|
|
431
|
+
```bash
|
|
432
|
+
npm install -g mnemonad-search-transformers
|
|
433
|
+
mnemonad index ~/my-data --model minilm
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
An index keeps the model it was built with, and `search` always uses it. To switch an existing
|
|
437
|
+
index to another model, add `--rebuild`. See `docs/search.md` in the repository for how the two
|
|
438
|
+
compare on this project's own data.
|
|
439
|
+
|
|
440
|
+
`push` can also keep the index current for you: `--index` runs the same update right before pushing,
|
|
441
|
+
with the push's own `--exclude`, so the index always covers exactly what the push uploads.
|
|
442
|
+
If indexing fails (the first-time model download, say), nothing is pushed:
|
|
443
|
+
|
|
444
|
+
```bash
|
|
445
|
+
mnemonad push ~/my-data --index
|
|
446
|
+
# updating search index...
|
|
447
|
+
# index: 1 file(s) re-embedded, 0 removed (3 chunks)
|
|
448
|
+
|
|
449
|
+
mnemonad watch <stream-id> ~/my-data --index
|
|
450
|
+
# re-indexes before every automatic push; the model loads once for the whole session
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
A plain `push` of a folder that has an index checks it first. It doesn't load the model and
|
|
454
|
+
doesn't touch the file. If files changed since the index was built, it says so and suggests
|
|
455
|
+
`--index`, because a stale index would keep returning the old text's passages.
|
|
456
|
+
|
|
457
|
+
See [`../docs/search.md`](../docs/search.md) for the whole process, and why the index stays
|
|
458
|
+
diff-friendly on repeated pushes.
|
|
459
|
+
|
|
460
|
+
A pushed index also turns on a search box in the explorer's file browser for that stream.
|
|
461
|
+
The search runs in the browser on the same file and gives the same ranking as `mnemonad search`.
|
|
462
|
+
|
|
323
463
|
## End-to-end workflow
|
|
324
464
|
|
|
325
465
|
Create a stream, verify what was stored, then roll back to an earlier version:
|
|
@@ -368,10 +508,13 @@ mnemonad pull 123 ~/my-data-v1 --version 1
|
|
|
368
508
|
the on-chain version.
|
|
369
509
|
5. **watch** — combines push and pull in a loop. A filesystem watcher triggers a debounced
|
|
370
510
|
push on local changes. A poll interval checks the remote stream for new versions and
|
|
371
|
-
pulls them if found. Push and pull never run concurrently.
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
511
|
+
pulls them if found. Push and pull never run concurrently. The contract *does* check an
|
|
512
|
+
`expectedLength` atomically on every write, so two writers can never land at the same
|
|
513
|
+
index — one just reverts. What it can't check is content: it stores opaque bytes, with no
|
|
514
|
+
way to verify a diff patch is actually consistent with the real previous item. `watch`
|
|
515
|
+
re-checks the remote length and resyncs before pushing specifically to narrow that
|
|
516
|
+
separate window — a best-effort narrowing, not a hard guarantee, since only an atomic
|
|
517
|
+
on-chain check (which it already has, for the index) could close it outright.
|
|
375
518
|
6. **compact** — drops all existing history on-chain and pushes the current folder as a
|
|
376
519
|
single fresh full snapshot, speeding up future replay after many incremental syncs.
|
|
377
520
|
|
|
@@ -379,7 +522,8 @@ mnemonad pull 123 ~/my-data-v1 --version 1
|
|
|
379
522
|
|
|
380
523
|
The following are always excluded from snapshots: `node_modules`, `.git`, `.env`,
|
|
381
524
|
`.DS_Store`, `.mnemonad`, `.claude`, `pnpm-lock.yaml`, `package-lock.json`. Add more with
|
|
382
|
-
`--exclude`.
|
|
525
|
+
`--exclude`. `search_index.db` (see "Vector search" above) is deliberately **not** on this
|
|
526
|
+
list — it's a plain file, meant to be pushed like any other.
|
|
383
527
|
|
|
384
528
|
## Dependencies
|
|
385
529
|
|
|
@@ -387,4 +531,7 @@ The following are always excluded from snapshots: `node_modules`, `.git`, `.env`
|
|
|
387
531
|
|---|---|
|
|
388
532
|
| `mnemonad` | On-chain stream primitive (this repo's `js/`) |
|
|
389
533
|
| `monadsync` | Folder-diff / snapshot layer on top of `mnemonad` (this repo's `monadsync/`) |
|
|
534
|
+
| `better-sqlite3`, `@sqliteai/sqlite-vector` | The search index: SQLite with vector search (`lib/search/`) |
|
|
535
|
+
| `@huggingface/tokenizers` | Tokenizer for the built-in embedding model (pure JS) |
|
|
536
|
+
| `mnemonad-search-transformers` | *Not a dependency* — the optional MiniLM extension, installed separately and found at runtime |
|
|
390
537
|
| `viem` | Monad client / key management |
|
package/bin/mnemonad.js
CHANGED
|
@@ -17,8 +17,10 @@ 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, burn } from '../lib/commands/index.js';
|
|
20
|
+
import { push, pull, info, diff, watch, compact, burn, buildIndex, search } from '../lib/commands/index.js';
|
|
21
21
|
import { parseQualifiedStreamId } from '../lib/commands/shared.js';
|
|
22
|
+
import { closePasskeyBridge } from '../lib/passkeyBridge.js';
|
|
23
|
+
import { runPushJob } from '../lib/jobs.js';
|
|
22
24
|
import defaultConfig from '../mnemonad.config.js';
|
|
23
25
|
|
|
24
26
|
const PKG_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
@@ -40,7 +42,7 @@ const CONFIG = defaultConfig || {};
|
|
|
40
42
|
// Same default the explorer dApp uses (explorer/src/mnemonad/presignProvider.js) — a
|
|
41
43
|
// public read gateway so pulling/diffing/inspecting a stream with IPFS-offloaded items
|
|
42
44
|
// works out of the box, with no setup, for anyone who didn't push it themselves.
|
|
43
|
-
const DEFAULT_IPFS_GATEWAY = CONFIG.gatewayUrl || 'https://
|
|
45
|
+
const DEFAULT_IPFS_GATEWAY = CONFIG.gatewayUrl || 'https://azure-casual-firefly-850.mypinata.cloud/ipfs/';
|
|
44
46
|
|
|
45
47
|
// The deployed presign server (presign-server/) this project runs by default — see its own
|
|
46
48
|
// README for what it does. Pushing an item too big to fit on-chain uses this automatically
|
|
@@ -48,6 +50,10 @@ const DEFAULT_IPFS_GATEWAY = CONFIG.gatewayUrl || 'https://gateway.pinata.cloud/
|
|
|
48
50
|
// alternative for anyone who'd rather supply their own.
|
|
49
51
|
const DEFAULT_PRESIGN_URL = CONFIG.presignUrl || null;
|
|
50
52
|
|
|
53
|
+
// See mnemonad.config.js's own comment on why this has to be a real deployed domain, not a
|
|
54
|
+
// placeholder — --passkey opens it to run the WebAuthn ceremony there.
|
|
55
|
+
const DEFAULT_AUTH_ORIGIN = CONFIG.authOrigin || 'https://mnemonad.vercel.app';
|
|
56
|
+
|
|
51
57
|
const USAGE = `mnemonad v${VERSION} — sync local folders to a versioned, diffed stream on Monad
|
|
52
58
|
|
|
53
59
|
Usage:
|
|
@@ -58,6 +64,9 @@ Usage:
|
|
|
58
64
|
mnemonad watch <stream-id> <path> [options] Watch folder and auto-push/pull
|
|
59
65
|
mnemonad compact <stream-id> <path> [options] Truncate old history, push a fresh snapshot
|
|
60
66
|
mnemonad burn <stream-id> --yes [options] Permanently destroy a stream (irreversible)
|
|
67
|
+
mnemonad index <path> [options] Build/update a local vector-search index
|
|
68
|
+
for the folder (optional; see below)
|
|
69
|
+
mnemonad search <path> <query> [options] Search a folder's vector index
|
|
61
70
|
|
|
62
71
|
<path> is always required wherever a command touches a folder — the CLI never assumes the
|
|
63
72
|
current directory. The exceptions are \`mnemonad info\` with no stream-id (just checks your
|
|
@@ -73,11 +82,17 @@ Options:
|
|
|
73
82
|
--contract-address <addr> Registry address to use instead of the chain's known deployment
|
|
74
83
|
(required with --chain local)
|
|
75
84
|
--gateway-url <url> IPFS gateway for reading externally-offloaded items
|
|
76
|
-
(default:
|
|
85
|
+
(default: this project's Pinata dedicated gateway, or MNEMONAD_IPFS_GATEWAY env var)
|
|
77
86
|
--presign-url <url> Presign server for paid IPFS uploads with no Pinata credential
|
|
78
87
|
(default: the deployed server in mnemonad.config.js, or MNEMONAD_PRESIGN_URL env var)
|
|
79
88
|
--key <privkey> Monad private key (or set MNEMONAD_KEY env var)
|
|
80
89
|
--phrase <mnemonic> Mnemonic phrase
|
|
90
|
+
--passkey Sign with a passkey instead of --key/--phrase: opens the explorer
|
|
91
|
+
in your browser to sign in with WebAuthn, then relays signing
|
|
92
|
+
requests to that tab for this command. Interactive only — not for
|
|
93
|
+
scripts/CI. See docs/wallet/passkey-accounts.md
|
|
94
|
+
--auth-origin <url> Explorer deployment --passkey opens (default: this project's
|
|
95
|
+
explorer deployment, or MNEMONAD_AUTH_ORIGIN env var)
|
|
81
96
|
--version <n> Version to restore/compare (pull, diff, info; default: latest)
|
|
82
97
|
--exclude <p1,p2> Extra exclude patterns (comma-separated)
|
|
83
98
|
--no-compress Disable gzip compression
|
|
@@ -90,12 +105,39 @@ Options:
|
|
|
90
105
|
With no stream-id given, every command recovers it from an
|
|
91
106
|
existing .mnemonad in <path> instead of requiring it explicitly
|
|
92
107
|
--force-snapshot Push a full snapshot regardless of prior history (repairs corrupt streams)
|
|
108
|
+
--detach push: build the next version here, send it from a background process,
|
|
109
|
+
and return right away. Existing streams only (an id, or --manifest).
|
|
110
|
+
Later pushes and pulls of that stream wait for it; \`mnemonad info\`
|
|
111
|
+
shows how it went
|
|
112
|
+
--index push/watch: update the folder's search index (search_index.db)
|
|
113
|
+
before every push, so the stream is searchable right after a pull.
|
|
114
|
+
Takes --db/--model/--chunk-size/--chunk-overlap like \`index\`
|
|
93
115
|
--poll-interval <s> Watch: seconds between remote checks (default: 2)
|
|
94
116
|
--debounce <ms> Watch: ms quiet period before pushing after a change (default: 1000)
|
|
95
117
|
--push-only Watch: disable auto-pull
|
|
96
118
|
--pull-only Watch: disable auto-push
|
|
97
119
|
--yes Required by burn — explicit opt-in for an irreversible action
|
|
120
|
+
--db <name> index/search: vector-index filename (default: search_index.db)
|
|
121
|
+
--model <name> index / --index: embedding model. \`potion\` (built in, the default for
|
|
122
|
+
a new index) or \`minilm\` (better on paraphrased queries; needs the
|
|
123
|
+
optional extension: npm install -g mnemonad-search-transformers),
|
|
124
|
+
or a full model id. Without it, an existing index keeps its own
|
|
125
|
+
model. \`search\` always uses the index's own model. All local —
|
|
126
|
+
no API key, nothing sent anywhere
|
|
127
|
+
--rebuild index / --index: start the index over (the way to switch models);
|
|
128
|
+
the next push uploads the whole index again
|
|
129
|
+
--chunk-size <n> index: target characters per chunk (default: 1000)
|
|
130
|
+
--chunk-overlap <n> index: characters of overlap between chunks (default: 150)
|
|
131
|
+
--limit, -k <n> search: number of results (default: 5)
|
|
98
132
|
--help Show this help
|
|
133
|
+
|
|
134
|
+
index/search are optional and independent of push/pull — a folder with no search_index.db
|
|
135
|
+
just has no search capability. \`index\` writes a plain (non-dot-prefixed) file at the
|
|
136
|
+
folder's root, so it rides through the next \`push\` like any other file: whoever pulls the
|
|
137
|
+
stream gets the index already built, with no re-embedding of their own. \`push --index\` and
|
|
138
|
+
\`watch --index\` run \`index\` for you before each push, and a plain \`push\` of a folder whose
|
|
139
|
+
index has fallen behind its files says so. See docs/search.md in the repository for how the
|
|
140
|
+
index works and stays diff-friendly on repeated pushes.
|
|
99
141
|
`;
|
|
100
142
|
|
|
101
143
|
function parseArgs(argv) {
|
|
@@ -112,6 +154,8 @@ function parseArgs(argv) {
|
|
|
112
154
|
presignUrl: process.env.MNEMONAD_PRESIGN_URL || DEFAULT_PRESIGN_URL,
|
|
113
155
|
key: null,
|
|
114
156
|
phrase: null,
|
|
157
|
+
passkey: false,
|
|
158
|
+
authOrigin: process.env.MNEMONAD_AUTH_ORIGIN || DEFAULT_AUTH_ORIGIN,
|
|
115
159
|
version: null,
|
|
116
160
|
exclude: null,
|
|
117
161
|
compress: 'gzip',
|
|
@@ -120,12 +164,21 @@ function parseArgs(argv) {
|
|
|
120
164
|
password: null,
|
|
121
165
|
pinataJwt: process.env.PINATA_JWT || null,
|
|
122
166
|
manifest: false,
|
|
167
|
+
index: false,
|
|
168
|
+
rebuild: false,
|
|
123
169
|
forceSnapshot: false,
|
|
170
|
+
detach: false,
|
|
124
171
|
pollInterval: 2,
|
|
125
172
|
debounce: 1000,
|
|
126
173
|
pushOnly: false,
|
|
127
174
|
pullOnly: false,
|
|
128
175
|
yes: false,
|
|
176
|
+
query: null,
|
|
177
|
+
searchDb: null,
|
|
178
|
+
model: null,
|
|
179
|
+
chunkSize: null,
|
|
180
|
+
chunkOverlap: null,
|
|
181
|
+
limit: null,
|
|
129
182
|
};
|
|
130
183
|
|
|
131
184
|
const raw = argv.slice(2);
|
|
@@ -164,12 +217,22 @@ function parseArgs(argv) {
|
|
|
164
217
|
return qualified.id;
|
|
165
218
|
};
|
|
166
219
|
|
|
220
|
+
// `-k` is the one short flag — it has to end the positionals too, or `search`'s
|
|
221
|
+
// join-the-rest query below swallows it (`search ./f slow and steady -k 3` once searched
|
|
222
|
+
// for "slow and steady -k 3").
|
|
223
|
+
const isFlag = (arg) => arg.startsWith('--') || arg === '-k';
|
|
167
224
|
const positionals = [];
|
|
168
|
-
while (i < raw.length && !raw[i]
|
|
225
|
+
while (i < raw.length && !isFlag(raw[i])) {
|
|
169
226
|
positionals.push(raw[i]);
|
|
170
227
|
i++;
|
|
171
228
|
}
|
|
172
|
-
if (
|
|
229
|
+
if (args.command === 'search') {
|
|
230
|
+
// <path> <query...> — everything after the path is the query, joined back with spaces
|
|
231
|
+
// so an unquoted multi-word query (`mnemonad search ./docs how does chunking work`)
|
|
232
|
+
// works the same as a quoted one. Never a stream id — `search` doesn't take one.
|
|
233
|
+
args.path = positionals[0] || null;
|
|
234
|
+
args.query = positionals.length > 1 ? positionals.slice(1).join(' ') : null;
|
|
235
|
+
} else if (positionals.length === 2) {
|
|
173
236
|
args.streamId = takeStreamId(positionals[0]);
|
|
174
237
|
args.path = positionals[1];
|
|
175
238
|
} else if (positionals.length === 1) {
|
|
@@ -198,6 +261,10 @@ function parseArgs(argv) {
|
|
|
198
261
|
args.key = raw[++i];
|
|
199
262
|
} else if (flag === '--phrase' && i + 1 < raw.length) {
|
|
200
263
|
args.phrase = raw[++i];
|
|
264
|
+
} else if (flag === '--passkey') {
|
|
265
|
+
args.passkey = true;
|
|
266
|
+
} else if (flag === '--auth-origin' && i + 1 < raw.length) {
|
|
267
|
+
args.authOrigin = raw[++i];
|
|
201
268
|
} else if (flag === '--version' && i + 1 < raw.length) {
|
|
202
269
|
args.version = raw[++i];
|
|
203
270
|
} else if (flag === '--exclude' && i + 1 < raw.length) {
|
|
@@ -214,8 +281,14 @@ function parseArgs(argv) {
|
|
|
214
281
|
args.pinataJwt = raw[++i];
|
|
215
282
|
} else if (flag === '--manifest') {
|
|
216
283
|
args.manifest = true;
|
|
284
|
+
} else if (flag === '--index') {
|
|
285
|
+
args.index = true;
|
|
286
|
+
} else if (flag === '--rebuild') {
|
|
287
|
+
args.rebuild = true;
|
|
217
288
|
} else if (flag === '--force-snapshot') {
|
|
218
289
|
args.forceSnapshot = true;
|
|
290
|
+
} else if (flag === '--detach') {
|
|
291
|
+
args.detach = true;
|
|
219
292
|
} else if (flag === '--poll-interval' && i + 1 < raw.length) {
|
|
220
293
|
args.pollInterval = Number(raw[++i]);
|
|
221
294
|
} else if (flag === '--debounce' && i + 1 < raw.length) {
|
|
@@ -226,6 +299,16 @@ function parseArgs(argv) {
|
|
|
226
299
|
args.pullOnly = true;
|
|
227
300
|
} else if (flag === '--yes') {
|
|
228
301
|
args.yes = true;
|
|
302
|
+
} else if (flag === '--db' && i + 1 < raw.length) {
|
|
303
|
+
args.searchDb = raw[++i];
|
|
304
|
+
} else if (flag === '--model' && i + 1 < raw.length) {
|
|
305
|
+
args.model = raw[++i];
|
|
306
|
+
} else if (flag === '--chunk-size' && i + 1 < raw.length) {
|
|
307
|
+
args.chunkSize = Number(raw[++i]);
|
|
308
|
+
} else if (flag === '--chunk-overlap' && i + 1 < raw.length) {
|
|
309
|
+
args.chunkOverlap = Number(raw[++i]);
|
|
310
|
+
} else if ((flag === '--limit' || flag === '-k') && i + 1 < raw.length) {
|
|
311
|
+
args.limit = Number(raw[++i]);
|
|
229
312
|
}
|
|
230
313
|
i++;
|
|
231
314
|
}
|
|
@@ -253,6 +336,22 @@ console.log(`mnemonad v${VERSION}`);
|
|
|
253
336
|
// anything in the RPC/fetch stack unref's its own I/O (see the process.exit note above).
|
|
254
337
|
const keepAlive = setInterval(() => {}, 60_000);
|
|
255
338
|
|
|
339
|
+
// --passkey's browser tab shows a result line when the command finishes (see
|
|
340
|
+
// closePasskeyBridge()) rather than a bare "done" — commands don't return a summary
|
|
341
|
+
// explicitly, so this captures their own last console.log line instead, the same thing this
|
|
342
|
+
// terminal already sees. Only wired up when --passkey is in play; every other run's
|
|
343
|
+
// console.log is untouched.
|
|
344
|
+
const logLines = [];
|
|
345
|
+
const originalConsoleLog = console.log;
|
|
346
|
+
if (args.passkey) {
|
|
347
|
+
console.log = (...parts) => {
|
|
348
|
+
originalConsoleLog(...parts);
|
|
349
|
+
logLines.push(parts.join(' '));
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
let succeeded = true;
|
|
353
|
+
let errorMessage = null;
|
|
354
|
+
|
|
256
355
|
try {
|
|
257
356
|
if (args.command === 'push') {
|
|
258
357
|
await push(args);
|
|
@@ -268,6 +367,13 @@ try {
|
|
|
268
367
|
await compact(args);
|
|
269
368
|
} else if (args.command === 'burn') {
|
|
270
369
|
await burn(args);
|
|
370
|
+
} else if (args.command === 'index') {
|
|
371
|
+
await buildIndex(args);
|
|
372
|
+
} else if (args.command === 'search') {
|
|
373
|
+
await search(args);
|
|
374
|
+
} else if (args.command === '__push-job') {
|
|
375
|
+
// The detached half of `push --detach` (lib/jobs.js) — never typed by a person.
|
|
376
|
+
await runPushJob(args.path);
|
|
271
377
|
} else {
|
|
272
378
|
console.error('Unknown command:', args.command);
|
|
273
379
|
console.log(USAGE);
|
|
@@ -276,15 +382,26 @@ try {
|
|
|
276
382
|
} catch (err) {
|
|
277
383
|
if (err._isProcessExit) {
|
|
278
384
|
process.exitCode = err._exitCode;
|
|
385
|
+
succeeded = err._exitCode === 0;
|
|
279
386
|
} else if (err._isUserError) {
|
|
280
387
|
// Expected, actionable failure — message only, no stack trace.
|
|
281
388
|
console.error('Error:', err.message);
|
|
282
389
|
process.exitCode = 1;
|
|
390
|
+
succeeded = false;
|
|
283
391
|
} else {
|
|
284
392
|
console.error('Error:', err.message);
|
|
285
393
|
if (err.stack) console.error(err.stack);
|
|
286
394
|
process.exitCode = 1;
|
|
395
|
+
succeeded = false;
|
|
287
396
|
}
|
|
397
|
+
errorMessage = err.message;
|
|
288
398
|
} finally {
|
|
399
|
+
console.log = originalConsoleLog;
|
|
400
|
+
// No-op when --passkey was never used — see passkeyBridge.js. Runs whether the command
|
|
401
|
+
// succeeded or failed, so a browser tab left open on a failed push doesn't linger.
|
|
402
|
+
closePasskeyBridge({
|
|
403
|
+
ok: succeeded,
|
|
404
|
+
summary: succeeded ? (logLines.filter(Boolean).pop() || 'Done.') : (errorMessage || 'Failed.'),
|
|
405
|
+
});
|
|
289
406
|
clearInterval(keepAlive);
|
|
290
407
|
}
|
|
@@ -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
|
+
}
|
package/lib/chainClient.js
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
import { createPublicClient, createWalletClient, defineChain, http } from 'viem';
|
|
9
9
|
import { privateKeyToAccount, mnemonicToAccount } from 'viem/accounts';
|
|
10
10
|
import { monad, monadTestnet } from 'viem/chains';
|
|
11
|
+
import { openPasskeyBridge } from './passkeyBridge.js';
|
|
12
|
+
import { userError } from './commands/shared.js';
|
|
11
13
|
|
|
12
14
|
/**
|
|
13
15
|
* CLI `--chain` name -> { viem chain object, Mnemonad network shorthand for
|
|
@@ -45,10 +47,20 @@ function normalizeKey(key) {
|
|
|
45
47
|
return key.startsWith('0x') ? key : `0x${key}`;
|
|
46
48
|
}
|
|
47
49
|
|
|
48
|
-
|
|
50
|
+
/**
|
|
51
|
+
* `--key`/`--phrase`/`MNEMONAD_KEY` resolve synchronously; `--passkey` doesn't (it opens a
|
|
52
|
+
* browser tab and waits for a human to complete a WebAuthn prompt) — see
|
|
53
|
+
* passkeyBridge.js. That's the one thing that makes this function, and therefore
|
|
54
|
+
* `makeChainClient`, async rather than the plain synchronous lookup it used to be.
|
|
55
|
+
*/
|
|
56
|
+
async function resolveAccount(args) {
|
|
49
57
|
const key = args.key || process.env.MNEMONAD_KEY;
|
|
58
|
+
if (args.passkey && (key || args.phrase)) {
|
|
59
|
+
throw userError('--passkey cannot be combined with --key/--phrase/MNEMONAD_KEY — pick one signing method.');
|
|
60
|
+
}
|
|
50
61
|
if (key) return privateKeyToAccount(normalizeKey(key));
|
|
51
62
|
if (args.phrase) return mnemonicToAccount(args.phrase);
|
|
63
|
+
if (args.passkey) return openPasskeyBridge(args);
|
|
52
64
|
return null;
|
|
53
65
|
}
|
|
54
66
|
|
|
@@ -60,16 +72,17 @@ function resolveAccount(args) {
|
|
|
60
72
|
* `requireWalletClient` themselves, so the "no key" error only surfaces where it's
|
|
61
73
|
* actually required.
|
|
62
74
|
*
|
|
63
|
-
* @param {Object} args - parsed CLI args: `chain`, `rpcUrl`, `key`, `phrase`, `
|
|
64
|
-
*
|
|
75
|
+
* @param {Object} args - parsed CLI args: `chain`, `rpcUrl`, `key`, `phrase`, `passkey`,
|
|
76
|
+
* `authOrigin`, `contractAddress`
|
|
77
|
+
* @returns {Promise<{
|
|
65
78
|
* publicClient: Object,
|
|
66
79
|
* walletClient: ?Object,
|
|
67
80
|
* account: ?Object,
|
|
68
81
|
* contractAddress: string,
|
|
69
82
|
* chain: Object,
|
|
70
|
-
* }}
|
|
83
|
+
* }>}
|
|
71
84
|
*/
|
|
72
|
-
export function makeChainClient(args) {
|
|
85
|
+
export async function makeChainClient(args) {
|
|
73
86
|
let chain, contractAddress;
|
|
74
87
|
if (args.chain === 'local') {
|
|
75
88
|
// Dev/test only — see localChain() above.
|
|
@@ -95,7 +108,7 @@ export function makeChainClient(args) {
|
|
|
95
108
|
const transport = http(args.rpcUrl);
|
|
96
109
|
const publicClient = createPublicClient({ chain, transport });
|
|
97
110
|
|
|
98
|
-
const account = resolveAccount(args);
|
|
111
|
+
const account = await resolveAccount(args);
|
|
99
112
|
const walletClient = account ? createWalletClient({ chain, transport, account }) : null;
|
|
100
113
|
|
|
101
114
|
return { publicClient, walletClient, account, contractAddress, chain };
|
|
@@ -111,7 +124,8 @@ export function requireWalletClient(client) {
|
|
|
111
124
|
// first thing anyone hits before configuring a key, and a stack trace through
|
|
112
125
|
// chainClient makes an actionable one-liner look like a crash.
|
|
113
126
|
const err = new Error(
|
|
114
|
-
'No signing key. Set the MNEMONAD_KEY env var to a private key,
|
|
127
|
+
'No signing key. Set the MNEMONAD_KEY env var to a private key, pass --key / --phrase, ' +
|
|
128
|
+
'or pass --passkey to sign in with a passkey via the browser.'
|
|
115
129
|
);
|
|
116
130
|
err._isUserError = true;
|
|
117
131
|
throw err;
|