@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,257 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+
3
+ import Database from 'better-sqlite3';
4
+
5
+ /**
6
+ * Tests for the register step.
7
+ *
8
+ * Verifies: parameterized SQL (no injection), file templating,
9
+ * apostrophe in names, .env updates.
10
+ */
11
+
12
+ function createTestDb(): Database.Database {
13
+ const db = new Database(':memory:');
14
+ db.exec(`CREATE TABLE IF NOT EXISTS registered_groups (
15
+ jid TEXT PRIMARY KEY,
16
+ name TEXT NOT NULL,
17
+ folder TEXT NOT NULL UNIQUE,
18
+ trigger_pattern TEXT NOT NULL,
19
+ added_at TEXT NOT NULL,
20
+ container_config TEXT,
21
+ requires_trigger INTEGER DEFAULT 1,
22
+ is_main INTEGER DEFAULT 0
23
+ )`);
24
+ return db;
25
+ }
26
+
27
+ describe('parameterized SQL registration', () => {
28
+ let db: Database.Database;
29
+
30
+ beforeEach(() => {
31
+ db = createTestDb();
32
+ });
33
+
34
+ it('registers a group with parameterized query', () => {
35
+ db.prepare(
36
+ `INSERT OR REPLACE INTO registered_groups
37
+ (jid, name, folder, trigger_pattern, added_at, container_config, requires_trigger)
38
+ VALUES (?, ?, ?, ?, ?, NULL, ?)`,
39
+ ).run(
40
+ '123@g.us',
41
+ 'Test Group',
42
+ 'test-group',
43
+ '@Andy',
44
+ '2024-01-01T00:00:00.000Z',
45
+ 1,
46
+ );
47
+
48
+ const row = db
49
+ .prepare('SELECT * FROM registered_groups WHERE jid = ?')
50
+ .get('123@g.us') as {
51
+ jid: string;
52
+ name: string;
53
+ folder: string;
54
+ trigger_pattern: string;
55
+ requires_trigger: number;
56
+ };
57
+
58
+ expect(row.jid).toBe('123@g.us');
59
+ expect(row.name).toBe('Test Group');
60
+ expect(row.folder).toBe('test-group');
61
+ expect(row.trigger_pattern).toBe('@Andy');
62
+ expect(row.requires_trigger).toBe(1);
63
+ });
64
+
65
+ it('handles apostrophes in group names safely', () => {
66
+ const name = "O'Brien's Group";
67
+
68
+ db.prepare(
69
+ `INSERT OR REPLACE INTO registered_groups
70
+ (jid, name, folder, trigger_pattern, added_at, container_config, requires_trigger)
71
+ VALUES (?, ?, ?, ?, ?, NULL, ?)`,
72
+ ).run(
73
+ '456@g.us',
74
+ name,
75
+ 'obriens-group',
76
+ '@Andy',
77
+ '2024-01-01T00:00:00.000Z',
78
+ 0,
79
+ );
80
+
81
+ const row = db
82
+ .prepare('SELECT name FROM registered_groups WHERE jid = ?')
83
+ .get('456@g.us') as {
84
+ name: string;
85
+ };
86
+
87
+ expect(row.name).toBe(name);
88
+ });
89
+
90
+ it('prevents SQL injection in JID field', () => {
91
+ const maliciousJid = "'; DROP TABLE registered_groups; --";
92
+
93
+ db.prepare(
94
+ `INSERT OR REPLACE INTO registered_groups
95
+ (jid, name, folder, trigger_pattern, added_at, container_config, requires_trigger)
96
+ VALUES (?, ?, ?, ?, ?, NULL, ?)`,
97
+ ).run(maliciousJid, 'Evil', 'evil', '@Andy', '2024-01-01T00:00:00.000Z', 1);
98
+
99
+ // Table should still exist and have the row
100
+ const count = db
101
+ .prepare('SELECT COUNT(*) as count FROM registered_groups')
102
+ .get() as {
103
+ count: number;
104
+ };
105
+ expect(count.count).toBe(1);
106
+
107
+ const row = db.prepare('SELECT jid FROM registered_groups').get() as {
108
+ jid: string;
109
+ };
110
+ expect(row.jid).toBe(maliciousJid);
111
+ });
112
+
113
+ it('handles requiresTrigger=false', () => {
114
+ db.prepare(
115
+ `INSERT OR REPLACE INTO registered_groups
116
+ (jid, name, folder, trigger_pattern, added_at, container_config, requires_trigger)
117
+ VALUES (?, ?, ?, ?, ?, NULL, ?)`,
118
+ ).run(
119
+ '789@s.whatsapp.net',
120
+ 'Personal',
121
+ 'main',
122
+ '@Andy',
123
+ '2024-01-01T00:00:00.000Z',
124
+ 0,
125
+ );
126
+
127
+ const row = db
128
+ .prepare('SELECT requires_trigger FROM registered_groups WHERE jid = ?')
129
+ .get('789@s.whatsapp.net') as { requires_trigger: number };
130
+
131
+ expect(row.requires_trigger).toBe(0);
132
+ });
133
+
134
+ it('stores is_main flag', () => {
135
+ db.prepare(
136
+ `INSERT OR REPLACE INTO registered_groups
137
+ (jid, name, folder, trigger_pattern, added_at, container_config, requires_trigger, is_main)
138
+ VALUES (?, ?, ?, ?, ?, NULL, ?, ?)`,
139
+ ).run(
140
+ '789@s.whatsapp.net',
141
+ 'Personal',
142
+ 'whatsapp_main',
143
+ '@Andy',
144
+ '2024-01-01T00:00:00.000Z',
145
+ 0,
146
+ 1,
147
+ );
148
+
149
+ const row = db
150
+ .prepare('SELECT is_main FROM registered_groups WHERE jid = ?')
151
+ .get('789@s.whatsapp.net') as { is_main: number };
152
+
153
+ expect(row.is_main).toBe(1);
154
+ });
155
+
156
+ it('defaults is_main to 0', () => {
157
+ db.prepare(
158
+ `INSERT OR REPLACE INTO registered_groups
159
+ (jid, name, folder, trigger_pattern, added_at, container_config, requires_trigger)
160
+ VALUES (?, ?, ?, ?, ?, NULL, ?)`,
161
+ ).run(
162
+ '123@g.us',
163
+ 'Some Group',
164
+ 'whatsapp_some-group',
165
+ '@Andy',
166
+ '2024-01-01T00:00:00.000Z',
167
+ 1,
168
+ );
169
+
170
+ const row = db
171
+ .prepare('SELECT is_main FROM registered_groups WHERE jid = ?')
172
+ .get('123@g.us') as { is_main: number };
173
+
174
+ expect(row.is_main).toBe(0);
175
+ });
176
+
177
+ it('upserts on conflict', () => {
178
+ const stmt = db.prepare(
179
+ `INSERT OR REPLACE INTO registered_groups
180
+ (jid, name, folder, trigger_pattern, added_at, container_config, requires_trigger)
181
+ VALUES (?, ?, ?, ?, ?, NULL, ?)`,
182
+ );
183
+
184
+ stmt.run(
185
+ '123@g.us',
186
+ 'Original',
187
+ 'main',
188
+ '@Andy',
189
+ '2024-01-01T00:00:00.000Z',
190
+ 1,
191
+ );
192
+ stmt.run(
193
+ '123@g.us',
194
+ 'Updated',
195
+ 'main',
196
+ '@Bot',
197
+ '2024-02-01T00:00:00.000Z',
198
+ 0,
199
+ );
200
+
201
+ const rows = db.prepare('SELECT * FROM registered_groups').all();
202
+ expect(rows).toHaveLength(1);
203
+
204
+ const row = rows[0] as {
205
+ name: string;
206
+ trigger_pattern: string;
207
+ requires_trigger: number;
208
+ };
209
+ expect(row.name).toBe('Updated');
210
+ expect(row.trigger_pattern).toBe('@Bot');
211
+ expect(row.requires_trigger).toBe(0);
212
+ });
213
+ });
214
+
215
+ describe('file templating', () => {
216
+ it('replaces assistant name in CLAUDE.md content', () => {
217
+ let content = '# Andy\n\nYou are Andy, a personal assistant.';
218
+
219
+ content = content.replace(/^# Andy$/m, '# Nova');
220
+ content = content.replace(/You are Andy/g, 'You are Nova');
221
+
222
+ expect(content).toBe('# Nova\n\nYou are Nova, a personal assistant.');
223
+ });
224
+
225
+ it('handles names with special regex characters', () => {
226
+ let content = '# Andy\n\nYou are Andy.';
227
+
228
+ const newName = 'C.L.A.U.D.E';
229
+ content = content.replace(/^# Andy$/m, `# ${newName}`);
230
+ content = content.replace(/You are Andy/g, `You are ${newName}`);
231
+
232
+ expect(content).toContain('# C.L.A.U.D.E');
233
+ expect(content).toContain('You are C.L.A.U.D.E.');
234
+ });
235
+
236
+ it('updates .env ASSISTANT_NAME line', () => {
237
+ let envContent = 'SOME_KEY=value\nASSISTANT_NAME="Andy"\nOTHER=test';
238
+
239
+ envContent = envContent.replace(
240
+ /^ASSISTANT_NAME=.*$/m,
241
+ 'ASSISTANT_NAME="Nova"',
242
+ );
243
+
244
+ expect(envContent).toContain('ASSISTANT_NAME="Nova"');
245
+ expect(envContent).toContain('SOME_KEY=value');
246
+ });
247
+
248
+ it('appends ASSISTANT_NAME to .env if not present', () => {
249
+ let envContent = 'SOME_KEY=value\n';
250
+
251
+ if (!envContent.includes('ASSISTANT_NAME=')) {
252
+ envContent += '\nASSISTANT_NAME="Nova"';
253
+ }
254
+
255
+ expect(envContent).toContain('ASSISTANT_NAME="Nova"');
256
+ });
257
+ });
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Step: register — Write channel registration config, create group folders.
3
+ *
4
+ * Accepts --channel to specify the messaging platform (whatsapp, telegram, slack, discord).
5
+ * Uses parameterized SQL queries to prevent injection.
6
+ */
7
+ import fs from 'fs';
8
+ import path from 'path';
9
+
10
+ import { STORE_DIR } from '../src/config.ts';
11
+ import { initDatabase, setRegisteredGroup } from '../src/db.ts';
12
+ import { isValidGroupFolder } from '../src/group-folder.ts';
13
+ import { logger } from '../src/logger.ts';
14
+ import { emitStatus } from './status.ts';
15
+
16
+ interface RegisterArgs {
17
+ jid: string;
18
+ name: string;
19
+ trigger: string;
20
+ folder: string;
21
+ channel: string;
22
+ requiresTrigger: boolean;
23
+ isMain: boolean;
24
+ assistantName: string;
25
+ }
26
+
27
+ function parseArgs(args: string[]): RegisterArgs {
28
+ const result: RegisterArgs = {
29
+ jid: '',
30
+ name: '',
31
+ trigger: '',
32
+ folder: '',
33
+ channel: 'whatsapp', // backward-compat: pre-refactor installs omit --channel
34
+ requiresTrigger: true,
35
+ isMain: false,
36
+ assistantName: 'Andy',
37
+ };
38
+
39
+ for (let i = 0; i < args.length; i++) {
40
+ switch (args[i]) {
41
+ case '--jid':
42
+ result.jid = args[++i] || '';
43
+ break;
44
+ case '--name':
45
+ result.name = args[++i] || '';
46
+ break;
47
+ case '--trigger':
48
+ result.trigger = args[++i] || '';
49
+ break;
50
+ case '--folder':
51
+ result.folder = args[++i] || '';
52
+ break;
53
+ case '--channel':
54
+ result.channel = (args[++i] || '').toLowerCase();
55
+ break;
56
+ case '--no-trigger-required':
57
+ result.requiresTrigger = false;
58
+ break;
59
+ case '--is-main':
60
+ result.isMain = true;
61
+ break;
62
+ case '--assistant-name':
63
+ result.assistantName = args[++i] || 'Andy';
64
+ break;
65
+ }
66
+ }
67
+
68
+ return result;
69
+ }
70
+
71
+ export async function run(args: string[]): Promise<void> {
72
+ const projectRoot = process.cwd();
73
+ const parsed = parseArgs(args);
74
+
75
+ if (!parsed.jid || !parsed.name || !parsed.trigger || !parsed.folder) {
76
+ emitStatus('REGISTER_CHANNEL', {
77
+ STATUS: 'failed',
78
+ ERROR: 'missing_required_args',
79
+ LOG: 'logs/setup.log',
80
+ });
81
+ process.exit(4);
82
+ }
83
+
84
+ if (!isValidGroupFolder(parsed.folder)) {
85
+ emitStatus('REGISTER_CHANNEL', {
86
+ STATUS: 'failed',
87
+ ERROR: 'invalid_folder',
88
+ LOG: 'logs/setup.log',
89
+ });
90
+ process.exit(4);
91
+ }
92
+
93
+ logger.info(parsed, 'Registering channel');
94
+
95
+ // Ensure data and store directories exist (store/ may not exist on
96
+ // fresh installs that skip WhatsApp auth, which normally creates it)
97
+ fs.mkdirSync(path.join(projectRoot, 'data'), { recursive: true });
98
+ fs.mkdirSync(STORE_DIR, { recursive: true });
99
+
100
+ // Initialize database (creates schema + runs migrations)
101
+ initDatabase();
102
+
103
+ setRegisteredGroup(parsed.jid, {
104
+ name: parsed.name,
105
+ folder: parsed.folder,
106
+ trigger: parsed.trigger,
107
+ added_at: new Date().toISOString(),
108
+ requiresTrigger: parsed.requiresTrigger,
109
+ isMain: parsed.isMain,
110
+ });
111
+
112
+ logger.info('Wrote registration to SQLite');
113
+
114
+ // Create group folders
115
+ fs.mkdirSync(path.join(projectRoot, 'groups', parsed.folder, 'logs'), {
116
+ recursive: true,
117
+ });
118
+
119
+ // Update assistant name in CLAUDE.md files if different from default
120
+ let nameUpdated = false;
121
+ if (parsed.assistantName !== 'Andy') {
122
+ logger.info(
123
+ { from: 'Andy', to: parsed.assistantName },
124
+ 'Updating assistant name',
125
+ );
126
+
127
+ const mdFiles = [
128
+ path.join(projectRoot, 'groups', 'global', 'CLAUDE.md'),
129
+ path.join(projectRoot, 'groups', parsed.folder, 'CLAUDE.md'),
130
+ ];
131
+
132
+ for (const mdFile of mdFiles) {
133
+ if (fs.existsSync(mdFile)) {
134
+ let content = fs.readFileSync(mdFile, 'utf-8');
135
+ content = content.replace(/^# Andy$/m, `# ${parsed.assistantName}`);
136
+ content = content.replace(
137
+ /You are Andy/g,
138
+ `You are ${parsed.assistantName}`,
139
+ );
140
+ fs.writeFileSync(mdFile, content);
141
+ logger.info({ file: mdFile }, 'Updated CLAUDE.md');
142
+ }
143
+ }
144
+
145
+ // Update .env
146
+ const envFile = path.join(projectRoot, '.env');
147
+ if (fs.existsSync(envFile)) {
148
+ let envContent = fs.readFileSync(envFile, 'utf-8');
149
+ if (envContent.includes('ASSISTANT_NAME=')) {
150
+ envContent = envContent.replace(
151
+ /^ASSISTANT_NAME=.*$/m,
152
+ `ASSISTANT_NAME="${parsed.assistantName}"`,
153
+ );
154
+ } else {
155
+ envContent += `\nASSISTANT_NAME="${parsed.assistantName}"`;
156
+ }
157
+ fs.writeFileSync(envFile, envContent);
158
+ } else {
159
+ fs.writeFileSync(envFile, `ASSISTANT_NAME="${parsed.assistantName}"\n`);
160
+ }
161
+ logger.info('Set ASSISTANT_NAME in .env');
162
+ nameUpdated = true;
163
+ }
164
+
165
+ emitStatus('REGISTER_CHANNEL', {
166
+ JID: parsed.jid,
167
+ NAME: parsed.name,
168
+ FOLDER: parsed.folder,
169
+ CHANNEL: parsed.channel,
170
+ TRIGGER: parsed.trigger,
171
+ REQUIRES_TRIGGER: parsed.requiresTrigger,
172
+ ASSISTANT_NAME: parsed.assistantName,
173
+ NAME_UPDATED: nameUpdated,
174
+ STATUS: 'success',
175
+ LOG: 'logs/setup.log',
176
+ });
177
+ }
@@ -0,0 +1,187 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import path from 'path';
3
+
4
+ /**
5
+ * Tests for service configuration generation.
6
+ *
7
+ * These tests verify the generated content of plist/systemd/nohup configs
8
+ * without actually loading services.
9
+ */
10
+
11
+ // Helper: generate a plist string the same way service.ts does
12
+ function generatePlist(
13
+ nodePath: string,
14
+ projectRoot: string,
15
+ homeDir: string,
16
+ ): string {
17
+ return `<?xml version="1.0" encoding="UTF-8"?>
18
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
19
+ <plist version="1.0">
20
+ <dict>
21
+ <key>Label</key>
22
+ <string>com.nanoclaw</string>
23
+ <key>ProgramArguments</key>
24
+ <array>
25
+ <string>${nodePath}</string>
26
+ <string>${projectRoot}/dist/index.js</string>
27
+ </array>
28
+ <key>WorkingDirectory</key>
29
+ <string>${projectRoot}</string>
30
+ <key>RunAtLoad</key>
31
+ <true/>
32
+ <key>KeepAlive</key>
33
+ <true/>
34
+ <key>EnvironmentVariables</key>
35
+ <dict>
36
+ <key>PATH</key>
37
+ <string>/usr/local/bin:/usr/bin:/bin:${homeDir}/.local/bin</string>
38
+ <key>HOME</key>
39
+ <string>${homeDir}</string>
40
+ </dict>
41
+ <key>StandardOutPath</key>
42
+ <string>${projectRoot}/logs/nanoclaw.log</string>
43
+ <key>StandardErrorPath</key>
44
+ <string>${projectRoot}/logs/nanoclaw.error.log</string>
45
+ </dict>
46
+ </plist>`;
47
+ }
48
+
49
+ function generateSystemdUnit(
50
+ nodePath: string,
51
+ projectRoot: string,
52
+ homeDir: string,
53
+ isSystem: boolean,
54
+ ): string {
55
+ return `[Unit]
56
+ Description=NanoClaw Personal Assistant
57
+ After=network.target
58
+
59
+ [Service]
60
+ Type=simple
61
+ ExecStart=${nodePath} ${projectRoot}/dist/index.js
62
+ WorkingDirectory=${projectRoot}
63
+ Restart=always
64
+ RestartSec=5
65
+ KillMode=process
66
+ Environment=HOME=${homeDir}
67
+ Environment=PATH=/usr/local/bin:/usr/bin:/bin:${homeDir}/.local/bin
68
+ StandardOutput=append:${projectRoot}/logs/nanoclaw.log
69
+ StandardError=append:${projectRoot}/logs/nanoclaw.error.log
70
+
71
+ [Install]
72
+ WantedBy=${isSystem ? 'multi-user.target' : 'default.target'}`;
73
+ }
74
+
75
+ describe('plist generation', () => {
76
+ it('contains the correct label', () => {
77
+ const plist = generatePlist(
78
+ '/usr/local/bin/node',
79
+ '/home/user/nanoclaw',
80
+ '/home/user',
81
+ );
82
+ expect(plist).toContain('<string>com.nanoclaw</string>');
83
+ });
84
+
85
+ it('uses the correct node path', () => {
86
+ const plist = generatePlist(
87
+ '/opt/node/bin/node',
88
+ '/home/user/nanoclaw',
89
+ '/home/user',
90
+ );
91
+ expect(plist).toContain('<string>/opt/node/bin/node</string>');
92
+ });
93
+
94
+ it('points to dist/index.js', () => {
95
+ const plist = generatePlist(
96
+ '/usr/local/bin/node',
97
+ '/home/user/nanoclaw',
98
+ '/home/user',
99
+ );
100
+ expect(plist).toContain('/home/user/nanoclaw/dist/index.js');
101
+ });
102
+
103
+ it('sets log paths', () => {
104
+ const plist = generatePlist(
105
+ '/usr/local/bin/node',
106
+ '/home/user/nanoclaw',
107
+ '/home/user',
108
+ );
109
+ expect(plist).toContain('nanoclaw.log');
110
+ expect(plist).toContain('nanoclaw.error.log');
111
+ });
112
+ });
113
+
114
+ describe('systemd unit generation', () => {
115
+ it('user unit uses default.target', () => {
116
+ const unit = generateSystemdUnit(
117
+ '/usr/bin/node',
118
+ '/home/user/nanoclaw',
119
+ '/home/user',
120
+ false,
121
+ );
122
+ expect(unit).toContain('WantedBy=default.target');
123
+ });
124
+
125
+ it('system unit uses multi-user.target', () => {
126
+ const unit = generateSystemdUnit(
127
+ '/usr/bin/node',
128
+ '/home/user/nanoclaw',
129
+ '/home/user',
130
+ true,
131
+ );
132
+ expect(unit).toContain('WantedBy=multi-user.target');
133
+ });
134
+
135
+ it('contains restart policy', () => {
136
+ const unit = generateSystemdUnit(
137
+ '/usr/bin/node',
138
+ '/home/user/nanoclaw',
139
+ '/home/user',
140
+ false,
141
+ );
142
+ expect(unit).toContain('Restart=always');
143
+ expect(unit).toContain('RestartSec=5');
144
+ });
145
+
146
+ it('uses KillMode=process to preserve detached children', () => {
147
+ const unit = generateSystemdUnit(
148
+ '/usr/bin/node',
149
+ '/home/user/nanoclaw',
150
+ '/home/user',
151
+ false,
152
+ );
153
+ expect(unit).toContain('KillMode=process');
154
+ });
155
+
156
+ it('sets correct ExecStart', () => {
157
+ const unit = generateSystemdUnit(
158
+ '/usr/bin/node',
159
+ '/srv/nanoclaw',
160
+ '/home/user',
161
+ false,
162
+ );
163
+ expect(unit).toContain(
164
+ 'ExecStart=/usr/bin/node /srv/nanoclaw/dist/index.js',
165
+ );
166
+ });
167
+ });
168
+
169
+ describe('WSL nohup fallback', () => {
170
+ it('generates a valid wrapper script', () => {
171
+ const projectRoot = '/home/user/nanoclaw';
172
+ const nodePath = '/usr/bin/node';
173
+ const pidFile = path.join(projectRoot, 'nanoclaw.pid');
174
+
175
+ // Simulate what service.ts generates
176
+ const wrapper = `#!/bin/bash
177
+ set -euo pipefail
178
+ cd ${JSON.stringify(projectRoot)}
179
+ nohup ${JSON.stringify(nodePath)} ${JSON.stringify(projectRoot)}/dist/index.js >> ${JSON.stringify(projectRoot)}/logs/nanoclaw.log 2>> ${JSON.stringify(projectRoot)}/logs/nanoclaw.error.log &
180
+ echo $! > ${JSON.stringify(pidFile)}`;
181
+
182
+ expect(wrapper).toContain('#!/bin/bash');
183
+ expect(wrapper).toContain('nohup');
184
+ expect(wrapper).toContain(nodePath);
185
+ expect(wrapper).toContain('nanoclaw.pid');
186
+ });
187
+ });