@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
@@ -0,0 +1,210 @@
1
+ import { CronExpressionParser } from 'cron-parser';
2
+ import fs from 'fs';
3
+ import { ASSISTANT_NAME, SCHEDULER_POLL_INTERVAL, TIMEZONE } from './config.js';
4
+ import { runContainerAgent, writeTasksSnapshot, } from './container-runner.js';
5
+ import { getAllTasks, getDueTasks, getTaskById, logTaskRun, updateTask, updateTaskAfterRun, } from './db.js';
6
+ import { resolveGroupFolderPath } from './group-folder.js';
7
+ import { logger } from './logger.js';
8
+ /**
9
+ * Compute the next run time for a recurring task, anchored to the
10
+ * task's scheduled time rather than Date.now() to prevent cumulative
11
+ * drift on interval-based tasks.
12
+ *
13
+ * Co-authored-by: @community-pr-601
14
+ */
15
+ export function computeNextRun(task) {
16
+ if (task.schedule_type === 'once')
17
+ return null;
18
+ const now = Date.now();
19
+ if (task.schedule_type === 'cron') {
20
+ const interval = CronExpressionParser.parse(task.schedule_value, {
21
+ tz: TIMEZONE,
22
+ });
23
+ return interval.next().toISOString();
24
+ }
25
+ if (task.schedule_type === 'interval') {
26
+ const ms = parseInt(task.schedule_value, 10);
27
+ if (!ms || ms <= 0) {
28
+ // Guard against malformed interval that would cause an infinite loop
29
+ logger.warn({ taskId: task.id, value: task.schedule_value }, 'Invalid interval value');
30
+ return new Date(now + 60_000).toISOString();
31
+ }
32
+ // Anchor to the scheduled time, not now, to prevent drift.
33
+ // Skip past any missed intervals so we always land in the future.
34
+ let next = new Date(task.next_run).getTime() + ms;
35
+ while (next <= now) {
36
+ next += ms;
37
+ }
38
+ return new Date(next).toISOString();
39
+ }
40
+ return null;
41
+ }
42
+ async function runTask(task, deps) {
43
+ const startTime = Date.now();
44
+ let groupDir;
45
+ try {
46
+ groupDir = resolveGroupFolderPath(task.group_folder);
47
+ }
48
+ catch (err) {
49
+ const error = err instanceof Error ? err.message : String(err);
50
+ // Stop retry churn for malformed legacy rows.
51
+ updateTask(task.id, { status: 'paused' });
52
+ logger.error({ taskId: task.id, groupFolder: task.group_folder, error }, 'Task has invalid group folder');
53
+ logTaskRun({
54
+ task_id: task.id,
55
+ run_at: new Date().toISOString(),
56
+ duration_ms: Date.now() - startTime,
57
+ status: 'error',
58
+ result: null,
59
+ error,
60
+ });
61
+ return;
62
+ }
63
+ fs.mkdirSync(groupDir, { recursive: true });
64
+ logger.info({ taskId: task.id, group: task.group_folder }, 'Running scheduled task');
65
+ const groups = deps.registeredGroups();
66
+ let group = Object.values(groups).find((g) => g.folder === task.group_folder);
67
+ if (!group) {
68
+ // Fall back to the first isMain group so that legacy tasks created with
69
+ // group_folder='main' still run after a web-channel migration.
70
+ const fallback = Object.values(groups).find((g) => g.isMain === true);
71
+ if (fallback) {
72
+ logger.warn({ taskId: task.id, groupFolder: task.group_folder, fallback: fallback.folder }, 'Group not found for task, falling back to main group');
73
+ group = fallback;
74
+ }
75
+ else {
76
+ logger.error({ taskId: task.id, groupFolder: task.group_folder }, 'Group not found for task');
77
+ logTaskRun({
78
+ task_id: task.id,
79
+ run_at: new Date().toISOString(),
80
+ duration_ms: Date.now() - startTime,
81
+ status: 'error',
82
+ result: null,
83
+ error: `Group not found: ${task.group_folder}`,
84
+ });
85
+ return;
86
+ }
87
+ }
88
+ // Update tasks snapshot for container to read (filtered by group)
89
+ const isMain = group.isMain === true;
90
+ const tasks = getAllTasks();
91
+ writeTasksSnapshot(group.folder, isMain, tasks.map((t) => ({
92
+ id: t.id,
93
+ groupFolder: t.group_folder,
94
+ prompt: t.prompt,
95
+ schedule_type: t.schedule_type,
96
+ schedule_value: t.schedule_value,
97
+ status: t.status,
98
+ next_run: t.next_run,
99
+ })));
100
+ let result = null;
101
+ let error = null;
102
+ // For group context mode, use the group's current session
103
+ const sessions = deps.getSessions();
104
+ const sessionId = task.context_mode === 'group' ? sessions[task.group_folder] : undefined;
105
+ // After the task produces a result, close the container promptly.
106
+ // Tasks are single-turn — no need to wait IDLE_TIMEOUT (30 min) for the
107
+ // query loop to time out. A short delay handles any final MCP calls.
108
+ const TASK_CLOSE_DELAY_MS = 10000;
109
+ let closeTimer = null;
110
+ const scheduleClose = () => {
111
+ if (closeTimer)
112
+ return; // already scheduled
113
+ closeTimer = setTimeout(() => {
114
+ logger.debug({ taskId: task.id }, 'Closing task container after result');
115
+ deps.queue.closeStdin(task.chat_jid);
116
+ }, TASK_CLOSE_DELAY_MS);
117
+ };
118
+ try {
119
+ const output = await runContainerAgent(group, {
120
+ prompt: task.prompt,
121
+ sessionId,
122
+ groupFolder: group.folder,
123
+ chatJid: task.chat_jid,
124
+ isMain,
125
+ isScheduledTask: true,
126
+ assistantName: ASSISTANT_NAME,
127
+ }, (proc, containerName) => deps.onProcess(task.chat_jid, proc, containerName, group.folder), async (streamedOutput) => {
128
+ if (streamedOutput.result) {
129
+ result = streamedOutput.result;
130
+ // Forward result to user (sendMessage handles formatting)
131
+ await deps.sendMessage(task.chat_jid, streamedOutput.result);
132
+ scheduleClose();
133
+ }
134
+ if (streamedOutput.status === 'success') {
135
+ deps.queue.notifyIdle(task.chat_jid);
136
+ scheduleClose(); // Close promptly even when result is null (e.g. IPC-only tasks)
137
+ }
138
+ if (streamedOutput.status === 'error') {
139
+ error = streamedOutput.error || 'Unknown error';
140
+ }
141
+ });
142
+ if (closeTimer)
143
+ clearTimeout(closeTimer);
144
+ if (output.status === 'error') {
145
+ error = output.error || 'Unknown error';
146
+ }
147
+ else if (output.result) {
148
+ // Result was already forwarded to the user via the streaming callback above
149
+ result = output.result;
150
+ }
151
+ logger.info({ taskId: task.id, durationMs: Date.now() - startTime }, 'Task completed');
152
+ }
153
+ catch (err) {
154
+ if (closeTimer)
155
+ clearTimeout(closeTimer);
156
+ error = err instanceof Error ? err.message : String(err);
157
+ logger.error({ taskId: task.id, error }, 'Task failed');
158
+ }
159
+ const durationMs = Date.now() - startTime;
160
+ logTaskRun({
161
+ task_id: task.id,
162
+ run_at: new Date().toISOString(),
163
+ duration_ms: durationMs,
164
+ status: error ? 'error' : 'success',
165
+ result,
166
+ error,
167
+ });
168
+ const nextRun = computeNextRun(task);
169
+ const resultSummary = error
170
+ ? `Error: ${error}`
171
+ : result
172
+ ? result.slice(0, 200)
173
+ : 'Completed';
174
+ updateTaskAfterRun(task.id, nextRun, resultSummary);
175
+ }
176
+ let schedulerRunning = false;
177
+ export function startSchedulerLoop(deps) {
178
+ if (schedulerRunning) {
179
+ logger.debug('Scheduler loop already running, skipping duplicate start');
180
+ return;
181
+ }
182
+ schedulerRunning = true;
183
+ logger.info('Scheduler loop started');
184
+ const loop = async () => {
185
+ try {
186
+ const dueTasks = getDueTasks();
187
+ if (dueTasks.length > 0) {
188
+ logger.info({ count: dueTasks.length }, 'Found due tasks');
189
+ }
190
+ for (const task of dueTasks) {
191
+ // Re-check task status in case it was paused/cancelled
192
+ const currentTask = getTaskById(task.id);
193
+ if (!currentTask || currentTask.status !== 'active') {
194
+ continue;
195
+ }
196
+ deps.queue.enqueueTask(currentTask.chat_jid, currentTask.id, () => runTask(currentTask, deps));
197
+ }
198
+ }
199
+ catch (err) {
200
+ logger.error({ err }, 'Error in scheduler loop');
201
+ }
202
+ setTimeout(loop, SCHEDULER_POLL_INTERVAL);
203
+ };
204
+ loop();
205
+ }
206
+ /** @internal - for tests only. */
207
+ export function _resetSchedulerLoopForTests() {
208
+ schedulerRunning = false;
209
+ }
210
+ //# sourceMappingURL=task-scheduler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"task-scheduler.js","sourceRoot":"","sources":["../src/task-scheduler.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=task-scheduler.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"task-scheduler.test.d.ts","sourceRoot":"","sources":["../src/task-scheduler.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,107 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { _initTestDatabase, createTask, getTaskById } from './db.js';
3
+ import { _resetSchedulerLoopForTests, computeNextRun, startSchedulerLoop, } from './task-scheduler.js';
4
+ describe('task scheduler', () => {
5
+ beforeEach(() => {
6
+ _initTestDatabase();
7
+ _resetSchedulerLoopForTests();
8
+ vi.useFakeTimers();
9
+ });
10
+ afterEach(() => {
11
+ vi.useRealTimers();
12
+ });
13
+ it('pauses due tasks with invalid group folders to prevent retry churn', async () => {
14
+ createTask({
15
+ id: 'task-invalid-folder',
16
+ group_folder: '../../outside',
17
+ chat_jid: 'bad@g.us',
18
+ prompt: 'run',
19
+ schedule_type: 'once',
20
+ schedule_value: '2026-02-22T00:00:00.000Z',
21
+ context_mode: 'isolated',
22
+ next_run: new Date(Date.now() - 60_000).toISOString(),
23
+ status: 'active',
24
+ created_at: '2026-02-22T00:00:00.000Z',
25
+ });
26
+ const enqueueTask = vi.fn((_groupJid, _taskId, fn) => {
27
+ void fn();
28
+ });
29
+ startSchedulerLoop({
30
+ registeredGroups: () => ({}),
31
+ getSessions: () => ({}),
32
+ queue: { enqueueTask },
33
+ onProcess: () => { },
34
+ sendMessage: async () => { },
35
+ });
36
+ await vi.advanceTimersByTimeAsync(10);
37
+ const task = getTaskById('task-invalid-folder');
38
+ expect(task?.status).toBe('paused');
39
+ });
40
+ it('computeNextRun anchors interval tasks to scheduled time to prevent drift', () => {
41
+ const scheduledTime = new Date(Date.now() - 2000).toISOString(); // 2s ago
42
+ const task = {
43
+ id: 'drift-test',
44
+ group_folder: 'test',
45
+ chat_jid: 'test@g.us',
46
+ prompt: 'test',
47
+ schedule_type: 'interval',
48
+ schedule_value: '60000', // 1 minute
49
+ context_mode: 'isolated',
50
+ next_run: scheduledTime,
51
+ last_run: null,
52
+ last_result: null,
53
+ status: 'active',
54
+ created_at: '2026-01-01T00:00:00.000Z',
55
+ };
56
+ const nextRun = computeNextRun(task);
57
+ expect(nextRun).not.toBeNull();
58
+ // Should be anchored to scheduledTime + 60s, NOT Date.now() + 60s
59
+ const expected = new Date(scheduledTime).getTime() + 60000;
60
+ expect(new Date(nextRun).getTime()).toBe(expected);
61
+ });
62
+ it('computeNextRun returns null for once-tasks', () => {
63
+ const task = {
64
+ id: 'once-test',
65
+ group_folder: 'test',
66
+ chat_jid: 'test@g.us',
67
+ prompt: 'test',
68
+ schedule_type: 'once',
69
+ schedule_value: '2026-01-01T00:00:00.000Z',
70
+ context_mode: 'isolated',
71
+ next_run: new Date(Date.now() - 1000).toISOString(),
72
+ last_run: null,
73
+ last_result: null,
74
+ status: 'active',
75
+ created_at: '2026-01-01T00:00:00.000Z',
76
+ };
77
+ expect(computeNextRun(task)).toBeNull();
78
+ });
79
+ it('computeNextRun skips missed intervals without infinite loop', () => {
80
+ // Task was due 10 intervals ago (missed)
81
+ const ms = 60000;
82
+ const missedBy = ms * 10;
83
+ const scheduledTime = new Date(Date.now() - missedBy).toISOString();
84
+ const task = {
85
+ id: 'skip-test',
86
+ group_folder: 'test',
87
+ chat_jid: 'test@g.us',
88
+ prompt: 'test',
89
+ schedule_type: 'interval',
90
+ schedule_value: String(ms),
91
+ context_mode: 'isolated',
92
+ next_run: scheduledTime,
93
+ last_run: null,
94
+ last_result: null,
95
+ status: 'active',
96
+ created_at: '2026-01-01T00:00:00.000Z',
97
+ };
98
+ const nextRun = computeNextRun(task);
99
+ expect(nextRun).not.toBeNull();
100
+ // Must be in the future
101
+ expect(new Date(nextRun).getTime()).toBeGreaterThan(Date.now());
102
+ // Must be aligned to the original schedule grid
103
+ const offset = (new Date(nextRun).getTime() - new Date(scheduledTime).getTime()) % ms;
104
+ expect(offset).toBe(0);
105
+ });
106
+ });
107
+ //# sourceMappingURL=task-scheduler.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"task-scheduler.test.js","sourceRoot":"","sources":["../src/task-scheduler.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Convert a UTC ISO timestamp to a localized display string.
3
+ * Uses the Intl API (no external dependencies).
4
+ */
5
+ export declare function formatLocalTime(utcIso: string, timezone: string): string;
6
+ //# sourceMappingURL=timezone.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"timezone.d.ts","sourceRoot":"","sources":["../src/timezone.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAWxE"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Convert a UTC ISO timestamp to a localized display string.
3
+ * Uses the Intl API (no external dependencies).
4
+ */
5
+ export function formatLocalTime(utcIso, timezone) {
6
+ const date = new Date(utcIso);
7
+ return date.toLocaleString('en-US', {
8
+ timeZone: timezone,
9
+ year: 'numeric',
10
+ month: 'short',
11
+ day: 'numeric',
12
+ hour: 'numeric',
13
+ minute: '2-digit',
14
+ hour12: true,
15
+ });
16
+ }
17
+ //# sourceMappingURL=timezone.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"timezone.js","sourceRoot":"","sources":["../src/timezone.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,MAAc,EAAE,QAAgB;IAC9D,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,MAAM,CAAC,CAAC;IAC9B,OAAO,IAAI,CAAC,cAAc,CAAC,OAAO,EAAE;QAClC,QAAQ,EAAE,QAAQ;QAClB,IAAI,EAAE,SAAS;QACf,KAAK,EAAE,OAAO;QACd,GAAG,EAAE,SAAS;QACd,IAAI,EAAE,SAAS;QACf,MAAM,EAAE,SAAS;QACjB,MAAM,EAAE,IAAI;KACb,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=timezone.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"timezone.test.d.ts","sourceRoot":"","sources":["../src/timezone.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,23 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { formatLocalTime } from './timezone.js';
3
+ // --- formatLocalTime ---
4
+ describe('formatLocalTime', () => {
5
+ it('converts UTC to local time display', () => {
6
+ // 2026-02-04T18:30:00Z in America/New_York (EST, UTC-5) = 1:30 PM
7
+ const result = formatLocalTime('2026-02-04T18:30:00.000Z', 'America/New_York');
8
+ expect(result).toContain('1:30');
9
+ expect(result).toContain('PM');
10
+ expect(result).toContain('Feb');
11
+ expect(result).toContain('2026');
12
+ });
13
+ it('handles different timezones', () => {
14
+ // Same UTC time should produce different local times
15
+ const utc = '2026-06-15T12:00:00.000Z';
16
+ const ny = formatLocalTime(utc, 'America/New_York');
17
+ const tokyo = formatLocalTime(utc, 'Asia/Tokyo');
18
+ // NY is UTC-4 in summer (EDT), Tokyo is UTC+9
19
+ expect(ny).toContain('8:00');
20
+ expect(tokyo).toContain('9:00');
21
+ });
22
+ });
23
+ //# sourceMappingURL=timezone.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"timezone.test.js","sourceRoot":"","sources":["../src/timezone.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAE9C,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAEhD,0BAA0B;AAE1B,QAAQ,CAAC,iBAAiB,EAAE,GAAG,EAAE;IAC/B,EAAE,CAAC,oCAAoC,EAAE,GAAG,EAAE;QAC5C,kEAAkE;QAClE,MAAM,MAAM,GAAG,eAAe,CAC5B,0BAA0B,EAC1B,kBAAkB,CACnB,CAAC;QACF,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;QACjC,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;QAC/B,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;QAChC,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IACnC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6BAA6B,EAAE,GAAG,EAAE;QACrC,qDAAqD;QACrD,MAAM,GAAG,GAAG,0BAA0B,CAAC;QACvC,MAAM,EAAE,GAAG,eAAe,CAAC,GAAG,EAAE,kBAAkB,CAAC,CAAC;QACpD,MAAM,KAAK,GAAG,eAAe,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC;QACjD,8CAA8C;QAC9C,MAAM,CAAC,EAAE,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;QAC7B,MAAM,CAAC,KAAK,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IAClC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,78 @@
1
+ export interface AdditionalMount {
2
+ hostPath: string;
3
+ containerPath?: string;
4
+ readonly?: boolean;
5
+ }
6
+ /**
7
+ * Mount Allowlist - Security configuration for additional mounts
8
+ * This file should be stored at ~/.config/nanoclaw/mount-allowlist.json
9
+ * and is NOT mounted into any container, making it tamper-proof from agents.
10
+ */
11
+ export interface MountAllowlist {
12
+ allowedRoots: AllowedRoot[];
13
+ blockedPatterns: string[];
14
+ nonMainReadOnly: boolean;
15
+ }
16
+ export interface AllowedRoot {
17
+ path: string;
18
+ allowReadWrite: boolean;
19
+ description?: string;
20
+ }
21
+ export interface ContainerConfig {
22
+ additionalMounts?: AdditionalMount[];
23
+ timeout?: number;
24
+ }
25
+ export interface RegisteredGroup {
26
+ name: string;
27
+ folder: string;
28
+ trigger: string;
29
+ added_at: string;
30
+ containerConfig?: ContainerConfig;
31
+ requiresTrigger?: boolean;
32
+ isMain?: boolean;
33
+ }
34
+ export interface NewMessage {
35
+ id: string;
36
+ chat_jid: string;
37
+ sender: string;
38
+ sender_name: string;
39
+ content: string;
40
+ timestamp: string;
41
+ is_from_me?: boolean;
42
+ is_bot_message?: boolean;
43
+ }
44
+ export interface ScheduledTask {
45
+ id: string;
46
+ group_folder: string;
47
+ chat_jid: string;
48
+ prompt: string;
49
+ schedule_type: 'cron' | 'interval' | 'once';
50
+ schedule_value: string;
51
+ context_mode: 'group' | 'isolated';
52
+ next_run: string | null;
53
+ last_run: string | null;
54
+ last_result: string | null;
55
+ status: 'active' | 'paused' | 'completed';
56
+ created_at: string;
57
+ }
58
+ export interface TaskRunLog {
59
+ task_id: string;
60
+ run_at: string;
61
+ duration_ms: number;
62
+ status: 'success' | 'error';
63
+ result: string | null;
64
+ error: string | null;
65
+ }
66
+ export interface Channel {
67
+ name: string;
68
+ connect(): Promise<void>;
69
+ sendMessage(jid: string, text: string): Promise<void>;
70
+ isConnected(): boolean;
71
+ ownsJid(jid: string): boolean;
72
+ disconnect(): Promise<void>;
73
+ setTyping?(jid: string, isTyping: boolean): Promise<void>;
74
+ syncGroups?(force: boolean): Promise<void>;
75
+ }
76
+ export type OnInboundMessage = (chatJid: string, message: NewMessage) => void;
77
+ export type OnChatMetadata = (chatJid: string, timestamp: string, name?: string, channel?: string, isGroup?: boolean) => void;
78
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,90 @@
1
+ # Apple Container Networking Setup (macOS 26)
2
+
3
+ Apple Container's vmnet networking requires manual configuration for containers to access the internet. Without this, containers can communicate with the host but cannot reach external services (DNS, HTTPS, APIs).
4
+
5
+ ## Quick Setup
6
+
7
+ Run these two commands (requires `sudo`):
8
+
9
+ ```bash
10
+ # 1. Enable IP forwarding so the host routes container traffic
11
+ sudo sysctl -w net.inet.ip.forwarding=1
12
+
13
+ # 2. Enable NAT so container traffic gets masqueraded through your internet interface
14
+ echo "nat on en0 from 192.168.64.0/24 to any -> (en0)" | sudo pfctl -ef -
15
+ ```
16
+
17
+ > **Note:** Replace `en0` with your active internet interface. Check with: `route get 8.8.8.8 | grep interface`
18
+
19
+ ## Making It Persistent
20
+
21
+ These settings reset on reboot. To make them permanent:
22
+
23
+ **IP Forwarding** — add to `/etc/sysctl.conf`:
24
+ ```
25
+ net.inet.ip.forwarding=1
26
+ ```
27
+
28
+ **NAT Rules** — add to `/etc/pf.conf` (before any existing rules):
29
+ ```
30
+ nat on en0 from 192.168.64.0/24 to any -> (en0)
31
+ ```
32
+
33
+ Then reload: `sudo pfctl -f /etc/pf.conf`
34
+
35
+ ## IPv6 DNS Issue
36
+
37
+ By default, DNS resolvers return IPv6 (AAAA) records before IPv4 (A) records. Since our NAT only handles IPv4, Node.js applications inside containers will try IPv6 first and fail.
38
+
39
+ The container image and runner are configured to prefer IPv4 via:
40
+ ```
41
+ NODE_OPTIONS=--dns-result-order=ipv4first
42
+ ```
43
+
44
+ This is set both in the `Dockerfile` and passed via `-e` flag in `container-runner.ts`.
45
+
46
+ ## Verification
47
+
48
+ ```bash
49
+ # Check IP forwarding is enabled
50
+ sysctl net.inet.ip.forwarding
51
+ # Expected: net.inet.ip.forwarding: 1
52
+
53
+ # Test container internet access
54
+ container run --rm --entrypoint curl nanoclaw-agent:latest \
55
+ -s4 --connect-timeout 5 -o /dev/null -w "%{http_code}" https://api.anthropic.com
56
+ # Expected: 404
57
+
58
+ # Check bridge interface (only exists when a container is running)
59
+ ifconfig bridge100
60
+ ```
61
+
62
+ ## Troubleshooting
63
+
64
+ | Symptom | Cause | Fix |
65
+ |---------|-------|-----|
66
+ | `curl: (28) Connection timed out` | IP forwarding disabled | `sudo sysctl -w net.inet.ip.forwarding=1` |
67
+ | HTTP works, HTTPS times out | IPv6 DNS resolution | Add `NODE_OPTIONS=--dns-result-order=ipv4first` |
68
+ | `Could not resolve host` | DNS not forwarded | Check bridge100 exists, verify pfctl NAT rules |
69
+ | Container hangs after output | Missing `process.exit(0)` in agent-runner | Rebuild container image |
70
+
71
+ ## How It Works
72
+
73
+ ```
74
+ Container VM (192.168.64.x)
75
+
76
+ ├── eth0 → gateway 192.168.64.1
77
+
78
+ bridge100 (192.168.64.1) ← host bridge, created by vmnet when container runs
79
+
80
+ ├── IP forwarding (sysctl) routes packets from bridge100 → en0
81
+
82
+ ├── NAT (pfctl) masquerades 192.168.64.0/24 → en0's IP
83
+
84
+ en0 (your WiFi/Ethernet) → Internet
85
+ ```
86
+
87
+ ## References
88
+
89
+ - [apple/container#469](https://github.com/apple/container/issues/469) — No network from container on macOS 26
90
+ - [apple/container#656](https://github.com/apple/container/issues/656) — Cannot access internet URLs during building