@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/dist/db.d.ts ADDED
@@ -0,0 +1,115 @@
1
+ import { NewMessage, RegisteredGroup, ScheduledTask, TaskRunLog } from './types.js';
2
+ export declare function initDatabase(): void;
3
+ /** @internal - for tests only. Creates a fresh in-memory database. */
4
+ export declare function _initTestDatabase(): void;
5
+ /**
6
+ * Store chat metadata only (no message content).
7
+ * Used for all chats to enable group discovery without storing sensitive content.
8
+ */
9
+ export declare function storeChatMetadata(chatJid: string, timestamp: string, name?: string, channel?: string, isGroup?: boolean): void;
10
+ /**
11
+ * Update chat name, guarded by a timestamp comparison.
12
+ * The name is only written if nameUpdatedAt is >= the currently stored name_updated_at,
13
+ * so that an older push from another client can never overwrite a newer rename.
14
+ * New chats (INSERT path) always get the supplied values.
15
+ */
16
+ export declare function updateChatName(chatJid: string, name: string, nameUpdatedAt?: number): void;
17
+ export interface ChatInfo {
18
+ jid: string;
19
+ name: string;
20
+ last_message_time: string;
21
+ channel: string;
22
+ is_group: number;
23
+ name_updated_at: number;
24
+ cwd: string;
25
+ }
26
+ /**
27
+ * Get all known chats, ordered by most recent activity.
28
+ */
29
+ export declare function getAllChats(): ChatInfo[];
30
+ /**
31
+ * Get the user-defined display order for web sessions.
32
+ * Returns an array of chat JIDs (local@web-<id>) in the desired order.
33
+ * An empty array means no custom order has been saved yet.
34
+ */
35
+ export declare function getWebSessionOrder(): string[];
36
+ /**
37
+ * Persist the user-defined display order for web sessions.
38
+ * @param jids - Ordered array of chat JIDs (local@web-<id>)
39
+ */
40
+ export declare function setWebSessionOrder(jids: string[]): void;
41
+ /**
42
+ * Persist the current working directory for a web session.
43
+ */
44
+ export declare function updateChatCwd(chatJid: string, cwd: string): void;
45
+ /**
46
+ * Get full conversation history for a chat (both user and bot messages),
47
+ * ordered chronologically. Used by the web channel history endpoint.
48
+ */
49
+ export declare function getConversation(chatJid: string, limit?: number): NewMessage[];
50
+ /**
51
+ * Delete a chat and all its messages from the database.
52
+ * Used by the web channel when a session is deleted.
53
+ */
54
+ export declare function deleteChat(chatJid: string): void;
55
+ /**
56
+ * Delete only the messages of a chat, keeping the chat entry itself.
57
+ * Used by the web channel /clear command.
58
+ */
59
+ export declare function clearChatMessages(chatJid: string): void;
60
+ /**
61
+ * Get timestamp of last group metadata sync.
62
+ */
63
+ export declare function getLastGroupSync(): string | null;
64
+ /**
65
+ * Record that group metadata was synced.
66
+ */
67
+ export declare function setLastGroupSync(): void;
68
+ /**
69
+ * Store a message with full content.
70
+ * Only call this for registered groups where message history is needed.
71
+ */
72
+ export declare function storeMessage(msg: NewMessage): void;
73
+ /**
74
+ * Store a message directly.
75
+ */
76
+ export declare function storeMessageDirect(msg: {
77
+ id: string;
78
+ chat_jid: string;
79
+ sender: string;
80
+ sender_name: string;
81
+ content: string;
82
+ timestamp: string;
83
+ is_from_me: boolean;
84
+ is_bot_message?: boolean;
85
+ }): void;
86
+ export declare function getNewMessages(jids: string[], lastTimestamp: string, botPrefix: string, limit?: number): {
87
+ messages: NewMessage[];
88
+ newTimestamp: string;
89
+ };
90
+ export declare function getMessagesSince(chatJid: string, sinceTimestamp: string, botPrefix: string, limit?: number): NewMessage[];
91
+ /**
92
+ * Delete a single message by its ID.
93
+ * Used by the web channel when the user clicks the trash icon on a message.
94
+ */
95
+ export declare function deleteMessage(id: string): void;
96
+ export declare function createTask(task: Omit<ScheduledTask, 'last_run' | 'last_result'>): void;
97
+ export declare function getTaskById(id: string): ScheduledTask | undefined;
98
+ export declare function getTasksForGroup(groupFolder: string): ScheduledTask[];
99
+ export declare function getAllTasks(): ScheduledTask[];
100
+ export declare function updateTask(id: string, updates: Partial<Pick<ScheduledTask, 'prompt' | 'schedule_type' | 'schedule_value' | 'next_run' | 'status'>>): void;
101
+ export declare function deleteTask(id: string): void;
102
+ export declare function getDueTasks(): ScheduledTask[];
103
+ export declare function updateTaskAfterRun(id: string, nextRun: string | null, lastResult: string): void;
104
+ export declare function logTaskRun(log: TaskRunLog): void;
105
+ export declare function getRouterState(key: string): string | undefined;
106
+ export declare function setRouterState(key: string, value: string): void;
107
+ export declare function getSession(groupFolder: string): string | undefined;
108
+ export declare function setSession(groupFolder: string, sessionId: string): void;
109
+ export declare function getAllSessions(): Record<string, string>;
110
+ export declare function getRegisteredGroup(jid: string): (RegisteredGroup & {
111
+ jid: string;
112
+ }) | undefined;
113
+ export declare function setRegisteredGroup(jid: string, group: RegisteredGroup): void;
114
+ export declare function getAllRegisteredGroups(): Record<string, RegisteredGroup>;
115
+ //# sourceMappingURL=db.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../src/db.ts"],"names":[],"mappings":"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"}
package/dist/db.js ADDED
@@ -0,0 +1,549 @@
1
+ import Database from 'better-sqlite3';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { ASSISTANT_NAME, DATA_DIR, STORE_DIR } from './config.js';
5
+ import { isValidGroupFolder } from './group-folder.js';
6
+ import { logger } from './logger.js';
7
+ let db;
8
+ function createSchema(database) {
9
+ database.exec(`
10
+ CREATE TABLE IF NOT EXISTS chats (
11
+ jid TEXT PRIMARY KEY,
12
+ name TEXT,
13
+ last_message_time TEXT,
14
+ channel TEXT,
15
+ is_group INTEGER DEFAULT 0
16
+ );
17
+ CREATE TABLE IF NOT EXISTS messages (
18
+ id TEXT,
19
+ chat_jid TEXT,
20
+ sender TEXT,
21
+ sender_name TEXT,
22
+ content TEXT,
23
+ timestamp TEXT,
24
+ is_from_me INTEGER,
25
+ is_bot_message INTEGER DEFAULT 0,
26
+ PRIMARY KEY (id, chat_jid),
27
+ FOREIGN KEY (chat_jid) REFERENCES chats(jid)
28
+ );
29
+ CREATE INDEX IF NOT EXISTS idx_timestamp ON messages(timestamp);
30
+
31
+ CREATE TABLE IF NOT EXISTS scheduled_tasks (
32
+ id TEXT PRIMARY KEY,
33
+ group_folder TEXT NOT NULL,
34
+ chat_jid TEXT NOT NULL,
35
+ prompt TEXT NOT NULL,
36
+ schedule_type TEXT NOT NULL,
37
+ schedule_value TEXT NOT NULL,
38
+ next_run TEXT,
39
+ last_run TEXT,
40
+ last_result TEXT,
41
+ status TEXT DEFAULT 'active',
42
+ created_at TEXT NOT NULL
43
+ );
44
+ CREATE INDEX IF NOT EXISTS idx_next_run ON scheduled_tasks(next_run);
45
+ CREATE INDEX IF NOT EXISTS idx_status ON scheduled_tasks(status);
46
+
47
+ CREATE TABLE IF NOT EXISTS task_run_logs (
48
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
49
+ task_id TEXT NOT NULL,
50
+ run_at TEXT NOT NULL,
51
+ duration_ms INTEGER NOT NULL,
52
+ status TEXT NOT NULL,
53
+ result TEXT,
54
+ error TEXT,
55
+ FOREIGN KEY (task_id) REFERENCES scheduled_tasks(id)
56
+ );
57
+ CREATE INDEX IF NOT EXISTS idx_task_run_logs ON task_run_logs(task_id, run_at);
58
+
59
+ CREATE TABLE IF NOT EXISTS router_state (
60
+ key TEXT PRIMARY KEY,
61
+ value TEXT NOT NULL
62
+ );
63
+ CREATE TABLE IF NOT EXISTS sessions (
64
+ group_folder TEXT PRIMARY KEY,
65
+ session_id TEXT NOT NULL
66
+ );
67
+ CREATE TABLE IF NOT EXISTS registered_groups (
68
+ jid TEXT PRIMARY KEY,
69
+ name TEXT NOT NULL,
70
+ folder TEXT NOT NULL UNIQUE,
71
+ trigger_pattern TEXT NOT NULL,
72
+ added_at TEXT NOT NULL,
73
+ container_config TEXT,
74
+ requires_trigger INTEGER DEFAULT 1
75
+ );
76
+ `);
77
+ // Add context_mode column if it doesn't exist (migration for existing DBs)
78
+ try {
79
+ database.exec(`ALTER TABLE scheduled_tasks ADD COLUMN context_mode TEXT DEFAULT 'isolated'`);
80
+ }
81
+ catch {
82
+ /* column already exists */
83
+ }
84
+ // Add is_bot_message column if it doesn't exist (migration for existing DBs)
85
+ try {
86
+ database.exec(`ALTER TABLE messages ADD COLUMN is_bot_message INTEGER DEFAULT 0`);
87
+ // Backfill: mark existing bot messages that used the content prefix pattern
88
+ database
89
+ .prepare(`UPDATE messages SET is_bot_message = 1 WHERE content LIKE ?`)
90
+ .run(`${ASSISTANT_NAME}:%`);
91
+ }
92
+ catch {
93
+ /* column already exists */
94
+ }
95
+ // Add is_main column if it doesn't exist (migration for existing DBs)
96
+ try {
97
+ database.exec(`ALTER TABLE registered_groups ADD COLUMN is_main INTEGER DEFAULT 0`);
98
+ // Backfill: existing rows with folder = 'main' are the main group
99
+ database.exec(`UPDATE registered_groups SET is_main = 1 WHERE folder = 'main'`);
100
+ }
101
+ catch {
102
+ /* column already exists */
103
+ }
104
+ // Add name_updated_at column if it doesn't exist (migration for existing DBs)
105
+ try {
106
+ database.exec(`ALTER TABLE chats ADD COLUMN name_updated_at INTEGER DEFAULT 0`);
107
+ }
108
+ catch {
109
+ /* column already exists */
110
+ }
111
+ // Add cwd column if it doesn't exist (migration for existing DBs)
112
+ try {
113
+ database.exec(`ALTER TABLE chats ADD COLUMN cwd TEXT DEFAULT ''`);
114
+ }
115
+ catch {
116
+ /* column already exists */
117
+ }
118
+ // Add channel and is_group columns if they don't exist (migration for existing DBs)
119
+ try {
120
+ database.exec(`ALTER TABLE chats ADD COLUMN channel TEXT`);
121
+ database.exec(`ALTER TABLE chats ADD COLUMN is_group INTEGER DEFAULT 0`);
122
+ // Backfill from JID patterns
123
+ database.exec(`UPDATE chats SET channel = 'whatsapp', is_group = 1 WHERE jid LIKE '%@g.us'`);
124
+ database.exec(`UPDATE chats SET channel = 'whatsapp', is_group = 0 WHERE jid LIKE '%@s.whatsapp.net'`);
125
+ database.exec(`UPDATE chats SET channel = 'discord', is_group = 1 WHERE jid LIKE 'dc:%'`);
126
+ database.exec(`UPDATE chats SET channel = 'telegram', is_group = 1 WHERE jid LIKE 'tg:%'`);
127
+ }
128
+ catch {
129
+ /* columns already exist */
130
+ }
131
+ }
132
+ export function initDatabase() {
133
+ const dbPath = path.join(STORE_DIR, 'messages.db');
134
+ fs.mkdirSync(path.dirname(dbPath), { recursive: true });
135
+ db = new Database(dbPath);
136
+ createSchema(db);
137
+ // Migrate from JSON files if they exist
138
+ migrateJsonState();
139
+ }
140
+ /** @internal - for tests only. Creates a fresh in-memory database. */
141
+ export function _initTestDatabase() {
142
+ db = new Database(':memory:');
143
+ createSchema(db);
144
+ }
145
+ /**
146
+ * Store chat metadata only (no message content).
147
+ * Used for all chats to enable group discovery without storing sensitive content.
148
+ */
149
+ export function storeChatMetadata(chatJid, timestamp, name, channel, isGroup) {
150
+ const ch = channel ?? null;
151
+ const group = isGroup === undefined ? null : isGroup ? 1 : 0;
152
+ if (name) {
153
+ // Update with name, preserving existing timestamp if newer
154
+ db.prepare(`
155
+ INSERT INTO chats (jid, name, last_message_time, channel, is_group) VALUES (?, ?, ?, ?, ?)
156
+ ON CONFLICT(jid) DO UPDATE SET
157
+ name = excluded.name,
158
+ last_message_time = MAX(last_message_time, excluded.last_message_time),
159
+ channel = COALESCE(excluded.channel, channel),
160
+ is_group = COALESCE(excluded.is_group, is_group)
161
+ `).run(chatJid, name, timestamp, ch, group);
162
+ }
163
+ else {
164
+ // Update timestamp only, preserve existing name if any
165
+ db.prepare(`
166
+ INSERT INTO chats (jid, name, last_message_time, channel, is_group) VALUES (?, ?, ?, ?, ?)
167
+ ON CONFLICT(jid) DO UPDATE SET
168
+ last_message_time = MAX(last_message_time, excluded.last_message_time),
169
+ channel = COALESCE(excluded.channel, channel),
170
+ is_group = COALESCE(excluded.is_group, is_group)
171
+ `).run(chatJid, chatJid, timestamp, ch, group);
172
+ }
173
+ }
174
+ /**
175
+ * Update chat name, guarded by a timestamp comparison.
176
+ * The name is only written if nameUpdatedAt is >= the currently stored name_updated_at,
177
+ * so that an older push from another client can never overwrite a newer rename.
178
+ * New chats (INSERT path) always get the supplied values.
179
+ */
180
+ export function updateChatName(chatJid, name, nameUpdatedAt = Date.now()) {
181
+ db.prepare(`
182
+ INSERT INTO chats (jid, name, last_message_time, name_updated_at) VALUES (?, ?, ?, ?)
183
+ ON CONFLICT(jid) DO UPDATE SET
184
+ name = excluded.name,
185
+ name_updated_at = excluded.name_updated_at
186
+ WHERE excluded.name_updated_at >= chats.name_updated_at
187
+ `).run(chatJid, name, new Date().toISOString(), nameUpdatedAt);
188
+ }
189
+ /**
190
+ * Get all known chats, ordered by most recent activity.
191
+ */
192
+ export function getAllChats() {
193
+ return db
194
+ .prepare(`
195
+ SELECT jid, name, last_message_time, channel, is_group,
196
+ COALESCE(name_updated_at, 0) AS name_updated_at,
197
+ COALESCE(cwd, '') AS cwd
198
+ FROM chats
199
+ ORDER BY last_message_time DESC
200
+ `)
201
+ .all();
202
+ }
203
+ /**
204
+ * Get the user-defined display order for web sessions.
205
+ * Returns an array of chat JIDs (local@web-<id>) in the desired order.
206
+ * An empty array means no custom order has been saved yet.
207
+ */
208
+ export function getWebSessionOrder() {
209
+ const row = db
210
+ .prepare(`SELECT value FROM router_state WHERE key = 'web_session_order'`)
211
+ .get();
212
+ try {
213
+ return row ? JSON.parse(row.value) : [];
214
+ }
215
+ catch {
216
+ return [];
217
+ }
218
+ }
219
+ /**
220
+ * Persist the user-defined display order for web sessions.
221
+ * @param jids - Ordered array of chat JIDs (local@web-<id>)
222
+ */
223
+ export function setWebSessionOrder(jids) {
224
+ db.prepare(`INSERT OR REPLACE INTO router_state (key, value) VALUES ('web_session_order', ?)`).run(JSON.stringify(jids));
225
+ }
226
+ /**
227
+ * Persist the current working directory for a web session.
228
+ */
229
+ export function updateChatCwd(chatJid, cwd) {
230
+ db.prepare(`UPDATE chats SET cwd = ? WHERE jid = ?`).run(cwd, chatJid);
231
+ }
232
+ /**
233
+ * Get full conversation history for a chat (both user and bot messages),
234
+ * ordered chronologically. Used by the web channel history endpoint.
235
+ */
236
+ export function getConversation(chatJid, limit = 500) {
237
+ return db
238
+ .prepare(`SELECT id, chat_jid, sender, sender_name, content, timestamp,
239
+ is_from_me, is_bot_message
240
+ FROM messages
241
+ WHERE chat_jid = ? AND content != '' AND content IS NOT NULL
242
+ ORDER BY timestamp ASC
243
+ LIMIT ?`)
244
+ .all(chatJid, limit);
245
+ }
246
+ /**
247
+ * Delete a chat and all its messages from the database.
248
+ * Used by the web channel when a session is deleted.
249
+ */
250
+ export function deleteChat(chatJid) {
251
+ db.prepare(`DELETE FROM messages WHERE chat_jid = ?`).run(chatJid);
252
+ db.prepare(`DELETE FROM chats WHERE jid = ?`).run(chatJid);
253
+ }
254
+ /**
255
+ * Delete only the messages of a chat, keeping the chat entry itself.
256
+ * Used by the web channel /clear command.
257
+ */
258
+ export function clearChatMessages(chatJid) {
259
+ db.prepare(`DELETE FROM messages WHERE chat_jid = ?`).run(chatJid);
260
+ }
261
+ /**
262
+ * Get timestamp of last group metadata sync.
263
+ */
264
+ export function getLastGroupSync() {
265
+ // Store sync time in a special chat entry
266
+ const row = db
267
+ .prepare(`SELECT last_message_time FROM chats WHERE jid = '__group_sync__'`)
268
+ .get();
269
+ return row?.last_message_time || null;
270
+ }
271
+ /**
272
+ * Record that group metadata was synced.
273
+ */
274
+ export function setLastGroupSync() {
275
+ const now = new Date().toISOString();
276
+ db.prepare(`INSERT OR REPLACE INTO chats (jid, name, last_message_time) VALUES ('__group_sync__', '__group_sync__', ?)`).run(now);
277
+ }
278
+ /**
279
+ * Store a message with full content.
280
+ * Only call this for registered groups where message history is needed.
281
+ */
282
+ export function storeMessage(msg) {
283
+ db.prepare(`INSERT OR REPLACE INTO messages (id, chat_jid, sender, sender_name, content, timestamp, is_from_me, is_bot_message) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(msg.id, msg.chat_jid, msg.sender, msg.sender_name, msg.content, msg.timestamp, msg.is_from_me ? 1 : 0, msg.is_bot_message ? 1 : 0);
284
+ }
285
+ /**
286
+ * Store a message directly.
287
+ */
288
+ export function storeMessageDirect(msg) {
289
+ db.prepare(`INSERT OR REPLACE INTO messages (id, chat_jid, sender, sender_name, content, timestamp, is_from_me, is_bot_message) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(msg.id, msg.chat_jid, msg.sender, msg.sender_name, msg.content, msg.timestamp, msg.is_from_me ? 1 : 0, msg.is_bot_message ? 1 : 0);
290
+ }
291
+ export function getNewMessages(jids, lastTimestamp, botPrefix, limit = 200) {
292
+ if (jids.length === 0)
293
+ return { messages: [], newTimestamp: lastTimestamp };
294
+ const placeholders = jids.map(() => '?').join(',');
295
+ // Filter bot messages using both the is_bot_message flag AND the content
296
+ // prefix as a backstop for messages written before the migration ran.
297
+ // Subquery takes the N most recent, outer query re-sorts chronologically.
298
+ const sql = `
299
+ SELECT * FROM (
300
+ SELECT id, chat_jid, sender, sender_name, content, timestamp, is_from_me
301
+ FROM messages
302
+ WHERE timestamp > ? AND chat_jid IN (${placeholders})
303
+ AND is_bot_message = 0 AND content NOT LIKE ?
304
+ AND content != '' AND content IS NOT NULL
305
+ ORDER BY timestamp DESC
306
+ LIMIT ?
307
+ ) ORDER BY timestamp
308
+ `;
309
+ const rows = db
310
+ .prepare(sql)
311
+ .all(lastTimestamp, ...jids, `${botPrefix}:%`, limit);
312
+ let newTimestamp = lastTimestamp;
313
+ for (const row of rows) {
314
+ if (row.timestamp > newTimestamp)
315
+ newTimestamp = row.timestamp;
316
+ }
317
+ return { messages: rows, newTimestamp };
318
+ }
319
+ export function getMessagesSince(chatJid, sinceTimestamp, botPrefix, limit = 200) {
320
+ // Filter bot messages using both the is_bot_message flag AND the content
321
+ // prefix as a backstop for messages written before the migration ran.
322
+ // Subquery takes the N most recent, outer query re-sorts chronologically.
323
+ const sql = `
324
+ SELECT * FROM (
325
+ SELECT id, chat_jid, sender, sender_name, content, timestamp, is_from_me
326
+ FROM messages
327
+ WHERE chat_jid = ? AND timestamp > ?
328
+ AND is_bot_message = 0 AND content NOT LIKE ?
329
+ AND content != '' AND content IS NOT NULL
330
+ ORDER BY timestamp DESC
331
+ LIMIT ?
332
+ ) ORDER BY timestamp
333
+ `;
334
+ return db
335
+ .prepare(sql)
336
+ .all(chatJid, sinceTimestamp, `${botPrefix}:%`, limit);
337
+ }
338
+ /**
339
+ * Delete a single message by its ID.
340
+ * Used by the web channel when the user clicks the trash icon on a message.
341
+ */
342
+ export function deleteMessage(id) {
343
+ db.prepare(`DELETE FROM messages WHERE id = ?`).run(id);
344
+ }
345
+ export function createTask(task) {
346
+ db.prepare(`
347
+ INSERT INTO scheduled_tasks (id, group_folder, chat_jid, prompt, schedule_type, schedule_value, context_mode, next_run, status, created_at)
348
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
349
+ `).run(task.id, task.group_folder, task.chat_jid, task.prompt, task.schedule_type, task.schedule_value, task.context_mode || 'isolated', task.next_run, task.status, task.created_at);
350
+ }
351
+ export function getTaskById(id) {
352
+ return db.prepare('SELECT * FROM scheduled_tasks WHERE id = ?').get(id);
353
+ }
354
+ export function getTasksForGroup(groupFolder) {
355
+ return db
356
+ .prepare('SELECT * FROM scheduled_tasks WHERE group_folder = ? ORDER BY created_at DESC')
357
+ .all(groupFolder);
358
+ }
359
+ export function getAllTasks() {
360
+ return db
361
+ .prepare('SELECT * FROM scheduled_tasks ORDER BY created_at DESC')
362
+ .all();
363
+ }
364
+ export function updateTask(id, updates) {
365
+ const fields = [];
366
+ const values = [];
367
+ if (updates.prompt !== undefined) {
368
+ fields.push('prompt = ?');
369
+ values.push(updates.prompt);
370
+ }
371
+ if (updates.schedule_type !== undefined) {
372
+ fields.push('schedule_type = ?');
373
+ values.push(updates.schedule_type);
374
+ }
375
+ if (updates.schedule_value !== undefined) {
376
+ fields.push('schedule_value = ?');
377
+ values.push(updates.schedule_value);
378
+ }
379
+ if (updates.next_run !== undefined) {
380
+ fields.push('next_run = ?');
381
+ values.push(updates.next_run);
382
+ }
383
+ if (updates.status !== undefined) {
384
+ fields.push('status = ?');
385
+ values.push(updates.status);
386
+ }
387
+ if (fields.length === 0)
388
+ return;
389
+ values.push(id);
390
+ db.prepare(`UPDATE scheduled_tasks SET ${fields.join(', ')} WHERE id = ?`).run(...values);
391
+ }
392
+ export function deleteTask(id) {
393
+ // Delete child records first (FK constraint)
394
+ db.prepare('DELETE FROM task_run_logs WHERE task_id = ?').run(id);
395
+ db.prepare('DELETE FROM scheduled_tasks WHERE id = ?').run(id);
396
+ }
397
+ export function getDueTasks() {
398
+ const now = new Date().toISOString();
399
+ return db
400
+ .prepare(`
401
+ SELECT * FROM scheduled_tasks
402
+ WHERE status = 'active' AND next_run IS NOT NULL AND next_run <= ?
403
+ ORDER BY next_run
404
+ `)
405
+ .all(now);
406
+ }
407
+ export function updateTaskAfterRun(id, nextRun, lastResult) {
408
+ const now = new Date().toISOString();
409
+ db.prepare(`
410
+ UPDATE scheduled_tasks
411
+ SET next_run = ?, last_run = ?, last_result = ?, status = CASE WHEN ? IS NULL THEN 'completed' ELSE status END
412
+ WHERE id = ?
413
+ `).run(nextRun, now, lastResult, nextRun, id);
414
+ }
415
+ export function logTaskRun(log) {
416
+ db.prepare(`
417
+ INSERT INTO task_run_logs (task_id, run_at, duration_ms, status, result, error)
418
+ VALUES (?, ?, ?, ?, ?, ?)
419
+ `).run(log.task_id, log.run_at, log.duration_ms, log.status, log.result, log.error);
420
+ }
421
+ // --- Router state accessors ---
422
+ export function getRouterState(key) {
423
+ const row = db
424
+ .prepare('SELECT value FROM router_state WHERE key = ?')
425
+ .get(key);
426
+ return row?.value;
427
+ }
428
+ export function setRouterState(key, value) {
429
+ db.prepare('INSERT OR REPLACE INTO router_state (key, value) VALUES (?, ?)').run(key, value);
430
+ }
431
+ // --- Session accessors ---
432
+ export function getSession(groupFolder) {
433
+ const row = db
434
+ .prepare('SELECT session_id FROM sessions WHERE group_folder = ?')
435
+ .get(groupFolder);
436
+ return row?.session_id;
437
+ }
438
+ export function setSession(groupFolder, sessionId) {
439
+ db.prepare('INSERT OR REPLACE INTO sessions (group_folder, session_id) VALUES (?, ?)').run(groupFolder, sessionId);
440
+ }
441
+ export function getAllSessions() {
442
+ const rows = db
443
+ .prepare('SELECT group_folder, session_id FROM sessions')
444
+ .all();
445
+ const result = {};
446
+ for (const row of rows) {
447
+ result[row.group_folder] = row.session_id;
448
+ }
449
+ return result;
450
+ }
451
+ // --- Registered group accessors ---
452
+ export function getRegisteredGroup(jid) {
453
+ const row = db
454
+ .prepare('SELECT * FROM registered_groups WHERE jid = ?')
455
+ .get(jid);
456
+ if (!row)
457
+ return undefined;
458
+ if (!isValidGroupFolder(row.folder)) {
459
+ logger.warn({ jid: row.jid, folder: row.folder }, 'Skipping registered group with invalid folder');
460
+ return undefined;
461
+ }
462
+ return {
463
+ jid: row.jid,
464
+ name: row.name,
465
+ folder: row.folder,
466
+ trigger: row.trigger_pattern,
467
+ added_at: row.added_at,
468
+ containerConfig: row.container_config
469
+ ? JSON.parse(row.container_config)
470
+ : undefined,
471
+ requiresTrigger: row.requires_trigger === null ? undefined : row.requires_trigger === 1,
472
+ isMain: row.is_main === 1 ? true : undefined,
473
+ };
474
+ }
475
+ export function setRegisteredGroup(jid, group) {
476
+ if (!isValidGroupFolder(group.folder)) {
477
+ throw new Error(`Invalid group folder "${group.folder}" for JID ${jid}`);
478
+ }
479
+ db.prepare(`INSERT OR REPLACE INTO registered_groups (jid, name, folder, trigger_pattern, added_at, container_config, requires_trigger, is_main)
480
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(jid, group.name, group.folder, group.trigger, group.added_at, group.containerConfig ? JSON.stringify(group.containerConfig) : null, group.requiresTrigger === undefined ? 1 : group.requiresTrigger ? 1 : 0, group.isMain ? 1 : 0);
481
+ }
482
+ export function getAllRegisteredGroups() {
483
+ const rows = db.prepare('SELECT * FROM registered_groups').all();
484
+ const result = {};
485
+ for (const row of rows) {
486
+ if (!isValidGroupFolder(row.folder)) {
487
+ logger.warn({ jid: row.jid, folder: row.folder }, 'Skipping registered group with invalid folder');
488
+ continue;
489
+ }
490
+ result[row.jid] = {
491
+ name: row.name,
492
+ folder: row.folder,
493
+ trigger: row.trigger_pattern,
494
+ added_at: row.added_at,
495
+ containerConfig: row.container_config
496
+ ? JSON.parse(row.container_config)
497
+ : undefined,
498
+ requiresTrigger: row.requires_trigger === null ? undefined : row.requires_trigger === 1,
499
+ isMain: row.is_main === 1 ? true : undefined,
500
+ };
501
+ }
502
+ return result;
503
+ }
504
+ // --- JSON migration ---
505
+ function migrateJsonState() {
506
+ const migrateFile = (filename) => {
507
+ const filePath = path.join(DATA_DIR, filename);
508
+ if (!fs.existsSync(filePath))
509
+ return null;
510
+ try {
511
+ const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
512
+ fs.renameSync(filePath, `${filePath}.migrated`);
513
+ return data;
514
+ }
515
+ catch {
516
+ return null;
517
+ }
518
+ };
519
+ // Migrate router_state.json
520
+ const routerState = migrateFile('router_state.json');
521
+ if (routerState) {
522
+ if (routerState.last_timestamp) {
523
+ setRouterState('last_timestamp', routerState.last_timestamp);
524
+ }
525
+ if (routerState.last_agent_timestamp) {
526
+ setRouterState('last_agent_timestamp', JSON.stringify(routerState.last_agent_timestamp));
527
+ }
528
+ }
529
+ // Migrate sessions.json
530
+ const sessions = migrateFile('sessions.json');
531
+ if (sessions) {
532
+ for (const [folder, sessionId] of Object.entries(sessions)) {
533
+ setSession(folder, sessionId);
534
+ }
535
+ }
536
+ // Migrate registered_groups.json
537
+ const groups = migrateFile('registered_groups.json');
538
+ if (groups) {
539
+ for (const [jid, group] of Object.entries(groups)) {
540
+ try {
541
+ setRegisteredGroup(jid, group);
542
+ }
543
+ catch (err) {
544
+ logger.warn({ jid, folder: group.folder, err }, 'Skipping migrated registered group with invalid folder');
545
+ }
546
+ }
547
+ }
548
+ }
549
+ //# sourceMappingURL=db.js.map