@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/ipc.ts ADDED
@@ -0,0 +1,461 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+
4
+ import { CronExpressionParser } from 'cron-parser';
5
+
6
+ import { DATA_DIR, IPC_POLL_INTERVAL, TIMEZONE } from './config.js';
7
+ import { AvailableGroup } from './container-runner.js';
8
+ import { createTask, deleteTask, getTaskById, updateTask } from './db.js';
9
+ import { isValidGroupFolder } from './group-folder.js';
10
+ import { logger } from './logger.js';
11
+ import { RegisteredGroup } from './types.js';
12
+
13
+ export interface IpcDeps {
14
+ sendMessage: (jid: string, text: string) => Promise<void>;
15
+ registeredGroups: () => Record<string, RegisteredGroup>;
16
+ registerGroup: (jid: string, group: RegisteredGroup) => void;
17
+ syncGroups: (force: boolean) => Promise<void>;
18
+ getAvailableGroups: () => AvailableGroup[];
19
+ writeGroupsSnapshot: (
20
+ groupFolder: string,
21
+ isMain: boolean,
22
+ availableGroups: AvailableGroup[],
23
+ registeredJids: Set<string>,
24
+ ) => void;
25
+ onTasksChanged: () => void;
26
+ }
27
+
28
+ let ipcWatcherRunning = false;
29
+
30
+ export function startIpcWatcher(deps: IpcDeps): void {
31
+ if (ipcWatcherRunning) {
32
+ logger.debug('IPC watcher already running, skipping duplicate start');
33
+ return;
34
+ }
35
+ ipcWatcherRunning = true;
36
+
37
+ const ipcBaseDir = path.join(DATA_DIR, 'ipc');
38
+ fs.mkdirSync(ipcBaseDir, { recursive: true });
39
+
40
+ const processIpcFiles = async () => {
41
+ // Scan all group IPC directories (identity determined by directory)
42
+ let groupFolders: string[];
43
+ try {
44
+ groupFolders = fs.readdirSync(ipcBaseDir).filter((f) => {
45
+ const stat = fs.statSync(path.join(ipcBaseDir, f));
46
+ return stat.isDirectory() && f !== 'errors';
47
+ });
48
+ } catch (err) {
49
+ logger.error({ err }, 'Error reading IPC base directory');
50
+ setTimeout(processIpcFiles, IPC_POLL_INTERVAL);
51
+ return;
52
+ }
53
+
54
+ const registeredGroups = deps.registeredGroups();
55
+
56
+ // Build folder→isMain lookup from registered groups
57
+ const folderIsMain = new Map<string, boolean>();
58
+ for (const group of Object.values(registeredGroups)) {
59
+ if (group.isMain) folderIsMain.set(group.folder, true);
60
+ }
61
+
62
+ for (const sourceGroup of groupFolders) {
63
+ const isMain = folderIsMain.get(sourceGroup) === true;
64
+ const messagesDir = path.join(ipcBaseDir, sourceGroup, 'messages');
65
+ const tasksDir = path.join(ipcBaseDir, sourceGroup, 'tasks');
66
+
67
+ // Process messages from this group's IPC directory
68
+ try {
69
+ if (fs.existsSync(messagesDir)) {
70
+ const messageFiles = fs
71
+ .readdirSync(messagesDir)
72
+ .filter((f) => f.endsWith('.json'));
73
+ for (const file of messageFiles) {
74
+ const filePath = path.join(messagesDir, file);
75
+ try {
76
+ const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
77
+ if (data.type === 'message' && data.chatJid && data.text) {
78
+ // Authorization: verify this group can send to this chatJid
79
+ const targetGroup = registeredGroups[data.chatJid];
80
+ if (
81
+ isMain ||
82
+ (targetGroup && targetGroup.folder === sourceGroup)
83
+ ) {
84
+ await deps.sendMessage(data.chatJid, data.text);
85
+ logger.info(
86
+ { chatJid: data.chatJid, sourceGroup },
87
+ 'IPC message sent',
88
+ );
89
+ } else {
90
+ logger.warn(
91
+ { chatJid: data.chatJid, sourceGroup },
92
+ 'Unauthorized IPC message attempt blocked',
93
+ );
94
+ }
95
+ }
96
+ fs.unlinkSync(filePath);
97
+ } catch (err) {
98
+ logger.error(
99
+ { file, sourceGroup, err },
100
+ 'Error processing IPC message',
101
+ );
102
+ const errorDir = path.join(ipcBaseDir, 'errors');
103
+ fs.mkdirSync(errorDir, { recursive: true });
104
+ fs.renameSync(
105
+ filePath,
106
+ path.join(errorDir, `${sourceGroup}-${file}`),
107
+ );
108
+ }
109
+ }
110
+ }
111
+ } catch (err) {
112
+ logger.error(
113
+ { err, sourceGroup },
114
+ 'Error reading IPC messages directory',
115
+ );
116
+ }
117
+
118
+ // Process tasks from this group's IPC directory
119
+ try {
120
+ if (fs.existsSync(tasksDir)) {
121
+ const taskFiles = fs
122
+ .readdirSync(tasksDir)
123
+ .filter((f) => f.endsWith('.json'));
124
+ for (const file of taskFiles) {
125
+ const filePath = path.join(tasksDir, file);
126
+ try {
127
+ const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
128
+ // Pass source group identity to processTaskIpc for authorization
129
+ await processTaskIpc(data, sourceGroup, isMain, deps);
130
+ fs.unlinkSync(filePath);
131
+ } catch (err) {
132
+ logger.error(
133
+ { file, sourceGroup, err },
134
+ 'Error processing IPC task',
135
+ );
136
+ const errorDir = path.join(ipcBaseDir, 'errors');
137
+ fs.mkdirSync(errorDir, { recursive: true });
138
+ fs.renameSync(
139
+ filePath,
140
+ path.join(errorDir, `${sourceGroup}-${file}`),
141
+ );
142
+ }
143
+ }
144
+ }
145
+ } catch (err) {
146
+ logger.error({ err, sourceGroup }, 'Error reading IPC tasks directory');
147
+ }
148
+ }
149
+
150
+ setTimeout(processIpcFiles, IPC_POLL_INTERVAL);
151
+ };
152
+
153
+ processIpcFiles();
154
+ logger.info('IPC watcher started (per-group namespaces)');
155
+ }
156
+
157
+ export async function processTaskIpc(
158
+ data: {
159
+ type: string;
160
+ taskId?: string;
161
+ prompt?: string;
162
+ schedule_type?: string;
163
+ schedule_value?: string;
164
+ context_mode?: string;
165
+ groupFolder?: string;
166
+ chatJid?: string;
167
+ targetJid?: string;
168
+ // For register_group
169
+ jid?: string;
170
+ name?: string;
171
+ folder?: string;
172
+ trigger?: string;
173
+ requiresTrigger?: boolean;
174
+ containerConfig?: RegisteredGroup['containerConfig'];
175
+ },
176
+ sourceGroup: string, // Verified identity from IPC directory
177
+ isMain: boolean, // Verified from directory path
178
+ deps: IpcDeps,
179
+ ): Promise<void> {
180
+ const registeredGroups = deps.registeredGroups();
181
+
182
+ switch (data.type) {
183
+ case 'schedule_task':
184
+ if (
185
+ data.prompt &&
186
+ data.schedule_type &&
187
+ data.schedule_value &&
188
+ data.targetJid
189
+ ) {
190
+ // Resolve the target group from JID
191
+ const targetJid = data.targetJid as string;
192
+ const targetGroupEntry = registeredGroups[targetJid];
193
+
194
+ if (!targetGroupEntry) {
195
+ logger.warn(
196
+ { targetJid },
197
+ 'Cannot schedule task: target group not registered',
198
+ );
199
+ break;
200
+ }
201
+
202
+ const targetFolder = targetGroupEntry.folder;
203
+
204
+ // Authorization: non-main groups can only schedule for themselves
205
+ if (!isMain && targetFolder !== sourceGroup) {
206
+ logger.warn(
207
+ { sourceGroup, targetFolder },
208
+ 'Unauthorized schedule_task attempt blocked',
209
+ );
210
+ break;
211
+ }
212
+
213
+ const scheduleType = data.schedule_type as 'cron' | 'interval' | 'once';
214
+
215
+ let nextRun: string | null = null;
216
+ if (scheduleType === 'cron') {
217
+ try {
218
+ const interval = CronExpressionParser.parse(data.schedule_value, {
219
+ tz: TIMEZONE,
220
+ });
221
+ nextRun = interval.next().toISOString();
222
+ } catch {
223
+ logger.warn(
224
+ { scheduleValue: data.schedule_value },
225
+ 'Invalid cron expression',
226
+ );
227
+ break;
228
+ }
229
+ } else if (scheduleType === 'interval') {
230
+ const ms = parseInt(data.schedule_value, 10);
231
+ if (isNaN(ms) || ms <= 0) {
232
+ logger.warn(
233
+ { scheduleValue: data.schedule_value },
234
+ 'Invalid interval',
235
+ );
236
+ break;
237
+ }
238
+ nextRun = new Date(Date.now() + ms).toISOString();
239
+ } else if (scheduleType === 'once') {
240
+ const date = new Date(data.schedule_value);
241
+ if (isNaN(date.getTime())) {
242
+ logger.warn(
243
+ { scheduleValue: data.schedule_value },
244
+ 'Invalid timestamp',
245
+ );
246
+ break;
247
+ }
248
+ nextRun = date.toISOString();
249
+ }
250
+
251
+ const taskId =
252
+ data.taskId ||
253
+ `task-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
254
+ const contextMode =
255
+ data.context_mode === 'group' || data.context_mode === 'isolated'
256
+ ? data.context_mode
257
+ : 'isolated';
258
+ createTask({
259
+ id: taskId,
260
+ group_folder: targetFolder,
261
+ chat_jid: targetJid,
262
+ prompt: data.prompt,
263
+ schedule_type: scheduleType,
264
+ schedule_value: data.schedule_value,
265
+ context_mode: contextMode,
266
+ next_run: nextRun,
267
+ status: 'active',
268
+ created_at: new Date().toISOString(),
269
+ });
270
+ logger.info(
271
+ { taskId, sourceGroup, targetFolder, contextMode },
272
+ 'Task created via IPC',
273
+ );
274
+ deps.onTasksChanged();
275
+ }
276
+ break;
277
+
278
+ case 'pause_task':
279
+ if (data.taskId) {
280
+ const task = getTaskById(data.taskId);
281
+ if (task && (isMain || task.group_folder === sourceGroup)) {
282
+ updateTask(data.taskId, { status: 'paused' });
283
+ logger.info(
284
+ { taskId: data.taskId, sourceGroup },
285
+ 'Task paused via IPC',
286
+ );
287
+ deps.onTasksChanged();
288
+ } else {
289
+ logger.warn(
290
+ { taskId: data.taskId, sourceGroup },
291
+ 'Unauthorized task pause attempt',
292
+ );
293
+ }
294
+ }
295
+ break;
296
+
297
+ case 'resume_task':
298
+ if (data.taskId) {
299
+ const task = getTaskById(data.taskId);
300
+ if (task && (isMain || task.group_folder === sourceGroup)) {
301
+ updateTask(data.taskId, { status: 'active' });
302
+ logger.info(
303
+ { taskId: data.taskId, sourceGroup },
304
+ 'Task resumed via IPC',
305
+ );
306
+ deps.onTasksChanged();
307
+ } else {
308
+ logger.warn(
309
+ { taskId: data.taskId, sourceGroup },
310
+ 'Unauthorized task resume attempt',
311
+ );
312
+ }
313
+ }
314
+ break;
315
+
316
+ case 'cancel_task':
317
+ if (data.taskId) {
318
+ const task = getTaskById(data.taskId);
319
+ if (task && (isMain || task.group_folder === sourceGroup)) {
320
+ deleteTask(data.taskId);
321
+ logger.info(
322
+ { taskId: data.taskId, sourceGroup },
323
+ 'Task cancelled via IPC',
324
+ );
325
+ deps.onTasksChanged();
326
+ } else {
327
+ logger.warn(
328
+ { taskId: data.taskId, sourceGroup },
329
+ 'Unauthorized task cancel attempt',
330
+ );
331
+ }
332
+ }
333
+ break;
334
+
335
+ case 'update_task':
336
+ if (data.taskId) {
337
+ const task = getTaskById(data.taskId);
338
+ if (!task) {
339
+ logger.warn(
340
+ { taskId: data.taskId, sourceGroup },
341
+ 'Task not found for update',
342
+ );
343
+ break;
344
+ }
345
+ if (!isMain && task.group_folder !== sourceGroup) {
346
+ logger.warn(
347
+ { taskId: data.taskId, sourceGroup },
348
+ 'Unauthorized task update attempt',
349
+ );
350
+ break;
351
+ }
352
+
353
+ const updates: Parameters<typeof updateTask>[1] = {};
354
+ if (data.prompt !== undefined) updates.prompt = data.prompt;
355
+ if (data.schedule_type !== undefined)
356
+ updates.schedule_type = data.schedule_type as
357
+ | 'cron'
358
+ | 'interval'
359
+ | 'once';
360
+ if (data.schedule_value !== undefined)
361
+ updates.schedule_value = data.schedule_value;
362
+
363
+ // Recompute next_run if schedule changed
364
+ if (data.schedule_type || data.schedule_value) {
365
+ const updatedTask = {
366
+ ...task,
367
+ ...updates,
368
+ };
369
+ if (updatedTask.schedule_type === 'cron') {
370
+ try {
371
+ const interval = CronExpressionParser.parse(
372
+ updatedTask.schedule_value,
373
+ { tz: TIMEZONE },
374
+ );
375
+ updates.next_run = interval.next().toISOString();
376
+ } catch {
377
+ logger.warn(
378
+ { taskId: data.taskId, value: updatedTask.schedule_value },
379
+ 'Invalid cron in task update',
380
+ );
381
+ break;
382
+ }
383
+ } else if (updatedTask.schedule_type === 'interval') {
384
+ const ms = parseInt(updatedTask.schedule_value, 10);
385
+ if (!isNaN(ms) && ms > 0) {
386
+ updates.next_run = new Date(Date.now() + ms).toISOString();
387
+ }
388
+ }
389
+ }
390
+
391
+ updateTask(data.taskId, updates);
392
+ logger.info(
393
+ { taskId: data.taskId, sourceGroup, updates },
394
+ 'Task updated via IPC',
395
+ );
396
+ deps.onTasksChanged();
397
+ }
398
+ break;
399
+
400
+ case 'refresh_groups':
401
+ // Only main group can request a refresh
402
+ if (isMain) {
403
+ logger.info(
404
+ { sourceGroup },
405
+ 'Group metadata refresh requested via IPC',
406
+ );
407
+ await deps.syncGroups(true);
408
+ // Write updated snapshot immediately
409
+ const availableGroups = deps.getAvailableGroups();
410
+ deps.writeGroupsSnapshot(
411
+ sourceGroup,
412
+ true,
413
+ availableGroups,
414
+ new Set(Object.keys(registeredGroups)),
415
+ );
416
+ } else {
417
+ logger.warn(
418
+ { sourceGroup },
419
+ 'Unauthorized refresh_groups attempt blocked',
420
+ );
421
+ }
422
+ break;
423
+
424
+ case 'register_group':
425
+ // Only main group can register new groups
426
+ if (!isMain) {
427
+ logger.warn(
428
+ { sourceGroup },
429
+ 'Unauthorized register_group attempt blocked',
430
+ );
431
+ break;
432
+ }
433
+ if (data.jid && data.name && data.folder && data.trigger) {
434
+ if (!isValidGroupFolder(data.folder)) {
435
+ logger.warn(
436
+ { sourceGroup, folder: data.folder },
437
+ 'Invalid register_group request - unsafe folder name',
438
+ );
439
+ break;
440
+ }
441
+ // Defense in depth: agent cannot set isMain via IPC
442
+ deps.registerGroup(data.jid, {
443
+ name: data.name,
444
+ folder: data.folder,
445
+ trigger: data.trigger,
446
+ added_at: new Date().toISOString(),
447
+ containerConfig: data.containerConfig,
448
+ requiresTrigger: data.requiresTrigger,
449
+ });
450
+ } else {
451
+ logger.warn(
452
+ { data },
453
+ 'Invalid register_group request - missing required fields',
454
+ );
455
+ }
456
+ break;
457
+
458
+ default:
459
+ logger.warn({ type: data.type }, 'Unknown IPC task type');
460
+ }
461
+ }
package/src/logger.ts ADDED
@@ -0,0 +1,16 @@
1
+ import pino from 'pino';
2
+
3
+ export const logger = pino({
4
+ level: process.env.LOG_LEVEL || 'info',
5
+ transport: { target: 'pino-pretty', options: { colorize: true } },
6
+ });
7
+
8
+ // Route uncaught errors through pino so they get timestamps in stderr
9
+ process.on('uncaughtException', (err) => {
10
+ logger.fatal({ err }, 'Uncaught exception');
11
+ process.exit(1);
12
+ });
13
+
14
+ process.on('unhandledRejection', (reason) => {
15
+ logger.error({ err: reason }, 'Unhandled rejection');
16
+ });