mnemonad-cli 0.2.0 → 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 +113 -9
- package/bin/mnemonad.js +111 -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 +1 -1
- package/lib/commands/diff.js +1 -1
- package/lib/commands/index.js +2 -0
- package/lib/commands/info.js +1 -1
- package/lib/commands/pull.js +1 -1
- package/lib/commands/push.js +44 -1
- package/lib/commands/search.js +91 -0
- package/lib/commands/shared.js +3 -2
- package/lib/commands/watch.js +58 -9
- 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 +13 -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
|
|
@@ -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,18 @@ 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
|
+
| `--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
122
|
| `--poll-interval <s>` | `watch`: seconds between remote version checks (default: `2`) |
|
|
104
123
|
| `--debounce <ms>` | `watch`: quiet period in ms before pushing after a local change (default: `1000`) |
|
|
105
124
|
| `--push-only` | `watch`: disable auto-pull |
|
|
106
125
|
| `--pull-only` | `watch`: disable auto-push |
|
|
107
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`) |
|
|
108
133
|
| `--help` | Show help |
|
|
109
134
|
|
|
110
135
|
### Authentication
|
|
@@ -125,6 +150,21 @@ mnemonad push ~/my-data
|
|
|
125
150
|
A mnemonic works instead of a raw key: `--phrase "word1 word2 ..."`. `--key` and
|
|
126
151
|
`MNEMONAD_KEY` both take precedence over `--phrase`.
|
|
127
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
|
+
|
|
128
168
|
`pull`, `info` and `diff` work without a key for public (unencrypted) streams. A key is
|
|
129
169
|
required for encrypted streams that use wallet-based encryption (not needed for
|
|
130
170
|
password-based encryption — see below), and for any `push`.
|
|
@@ -320,6 +360,63 @@ mnemonad push 123 ~/my-data --force-snapshot
|
|
|
320
360
|
# restore() will recover from this snapshot, skipping any corrupt diffs before it
|
|
321
361
|
```
|
|
322
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
|
+
|
|
323
420
|
## End-to-end workflow
|
|
324
421
|
|
|
325
422
|
Create a stream, verify what was stored, then roll back to an earlier version:
|
|
@@ -368,10 +465,13 @@ mnemonad pull 123 ~/my-data-v1 --version 1
|
|
|
368
465
|
the on-chain version.
|
|
369
466
|
5. **watch** — combines push and pull in a loop. A filesystem watcher triggers a debounced
|
|
370
467
|
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
|
-
|
|
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.
|
|
375
475
|
6. **compact** — drops all existing history on-chain and pushes the current folder as a
|
|
376
476
|
single fresh full snapshot, speeding up future replay after many incremental syncs.
|
|
377
477
|
|
|
@@ -379,7 +479,8 @@ mnemonad pull 123 ~/my-data-v1 --version 1
|
|
|
379
479
|
|
|
380
480
|
The following are always excluded from snapshots: `node_modules`, `.git`, `.env`,
|
|
381
481
|
`.DS_Store`, `.mnemonad`, `.claude`, `pnpm-lock.yaml`, `package-lock.json`. Add more with
|
|
382
|
-
`--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.
|
|
383
484
|
|
|
384
485
|
## Dependencies
|
|
385
486
|
|
|
@@ -387,4 +488,7 @@ The following are always excluded from snapshots: `node_modules`, `.git`, `.env`
|
|
|
387
488
|
|---|---|
|
|
388
489
|
| `mnemonad` | On-chain stream primitive (this repo's `js/`) |
|
|
389
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 |
|
|
390
494
|
| `viem` | Monad client / key management |
|
package/bin/mnemonad.js
CHANGED
|
@@ -17,8 +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, 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';
|
|
22
23
|
import defaultConfig from '../mnemonad.config.js';
|
|
23
24
|
|
|
24
25
|
const PKG_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
@@ -40,7 +41,7 @@ const CONFIG = defaultConfig || {};
|
|
|
40
41
|
// Same default the explorer dApp uses (explorer/src/mnemonad/presignProvider.js) — a
|
|
41
42
|
// public read gateway so pulling/diffing/inspecting a stream with IPFS-offloaded items
|
|
42
43
|
// works out of the box, with no setup, for anyone who didn't push it themselves.
|
|
43
|
-
const DEFAULT_IPFS_GATEWAY = CONFIG.gatewayUrl || 'https://
|
|
44
|
+
const DEFAULT_IPFS_GATEWAY = CONFIG.gatewayUrl || 'https://azure-casual-firefly-850.mypinata.cloud/ipfs/';
|
|
44
45
|
|
|
45
46
|
// The deployed presign server (presign-server/) this project runs by default — see its own
|
|
46
47
|
// README for what it does. Pushing an item too big to fit on-chain uses this automatically
|
|
@@ -48,6 +49,10 @@ const DEFAULT_IPFS_GATEWAY = CONFIG.gatewayUrl || 'https://gateway.pinata.cloud/
|
|
|
48
49
|
// alternative for anyone who'd rather supply their own.
|
|
49
50
|
const DEFAULT_PRESIGN_URL = CONFIG.presignUrl || null;
|
|
50
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
|
+
|
|
51
56
|
const USAGE = `mnemonad v${VERSION} — sync local folders to a versioned, diffed stream on Monad
|
|
52
57
|
|
|
53
58
|
Usage:
|
|
@@ -58,6 +63,9 @@ Usage:
|
|
|
58
63
|
mnemonad watch <stream-id> <path> [options] Watch folder and auto-push/pull
|
|
59
64
|
mnemonad compact <stream-id> <path> [options] Truncate old history, push a fresh snapshot
|
|
60
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
|
|
61
69
|
|
|
62
70
|
<path> is always required wherever a command touches a folder — the CLI never assumes the
|
|
63
71
|
current directory. The exceptions are \`mnemonad info\` with no stream-id (just checks your
|
|
@@ -73,11 +81,17 @@ Options:
|
|
|
73
81
|
--contract-address <addr> Registry address to use instead of the chain's known deployment
|
|
74
82
|
(required with --chain local)
|
|
75
83
|
--gateway-url <url> IPFS gateway for reading externally-offloaded items
|
|
76
|
-
(default:
|
|
84
|
+
(default: this project's Pinata dedicated gateway, or MNEMONAD_IPFS_GATEWAY env var)
|
|
77
85
|
--presign-url <url> Presign server for paid IPFS uploads with no Pinata credential
|
|
78
86
|
(default: the deployed server in mnemonad.config.js, or MNEMONAD_PRESIGN_URL env var)
|
|
79
87
|
--key <privkey> Monad private key (or set MNEMONAD_KEY env var)
|
|
80
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)
|
|
81
95
|
--version <n> Version to restore/compare (pull, diff, info; default: latest)
|
|
82
96
|
--exclude <p1,p2> Extra exclude patterns (comma-separated)
|
|
83
97
|
--no-compress Disable gzip compression
|
|
@@ -90,12 +104,35 @@ Options:
|
|
|
90
104
|
With no stream-id given, every command recovers it from an
|
|
91
105
|
existing .mnemonad in <path> instead of requiring it explicitly
|
|
92
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\`
|
|
93
110
|
--poll-interval <s> Watch: seconds between remote checks (default: 2)
|
|
94
111
|
--debounce <ms> Watch: ms quiet period before pushing after a change (default: 1000)
|
|
95
112
|
--push-only Watch: disable auto-pull
|
|
96
113
|
--pull-only Watch: disable auto-push
|
|
97
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)
|
|
98
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.
|
|
99
136
|
`;
|
|
100
137
|
|
|
101
138
|
function parseArgs(argv) {
|
|
@@ -112,6 +149,8 @@ function parseArgs(argv) {
|
|
|
112
149
|
presignUrl: process.env.MNEMONAD_PRESIGN_URL || DEFAULT_PRESIGN_URL,
|
|
113
150
|
key: null,
|
|
114
151
|
phrase: null,
|
|
152
|
+
passkey: false,
|
|
153
|
+
authOrigin: process.env.MNEMONAD_AUTH_ORIGIN || DEFAULT_AUTH_ORIGIN,
|
|
115
154
|
version: null,
|
|
116
155
|
exclude: null,
|
|
117
156
|
compress: 'gzip',
|
|
@@ -120,12 +159,20 @@ function parseArgs(argv) {
|
|
|
120
159
|
password: null,
|
|
121
160
|
pinataJwt: process.env.PINATA_JWT || null,
|
|
122
161
|
manifest: false,
|
|
162
|
+
index: false,
|
|
163
|
+
rebuild: false,
|
|
123
164
|
forceSnapshot: false,
|
|
124
165
|
pollInterval: 2,
|
|
125
166
|
debounce: 1000,
|
|
126
167
|
pushOnly: false,
|
|
127
168
|
pullOnly: false,
|
|
128
169
|
yes: false,
|
|
170
|
+
query: null,
|
|
171
|
+
searchDb: null,
|
|
172
|
+
model: null,
|
|
173
|
+
chunkSize: null,
|
|
174
|
+
chunkOverlap: null,
|
|
175
|
+
limit: null,
|
|
129
176
|
};
|
|
130
177
|
|
|
131
178
|
const raw = argv.slice(2);
|
|
@@ -164,12 +211,22 @@ function parseArgs(argv) {
|
|
|
164
211
|
return qualified.id;
|
|
165
212
|
};
|
|
166
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';
|
|
167
218
|
const positionals = [];
|
|
168
|
-
while (i < raw.length && !raw[i]
|
|
219
|
+
while (i < raw.length && !isFlag(raw[i])) {
|
|
169
220
|
positionals.push(raw[i]);
|
|
170
221
|
i++;
|
|
171
222
|
}
|
|
172
|
-
if (
|
|
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) {
|
|
173
230
|
args.streamId = takeStreamId(positionals[0]);
|
|
174
231
|
args.path = positionals[1];
|
|
175
232
|
} else if (positionals.length === 1) {
|
|
@@ -198,6 +255,10 @@ function parseArgs(argv) {
|
|
|
198
255
|
args.key = raw[++i];
|
|
199
256
|
} else if (flag === '--phrase' && i + 1 < raw.length) {
|
|
200
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];
|
|
201
262
|
} else if (flag === '--version' && i + 1 < raw.length) {
|
|
202
263
|
args.version = raw[++i];
|
|
203
264
|
} else if (flag === '--exclude' && i + 1 < raw.length) {
|
|
@@ -214,6 +275,10 @@ function parseArgs(argv) {
|
|
|
214
275
|
args.pinataJwt = raw[++i];
|
|
215
276
|
} else if (flag === '--manifest') {
|
|
216
277
|
args.manifest = true;
|
|
278
|
+
} else if (flag === '--index') {
|
|
279
|
+
args.index = true;
|
|
280
|
+
} else if (flag === '--rebuild') {
|
|
281
|
+
args.rebuild = true;
|
|
217
282
|
} else if (flag === '--force-snapshot') {
|
|
218
283
|
args.forceSnapshot = true;
|
|
219
284
|
} else if (flag === '--poll-interval' && i + 1 < raw.length) {
|
|
@@ -226,6 +291,16 @@ function parseArgs(argv) {
|
|
|
226
291
|
args.pullOnly = true;
|
|
227
292
|
} else if (flag === '--yes') {
|
|
228
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]);
|
|
229
304
|
}
|
|
230
305
|
i++;
|
|
231
306
|
}
|
|
@@ -253,6 +328,22 @@ console.log(`mnemonad v${VERSION}`);
|
|
|
253
328
|
// anything in the RPC/fetch stack unref's its own I/O (see the process.exit note above).
|
|
254
329
|
const keepAlive = setInterval(() => {}, 60_000);
|
|
255
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
|
+
|
|
256
347
|
try {
|
|
257
348
|
if (args.command === 'push') {
|
|
258
349
|
await push(args);
|
|
@@ -268,6 +359,10 @@ try {
|
|
|
268
359
|
await compact(args);
|
|
269
360
|
} else if (args.command === 'burn') {
|
|
270
361
|
await burn(args);
|
|
362
|
+
} else if (args.command === 'index') {
|
|
363
|
+
await buildIndex(args);
|
|
364
|
+
} else if (args.command === 'search') {
|
|
365
|
+
await search(args);
|
|
271
366
|
} else {
|
|
272
367
|
console.error('Unknown command:', args.command);
|
|
273
368
|
console.log(USAGE);
|
|
@@ -276,15 +371,26 @@ try {
|
|
|
276
371
|
} catch (err) {
|
|
277
372
|
if (err._isProcessExit) {
|
|
278
373
|
process.exitCode = err._exitCode;
|
|
374
|
+
succeeded = err._exitCode === 0;
|
|
279
375
|
} else if (err._isUserError) {
|
|
280
376
|
// Expected, actionable failure — message only, no stack trace.
|
|
281
377
|
console.error('Error:', err.message);
|
|
282
378
|
process.exitCode = 1;
|
|
379
|
+
succeeded = false;
|
|
283
380
|
} else {
|
|
284
381
|
console.error('Error:', err.message);
|
|
285
382
|
if (err.stack) console.error(err.stack);
|
|
286
383
|
process.exitCode = 1;
|
|
384
|
+
succeeded = false;
|
|
287
385
|
}
|
|
386
|
+
errorMessage = err.message;
|
|
288
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
|
+
});
|
|
289
395
|
clearInterval(keepAlive);
|
|
290
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
|
+
}
|
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;
|