@rozek/nanoclaw 1.2.17

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 (305) hide show
  1. package/.claude/settings.json +1 -0
  2. package/.claude/skills/add-compact/SKILL.md +135 -0
  3. package/.claude/skills/add-discord/SKILL.md +203 -0
  4. package/.claude/skills/add-gmail/SKILL.md +220 -0
  5. package/.claude/skills/add-image-vision/SKILL.md +94 -0
  6. package/.claude/skills/add-ollama-tool/SKILL.md +153 -0
  7. package/.claude/skills/add-parallel/SKILL.md +290 -0
  8. package/.claude/skills/add-pdf-reader/SKILL.md +104 -0
  9. package/.claude/skills/add-reactions/SKILL.md +117 -0
  10. package/.claude/skills/add-slack/SKILL.md +207 -0
  11. package/.claude/skills/add-telegram/SKILL.md +222 -0
  12. package/.claude/skills/add-telegram-swarm/SKILL.md +384 -0
  13. package/.claude/skills/add-voice-transcription/SKILL.md +148 -0
  14. package/.claude/skills/add-whatsapp/SKILL.md +372 -0
  15. package/.claude/skills/convert-to-apple-container/SKILL.md +175 -0
  16. package/.claude/skills/customize/SKILL.md +110 -0
  17. package/.claude/skills/debug/SKILL.md +349 -0
  18. package/.claude/skills/get-qodo-rules/SKILL.md +122 -0
  19. package/.claude/skills/get-qodo-rules/references/output-format.md +41 -0
  20. package/.claude/skills/get-qodo-rules/references/pagination.md +33 -0
  21. package/.claude/skills/get-qodo-rules/references/repository-scope.md +26 -0
  22. package/.claude/skills/qodo-pr-resolver/SKILL.md +326 -0
  23. package/.claude/skills/qodo-pr-resolver/resources/providers.md +329 -0
  24. package/.claude/skills/setup/SKILL.md +218 -0
  25. package/.claude/skills/update-nanoclaw/SKILL.md +235 -0
  26. package/.claude/skills/update-skills/SKILL.md +130 -0
  27. package/.claude/skills/use-local-whisper/SKILL.md +152 -0
  28. package/.claude/skills/x-integration/SKILL.md +417 -0
  29. package/.claude/skills/x-integration/agent.ts +243 -0
  30. package/.claude/skills/x-integration/host.ts +159 -0
  31. package/.claude/skills/x-integration/lib/browser.ts +148 -0
  32. package/.claude/skills/x-integration/lib/config.ts +62 -0
  33. package/.claude/skills/x-integration/scripts/like.ts +56 -0
  34. package/.claude/skills/x-integration/scripts/post.ts +66 -0
  35. package/.claude/skills/x-integration/scripts/quote.ts +80 -0
  36. package/.claude/skills/x-integration/scripts/reply.ts +74 -0
  37. package/.claude/skills/x-integration/scripts/retweet.ts +62 -0
  38. package/.claude/skills/x-integration/scripts/setup.ts +87 -0
  39. package/.env.example +1 -0
  40. package/.github/CODEOWNERS +10 -0
  41. package/.github/PULL_REQUEST_TEMPLATE.md +14 -0
  42. package/.github/workflows/bump-version.yml +32 -0
  43. package/.github/workflows/ci.yml +25 -0
  44. package/.github/workflows/merge-forward-skills.yml +160 -0
  45. package/.github/workflows/update-tokens.yml +42 -0
  46. package/.husky/pre-commit +1 -0
  47. package/.mcp.json +3 -0
  48. package/.nvmrc +1 -0
  49. package/.prettierrc +3 -0
  50. package/CHANGELOG.md +8 -0
  51. package/CLAUDE.md +64 -0
  52. package/CONTRIBUTING.md +23 -0
  53. package/CONTRIBUTORS.md +15 -0
  54. package/LICENSE +21 -0
  55. package/NanoClaw_with_Web-Support.md +290 -0
  56. package/README.md +261 -0
  57. package/README_zh.md +200 -0
  58. package/assets/nanoclaw-favicon.png +0 -0
  59. package/assets/nanoclaw-icon.png +0 -0
  60. package/assets/nanoclaw-logo-dark.png +0 -0
  61. package/assets/nanoclaw-logo.png +0 -0
  62. package/assets/nanoclaw-profile.jpeg +0 -0
  63. package/assets/nanoclaw-sales.png +0 -0
  64. package/assets/social-preview.jpg +0 -0
  65. package/config-examples/mount-allowlist.json +25 -0
  66. package/container/Dockerfile +70 -0
  67. package/container/agent-runner/package-lock.json +1524 -0
  68. package/container/agent-runner/package.json +21 -0
  69. package/container/agent-runner/src/index.ts +558 -0
  70. package/container/agent-runner/src/ipc-mcp-stdio.ts +338 -0
  71. package/container/agent-runner/tsconfig.json +15 -0
  72. package/container/build.sh +23 -0
  73. package/container/skills/agent-browser/SKILL.md +159 -0
  74. package/container/skills/capabilities/SKILL.md +100 -0
  75. package/container/skills/status/SKILL.md +104 -0
  76. package/dist/channels/index.d.ts +2 -0
  77. package/dist/channels/index.d.ts.map +1 -0
  78. package/dist/channels/index.js +9 -0
  79. package/dist/channels/index.js.map +1 -0
  80. package/dist/channels/registry.d.ts +13 -0
  81. package/dist/channels/registry.d.ts.map +1 -0
  82. package/dist/channels/registry.js +11 -0
  83. package/dist/channels/registry.js.map +1 -0
  84. package/dist/channels/registry.test.d.ts +2 -0
  85. package/dist/channels/registry.test.d.ts.map +1 -0
  86. package/dist/channels/registry.test.js +32 -0
  87. package/dist/channels/registry.test.js.map +1 -0
  88. package/dist/channels/web.d.ts +2 -0
  89. package/dist/channels/web.d.ts.map +1 -0
  90. package/dist/channels/web.js +1738 -0
  91. package/dist/channels/web.js.map +1 -0
  92. package/dist/cli.d.ts +11 -0
  93. package/dist/cli.d.ts.map +1 -0
  94. package/dist/cli.js +182 -0
  95. package/dist/cli.js.map +1 -0
  96. package/dist/config.d.ts +19 -0
  97. package/dist/config.d.ts.map +1 -0
  98. package/dist/config.js +36 -0
  99. package/dist/config.js.map +1 -0
  100. package/dist/container-runner.d.ts +44 -0
  101. package/dist/container-runner.d.ts.map +1 -0
  102. package/dist/container-runner.js +467 -0
  103. package/dist/container-runner.js.map +1 -0
  104. package/dist/container-runner.test.d.ts +2 -0
  105. package/dist/container-runner.test.d.ts.map +1 -0
  106. package/dist/container-runner.test.js +150 -0
  107. package/dist/container-runner.test.js.map +1 -0
  108. package/dist/container-runtime.d.ts +22 -0
  109. package/dist/container-runtime.d.ts.map +1 -0
  110. package/dist/container-runtime.js +96 -0
  111. package/dist/container-runtime.js.map +1 -0
  112. package/dist/container-runtime.test.d.ts +2 -0
  113. package/dist/container-runtime.test.d.ts.map +1 -0
  114. package/dist/container-runtime.test.js +93 -0
  115. package/dist/container-runtime.test.js.map +1 -0
  116. package/dist/credential-proxy.d.ts +21 -0
  117. package/dist/credential-proxy.d.ts.map +1 -0
  118. package/dist/credential-proxy.js +95 -0
  119. package/dist/credential-proxy.js.map +1 -0
  120. package/dist/credential-proxy.test.d.ts +2 -0
  121. package/dist/credential-proxy.test.d.ts.map +1 -0
  122. package/dist/credential-proxy.test.js +134 -0
  123. package/dist/credential-proxy.test.js.map +1 -0
  124. package/dist/db.d.ts +115 -0
  125. package/dist/db.d.ts.map +1 -0
  126. package/dist/db.js +549 -0
  127. package/dist/db.js.map +1 -0
  128. package/dist/db.test.d.ts +2 -0
  129. package/dist/db.test.d.ts.map +1 -0
  130. package/dist/db.test.js +360 -0
  131. package/dist/db.test.js.map +1 -0
  132. package/dist/env.d.ts +8 -0
  133. package/dist/env.d.ts.map +1 -0
  134. package/dist/env.js +42 -0
  135. package/dist/env.js.map +1 -0
  136. package/dist/formatting.test.d.ts +2 -0
  137. package/dist/formatting.test.d.ts.map +1 -0
  138. package/dist/formatting.test.js +183 -0
  139. package/dist/formatting.test.js.map +1 -0
  140. package/dist/group-folder.d.ts +5 -0
  141. package/dist/group-folder.d.ts.map +1 -0
  142. package/dist/group-folder.js +44 -0
  143. package/dist/group-folder.js.map +1 -0
  144. package/dist/group-folder.test.d.ts +2 -0
  145. package/dist/group-folder.test.d.ts.map +1 -0
  146. package/dist/group-folder.test.js +29 -0
  147. package/dist/group-folder.test.js.map +1 -0
  148. package/dist/group-queue.d.ts +34 -0
  149. package/dist/group-queue.d.ts.map +1 -0
  150. package/dist/group-queue.js +263 -0
  151. package/dist/group-queue.js.map +1 -0
  152. package/dist/group-queue.test.d.ts +2 -0
  153. package/dist/group-queue.test.d.ts.map +1 -0
  154. package/dist/group-queue.test.js +341 -0
  155. package/dist/group-queue.test.js.map +1 -0
  156. package/dist/index.d.ts +12 -0
  157. package/dist/index.d.ts.map +1 -0
  158. package/dist/index.js +518 -0
  159. package/dist/index.js.map +1 -0
  160. package/dist/ipc-auth.test.d.ts +2 -0
  161. package/dist/ipc-auth.test.d.ts.map +1 -0
  162. package/dist/ipc-auth.test.js +434 -0
  163. package/dist/ipc-auth.test.js.map +1 -0
  164. package/dist/ipc.d.ts +32 -0
  165. package/dist/ipc.d.ts.map +1 -0
  166. package/dist/ipc.js +311 -0
  167. package/dist/ipc.js.map +1 -0
  168. package/dist/logger.d.ts +3 -0
  169. package/dist/logger.d.ts.map +1 -0
  170. package/dist/logger.js +14 -0
  171. package/dist/logger.js.map +1 -0
  172. package/dist/mount-security.d.ts +34 -0
  173. package/dist/mount-security.d.ts.map +1 -0
  174. package/dist/mount-security.js +325 -0
  175. package/dist/mount-security.js.map +1 -0
  176. package/dist/remote-control.d.ts +32 -0
  177. package/dist/remote-control.d.ts.map +1 -0
  178. package/dist/remote-control.js +185 -0
  179. package/dist/remote-control.js.map +1 -0
  180. package/dist/remote-control.test.d.ts +2 -0
  181. package/dist/remote-control.test.d.ts.map +1 -0
  182. package/dist/remote-control.test.js +321 -0
  183. package/dist/remote-control.test.js.map +1 -0
  184. package/dist/router.d.ts +8 -0
  185. package/dist/router.d.ts.map +1 -0
  186. package/dist/router.js +37 -0
  187. package/dist/router.js.map +1 -0
  188. package/dist/routing.test.d.ts +2 -0
  189. package/dist/routing.test.d.ts.map +1 -0
  190. package/dist/routing.test.js +81 -0
  191. package/dist/routing.test.js.map +1 -0
  192. package/dist/sender-allowlist.d.ts +14 -0
  193. package/dist/sender-allowlist.d.ts.map +1 -0
  194. package/dist/sender-allowlist.js +79 -0
  195. package/dist/sender-allowlist.js.map +1 -0
  196. package/dist/sender-allowlist.test.d.ts +2 -0
  197. package/dist/sender-allowlist.test.d.ts.map +1 -0
  198. package/dist/sender-allowlist.test.js +186 -0
  199. package/dist/sender-allowlist.test.js.map +1 -0
  200. package/dist/session-commands.d.ts +47 -0
  201. package/dist/session-commands.d.ts.map +1 -0
  202. package/dist/session-commands.js +102 -0
  203. package/dist/session-commands.js.map +1 -0
  204. package/dist/session-commands.test.d.ts +2 -0
  205. package/dist/session-commands.test.d.ts.map +1 -0
  206. package/dist/session-commands.test.js +190 -0
  207. package/dist/session-commands.test.js.map +1 -0
  208. package/dist/task-scheduler.d.ts +22 -0
  209. package/dist/task-scheduler.d.ts.map +1 -0
  210. package/dist/task-scheduler.js +210 -0
  211. package/dist/task-scheduler.js.map +1 -0
  212. package/dist/task-scheduler.test.d.ts +2 -0
  213. package/dist/task-scheduler.test.d.ts.map +1 -0
  214. package/dist/task-scheduler.test.js +107 -0
  215. package/dist/task-scheduler.test.js.map +1 -0
  216. package/dist/timezone.d.ts +6 -0
  217. package/dist/timezone.d.ts.map +1 -0
  218. package/dist/timezone.js +17 -0
  219. package/dist/timezone.js.map +1 -0
  220. package/dist/timezone.test.d.ts +2 -0
  221. package/dist/timezone.test.d.ts.map +1 -0
  222. package/dist/timezone.test.js +23 -0
  223. package/dist/timezone.test.js.map +1 -0
  224. package/dist/types.d.ts +78 -0
  225. package/dist/types.d.ts.map +1 -0
  226. package/dist/types.js +2 -0
  227. package/dist/types.js.map +1 -0
  228. package/docs/APPLE-CONTAINER-NETWORKING.md +90 -0
  229. package/docs/DEBUG_CHECKLIST.md +143 -0
  230. package/docs/REQUIREMENTS.md +196 -0
  231. package/docs/SDK_DEEP_DIVE.md +643 -0
  232. package/docs/SECURITY.md +122 -0
  233. package/docs/SPEC.md +785 -0
  234. package/docs/docker-sandboxes.md +359 -0
  235. package/docs/nanoclaw-architecture-final.md +1063 -0
  236. package/docs/nanorepo-architecture.md +168 -0
  237. package/docs/skills-as-branches.md +662 -0
  238. package/groups/global/CLAUDE.md +58 -0
  239. package/groups/main/CLAUDE.md +246 -0
  240. package/launchd/com.nanoclaw.plist +32 -0
  241. package/package.json +45 -0
  242. package/repo-tokens/README.md +113 -0
  243. package/repo-tokens/action.yml +186 -0
  244. package/repo-tokens/badge.svg +23 -0
  245. package/repo-tokens/examples/green.svg +14 -0
  246. package/repo-tokens/examples/red.svg +14 -0
  247. package/repo-tokens/examples/yellow-green.svg +14 -0
  248. package/repo-tokens/examples/yellow.svg +14 -0
  249. package/scripts/run-migrations.ts +105 -0
  250. package/setup/container.ts +144 -0
  251. package/setup/environment.test.ts +121 -0
  252. package/setup/environment.ts +94 -0
  253. package/setup/groups.ts +229 -0
  254. package/setup/index.ts +58 -0
  255. package/setup/mounts.ts +115 -0
  256. package/setup/platform.test.ts +120 -0
  257. package/setup/platform.ts +132 -0
  258. package/setup/register.test.ts +257 -0
  259. package/setup/register.ts +177 -0
  260. package/setup/service.test.ts +187 -0
  261. package/setup/service.ts +362 -0
  262. package/setup/status.ts +16 -0
  263. package/setup/verify.ts +192 -0
  264. package/setup.sh +161 -0
  265. package/src/channels/index.ts +12 -0
  266. package/src/channels/registry.test.ts +42 -0
  267. package/src/channels/registry.ts +32 -0
  268. package/src/channels/web.ts +1856 -0
  269. package/src/cli.ts +209 -0
  270. package/src/config.ts +73 -0
  271. package/src/container-runner.test.ts +210 -0
  272. package/src/container-runner.ts +707 -0
  273. package/src/container-runtime.test.ts +149 -0
  274. package/src/container-runtime.ts +127 -0
  275. package/src/credential-proxy.test.ts +192 -0
  276. package/src/credential-proxy.ts +125 -0
  277. package/src/db.test.ts +484 -0
  278. package/src/db.ts +803 -0
  279. package/src/env.ts +42 -0
  280. package/src/formatting.test.ts +256 -0
  281. package/src/group-folder.test.ts +43 -0
  282. package/src/group-folder.ts +44 -0
  283. package/src/group-queue.test.ts +484 -0
  284. package/src/group-queue.ts +365 -0
  285. package/src/index.ts +731 -0
  286. package/src/ipc-auth.test.ts +679 -0
  287. package/src/ipc.ts +461 -0
  288. package/src/logger.ts +16 -0
  289. package/src/mount-security.ts +419 -0
  290. package/src/remote-control.test.ts +397 -0
  291. package/src/remote-control.ts +224 -0
  292. package/src/router.ts +52 -0
  293. package/src/routing.test.ts +170 -0
  294. package/src/sender-allowlist.test.ts +216 -0
  295. package/src/sender-allowlist.ts +128 -0
  296. package/src/session-commands.test.ts +247 -0
  297. package/src/session-commands.ts +163 -0
  298. package/src/task-scheduler.test.ts +129 -0
  299. package/src/task-scheduler.ts +295 -0
  300. package/src/timezone.test.ts +29 -0
  301. package/src/timezone.ts +16 -0
  302. package/src/types.ts +107 -0
  303. package/tsconfig.json +20 -0
  304. package/vitest.config.ts +7 -0
  305. package/vitest.skills.config.ts +7 -0
package/src/db.test.ts ADDED
@@ -0,0 +1,484 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+
3
+ import {
4
+ _initTestDatabase,
5
+ createTask,
6
+ deleteTask,
7
+ getAllChats,
8
+ getAllRegisteredGroups,
9
+ getMessagesSince,
10
+ getNewMessages,
11
+ getTaskById,
12
+ setRegisteredGroup,
13
+ storeChatMetadata,
14
+ storeMessage,
15
+ updateTask,
16
+ } from './db.js';
17
+
18
+ beforeEach(() => {
19
+ _initTestDatabase();
20
+ });
21
+
22
+ // Helper to store a message using the normalized NewMessage interface
23
+ function store(overrides: {
24
+ id: string;
25
+ chat_jid: string;
26
+ sender: string;
27
+ sender_name: string;
28
+ content: string;
29
+ timestamp: string;
30
+ is_from_me?: boolean;
31
+ }) {
32
+ storeMessage({
33
+ id: overrides.id,
34
+ chat_jid: overrides.chat_jid,
35
+ sender: overrides.sender,
36
+ sender_name: overrides.sender_name,
37
+ content: overrides.content,
38
+ timestamp: overrides.timestamp,
39
+ is_from_me: overrides.is_from_me ?? false,
40
+ });
41
+ }
42
+
43
+ // --- storeMessage (NewMessage format) ---
44
+
45
+ describe('storeMessage', () => {
46
+ it('stores a message and retrieves it', () => {
47
+ storeChatMetadata('group@g.us', '2024-01-01T00:00:00.000Z');
48
+
49
+ store({
50
+ id: 'msg-1',
51
+ chat_jid: 'group@g.us',
52
+ sender: '123@s.whatsapp.net',
53
+ sender_name: 'Alice',
54
+ content: 'hello world',
55
+ timestamp: '2024-01-01T00:00:01.000Z',
56
+ });
57
+
58
+ const messages = getMessagesSince(
59
+ 'group@g.us',
60
+ '2024-01-01T00:00:00.000Z',
61
+ 'Andy',
62
+ );
63
+ expect(messages).toHaveLength(1);
64
+ expect(messages[0].id).toBe('msg-1');
65
+ expect(messages[0].sender).toBe('123@s.whatsapp.net');
66
+ expect(messages[0].sender_name).toBe('Alice');
67
+ expect(messages[0].content).toBe('hello world');
68
+ });
69
+
70
+ it('filters out empty content', () => {
71
+ storeChatMetadata('group@g.us', '2024-01-01T00:00:00.000Z');
72
+
73
+ store({
74
+ id: 'msg-2',
75
+ chat_jid: 'group@g.us',
76
+ sender: '111@s.whatsapp.net',
77
+ sender_name: 'Dave',
78
+ content: '',
79
+ timestamp: '2024-01-01T00:00:04.000Z',
80
+ });
81
+
82
+ const messages = getMessagesSince(
83
+ 'group@g.us',
84
+ '2024-01-01T00:00:00.000Z',
85
+ 'Andy',
86
+ );
87
+ expect(messages).toHaveLength(0);
88
+ });
89
+
90
+ it('stores is_from_me flag', () => {
91
+ storeChatMetadata('group@g.us', '2024-01-01T00:00:00.000Z');
92
+
93
+ store({
94
+ id: 'msg-3',
95
+ chat_jid: 'group@g.us',
96
+ sender: 'me@s.whatsapp.net',
97
+ sender_name: 'Me',
98
+ content: 'my message',
99
+ timestamp: '2024-01-01T00:00:05.000Z',
100
+ is_from_me: true,
101
+ });
102
+
103
+ // Message is stored (we can retrieve it — is_from_me doesn't affect retrieval)
104
+ const messages = getMessagesSince(
105
+ 'group@g.us',
106
+ '2024-01-01T00:00:00.000Z',
107
+ 'Andy',
108
+ );
109
+ expect(messages).toHaveLength(1);
110
+ });
111
+
112
+ it('upserts on duplicate id+chat_jid', () => {
113
+ storeChatMetadata('group@g.us', '2024-01-01T00:00:00.000Z');
114
+
115
+ store({
116
+ id: 'msg-dup',
117
+ chat_jid: 'group@g.us',
118
+ sender: '123@s.whatsapp.net',
119
+ sender_name: 'Alice',
120
+ content: 'original',
121
+ timestamp: '2024-01-01T00:00:01.000Z',
122
+ });
123
+
124
+ store({
125
+ id: 'msg-dup',
126
+ chat_jid: 'group@g.us',
127
+ sender: '123@s.whatsapp.net',
128
+ sender_name: 'Alice',
129
+ content: 'updated',
130
+ timestamp: '2024-01-01T00:00:01.000Z',
131
+ });
132
+
133
+ const messages = getMessagesSince(
134
+ 'group@g.us',
135
+ '2024-01-01T00:00:00.000Z',
136
+ 'Andy',
137
+ );
138
+ expect(messages).toHaveLength(1);
139
+ expect(messages[0].content).toBe('updated');
140
+ });
141
+ });
142
+
143
+ // --- getMessagesSince ---
144
+
145
+ describe('getMessagesSince', () => {
146
+ beforeEach(() => {
147
+ storeChatMetadata('group@g.us', '2024-01-01T00:00:00.000Z');
148
+
149
+ store({
150
+ id: 'm1',
151
+ chat_jid: 'group@g.us',
152
+ sender: 'Alice@s.whatsapp.net',
153
+ sender_name: 'Alice',
154
+ content: 'first',
155
+ timestamp: '2024-01-01T00:00:01.000Z',
156
+ });
157
+ store({
158
+ id: 'm2',
159
+ chat_jid: 'group@g.us',
160
+ sender: 'Bob@s.whatsapp.net',
161
+ sender_name: 'Bob',
162
+ content: 'second',
163
+ timestamp: '2024-01-01T00:00:02.000Z',
164
+ });
165
+ storeMessage({
166
+ id: 'm3',
167
+ chat_jid: 'group@g.us',
168
+ sender: 'Bot@s.whatsapp.net',
169
+ sender_name: 'Bot',
170
+ content: 'bot reply',
171
+ timestamp: '2024-01-01T00:00:03.000Z',
172
+ is_bot_message: true,
173
+ });
174
+ store({
175
+ id: 'm4',
176
+ chat_jid: 'group@g.us',
177
+ sender: 'Carol@s.whatsapp.net',
178
+ sender_name: 'Carol',
179
+ content: 'third',
180
+ timestamp: '2024-01-01T00:00:04.000Z',
181
+ });
182
+ });
183
+
184
+ it('returns messages after the given timestamp', () => {
185
+ const msgs = getMessagesSince(
186
+ 'group@g.us',
187
+ '2024-01-01T00:00:02.000Z',
188
+ 'Andy',
189
+ );
190
+ // Should exclude m1, m2 (before/at timestamp), m3 (bot message)
191
+ expect(msgs).toHaveLength(1);
192
+ expect(msgs[0].content).toBe('third');
193
+ });
194
+
195
+ it('excludes bot messages via is_bot_message flag', () => {
196
+ const msgs = getMessagesSince(
197
+ 'group@g.us',
198
+ '2024-01-01T00:00:00.000Z',
199
+ 'Andy',
200
+ );
201
+ const botMsgs = msgs.filter((m) => m.content === 'bot reply');
202
+ expect(botMsgs).toHaveLength(0);
203
+ });
204
+
205
+ it('returns all non-bot messages when sinceTimestamp is empty', () => {
206
+ const msgs = getMessagesSince('group@g.us', '', 'Andy');
207
+ // 3 user messages (bot message excluded)
208
+ expect(msgs).toHaveLength(3);
209
+ });
210
+
211
+ it('filters pre-migration bot messages via content prefix backstop', () => {
212
+ // Simulate a message written before migration: has prefix but is_bot_message = 0
213
+ store({
214
+ id: 'm5',
215
+ chat_jid: 'group@g.us',
216
+ sender: 'Bot@s.whatsapp.net',
217
+ sender_name: 'Bot',
218
+ content: 'Andy: old bot reply',
219
+ timestamp: '2024-01-01T00:00:05.000Z',
220
+ });
221
+ const msgs = getMessagesSince(
222
+ 'group@g.us',
223
+ '2024-01-01T00:00:04.000Z',
224
+ 'Andy',
225
+ );
226
+ expect(msgs).toHaveLength(0);
227
+ });
228
+ });
229
+
230
+ // --- getNewMessages ---
231
+
232
+ describe('getNewMessages', () => {
233
+ beforeEach(() => {
234
+ storeChatMetadata('group1@g.us', '2024-01-01T00:00:00.000Z');
235
+ storeChatMetadata('group2@g.us', '2024-01-01T00:00:00.000Z');
236
+
237
+ store({
238
+ id: 'a1',
239
+ chat_jid: 'group1@g.us',
240
+ sender: 'user@s.whatsapp.net',
241
+ sender_name: 'User',
242
+ content: 'g1 msg1',
243
+ timestamp: '2024-01-01T00:00:01.000Z',
244
+ });
245
+ store({
246
+ id: 'a2',
247
+ chat_jid: 'group2@g.us',
248
+ sender: 'user@s.whatsapp.net',
249
+ sender_name: 'User',
250
+ content: 'g2 msg1',
251
+ timestamp: '2024-01-01T00:00:02.000Z',
252
+ });
253
+ storeMessage({
254
+ id: 'a3',
255
+ chat_jid: 'group1@g.us',
256
+ sender: 'user@s.whatsapp.net',
257
+ sender_name: 'User',
258
+ content: 'bot reply',
259
+ timestamp: '2024-01-01T00:00:03.000Z',
260
+ is_bot_message: true,
261
+ });
262
+ store({
263
+ id: 'a4',
264
+ chat_jid: 'group1@g.us',
265
+ sender: 'user@s.whatsapp.net',
266
+ sender_name: 'User',
267
+ content: 'g1 msg2',
268
+ timestamp: '2024-01-01T00:00:04.000Z',
269
+ });
270
+ });
271
+
272
+ it('returns new messages across multiple groups', () => {
273
+ const { messages, newTimestamp } = getNewMessages(
274
+ ['group1@g.us', 'group2@g.us'],
275
+ '2024-01-01T00:00:00.000Z',
276
+ 'Andy',
277
+ );
278
+ // Excludes bot message, returns 3 user messages
279
+ expect(messages).toHaveLength(3);
280
+ expect(newTimestamp).toBe('2024-01-01T00:00:04.000Z');
281
+ });
282
+
283
+ it('filters by timestamp', () => {
284
+ const { messages } = getNewMessages(
285
+ ['group1@g.us', 'group2@g.us'],
286
+ '2024-01-01T00:00:02.000Z',
287
+ 'Andy',
288
+ );
289
+ // Only g1 msg2 (after ts, not bot)
290
+ expect(messages).toHaveLength(1);
291
+ expect(messages[0].content).toBe('g1 msg2');
292
+ });
293
+
294
+ it('returns empty for no registered groups', () => {
295
+ const { messages, newTimestamp } = getNewMessages([], '', 'Andy');
296
+ expect(messages).toHaveLength(0);
297
+ expect(newTimestamp).toBe('');
298
+ });
299
+ });
300
+
301
+ // --- storeChatMetadata ---
302
+
303
+ describe('storeChatMetadata', () => {
304
+ it('stores chat with JID as default name', () => {
305
+ storeChatMetadata('group@g.us', '2024-01-01T00:00:00.000Z');
306
+ const chats = getAllChats();
307
+ expect(chats).toHaveLength(1);
308
+ expect(chats[0].jid).toBe('group@g.us');
309
+ expect(chats[0].name).toBe('group@g.us');
310
+ });
311
+
312
+ it('stores chat with explicit name', () => {
313
+ storeChatMetadata('group@g.us', '2024-01-01T00:00:00.000Z', 'My Group');
314
+ const chats = getAllChats();
315
+ expect(chats[0].name).toBe('My Group');
316
+ });
317
+
318
+ it('updates name on subsequent call with name', () => {
319
+ storeChatMetadata('group@g.us', '2024-01-01T00:00:00.000Z');
320
+ storeChatMetadata('group@g.us', '2024-01-01T00:00:01.000Z', 'Updated Name');
321
+ const chats = getAllChats();
322
+ expect(chats).toHaveLength(1);
323
+ expect(chats[0].name).toBe('Updated Name');
324
+ });
325
+
326
+ it('preserves newer timestamp on conflict', () => {
327
+ storeChatMetadata('group@g.us', '2024-01-01T00:00:05.000Z');
328
+ storeChatMetadata('group@g.us', '2024-01-01T00:00:01.000Z');
329
+ const chats = getAllChats();
330
+ expect(chats[0].last_message_time).toBe('2024-01-01T00:00:05.000Z');
331
+ });
332
+ });
333
+
334
+ // --- Task CRUD ---
335
+
336
+ describe('task CRUD', () => {
337
+ it('creates and retrieves a task', () => {
338
+ createTask({
339
+ id: 'task-1',
340
+ group_folder: 'main',
341
+ chat_jid: 'group@g.us',
342
+ prompt: 'do something',
343
+ schedule_type: 'once',
344
+ schedule_value: '2024-06-01T00:00:00.000Z',
345
+ context_mode: 'isolated',
346
+ next_run: '2024-06-01T00:00:00.000Z',
347
+ status: 'active',
348
+ created_at: '2024-01-01T00:00:00.000Z',
349
+ });
350
+
351
+ const task = getTaskById('task-1');
352
+ expect(task).toBeDefined();
353
+ expect(task!.prompt).toBe('do something');
354
+ expect(task!.status).toBe('active');
355
+ });
356
+
357
+ it('updates task status', () => {
358
+ createTask({
359
+ id: 'task-2',
360
+ group_folder: 'main',
361
+ chat_jid: 'group@g.us',
362
+ prompt: 'test',
363
+ schedule_type: 'once',
364
+ schedule_value: '2024-06-01T00:00:00.000Z',
365
+ context_mode: 'isolated',
366
+ next_run: null,
367
+ status: 'active',
368
+ created_at: '2024-01-01T00:00:00.000Z',
369
+ });
370
+
371
+ updateTask('task-2', { status: 'paused' });
372
+ expect(getTaskById('task-2')!.status).toBe('paused');
373
+ });
374
+
375
+ it('deletes a task and its run logs', () => {
376
+ createTask({
377
+ id: 'task-3',
378
+ group_folder: 'main',
379
+ chat_jid: 'group@g.us',
380
+ prompt: 'delete me',
381
+ schedule_type: 'once',
382
+ schedule_value: '2024-06-01T00:00:00.000Z',
383
+ context_mode: 'isolated',
384
+ next_run: null,
385
+ status: 'active',
386
+ created_at: '2024-01-01T00:00:00.000Z',
387
+ });
388
+
389
+ deleteTask('task-3');
390
+ expect(getTaskById('task-3')).toBeUndefined();
391
+ });
392
+ });
393
+
394
+ // --- LIMIT behavior ---
395
+
396
+ describe('message query LIMIT', () => {
397
+ beforeEach(() => {
398
+ storeChatMetadata('group@g.us', '2024-01-01T00:00:00.000Z');
399
+
400
+ for (let i = 1; i <= 10; i++) {
401
+ store({
402
+ id: `lim-${i}`,
403
+ chat_jid: 'group@g.us',
404
+ sender: 'user@s.whatsapp.net',
405
+ sender_name: 'User',
406
+ content: `message ${i}`,
407
+ timestamp: `2024-01-01T00:00:${String(i).padStart(2, '0')}.000Z`,
408
+ });
409
+ }
410
+ });
411
+
412
+ it('getNewMessages caps to limit and returns most recent in chronological order', () => {
413
+ const { messages, newTimestamp } = getNewMessages(
414
+ ['group@g.us'],
415
+ '2024-01-01T00:00:00.000Z',
416
+ 'Andy',
417
+ 3,
418
+ );
419
+ expect(messages).toHaveLength(3);
420
+ expect(messages[0].content).toBe('message 8');
421
+ expect(messages[2].content).toBe('message 10');
422
+ // Chronological order preserved
423
+ expect(messages[1].timestamp > messages[0].timestamp).toBe(true);
424
+ // newTimestamp reflects latest returned row
425
+ expect(newTimestamp).toBe('2024-01-01T00:00:10.000Z');
426
+ });
427
+
428
+ it('getMessagesSince caps to limit and returns most recent in chronological order', () => {
429
+ const messages = getMessagesSince(
430
+ 'group@g.us',
431
+ '2024-01-01T00:00:00.000Z',
432
+ 'Andy',
433
+ 3,
434
+ );
435
+ expect(messages).toHaveLength(3);
436
+ expect(messages[0].content).toBe('message 8');
437
+ expect(messages[2].content).toBe('message 10');
438
+ expect(messages[1].timestamp > messages[0].timestamp).toBe(true);
439
+ });
440
+
441
+ it('returns all messages when count is under the limit', () => {
442
+ const { messages } = getNewMessages(
443
+ ['group@g.us'],
444
+ '2024-01-01T00:00:00.000Z',
445
+ 'Andy',
446
+ 50,
447
+ );
448
+ expect(messages).toHaveLength(10);
449
+ });
450
+ });
451
+
452
+ // --- RegisteredGroup isMain round-trip ---
453
+
454
+ describe('registered group isMain', () => {
455
+ it('persists isMain=true through set/get round-trip', () => {
456
+ setRegisteredGroup('main@s.whatsapp.net', {
457
+ name: 'Main Chat',
458
+ folder: 'whatsapp_main',
459
+ trigger: '@Andy',
460
+ added_at: '2024-01-01T00:00:00.000Z',
461
+ isMain: true,
462
+ });
463
+
464
+ const groups = getAllRegisteredGroups();
465
+ const group = groups['main@s.whatsapp.net'];
466
+ expect(group).toBeDefined();
467
+ expect(group.isMain).toBe(true);
468
+ expect(group.folder).toBe('whatsapp_main');
469
+ });
470
+
471
+ it('omits isMain for non-main groups', () => {
472
+ setRegisteredGroup('group@g.us', {
473
+ name: 'Family Chat',
474
+ folder: 'whatsapp_family-chat',
475
+ trigger: '@Andy',
476
+ added_at: '2024-01-01T00:00:00.000Z',
477
+ });
478
+
479
+ const groups = getAllRegisteredGroups();
480
+ const group = groups['group@g.us'];
481
+ expect(group).toBeDefined();
482
+ expect(group.isMain).toBeUndefined();
483
+ });
484
+ });