@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,229 @@
1
+ /**
2
+ * Step: groups — Fetch group metadata from messaging platforms, write to DB.
3
+ * WhatsApp requires an upfront sync (Baileys groupFetchAllParticipating).
4
+ * Other channels discover group names at runtime — this step auto-skips for them.
5
+ * Replaces 05-sync-groups.sh + 05b-list-groups.sh
6
+ */
7
+ import { execSync } from 'child_process';
8
+ import fs from 'fs';
9
+ import path from 'path';
10
+
11
+ import Database from 'better-sqlite3';
12
+
13
+ import { STORE_DIR } from '../src/config.js';
14
+ import { logger } from '../src/logger.js';
15
+ import { emitStatus } from './status.js';
16
+
17
+ function parseArgs(args: string[]): { list: boolean; limit: number } {
18
+ let list = false;
19
+ let limit = 30;
20
+ for (let i = 0; i < args.length; i++) {
21
+ if (args[i] === '--list') list = true;
22
+ if (args[i] === '--limit' && args[i + 1]) {
23
+ limit = parseInt(args[i + 1], 10);
24
+ i++;
25
+ }
26
+ }
27
+ return { list, limit };
28
+ }
29
+
30
+ export async function run(args: string[]): Promise<void> {
31
+ const projectRoot = process.cwd();
32
+ const { list, limit } = parseArgs(args);
33
+
34
+ if (list) {
35
+ await listGroups(limit);
36
+ return;
37
+ }
38
+
39
+ await syncGroups(projectRoot);
40
+ }
41
+
42
+ async function listGroups(limit: number): Promise<void> {
43
+ const dbPath = path.join(STORE_DIR, 'messages.db');
44
+
45
+ if (!fs.existsSync(dbPath)) {
46
+ console.error('ERROR: database not found');
47
+ process.exit(1);
48
+ }
49
+
50
+ const db = new Database(dbPath, { readonly: true });
51
+ const rows = db
52
+ .prepare(
53
+ `SELECT jid, name FROM chats
54
+ WHERE jid LIKE '%@g.us' AND jid <> '__group_sync__' AND name <> jid
55
+ ORDER BY last_message_time DESC
56
+ LIMIT ?`,
57
+ )
58
+ .all(limit) as Array<{ jid: string; name: string }>;
59
+ db.close();
60
+
61
+ for (const row of rows) {
62
+ console.log(`${row.jid}|${row.name}`);
63
+ }
64
+ }
65
+
66
+ async function syncGroups(projectRoot: string): Promise<void> {
67
+ // Only WhatsApp needs an upfront group sync; other channels resolve names at runtime.
68
+ // Detect WhatsApp by checking for auth credentials on disk.
69
+ const authDir = path.join(projectRoot, 'store', 'auth');
70
+ const hasWhatsAppAuth =
71
+ fs.existsSync(authDir) && fs.readdirSync(authDir).length > 0;
72
+
73
+ if (!hasWhatsAppAuth) {
74
+ logger.info('WhatsApp auth not found — skipping group sync');
75
+ emitStatus('SYNC_GROUPS', {
76
+ BUILD: 'skipped',
77
+ SYNC: 'skipped',
78
+ GROUPS_IN_DB: 0,
79
+ REASON: 'whatsapp_not_configured',
80
+ STATUS: 'success',
81
+ LOG: 'logs/setup.log',
82
+ });
83
+ return;
84
+ }
85
+
86
+ // Build TypeScript first
87
+ logger.info('Building TypeScript');
88
+ let buildOk = false;
89
+ try {
90
+ execSync('npm run build', {
91
+ cwd: projectRoot,
92
+ stdio: ['ignore', 'pipe', 'pipe'],
93
+ });
94
+ buildOk = true;
95
+ logger.info('Build succeeded');
96
+ } catch {
97
+ logger.error('Build failed');
98
+ emitStatus('SYNC_GROUPS', {
99
+ BUILD: 'failed',
100
+ SYNC: 'skipped',
101
+ GROUPS_IN_DB: 0,
102
+ STATUS: 'failed',
103
+ ERROR: 'build_failed',
104
+ LOG: 'logs/setup.log',
105
+ });
106
+ process.exit(1);
107
+ }
108
+
109
+ // Run sync script via a temp file to avoid shell escaping issues with node -e
110
+ logger.info('Fetching group metadata');
111
+ let syncOk = false;
112
+ try {
113
+ const syncScript = `
114
+ import makeWASocket, { useMultiFileAuthState, makeCacheableSignalKeyStore, Browsers } from '@whiskeysockets/baileys';
115
+ import pino from 'pino';
116
+ import path from 'path';
117
+ import fs from 'fs';
118
+ import Database from 'better-sqlite3';
119
+
120
+ const logger = pino({ level: 'silent' });
121
+ const authDir = path.join('store', 'auth');
122
+ const dbPath = path.join('store', 'messages.db');
123
+
124
+ if (!fs.existsSync(authDir)) {
125
+ console.error('NO_AUTH');
126
+ process.exit(1);
127
+ }
128
+
129
+ const db = new Database(dbPath);
130
+ db.pragma('journal_mode = WAL');
131
+ db.exec('CREATE TABLE IF NOT EXISTS chats (jid TEXT PRIMARY KEY, name TEXT, last_message_time TEXT)');
132
+
133
+ const upsert = db.prepare(
134
+ 'INSERT INTO chats (jid, name, last_message_time) VALUES (?, ?, ?) ON CONFLICT(jid) DO UPDATE SET name = excluded.name'
135
+ );
136
+
137
+ const { state, saveCreds } = await useMultiFileAuthState(authDir);
138
+
139
+ const sock = makeWASocket({
140
+ auth: { creds: state.creds, keys: makeCacheableSignalKeyStore(state.keys, logger) },
141
+ printQRInTerminal: false,
142
+ logger,
143
+ browser: Browsers.macOS('Chrome'),
144
+ });
145
+
146
+ const timeout = setTimeout(() => {
147
+ console.error('TIMEOUT');
148
+ process.exit(1);
149
+ }, 30000);
150
+
151
+ sock.ev.on('creds.update', saveCreds);
152
+
153
+ sock.ev.on('connection.update', async (update) => {
154
+ if (update.connection === 'open') {
155
+ try {
156
+ const groups = await sock.groupFetchAllParticipating();
157
+ const now = new Date().toISOString();
158
+ let count = 0;
159
+ for (const [jid, metadata] of Object.entries(groups)) {
160
+ if (metadata.subject) {
161
+ upsert.run(jid, metadata.subject, now);
162
+ count++;
163
+ }
164
+ }
165
+ console.log('SYNCED:' + count);
166
+ } catch (err) {
167
+ console.error('FETCH_ERROR:' + err.message);
168
+ } finally {
169
+ clearTimeout(timeout);
170
+ sock.end(undefined);
171
+ db.close();
172
+ process.exit(0);
173
+ }
174
+ } else if (update.connection === 'close') {
175
+ clearTimeout(timeout);
176
+ console.error('CONNECTION_CLOSED');
177
+ process.exit(1);
178
+ }
179
+ });
180
+ `;
181
+
182
+ const tmpScript = path.join(projectRoot, '.tmp-group-sync.mjs');
183
+ fs.writeFileSync(tmpScript, syncScript, 'utf-8');
184
+ try {
185
+ const output = execSync(`node ${tmpScript}`, {
186
+ cwd: projectRoot,
187
+ encoding: 'utf-8',
188
+ timeout: 45000,
189
+ stdio: ['ignore', 'pipe', 'pipe'],
190
+ });
191
+ syncOk = output.includes('SYNCED:');
192
+ logger.info({ output: output.trim() }, 'Sync output');
193
+ } finally {
194
+ try { fs.unlinkSync(tmpScript); } catch { /* ignore cleanup errors */ }
195
+ }
196
+ } catch (err) {
197
+ logger.error({ err }, 'Sync failed');
198
+ }
199
+
200
+ // Count groups in DB using better-sqlite3 (no sqlite3 CLI)
201
+ let groupsInDb = 0;
202
+ const dbPath = path.join(STORE_DIR, 'messages.db');
203
+ if (fs.existsSync(dbPath)) {
204
+ try {
205
+ const db = new Database(dbPath, { readonly: true });
206
+ const row = db
207
+ .prepare(
208
+ "SELECT COUNT(*) as count FROM chats WHERE jid LIKE '%@g.us' AND jid <> '__group_sync__'",
209
+ )
210
+ .get() as { count: number };
211
+ groupsInDb = row.count;
212
+ db.close();
213
+ } catch {
214
+ // DB may not exist yet
215
+ }
216
+ }
217
+
218
+ const status = syncOk ? 'success' : 'failed';
219
+
220
+ emitStatus('SYNC_GROUPS', {
221
+ BUILD: buildOk ? 'success' : 'failed',
222
+ SYNC: syncOk ? 'success' : 'failed',
223
+ GROUPS_IN_DB: groupsInDb,
224
+ STATUS: status,
225
+ LOG: 'logs/setup.log',
226
+ });
227
+
228
+ if (status === 'failed') process.exit(1);
229
+ }
package/setup/index.ts ADDED
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Setup CLI entry point.
3
+ * Usage: npx tsx setup/index.ts --step <name> [args...]
4
+ */
5
+ import { logger } from '../src/logger.js';
6
+ import { emitStatus } from './status.js';
7
+
8
+ const STEPS: Record<
9
+ string,
10
+ () => Promise<{ run: (args: string[]) => Promise<void> }>
11
+ > = {
12
+ environment: () => import('./environment.js'),
13
+ container: () => import('./container.js'),
14
+ groups: () => import('./groups.js'),
15
+ register: () => import('./register.js'),
16
+ mounts: () => import('./mounts.js'),
17
+ service: () => import('./service.js'),
18
+ verify: () => import('./verify.js'),
19
+ };
20
+
21
+ async function main(): Promise<void> {
22
+ const args = process.argv.slice(2);
23
+ const stepIdx = args.indexOf('--step');
24
+
25
+ if (stepIdx === -1 || !args[stepIdx + 1]) {
26
+ console.error(
27
+ `Usage: npx tsx setup/index.ts --step <${Object.keys(STEPS).join('|')}> [args...]`,
28
+ );
29
+ process.exit(1);
30
+ }
31
+
32
+ const stepName = args[stepIdx + 1];
33
+ const stepArgs = args.filter(
34
+ (a, i) => i !== stepIdx && i !== stepIdx + 1 && a !== '--',
35
+ );
36
+
37
+ const loader = STEPS[stepName];
38
+ if (!loader) {
39
+ console.error(`Unknown step: ${stepName}`);
40
+ console.error(`Available steps: ${Object.keys(STEPS).join(', ')}`);
41
+ process.exit(1);
42
+ }
43
+
44
+ try {
45
+ const mod = await loader();
46
+ await mod.run(stepArgs);
47
+ } catch (err) {
48
+ const message = err instanceof Error ? err.message : String(err);
49
+ logger.error({ err, step: stepName }, 'Setup step failed');
50
+ emitStatus(stepName.toUpperCase(), {
51
+ STATUS: 'failed',
52
+ ERROR: message,
53
+ });
54
+ process.exit(1);
55
+ }
56
+ }
57
+
58
+ main();
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Step: mounts — Write mount allowlist config file.
3
+ * Replaces 07-configure-mounts.sh
4
+ */
5
+ import fs from 'fs';
6
+ import path from 'path';
7
+ import os from 'os';
8
+
9
+ import { logger } from '../src/logger.js';
10
+ import { isRoot } from './platform.js';
11
+ import { emitStatus } from './status.js';
12
+
13
+ function parseArgs(args: string[]): { empty: boolean; json: string } {
14
+ let empty = false;
15
+ let json = '';
16
+ for (let i = 0; i < args.length; i++) {
17
+ if (args[i] === '--empty') empty = true;
18
+ if (args[i] === '--json' && args[i + 1]) {
19
+ json = args[i + 1];
20
+ i++;
21
+ }
22
+ }
23
+ return { empty, json };
24
+ }
25
+
26
+ export async function run(args: string[]): Promise<void> {
27
+ const { empty, json } = parseArgs(args);
28
+ const homeDir = os.homedir();
29
+ const configDir = path.join(homeDir, '.config', 'nanoclaw');
30
+ const configFile = path.join(configDir, 'mount-allowlist.json');
31
+
32
+ if (isRoot()) {
33
+ logger.warn(
34
+ 'Running as root — mount allowlist will be written to root home directory',
35
+ );
36
+ }
37
+
38
+ fs.mkdirSync(configDir, { recursive: true });
39
+
40
+ let allowedRoots = 0;
41
+ let nonMainReadOnly = 'true';
42
+
43
+ if (empty) {
44
+ logger.info('Writing empty mount allowlist');
45
+ const emptyConfig = {
46
+ allowedRoots: [],
47
+ blockedPatterns: [],
48
+ nonMainReadOnly: true,
49
+ };
50
+ fs.writeFileSync(configFile, JSON.stringify(emptyConfig, null, 2) + '\n');
51
+ } else if (json) {
52
+ // Validate JSON with JSON.parse (not piped through shell)
53
+ let parsed: { allowedRoots?: unknown[]; nonMainReadOnly?: boolean };
54
+ try {
55
+ parsed = JSON.parse(json);
56
+ } catch {
57
+ logger.error('Invalid JSON input');
58
+ emitStatus('CONFIGURE_MOUNTS', {
59
+ PATH: configFile,
60
+ ALLOWED_ROOTS: 0,
61
+ NON_MAIN_READ_ONLY: 'unknown',
62
+ STATUS: 'failed',
63
+ ERROR: 'invalid_json',
64
+ LOG: 'logs/setup.log',
65
+ });
66
+ process.exit(4);
67
+ return; // unreachable but satisfies TS
68
+ }
69
+
70
+ fs.writeFileSync(configFile, JSON.stringify(parsed, null, 2) + '\n');
71
+ allowedRoots = Array.isArray(parsed.allowedRoots)
72
+ ? parsed.allowedRoots.length
73
+ : 0;
74
+ nonMainReadOnly = parsed.nonMainReadOnly === false ? 'false' : 'true';
75
+ } else {
76
+ // Read from stdin
77
+ logger.info('Reading mount allowlist from stdin');
78
+ const input = fs.readFileSync(0, 'utf-8');
79
+ let parsed: { allowedRoots?: unknown[]; nonMainReadOnly?: boolean };
80
+ try {
81
+ parsed = JSON.parse(input);
82
+ } catch {
83
+ logger.error('Invalid JSON from stdin');
84
+ emitStatus('CONFIGURE_MOUNTS', {
85
+ PATH: configFile,
86
+ ALLOWED_ROOTS: 0,
87
+ NON_MAIN_READ_ONLY: 'unknown',
88
+ STATUS: 'failed',
89
+ ERROR: 'invalid_json',
90
+ LOG: 'logs/setup.log',
91
+ });
92
+ process.exit(4);
93
+ return;
94
+ }
95
+
96
+ fs.writeFileSync(configFile, JSON.stringify(parsed, null, 2) + '\n');
97
+ allowedRoots = Array.isArray(parsed.allowedRoots)
98
+ ? parsed.allowedRoots.length
99
+ : 0;
100
+ nonMainReadOnly = parsed.nonMainReadOnly === false ? 'false' : 'true';
101
+ }
102
+
103
+ logger.info(
104
+ { configFile, allowedRoots, nonMainReadOnly },
105
+ 'Allowlist configured',
106
+ );
107
+
108
+ emitStatus('CONFIGURE_MOUNTS', {
109
+ PATH: configFile,
110
+ ALLOWED_ROOTS: allowedRoots,
111
+ NON_MAIN_READ_ONLY: nonMainReadOnly,
112
+ STATUS: 'success',
113
+ LOG: 'logs/setup.log',
114
+ });
115
+ }
@@ -0,0 +1,120 @@
1
+ import { describe, it, expect } from 'vitest';
2
+
3
+ import {
4
+ getPlatform,
5
+ isWSL,
6
+ isRoot,
7
+ isHeadless,
8
+ hasSystemd,
9
+ getServiceManager,
10
+ commandExists,
11
+ getNodeVersion,
12
+ getNodeMajorVersion,
13
+ } from './platform.js';
14
+
15
+ // --- getPlatform ---
16
+
17
+ describe('getPlatform', () => {
18
+ it('returns a valid platform string', () => {
19
+ const result = getPlatform();
20
+ expect(['macos', 'linux', 'unknown']).toContain(result);
21
+ });
22
+ });
23
+
24
+ // --- isWSL ---
25
+
26
+ describe('isWSL', () => {
27
+ it('returns a boolean', () => {
28
+ expect(typeof isWSL()).toBe('boolean');
29
+ });
30
+
31
+ it('checks /proc/version for WSL markers', () => {
32
+ // On non-WSL Linux, should return false
33
+ // On WSL, should return true
34
+ // Just verify it doesn't throw
35
+ const result = isWSL();
36
+ expect(typeof result).toBe('boolean');
37
+ });
38
+ });
39
+
40
+ // --- isRoot ---
41
+
42
+ describe('isRoot', () => {
43
+ it('returns a boolean', () => {
44
+ expect(typeof isRoot()).toBe('boolean');
45
+ });
46
+ });
47
+
48
+ // --- isHeadless ---
49
+
50
+ describe('isHeadless', () => {
51
+ it('returns a boolean', () => {
52
+ expect(typeof isHeadless()).toBe('boolean');
53
+ });
54
+ });
55
+
56
+ // --- hasSystemd ---
57
+
58
+ describe('hasSystemd', () => {
59
+ it('returns a boolean', () => {
60
+ expect(typeof hasSystemd()).toBe('boolean');
61
+ });
62
+
63
+ it('checks /proc/1/comm', () => {
64
+ // On systemd systems, should return true
65
+ // Just verify it doesn't throw
66
+ const result = hasSystemd();
67
+ expect(typeof result).toBe('boolean');
68
+ });
69
+ });
70
+
71
+ // --- getServiceManager ---
72
+
73
+ describe('getServiceManager', () => {
74
+ it('returns a valid service manager', () => {
75
+ const result = getServiceManager();
76
+ expect(['launchd', 'systemd', 'none']).toContain(result);
77
+ });
78
+
79
+ it('matches the detected platform', () => {
80
+ const platform = getPlatform();
81
+ const result = getServiceManager();
82
+ if (platform === 'macos') {
83
+ expect(result).toBe('launchd');
84
+ } else {
85
+ expect(['systemd', 'none']).toContain(result);
86
+ }
87
+ });
88
+ });
89
+
90
+ // --- commandExists ---
91
+
92
+ describe('commandExists', () => {
93
+ it('returns true for node', () => {
94
+ expect(commandExists('node')).toBe(true);
95
+ });
96
+
97
+ it('returns false for nonexistent command', () => {
98
+ expect(commandExists('this_command_does_not_exist_xyz_123')).toBe(false);
99
+ });
100
+ });
101
+
102
+ // --- getNodeVersion ---
103
+
104
+ describe('getNodeVersion', () => {
105
+ it('returns a version string', () => {
106
+ const version = getNodeVersion();
107
+ expect(version).not.toBeNull();
108
+ expect(version).toMatch(/^\d+\.\d+\.\d+/);
109
+ });
110
+ });
111
+
112
+ // --- getNodeMajorVersion ---
113
+
114
+ describe('getNodeMajorVersion', () => {
115
+ it('returns at least 20', () => {
116
+ const major = getNodeMajorVersion();
117
+ expect(major).not.toBeNull();
118
+ expect(major!).toBeGreaterThanOrEqual(20);
119
+ });
120
+ });
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Cross-platform detection utilities for NanoClaw setup.
3
+ */
4
+ import { execSync } from 'child_process';
5
+ import fs from 'fs';
6
+ import os from 'os';
7
+
8
+ export type Platform = 'macos' | 'linux' | 'unknown';
9
+ export type ServiceManager = 'launchd' | 'systemd' | 'none';
10
+
11
+ export function getPlatform(): Platform {
12
+ const platform = os.platform();
13
+ if (platform === 'darwin') return 'macos';
14
+ if (platform === 'linux') return 'linux';
15
+ return 'unknown';
16
+ }
17
+
18
+ export function isWSL(): boolean {
19
+ if (os.platform() !== 'linux') return false;
20
+ try {
21
+ const release = fs.readFileSync('/proc/version', 'utf-8').toLowerCase();
22
+ return release.includes('microsoft') || release.includes('wsl');
23
+ } catch {
24
+ return false;
25
+ }
26
+ }
27
+
28
+ export function isRoot(): boolean {
29
+ return process.getuid?.() === 0;
30
+ }
31
+
32
+ export function isHeadless(): boolean {
33
+ // No display server available
34
+ if (getPlatform() === 'linux') {
35
+ return !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY;
36
+ }
37
+ // macOS is never headless in practice (even SSH sessions can open URLs)
38
+ return false;
39
+ }
40
+
41
+ export function hasSystemd(): boolean {
42
+ if (getPlatform() !== 'linux') return false;
43
+ try {
44
+ // Check if systemd is PID 1
45
+ const init = fs.readFileSync('/proc/1/comm', 'utf-8').trim();
46
+ return init === 'systemd';
47
+ } catch {
48
+ return false;
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Open a URL in the default browser, cross-platform.
54
+ * Returns true if the command was attempted, false if no method available.
55
+ */
56
+ export function openBrowser(url: string): boolean {
57
+ try {
58
+ const platform = getPlatform();
59
+ if (platform === 'macos') {
60
+ execSync(`open ${JSON.stringify(url)}`, { stdio: 'ignore' });
61
+ return true;
62
+ }
63
+ if (platform === 'linux') {
64
+ // Try xdg-open first, then wslview for WSL
65
+ if (commandExists('xdg-open')) {
66
+ execSync(`xdg-open ${JSON.stringify(url)}`, { stdio: 'ignore' });
67
+ return true;
68
+ }
69
+ if (isWSL() && commandExists('wslview')) {
70
+ execSync(`wslview ${JSON.stringify(url)}`, { stdio: 'ignore' });
71
+ return true;
72
+ }
73
+ // WSL without wslview: try cmd.exe
74
+ if (isWSL()) {
75
+ try {
76
+ execSync(`cmd.exe /c start "" ${JSON.stringify(url)}`, {
77
+ stdio: 'ignore',
78
+ });
79
+ return true;
80
+ } catch {
81
+ // cmd.exe not available
82
+ }
83
+ }
84
+ }
85
+ } catch {
86
+ // Command failed
87
+ }
88
+ return false;
89
+ }
90
+
91
+ export function getServiceManager(): ServiceManager {
92
+ const platform = getPlatform();
93
+ if (platform === 'macos') return 'launchd';
94
+ if (platform === 'linux') {
95
+ if (hasSystemd()) return 'systemd';
96
+ return 'none';
97
+ }
98
+ return 'none';
99
+ }
100
+
101
+ export function getNodePath(): string {
102
+ try {
103
+ return execSync('command -v node', { encoding: 'utf-8' }).trim();
104
+ } catch {
105
+ return process.execPath;
106
+ }
107
+ }
108
+
109
+ export function commandExists(name: string): boolean {
110
+ try {
111
+ execSync(`command -v ${name}`, { stdio: 'ignore' });
112
+ return true;
113
+ } catch {
114
+ return false;
115
+ }
116
+ }
117
+
118
+ export function getNodeVersion(): string | null {
119
+ try {
120
+ const version = execSync('node --version', { encoding: 'utf-8' }).trim();
121
+ return version.replace(/^v/, '');
122
+ } catch {
123
+ return null;
124
+ }
125
+ }
126
+
127
+ export function getNodeMajorVersion(): number | null {
128
+ const version = getNodeVersion();
129
+ if (!version) return null;
130
+ const major = parseInt(version.split('.')[0], 10);
131
+ return isNaN(major) ? null : major;
132
+ }