@leemour/max-cli 0.1.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.
Files changed (109) hide show
  1. package/README.md +28 -12
  2. package/dist/cache/index.d.ts.map +1 -1
  3. package/dist/cache/index.js +3 -0
  4. package/dist/cache/index.js.map +1 -1
  5. package/dist/cache/schema.d.ts +13 -1
  6. package/dist/cache/schema.d.ts.map +1 -1
  7. package/dist/cache/schema.js +159 -8
  8. package/dist/cache/schema.js.map +1 -1
  9. package/dist/cache/store.d.ts +115 -4
  10. package/dist/cache/store.d.ts.map +1 -1
  11. package/dist/cache/store.js +248 -27
  12. package/dist/cache/store.js.map +1 -1
  13. package/dist/client.d.ts +91 -9
  14. package/dist/client.d.ts.map +1 -1
  15. package/dist/client.js +371 -33
  16. package/dist/client.js.map +1 -1
  17. package/dist/commands/chats.d.ts.map +1 -1
  18. package/dist/commands/chats.js +25 -8
  19. package/dist/commands/chats.js.map +1 -1
  20. package/dist/commands/contacts.d.ts.map +1 -1
  21. package/dist/commands/contacts.js +51 -5
  22. package/dist/commands/contacts.js.map +1 -1
  23. package/dist/commands/context.d.ts +4 -1
  24. package/dist/commands/context.d.ts.map +1 -1
  25. package/dist/commands/context.js +4 -2
  26. package/dist/commands/context.js.map +1 -1
  27. package/dist/commands/login.d.ts +15 -0
  28. package/dist/commands/login.d.ts.map +1 -0
  29. package/dist/commands/login.js +43 -0
  30. package/dist/commands/login.js.map +1 -0
  31. package/dist/commands/logout.d.ts +10 -0
  32. package/dist/commands/logout.d.ts.map +1 -0
  33. package/dist/commands/logout.js +22 -0
  34. package/dist/commands/logout.js.map +1 -0
  35. package/dist/commands/me.d.ts +3 -0
  36. package/dist/commands/me.d.ts.map +1 -0
  37. package/dist/commands/me.js +18 -0
  38. package/dist/commands/me.js.map +1 -0
  39. package/dist/commands/messages.d.ts.map +1 -1
  40. package/dist/commands/messages.js +82 -3
  41. package/dist/commands/messages.js.map +1 -1
  42. package/dist/commands/paging.d.ts +41 -0
  43. package/dist/commands/paging.d.ts.map +1 -0
  44. package/dist/commands/paging.js +54 -0
  45. package/dist/commands/paging.js.map +1 -0
  46. package/dist/commands/send.d.ts +10 -0
  47. package/dist/commands/send.d.ts.map +1 -0
  48. package/dist/commands/send.js +30 -0
  49. package/dist/commands/send.js.map +1 -0
  50. package/dist/commands/session.d.ts.map +1 -1
  51. package/dist/commands/session.js +3 -3
  52. package/dist/commands/session.js.map +1 -1
  53. package/dist/config.d.ts +16 -2
  54. package/dist/config.d.ts.map +1 -1
  55. package/dist/config.js +15 -1
  56. package/dist/config.js.map +1 -1
  57. package/dist/domain/map.d.ts +1 -0
  58. package/dist/domain/map.d.ts.map +1 -1
  59. package/dist/domain/map.js +44 -3
  60. package/dist/domain/map.js.map +1 -1
  61. package/dist/domain/models.d.ts +54 -0
  62. package/dist/domain/models.d.ts.map +1 -1
  63. package/dist/generated/operations.generated.d.ts +1 -0
  64. package/dist/generated/operations.generated.d.ts.map +1 -1
  65. package/dist/output.d.ts +7 -4
  66. package/dist/output.d.ts.map +1 -1
  67. package/dist/output.js +8 -11
  68. package/dist/output.js.map +1 -1
  69. package/dist/program.d.ts.map +1 -1
  70. package/dist/program.js +4 -2
  71. package/dist/program.js.map +1 -1
  72. package/dist/protocol/connection.d.ts +10 -0
  73. package/dist/protocol/connection.d.ts.map +1 -1
  74. package/dist/protocol/connection.js +11 -3
  75. package/dist/protocol/connection.js.map +1 -1
  76. package/dist/protocol/session.d.ts +54 -0
  77. package/dist/protocol/session.d.ts.map +1 -0
  78. package/dist/protocol/session.js +57 -0
  79. package/dist/protocol/session.js.map +1 -0
  80. package/dist/rendering/messages.d.ts +19 -0
  81. package/dist/rendering/messages.d.ts.map +1 -0
  82. package/dist/rendering/messages.js +137 -0
  83. package/dist/rendering/messages.js.map +1 -0
  84. package/dist/runs/events.d.ts +1 -1
  85. package/dist/runs/recording.d.ts +2 -2
  86. package/dist/runs/recording.d.ts.map +1 -1
  87. package/dist/runs/recording.js +3 -3
  88. package/dist/runs/recording.js.map +1 -1
  89. package/dist/session/adopt.d.ts +22 -0
  90. package/dist/session/adopt.d.ts.map +1 -0
  91. package/dist/session/adopt.js +29 -0
  92. package/dist/session/adopt.js.map +1 -0
  93. package/dist/session/handshake.d.ts +14 -1
  94. package/dist/session/handshake.d.ts.map +1 -1
  95. package/dist/session/handshake.js +5 -5
  96. package/dist/session/handshake.js.map +1 -1
  97. package/dist/spec/operations/auth.d.ts +42 -0
  98. package/dist/spec/operations/auth.d.ts.map +1 -0
  99. package/dist/spec/operations/session.d.ts +16 -0
  100. package/dist/spec/operations/session.d.ts.map +1 -1
  101. package/dist/spec/operations/session.js +21 -1
  102. package/dist/spec/operations/session.js.map +1 -1
  103. package/dist/testing/sandbox.d.ts +2 -0
  104. package/dist/testing/sandbox.d.ts.map +1 -0
  105. package/dist/testing/sandbox.js +24 -0
  106. package/dist/testing/sandbox.js.map +1 -0
  107. package/dist/version.d.ts +1 -1
  108. package/dist/version.js +1 -1
  109. package/package.json +9 -5
package/README.md CHANGED
@@ -14,9 +14,6 @@ max chats list --limit 5
14
14
  max messages list "Иван Петров"
15
15
  ```
16
16
 
17
- > ⚠ **Версия `0.1.0` ещё не опубликована.** Под именем `@leemour/max-cli` на npm пока нет ничего.
18
- > Пока — [установка из исходников](docs/installation.md).
19
-
20
17
  ## Что это даёт
21
18
 
22
19
  - **Один запуск — одна операция.** Соединиться, сделать, напечатать, отключиться. Ничего не висит
@@ -33,12 +30,15 @@ max messages list "Иван Петров"
33
30
  MAX схлопывает дубль — это измерено на реальном аккаунте, а не предположено.
34
31
  - **Имя чата не угадывается.** Часть названия, подходящая к двум чатам, — это отказ со списком
35
32
  кандидатов. Отправить не в тот разговор нельзя отменить.
36
- - **Диагностика, в которой нет содержимого.** `--verbose` показывает по строке на запрос,
33
+ - **Диагностика, в которой нет содержимого.** `--trace` показывает по строке на запрос,
37
34
  `--record` кладёт их в каталог запуска на 30 дней. Операция, опкод, идентификаторы, байты,
38
35
  длительности — да. Название чата, имя, текст, телефон, токен — никогда, ни обрезанными, ни
39
36
  хэшем.
40
37
  - **По умолчанию не записывается ничего.** Мессенджер, который сам собирает каталог с историей
41
38
  того, кого вы читали, — это чужая жизнь в чужом логе; запись включается флагом.
39
+ - **Каждый вход спрашивает только то, что изменилось.** Метка времени прошлого входа уходит
40
+ обратно, и MAX присылает дельту, а не весь список заново — так что контакты остаются свежими,
41
+ не стоя ничего.
42
42
  - **Два рантайма, и это проверяется.** Node 22+ и Bun; под обоими в CI выполняется собранная
43
43
  команда, а не только проверка типов.
44
44
  - **Мы выглядим как официальный клиент.** На проводе нет ни нашего имени, ни своего user-agent:
@@ -59,11 +59,16 @@ max messages list "Иван Петров"
59
59
  Пакет — **`@leemour/max-cli`**, команда, которую он ставит, — **`max`**. Имя без области занято
60
60
  чужим пакетом с 2018 года.
61
61
 
62
- Пока `0.1.0` не опубликован, ставится из исходников:
62
+ Запустить, ничего не устанавливая:
63
+
64
+ ```sh
65
+ npx @leemour/max-cli --help
66
+ ```
67
+
68
+ Поставить насовсем:
63
69
 
64
70
  ```sh
65
- git clone git@github.com:leemour/max-cli.git
66
- cd max-cli && pnpm install && pnpm build && pnpm link --global
71
+ npm install -g @leemour/max-cli
67
72
  max --version # 0.1.0
68
73
  ```
69
74
 
@@ -93,16 +98,24 @@ export MAX_PROFILE=personal # или на всю сессию оболочки
93
98
 
94
99
  ```sh
95
100
  max chats list --limit 5
96
- max contacts list
101
+ max contacts list # люди, с кем есть личный чат
97
102
  max messages list 0 --limit 50 # по id чата
98
103
  max messages list "Иван Петров" # или по части названия
99
104
  max messages send 0 "текст"
100
105
  ```
101
106
 
107
+ Списки листаются одинаково везде, а история — по времени, потому что она к нему и привязана:
108
+
109
+ ```sh
110
+ max contacts list --limit 5 --page 2 # шестой по десятый
111
+ max contacts list --all --order name # всё, по алфавиту
112
+ max messages list 0 --before 2026-09-20T01:00:00Z
113
+ ```
114
+
102
115
  Что делала команда:
103
116
 
104
117
  ```sh
105
- max chats list --verbose # показать по строке на запрос, ничего не сохраняя
118
+ max chats list --trace # показать по строке на запрос, ничего не сохраняя
106
119
  max chats list --record # сохранить, ничего не показывая
107
120
  max runs list # что делалось, новое сверху
108
121
  max runs show <id>
@@ -126,8 +139,11 @@ max chats list --json
126
139
  ```
127
140
 
128
141
  `--json` — это **ровно одно значение JSON на stdout и больше ничего**: ни спиннера, ни галочки, ни
129
- предупреждения. То же самое включается само, когда stdout не терминал. Ошибка уходит на stderr, а
130
- stdout остаётся пустым, поэтому отказ невозможно принять за результат:
142
+ предупреждения. То же самое включается само, когда stdout не терминал. Любой список отвечает одним
143
+ объектом — `{ "items": […], "page": 1, "limit": 20, "hasMore": true }` — и `--all` с `--offline`
144
+ отвечают **тем же**, чтобы разбирающему ответ не приходилось ветвиться на две формы.
145
+
146
+ Ошибка уходит на stderr, а stdout остаётся пустым, поэтому отказ невозможно принять за результат:
131
147
 
132
148
  ```json
133
149
  {"error":{"code":"authentication_error","message":"no session for profile \"default\" — run `max session start`"}}
@@ -144,7 +160,7 @@ stdout остаётся пустым, поэтому отказ невозмож
144
160
  | [docs/usage.md](docs/usage.md) | вход, чтение, отправка, машинный режим |
145
161
  | [docs/sessions.md](docs/sessions.md) | токен, ключница, профили |
146
162
  | [docs/configuration.md](docs/configuration.md) | настройки, переменные, порядок разрешения |
147
- | [docs/diagnostics.md](docs/diagnostics.md) | `--verbose`, `--record`, `max runs` |
163
+ | [docs/diagnostics.md](docs/diagnostics.md) | `--trace`, `--record`, `max runs` |
148
164
  | [docs/security.md](docs/security.md) | что пишется на диск, а что никогда |
149
165
  | [docs/troubleshooting.md](docs/troubleshooting.md) | по симптому: что делать, когда не работает |
150
166
  | [docs/commands.md](docs/commands.md) | каждая команда и опция — **генерируется** из программы |
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cache/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,UAAU,EAAa,MAAM,YAAY,CAAA;AAEvD,YAAY,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAE5C,MAAM,WAAW,mBAAmB;IAClC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAA;IACvB;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;CACtC;AAED;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,GAC3B,SAAS,MAAM,EACf,qBAAkC,mBAAwB,KACzD,OAAO,CAAC,UAAU,GAAG,SAAS,CAehC,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cache/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,UAAU,EAAa,MAAM,YAAY,CAAA;AAEvD,YAAY,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAE5C,MAAM,WAAW,mBAAmB;IAClC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAA;IACvB;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;CACtC;AAED;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,GAC3B,SAAS,MAAM,EACf,qBAAkC,mBAAwB,KACzD,OAAO,CAAC,UAAU,GAAG,SAAS,CAehC,CAAA"}
@@ -2,6 +2,7 @@ import { chmodSync, mkdirSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { resolvePaths } from "@leemour/cli-core";
4
4
  import { openCache } from "./open.js";
5
+ import { NewerCacheError } from "./schema.js";
5
6
  import { openStore } from "./store.js";
6
7
  /**
7
8
  * Opens this profile's record, **or gives up without failing the command**.
@@ -27,6 +28,8 @@ export const openProfileCache = async (profile, { env = process.env, onProblem }
27
28
  };
28
29
  /** The reason, never the path — a profile name and a home directory are nobody else's business. */
29
30
  const asReason = (error) => {
31
+ if (error instanceof NewerCacheError)
32
+ return error.message;
30
33
  const code = error?.code;
31
34
  return typeof code === "string" ? code : error instanceof Error ? error.name : "an unknown problem";
32
35
  };
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cache/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AAC9C,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAChC,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAChD,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AACrC,OAAO,EAAmB,SAAS,EAAE,MAAM,YAAY,CAAA;AAiBvD;;;;;GAKG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,KAAK,EACnC,OAAe,EACf,EAAE,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE,SAAS,KAA0B,EAAE,EACzB,EAAE;IACnC,MAAM,KAAK,GAAG,YAAY,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAA;IACtE,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,OAAO,KAAK,CAAC,CAAA;IAE/C,IAAI,CAAC;QACH,2FAA2F;QAC3F,uBAAuB;QACvB,SAAS,CAAC,KAAK,CAAC,KAAK,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAA;QACxD,MAAM,KAAK,GAAG,SAAS,CAAC,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QAC5D,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,CAAA;QACtB,OAAO,KAAK,CAAA;IACd,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,SAAS,EAAE,CAAC,6DAA6D,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,CAAA;QAC3F,OAAO,SAAS,CAAA;IAClB,CAAC;AACH,CAAC,CAAA;AAED,mGAAmG;AACnG,MAAM,QAAQ,GAAG,CAAC,KAAc,EAAU,EAAE;IAC1C,MAAM,IAAI,GAAI,KAA4B,EAAE,IAAI,CAAA;IAChD,OAAO,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,oBAAoB,CAAA;AACrG,CAAC,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cache/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AAC9C,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAChC,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAChD,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AACrC,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAC7C,OAAO,EAAmB,SAAS,EAAE,MAAM,YAAY,CAAA;AAiBvD;;;;;GAKG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,KAAK,EACnC,OAAe,EACf,EAAE,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE,SAAS,KAA0B,EAAE,EACzB,EAAE;IACnC,MAAM,KAAK,GAAG,YAAY,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAA;IACtE,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,OAAO,KAAK,CAAC,CAAA;IAE/C,IAAI,CAAC;QACH,2FAA2F;QAC3F,uBAAuB;QACvB,SAAS,CAAC,KAAK,CAAC,KAAK,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAA;QACxD,MAAM,KAAK,GAAG,SAAS,CAAC,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QAC5D,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,CAAA;QACtB,OAAO,KAAK,CAAA;IACd,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,SAAS,EAAE,CAAC,6DAA6D,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,CAAA;QAC3F,OAAO,SAAS,CAAA;IAClB,CAAC;AACH,CAAC,CAAA;AAED,mGAAmG;AACnG,MAAM,QAAQ,GAAG,CAAC,KAAc,EAAU,EAAE;IAC1C,IAAI,KAAK,YAAY,eAAe;QAAE,OAAO,KAAK,CAAC,OAAO,CAAA;IAC1D,MAAM,IAAI,GAAI,KAA4B,EAAE,IAAI,CAAA;IAChD,OAAO,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,oBAAoB,CAAA;AACrG,CAAC,CAAA"}
@@ -3,12 +3,24 @@ import type { CacheDatabase } from "./driver.js";
3
3
  * **Raise this on every change to the statements below.** The second schema change is the one that
4
4
  * corrupts somebody's file, because the first is always made while the only copy is your own.
5
5
  */
6
- export declare const SCHEMA_VERSION = 1;
6
+ export declare const SCHEMA_VERSION = 4;
7
7
  /**
8
8
  * Brings a file up to date, and **refuses a file from the future** rather than writing to it.
9
9
  *
10
10
  * A newer `max` may have added a column this one does not know about. Reading it is survivable;
11
11
  * writing to it is how one version quietly destroys what another stored.
12
+ *
13
+ * **An older file is rebuilt, not altered.** Everything in here comes back from MAX, so a
14
+ * column-by-column migration would be code that runs once, is tested never, and is how the second
15
+ * schema change corrupts somebody's file. What a rebuild costs is one full login — which is what
16
+ * every command did before the delta sync existed.
17
+ *
18
+ * ⚠ **This stops being the right answer the day the database holds something MAX cannot re-send.**
19
+ * Contacts that are in no chat would be exactly that (`RES-7`), and the day they arrive this
20
+ * becomes `ALTER TABLE … ADD COLUMN` and this paragraph gets rewritten.
12
21
  */
22
+ /** Ours, so its message is known to hold no path and can be shown as it is. */
23
+ export declare class NewerCacheError extends Error {
24
+ }
13
25
  export declare const migrate: (database: CacheDatabase) => void;
14
26
  //# sourceMappingURL=schema.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/cache/schema.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAEhD;;;GAGG;AACH,eAAO,MAAM,cAAc,IAAI,CAAA;AAiF/B;;;;;GAKG;AACH,eAAO,MAAM,OAAO,GAAI,UAAU,aAAa,KAAG,IAcjD,CAAA"}
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/cache/schema.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAEhD;;;GAGG;AACH,eAAO,MAAM,cAAc,IAAI,CAAA;AAqM/B;;;;;;;;;;;;;;GAcG;AACH,+EAA+E;AAC/E,qBAAa,eAAgB,SAAQ,KAAK;CAAG;AAE7C,eAAO,MAAM,OAAO,GAAI,UAAU,aAAa,KAAG,IAgBjD,CAAA"}
@@ -2,7 +2,7 @@
2
2
  * **Raise this on every change to the statements below.** The second schema change is the one that
3
3
  * corrupts somebody's file, because the first is always made while the only copy is your own.
4
4
  */
5
- export const SCHEMA_VERSION = 1;
5
+ export const SCHEMA_VERSION = 4;
6
6
  /**
7
7
  * Everything the cache holds, and the indexes are part of it rather than an afterthought — each
8
8
  * one exists for a query that is actually made.
@@ -25,12 +25,48 @@ const STATEMENTS = [
25
25
  fetched_at INTEGER NOT NULL
26
26
  )`,
27
27
  `CREATE INDEX IF NOT EXISTS chats_by_recency ON chats (last_message_at DESC)`,
28
- `CREATE TABLE IF NOT EXISTS contacts (
29
- id TEXT PRIMARY KEY,
30
- name TEXT,
31
- username TEXT,
32
- description TEXT,
33
- fetched_at INTEGER NOT NULL
28
+ /**
29
+ * **Everyone MAX has named for us, whatever the reason** — not an address book and not a contact
30
+ * list. A group member gets a row here because their name has to live somewhere; whether they
31
+ * are a *contact* is a question the query answers (a dialog with them exists), never a flag
32
+ * somebody has to maintain (`NEED-105`).
33
+ *
34
+ * `last_messaged_at` is stored rather than derived because it is the default order: recomputing
35
+ * `MAX(chats.last_message_at)` per person on every listing is the work this table exists to
36
+ * avoid. `source` records how we met them and is diagnostic — it is not what the filter reads.
37
+ */
38
+ `CREATE TABLE IF NOT EXISTS people (
39
+ id TEXT PRIMARY KEY,
40
+ name TEXT,
41
+ username TEXT,
42
+ description TEXT,
43
+ last_messaged_at INTEGER,
44
+ source TEXT NOT NULL,
45
+ fetched_at INTEGER NOT NULL
46
+ )`,
47
+ `CREATE INDEX IF NOT EXISTS people_by_recency ON people (last_messaged_at DESC)`,
48
+ /**
49
+ * Who is in what. It is what the client used to work out in memory and throw away, and it is how
50
+ * "which groups do I share with this person" becomes one indexed lookup — every chat in this
51
+ * database is one the owner is in, so the chats of a person *are* the shared set.
52
+ */
53
+ `CREATE TABLE IF NOT EXISTS chat_members (
54
+ chat_id TEXT NOT NULL,
55
+ person_id TEXT NOT NULL,
56
+ PRIMARY KEY (chat_id, person_id)
57
+ )`,
58
+ `CREATE INDEX IF NOT EXISTS members_by_person ON chat_members (person_id)`,
59
+ /**
60
+ * Where the last login's `time` is kept, so the next one can ask for the delta instead of the
61
+ * whole collection. One row, and the `CHECK` is what keeps it that way.
62
+ *
63
+ * It lives here rather than in the profile's state file because a marker that outlives the rows
64
+ * it describes is a promise this database cannot keep: `max cache clear` has to forget both in
65
+ * the same breath, and a marker in another file would survive it.
66
+ */
67
+ `CREATE TABLE IF NOT EXISTS sync_marker (
68
+ id INTEGER PRIMARY KEY CHECK (id = 1),
69
+ marker INTEGER NOT NULL
34
70
  )`,
35
71
  `CREATE TABLE IF NOT EXISTS messages (
36
72
  chat_id TEXT NOT NULL,
@@ -42,11 +78,14 @@ const STATEMENTS = [
42
78
  text TEXT NOT NULL,
43
79
  outgoing INTEGER,
44
80
  attachments TEXT NOT NULL,
81
+ link TEXT,
45
82
  fetched_at INTEGER NOT NULL,
46
83
  PRIMARY KEY (chat_id, id)
47
84
  )`,
48
85
  `CREATE INDEX IF NOT EXISTS messages_by_time ON messages (chat_id, time DESC)`,
49
86
  `CREATE INDEX IF NOT EXISTS messages_by_update ON messages (chat_id, update_time)`,
87
+ /** `messages list --before <id>` resolves an id to its `time`, without knowing which chat. */
88
+ `CREATE INDEX IF NOT EXISTS messages_by_id ON messages (id)`,
50
89
  /**
51
90
  * The windows we claim to hold **completely**, which is what makes "absent means deleted" safe
52
91
  * to believe. A message outside every range is a message we have never looked for; a message
@@ -68,6 +107,77 @@ const STATEMENTS = [
68
107
  kind TEXT PRIMARY KEY,
69
108
  at INTEGER NOT NULL
70
109
  )`,
110
+ /**
111
+ * **Search, as three indexes over tables that already hold the text.**
112
+ *
113
+ * `content=` means FTS5 stores the index and **not a second copy of the words** — it reads the
114
+ * columns back from the base table by rowid. That matters twice: the file does not double, and
115
+ * message bodies do not get a second home in it.
116
+ *
117
+ * ⚠ **`trigram`, not `unicode61`, and the difference is the whole point.** Measured 2026-09-22
118
+ * on both runtimes: `unicode61` matches whole words or prefixes, so searching `етро` finds
119
+ * nothing, while `trigram` matches inside a word and finds `Иван Петров`. `chats.resolve`
120
+ * already matches with `includes()`, so substring is the behaviour that was already promised.
121
+ *
122
+ * Both tokenizers fold case for any alphabet, which `LIKE`, `lower()` and `COLLATE NOCASE` do
123
+ * **not** — those are ASCII-only, and a name search built on them silently misses half a Russian
124
+ * address book. That measurement is why this is FTS5 at all.
125
+ *
126
+ * ⚠ **A trigram index cannot answer a query shorter than three characters.** It returns nothing
127
+ * rather than failing, so the caller refuses such a query instead of printing an empty list —
128
+ * `src/client.ts`, and it refuses on every path so the answer cannot depend on whether a cache
129
+ * happens to exist.
130
+ */
131
+ `CREATE VIRTUAL TABLE IF NOT EXISTS chats_fts USING fts5(title, content='chats', tokenize='trigram')`,
132
+ `CREATE VIRTUAL TABLE IF NOT EXISTS people_fts USING fts5(name, username, content='people', tokenize='trigram')`,
133
+ `CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(text, content='messages', tokenize='trigram')`,
134
+ /**
135
+ * **Triggers, because an external-content index does not follow its table on its own.**
136
+ *
137
+ * Measured: rename a chat with no trigger in place and a search for the *old* title still
138
+ * matches the row, handing back the new name. Nothing errors; the index is simply a lie. And
139
+ * SQLite reuses a freed rowid, so a deleted chat can bequeath its words to the next one.
140
+ *
141
+ * They are triggers rather than writes in TypeScript because there is more than one write path
142
+ * — the chat list, the people upsert, the delta merge, the message window — and the failure of
143
+ * forgetting one is invisible.
144
+ *
145
+ * ⚠ Verified on this project's real statements, not on a plain `UPDATE`: our writes are
146
+ * `INSERT … ON CONFLICT DO UPDATE`, and `people` resolves its columns with `coalesce`, so the
147
+ * trigger has to index the row that results rather than the values that arrived. `new.*` in an
148
+ * `AFTER UPDATE` trigger is the finished row, which is what makes a re-sent person with no name
149
+ * keep the name we already had.
150
+ */
151
+ `CREATE TRIGGER IF NOT EXISTS chats_fts_ai AFTER INSERT ON chats BEGIN
152
+ INSERT INTO chats_fts(rowid, title) VALUES (new.rowid, new.title);
153
+ END`,
154
+ `CREATE TRIGGER IF NOT EXISTS chats_fts_ad AFTER DELETE ON chats BEGIN
155
+ INSERT INTO chats_fts(chats_fts, rowid, title) VALUES ('delete', old.rowid, old.title);
156
+ END`,
157
+ `CREATE TRIGGER IF NOT EXISTS chats_fts_au AFTER UPDATE ON chats BEGIN
158
+ INSERT INTO chats_fts(chats_fts, rowid, title) VALUES ('delete', old.rowid, old.title);
159
+ INSERT INTO chats_fts(rowid, title) VALUES (new.rowid, new.title);
160
+ END`,
161
+ `CREATE TRIGGER IF NOT EXISTS people_fts_ai AFTER INSERT ON people BEGIN
162
+ INSERT INTO people_fts(rowid, name, username) VALUES (new.rowid, new.name, new.username);
163
+ END`,
164
+ `CREATE TRIGGER IF NOT EXISTS people_fts_ad AFTER DELETE ON people BEGIN
165
+ INSERT INTO people_fts(people_fts, rowid, name, username) VALUES ('delete', old.rowid, old.name, old.username);
166
+ END`,
167
+ `CREATE TRIGGER IF NOT EXISTS people_fts_au AFTER UPDATE ON people BEGIN
168
+ INSERT INTO people_fts(people_fts, rowid, name, username) VALUES ('delete', old.rowid, old.name, old.username);
169
+ INSERT INTO people_fts(rowid, name, username) VALUES (new.rowid, new.name, new.username);
170
+ END`,
171
+ `CREATE TRIGGER IF NOT EXISTS messages_fts_ai AFTER INSERT ON messages BEGIN
172
+ INSERT INTO messages_fts(rowid, text) VALUES (new.rowid, new.text);
173
+ END`,
174
+ `CREATE TRIGGER IF NOT EXISTS messages_fts_ad AFTER DELETE ON messages BEGIN
175
+ INSERT INTO messages_fts(messages_fts, rowid, text) VALUES ('delete', old.rowid, old.text);
176
+ END`,
177
+ `CREATE TRIGGER IF NOT EXISTS messages_fts_au AFTER UPDATE ON messages BEGIN
178
+ INSERT INTO messages_fts(messages_fts, rowid, text) VALUES ('delete', old.rowid, old.text);
179
+ INSERT INTO messages_fts(rowid, text) VALUES (new.rowid, new.text);
180
+ END`,
71
181
  `CREATE TABLE IF NOT EXISTS fetch_lease (
72
182
  chat_id TEXT NOT NULL,
73
183
  anchor TEXT NOT NULL,
@@ -81,15 +191,56 @@ const STATEMENTS = [
81
191
  *
82
192
  * A newer `max` may have added a column this one does not know about. Reading it is survivable;
83
193
  * writing to it is how one version quietly destroys what another stored.
194
+ *
195
+ * **An older file is rebuilt, not altered.** Everything in here comes back from MAX, so a
196
+ * column-by-column migration would be code that runs once, is tested never, and is how the second
197
+ * schema change corrupts somebody's file. What a rebuild costs is one full login — which is what
198
+ * every command did before the delta sync existed.
199
+ *
200
+ * ⚠ **This stops being the right answer the day the database holds something MAX cannot re-send.**
201
+ * Contacts that are in no chat would be exactly that (`RES-7`), and the day they arrive this
202
+ * becomes `ALTER TABLE … ADD COLUMN` and this paragraph gets rewritten.
84
203
  */
204
+ /** Ours, so its message is known to hold no path and can be shown as it is. */
205
+ export class NewerCacheError extends Error {
206
+ }
85
207
  export const migrate = (database) => {
86
208
  const current = Number(database.prepare("PRAGMA user_version").get()?.user_version ?? 0);
87
209
  if (current > SCHEMA_VERSION) {
88
- throw new Error(`this cache was written by a newer max (schema ${current}, this one speaks ${SCHEMA_VERSION}) — ` +
210
+ throw new NewerCacheError(`this cache was written by a newer max (schema ${current}, this one speaks ${SCHEMA_VERSION}) — ` +
89
211
  "run `max cache clear`, or use the newer version");
90
212
  }
213
+ if (current > 0 && current < SCHEMA_VERSION)
214
+ rebuild(database);
91
215
  for (const statement of STATEMENTS)
92
216
  database.exec(statement);
93
217
  database.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`);
94
218
  };
219
+ /**
220
+ * Drops what is there by asking the file rather than by listing what we think a previous version
221
+ * wrote. A version that added a table we have since forgotten would otherwise survive the rebuild
222
+ * and collide with a later name.
223
+ *
224
+ * The `fetched` table goes with the rest on purpose: it records that a collection was complete,
225
+ * and keeping it would claim a sweep whose rows have just been thrown away.
226
+ */
227
+ const rebuild = (database) => {
228
+ // ⚠ **Virtual tables first.** An FTS5 index keeps four shadow tables of its own, and
229
+ // `sqlite_master` lists them as ordinary tables. Dropping one of those out from under a live
230
+ // index is how a rebuild leaves a corrupt file behind. Dropping the virtual table takes its
231
+ // shadows with it, and the second pass then finds only real tables.
232
+ //
233
+ // It happens to work without this today, because `sqlite_master` returns them in creation order
234
+ // and `IF EXISTS` swallows the leftovers. That is an accident of ordering, not a guarantee.
235
+ const virtual = database
236
+ .prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND sql LIKE 'CREATE VIRTUAL TABLE%'")
237
+ .all();
238
+ for (const { name } of virtual)
239
+ database.exec(`DROP TABLE IF EXISTS "${String(name)}"`);
240
+ const tables = database
241
+ .prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'")
242
+ .all();
243
+ for (const { name } of tables)
244
+ database.exec(`DROP TABLE IF EXISTS "${String(name)}"`);
245
+ };
95
246
  //# sourceMappingURL=schema.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/cache/schema.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,CAAA;AAE/B;;;;;;;;;GASG;AACH,MAAM,UAAU,GAAG;IACjB;;;;;;;;;KASG;IACH,6EAA6E;IAE7E;;;;;;KAMG;IAEH;;;;;;;;;;;;KAYG;IACH,8EAA8E;IAC9E,kFAAkF;IAElF;;;;OAIG;IACH;;;;;KAKG;IACH,0EAA0E;IAE1E;;;;OAIG;IACH;;;KAGG;IAEH;;;;;;KAMG;CACJ,CAAA;AAED;;;;;GAKG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,QAAuB,EAAQ,EAAE;IACvD,MAAM,OAAO,GAAG,MAAM,CACnB,QAAQ,CAAC,OAAO,CAAC,qBAAqB,CAAC,CAAC,GAAG,EAAgC,EAAE,YAAY,IAAI,CAAC,CAChG,CAAA;IAED,IAAI,OAAO,GAAG,cAAc,EAAE,CAAC;QAC7B,MAAM,IAAI,KAAK,CACb,iDAAiD,OAAO,qBAAqB,cAAc,MAAM;YAC/F,iDAAiD,CACpD,CAAA;IACH,CAAC;IAED,KAAK,MAAM,SAAS,IAAI,UAAU;QAAE,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;IAC5D,QAAQ,CAAC,IAAI,CAAC,yBAAyB,cAAc,EAAE,CAAC,CAAA;AAC1D,CAAC,CAAA"}
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/cache/schema.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,CAAA;AAE/B;;;;;;;;;GASG;AACH,MAAM,UAAU,GAAG;IACjB;;;;;;;;;KASG;IACH,6EAA6E;IAE7E;;;;;;;;;OASG;IACH;;;;;;;;KAQG;IACH,gFAAgF;IAEhF;;;;OAIG;IACH;;;;KAIG;IACH,0EAA0E;IAE1E;;;;;;;OAOG;IACH;;;KAGG;IAEH;;;;;;;;;;;;;KAaG;IACH,8EAA8E;IAC9E,kFAAkF;IAClF,8FAA8F;IAC9F,4DAA4D;IAE5D;;;;OAIG;IACH;;;;;KAKG;IACH,0EAA0E;IAE1E;;;;OAIG;IACH;;;KAGG;IAEH;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,qGAAqG;IACrG,gHAAgH;IAChH,0GAA0G;IAE1G;;;;;;;;;;;;;;;;OAgBG;IACH;;OAEK;IACL;;OAEK;IACL;;;OAGK;IAEL;;OAEK;IACL;;OAEK;IACL;;;OAGK;IAEL;;OAEK;IACL;;OAEK;IACL;;;OAGK;IAEL;;;;;;KAMG;CACJ,CAAA;AAED;;;;;;;;;;;;;;GAcG;AACH,+EAA+E;AAC/E,MAAM,OAAO,eAAgB,SAAQ,KAAK;CAAG;AAE7C,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,QAAuB,EAAQ,EAAE;IACvD,MAAM,OAAO,GAAG,MAAM,CACnB,QAAQ,CAAC,OAAO,CAAC,qBAAqB,CAAC,CAAC,GAAG,EAAgC,EAAE,YAAY,IAAI,CAAC,CAChG,CAAA;IAED,IAAI,OAAO,GAAG,cAAc,EAAE,CAAC;QAC7B,MAAM,IAAI,eAAe,CACvB,iDAAiD,OAAO,qBAAqB,cAAc,MAAM;YAC/F,iDAAiD,CACpD,CAAA;IACH,CAAC;IAED,IAAI,OAAO,GAAG,CAAC,IAAI,OAAO,GAAG,cAAc;QAAE,OAAO,CAAC,QAAQ,CAAC,CAAA;IAE9D,KAAK,MAAM,SAAS,IAAI,UAAU;QAAE,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;IAC5D,QAAQ,CAAC,IAAI,CAAC,yBAAyB,cAAc,EAAE,CAAC,CAAA;AAC1D,CAAC,CAAA;AAED;;;;;;;GAOG;AACH,MAAM,OAAO,GAAG,CAAC,QAAuB,EAAQ,EAAE;IAChD,qFAAqF;IACrF,6FAA6F;IAC7F,4FAA4F;IAC5F,oEAAoE;IACpE,EAAE;IACF,gGAAgG;IAChG,4FAA4F;IAC5F,MAAM,OAAO,GAAG,QAAQ;SACrB,OAAO,CAAC,0FAA0F,CAAC;SACnG,GAAG,EAAE,CAAA;IACR,KAAK,MAAM,EAAE,IAAI,EAAE,IAAI,OAAO;QAAE,QAAQ,CAAC,IAAI,CAAC,yBAAyB,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;IAEvF,MAAM,MAAM,GAAG,QAAQ;SACpB,OAAO,CAAC,kFAAkF,CAAC;SAC3F,GAAG,EAAE,CAAA;IAER,KAAK,MAAM,EAAE,IAAI,EAAE,IAAI,MAAM;QAAE,QAAQ,CAAC,IAAI,CAAC,yBAAyB,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;AACxF,CAAC,CAAA"}
@@ -1,5 +1,56 @@
1
- import type { Chat, Contact, Id, Message } from "../domain/models.js";
1
+ import type { Chat, ChatKind, Contact, Id, Message, MessageHit } from "../domain/models.js";
2
2
  import type { CacheDatabase } from "./driver.js";
3
+ /** How we came to know a person. Diagnostic — what makes somebody a *contact* is the query. */
4
+ export type PersonSource = "login" | "info" | "participant" | "sync";
5
+ export type PersonOrder = "recent" | "name";
6
+ export interface PageOptions {
7
+ order: PersonOrder;
8
+ limit: number;
9
+ offset: number;
10
+ /** Part of a name, already checked to be long enough for the index (`src/client.ts`). */
11
+ query?: string;
12
+ }
13
+ export interface ChatPageOptions {
14
+ limit: number;
15
+ offset: number;
16
+ query?: string;
17
+ kind?: ChatKind;
18
+ }
19
+ export interface MessageSearch {
20
+ query: string;
21
+ /** One chat, or every chat we hold when absent. */
22
+ chatId?: Id;
23
+ limit: number;
24
+ offset: number;
25
+ }
26
+ /**
27
+ * One login's worth of change: the chats that moved, the people that changed, who is in which
28
+ * chat, and the `time` MAX answered with.
29
+ *
30
+ * `members` is keyed by chat id and holds that chat's **whole** membership minus ourselves, so a
31
+ * chat named here has its rows replaced rather than added to. A chat absent from the map keeps
32
+ * the members it already had.
33
+ */
34
+ /**
35
+ * What a merge did, in counts and **nothing else** — no name, no username, no description. It is
36
+ * what `max contacts sync` prints, and a summary that named anybody would be the one place the
37
+ * sixth constraint leaks.
38
+ *
39
+ * `changed` is "already known and sent again". A delta carries only what moved, so for an ordinary
40
+ * login that is exactly what it sounds like; for a full re-take, where MAX resends everything, it
41
+ * reads as "re-sent" instead — which is what `full` in the command's output is there to say.
42
+ */
43
+ export interface SyncSummary {
44
+ known: number;
45
+ added: number;
46
+ changed: number;
47
+ }
48
+ export interface SyncDelta {
49
+ chats: Chat[];
50
+ people: Contact[];
51
+ members: Map<Id, Id[]>;
52
+ marker: number;
53
+ }
3
54
  export interface CacheOptions {
4
55
  database: CacheDatabase;
5
56
  /** Injected so a freshness test costs nothing and does not wait. */
@@ -10,17 +61,77 @@ export interface CacheStore {
10
61
  chats: {
11
62
  read(freshForMs: number): Chat[] | undefined;
12
63
  write(chats: Chat[]): void;
64
+ /** One page, newest first, in SQL rather than by building the whole list and slicing it. */
65
+ page(options: ChatPageOptions): Chat[];
66
+ /** ⚠ Takes the same filter as `page`, or the two disagree about whether a next page exists. */
67
+ count(options?: {
68
+ query?: string;
69
+ kind?: ChatKind;
70
+ }): number;
13
71
  };
14
- contacts: {
15
- read(freshForMs: number): Contact[] | undefined;
16
- write(contacts: Contact[]): void;
72
+ people: {
73
+ /**
74
+ * One page of **contacts** — the people a one-to-one chat exists with. `recent` is
75
+ * `last_messaged_at` newest first, the never-messaged last, then by name.
76
+ *
77
+ * A group member is not in this answer, and nothing marks them as excluded: they are simply
78
+ * not in the set the query asks for (`NEED-105`).
79
+ */
80
+ contacts(options: PageOptions): Contact[];
81
+ /** How many that query would return, so a page can say whether another one exists. */
82
+ countContacts(options?: {
83
+ query?: string;
84
+ }): number;
85
+ /** Everyone we can put a name to, contact or not. */
86
+ page(options: PageOptions): Contact[];
87
+ /** Everyone, counted — what `max contacts sync` reports as known. */
88
+ count(): number;
89
+ /** Carries how we met them. Never deletes: absence from a delta means unchanged. */
90
+ upsert(people: Contact[], source: PersonSource): void;
91
+ /** The chats this person is in — which, every chat here being one we are in, is the shared set. */
92
+ chatsWith(personId: Id): Id[];
93
+ /**
94
+ * Recomputes `last_messaged_at` from the dialogs we hold.
95
+ *
96
+ * ⚠ **Call it after any write that adds people.** A person's recency cannot be set while
97
+ * writing the chats, because most people are not known yet at that moment: the login names a
98
+ * handful and the rest arrive from a `CONTACT_INFO` that has not been sent. Measured on the
99
+ * real account 2026-09-21: 6 of 22 people had a recency and the other 16 sorted as
100
+ * never-messaged, which is every dialog partner the login did not name.
101
+ */
102
+ refreshRecency(): void;
17
103
  };
104
+ /** The `time` the last login answered with, or `undefined` for a store that has never synced. */
105
+ syncMarker(): number | undefined;
106
+ /** Rows, memberships, recency and the marker — **in one transaction**. */
107
+ mergeDelta(delta: SyncDelta): SyncSummary;
108
+ /** Makes the next login ask for everything again. */
109
+ forgetSyncMarker(): void;
18
110
  messages: {
19
111
  /** The newest `limit` messages, but only if the window we hold is both fresh and contiguous. */
20
112
  read(chatId: Id, limit: number, freshForMs: number): Message[] | undefined;
21
113
  write(chatId: Id, messages: Message[]): void;
22
114
  /** After a send, what we hold for that chat is missing the message we just added. */
23
115
  invalidate(chatId: Id): void;
116
+ /**
117
+ * When a message was sent, for `messages list --before <id>`.
118
+ *
119
+ * `undefined` for an id we have never stored — which is exactly what a deleted message looks
120
+ * like, since it is no longer in the history MAX returns either.
121
+ */
122
+ timeOf(id: Id): number | undefined;
123
+ /**
124
+ * Messages whose text contains `query`, newest first, across every chat we hold or one.
125
+ *
126
+ * ⚠ **It answers from this database and never asks MAX**, because MAX has no search operation
127
+ * we know of. So it finds what has been read, not what exists — `src/client.ts` is where that
128
+ * is said out loud rather than left for somebody to discover.
129
+ */
130
+ search(options: MessageSearch): MessageHit[];
131
+ countSearch(options: {
132
+ query: string;
133
+ chatId?: Id;
134
+ }): number;
24
135
  };
25
136
  /** True when this process may go to MAX for that window; false when somebody else already is. */
26
137
  claim(chatId: Id, anchor: string, holder: string, forMs: number): boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/cache/store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AACrE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAGhD,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,aAAa,CAAA;IACvB,oEAAoE;IACpE,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CACnB;AAED,sFAAsF;AACtF,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE;QACL,IAAI,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,EAAE,GAAG,SAAS,CAAA;QAC5C,KAAK,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,IAAI,CAAA;KAC3B,CAAA;IACD,QAAQ,EAAE;QACR,IAAI,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,EAAE,GAAG,SAAS,CAAA;QAC/C,KAAK,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,IAAI,CAAA;KACjC,CAAA;IACD,QAAQ,EAAE;QACR,gGAAgG;QAChG,IAAI,CAAC,MAAM,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,EAAE,GAAG,SAAS,CAAA;QAC1E,KAAK,CAAC,MAAM,EAAE,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,IAAI,CAAA;QAC5C,qFAAqF;QACrF,UAAU,CAAC,MAAM,EAAE,EAAE,GAAG,IAAI,CAAA;KAC7B,CAAA;IACD,iGAAiG;IACjG,KAAK,CAAC,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAA;IACzE,OAAO,CAAC,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IACzC,KAAK,IAAI,IAAI,CAAA;IACb,KAAK,IAAI,IAAI,CAAA;CACd;AAED,eAAO,MAAM,SAAS,GAAI,mBAAsC,YAAY,KAAG,UA8L9E,CAAA"}
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/cache/store.ts"],"names":[],"mappings":"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"}