arkiv-cli 0.0.0-stage → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Harish Kotra
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,513 @@
1
- # Temporary Holding Version
1
+ # arkiv
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A terminal toolkit for the [Arkiv](https://arkiv.network) network. One binary that takes you from "I have a key" to "I can read and write entities" without reading the SDK source first.
4
+
5
+ ```
6
+ arkiv init [dir] scaffold a runnable project
7
+ arkiv doctor check node, keys, balance, network and .env safety
8
+ arkiv query <expression> run a query
9
+ arkiv entity <key> read one entity in full
10
+ arkiv schema [name] discover schemas, lint their names, or open one
11
+ arkiv create write one entity
12
+ arkiv watch <expression> poll a query and print changes
13
+ arkiv cost size a write before you send it
14
+ arkiv limits protocol limits in one table
15
+ ```
16
+
17
+ ## Why this exists
18
+
19
+ Arkiv ships a good SDK and good docs. What it does not ship is the loop every developer runs on day one: check the environment, get a key working, run one query, read one entity, write one entity, and see the write land. Today that loop is assembled by hand from docs pages and examples, and it fails in ways the SDK cannot explain. Wrong node version. A key with a stray newline. A rate-limited public RPC. A schema name that is legal but collides with someone else's.
20
+
21
+ `arkiv` is that loop, in the terminal, with the failure modes named.
22
+
23
+ ## Install
24
+
25
+ ```bash
26
+ npm install -g arkiv-cli # or: npx arkiv-cli <command>
27
+ ```
28
+
29
+ Node 22.10 or newer. That floor comes from the SDK, not from taste. Verified on Node 26.
30
+
31
+ From source:
32
+
33
+ ```bash
34
+ git clone https://github.com/harishkotra/arkiv-cli
35
+ cd arkiv-cli
36
+ npm install
37
+ npm run build
38
+ node dist/cli.js doctor
39
+ ```
40
+
41
+ ## Stack
42
+
43
+ | Layer | Choice | Why |
44
+ | --- | --- | --- |
45
+ | Language | TypeScript 5.6, ES2022, NodeNext | Types catch the SDK's positional APIs at build time |
46
+ | Runtime | Node 22.10+ | The SDK's floor |
47
+ | Chain access | `@arkiv-network/sdk` | The same client everyone else uses, no private endpoints |
48
+ | Transport and accounts | `viem` | Key validation, address derivation, `http()` transport |
49
+ | CLI plumbing | Hand written | No framework. ~140 lines of parser buys strict flags and typed values |
50
+ | Tests | `node:test` | Ships with Node, no test runner to configure |
51
+ | Build | `tsc` | One step, no bundler |
52
+
53
+ Runtime dependencies: two. That is deliberate. A CLI that pulls thirty packages to print a table is harder to audit than one you can read in an evening.
54
+
55
+ ## Credentials
56
+
57
+ Two environment variables, read from `.env` in the current directory (or `--env <path>`, or the process environment):
58
+
59
+ ```bash
60
+ ARKIV_ACCESS_KEY=... # free at https://hub.arkiv.network/access-keys
61
+ ARKIV_PRIVATE_KEY=0x... # only needed for writes
62
+ ARKIV_RPC_URL=... # optional, overrides the network default
63
+ ```
64
+
65
+ The CLI never generates a wallet, never derives an address for you to copy, never prints a key. `doctor` checks the format of both values without echoing them, and every value it validates registers with the output redactor, so a value that leaks into an error message comes back as `[REDACTED]`.
66
+
67
+ Copy `.env.example` to `.env` to start. `.gitignore` already excludes `.env`; `doctor` fails loudly if it does not.
68
+
69
+ ## Quickstart
70
+
71
+ ```bash
72
+ npx arkiv-cli init my-app
73
+ cd my-app
74
+ cp .env.example .env # paste your key, keep the file out of git
75
+ npx arkiv-cli doctor
76
+ npm run dev
77
+ ```
78
+
79
+ `init` writes a single-file runnable project (TypeScript, `tsx`, the SDK), and refuses to touch a directory that already has files unless you pass `--force`.
80
+
81
+ ## Commands
82
+
83
+ ### `arkiv doctor`
84
+
85
+ The front door. Every check prints `ok`, `warn` or `fail` with a fix:
86
+
87
+ - Node version against the SDK's floor (22.10)
88
+ - `ARKIV_ACCESS_KEY` presence and shape
89
+ - `ARKIV_PRIVATE_KEY` presence and shape (only for write work)
90
+ - tGLM balance for the configured key
91
+ - clock skew between your machine and the node's last block
92
+ - reachability and chain id of the configured network
93
+ - whether `.env` is git-ignored and whether its mode is world-readable
94
+
95
+ Exit code is non-zero only when a check `fail`s, so it is safe in CI.
96
+
97
+ ```bash
98
+ arkiv doctor
99
+ arkiv doctor --json | jq '.checks[] | select(.status != "ok")'
100
+ ```
101
+
102
+ ### `arkiv query <expression>`
103
+
104
+ ![arkiv query](https://raw.githubusercontent.com/harishkotra/arkiv-cli/main/docs/screenshots/03-query.png)
105
+
106
+ Runs a query with the SDK's own predicate language, no translation layer:
107
+
108
+ ```bash
109
+ arkiv query "arkiv.schema = str('notes/v1')" --limit 5
110
+ arkiv query "EXISTS(title) AND kind = str('note')" --sort updatedAt --type u64 --desc
111
+ arkiv query "arkiv.schema = str('notes/v1')" --count
112
+ arkiv query "..." --key # keys only, cheap to pipe
113
+ arkiv query "..." --select title,kind
114
+ ```
115
+
116
+ `--schema <name>` is a shortcut that adds `arkiv.schema = str(name)` and refuses names that could break out of the string literal. An empty expression is a usage error rather than a full scan. `--limit` is capped at 100, which is the node's page size.
117
+
118
+ ### `arkiv entity <key>`
119
+
120
+ ![arkiv entity](https://raw.githubusercontent.com/harishkotra/arkiv-cli/main/docs/screenshots/04-entity.png)
121
+
122
+ Reads one entity in full: owner, created and expiry timestamps, attribute schema, every attribute value, and an explorer link. The key is validated locally (`0x` plus 64 hex) before any network call, so a typo costs nothing and exits 2.
123
+
124
+ Attribute values are chain data, which means arbitrary bytes. Every value is stripped of control sequences and non-printable characters before it reaches your terminal, and long values are truncated with a byte count.
125
+
126
+ ### `arkiv schema [name]`
127
+
128
+ ![arkiv schema](https://raw.githubusercontent.com/harishkotra/arkiv-cli/main/docs/screenshots/05-schema.png)
129
+
130
+ There is no "list schemas" call on the node, so this pages through entities that carry `arkiv.schema`, samples them, and builds the picture from what is actually written:
131
+
132
+ ```bash
133
+ arkiv schema # every schema seen in the sample, with owners and counts
134
+ arkiv schema notes/v1 # fields, types, index flags, sample values for one schema
135
+ arkiv schema --sample 1000 # sample deeper
136
+ arkiv schema --atlas atlas.html # write a standalone HTML page for the sample
137
+ ```
138
+
139
+ A named schema is queried directly rather than filtered out of a sample, so it resolves even when it sits deep in the keyspace.
140
+
141
+ The linter flags the naming problems that make a schema hard to find or unsafe to extend:
142
+
143
+ - dots, slashes and hyphens mixed in one namespace
144
+ - no version suffix, so the schema cannot evolve
145
+ - names longer than the protocol's 32-byte attribute-name limit
146
+ - the same schema name written by more than one owner (collision)
147
+ - `str` attributes close to the 128-byte limit
148
+ - entities at or near the 32-attribute limit
149
+
150
+ ### `arkiv create`
151
+
152
+ ![arkiv create dry run](https://raw.githubusercontent.com/harishkotra/arkiv-cli/main/docs/screenshots/07-create.png)
153
+
154
+ Writes one entity. The schema is built at runtime from the flags, so there is no separate schema file to keep in sync:
155
+
156
+ ```bash
157
+ arkiv create --schema notes/v1 \
158
+ --set title=str:Hello \
159
+ --set kind=str:note \
160
+ --set body=bytes:@./README.md \
161
+ --expires 30d \
162
+ --dry-run
163
+ ```
164
+
165
+ Attribute syntax is `name=type:value`, where type is one of `str`, `u64`, `i32`, `u256`, `bool`, `bytes`, `addr`, `key`, `dec`. `bytes:@path` reads a file; `bytes:0x...` takes hex. `--expires` accepts `s`, `m`, `h`, `d`, `w`, `y` and is validated against the protocol's maximum lifetime.
166
+
167
+ `--dry-run` prints the exact plan (attribute table, payload bytes, blocks until expiry, gas estimate) and stops before signing. Use it to check a write, and in tests.
168
+
169
+ ### `arkiv watch <expression>`
170
+
171
+ ![arkiv watch](https://raw.githubusercontent.com/harishkotra/arkiv-cli/main/docs/screenshots/08-watch.png)
172
+
173
+ Polls a query and prints only what changed:
174
+
175
+ ```bash
176
+ arkiv watch "arkiv.schema = str('notes/v1')" --interval 5
177
+ arkiv watch "..." --once # one snapshot, then exit
178
+ ```
179
+
180
+ The watcher keeps a bounded, key-ordered window of matches and diffs it each tick. When the window saturates it says so, because a full window can hide new entities behind the ordering.
181
+
182
+ ### `arkiv cost`
183
+
184
+ ![arkiv cost](https://raw.githubusercontent.com/harishkotra/arkiv-cli/main/docs/screenshots/09-cost.png)
185
+
186
+ Sizes a write before you pay for it:
187
+
188
+ ```bash
189
+ arkiv cost --bytes 2048 --attributes 4 --expires 30d
190
+ arkiv cost --file ./payload.json --attributes 6
191
+ ```
192
+
193
+ Reports payload bytes, attribute overhead, blocks until expiry, the share of each protocol limit the write consumes, how many such writes fit in the limits, and the live gas price. It does not invent a GLM price: the network's pricing page is the source for that, and a CLI that guesses is worse than one that shows you the inputs.
194
+
195
+ ### `arkiv limits`
196
+
197
+ ![arkiv limits](https://raw.githubusercontent.com/harishkotra/arkiv-cli/main/docs/screenshots/10-limits.png)
198
+
199
+ The protocol's hard limits in one table, sourced from the SDK's own exports with documented fallbacks, so the CLI cannot drift from the chain.
200
+
201
+ ## Configuration
202
+
203
+ | Flag | Meaning |
204
+ | --- | --- |
205
+ | `--network <name>` | `rabdi` (default) or `localhost` |
206
+ | `--rpc <url>` | override the RPC endpoint (http/https only) |
207
+ | `--env <path>` | use a different env file |
208
+ | `--json` | machine-readable output where supported |
209
+ | `--no-colour` | plain text, also honoured via `NO_COLOR` |
210
+ | `--verbose` | extra detail on failures |
211
+ | `-h`, `--help` | help for a command |
212
+ | `-v`, `--version` | version |
213
+
214
+ Unknown flags are an error, not something to ignore. The argument parser rejects them, because a silently ignored `--limt 5` looks like a query that returned everything.
215
+
216
+ ## Architecture
217
+
218
+ ```mermaid
219
+ flowchart TD
220
+ A["argv"] --> B["args.ts<br/>strict parser"]
221
+ B --> C["cli.ts<br/>dispatch + exit codes"]
222
+ C --> D["context.ts<br/>network, env file, client"]
223
+ D --> E["env.ts<br/>dotenv subset + secret registry"]
224
+ D --> F["arkiv.ts<br/>client and wallet factories"]
225
+ F --> G["@arkiv-network/sdk"]
226
+ F --> H["viem<br/>transport + accounts"]
227
+ C --> I["commands/query.ts"]
228
+ C --> J["commands/schema.ts"]
229
+ C --> K["commands/entity.ts"]
230
+ C --> L["commands/create.ts"]
231
+ C --> M["commands/watch.ts"]
232
+ C --> N["commands/cost.ts"]
233
+ C --> O["commands/doctor.ts"]
234
+ C --> P["commands/init.ts"]
235
+ I --> Q["values.ts<br/>types, sizes, printable text"]
236
+ J --> Q
237
+ L --> Q
238
+ I --> R["render.ts<br/>tables and entity blocks"]
239
+ J --> R
240
+ M --> R
241
+ C --> S["ui.ts<br/>colour, sanitize, redact"]
242
+ S --> T["stdout / --json"]
243
+ ```
244
+
245
+ The same flow as text, for anyone reading this in a terminal:
246
+
247
+ ```
248
+ argv
249
+ └─ args.ts ──► cli.ts ──► context.ts ──► arkiv.ts ──► SDK ──► RPC
250
+ │ │
251
+ │ └─ env.ts (dotenv, secret registry, URL guard)
252
+ │
253
+ ├─ commands/{init,doctor,query,entity,schema,create,watch,cost}
254
+ │ ├─ values.ts (attribute types and sizing)
255
+ │ └─ render.ts (tables, entity blocks)
256
+ │
257
+ └─ ui.ts (colour, sanitize, redact) ──► stdout
258
+ ```
259
+
260
+ ### Module map
261
+
262
+ | Module | Lines | Responsibility |
263
+ | --- | --- | --- |
264
+ | `commands/schema.ts` | 499 | Sampling, schema map, linter, atlas renderer |
265
+ | `cli.ts` | 396 | Flag declarations, dispatch, exit codes, help |
266
+ | `commands/create.ts` | 299 | Attribute parsing, limit checks, dry run, signing |
267
+ | `arkiv.ts` | 241 | Network table, client and wallet factories, notice capture |
268
+ | `commands/init.ts` | 226 | Scaffold templates and safe writes |
269
+ | `commands/doctor.ts` | 224 | Checks, fixes, JSON report |
270
+ | `commands/cost.ts` | 188 | Lifetime parsing, byte math, limit comparison |
271
+ | `ui.ts` | 176 | Colour, sanitize, redact, table primitives |
272
+ | `commands/watch.ts` | 147 | Windowed polling and diffing |
273
+ | `env.ts` | 146 | dotenv subset, mode checks, secret capture, URL guard |
274
+ | `values.ts` | 141 | Attribute constructors, sizing, safe display |
275
+ | `commands/query.ts` | 140 | Expression building, sort, paging |
276
+ | `args.ts` | 139 | Strict argument parser |
277
+ | `commands/entity.ts` | 109 | Key validation and single read |
278
+ | `render.ts` | 96 | Entity rendering |
279
+ | `context.ts` | 54 | Wiring: env, network, client |
280
+ | `limits.ts` | 47 | Protocol constants in one place |
281
+
282
+ About 3,400 lines including tests.
283
+
284
+ ## How a few parts work
285
+
286
+ ### Arguments are parsed strictly
287
+
288
+ The parser knows every flag, its type and whether it repeats. Anything else is a usage error with the valid flags printed. That is why `--limit` cannot arrive as a string, and why repeated `--set` flags accumulate instead of overwriting:
289
+
290
+ ```ts
291
+ const put = (key: string, value: FlagValue): void => {
292
+ const previous = flags[key];
293
+ if (spec.repeat) {
294
+ const list = Array.isArray(previous) ? previous : previous === undefined ? [] : [previous];
295
+ flags[key] = [...list, value];
296
+ return;
297
+ }
298
+ if (previous !== undefined) throw new UsageError(`--${key} was given more than once`);
299
+ flags[key] = value;
300
+ };
301
+ ```
302
+
303
+ ### Query expressions are built with a guard, not concatenated blindly
304
+
305
+ ```ts
306
+ export function buildExpression(options: QueryOptions): string {
307
+ const parts: string[] = [];
308
+ const raw = options.expression.trim();
309
+ if (raw) parts.push(`(${raw})`);
310
+ if (options.schema) {
311
+ if (!/^[A-Za-z0-9._/-]{1,64}$/.test(options.schema)) {
312
+ throw new UsageError("--schema contains characters that are not allowed in a schema name");
313
+ }
314
+ parts.push(`arkiv.schema = str('${options.schema}')`);
315
+ }
316
+ if (parts.length === 0) {
317
+ throw new UsageError(
318
+ 'give a filter expression, for example: arkiv query "arkiv.schema = str(\'my-app-v1.post\')"',
319
+ );
320
+ }
321
+ const expression = parts.join(" AND ");
322
+ if (expression.length > 2000) throw new UsageError("expression is too long (2000 character limit)");
323
+ return expression;
324
+ }
325
+ ```
326
+
327
+ `--schema` is checked against a character whitelist because that value is interpolated into the predicate, not bound. The free-form expression is passed through as written, since writing predicates is the point of the command.
328
+
329
+ ### Redaction sits in front of stdout
330
+
331
+ ```ts
332
+ export function redact(input: unknown): string {
333
+ let s = typeof input === "string" ? input : String(input ?? "");
334
+ for (const secret of secrets) {
335
+ if (secret) s = s.split(secret).join("[REDACTED]");
336
+ }
337
+ s = s
338
+ .split("\n")
339
+ .map((line) => (KEY_CONTEXT_RE.test(line) ? line.replace(KEY_RE, "0x[REDACTED]") : line))
340
+ .join("\n");
341
+ s = s.replace(ASSIGN_RE, (_m, p1: string) => `${p1}[REDACTED]`);
342
+ return s;
343
+ }
344
+ ```
345
+
346
+ Registered secrets are replaced everywhere. Key-shaped strings are only redacted on lines that talk about key material, because entity keys are also 64 hex characters and redacting those would break the tool. The first version got this wrong and printed `key 0x[REDACTED]` for a valid entity.
347
+
348
+ Chain data takes a different path. `sanitize()` strips ANSI escapes and replaces control characters, so a hostile attribute value cannot move your cursor or repaint the screen.
349
+
350
+ ### Watching uses a window, not a cursor
351
+
352
+ Arkiv's public RPC has no subscription stream, so the watcher reads a key-ordered window each tick and diffs it:
353
+
354
+ ```ts
355
+ const next = new Map(current.map((entity) => [entity.$key, entity]));
356
+ for (const [key, entity] of next) {
357
+ if (previous.has(key)) continue;
358
+ out(`${green("+")} ${grey(stamp(entity))} ${summarise(entity)}`);
359
+ }
360
+ for (const [key, entity] of previous) {
361
+ if (next.has(key)) continue;
362
+ out(`${red("-")} ${grey(stamp(entity))} ${summarise(entity)}`);
363
+ }
364
+ previous = next;
365
+ ```
366
+
367
+ Ordering by key keeps the window stable between ticks. If the window fills, the CLI prints a saturation warning rather than silently dropping entities that fell outside the range.
368
+
369
+ ### SDK noise is captured, not printed
370
+
371
+ The SDK writes a rate-limit notice through `console.warn`. Left alone it lands in the middle of a table and breaks `--json`:
372
+
373
+ ```ts
374
+ function quietWarn(): () => void {
375
+ const originals = { log: console.log, warn: console.warn, info: console.info, debug: console.debug };
376
+ const capture = (...args: unknown[]) => {
377
+ const text = args.map(String).join(" ");
378
+ if (text.includes("heavily rate limited")) {
379
+ capturedNotice = text.trim();
380
+ return;
381
+ }
382
+ originals.log(...args);
383
+ };
384
+ console.log = capture;
385
+ console.info = capture;
386
+ console.debug = capture;
387
+ console.warn = capture;
388
+ // returns a restore function, called in a finally block
389
+ }
390
+ ```
391
+
392
+ The notice is replayed once at the end, after the results, so the information survives without corrupting the output.
393
+
394
+ ## Security model
395
+
396
+ The threat model is a CLI that talks to a public network and prints data it did not author.
397
+
398
+ - **Credentials cannot be printed.** `.env` values are parsed without shell expansion (a hostile `.env` cannot execute anything), validated, and registered with a central redactor on read. Every output path runs through `sanitize`/`redact`, so key-shaped strings and registered secrets become `[REDACTED]`.
399
+ - **No key material on disk.** The CLI never writes a private key anywhere, never generates one, and never puts one in a URL.
400
+ - **Chain data is untrusted.** Attribute values are arbitrary bytes and can contain ANSI escapes or terminal control sequences. All chain-sourced strings are stripped before display.
401
+ - **RPC URLs are constrained.** Only `http`/`https` are accepted; anything else is refused before a client is built.
402
+ - **No implicit writes.** Writes require an explicit `create`, and `--dry-run` gives you the plan first. Nothing else in the CLI mutates the chain.
403
+ - **No destructive defaults.** `init` uses `wx` (fail if the file exists) and requires `--force` for a non-empty directory.
404
+ - **Rate-limit notices are captured, not printed over your output.** The SDK's public-endpoint warning is intercepted on every console channel and surfaced once, at the end, so it cannot corrupt `--json`.
405
+
406
+ Tests cover the parser, the dotenv reader, the redactor, the sanitizer, the query builder and the protocol-limit checks. 23 tests, run against the built output because that is what ships.
407
+
408
+ ## The Schema Atlas
409
+
410
+ `schema --atlas out.html` writes a standalone page (no network calls, no scripts) listing every schema in the sample, its owners, attribute names and types. It is generated from live data, so it is a snapshot rather than a registry. `docs/schema-atlas.html` is one such snapshot of Rabdi, built from a 500 entity sample: 78 schemas.
411
+
412
+ ## Development
413
+
414
+ ```bash
415
+ npm install
416
+ npm run build # tsc -> dist/
417
+ npm test # build + node --test
418
+ npm run typecheck # no emit
419
+ npm run dev -- ... # run from source with tsx
420
+ ```
421
+
422
+ Tests run against `dist/` on purpose. A green suite against source that fails after `tsc` is a class of bug worth excluding.
423
+
424
+ Repo layout:
425
+
426
+ ```
427
+ arkiv-cli/
428
+ ├─ src/
429
+ │ ├─ cli.ts command surface and flag declarations
430
+ │ ├─ args.ts strict argument parser
431
+ │ ├─ env.ts dotenv reading, mode checks, credential validation
432
+ │ ├─ arkiv.ts client construction, network table, quiet SDK warnings
433
+ │ ├─ context.ts wiring
434
+ │ ├─ values.ts attribute constructors, safe display, sizing
435
+ │ ├─ render.ts table and entity rendering
436
+ │ ├─ ui.ts colour, sanitize, redact
437
+ │ ├─ limits.ts protocol limits with fallbacks
438
+ │ └─ commands/ one module per command
439
+ ├─ test/ node:test suites against dist/
440
+ ├─ docs/
441
+ │ ├─ schema-atlas.html generated atlas
442
+ │ ├─ screenshots/ real runs, 2x PNG
443
+ │ └─ blog/ technical write-up
444
+ └─ dist/ build output, not committed
445
+ ```
446
+
447
+ ## Contributing
448
+
449
+ Fork, branch, write a failing test, make it pass, open a PR. Keep the diff small and put the reasoning in the PR body.
450
+
451
+ ```bash
452
+ gh repo fork harishkotra/arkiv-cli --clone
453
+ cd arkiv-cli
454
+ npm install
455
+ npm test
456
+ git checkout -b feat/your-thing
457
+ ```
458
+
459
+ Two rules matter more than style:
460
+
461
+ 1. Nothing that touches a credential gets printed. Add a test if you touch output.
462
+ 2. New protocol limits go in `limits.ts`, never inline.
463
+
464
+ A good PR shows the command working, includes the failure path, and says what you chose not to do.
465
+
466
+ ### Good first issues
467
+
468
+ - `arkiv schema --diff a b`, printing field additions and removals between two schema names.
469
+ - `--format csv` for `query` and `schema`, alongside the existing `--json`.
470
+ - Shell completion generation for zsh, bash and fish.
471
+ - `arkiv export --schema <name> --out dir/`, writing one JSON file per entity and resuming by key.
472
+ - Colour handling: honour `NO_COLOR`, `FORCE_COLOR` and `isatty` in one place, with tests.
473
+
474
+ ### Larger ideas
475
+
476
+ - `arkiv push <dir>`, reading a directory of JSON entities and writing them with a per-entity dry run and idempotency by key.
477
+ - `arkiv replay`, recording a query window to disk and replaying it offline, so CI can test against a fixed view of the chain.
478
+ - Schema drift in the atlas: show what changed in a namespace between two snapshots.
479
+ - `arkiv bench`, measuring read latency and write confirmation time per network.
480
+ - An MCP server exposing `query`, `schema` and `create --dry-run` to agents, reusing the validated core.
481
+ - A scheduled GitHub Action running `doctor` plus `schema --lint`, opening an issue when a namespace starts colliding.
482
+
483
+ ### Conventions
484
+
485
+ - ES modules, NodeNext resolution, strict TypeScript. `noUncheckedIndexedAccess` is off on purpose; the casts at the SDK boundary are explicit.
486
+ - No new runtime dependency without a reason in the PR. Current runtime deps: `@arkiv-network/sdk`, `viem`.
487
+ - Command functions take `Context` and an options object, return `Promise<void>`, and print. They do not throw strings.
488
+ - Exit codes: `0` success, `1` runtime failure, `2` usage error.
489
+
490
+ ## Troubleshooting
491
+
492
+ | Symptom | Cause | Fix |
493
+ | --- | --- | --- |
494
+ | `heavily rate limited` notice | Public endpoint | Set `ARKIV_ACCESS_KEY` in `.env` |
495
+ | `doctor` clock check fails | Local clock drift | Turn on automatic time sync |
496
+ | `entity` exits 2 | Malformed key | Keys are `0x` plus 64 hex characters |
497
+ | `create` reports the attribute limit | Over 32 attributes | Drop fields or split the entity |
498
+ | `create` reports a string limit | `str` over 128 bytes | Use `bytes` for long values |
499
+ | `schema` misses a namespace | Sample did not reach it | Query it by name, or raise `--sample` |
500
+ | `init` refuses to write | Directory is not empty | Pass `--force` or pick another path |
501
+
502
+ ## Protocol facts the CLI relies on
503
+
504
+ - An entity carries at most 32 attributes, and a `str` attribute is at most 128 bytes. Long text belongs in the payload as `bytes`.
505
+ - Queries cannot match every entity. A predicate or schema filter is required, which is why an empty expression is a usage error rather than a table dump.
506
+ - `$createdAt` is not a sortable column.
507
+ - The public Rabdi endpoint is rate limited and shared. An access key is the difference between a demo and a workflow.
508
+
509
+ ## License
510
+
511
+ MIT. See [LICENSE](LICENSE).
512
+
513
+ Built By [Harish Kotra](https://harishkotra.me) · [Checkout my other builds](https://dailybuild.xyz)
package/dist/args.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ /**
2
+ * A deliberately small argument parser.
3
+ *
4
+ * It rejects unknown flags instead of guessing, which turns a typo like
5
+ * `--limt 5` into a clear error rather than a confusing empty result.
6
+ */
7
+ export type FlagType = "bool" | "string" | "number";
8
+ export type FlagSpec = Record<string, FlagType>;
9
+ export type FlagValue = string | number | boolean | Array<string | number>;
10
+ export interface Parsed {
11
+ positionals: string[];
12
+ flags: Record<string, FlagValue>;
13
+ }
14
+ export declare class UsageError extends Error {
15
+ }
16
+ export declare function parseArgs(argv: string[], spec: FlagSpec, aliases?: Record<string, string>): Parsed;
17
+ export declare function flagString(p: Parsed, name: string, fallback?: string): string;
18
+ export declare function flagNumber(p: Parsed, name: string, fallback: number): number;
19
+ export declare function flagBool(p: Parsed, name: string): boolean;
package/dist/args.js ADDED
@@ -0,0 +1,124 @@
1
+ /**
2
+ * A deliberately small argument parser.
3
+ *
4
+ * It rejects unknown flags instead of guessing, which turns a typo like
5
+ * `--limt 5` into a clear error rather than a confusing empty result.
6
+ */
7
+ export class UsageError extends Error {
8
+ }
9
+ const SHORT = {
10
+ h: "help",
11
+ v: "version",
12
+ n: "limit",
13
+ j: "json",
14
+ s: "schema",
15
+ q: "quiet",
16
+ };
17
+ export function parseArgs(argv, spec, aliases = {}) {
18
+ const flags = {};
19
+ const positionals = [];
20
+ /* repeating a flag accumulates, so --set a=1 --set b=2 keeps both */
21
+ const put = (name, value) => {
22
+ const previous = flags[name];
23
+ if (previous === undefined) {
24
+ flags[name] = value;
25
+ }
26
+ else if (Array.isArray(previous)) {
27
+ previous.push(value);
28
+ }
29
+ else {
30
+ flags[name] = [previous, value];
31
+ }
32
+ };
33
+ const allowed = new Set(Object.keys(spec));
34
+ for (let i = 0; i < argv.length; i += 1) {
35
+ const token = argv[i];
36
+ if (token === "--") {
37
+ positionals.push(...argv.slice(i + 1));
38
+ break;
39
+ }
40
+ if (token.startsWith("--")) {
41
+ let name = token.slice(2);
42
+ let inline;
43
+ const eq = name.indexOf("=");
44
+ if (eq >= 0) {
45
+ inline = name.slice(eq + 1);
46
+ name = name.slice(0, eq);
47
+ }
48
+ let negated = false;
49
+ if (name.startsWith("no-")) {
50
+ negated = true;
51
+ name = name.slice(3);
52
+ }
53
+ name = aliases[name] ?? name;
54
+ if (!allowed.has(name)) {
55
+ throw new UsageError(`unknown option --${name}`);
56
+ }
57
+ const type = spec[name];
58
+ if (type === "bool") {
59
+ flags[name] = !negated;
60
+ continue;
61
+ }
62
+ if (negated)
63
+ throw new UsageError(`--no-${name} is not valid`);
64
+ let value = inline;
65
+ if (value === undefined) {
66
+ const next = argv[i + 1];
67
+ if (next === undefined || next.startsWith("-")) {
68
+ throw new UsageError(`--${name} needs a value`);
69
+ }
70
+ value = next;
71
+ i += 1;
72
+ }
73
+ if (type === "number") {
74
+ const n = Number(value);
75
+ if (!Number.isFinite(n))
76
+ throw new UsageError(`--${name} must be a number`);
77
+ put(name, n);
78
+ }
79
+ else {
80
+ put(name, value);
81
+ }
82
+ continue;
83
+ }
84
+ if (token.startsWith("-") && token.length > 1) {
85
+ const letters = token.slice(1);
86
+ for (let k = 0; k < letters.length; k += 1) {
87
+ const letter = letters[k];
88
+ const name = SHORT[letter] ?? aliases[letter] ?? letter;
89
+ if (!allowed.has(name))
90
+ throw new UsageError(`unknown option -${letter}`);
91
+ if (spec[name] === "bool") {
92
+ flags[name] = true;
93
+ }
94
+ else {
95
+ const rest = letters.slice(k + 1);
96
+ let value = rest;
97
+ if (!value) {
98
+ const next = argv[i + 1];
99
+ if (next === undefined)
100
+ throw new UsageError(`-${letter} needs a value`);
101
+ value = next;
102
+ i += 1;
103
+ }
104
+ put(name, spec[name] === "number" ? Number(value) : value);
105
+ break;
106
+ }
107
+ }
108
+ continue;
109
+ }
110
+ positionals.push(token);
111
+ }
112
+ return { positionals, flags };
113
+ }
114
+ export function flagString(p, name, fallback = "") {
115
+ const v = p.flags[name];
116
+ return typeof v === "string" ? v : fallback;
117
+ }
118
+ export function flagNumber(p, name, fallback) {
119
+ const v = p.flags[name];
120
+ return typeof v === "number" ? v : fallback;
121
+ }
122
+ export function flagBool(p, name) {
123
+ return p.flags[name] === true;
124
+ }