gigaclaw 1.4.0

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 (249) hide show
  1. package/LICENSE +26 -0
  2. package/README.md +237 -0
  3. package/api/CLAUDE.md +19 -0
  4. package/api/index.js +265 -0
  5. package/bin/cli.js +823 -0
  6. package/bin/local.sh +85 -0
  7. package/bin/postinstall.js +63 -0
  8. package/config/index.js +26 -0
  9. package/config/instrumentation.js +62 -0
  10. package/drizzle/0000_initial.sql +52 -0
  11. package/drizzle/0001_nostalgic_sersi.sql +11 -0
  12. package/drizzle/0002_black_daimon_hellstrom.sql +19 -0
  13. package/drizzle/0003_rename_code_workspaces.sql +5 -0
  14. package/drizzle/meta/0000_snapshot.json +321 -0
  15. package/drizzle/meta/0001_snapshot.json +390 -0
  16. package/drizzle/meta/0002_snapshot.json +411 -0
  17. package/drizzle/meta/0003_snapshot.json +419 -0
  18. package/drizzle/meta/_journal.json +34 -0
  19. package/lib/actions.js +44 -0
  20. package/lib/ai/agent.js +86 -0
  21. package/lib/ai/index.js +342 -0
  22. package/lib/ai/model.js +180 -0
  23. package/lib/ai/tools.js +269 -0
  24. package/lib/ai/web-search.js +42 -0
  25. package/lib/auth/actions.js +28 -0
  26. package/lib/auth/config.js +27 -0
  27. package/lib/auth/edge-config.js +27 -0
  28. package/lib/auth/index.js +27 -0
  29. package/lib/auth/middleware.js +62 -0
  30. package/lib/channels/base.js +56 -0
  31. package/lib/channels/index.js +15 -0
  32. package/lib/channels/telegram.js +148 -0
  33. package/lib/chat/actions.js +579 -0
  34. package/lib/chat/api.js +140 -0
  35. package/lib/chat/components/app-sidebar.js +213 -0
  36. package/lib/chat/components/app-sidebar.jsx +279 -0
  37. package/lib/chat/components/chat-header.js +192 -0
  38. package/lib/chat/components/chat-header.jsx +223 -0
  39. package/lib/chat/components/chat-input.js +236 -0
  40. package/lib/chat/components/chat-input.jsx +249 -0
  41. package/lib/chat/components/chat-nav-context.js +11 -0
  42. package/lib/chat/components/chat-nav-context.jsx +11 -0
  43. package/lib/chat/components/chat-page.js +99 -0
  44. package/lib/chat/components/chat-page.jsx +121 -0
  45. package/lib/chat/components/chat.js +153 -0
  46. package/lib/chat/components/chat.jsx +199 -0
  47. package/lib/chat/components/chats-page.js +367 -0
  48. package/lib/chat/components/chats-page.jsx +394 -0
  49. package/lib/chat/components/code-mode-toggle.js +132 -0
  50. package/lib/chat/components/code-mode-toggle.jsx +163 -0
  51. package/lib/chat/components/crons-page.js +172 -0
  52. package/lib/chat/components/crons-page.jsx +244 -0
  53. package/lib/chat/components/greeting.js +11 -0
  54. package/lib/chat/components/greeting.jsx +16 -0
  55. package/lib/chat/components/icons.js +805 -0
  56. package/lib/chat/components/icons.jsx +751 -0
  57. package/lib/chat/components/index.js +20 -0
  58. package/lib/chat/components/message.js +363 -0
  59. package/lib/chat/components/message.jsx +422 -0
  60. package/lib/chat/components/messages.js +65 -0
  61. package/lib/chat/components/messages.jsx +74 -0
  62. package/lib/chat/components/notifications-page.js +56 -0
  63. package/lib/chat/components/notifications-page.jsx +87 -0
  64. package/lib/chat/components/page-layout.js +21 -0
  65. package/lib/chat/components/page-layout.jsx +28 -0
  66. package/lib/chat/components/pull-requests-page.js +103 -0
  67. package/lib/chat/components/pull-requests-page.jsx +113 -0
  68. package/lib/chat/components/settings-layout.js +39 -0
  69. package/lib/chat/components/settings-layout.jsx +53 -0
  70. package/lib/chat/components/settings-secrets-page.js +216 -0
  71. package/lib/chat/components/settings-secrets-page.jsx +264 -0
  72. package/lib/chat/components/sidebar-history-item.js +138 -0
  73. package/lib/chat/components/sidebar-history-item.jsx +119 -0
  74. package/lib/chat/components/sidebar-history.js +167 -0
  75. package/lib/chat/components/sidebar-history.jsx +220 -0
  76. package/lib/chat/components/sidebar-user-nav.js +61 -0
  77. package/lib/chat/components/sidebar-user-nav.jsx +77 -0
  78. package/lib/chat/components/swarm-page.js +157 -0
  79. package/lib/chat/components/swarm-page.jsx +210 -0
  80. package/lib/chat/components/tool-call.js +89 -0
  81. package/lib/chat/components/tool-call.jsx +107 -0
  82. package/lib/chat/components/triggers-page.js +153 -0
  83. package/lib/chat/components/triggers-page.jsx +221 -0
  84. package/lib/chat/components/ui/combobox.js +98 -0
  85. package/lib/chat/components/ui/combobox.jsx +114 -0
  86. package/lib/chat/components/ui/confirm-dialog.js +53 -0
  87. package/lib/chat/components/ui/confirm-dialog.jsx +57 -0
  88. package/lib/chat/components/ui/dropdown-menu.js +194 -0
  89. package/lib/chat/components/ui/dropdown-menu.jsx +215 -0
  90. package/lib/chat/components/ui/rename-dialog.js +78 -0
  91. package/lib/chat/components/ui/rename-dialog.jsx +74 -0
  92. package/lib/chat/components/ui/scroll-area.js +13 -0
  93. package/lib/chat/components/ui/scroll-area.jsx +17 -0
  94. package/lib/chat/components/ui/separator.js +21 -0
  95. package/lib/chat/components/ui/separator.jsx +18 -0
  96. package/lib/chat/components/ui/sheet.js +75 -0
  97. package/lib/chat/components/ui/sheet.jsx +95 -0
  98. package/lib/chat/components/ui/sidebar.js +228 -0
  99. package/lib/chat/components/ui/sidebar.jsx +246 -0
  100. package/lib/chat/components/ui/tooltip.js +56 -0
  101. package/lib/chat/components/ui/tooltip.jsx +66 -0
  102. package/lib/chat/components/upgrade-dialog.js +151 -0
  103. package/lib/chat/components/upgrade-dialog.jsx +170 -0
  104. package/lib/chat/utils.js +11 -0
  105. package/lib/code/actions.js +153 -0
  106. package/lib/code/code-page.js +22 -0
  107. package/lib/code/code-page.jsx +25 -0
  108. package/lib/code/index.js +1 -0
  109. package/lib/code/terminal-view.js +201 -0
  110. package/lib/code/terminal-view.jsx +224 -0
  111. package/lib/code/ws-proxy.js +80 -0
  112. package/lib/cron.js +246 -0
  113. package/lib/db/api-keys.js +163 -0
  114. package/lib/db/chats.js +168 -0
  115. package/lib/db/code-workspaces.js +110 -0
  116. package/lib/db/index.js +52 -0
  117. package/lib/db/notifications.js +99 -0
  118. package/lib/db/schema.js +66 -0
  119. package/lib/db/update-check.js +96 -0
  120. package/lib/db/users.js +89 -0
  121. package/lib/paths.js +42 -0
  122. package/lib/tools/create-job.js +97 -0
  123. package/lib/tools/docker.js +146 -0
  124. package/lib/tools/github.js +271 -0
  125. package/lib/tools/openai.js +35 -0
  126. package/lib/tools/telegram.js +292 -0
  127. package/lib/triggers.js +104 -0
  128. package/lib/utils/render-md.js +111 -0
  129. package/package.json +118 -0
  130. package/setup/lib/auth.mjs +81 -0
  131. package/setup/lib/env.mjs +21 -0
  132. package/setup/lib/fs-utils.mjs +20 -0
  133. package/setup/lib/github.mjs +149 -0
  134. package/setup/lib/prerequisites.mjs +155 -0
  135. package/setup/lib/prompts.mjs +267 -0
  136. package/setup/lib/providers.mjs +105 -0
  137. package/setup/lib/sync.mjs +125 -0
  138. package/setup/lib/targets.mjs +45 -0
  139. package/setup/lib/telegram-verify.mjs +63 -0
  140. package/setup/lib/telegram.mjs +76 -0
  141. package/setup/setup-cloud.mjs +833 -0
  142. package/setup/setup-local.mjs +377 -0
  143. package/setup/setup-telegram.mjs +265 -0
  144. package/setup/setup.mjs +87 -0
  145. package/templates/.dockerignore +5 -0
  146. package/templates/.env.example +104 -0
  147. package/templates/.github/workflows/auto-merge.yml +117 -0
  148. package/templates/.github/workflows/notify-job-failed.yml +64 -0
  149. package/templates/.github/workflows/notify-pr-complete.yml +119 -0
  150. package/templates/.github/workflows/rebuild-event-handler.yml +121 -0
  151. package/templates/.github/workflows/run-job.yml +89 -0
  152. package/templates/.github/workflows/upgrade-event-handler.yml +62 -0
  153. package/templates/.gitignore.template +45 -0
  154. package/templates/.pi/extensions/env-sanitizer/index.ts +48 -0
  155. package/templates/.pi/extensions/env-sanitizer/package.json +5 -0
  156. package/templates/CLAUDE.md +29 -0
  157. package/templates/CLAUDE.md.template +308 -0
  158. package/templates/app/api/[...gigaclaw]/route.js +1 -0
  159. package/templates/app/api/auth/[...nextauth]/route.js +1 -0
  160. package/templates/app/chat/[chatId]/page.js +9 -0
  161. package/templates/app/chats/page.js +7 -0
  162. package/templates/app/code/[codeWorkspaceId]/page.js +9 -0
  163. package/templates/app/components/ascii-logo.jsx +12 -0
  164. package/templates/app/components/login-form.jsx +92 -0
  165. package/templates/app/components/setup-form.jsx +82 -0
  166. package/templates/app/components/theme-provider.jsx +11 -0
  167. package/templates/app/components/theme-toggle.jsx +38 -0
  168. package/templates/app/components/ui/button.jsx +21 -0
  169. package/templates/app/components/ui/card.jsx +23 -0
  170. package/templates/app/components/ui/input.jsx +10 -0
  171. package/templates/app/components/ui/label.jsx +10 -0
  172. package/templates/app/crons/page.js +5 -0
  173. package/templates/app/globals.css +90 -0
  174. package/templates/app/layout.js +33 -0
  175. package/templates/app/login/page.js +15 -0
  176. package/templates/app/notifications/page.js +7 -0
  177. package/templates/app/page.js +7 -0
  178. package/templates/app/pull-requests/page.js +7 -0
  179. package/templates/app/settings/crons/page.js +5 -0
  180. package/templates/app/settings/layout.js +7 -0
  181. package/templates/app/settings/page.js +5 -0
  182. package/templates/app/settings/secrets/page.js +5 -0
  183. package/templates/app/settings/triggers/page.js +5 -0
  184. package/templates/app/stream/chat/route.js +1 -0
  185. package/templates/app/swarm/page.js +7 -0
  186. package/templates/app/triggers/page.js +5 -0
  187. package/templates/config/CODE_PLANNING.md +14 -0
  188. package/templates/config/CRONS.json +56 -0
  189. package/templates/config/HEARTBEAT.md +3 -0
  190. package/templates/config/JOB_AGENT.md +30 -0
  191. package/templates/config/JOB_PLANNING.md +240 -0
  192. package/templates/config/JOB_SUMMARY.md +130 -0
  193. package/templates/config/SKILL_BUILDING_GUIDE.md +96 -0
  194. package/templates/config/SOUL.md +48 -0
  195. package/templates/config/TRIGGERS.json +58 -0
  196. package/templates/config/WEB_SEARCH_AVAILABLE.md +5 -0
  197. package/templates/config/WEB_SEARCH_UNAVAILABLE.md +3 -0
  198. package/templates/docker/claude-code-job/Dockerfile +34 -0
  199. package/templates/docker/claude-code-job/entrypoint.sh +149 -0
  200. package/templates/docker/claude-code-workspace/.tmux.conf +5 -0
  201. package/templates/docker/claude-code-workspace/Dockerfile +61 -0
  202. package/templates/docker/claude-code-workspace/entrypoint.sh +51 -0
  203. package/templates/docker/event-handler/Dockerfile +20 -0
  204. package/templates/docker/event-handler/ecosystem.config.cjs +7 -0
  205. package/templates/docker/pi-coding-agent-job/Dockerfile +51 -0
  206. package/templates/docker/pi-coding-agent-job/entrypoint.sh +164 -0
  207. package/templates/docker-compose.local.yml +78 -0
  208. package/templates/docker-compose.yml +64 -0
  209. package/templates/instrumentation.js +6 -0
  210. package/templates/middleware.js +23 -0
  211. package/templates/next.config.mjs +3 -0
  212. package/templates/postcss.config.mjs +5 -0
  213. package/templates/public/favicon.ico +0 -0
  214. package/templates/server.js +25 -0
  215. package/templates/skills/LICENSE +21 -0
  216. package/templates/skills/README.md +119 -0
  217. package/templates/skills/brave-search/SKILL.md +79 -0
  218. package/templates/skills/brave-search/content.js +86 -0
  219. package/templates/skills/brave-search/package-lock.json +621 -0
  220. package/templates/skills/brave-search/package.json +14 -0
  221. package/templates/skills/brave-search/search.js +199 -0
  222. package/templates/skills/browser-tools/SKILL.md +196 -0
  223. package/templates/skills/browser-tools/browser-content.js +103 -0
  224. package/templates/skills/browser-tools/browser-cookies.js +35 -0
  225. package/templates/skills/browser-tools/browser-eval.js +53 -0
  226. package/templates/skills/browser-tools/browser-hn-scraper.js +108 -0
  227. package/templates/skills/browser-tools/browser-nav.js +44 -0
  228. package/templates/skills/browser-tools/browser-pick.js +162 -0
  229. package/templates/skills/browser-tools/browser-screenshot.js +34 -0
  230. package/templates/skills/browser-tools/browser-start.js +87 -0
  231. package/templates/skills/browser-tools/package-lock.json +2556 -0
  232. package/templates/skills/browser-tools/package.json +19 -0
  233. package/templates/skills/google-docs/SKILL.md +23 -0
  234. package/templates/skills/google-docs/create.sh +69 -0
  235. package/templates/skills/google-drive/SKILL.md +47 -0
  236. package/templates/skills/google-drive/delete.sh +47 -0
  237. package/templates/skills/google-drive/download.sh +50 -0
  238. package/templates/skills/google-drive/list.sh +41 -0
  239. package/templates/skills/google-drive/upload.sh +76 -0
  240. package/templates/skills/kie-ai/SKILL.md +38 -0
  241. package/templates/skills/kie-ai/generate-image.sh +77 -0
  242. package/templates/skills/kie-ai/generate-video.sh +69 -0
  243. package/templates/skills/llm-secrets/SKILL.md +34 -0
  244. package/templates/skills/llm-secrets/llm-secrets.js +33 -0
  245. package/templates/skills/modify-self/SKILL.md +12 -0
  246. package/templates/skills/youtube-transcript/SKILL.md +41 -0
  247. package/templates/skills/youtube-transcript/package-lock.json +24 -0
  248. package/templates/skills/youtube-transcript/package.json +8 -0
  249. package/templates/skills/youtube-transcript/transcript.js +84 -0
@@ -0,0 +1,377 @@
1
+ import { execSync } from 'child_process';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import os from 'os';
5
+ import * as clack from '@clack/prompts';
6
+ import { loadEnvFile } from './lib/env.mjs';
7
+ import { updateEnvVariable } from './lib/auth.mjs';
8
+
9
+ // ─── Ollama helpers ───────────────────────────────────────────────────────────
10
+
11
+ const OLLAMA_BASE_URL = 'http://localhost:11434';
12
+
13
+ async function checkOllamaRunning() {
14
+ try {
15
+ const res = await fetch(`${OLLAMA_BASE_URL}/api/tags`, {
16
+ signal: AbortSignal.timeout(3000),
17
+ });
18
+ return res.ok;
19
+ } catch {
20
+ return false;
21
+ }
22
+ }
23
+
24
+ async function listOllamaModels() {
25
+ try {
26
+ const res = await fetch(`${OLLAMA_BASE_URL}/api/tags`, {
27
+ signal: AbortSignal.timeout(5000),
28
+ });
29
+ if (!res.ok) return [];
30
+ const data = await res.json();
31
+ return (data.models || []).map((m) => m.name);
32
+ } catch {
33
+ return [];
34
+ }
35
+ }
36
+
37
+ // ─── Hardware detection ───────────────────────────────────────────────────────
38
+
39
+ function detectRAMgb() {
40
+ try {
41
+ const totalBytes = os.totalmem();
42
+ return Math.floor(totalBytes / (1024 ** 3));
43
+ } catch {
44
+ return 8; // safe default
45
+ }
46
+ }
47
+
48
+ function recommendModel(ramGb) {
49
+ if (ramGb >= 64) return { model: 'llama3.1:70b', reason: '64 GB+ RAM — full precision 70B' };
50
+ if (ramGb >= 32) return { model: 'llama3.1:70b-q4_0', reason: '32 GB RAM — quantised 70B (Q4)' };
51
+ if (ramGb >= 16) return { model: 'llama3.1:8b', reason: '16 GB RAM — 8B model' };
52
+ return { model: 'llama3.2:3b', reason: '8 GB or less RAM — 3B model' };
53
+ }
54
+
55
+ // ─── .env writer ─────────────────────────────────────────────────────────────
56
+
57
+ function writeLocalEnv(vars) {
58
+ const envPath = path.join(process.cwd(), '.env');
59
+ let content = '';
60
+
61
+ if (fs.existsSync(envPath)) {
62
+ content = fs.readFileSync(envPath, 'utf-8');
63
+ }
64
+
65
+ for (const [key, value] of Object.entries(vars)) {
66
+ const regex = new RegExp(`^${key}=.*$`, 'm');
67
+ if (regex.test(content)) {
68
+ content = content.replace(regex, `${key}=${value}`);
69
+ } else {
70
+ content = content.trimEnd() + `\n${key}=${value}\n`;
71
+ }
72
+ }
73
+
74
+ fs.writeFileSync(envPath, content);
75
+ }
76
+
77
+ function generateSecret(length = 32) {
78
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
79
+ let result = '';
80
+ for (let i = 0; i < length; i++) {
81
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
82
+ }
83
+ return result;
84
+ }
85
+
86
+ // ─── Main wizard ──────────────────────────────────────────────────────────────
87
+
88
+ export async function run() {
89
+ clack.intro('Local Mode — Offline Setup Wizard');
90
+
91
+ // ─── Caution banner ───────────────────────────────────────────────────────
92
+ clack.note(
93
+ [
94
+ 'What you are giving up in Local Mode:',
95
+ ' ✗ Telegram bot integration (no internet = no Telegram webhooks)',
96
+ ' ✗ GitHub-triggered jobs (no GitHub Actions runner)',
97
+ ' ✗ ngrok tunnel (no public URL)',
98
+ ' ✗ Automatic upgrades via GitHub Actions',
99
+ '',
100
+ 'What still works perfectly:',
101
+ ' ✓ Web chat interface at http://localhost:3000',
102
+ ' ✓ Cron-scheduled jobs (internal scheduler)',
103
+ ' ✓ Ollama LLM inference — any model you have pulled',
104
+ ' ✓ File uploads and AI vision (multimodal models)',
105
+ ' ✓ API key management',
106
+ ' ✓ Persistent conversation history (SQLite)',
107
+ ' ✓ Job queue and Swarm view',
108
+ ' ✓ Push notifications via Ntfy (LAN only, optional)',
109
+ ].join('\n'),
110
+ 'Local Mode — Capabilities'
111
+ );
112
+
113
+ const proceed = await clack.confirm({
114
+ message: 'Continue with Local Mode setup?',
115
+ initialValue: true,
116
+ });
117
+
118
+ if (clack.isCancel(proceed) || !proceed) {
119
+ clack.cancel('Setup cancelled. Run npm run setup again to choose Cloud Mode.');
120
+ process.exit(0);
121
+ }
122
+
123
+ const TOTAL_STEPS = 5;
124
+ let currentStep = 0;
125
+
126
+ const env = loadEnvFile();
127
+ if (env) {
128
+ clack.log.info('Existing .env detected — previously configured values can be skipped.');
129
+ }
130
+
131
+ // ─── Step 1: Check Ollama ─────────────────────────────────────────────────
132
+ clack.log.step(`[${++currentStep}/${TOTAL_STEPS}] Checking Ollama`);
133
+
134
+ let ollamaRunning = await checkOllamaRunning();
135
+
136
+ if (!ollamaRunning) {
137
+ clack.log.warn('Ollama is not running on localhost:11434');
138
+
139
+ const platform = process.platform;
140
+ const installInstructions =
141
+ platform === 'darwin'
142
+ ? ' macOS: brew install ollama → ollama serve'
143
+ : platform === 'win32'
144
+ ? ' Windows: https://ollama.com/download → ollama serve'
145
+ : ' Linux: curl -fsSL https://ollama.com/install.sh | sh → ollama serve';
146
+
147
+ clack.log.info(
148
+ 'Install and start Ollama:\n\n' +
149
+ installInstructions + '\n\n' +
150
+ ' Then press Enter here to retry.'
151
+ );
152
+
153
+ // Retry loop
154
+ let retries = 0;
155
+ while (!ollamaRunning && retries < 5) {
156
+ await clack.text({
157
+ message: 'Press Enter once Ollama is running (or type "skip" to continue without it):',
158
+ }).then(async (val) => {
159
+ if (clack.isCancel(val)) { clack.cancel('Setup cancelled.'); process.exit(0); }
160
+ if (val === 'skip') { ollamaRunning = 'skipped'; return; }
161
+ const s = clack.spinner();
162
+ s.start('Checking Ollama...');
163
+ ollamaRunning = await checkOllamaRunning();
164
+ s.stop(ollamaRunning ? 'Ollama is running' : 'Still not reachable');
165
+ });
166
+ retries++;
167
+ }
168
+
169
+ if (!ollamaRunning) {
170
+ clack.log.warn('Ollama not detected. Continuing — you can start it later.');
171
+ }
172
+ } else {
173
+ clack.log.success('Ollama is running at localhost:11434');
174
+ }
175
+
176
+ // ─── Step 2: Model selection ──────────────────────────────────────────────
177
+ clack.log.step(`[${++currentStep}/${TOTAL_STEPS}] LLM Model Selection`);
178
+
179
+ const ramGb = detectRAMgb();
180
+ const { model: recommendedModel, reason } = recommendModel(ramGb);
181
+ clack.log.info(`Detected RAM: ${ramGb} GB → Recommended model: ${recommendedModel} (${reason})`);
182
+
183
+ let selectedModel = recommendedModel;
184
+
185
+ if (ollamaRunning && ollamaRunning !== 'skipped') {
186
+ const pulledModels = await listOllamaModels();
187
+
188
+ if (pulledModels.length > 0) {
189
+ clack.log.info(`You have ${pulledModels.length} model(s) already pulled.`);
190
+
191
+ const modelChoice = await clack.select({
192
+ message: 'Choose a model to use:',
193
+ options: [
194
+ ...pulledModels.map((m) => ({
195
+ value: m,
196
+ label: m,
197
+ hint: m === recommendedModel ? '← recommended for your RAM' : '',
198
+ })),
199
+ { value: '__custom__', label: 'Type a different model name' },
200
+ ],
201
+ });
202
+
203
+ if (clack.isCancel(modelChoice)) { clack.cancel('Setup cancelled.'); process.exit(0); }
204
+
205
+ if (modelChoice === '__custom__') {
206
+ const customModel = await clack.text({
207
+ message: 'Enter model name (e.g. mistral:7b, qwen2.5:3b):',
208
+ validate: (v) => (!v ? 'Model name is required' : undefined),
209
+ });
210
+ if (clack.isCancel(customModel)) { clack.cancel('Setup cancelled.'); process.exit(0); }
211
+ selectedModel = customModel;
212
+ } else {
213
+ selectedModel = modelChoice;
214
+ }
215
+ } else {
216
+ // No models pulled yet — show recommendation and pull command
217
+ clack.log.warn('No models pulled yet.');
218
+ clack.log.info(
219
+ `Recommended for your hardware (${ramGb} GB RAM):\n\n` +
220
+ ` ollama pull ${recommendedModel}\n\n` +
221
+ 'You can pull it now in another terminal, then press Enter to continue.\n' +
222
+ 'Or type a different model name below.'
223
+ );
224
+
225
+ const customModel = await clack.text({
226
+ message: `Model to use (default: ${recommendedModel}):`,
227
+ placeholder: recommendedModel,
228
+ });
229
+ if (clack.isCancel(customModel)) { clack.cancel('Setup cancelled.'); process.exit(0); }
230
+ selectedModel = customModel || recommendedModel;
231
+ }
232
+ } else {
233
+ // Ollama not running — use recommendation or let user type
234
+ const customModel = await clack.text({
235
+ message: `Model to use (default: ${recommendedModel}):`,
236
+ placeholder: recommendedModel,
237
+ initialValue: recommendedModel,
238
+ });
239
+ if (clack.isCancel(customModel)) { clack.cancel('Setup cancelled.'); process.exit(0); }
240
+ selectedModel = customModel || recommendedModel;
241
+ }
242
+
243
+ clack.log.success(`Model selected: ${selectedModel}`);
244
+
245
+ // ─── Step 3: Auth secrets ─────────────────────────────────────────────────
246
+ clack.log.step(`[${++currentStep}/${TOTAL_STEPS}] Generating Auth Secrets`);
247
+
248
+ const authSecret = env?.AUTH_SECRET || generateSecret(32);
249
+ const nextAuthSecret = env?.NEXTAUTH_SECRET || generateSecret(32);
250
+
251
+ clack.log.success('Auth secrets ready');
252
+
253
+ // ─── Step 4: Write .env ───────────────────────────────────────────────────
254
+ clack.log.step(`[${++currentStep}/${TOTAL_STEPS}] Writing Configuration`);
255
+
256
+ const envVars = {
257
+ GIGABOT_MODE: 'local',
258
+ LLM_PROVIDER: 'ollama',
259
+ LLM_MODEL: selectedModel,
260
+ OLLAMA_BASE_URL: 'http://localhost:11434',
261
+ NEXTAUTH_URL: 'http://localhost:3000',
262
+ NEXTAUTH_SECRET: nextAuthSecret,
263
+ AUTH_SECRET: authSecret,
264
+ };
265
+
266
+ writeLocalEnv(envVars);
267
+ clack.log.success('.env written with local configuration');
268
+
269
+ // ─── Step 5: Start server ─────────────────────────────────────────────────
270
+ clack.log.step(`[${++currentStep}/${TOTAL_STEPS}] How to start GigaBot`);
271
+
272
+ const composeFile = fs.existsSync(path.join(process.cwd(), 'docker-compose.local.yml'))
273
+ ? 'docker-compose.local.yml'
274
+ : 'docker-compose.yml';
275
+
276
+ // Check if the server is already running
277
+ let serverRunning = false;
278
+ try {
279
+ await fetch('http://localhost:3000/api/ping', { signal: AbortSignal.timeout(2000) });
280
+ serverRunning = true;
281
+ } catch { /* not running */ }
282
+
283
+ if (serverRunning) {
284
+ clack.log.success('GigaBot is already running at http://localhost:3000');
285
+ } else {
286
+ // Check if Docker is available
287
+ let dockerAvailable = false;
288
+ try {
289
+ execSync('docker info', { stdio: 'pipe' });
290
+ dockerAvailable = true;
291
+ } catch { /* Docker not running or not installed */ }
292
+
293
+ // Ask user how they want to start
294
+ const startMethod = await clack.select({
295
+ message: 'How would you like to start GigaBot?',
296
+ options: [
297
+ {
298
+ value: 'dev',
299
+ label: 'npm run dev (recommended — Next.js dev server, no Docker needed)',
300
+ hint: 'fastest to start, hot-reload enabled',
301
+ },
302
+ {
303
+ value: 'docker',
304
+ label: `docker compose (${dockerAvailable ? 'Docker detected ✓' : 'Docker not detected ✗ — install Docker first'})`,
305
+ hint: dockerAvailable ? 'builds from local source, no registry login needed' : 'https://docs.docker.com/get-docker/',
306
+ },
307
+ {
308
+ value: 'later',
309
+ label: 'Start later — just show me the commands',
310
+ },
311
+ ],
312
+ });
313
+
314
+ if (clack.isCancel(startMethod) || startMethod === 'later') {
315
+ clack.log.info('No problem — start GigaBot when you are ready.');
316
+ } else if (startMethod === 'dev') {
317
+ clack.log.info('Starting GigaBot with npm run dev...');
318
+ clack.log.info('(Press Ctrl+C to stop the server)');
319
+ clack.outro('Chat with your agent at http://localhost:3000');
320
+ // exec npm run dev in the foreground so the user sees the output
321
+ try {
322
+ execSync('npm run dev', { stdio: 'inherit', cwd: process.cwd() });
323
+ } catch {
324
+ // User pressed Ctrl+C — normal exit, not an error
325
+ }
326
+ return;
327
+ } else if (startMethod === 'docker') {
328
+ if (!dockerAvailable) {
329
+ clack.log.warn(
330
+ 'Docker is not running or not installed.\n' +
331
+ ' Install Docker: https://docs.docker.com/get-docker/\n' +
332
+ ' Then run: docker compose -f ' + composeFile + ' up -d'
333
+ );
334
+ } else {
335
+ clack.log.info(`Building and starting GigaBot with Docker...\n (First build takes ~2-3 minutes — subsequent starts are instant)`);
336
+ try {
337
+ execSync(`docker compose -f ${composeFile} up -d --build`, { stdio: 'inherit' });
338
+ clack.log.success('GigaBot started via Docker');
339
+ } catch {
340
+ clack.log.warn(
341
+ 'Docker start failed. Try the dev server instead:\n\n' +
342
+ ' npm run dev\n'
343
+ );
344
+ }
345
+ }
346
+ }
347
+ }
348
+
349
+ // ─── Summary ──────────────────────────────────────────────────────────────
350
+ clack.note(
351
+ [
352
+ `Mode: Local (offline)`,
353
+ `LLM: Ollama → ${selectedModel}`,
354
+ `Ollama URL: http://localhost:11434`,
355
+ `App URL: http://localhost:3000`,
356
+ `Database: SQLite (local)`,
357
+ '',
358
+ 'Start options:',
359
+ ' npm run dev — Next.js dev server (recommended)',
360
+ ` docker compose -f ${composeFile} up -d --build — Docker (builds from source)`,
361
+ '',
362
+ 'To pull the selected model:',
363
+ ` ollama pull ${selectedModel}`,
364
+ ].join('\n'),
365
+ 'Local Mode Configuration'
366
+ );
367
+
368
+ clack.outro('Chat with your agent at http://localhost:3000');
369
+ }
370
+
371
+ // Allow direct invocation: node setup/setup-local.mjs
372
+ if (process.argv[1] && process.argv[1].endsWith('setup-local.mjs')) {
373
+ run().catch((error) => {
374
+ clack.log.error(`Setup failed: ${error.message}`);
375
+ process.exit(1);
376
+ });
377
+ }
@@ -0,0 +1,265 @@
1
+ #!/usr/bin/env node
2
+
3
+ import chalk from 'chalk';
4
+ import open from 'open';
5
+ import * as clack from '@clack/prompts';
6
+
7
+ import { checkPrerequisites } from './lib/prerequisites.mjs';
8
+ import { setVariables, setSecrets } from './lib/github.mjs';
9
+ import { setTelegramWebhook, validateBotToken, generateVerificationCode, getBotFatherURL } from './lib/telegram.mjs';
10
+ import { confirm, keepOrReconfigure, generateTelegramWebhookSecret, promptForOptionalKey, maskSecret } from './lib/prompts.mjs';
11
+ import { updateEnvVariable } from './lib/auth.mjs';
12
+ import { runVerificationFlow } from './lib/telegram-verify.mjs';
13
+ import { loadEnvFile } from './lib/env.mjs';
14
+
15
+ const logo = `
16
+ _______ ____ __
17
+ / ____(_)___ _____ _/ __ )____ / /_
18
+ / / __/ / __ \`/ __ \`/ __ / __ \\/ __/
19
+ / /_/ / / /_/ / /_/ / /_/ / /_/ / /_
20
+ \\____/_/\\__, /\\__,_/_____/\\____/\\__/
21
+ /____/
22
+ India's Autonomous AI Agent · Powered by Gignaati
23
+ `;
24
+
25
+ async function main() {
26
+ console.log(chalk.cyan(logo));
27
+ clack.intro('Telegram Setup');
28
+ clack.log.info('Connect a Telegram bot to your agent. This wizard will walk you through creating a bot, registering a webhook, and linking your chat.');
29
+
30
+ const TOTAL_STEPS = 6;
31
+ let currentStep = 0;
32
+
33
+ // Track values for summary
34
+ let botUsername = null;
35
+ let webhookUrl = null;
36
+ let telegramChatId = null;
37
+
38
+ // ─── Step 1: Prerequisites ──────────────────────────────────────────
39
+ clack.log.step(`[${++currentStep}/${TOTAL_STEPS}] Checking prerequisites`);
40
+ clack.log.info('Verifying git remote and loading existing configuration.');
41
+
42
+ const prereqs = await checkPrerequisites();
43
+
44
+ if (!prereqs.git.remoteInfo) {
45
+ clack.log.error('Could not detect GitHub repository from git remote.');
46
+ clack.cancel('Run setup first to initialize your repository.');
47
+ process.exit(1);
48
+ }
49
+
50
+ const { owner, repo } = prereqs.git.remoteInfo;
51
+ clack.log.success(`Repository: ${owner}/${repo}`);
52
+
53
+ const env = loadEnvFile();
54
+
55
+ if (env) {
56
+ clack.log.info('Existing .env detected — previously configured values can be skipped.');
57
+ }
58
+
59
+ // ─── Step 2: App URL ────────────────────────────────────────────────
60
+ clack.log.step(`[${++currentStep}/${TOTAL_STEPS}] App URL`);
61
+ clack.log.info('Your bot needs a public HTTPS URL so Telegram can deliver messages to it via webhook.');
62
+ clack.log.warn('Make sure your server is running and publicly accessible.');
63
+
64
+ let appUrl = null;
65
+
66
+ if (await keepOrReconfigure('APP_URL', env?.APP_URL || null)) {
67
+ appUrl = env.APP_URL;
68
+ }
69
+
70
+ if (!appUrl) {
71
+ clack.log.info(
72
+ 'Enter the public URL where your agent is running.\n' +
73
+ ' Examples:\n' +
74
+ ' ngrok: https://abc123.ngrok.io\n' +
75
+ ' VPS: https://mybot.example.com\n' +
76
+ ' PaaS: https://mybot.vercel.app'
77
+ );
78
+
79
+ while (!appUrl) {
80
+ const url = await clack.text({
81
+ message: 'Enter your APP_URL (https://...):',
82
+ validate: (input) => {
83
+ if (!input) return 'URL is required';
84
+ if (!input.startsWith('https://')) return 'URL must start with https://';
85
+ },
86
+ });
87
+ if (clack.isCancel(url)) {
88
+ clack.cancel('Setup cancelled.');
89
+ process.exit(0);
90
+ }
91
+ appUrl = url.replace(/\/$/, '');
92
+ }
93
+ }
94
+
95
+ // Update APP_URL and APP_HOSTNAME in .env
96
+ const appHostname = new URL(appUrl).hostname;
97
+ updateEnvVariable('APP_URL', appUrl);
98
+ updateEnvVariable('APP_HOSTNAME', appHostname);
99
+ clack.log.success('APP_URL saved to .env');
100
+
101
+ // Set APP_URL variable on GitHub
102
+ const urlSpinner = clack.spinner();
103
+ urlSpinner.start('Updating APP_URL variable on GitHub...');
104
+ const urlResult = await setVariables(owner, repo, { APP_URL: appUrl });
105
+ if (urlResult.APP_URL.success) {
106
+ urlSpinner.stop('APP_URL variable updated on GitHub');
107
+ } else {
108
+ urlSpinner.stop(`Failed: ${urlResult.APP_URL.error}`);
109
+ }
110
+
111
+ // ─── Step 3: Bot Token ──────────────────────────────────────────────
112
+ clack.log.step(`[${++currentStep}/${TOTAL_STEPS}] Telegram Bot Token`);
113
+ clack.log.info('Your agent needs a Telegram bot token from @BotFather to send and receive messages.');
114
+
115
+ let token = null;
116
+
117
+ if (await keepOrReconfigure('Telegram Bot', env?.TELEGRAM_BOT_TOKEN ? maskSecret(env.TELEGRAM_BOT_TOKEN) : null)) {
118
+ // Validate existing token
119
+ token = env.TELEGRAM_BOT_TOKEN;
120
+ const validateSpinner = clack.spinner();
121
+ validateSpinner.start('Validating existing bot token...');
122
+ const validation = await validateBotToken(token);
123
+ if (validation.valid) {
124
+ botUsername = validation.botInfo.username;
125
+ validateSpinner.stop(`Bot: @${botUsername}`);
126
+ } else {
127
+ validateSpinner.stop(`Invalid token in .env: ${validation.error}`);
128
+ clack.log.warn('Existing token is invalid — you\'ll need to enter a new one.');
129
+ token = null;
130
+ }
131
+ }
132
+
133
+ if (!token) {
134
+ clack.log.info(
135
+ 'Create a Telegram bot via @BotFather:\n' +
136
+ ' 1. Open @BotFather in Telegram\n' +
137
+ ' 2. Send /newbot and follow the prompts\n' +
138
+ ' 3. Copy the bot token'
139
+ );
140
+
141
+ const openBotFather = await confirm('Open BotFather in browser?');
142
+ if (openBotFather) {
143
+ await open(getBotFatherURL());
144
+ }
145
+
146
+ let tokenValid = false;
147
+ while (!tokenValid) {
148
+ const inputToken = await clack.password({
149
+ message: 'Telegram bot token:',
150
+ validate: (input) => {
151
+ if (!input) return 'Token is required';
152
+ if (!/^\d+:[A-Za-z0-9_-]+$/.test(input)) {
153
+ return 'Invalid format. Should be like 123456789:ABC-DEF...';
154
+ }
155
+ },
156
+ });
157
+ if (clack.isCancel(inputToken)) {
158
+ clack.cancel('Setup cancelled.');
159
+ process.exit(0);
160
+ }
161
+
162
+ const validateSpinner = clack.spinner();
163
+ validateSpinner.start('Validating bot token...');
164
+ const validation = await validateBotToken(inputToken);
165
+
166
+ if (!validation.valid) {
167
+ validateSpinner.stop(`Invalid token: ${validation.error}`);
168
+ continue;
169
+ }
170
+
171
+ token = inputToken;
172
+ botUsername = validation.botInfo.username;
173
+ validateSpinner.stop(`Bot: @${botUsername}`);
174
+ tokenValid = true;
175
+ }
176
+ }
177
+
178
+ // Bug fix #146: save token to .env
179
+ updateEnvVariable('TELEGRAM_BOT_TOKEN', token);
180
+
181
+ // ─── Step 4: Webhook ────────────────────────────────────────────────
182
+ clack.log.step(`[${++currentStep}/${TOTAL_STEPS}] Register Webhook`);
183
+ clack.log.info('Registering a webhook tells Telegram where to send messages for your bot.');
184
+
185
+ // Handle webhook secret
186
+ let webhookSecret = env?.TELEGRAM_WEBHOOK_SECRET;
187
+ if (webhookSecret) {
188
+ clack.log.success('Using existing webhook secret');
189
+ } else {
190
+ webhookSecret = await generateTelegramWebhookSecret();
191
+ updateEnvVariable('TELEGRAM_WEBHOOK_SECRET', webhookSecret);
192
+ clack.log.success('Generated webhook secret');
193
+ }
194
+
195
+ // Register Telegram webhook
196
+ webhookUrl = `${appUrl}/api/telegram/webhook`;
197
+ const tgSpinner = clack.spinner();
198
+ tgSpinner.start('Registering Telegram webhook...');
199
+ const tgResult = await setTelegramWebhook(token, webhookUrl, webhookSecret);
200
+ if (tgResult.ok) {
201
+ tgSpinner.stop('Telegram webhook registered');
202
+ } else {
203
+ tgSpinner.stop(`Failed: ${tgResult.description}`);
204
+ }
205
+
206
+ // ─── Step 5: Chat Verification ──────────────────────────────────────
207
+ clack.log.step(`[${++currentStep}/${TOTAL_STEPS}] Chat Verification`);
208
+ clack.log.info('Link the bot to your Telegram chat so it only responds to you.');
209
+
210
+ telegramChatId = env?.TELEGRAM_CHAT_ID || null;
211
+
212
+ if (await keepOrReconfigure('Chat ID', telegramChatId)) {
213
+ // Keep existing — already set
214
+ } else {
215
+ telegramChatId = null;
216
+ const verificationCode = generateVerificationCode();
217
+ updateEnvVariable('TELEGRAM_VERIFICATION', verificationCode);
218
+
219
+ clack.log.warn('Waiting for server to restart with new verification code...');
220
+ await new Promise(resolve => setTimeout(resolve, 3000));
221
+
222
+ const chatId = await runVerificationFlow(verificationCode, { allowSkip: true });
223
+
224
+ if (chatId) {
225
+ telegramChatId = chatId;
226
+ updateEnvVariable('TELEGRAM_CHAT_ID', chatId);
227
+ clack.log.success(`Chat ID saved: ${chatId}`);
228
+ } else {
229
+ clack.log.warn('Chat ID is required — the bot will not respond without it.');
230
+ clack.log.info('Run npm run setup-telegram again to complete setup.');
231
+ }
232
+ }
233
+
234
+ // ─── Step 6: Optional Keys ──────────────────────────────────────────
235
+ clack.log.step(`[${++currentStep}/${TOTAL_STEPS}] Optional Keys`);
236
+ clack.log.info('An OpenAI API key enables voice message transcription via Whisper.');
237
+
238
+ if (await keepOrReconfigure('OpenAI key', env?.OPENAI_API_KEY ? maskSecret(env.OPENAI_API_KEY) : null)) {
239
+ // Keep existing
240
+ } else {
241
+ const openaiKey = await promptForOptionalKey('openai', 'voice messages');
242
+ if (openaiKey) {
243
+ updateEnvVariable('OPENAI_API_KEY', openaiKey);
244
+ const s = clack.spinner();
245
+ s.start('Setting OpenAI secret on GitHub...');
246
+ await setSecrets(owner, repo, { AGENT_OPENAI_API_KEY: openaiKey });
247
+ s.stop('OpenAI secret set');
248
+ clack.log.success(`OpenAI key added for voice (${maskSecret(openaiKey)})`);
249
+ }
250
+ }
251
+
252
+ // ─── Summary ────────────────────────────────────────────────────────
253
+ let summary = '';
254
+ summary += `Bot: @${botUsername || '(unknown)'}\n`;
255
+ summary += `Webhook: ${webhookUrl}\n`;
256
+ summary += `Chat ID: ${telegramChatId || '(not set)'}`;
257
+ clack.note(summary, 'Telegram Configuration');
258
+
259
+ clack.outro('Telegram setup complete!');
260
+ }
261
+
262
+ main().catch((error) => {
263
+ clack.log.error(`Failed: ${error.message}`);
264
+ process.exit(1);
265
+ });