@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,362 @@
1
+ /**
2
+ * Step: service — Generate and load service manager config.
3
+ * Replaces 08-setup-service.sh
4
+ *
5
+ * Fixes: Root→system systemd, WSL nohup fallback, no `|| true` swallowing errors.
6
+ */
7
+ import { execSync } from 'child_process';
8
+ import fs from 'fs';
9
+ import os from 'os';
10
+ import path from 'path';
11
+
12
+ import { logger } from '../src/logger.js';
13
+ import {
14
+ getPlatform,
15
+ getNodePath,
16
+ getServiceManager,
17
+ hasSystemd,
18
+ isRoot,
19
+ isWSL,
20
+ } from './platform.js';
21
+ import { emitStatus } from './status.js';
22
+
23
+ export async function run(_args: string[]): Promise<void> {
24
+ const projectRoot = process.cwd();
25
+ const platform = getPlatform();
26
+ const nodePath = getNodePath();
27
+ const homeDir = os.homedir();
28
+
29
+ logger.info({ platform, nodePath, projectRoot }, 'Setting up service');
30
+
31
+ // Build first
32
+ logger.info('Building TypeScript');
33
+ try {
34
+ execSync('npm run build', {
35
+ cwd: projectRoot,
36
+ stdio: ['ignore', 'pipe', 'pipe'],
37
+ });
38
+ logger.info('Build succeeded');
39
+ } catch {
40
+ logger.error('Build failed');
41
+ emitStatus('SETUP_SERVICE', {
42
+ SERVICE_TYPE: 'unknown',
43
+ NODE_PATH: nodePath,
44
+ PROJECT_PATH: projectRoot,
45
+ STATUS: 'failed',
46
+ ERROR: 'build_failed',
47
+ LOG: 'logs/setup.log',
48
+ });
49
+ process.exit(1);
50
+ }
51
+
52
+ fs.mkdirSync(path.join(projectRoot, 'logs'), { recursive: true });
53
+
54
+ if (platform === 'macos') {
55
+ setupLaunchd(projectRoot, nodePath, homeDir);
56
+ } else if (platform === 'linux') {
57
+ setupLinux(projectRoot, nodePath, homeDir);
58
+ } else {
59
+ emitStatus('SETUP_SERVICE', {
60
+ SERVICE_TYPE: 'unknown',
61
+ NODE_PATH: nodePath,
62
+ PROJECT_PATH: projectRoot,
63
+ STATUS: 'failed',
64
+ ERROR: 'unsupported_platform',
65
+ LOG: 'logs/setup.log',
66
+ });
67
+ process.exit(1);
68
+ }
69
+ }
70
+
71
+ function setupLaunchd(
72
+ projectRoot: string,
73
+ nodePath: string,
74
+ homeDir: string,
75
+ ): void {
76
+ const plistPath = path.join(
77
+ homeDir,
78
+ 'Library',
79
+ 'LaunchAgents',
80
+ 'com.nanoclaw.plist',
81
+ );
82
+ fs.mkdirSync(path.dirname(plistPath), { recursive: true });
83
+
84
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
85
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
86
+ <plist version="1.0">
87
+ <dict>
88
+ <key>Label</key>
89
+ <string>com.nanoclaw</string>
90
+ <key>ProgramArguments</key>
91
+ <array>
92
+ <string>${nodePath}</string>
93
+ <string>${projectRoot}/dist/index.js</string>
94
+ </array>
95
+ <key>WorkingDirectory</key>
96
+ <string>${projectRoot}</string>
97
+ <key>RunAtLoad</key>
98
+ <true/>
99
+ <key>KeepAlive</key>
100
+ <true/>
101
+ <key>EnvironmentVariables</key>
102
+ <dict>
103
+ <key>PATH</key>
104
+ <string>/usr/local/bin:/usr/bin:/bin:${homeDir}/.local/bin</string>
105
+ <key>HOME</key>
106
+ <string>${homeDir}</string>
107
+ </dict>
108
+ <key>StandardOutPath</key>
109
+ <string>${projectRoot}/logs/nanoclaw.log</string>
110
+ <key>StandardErrorPath</key>
111
+ <string>${projectRoot}/logs/nanoclaw.error.log</string>
112
+ </dict>
113
+ </plist>`;
114
+
115
+ fs.writeFileSync(plistPath, plist);
116
+ logger.info({ plistPath }, 'Wrote launchd plist');
117
+
118
+ try {
119
+ execSync(`launchctl load ${JSON.stringify(plistPath)}`, {
120
+ stdio: 'ignore',
121
+ });
122
+ logger.info('launchctl load succeeded');
123
+ } catch {
124
+ logger.warn('launchctl load failed (may already be loaded)');
125
+ }
126
+
127
+ // Verify
128
+ let serviceLoaded = false;
129
+ try {
130
+ const output = execSync('launchctl list', { encoding: 'utf-8' });
131
+ serviceLoaded = output.includes('com.nanoclaw');
132
+ } catch {
133
+ // launchctl list failed
134
+ }
135
+
136
+ emitStatus('SETUP_SERVICE', {
137
+ SERVICE_TYPE: 'launchd',
138
+ NODE_PATH: nodePath,
139
+ PROJECT_PATH: projectRoot,
140
+ PLIST_PATH: plistPath,
141
+ SERVICE_LOADED: serviceLoaded,
142
+ STATUS: 'success',
143
+ LOG: 'logs/setup.log',
144
+ });
145
+ }
146
+
147
+ function setupLinux(
148
+ projectRoot: string,
149
+ nodePath: string,
150
+ homeDir: string,
151
+ ): void {
152
+ const serviceManager = getServiceManager();
153
+
154
+ if (serviceManager === 'systemd') {
155
+ setupSystemd(projectRoot, nodePath, homeDir);
156
+ } else {
157
+ // WSL without systemd or other Linux without systemd
158
+ setupNohupFallback(projectRoot, nodePath, homeDir);
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Kill any orphaned nanoclaw node processes left from previous runs or debugging.
164
+ * Prevents connection conflicts when two instances connect to the same channel simultaneously.
165
+ */
166
+ function killOrphanedProcesses(projectRoot: string): void {
167
+ try {
168
+ execSync(`pkill -f '${projectRoot}/dist/index\\.js' || true`, {
169
+ stdio: 'ignore',
170
+ });
171
+ logger.info('Stopped any orphaned nanoclaw processes');
172
+ } catch {
173
+ // pkill not available or no orphans
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Detect stale docker group membership in the user systemd session.
179
+ *
180
+ * When a user is added to the docker group mid-session, the user systemd
181
+ * daemon (user@UID.service) keeps the old group list from login time.
182
+ * Docker works in the terminal but not in the service context.
183
+ *
184
+ * Only relevant on Linux with user-level systemd (not root, not macOS, not WSL nohup).
185
+ */
186
+ function checkDockerGroupStale(): boolean {
187
+ try {
188
+ execSync('systemd-run --user --pipe --wait docker info', {
189
+ stdio: 'pipe',
190
+ timeout: 10000,
191
+ });
192
+ return false; // Docker works from systemd session
193
+ } catch {
194
+ // Check if docker works from the current shell (to distinguish stale group vs broken docker)
195
+ try {
196
+ execSync('docker info', { stdio: 'pipe', timeout: 5000 });
197
+ return true; // Works in shell but not systemd session → stale group
198
+ } catch {
199
+ return false; // Docker itself is not working, different issue
200
+ }
201
+ }
202
+ }
203
+
204
+ function setupSystemd(
205
+ projectRoot: string,
206
+ nodePath: string,
207
+ homeDir: string,
208
+ ): void {
209
+ const runningAsRoot = isRoot();
210
+
211
+ // Root uses system-level service, non-root uses user-level
212
+ let unitPath: string;
213
+ let systemctlPrefix: string;
214
+
215
+ if (runningAsRoot) {
216
+ unitPath = '/etc/systemd/system/nanoclaw.service';
217
+ systemctlPrefix = 'systemctl';
218
+ logger.info('Running as root — installing system-level systemd unit');
219
+ } else {
220
+ // Check if user-level systemd session is available
221
+ try {
222
+ execSync('systemctl --user daemon-reload', { stdio: 'pipe' });
223
+ } catch {
224
+ logger.warn(
225
+ 'systemd user session not available — falling back to nohup wrapper',
226
+ );
227
+ setupNohupFallback(projectRoot, nodePath, homeDir);
228
+ return;
229
+ }
230
+ const unitDir = path.join(homeDir, '.config', 'systemd', 'user');
231
+ fs.mkdirSync(unitDir, { recursive: true });
232
+ unitPath = path.join(unitDir, 'nanoclaw.service');
233
+ systemctlPrefix = 'systemctl --user';
234
+ }
235
+
236
+ const unit = `[Unit]
237
+ Description=NanoClaw Personal Assistant
238
+ After=network.target
239
+
240
+ [Service]
241
+ Type=simple
242
+ ExecStart=${nodePath} ${projectRoot}/dist/index.js
243
+ WorkingDirectory=${projectRoot}
244
+ Restart=always
245
+ RestartSec=5
246
+ KillMode=process
247
+ Environment=HOME=${homeDir}
248
+ Environment=PATH=/usr/local/bin:/usr/bin:/bin:${homeDir}/.local/bin
249
+ StandardOutput=append:${projectRoot}/logs/nanoclaw.log
250
+ StandardError=append:${projectRoot}/logs/nanoclaw.error.log
251
+
252
+ [Install]
253
+ WantedBy=${runningAsRoot ? 'multi-user.target' : 'default.target'}`;
254
+
255
+ fs.writeFileSync(unitPath, unit);
256
+ logger.info({ unitPath }, 'Wrote systemd unit');
257
+
258
+ // Detect stale docker group before starting (user systemd only)
259
+ const dockerGroupStale = !runningAsRoot && checkDockerGroupStale();
260
+ if (dockerGroupStale) {
261
+ logger.warn(
262
+ 'Docker group not active in systemd session — user was likely added to docker group mid-session',
263
+ );
264
+ }
265
+
266
+ // Kill orphaned nanoclaw processes to avoid channel connection conflicts
267
+ killOrphanedProcesses(projectRoot);
268
+
269
+ // Enable and start
270
+ try {
271
+ execSync(`${systemctlPrefix} daemon-reload`, { stdio: 'ignore' });
272
+ } catch (err) {
273
+ logger.error({ err }, 'systemctl daemon-reload failed');
274
+ }
275
+
276
+ try {
277
+ execSync(`${systemctlPrefix} enable nanoclaw`, { stdio: 'ignore' });
278
+ } catch (err) {
279
+ logger.error({ err }, 'systemctl enable failed');
280
+ }
281
+
282
+ try {
283
+ execSync(`${systemctlPrefix} start nanoclaw`, { stdio: 'ignore' });
284
+ } catch (err) {
285
+ logger.error({ err }, 'systemctl start failed');
286
+ }
287
+
288
+ // Verify
289
+ let serviceLoaded = false;
290
+ try {
291
+ execSync(`${systemctlPrefix} is-active nanoclaw`, { stdio: 'ignore' });
292
+ serviceLoaded = true;
293
+ } catch {
294
+ // Not active
295
+ }
296
+
297
+ emitStatus('SETUP_SERVICE', {
298
+ SERVICE_TYPE: runningAsRoot ? 'systemd-system' : 'systemd-user',
299
+ NODE_PATH: nodePath,
300
+ PROJECT_PATH: projectRoot,
301
+ UNIT_PATH: unitPath,
302
+ SERVICE_LOADED: serviceLoaded,
303
+ ...(dockerGroupStale ? { DOCKER_GROUP_STALE: true } : {}),
304
+ STATUS: 'success',
305
+ LOG: 'logs/setup.log',
306
+ });
307
+ }
308
+
309
+ function setupNohupFallback(
310
+ projectRoot: string,
311
+ nodePath: string,
312
+ homeDir: string,
313
+ ): void {
314
+ logger.warn('No systemd detected — generating nohup wrapper script');
315
+
316
+ const wrapperPath = path.join(projectRoot, 'start-nanoclaw.sh');
317
+ const pidFile = path.join(projectRoot, 'nanoclaw.pid');
318
+
319
+ const lines = [
320
+ '#!/bin/bash',
321
+ '# start-nanoclaw.sh — Start NanoClaw without systemd',
322
+ `# To stop: kill \\$(cat ${pidFile})`,
323
+ '',
324
+ 'set -euo pipefail',
325
+ '',
326
+ `cd ${JSON.stringify(projectRoot)}`,
327
+ '',
328
+ '# Stop existing instance if running',
329
+ `if [ -f ${JSON.stringify(pidFile)} ]; then`,
330
+ ` OLD_PID=$(cat ${JSON.stringify(pidFile)} 2>/dev/null || echo "")`,
331
+ ' if [ -n "$OLD_PID" ] && kill -0 "$OLD_PID" 2>/dev/null; then',
332
+ ' echo "Stopping existing NanoClaw (PID $OLD_PID)..."',
333
+ ' kill "$OLD_PID" 2>/dev/null || true',
334
+ ' sleep 2',
335
+ ' fi',
336
+ 'fi',
337
+ '',
338
+ 'echo "Starting NanoClaw..."',
339
+ `nohup ${JSON.stringify(nodePath)} ${JSON.stringify(projectRoot + '/dist/index.js')} \\`,
340
+ ` >> ${JSON.stringify(projectRoot + '/logs/nanoclaw.log')} \\`,
341
+ ` 2>> ${JSON.stringify(projectRoot + '/logs/nanoclaw.error.log')} &`,
342
+ '',
343
+ `echo $! > ${JSON.stringify(pidFile)}`,
344
+ 'echo "NanoClaw started (PID $!)"',
345
+ `echo "Logs: tail -f ${projectRoot}/logs/nanoclaw.log"`,
346
+ ];
347
+ const wrapper = lines.join('\n') + '\n';
348
+
349
+ fs.writeFileSync(wrapperPath, wrapper, { mode: 0o755 });
350
+ logger.info({ wrapperPath }, 'Wrote nohup wrapper script');
351
+
352
+ emitStatus('SETUP_SERVICE', {
353
+ SERVICE_TYPE: 'nohup',
354
+ NODE_PATH: nodePath,
355
+ PROJECT_PATH: projectRoot,
356
+ WRAPPER_PATH: wrapperPath,
357
+ SERVICE_LOADED: false,
358
+ FALLBACK: 'wsl_no_systemd',
359
+ STATUS: 'success',
360
+ LOG: 'logs/setup.log',
361
+ });
362
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Structured status block output for setup steps.
3
+ * Each step emits a block that the SKILL.md LLM can parse.
4
+ */
5
+
6
+ export function emitStatus(
7
+ step: string,
8
+ fields: Record<string, string | number | boolean>,
9
+ ): void {
10
+ const lines = [`=== NANOCLAW SETUP: ${step} ===`];
11
+ for (const [key, value] of Object.entries(fields)) {
12
+ lines.push(`${key}: ${value}`);
13
+ }
14
+ lines.push('=== END ===');
15
+ console.log(lines.join('\n'));
16
+ }
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Step: verify — End-to-end health check of the full installation.
3
+ * Replaces 09-verify.sh
4
+ *
5
+ * Uses better-sqlite3 directly (no sqlite3 CLI), platform-aware service checks.
6
+ */
7
+ import { execSync } from 'child_process';
8
+ import fs from 'fs';
9
+ import os from 'os';
10
+ import path from 'path';
11
+
12
+ import Database from 'better-sqlite3';
13
+
14
+ import { STORE_DIR } from '../src/config.js';
15
+ import { readEnvFile } from '../src/env.js';
16
+ import { logger } from '../src/logger.js';
17
+ import {
18
+ getPlatform,
19
+ getServiceManager,
20
+ hasSystemd,
21
+ isRoot,
22
+ } from './platform.js';
23
+ import { emitStatus } from './status.js';
24
+
25
+ export async function run(_args: string[]): Promise<void> {
26
+ const projectRoot = process.cwd();
27
+ const platform = getPlatform();
28
+ const homeDir = os.homedir();
29
+
30
+ logger.info('Starting verification');
31
+
32
+ // 1. Check service status
33
+ let service = 'not_found';
34
+ const mgr = getServiceManager();
35
+
36
+ if (mgr === 'launchd') {
37
+ try {
38
+ const output = execSync('launchctl list', { encoding: 'utf-8' });
39
+ if (output.includes('com.nanoclaw')) {
40
+ // Check if it has a PID (actually running)
41
+ const line = output.split('\n').find((l) => l.includes('com.nanoclaw'));
42
+ if (line) {
43
+ const pidField = line.trim().split(/\s+/)[0];
44
+ service = pidField !== '-' && pidField ? 'running' : 'stopped';
45
+ }
46
+ }
47
+ } catch {
48
+ // launchctl not available
49
+ }
50
+ } else if (mgr === 'systemd') {
51
+ const prefix = isRoot() ? 'systemctl' : 'systemctl --user';
52
+ try {
53
+ execSync(`${prefix} is-active nanoclaw`, { stdio: 'ignore' });
54
+ service = 'running';
55
+ } catch {
56
+ try {
57
+ const output = execSync(`${prefix} list-unit-files`, {
58
+ encoding: 'utf-8',
59
+ });
60
+ if (output.includes('nanoclaw')) {
61
+ service = 'stopped';
62
+ }
63
+ } catch {
64
+ // systemctl not available
65
+ }
66
+ }
67
+ } else {
68
+ // Check for nohup PID file
69
+ const pidFile = path.join(projectRoot, 'nanoclaw.pid');
70
+ if (fs.existsSync(pidFile)) {
71
+ try {
72
+ const raw = fs.readFileSync(pidFile, 'utf-8').trim();
73
+ const pid = Number(raw);
74
+ if (raw && Number.isInteger(pid) && pid > 0) {
75
+ process.kill(pid, 0);
76
+ service = 'running';
77
+ }
78
+ } catch {
79
+ service = 'stopped';
80
+ }
81
+ }
82
+ }
83
+ logger.info({ service }, 'Service status');
84
+
85
+ // 2. Check container runtime
86
+ let containerRuntime = 'none';
87
+ try {
88
+ execSync('command -v container', { stdio: 'ignore' });
89
+ containerRuntime = 'apple-container';
90
+ } catch {
91
+ try {
92
+ execSync('docker info', { stdio: 'ignore' });
93
+ containerRuntime = 'docker';
94
+ } catch {
95
+ // No runtime
96
+ }
97
+ }
98
+
99
+ // 3. Check credentials
100
+ let credentials = 'missing';
101
+ const envFile = path.join(projectRoot, '.env');
102
+ if (fs.existsSync(envFile)) {
103
+ const envContent = fs.readFileSync(envFile, 'utf-8');
104
+ if (/^(CLAUDE_CODE_OAUTH_TOKEN|ANTHROPIC_API_KEY)=/m.test(envContent)) {
105
+ credentials = 'configured';
106
+ }
107
+ }
108
+
109
+ // 4. Check channel auth (detect configured channels by credentials)
110
+ const envVars = readEnvFile([
111
+ 'TELEGRAM_BOT_TOKEN',
112
+ 'SLACK_BOT_TOKEN',
113
+ 'SLACK_APP_TOKEN',
114
+ 'DISCORD_BOT_TOKEN',
115
+ ]);
116
+
117
+ const channelAuth: Record<string, string> = {};
118
+
119
+ // WhatsApp: check for auth credentials on disk
120
+ const authDir = path.join(projectRoot, 'store', 'auth');
121
+ if (fs.existsSync(authDir) && fs.readdirSync(authDir).length > 0) {
122
+ channelAuth.whatsapp = 'authenticated';
123
+ }
124
+
125
+ // Token-based channels: check .env
126
+ if (process.env.TELEGRAM_BOT_TOKEN || envVars.TELEGRAM_BOT_TOKEN) {
127
+ channelAuth.telegram = 'configured';
128
+ }
129
+ if (
130
+ (process.env.SLACK_BOT_TOKEN || envVars.SLACK_BOT_TOKEN) &&
131
+ (process.env.SLACK_APP_TOKEN || envVars.SLACK_APP_TOKEN)
132
+ ) {
133
+ channelAuth.slack = 'configured';
134
+ }
135
+ if (process.env.DISCORD_BOT_TOKEN || envVars.DISCORD_BOT_TOKEN) {
136
+ channelAuth.discord = 'configured';
137
+ }
138
+
139
+ const configuredChannels = Object.keys(channelAuth);
140
+ const anyChannelConfigured = configuredChannels.length > 0;
141
+
142
+ // 5. Check registered groups (using better-sqlite3, not sqlite3 CLI)
143
+ let registeredGroups = 0;
144
+ const dbPath = path.join(STORE_DIR, 'messages.db');
145
+ if (fs.existsSync(dbPath)) {
146
+ try {
147
+ const db = new Database(dbPath, { readonly: true });
148
+ const row = db
149
+ .prepare('SELECT COUNT(*) as count FROM registered_groups')
150
+ .get() as { count: number };
151
+ registeredGroups = row.count;
152
+ db.close();
153
+ } catch {
154
+ // Table might not exist
155
+ }
156
+ }
157
+
158
+ // 6. Check mount allowlist
159
+ let mountAllowlist = 'missing';
160
+ if (
161
+ fs.existsSync(
162
+ path.join(homeDir, '.config', 'nanoclaw', 'mount-allowlist.json'),
163
+ )
164
+ ) {
165
+ mountAllowlist = 'configured';
166
+ }
167
+
168
+ // Determine overall status
169
+ const status =
170
+ service === 'running' &&
171
+ credentials !== 'missing' &&
172
+ anyChannelConfigured &&
173
+ registeredGroups > 0
174
+ ? 'success'
175
+ : 'failed';
176
+
177
+ logger.info({ status, channelAuth }, 'Verification complete');
178
+
179
+ emitStatus('VERIFY', {
180
+ SERVICE: service,
181
+ CONTAINER_RUNTIME: containerRuntime,
182
+ CREDENTIALS: credentials,
183
+ CONFIGURED_CHANNELS: configuredChannels.join(','),
184
+ CHANNEL_AUTH: JSON.stringify(channelAuth),
185
+ REGISTERED_GROUPS: registeredGroups,
186
+ MOUNT_ALLOWLIST: mountAllowlist,
187
+ STATUS: status,
188
+ LOG: 'logs/setup.log',
189
+ });
190
+
191
+ if (status === 'failed') process.exit(1);
192
+ }