mx 0.1.174

A Swiss army knife for Claude Code and multi-agent toolkits
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
#import "lib.typ": *

#page-header("KV Store", "Fast local key-value state per agent.")

The KV subsystem gives each agent a lightweight, schema-driven key-value store
for operational state that needs to be fast and local. Counters, strings, lists,
timestamped history, and structured state fields -- all backed by a TOML schema
file and a JSON data file. No networking, no database. Reads and writes are
direct file operations with atomic saves (serialize to tmp, fsync, rename).
History and list entries can carry structured JSON data for queryable metadata.

Use KV for state that lives within a single agent session or across sessions:
build counters, track decisions as a history log, maintain a todo list, or
store the current goal as a string. For cross-agent knowledge that needs search,
tagging, and relationships, use #link("memory.html")[Memory] instead.

== Concepts

=== Data types

Every key has a type declared in the schema. Five types are supported:

/ string: A single text value. Has an optional `default`.
/ counter: An integer with optional `min`, `max`, and `default`. Clamped on every write.
/ history: A timestamped append-only log. Newest entries first. Has an optional `max_entries` cap that drops the oldest entries on overflow. Each entry gets a numeric index and a stable ID. Entries can carry optional structured JSON data.
/ list: An ordered collection with timestamps. Supports push and pop. Also has an optional `max_entries` cap. Each entry gets a numeric index and a stable ID. Entries can carry optional structured JSON data.
/ state: A structured record with named fields. Fields are declared in the schema and validated on write.

=== Schema files

Each agent has a TOML schema file that declares every valid key, its type,
and any constraints. The schema lives at:

```
$MX_HOME/kv/schema/{agent}.toml
```

The data file (JSON, auto-created on first write) lives at:

```
$MX_HOME/kv/data/{agent}.json
```

The active agent is determined by the `MX_CURRENT_AGENT` environment variable.

You can override the paths with `MX_KV_SCHEMA` and `MX_KV_DATA` environment
variables. Both support an `{agent}` placeholder that expands to the current
agent name.

=== Schema format

A schema file is TOML with a `[keys.<name>]` section per key:

```toml
[keys.builds]
type = "counter"
min = 0
default = "0"

[keys.session_goal]
type = "string"
default = ""

[keys.decisions]
type = "history"
max_entries = 50

[keys.ideas]
type = "list"

[keys.todos]
type = "list"
max_entries = 20
description = "Pending work items"

[keys.context]
type = "state"
fields = ["goal", "phase", "blocker"]
```

Schema fields:

/ `type`: Required. One of `string`, `counter`, `history`, `list`, `state`.
/ `default`: Optional. Initial value for string and counter types.
/ `min`: Optional. Minimum value for counters (clamped, never errors).
/ `max`: Optional. Maximum value for counters (clamped, never errors).
/ `max_entries`: Optional. Maximum entries for history and list types. Oldest entries are dropped when exceeded. Omit to allow unbounded growth.
/ `description`: Optional. Human-readable description of the key's purpose. Displayed as a third column by `mx kv keys`.
/ `fields`: Optional. List of valid field names for state types. Writes to unlisted fields are rejected.

=== Auto-creating keys

Keys can be added to the schema on the fly with `mx kv push --create`.
When a key does not exist in the schema, `--create history` or
`--create list` appends a new `[keys.<name>]` block to the TOML file
and reloads the in-memory schema. This avoids manual schema editing for
simple cases. See #link(<push>)[push] for details and validation rules.

=== Agent keying

All KV operations require `MX_CURRENT_AGENT` to be set. Each agent gets its
own schema and data file -- there is no cross-agent state leakage. Two agents
can define entirely different schemas with different keys.

=== Exit codes

KV commands use structured exit codes for scripting:

/ `0`: Success.
/ `1`: Key not found (or no data yet for that key).
/ `2`: Type mismatch (e.g., `inc` on a string key, or `get --id` on a non-history/list key).
/ `3`: Schema file not found.
/ `4`: Invalid input (e.g., reversed range, empty spec, empty ID after `kv-` prefix, entry not found by ID, ambiguous ID prefix).

== Basic operations

#command(
  "mx kv get <key>",
  [Get the current value of a key, or look up specific entries by ID.

  Without `--id`, prints the full current value: raw text for strings and
  counters, all entries with indexes and timestamps for history and list types,
  and fields as JSON for state types.

  With `--id`, retrieves specific entries from a history or list by numeric
  index or entry ID. Four formats are supported:

  / Single numeric index: `--id 35` -- returns exactly one entry.
  / Single entry ID: `--id kv-A3fB` -- returns the entry matching that ID. ID matching is prefix-based: `kv-A3f` will match if the prefix is unambiguous.
  / Numeric range: `--id 35-64` -- returns all entries with indexes 35 through 64 inclusive. Maximum range size is 10,000 entries. Ranges are numeric only.
  / Comma-separated: `--id 1,kv-A3fB,12` -- returns the listed entries. Numeric indexes and entry IDs can be mixed freely in comma lists.

  If any requested IDs are not found, a note listing the missing IDs is
  printed to stderr. The found entries are still printed to stdout.

  The `--id` flag only works on history and list types. Using it on a
  string, counter, or state key returns exit code 2 (type mismatch).
  Parse failures (reversed ranges, empty specs) return exit code 4
  (invalid input).],
  flags: (
    ([`--id <spec>`], [string], [Entry identifier: numeric index (`35`), entry ID (`kv-A3fB`), range (`35-64`), or comma-separated (`1,kv-A3fB,12`)]),
    ([`--memory`], [flag], [Resolve and display any linked memory entry]),
    ([`--json`], [flag], [Output as JSON. Collections emit a JSON array of entry objects. Scalars emit `{"value": "..."}`. Memory resolution is skipped in JSON mode; the raw `kn-` ID is included in each entry's `memory` field.]),
  ),
  examples: (
    "mx kv get session_goal",
    "mx kv get builds",
    "mx kv get decisions",
    "mx kv get context --memory",
    "mx kv get shipped --id 35",
    "mx kv get shipped --id kv-A3fB",
    "mx kv get shipped --id 35-64",
    "mx kv get shipped --id 1,kv-A3fB,12",
    "mx kv get shipped --id 35 --memory",
    "mx kv get shipped --id 42 --json",
    "mx kv get shipped --json",
  ),
)

#command(
  "mx kv set <key> [args...] [--json <value>]",
  [Set a value for a string, counter, or state key, or link a specific
  entry to a memory node. Supports four input modes for state keys:
  single-field, inline batch, JSON object, and JSON array (tensor).

  For *string* keys: `mx kv set <key> <value>` sets the value directly.

  For *counter* keys: `mx kv set <key> <value>` parses the value as an integer
  and clamps to min/max.

  For *state* keys, four input modes are available:

  / Single field (legacy): `mx kv set <key> <field> <value>` -- sets one field. Backward compatible with pre-batch syntax.
  / Inline batch: `mx kv set <key> field1=value1 field2=value2 ...` -- sets multiple fields atomically. All field names are validated against the schema before any writes. Unmentioned fields are preserved (partial update).
  / JSON object: `mx kv set <key> --json '{"field1":"value1","field2":"value2"}'` -- sets multiple fields from a JSON object. Non-string values (numbers, booleans, null) are coerced to strings. Same atomic validation as inline batch.
  / JSON array (tensor): `mx kv set <key> --json '[0.4, 0.6, 0.5]'` -- sets all fields by position. The number of array elements must exactly match the number of fields declared in the schema. Values are mapped to schema fields in declaration order.

  The `--json` flag accepts `"-"` to read JSON from stdin:
  `echo '{"goal":"done"}' | mx kv set context --json -`

  The `--json` flag and positional arguments cannot be combined -- use one
  or the other.

  Batch operations (inline and JSON) are all-or-nothing: if any field name
  is invalid, no fields are written. Duplicate field names in a single batch
  are rejected. All validation errors are reported, not just the first.

  With `--id` and `--memory`, links an existing history or list entry to a
  memory knowledge node. The `--id` flag accepts a numeric index (`17`) or an
  entry ID (`kv-A3fB`). ID matching is prefix-based -- an ambiguous
  prefix returns an error asking for more characters. `--id` requires
  `--memory`; it cannot be used alone. Pass an empty string
  (`--memory ""`) to clear a per-entry link.],
  flags: (
    ([`--json <value>`], [string], [JSON input: object for state batch set, array for tensor positional set. Use `"-"` to read from stdin.]),
    ([`--memory <kn-id>`], [string], [Link a memory entry (kn- ID) to this key or entry, or `""` to clear]),
    ([`--id <spec>`], [string], [Target a specific entry by numeric index or entry ID (requires `--memory`)]),
  ),
  examples: (
    "mx kv set session_goal \"ship the docs\"",
    "mx kv set builds 0",
    "mx kv set context goal \"finish KV docs\"",
    "mx kv set context phase \"writing\"",
    "mx kv set context goal=\"done\" phase=\"writing\"",
    "mx kv set context goal=\"done\" phase=\"writing\" blocker=\"none\"",
    "mx kv set context --json '{\"goal\":\"done\",\"phase\":\"writing\"}'",
    "mx kv set mytensor --json '[0.4, 0.6, 0.5]'",
    "echo '{\"goal\":\"done\"}' | mx kv set context --json -",
    "mx kv set decisions --memory kn-abc123",
    "mx kv set decisions --memory \"\"",
    "mx kv set decisions --id 17 --memory kn-abc123",
    "mx kv set decisions --id kv-A3fB --memory kn-def456",
    "mx kv set decisions --id 17 --memory \"\"",
  ),
)

#command(
  "mx kv keys",
  [List all keys defined in the schema with their types. Output is
  two columns: key name (left-aligned, 30 chars) and type. When a key
  has a `description` in the schema, it is shown as a third column.],
  examples: (
    "mx kv keys",
  ),
)

== Counters

#command(
  "mx kv inc <key>",
  [Increment a counter key. Returns the new value after incrementing.
  The result is clamped to the schema's min/max bounds -- it never errors
  on overflow, it just stops at the limit.],
  flags: (
    ([`--by <n>`], [integer], [Amount to increment by (default: 1)]),
  ),
  examples: (
    "mx kv inc builds",
    "mx kv inc builds --by 5",
  ),
)

#command(
  "mx kv dec <key>",
  [Decrement a counter key. Returns the new value after decrementing.
  Like `inc`, the result is clamped to schema bounds.],
  flags: (
    ([`--by <n>`], [integer], [Amount to decrement by (default: 1)]),
  ),
  examples: (
    "mx kv dec retries",
    "mx kv dec retries --by 3",
  ),
)

== Lists & History

History and list types both store timestamped entries with auto-assigned IDs.
The difference is semantic: history is append-only (newest first, no pop),
while lists support push/pop and maintain insertion order.

Every entry gets two identifiers: a numeric index (sequential, per-key) and a
stable entry ID (a base58 string prefixed with `kv-`, e.g. `kv-A3fB`). Both
can be used anywhere an ID is accepted. See #link(<entry-ids>)[Entry IDs] for
details.

Both types support `push`, `last`, `search`, `count`, `random`, `remove`,
`update`, `migrate`, and entry lookup by ID via `get --id`. Both support
structured data on entries (`--data` on push/update) and structured data
filtering (`--where` on queries). Only lists support `pop`. Only history supports `since`
(time-based queries).

=== push <push>

#command(
  "mx kv push <key> <value>",
  [Push a value onto a history or list key. The entry is automatically
  timestamped and assigned both a numeric index and a stable entry ID.

  On success, prints the new entry's identifiers:

  ```
  kv-A3fB (42)
  ```

  Entry ID first (the primary stable identifier), numeric index in parentheses.

  For *history* keys, new entries are inserted at the front (newest first).
  If the key has a `max_entries` schema constraint, the oldest entries are
  truncated after the push.

  For *list* keys, new entries are appended to the end. The same
  `max_entries` truncation applies, dropping from the front.

  Use `--data` to attach a JSON object to the entry. The data is stored
  alongside the value and timestamp, and is displayed inline in output.
  See #link(<structured-data>)[Structured data] for details and query
  examples.

  Use `--memory` to link the new entry to a knowledge node in the memory
  graph. This sets a per-entry memory pointer (a `kn-` ID) that is
  resolved when `--memory` is passed to read commands. See
  #link(<per-entry-memory>)[Per-entry memory links] for the full
  resolution hierarchy.

  Use `--create` to auto-add the key to the schema if it does not already
  exist. Pass the type as the value: `--create history` or `--create list`.
  Only `history` and `list` types are accepted (those are the types that
  support `push`). If the key already exists in the schema, `--create` is
  silently ignored -- this makes it safe to use unconditionally in scripts
  without checking whether the key has been defined yet.

  The optional `--max-entries` flag sets the entry cap for the new key.
  It requires `--create` and has no effect if the key already exists.

  Key names are validated on creation: alphanumeric characters, underscores,
  and hyphens only, maximum 128 characters. Dots are rejected because they
  conflict with TOML key quoting. The new key block is appended to the
  schema file without reformatting existing content.],
  flags: (
    ([`--data <json>`], [string], [Attach a JSON object to the entry. Must be a valid JSON object (not an array, string, or other type).]),
    ([`--memory <kn-id>`], [string], [Link this entry to a memory knowledge node (e.g. `kn-abc123`). Resolved when `--memory` is passed on read commands.]),
    ([`--create <type>`], [enum], [Auto-create the key in the schema if missing. Accepted types: `history`, `list`. Silently ignored if the key already exists.]),
    ([`--max-entries <n>`], [integer], [Maximum entries for the new key (only valid with `--create`). Oldest entries are dropped when exceeded.]),
  ),
  examples: (
    "mx kv push decisions \"chose Typst for docs\"",
    "mx kv push todos \"write tests for kv handler\"",
    "mx kv push projects \"palmtop DSI fix\" --data '{\"tags\":[\"palmtop\",\"i915\"],\"status\":\"active\"}'",
    "mx kv push shipped \"v0.1.156\" --data '{\"pr\":305,\"scope\":\"kv\"}'",
    "mx kv push decisions \"adopted per-entry memory links\" --memory kn-abc123",
    "mx kv push puns \"the joke\" --create history",
    "mx kv push ideas \"wild thought\" --create list --max-entries 500",
  ),
)

=== pop

#command(
  "mx kv pop <key>",
  [Pop the last item from a list key. Prints the removed entry with its
  numeric index, entry ID, value, and timestamp. Returns silently if the list
  is empty.

  Only works on list types. History keys are append-only and do not support
  pop.],
  examples: (
    "mx kv pop todos",
  ),
)

=== last

#command(
  "mx kv last <key>",
  [Get the last N entries from a history or list key. Entries are printed
  with their numeric index, entry ID, value, and timestamp.

  For history keys, "last" means the most recent (entries are stored newest
  first). For list keys, "last" means the tail of the list.

  Time-range flags narrow the result set before `--count` is applied. See
  #link(<time-range-queries>)[Time-range queries] for details and examples.

  The `--where` flag filters entries by structured data fields. Multiple
  `--where` flags are ANDed. See #link(<structured-data>)[Structured data]
  for filtering semantics.],
  flags: (
    ([`--count <n>`], [integer], [Number of entries to return (default: 1)]),
    ([`--memory`], [flag], [Resolve and display any linked memory entry]),
    ([`--json`], [flag], [Output as a JSON array of entry objects]),
    ([`--where <key=value>`], [string], [Filter by structured data field (repeatable, ANDed). Top-level fields only.]),
    ([`--day <YYYY-MM-DD>`], [string], [Entries from a specific day (UTC)]),
    ([`--month <YYYY-MM>`], [string], [Entries from a specific month (UTC)]),
    ([`--week <YYYY-Www>`], [string], [Entries from an ISO week, Monday to Sunday]),
    ([`--from <YYYY-MM-DD>`], [string], [Start of date range, inclusive (UTC)]),
    ([`--to <YYYY-MM-DD>`], [string], [End of date range, inclusive (UTC)]),
    ([`--since <relative-or-iso>`], [string], [Filter entries since a relative time (`30d`, `1w`, `2h`, `30m`) or ISO-8601 timestamp]),
  ),
  examples: (
    "mx kv last decisions",
    "mx kv last decisions --count 5",
    "mx kv last todos --count 3 --memory",
    "mx kv last shipped --day 2026-04-25",
    "mx kv last shipped --month 2026-04",
    "mx kv last shipped --month 2026-04 --count 5",
    "mx kv last shipped --since 1w",
    "mx kv last projects --where status=active",
    "mx kv last projects --where status=active --count 3",
    "mx kv last projects --count 5 --json",
  ),
)

=== since

#command(
  "mx kv since <key> <timeref>",
  [Get history entries since a time reference. Only works on history keys.

  The time reference can be relative or absolute:
  - Relative: `30m` (minutes), `1h` (hours), `7d` (days), `2w` (weeks)
  - Absolute: ISO-8601 format (e.g., `2025-01-15T10:00:00Z`)

  Entries are printed with their numeric index, entry ID, value, and timestamp.],
  flags: (
    ([`--memory`], [flag], [Resolve and display any linked memory entry]),
    ([`--json`], [flag], [Output as a JSON array of entry objects]),
  ),
  examples: (
    "mx kv since decisions 1h",
    "mx kv since decisions 7d",
    "mx kv since decisions 2w --memory",
    "mx kv since decisions 2025-01-15T10:00:00Z",
    "mx kv since shipped 1w --json",
  ),
)

=== search

#command(
  "mx kv search <key> [query]",
  [Search entries in a list or history by case-insensitive substring match
  and/or structured data filters. Prints matching entries with their numeric
  index, entry ID, value, timestamp, and any attached data.

  The text query is optional when `--where` filters are provided. You can
  search by text alone, by structured data alone, or by both. At least one
  of a text query or `--where` filter must be given.

  Multiple `--where` flags are ANDed. See
  #link(<structured-data>)[Structured data] for filtering semantics.

  Time-range flags narrow the search to entries within the specified period.
  See #link(<time-range-queries>)[Time-range queries] for details.],
  flags: (
    ([`--memory`], [flag], [Resolve and display any linked memory entry]),
    ([`--json`], [flag], [Output as a JSON array of entry objects]),
    ([`--where <key=value>`], [string], [Filter by structured data field (repeatable, ANDed). Top-level fields only.]),
    ([`--day <YYYY-MM-DD>`], [string], [Search within a specific day (UTC)]),
    ([`--month <YYYY-MM>`], [string], [Search within a specific month (UTC)]),
    ([`--week <YYYY-Www>`], [string], [Search within an ISO week, Monday to Sunday]),
    ([`--from <YYYY-MM-DD>`], [string], [Start of date range, inclusive (UTC)]),
    ([`--to <YYYY-MM-DD>`], [string], [End of date range, inclusive (UTC)]),
    ([`--since <relative-or-iso>`], [string], [Search since a relative time (`30d`, `1w`, `2h`, `30m`) or ISO-8601 timestamp]),
  ),
  examples: (
    "mx kv search decisions \"typst\"",
    "mx kv search todos \"test\"",
    "mx kv search shipped \"feature\" --month 2026-04",
    "mx kv search shipped \"feature\" --since 30d",
    "mx kv search projects --where status=active",
    "mx kv search projects \"DSI\" --where status=active",
    "mx kv search projects --where tags=palmtop --where status=active",
    "mx kv search projects --where status=active --json",
  ),
)

=== count

#command(
  "mx kv count <key> [value]",
  [Count entries in a list or history. Without a value filter or `--where`,
  prints the total count. With a value filter, `--where`, or both, prints
  the matched count, total, and percentage.

  Unfiltered output: `<count>` or `<count> (latest: <timestamp>)`.

  Filtered output: `<matched>/<total> (<pct>%) --- latest: <timestamp>`.

  The percentage display makes it easy to gauge ratios at a glance -- for
  example, what fraction of your decisions mentioned a particular topic,
  or how many entries have `status=active` in their structured data.

  Multiple `--where` flags are ANDed. See
  #link(<structured-data>)[Structured data] for filtering semantics.

  Time-range flags restrict the count to entries within the specified period.
  See #link(<time-range-queries>)[Time-range queries] for details.],
  flags: (
    ([`--json`], [flag], [Output as JSON: `{"count": N}` with optional `total` and `latest_ts` fields when filtering is active]),
    ([`--where <key=value>`], [string], [Filter by structured data field (repeatable, ANDed). Top-level fields only.]),
    ([`--day <YYYY-MM-DD>`], [string], [Count within a specific day (UTC)]),
    ([`--month <YYYY-MM>`], [string], [Count within a specific month (UTC)]),
    ([`--week <YYYY-Www>`], [string], [Count within an ISO week, Monday to Sunday]),
    ([`--from <YYYY-MM-DD>`], [string], [Start of date range, inclusive (UTC)]),
    ([`--to <YYYY-MM-DD>`], [string], [End of date range, inclusive (UTC)]),
    ([`--since <relative-or-iso>`], [string], [Count since a relative time (`30d`, `1w`, `2h`, `30m`) or ISO-8601 timestamp]),
  ),
  examples: (
    "mx kv count decisions",
    "mx kv count decisions \"typst\"",
    "mx kv count todos \"blocked\"",
    "mx kv count shipped --day 2026-05-07",
    "mx kv count shipped --from 2026-04-01 --to 2026-04-15",
    "mx kv count shipped --since 1w",
    "mx kv count projects --where status=active",
    "mx kv count projects --where status=active --since 30d",
    "mx kv count shipped --json",
  ),
)

=== random

#command(
  "mx kv random <key>",
  [Get N random entries from a history or list key. Entries are printed
  with their numeric index, entry ID, value, and timestamp.

  Useful for inspiration (pick a random idea), spot-checking (sample from
  a large history), or building variety into automated workflows.

  When fewer entries are available than requested, all matching entries are
  returned and a note is printed to stderr. If a time range or `--where`
  filter is specified, entries are filtered first, then random sampling is
  applied to the filtered set.

  Multiple `--where` flags are ANDed. See
  #link(<structured-data>)[Structured data] for filtering semantics.],
  flags: (
    ([`--count <n>`], [integer], [Number of random entries to return (default: 1, must be >= 1)]),
    ([`--memory`], [flag], [Resolve and display any linked memory entry]),
    ([`--json`], [flag], [Output as a JSON array of entry objects]),
    ([`--where <key=value>`], [string], [Filter by structured data field (repeatable, ANDed). Top-level fields only.]),
    ([`--day <YYYY-MM-DD>`], [string], [Sample from entries on a specific day (UTC)]),
    ([`--month <YYYY-MM>`], [string], [Sample from entries in a specific month (UTC)]),
    ([`--week <YYYY-Www>`], [string], [Sample from entries in an ISO week, Monday to Sunday]),
    ([`--from <YYYY-MM-DD>`], [string], [Start of date range, inclusive (UTC)]),
    ([`--to <YYYY-MM-DD>`], [string], [End of date range, inclusive (UTC)]),
    ([`--since <relative-or-iso>`], [string], [Sample from entries since a relative time (`30d`, `1w`, `2h`, `30m`) or ISO-8601 timestamp]),
  ),
  examples: (
    "mx kv random shipped",
    "mx kv random shipped --count 5",
    "mx kv random ideas --count 1",
    "mx kv random shipped --count 3 --since 30d",
    "mx kv random decisions --month 2026-04 --count 3",
    "mx kv random projects --where status=active --count 3",
    "mx kv random ideas --count 3 --json",
  ),
)

=== remove

#command(
  "mx kv remove <key> [value]",
  [Remove entries from a list or history by value substring or by ID.
  You must provide either a value substring or `--id`.

  The `--id` flag accepts a numeric index (`7`) or an entry ID (`kv-A3fB`).
  ID matching is prefix-based -- if the prefix is ambiguous (matches
  multiple entries), an error is returned asking for more characters.

  By default, only the first match is removed. Use `--all` to remove every
  matching entry.],
  flags: (
    ([`--id <spec>`], [string], [Remove the entry with this numeric index or entry ID (`kv-XXXX`)]),
    ([`--all`], [flag], [Remove all matching entries (default: first match only)]),
  ),
  examples: (
    "mx kv remove todos \"write tests\"",
    "mx kv remove todos --id 7",
    "mx kv remove todos --id kv-A3fB",
    "mx kv remove decisions \"typo\" --all",
  ),
)

=== update

#command(
  "mx kv update <key> [value] --id <spec>",
  [Update an existing entry's value and/or structured data in-place.
  Preserves the entry's ID, position, and timestamp.

  Requires `--id` to target a specific entry by numeric index or entry ID.
  ID matching is prefix-based -- if the prefix is ambiguous (matches
  multiple entries), an error is returned asking for more characters.
  The value argument is optional -- you can update only `--data`, only the
  value, or both.

  When `--data` is provided, the JSON object is shallow-merged into the
  entry's existing structured data. Fields in the patch overwrite existing
  fields. Null values in the patch _delete_ that field from the merged
  result (they do not set it to null). If the key has a `[data]` schema
  section, validation runs on the merged result before the write commits.

  At least one of a value argument or `--data` must be provided -- calling
  update with neither is rejected.

  On success, prints the updated entry's identifiers:

  ```
  Updated entry 42 (kv-A3fB)
  ```

  Works on both history and list types.],
  flags: (
    ([`--id <spec>`], [string], [Target entry by numeric index (`42`) or entry ID (`kv-A3fB`). Required.]),
    ([`--data <json>`], [string], [JSON object to merge into the entry's structured data. Null field values delete that field from the merged result.]),
  ),
  examples: (
    "mx kv update projects \"palmtop DSI fix (v2)\" --id kv-A3fB",
    "mx kv update projects --id 42 --data '{\"status\":\"done\"}'",
    "mx kv update projects \"renamed\" --id kv-A3fB --data '{\"status\":\"closed\"}'",
    "mx kv update projects --id 42 --data '{\"obsolete_field\":null}'",
  ),
)

=== migrate

#command(
  "mx kv migrate <key>",
  [Migrate existing entries to match current schema data definitions.
  Operates on all entries in a key.

  Compares each entry's structured data against the `[data]` section in
  the key's schema. Missing fields that have a default value in the schema
  are added. Required fields without defaults produce a warning. Type
  mismatches between existing data and schema declarations are reported as
  warnings.

  Entries without any structured data get a new data object populated from
  schema defaults.

  With `--prune`, fields present in entries but not declared in the schema
  are removed. Without `--prune`, undeclared fields are left untouched.

  With `--dry-run`, reports what would change without modifying any data.
  The output lists each affected entry with its added and pruned fields.

  If the key has no `[data]` section in the schema, nothing is migrated
  and a warning is printed.

  Works on both history and list types.],
  flags: (
    ([`--prune`], [flag], [Remove fields not declared in the current schema]),
    ([`--dry-run`], [flag], [Show what would change without modifying data]),
  ),
  examples: (
    "mx kv migrate projects",
    "mx kv migrate projects --dry-run",
    "mx kv migrate projects --prune",
    "mx kv migrate projects --prune --dry-run",
  ),
)

== Time-range queries <time-range-queries>

The `last`, `search`, `count`, and `random` subcommands accept time-range
flags that filter entries by their timestamp before any other processing. This
lets you answer questions like "what did I ship last Tuesday?" or "how many
decisions were recorded in April?" without scanning the full history.

=== Available flags

All time-range flags are mutually exclusive -- you can use one shorthand
(`--day`, `--month`, `--week`, `--since`) or one explicit range
(`--from`/`--to`), but not both.

#table(
  columns: (auto, auto, 1fr),
  table.header([*Flag*], [*Format*], [*Selects*]),
  [`--day`], [`YYYY-MM-DD`], [All entries from that calendar day (00:00 to 23:59 UTC)],
  [`--month`], [`YYYY-MM`], [All entries from that calendar month (first day to last day, UTC)],
  [`--week`], [`YYYY-Www`], [All entries from that ISO week (Monday 00:00 to Sunday 23:59 UTC)],
  [`--since`], [relative or ISO-8601], [All entries from the given point in time until now. Relative formats: `30d` (days), `1w` (weeks), `2h` (hours), `30m` (minutes). Also accepts full ISO-8601 timestamps.],
  [`--from`], [`YYYY-MM-DD`], [Start of range, inclusive (midnight UTC). Can be used alone (implies "to now")],
  [`--to`], [`YYYY-MM-DD`], [End of range, inclusive (end of day UTC). Can be used alone (implies "from the beginning")],
)

All dates are interpreted as UTC. The `--to` date is inclusive -- entries from
any time on that day are included.

=== Interaction with `--count`

When both a time range and `--count` are specified, the time range is applied
first, then `--count` limits the result. This applies to both `last` (which
takes the N most recent from the filtered set) and `random` (which samples N
entries from the filtered set).

```bash
# The 5 most recent entries from April 2026
mx kv last shipped --month 2026-04 --count 5

# 3 random entries from the last 30 days
mx kv random shipped --since 30d --count 3
```

=== Examples

```bash
# Everything shipped on a specific day
mx kv last shipped --day 2026-04-25

# Everything shipped in April
mx kv last shipped --month 2026-04

# Everything shipped in ISO week 17
mx kv last shipped --week 2026-W17

# Everything shipped in the first half of April
mx kv last shipped --from 2026-04-01 --to 2026-04-15

# Everything shipped in the last week
mx kv last shipped --since 1w

# Search within a time window
mx kv search shipped "feature" --month 2026-04

# Count entries on a specific day
mx kv count shipped --day 2026-05-07

# Count entries from the last 30 days
mx kv count shipped --since 30d

# Random entry from the last 2 hours
mx kv random shipped --since 2h
```

=== Relationship to `since` subcommand

The `since` subcommand (`mx kv since <key> <timeref>`) is a standalone command
that returns all history entries since a time reference. It only works on
history keys and predates the time-range flag system.

The `--since` flag brings relative time filtering to all time-range-aware
subcommands (`last`, `search`, `count`, `random`) and works on both history
and list types. It accepts the same relative formats (`30d`, `1w`, `2h`, `30m`)
and ISO-8601 timestamps.

Use the `since` subcommand when you want a quick "everything since X" dump
from a history key. Use the `--since` flag when you want to combine relative
time filtering with other operations like counting, searching, or random
sampling, or when you need it on a list key.

#note[Time-range flags (`--day`, `--month`, `--week`, `--since`,
`--from`/`--to`) are available on `last`, `search`, `count`, and `random`.
The `since` subcommand is unchanged and continues to work for history keys.]

== Structured data <structured-data>

History and list entries can carry structured JSON data alongside their text
value. This turns each entry from a plain string into a string with queryable
metadata -- tags, status, priority, or any key-value pairs relevant to the
domain.

=== Pushing data

Use `--data` on `push` to attach a JSON object to the entry:

```bash
mx kv push projects "palmtop DSI fix" \
  --data '{"tags":["palmtop","i915"],"status":"active"}'

mx kv push shipped "v0.1.156" \
  --data '{"pr":305,"scope":"kv"}'
```

The data must be a valid JSON object. Arrays, strings, numbers, and other
non-object JSON types are rejected. If `--data` is omitted, the entry has no
structured data (backward compatible with all existing entries).

=== Output format

Entries display the numeric index, entry ID in brackets, value, timestamp, and
any structured data:

```
42 [kv-A3fB]: palmtop DSI fix (2026-05-08T14:30:00Z) {"tags":["palmtop","i915"],"status":"active"}
43 [kv-B7xQ]: display rotation patch (2026-05-08T15:00:00Z)
```

Entries without data omit the trailing JSON. This format appears on all
commands that display entries: `get`, `last`, `search`, `since`, `pop`,
`random`, and `dump`.

=== Filtering with `--where`

The `--where` flag queries entries by their structured data fields. It is
available on `search`, `last`, `random`, and `count`.

```bash
# Exact match on a string field
mx kv search projects --where status=active

# Array-contains: matches if the array includes the value
mx kv search projects --where tags=palmtop

# Combine text search with structured data filter
mx kv search projects "DSI" --where status=active

# Multiple --where flags are ANDed
mx kv search projects --where tags=palmtop --where status=active

# Works on last, random, and count too
mx kv last projects --where status=active --count 5
mx kv random projects --where status=active --count 3
mx kv count projects --where status=active
```

=== Matching semantics

Each `--where` clause has the form `key=value` (split on the first `=`). The
match is evaluated against the top-level fields of the entry's JSON data:

/ String field: The field value must equal the clause value exactly.
/ Array field: The array must contain a string element equal to the clause value.
/ Number field: The field's string representation must equal the clause value (e.g., `--where pr=305`).
/ Boolean field: Matches against `true` or `false` as strings.
/ Missing field: Does not match. Entries without data never match any `--where` clause.

Only top-level fields are supported. Dot-path traversal (e.g.,
`--where nested.field=value`) is not available.

When multiple `--where` clauses are given, ALL must match (AND logic). There is
no OR operator -- use separate queries if you need union semantics.

=== Combining with other filters

The `--where` flag composes with both text queries and time-range flags. All
filters are applied together:

```bash
# Text + where + time range: all three must match
mx kv search projects "DSI" --where status=active --since 30d
```

Filter application order: time range first, then `--where`, then text query.
The `--count` limit is applied last.

=== Backward compatibility

Structured data is fully backward compatible. Existing data files written
before this feature was added continue to work without migration. Entries
without data are simply treated as having no structured fields -- they will
not match any `--where` clause, but they are otherwise unaffected.

== Entry IDs <entry-ids>

Every history and list entry has a stable entry ID in addition to its numeric
index. Entry IDs are short base58 strings (4--6 characters) prefixed with `kv-`
for visual identification, e.g. `kv-A3fB`.

=== Generation

The ID is generated from `blake3(key + timestamp + index)`, with the first 4
bytes encoded as base58 via base-d. This produces ~11 million unique addresses
per key -- sufficient for typical KV usage. The ID is deterministic: the
same key, timestamp, and numeric index always produce the same ID.

=== Push output

`mx kv push` prints the new entry's identifiers on success:

```
kv-A3fB (42)
```

Entry ID first (the primary stable identifier), numeric index in parentheses.
This makes it easy to capture the ID for later use in scripts or follow-up
commands.

=== Dual addressing

Anywhere a numeric index is accepted, an entry ID also works:

```bash
# Get by entry ID
mx kv get shipped --id kv-A3fB

# Get by numeric index (still works)
mx kv get shipped --id 42

# Mix in comma lists
mx kv get shipped --id 42,kv-A3fB,15

# Remove by entry ID
mx kv remove shipped --id kv-A3fB
```

Numeric ranges remain numeric only (`35-64`). Entry IDs cannot be used in
ranges because they are not ordered.

=== Prefix matching

ID lookups are prefix-based: `kv-A3f` will match an entry with ID
`A3fBx2` as long as the prefix uniquely identifies one entry. If the prefix
is ambiguous (matches multiple entries), an error is returned:

```
Error: ID prefix 'kv-A3' is ambiguous: matches 3 entries, provide more characters
```

=== Backward compatibility

Old data files written before entry IDs existed are back-filled automatically
on first load. The store generates IDs for all entries that lack one,
saves the file, and continues normally. This is a one-time migration -- no
manual action is needed.

== Management

#command(
  "mx kv dump",
  [Dump all KV state. Defaults to JSON output (the full data file, pretty-
  printed). Compact format shows one line per key in `key=value` notation,
  designed for embedding in wake prompts or status lines.

  Compact format examples:
  - Counters: `builds=42`
  - Strings: `session_goal=ship the docs`
  - History: `decisions=[chose Typst\@14:30,fixed bug\@13:15]`
  - Lists: `todos=[write tests\@14:30,review PR\@13:15]`
  - State: `context={finish KV docs,writing,}`
  - Memory links appended: `decisions=[...](kn-abc123)`],
  flags: (
    ([`--format <fmt>`], [enum], [Output format: `json` (default) or `compact`]),
    ([`--memory`], [flag], [Resolve and display all linked memory entries]),
  ),
  examples: (
    "mx kv dump",
    "mx kv dump --format compact",
    "mx kv dump --memory",
  ),
)

#command(
  "mx kv reset <key>",
  [Reset a key to its schema default value. Counters return to their default
  (or 0). Strings return to their default (or empty). History and list keys
  are cleared to empty. State keys reset all fields to empty strings.],
  examples: (
    "mx kv reset builds",
    "mx kv reset decisions",
    "mx kv reset context",
  ),
)

== Memory linking

History, list, and state keys can be linked to a memory graph entry via the
`--memory` flag. This creates a pointer from the KV key to a knowledge entry
(a `kn-` ID), bridging fast local state with the persistent knowledge graph.

When a memory link is set, commands that read the key (`get`, `last`, `since`,
`search`, `random`, `dump`) can resolve the link with `--memory`, which fetches
the linked entry from SurrealDB and prints its title, category, and body.

=== Key-level memory links

```bash
# Link a key to a memory entry
mx kv set decisions --memory kn-abc123

# Clear a memory link (pass empty string)
mx kv set decisions --memory ""
```

Key-level memory links are stored in the JSON data file alongside the key's
entries. They survive resets -- `mx kv reset` clears the data but preserves
the memory pointer.

=== Per-entry memory links <per-entry-memory>

Individual history and list entries can carry their own memory link. This
allows different entries within the same key to reference different knowledge
nodes.

*Set at creation time:*

```bash
# Link a new entry to a knowledge node on push
mx kv push decisions "adopted per-entry memory links" --memory kn-abc123
```

*Set on an existing entry:*

```bash
# Link by numeric index
mx kv set decisions --id 17 --memory kn-abc123

# Link by entry ID
mx kv set decisions --id kv-A3fB --memory kn-def456

# Clear a per-entry link
mx kv set decisions --id 17 --memory ""
```

The `--id` flag on `set` requires `--memory` -- it cannot be used alone.
ID matching is prefix-based: `kv-A3f` matches if the prefix uniquely
identifies one entry. If the prefix is ambiguous, an error is returned.
If the entry is not found, exit code 4 (invalid input) is returned.

=== Resolution hierarchy

When `--memory` is passed on a read command, memory links are resolved in
priority order:

+ *Per-entry `memory` field* -- if the entry has its own memory link, that
  link is resolved and displayed. This is the highest priority.
+ *Legacy `kn-` value* -- if the entry's value string starts with `kn-`,
  it is treated as a memory reference and resolved. This provides backward
  compatibility with entries that stored knowledge node IDs as their value.
+ *Key-level memory* -- after all entries are printed, the key-level memory
  pointer (if any) is resolved once at the end.

Per-entry memory wins. An entry with a `memory` field set will use that link
regardless of whether the key itself also has a memory pointer. The key-level
link serves as a fallback that applies to the key as a whole.

=== Resolving memory links

```bash
# Read a key and show its linked memory entry
mx kv get decisions --memory

# Show the last 5 entries plus linked memory
mx kv last decisions --count 5 --memory

# Look up a specific entry and resolve its memory link
mx kv get decisions --id 17 --memory

# Dump everything with all memory links resolved
mx kv dump --memory
```

Resolution connects to the memory store (SurrealDB). If the store is
unavailable or the linked entry has been deleted, a warning is printed to
stderr but the KV data is still shown. KV data is always primary -- memory
links are supplementary context.

#note[Memory links are only available on history, list, and state types.
String and counter keys do not support `--memory`.]

== JSON output <json-output>

The `--json` flag outputs results as pretty-printed JSON instead of the
human-readable format. It is available on six commands: `get`, `last`,
`search`, `random`, `since`, and `count`.

JSON output is designed for scripting and piping to tools like `jq`. When
`--json` is active, human formatting is skipped and `--memory` resolution
is not performed -- the raw `kn-` ID is included in each entry's `memory`
field for the caller to resolve if needed.

=== Entry format

Commands that return entries (`get`, `last`, `search`, `random`, `since`)
emit a JSON array of entry objects. Each object has this shape:

```json
{
  "index": 42,
  "id": "A3fB",
  "value": "palmtop DSI fix",
  "ts": "2026-05-08T14:30:00+00:00",
  "data": {"status": "active", "tags": ["palmtop", "i915"]},
  "memory": "kn-e1f646aa"
}
```

/ `index`: Numeric sequence index (integer).
/ `id`: Stable entry ID (base58 string, without the `kv-` prefix).
/ `value`: The entry's text value.
/ `ts`: Timestamp in ISO-8601 format.
/ `data`: Structured data object, omitted when the entry has no data.
/ `memory`: Per-entry memory link (`kn-` ID), omitted when not set.

The `data` and `memory` fields are omitted entirely (not `null`) when they
have no value. This keeps the output clean and avoids forcing callers to
handle nulls.

=== Special output shapes

`get --json` without `--id` adapts to the key type:

- *History and list keys:* JSON array of all entries (same format as above).
- *Scalar keys* (string, counter, state): `{"value": "..."}` -- the
  formatted value as a string.

`count --json` emits a count object:

```json
{"count": 12}
```

When filtering is active (value substring, `--where`, or time range), the
object includes additional context:

```json
{"count": 5, "total": 12, "latest_ts": "2026-05-08T14:30:00+00:00"}
```

/ `count`: Number of matched entries (always present).
/ `total`: Total entries before filtering (present when filtering).
/ `latest_ts`: Timestamp of the most recent matched entry (present when filtering).

=== Piping to `jq`

The primary use case for `--json` is piping to `jq` for complex queries
that go beyond what `--where` provides:

```bash
# Extract all status values
mx kv last projects --json | jq '.[].data.status'

# Filter by a nested condition
mx kv search projects --where status=active --json \
  | jq 'map(select(.data.tags | contains(["rust"])))'

# Get the count as a bare number
mx kv count shipped --json | jq '.count'

# Build a CSV of shipped items
mx kv last shipped --count 100 --json \
  | jq -r '.[] | [.index, .id, .value, .ts] | @csv'
```

#note[`--json` is available on `get`, `last`, `search`, `random`, `since`,
and `count`. It is not available on `push`, `pop`, `set`, `inc`, `dec`,
`remove`, `reset`, `keys`, or `dump` (which already has `--format json`).]