@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,66 @@
1
+ #!/usr/bin/env npx tsx
2
+ /**
3
+ * X Integration - Post Tweet
4
+ * Usage: echo '{"content":"Hello world"}' | npx tsx post.ts
5
+ */
6
+
7
+ import { getBrowserContext, runScript, validateContent, config, ScriptResult } from '../lib/browser.js';
8
+
9
+ interface PostInput {
10
+ content: string;
11
+ }
12
+
13
+ async function postTweet(input: PostInput): Promise<ScriptResult> {
14
+ const { content } = input;
15
+
16
+ const validationError = validateContent(content, 'Tweet');
17
+ if (validationError) return validationError;
18
+
19
+ let context = null;
20
+ try {
21
+ context = await getBrowserContext();
22
+ const page = context.pages()[0] || await context.newPage();
23
+
24
+ await page.goto('https://x.com/home', { timeout: config.timeouts.navigation, waitUntil: 'domcontentloaded' });
25
+ await page.waitForTimeout(config.timeouts.pageLoad);
26
+
27
+ // Check if logged in
28
+ const isLoggedIn = await page.locator('[data-testid="SideNav_AccountSwitcher_Button"]').isVisible().catch(() => false);
29
+ if (!isLoggedIn) {
30
+ const onLoginPage = await page.locator('input[autocomplete="username"]').isVisible().catch(() => false);
31
+ if (onLoginPage) {
32
+ return { success: false, message: 'X login expired. Run /x-integration to re-authenticate.' };
33
+ }
34
+ }
35
+
36
+ // Find and fill tweet input
37
+ const tweetInput = page.locator('[data-testid="tweetTextarea_0"]');
38
+ await tweetInput.waitFor({ timeout: config.timeouts.elementWait * 2 });
39
+ await tweetInput.click();
40
+ await page.waitForTimeout(config.timeouts.afterClick / 2);
41
+ await tweetInput.fill(content);
42
+ await page.waitForTimeout(config.timeouts.afterFill);
43
+
44
+ // Click post button
45
+ const postButton = page.locator('[data-testid="tweetButtonInline"]');
46
+ await postButton.waitFor({ timeout: config.timeouts.elementWait });
47
+
48
+ const isDisabled = await postButton.getAttribute('aria-disabled');
49
+ if (isDisabled === 'true') {
50
+ return { success: false, message: 'Post button disabled. Content may be empty or exceed character limit.' };
51
+ }
52
+
53
+ await postButton.click();
54
+ await page.waitForTimeout(config.timeouts.afterSubmit);
55
+
56
+ return {
57
+ success: true,
58
+ message: `Tweet posted: ${content.slice(0, 50)}${content.length > 50 ? '...' : ''}`
59
+ };
60
+
61
+ } finally {
62
+ if (context) await context.close();
63
+ }
64
+ }
65
+
66
+ runScript<PostInput>(postTweet);
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env npx tsx
2
+ /**
3
+ * X Integration - Quote Tweet
4
+ * Usage: echo '{"tweetUrl":"https://x.com/user/status/123","comment":"My thoughts"}' | npx tsx quote.ts
5
+ */
6
+
7
+ import { getBrowserContext, navigateToTweet, runScript, validateContent, config, ScriptResult } from '../lib/browser.js';
8
+
9
+ interface QuoteInput {
10
+ tweetUrl: string;
11
+ comment: string;
12
+ }
13
+
14
+ async function quoteTweet(input: QuoteInput): Promise<ScriptResult> {
15
+ const { tweetUrl, comment } = input;
16
+
17
+ if (!tweetUrl) {
18
+ return { success: false, message: 'Please provide a tweet URL' };
19
+ }
20
+
21
+ const validationError = validateContent(comment, 'Comment');
22
+ if (validationError) return validationError;
23
+
24
+ let context = null;
25
+ try {
26
+ context = await getBrowserContext();
27
+ const { page, success, error } = await navigateToTweet(context, tweetUrl);
28
+
29
+ if (!success) {
30
+ return { success: false, message: error || 'Navigation failed' };
31
+ }
32
+
33
+ // Click retweet button to open menu
34
+ const tweet = page.locator('article[data-testid="tweet"]').first();
35
+ const retweetButton = tweet.locator('[data-testid="retweet"]');
36
+ await retweetButton.waitFor({ timeout: config.timeouts.elementWait });
37
+ await retweetButton.click();
38
+ await page.waitForTimeout(config.timeouts.afterClick);
39
+
40
+ // Click quote option
41
+ const quoteOption = page.getByRole('menuitem').filter({ hasText: /Quote/i });
42
+ await quoteOption.waitFor({ timeout: config.timeouts.elementWait });
43
+ await quoteOption.click();
44
+ await page.waitForTimeout(config.timeouts.afterClick * 1.5);
45
+
46
+ // Find dialog with aria-modal="true"
47
+ const dialog = page.locator('[role="dialog"][aria-modal="true"]');
48
+ await dialog.waitFor({ timeout: config.timeouts.elementWait });
49
+
50
+ // Fill comment
51
+ const quoteInput = dialog.locator('[data-testid="tweetTextarea_0"]');
52
+ await quoteInput.waitFor({ timeout: config.timeouts.elementWait });
53
+ await quoteInput.click();
54
+ await page.waitForTimeout(config.timeouts.afterClick / 2);
55
+ await quoteInput.fill(comment);
56
+ await page.waitForTimeout(config.timeouts.afterFill);
57
+
58
+ // Click submit button
59
+ const submitButton = dialog.locator('[data-testid="tweetButton"]');
60
+ await submitButton.waitFor({ timeout: config.timeouts.elementWait });
61
+
62
+ const isDisabled = await submitButton.getAttribute('aria-disabled');
63
+ if (isDisabled === 'true') {
64
+ return { success: false, message: 'Submit button disabled. Content may be empty or exceed character limit.' };
65
+ }
66
+
67
+ await submitButton.click();
68
+ await page.waitForTimeout(config.timeouts.afterSubmit);
69
+
70
+ return {
71
+ success: true,
72
+ message: `Quote tweet posted: ${comment.slice(0, 50)}${comment.length > 50 ? '...' : ''}`
73
+ };
74
+
75
+ } finally {
76
+ if (context) await context.close();
77
+ }
78
+ }
79
+
80
+ runScript<QuoteInput>(quoteTweet);
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env npx tsx
2
+ /**
3
+ * X Integration - Reply to Tweet
4
+ * Usage: echo '{"tweetUrl":"https://x.com/user/status/123","content":"Great post!"}' | npx tsx reply.ts
5
+ */
6
+
7
+ import { getBrowserContext, navigateToTweet, runScript, validateContent, config, ScriptResult } from '../lib/browser.js';
8
+
9
+ interface ReplyInput {
10
+ tweetUrl: string;
11
+ content: string;
12
+ }
13
+
14
+ async function replyToTweet(input: ReplyInput): Promise<ScriptResult> {
15
+ const { tweetUrl, content } = input;
16
+
17
+ if (!tweetUrl) {
18
+ return { success: false, message: 'Please provide a tweet URL' };
19
+ }
20
+
21
+ const validationError = validateContent(content, 'Reply');
22
+ if (validationError) return validationError;
23
+
24
+ let context = null;
25
+ try {
26
+ context = await getBrowserContext();
27
+ const { page, success, error } = await navigateToTweet(context, tweetUrl);
28
+
29
+ if (!success) {
30
+ return { success: false, message: error || 'Navigation failed' };
31
+ }
32
+
33
+ // Click reply button
34
+ const tweet = page.locator('article[data-testid="tweet"]').first();
35
+ const replyButton = tweet.locator('[data-testid="reply"]');
36
+ await replyButton.waitFor({ timeout: config.timeouts.elementWait });
37
+ await replyButton.click();
38
+ await page.waitForTimeout(config.timeouts.afterClick * 1.5);
39
+
40
+ // Find dialog with aria-modal="true" to avoid matching other dialogs
41
+ const dialog = page.locator('[role="dialog"][aria-modal="true"]');
42
+ await dialog.waitFor({ timeout: config.timeouts.elementWait });
43
+
44
+ // Fill reply content
45
+ const replyInput = dialog.locator('[data-testid="tweetTextarea_0"]');
46
+ await replyInput.waitFor({ timeout: config.timeouts.elementWait });
47
+ await replyInput.click();
48
+ await page.waitForTimeout(config.timeouts.afterClick / 2);
49
+ await replyInput.fill(content);
50
+ await page.waitForTimeout(config.timeouts.afterFill);
51
+
52
+ // Click submit button
53
+ const submitButton = dialog.locator('[data-testid="tweetButton"]');
54
+ await submitButton.waitFor({ timeout: config.timeouts.elementWait });
55
+
56
+ const isDisabled = await submitButton.getAttribute('aria-disabled');
57
+ if (isDisabled === 'true') {
58
+ return { success: false, message: 'Submit button disabled. Content may be empty or exceed character limit.' };
59
+ }
60
+
61
+ await submitButton.click();
62
+ await page.waitForTimeout(config.timeouts.afterSubmit);
63
+
64
+ return {
65
+ success: true,
66
+ message: `Reply posted: ${content.slice(0, 50)}${content.length > 50 ? '...' : ''}`
67
+ };
68
+
69
+ } finally {
70
+ if (context) await context.close();
71
+ }
72
+ }
73
+
74
+ runScript<ReplyInput>(replyToTweet);
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env npx tsx
2
+ /**
3
+ * X Integration - Retweet
4
+ * Usage: echo '{"tweetUrl":"https://x.com/user/status/123"}' | npx tsx retweet.ts
5
+ */
6
+
7
+ import { getBrowserContext, navigateToTweet, runScript, config, ScriptResult } from '../lib/browser.js';
8
+
9
+ interface RetweetInput {
10
+ tweetUrl: string;
11
+ }
12
+
13
+ async function retweet(input: RetweetInput): Promise<ScriptResult> {
14
+ const { tweetUrl } = input;
15
+
16
+ if (!tweetUrl) {
17
+ return { success: false, message: 'Please provide a tweet URL' };
18
+ }
19
+
20
+ let context = null;
21
+ try {
22
+ context = await getBrowserContext();
23
+ const { page, success, error } = await navigateToTweet(context, tweetUrl);
24
+
25
+ if (!success) {
26
+ return { success: false, message: error || 'Navigation failed' };
27
+ }
28
+
29
+ const tweet = page.locator('article[data-testid="tweet"]').first();
30
+ const unretweetButton = tweet.locator('[data-testid="unretweet"]');
31
+ const retweetButton = tweet.locator('[data-testid="retweet"]');
32
+
33
+ // Check if already retweeted
34
+ const alreadyRetweeted = await unretweetButton.isVisible().catch(() => false);
35
+ if (alreadyRetweeted) {
36
+ return { success: true, message: 'Tweet already retweeted' };
37
+ }
38
+
39
+ await retweetButton.waitFor({ timeout: config.timeouts.elementWait });
40
+ await retweetButton.click();
41
+ await page.waitForTimeout(config.timeouts.afterClick);
42
+
43
+ // Click retweet confirm option
44
+ const retweetConfirm = page.locator('[data-testid="retweetConfirm"]');
45
+ await retweetConfirm.waitFor({ timeout: config.timeouts.elementWait });
46
+ await retweetConfirm.click();
47
+ await page.waitForTimeout(config.timeouts.afterClick * 2);
48
+
49
+ // Verify
50
+ const nowRetweeted = await unretweetButton.isVisible().catch(() => false);
51
+ if (nowRetweeted) {
52
+ return { success: true, message: 'Retweet successful' };
53
+ }
54
+
55
+ return { success: false, message: 'Retweet action completed but could not verify success' };
56
+
57
+ } finally {
58
+ if (context) await context.close();
59
+ }
60
+ }
61
+
62
+ runScript<RetweetInput>(retweet);
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env npx tsx
2
+ /**
3
+ * X Integration - Authentication Setup
4
+ * Usage: npx tsx setup.ts
5
+ *
6
+ * Interactive script - opens browser for manual login
7
+ */
8
+
9
+ import { chromium } from 'playwright';
10
+ import * as readline from 'readline';
11
+ import fs from 'fs';
12
+ import path from 'path';
13
+ import { config, cleanupLockFiles } from '../lib/browser.js';
14
+
15
+ async function setup(): Promise<void> {
16
+ console.log('=== X (Twitter) Authentication Setup ===\n');
17
+ console.log('This will open Chrome for you to log in to X.');
18
+ console.log('Your login session will be saved for automated interactions.\n');
19
+ console.log(`Chrome path: ${config.chromePath}`);
20
+ console.log(`Profile dir: ${config.browserDataDir}\n`);
21
+
22
+ // Ensure directories exist
23
+ fs.mkdirSync(path.dirname(config.authPath), { recursive: true });
24
+ fs.mkdirSync(config.browserDataDir, { recursive: true });
25
+
26
+ cleanupLockFiles();
27
+
28
+ console.log('Launching browser...\n');
29
+
30
+ const context = await chromium.launchPersistentContext(config.browserDataDir, {
31
+ executablePath: config.chromePath,
32
+ headless: false,
33
+ viewport: config.viewport,
34
+ args: config.chromeArgs.slice(0, 3), // Use first 3 args for setup (less restrictive)
35
+ ignoreDefaultArgs: config.chromeIgnoreDefaultArgs,
36
+ });
37
+
38
+ const page = context.pages()[0] || await context.newPage();
39
+
40
+ // Navigate to login page
41
+ await page.goto('https://x.com/login');
42
+
43
+ console.log('Please log in to X in the browser window.');
44
+ console.log('After you see your home feed, come back here and press Enter.\n');
45
+
46
+ // Wait for user to complete login
47
+ const rl = readline.createInterface({
48
+ input: process.stdin,
49
+ output: process.stdout
50
+ });
51
+
52
+ await new Promise<void>(resolve => {
53
+ rl.question('Press Enter when logged in... ', () => {
54
+ rl.close();
55
+ resolve();
56
+ });
57
+ });
58
+
59
+ // Verify login by navigating to home and checking for account button
60
+ console.log('\nVerifying login status...');
61
+ await page.goto('https://x.com/home');
62
+ await page.waitForTimeout(config.timeouts.pageLoad);
63
+
64
+ const isLoggedIn = await page.locator('[data-testid="SideNav_AccountSwitcher_Button"]').isVisible().catch(() => false);
65
+
66
+ if (isLoggedIn) {
67
+ // Save auth marker
68
+ fs.writeFileSync(config.authPath, JSON.stringify({
69
+ authenticated: true,
70
+ timestamp: new Date().toISOString()
71
+ }, null, 2));
72
+
73
+ console.log('\n✅ Authentication successful!');
74
+ console.log(`Session saved to: ${config.browserDataDir}`);
75
+ console.log('\nYou can now use X integration features.');
76
+ } else {
77
+ console.log('\n❌ Could not verify login status.');
78
+ console.log('Please try again and make sure you are logged in to X.');
79
+ }
80
+
81
+ await context.close();
82
+ }
83
+
84
+ setup().catch(err => {
85
+ console.error('Setup failed:', err.message);
86
+ process.exit(1);
87
+ });
package/.env.example ADDED
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,10 @@
1
+ # Core code - maintainer only
2
+ /src/ @gavrielc @gabi-simons
3
+ /container/ @gavrielc @gabi-simons
4
+ /groups/ @gavrielc @gabi-simons
5
+ /launchd/ @gavrielc @gabi-simons
6
+ /package.json @gavrielc @gabi-simons
7
+ /package-lock.json @gavrielc @gabi-simons
8
+
9
+ # Skills - open to contributors
10
+ /.claude/skills/
@@ -0,0 +1,14 @@
1
+ ## Type of Change
2
+
3
+ - [ ] **Skill** - adds a new skill in `.claude/skills/`
4
+ - [ ] **Fix** - bug fix or security fix to source code
5
+ - [ ] **Simplification** - reduces or simplifies source code
6
+
7
+ ## Description
8
+
9
+
10
+ ## For Skills
11
+
12
+ - [ ] I have not made any changes to source code
13
+ - [ ] My skill contains instructions for Claude to follow (not pre-built code)
14
+ - [ ] I tested this skill on a fresh clone
@@ -0,0 +1,32 @@
1
+ name: Bump version
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ paths: ['src/**', 'container/**']
7
+
8
+ jobs:
9
+ bump-version:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/create-github-app-token@v1
13
+ id: app-token
14
+ with:
15
+ app-id: ${{ secrets.APP_ID }}
16
+ private-key: ${{ secrets.APP_PRIVATE_KEY }}
17
+
18
+ - uses: actions/checkout@v4
19
+ with:
20
+ token: ${{ steps.app-token.outputs.token }}
21
+
22
+ - name: Bump patch version
23
+ run: |
24
+ npm version patch --no-git-tag-version
25
+ git add package.json package-lock.json
26
+ git diff --cached --quiet && exit 0
27
+ git config user.name "github-actions[bot]"
28
+ git config user.email "github-actions[bot]@users.noreply.github.com"
29
+ VERSION=$(node -p "require('./package.json').version")
30
+ git commit -m "chore: bump version to $VERSION"
31
+ git pull --rebase
32
+ git push
@@ -0,0 +1,25 @@
1
+ name: CI
2
+
3
+ on:
4
+ pull_request:
5
+ branches: [main]
6
+
7
+ jobs:
8
+ ci:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v4
12
+ - uses: actions/setup-node@v4
13
+ with:
14
+ node-version: 20
15
+ cache: npm
16
+ - run: npm ci
17
+
18
+ - name: Format check
19
+ run: npm run format:check
20
+
21
+ - name: Typecheck
22
+ run: npx tsc --noEmit
23
+
24
+ - name: Tests
25
+ run: npx vitest run
@@ -0,0 +1,160 @@
1
+ name: Merge-forward skill branches
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+
7
+ permissions:
8
+ contents: write
9
+ issues: write
10
+
11
+ jobs:
12
+ merge-forward:
13
+ if: github.repository == 'qwibitai/nanoclaw'
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ with:
18
+ fetch-depth: 0
19
+ token: ${{ secrets.GITHUB_TOKEN }}
20
+
21
+ - uses: actions/setup-node@v4
22
+ with:
23
+ node-version: 20
24
+ cache: npm
25
+
26
+ - name: Configure git
27
+ run: |
28
+ git config user.name "github-actions[bot]"
29
+ git config user.email "github-actions[bot]@users.noreply.github.com"
30
+
31
+ - name: Merge main into each skill branch
32
+ id: merge
33
+ run: |
34
+ FAILED=""
35
+ SUCCEEDED=""
36
+
37
+ # List all remote skill branches
38
+ SKILL_BRANCHES=$(git branch -r --list 'origin/skill/*' | sed 's|origin/||' | xargs)
39
+
40
+ if [ -z "$SKILL_BRANCHES" ]; then
41
+ echo "No skill branches found."
42
+ exit 0
43
+ fi
44
+
45
+ for BRANCH in $SKILL_BRANCHES; do
46
+ SKILL_NAME=$(echo "$BRANCH" | sed 's|skill/||')
47
+ echo ""
48
+ echo "=== Processing $BRANCH ==="
49
+
50
+ # Checkout the skill branch
51
+ git checkout -B "$BRANCH" "origin/$BRANCH"
52
+
53
+ # Attempt merge
54
+ if ! git merge main --no-edit; then
55
+ echo "::warning::Merge conflict in $BRANCH"
56
+ git merge --abort
57
+ FAILED="$FAILED $SKILL_NAME"
58
+ continue
59
+ fi
60
+
61
+ # Check if there's anything new to push
62
+ if git diff --quiet "origin/$BRANCH"; then
63
+ echo "$BRANCH is already up to date with main."
64
+ SUCCEEDED="$SUCCEEDED $SKILL_NAME"
65
+ continue
66
+ fi
67
+
68
+ # Install deps and validate
69
+ npm ci
70
+
71
+ if ! npm run build; then
72
+ echo "::warning::Build failed for $BRANCH"
73
+ git reset --hard "origin/$BRANCH"
74
+ FAILED="$FAILED $SKILL_NAME"
75
+ continue
76
+ fi
77
+
78
+ if ! npm test 2>/dev/null; then
79
+ echo "::warning::Tests failed for $BRANCH"
80
+ git reset --hard "origin/$BRANCH"
81
+ FAILED="$FAILED $SKILL_NAME"
82
+ continue
83
+ fi
84
+
85
+ # Push the updated branch
86
+ git push origin "$BRANCH"
87
+ SUCCEEDED="$SUCCEEDED $SKILL_NAME"
88
+ echo "$BRANCH merged and pushed successfully."
89
+ done
90
+
91
+ echo ""
92
+ echo "=== Results ==="
93
+ echo "Succeeded: $SUCCEEDED"
94
+ echo "Failed: $FAILED"
95
+
96
+ # Export for issue creation
97
+ echo "failed=$FAILED" >> "$GITHUB_OUTPUT"
98
+ echo "succeeded=$SUCCEEDED" >> "$GITHUB_OUTPUT"
99
+
100
+ - name: Open issue for failed merges
101
+ if: steps.merge.outputs.failed != ''
102
+ uses: actions/github-script@v7
103
+ with:
104
+ script: |
105
+ const failed = '${{ steps.merge.outputs.failed }}'.trim().split(/\s+/);
106
+ const sha = context.sha.substring(0, 7);
107
+ const body = [
108
+ `The merge-forward workflow failed to merge \`main\` (${sha}) into the following skill branches:`,
109
+ '',
110
+ ...failed.map(s => `- \`skill/${s}\`: merge conflict, build failure, or test failure`),
111
+ '',
112
+ 'Please resolve manually:',
113
+ '```bash',
114
+ ...failed.map(s => [
115
+ `git checkout skill/${s}`,
116
+ `git merge main`,
117
+ `# resolve conflicts, then: git push`,
118
+ ''
119
+ ]).flat(),
120
+ '```',
121
+ '',
122
+ `Triggered by push to main: ${context.sha}`
123
+ ].join('\n');
124
+
125
+ await github.rest.issues.create({
126
+ owner: context.repo.owner,
127
+ repo: context.repo.repo,
128
+ title: `Merge-forward failed for ${failed.length} skill branch(es) after ${sha}`,
129
+ body,
130
+ labels: ['skill-maintenance']
131
+ });
132
+
133
+ - name: Notify channel forks
134
+ if: always()
135
+ uses: actions/github-script@v7
136
+ with:
137
+ github-token: ${{ secrets.FORK_DISPATCH_TOKEN || secrets.GITHUB_TOKEN }}
138
+ script: |
139
+ const forks = [
140
+ 'nanoclaw-whatsapp',
141
+ 'nanoclaw-telegram',
142
+ 'nanoclaw-discord',
143
+ 'nanoclaw-slack',
144
+ 'nanoclaw-gmail',
145
+ 'nanoclaw-docker-sandboxes',
146
+ ];
147
+ const sha = context.sha.substring(0, 7);
148
+ for (const repo of forks) {
149
+ try {
150
+ await github.rest.repos.createDispatchEvent({
151
+ owner: 'qwibitai',
152
+ repo,
153
+ event_type: 'upstream-main-updated',
154
+ client_payload: { sha: context.sha },
155
+ });
156
+ console.log(`Notified ${repo}`);
157
+ } catch (e) {
158
+ console.log(`Failed to notify ${repo}: ${e.message}`);
159
+ }
160
+ }
@@ -0,0 +1,42 @@
1
+ name: Update token count
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ push:
6
+ branches: [main]
7
+ paths: ['src/**', 'container/**', 'launchd/**', 'CLAUDE.md']
8
+
9
+ jobs:
10
+ update-tokens:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/create-github-app-token@v1
14
+ id: app-token
15
+ with:
16
+ app-id: ${{ secrets.APP_ID }}
17
+ private-key: ${{ secrets.APP_PRIVATE_KEY }}
18
+
19
+ - uses: actions/checkout@v4
20
+ with:
21
+ token: ${{ steps.app-token.outputs.token }}
22
+
23
+ - uses: actions/setup-python@v5
24
+ with:
25
+ python-version: '3.12'
26
+
27
+ - uses: ./repo-tokens
28
+ id: tokens
29
+ with:
30
+ include: 'src/**/*.ts container/agent-runner/src/**/*.ts container/Dockerfile container/build.sh launchd/com.nanoclaw.plist CLAUDE.md'
31
+ exclude: 'src/**/*.test.ts'
32
+ badge-path: 'repo-tokens/badge.svg'
33
+
34
+ - name: Commit if changed
35
+ run: |
36
+ git add README.md repo-tokens/badge.svg
37
+ git diff --cached --quiet && exit 0
38
+ git config user.name "github-actions[bot]"
39
+ git config user.email "github-actions[bot]@users.noreply.github.com"
40
+ git commit -m "docs: update token count to ${{ steps.tokens.outputs.badge }}"
41
+ git pull --rebase
42
+ git push
@@ -0,0 +1 @@
1
+ npm run format:fix
package/.mcp.json ADDED
@@ -0,0 +1,3 @@
1
+ {
2
+ "mcpServers": {}
3
+ }
package/.nvmrc ADDED
@@ -0,0 +1 @@
1
+ 22
package/.prettierrc ADDED
@@ -0,0 +1,3 @@
1
+ {
2
+ "singleQuote": true
3
+ }