skimpyclaw 0.1.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 (219) hide show
  1. package/README.md +230 -0
  2. package/dist/__tests__/agent.test.d.ts +1 -0
  3. package/dist/__tests__/agent.test.js +131 -0
  4. package/dist/__tests__/api.test.d.ts +1 -0
  5. package/dist/__tests__/api.test.js +1227 -0
  6. package/dist/__tests__/audit.test.d.ts +1 -0
  7. package/dist/__tests__/audit.test.js +122 -0
  8. package/dist/__tests__/cache.test.d.ts +1 -0
  9. package/dist/__tests__/cache.test.js +65 -0
  10. package/dist/__tests__/channels.test.d.ts +1 -0
  11. package/dist/__tests__/channels.test.js +85 -0
  12. package/dist/__tests__/cli.integration.test.d.ts +1 -0
  13. package/dist/__tests__/cli.integration.test.js +16 -0
  14. package/dist/__tests__/cli.test.d.ts +1 -0
  15. package/dist/__tests__/cli.test.js +230 -0
  16. package/dist/__tests__/code-agents-executor.test.d.ts +1 -0
  17. package/dist/__tests__/code-agents-executor.test.js +75 -0
  18. package/dist/__tests__/code-agents-orchestrator.test.d.ts +1 -0
  19. package/dist/__tests__/code-agents-orchestrator.test.js +149 -0
  20. package/dist/__tests__/code-agents-parser.test.d.ts +1 -0
  21. package/dist/__tests__/code-agents-parser.test.js +39 -0
  22. package/dist/__tests__/code-agents-utils.test.d.ts +1 -0
  23. package/dist/__tests__/code-agents-utils.test.js +41 -0
  24. package/dist/__tests__/config.test.d.ts +1 -0
  25. package/dist/__tests__/config.test.js +46 -0
  26. package/dist/__tests__/cron.test.d.ts +1 -0
  27. package/dist/__tests__/cron.test.js +66 -0
  28. package/dist/__tests__/dashboard-mode.test.d.ts +1 -0
  29. package/dist/__tests__/dashboard-mode.test.js +145 -0
  30. package/dist/__tests__/dashboard.test.d.ts +1 -0
  31. package/dist/__tests__/dashboard.test.js +43 -0
  32. package/dist/__tests__/doctor.formatters.test.d.ts +1 -0
  33. package/dist/__tests__/doctor.formatters.test.js +65 -0
  34. package/dist/__tests__/doctor.index.test.d.ts +1 -0
  35. package/dist/__tests__/doctor.index.test.js +48 -0
  36. package/dist/__tests__/doctor.runner.test.d.ts +1 -0
  37. package/dist/__tests__/doctor.runner.test.js +204 -0
  38. package/dist/__tests__/exec-approval.test.d.ts +1 -0
  39. package/dist/__tests__/exec-approval.test.js +323 -0
  40. package/dist/__tests__/file-lock.test.d.ts +1 -0
  41. package/dist/__tests__/file-lock.test.js +92 -0
  42. package/dist/__tests__/langfuse.test.d.ts +1 -0
  43. package/dist/__tests__/langfuse.test.js +40 -0
  44. package/dist/__tests__/model-selection.test.d.ts +1 -0
  45. package/dist/__tests__/model-selection.test.js +62 -0
  46. package/dist/__tests__/orchestrator.test.d.ts +1 -0
  47. package/dist/__tests__/orchestrator.test.js +425 -0
  48. package/dist/__tests__/providers-init.test.d.ts +1 -0
  49. package/dist/__tests__/providers-init.test.js +32 -0
  50. package/dist/__tests__/providers-routing.test.d.ts +1 -0
  51. package/dist/__tests__/providers-routing.test.js +25 -0
  52. package/dist/__tests__/providers-utils.test.d.ts +1 -0
  53. package/dist/__tests__/providers-utils.test.js +54 -0
  54. package/dist/__tests__/security.test.d.ts +1 -0
  55. package/dist/__tests__/security.test.js +22 -0
  56. package/dist/__tests__/sessions.test.d.ts +1 -0
  57. package/dist/__tests__/sessions.test.js +147 -0
  58. package/dist/__tests__/setup.test.d.ts +1 -0
  59. package/dist/__tests__/setup.test.js +114 -0
  60. package/dist/__tests__/skills.test.d.ts +1 -0
  61. package/dist/__tests__/skills.test.js +333 -0
  62. package/dist/__tests__/subagent.test.d.ts +1 -0
  63. package/dist/__tests__/subagent.test.js +240 -0
  64. package/dist/__tests__/telegram-utils.test.d.ts +1 -0
  65. package/dist/__tests__/telegram-utils.test.js +22 -0
  66. package/dist/__tests__/telegram.test.d.ts +1 -0
  67. package/dist/__tests__/telegram.test.js +42 -0
  68. package/dist/__tests__/token-efficiency.test.d.ts +1 -0
  69. package/dist/__tests__/token-efficiency.test.js +38 -0
  70. package/dist/__tests__/tool-guard.test.d.ts +1 -0
  71. package/dist/__tests__/tool-guard.test.js +105 -0
  72. package/dist/__tests__/tools.test.d.ts +1 -0
  73. package/dist/__tests__/tools.test.js +589 -0
  74. package/dist/__tests__/usage.test.d.ts +1 -0
  75. package/dist/__tests__/usage.test.js +197 -0
  76. package/dist/__tests__/voice.test.d.ts +1 -0
  77. package/dist/__tests__/voice.test.js +214 -0
  78. package/dist/agent.d.ts +24 -0
  79. package/dist/agent.js +269 -0
  80. package/dist/api.d.ts +3 -0
  81. package/dist/api.js +943 -0
  82. package/dist/audit.d.ts +26 -0
  83. package/dist/audit.js +121 -0
  84. package/dist/cache.d.ts +8 -0
  85. package/dist/cache.js +24 -0
  86. package/dist/channels/telegram/handlers.d.ts +41 -0
  87. package/dist/channels/telegram/handlers.js +498 -0
  88. package/dist/channels/telegram/index.d.ts +14 -0
  89. package/dist/channels/telegram/index.js +326 -0
  90. package/dist/channels/telegram/types.d.ts +26 -0
  91. package/dist/channels/telegram/types.js +31 -0
  92. package/dist/channels/telegram/utils.d.ts +25 -0
  93. package/dist/channels/telegram/utils.js +256 -0
  94. package/dist/channels.d.ts +11 -0
  95. package/dist/channels.js +118 -0
  96. package/dist/cli.d.ts +5 -0
  97. package/dist/cli.js +768 -0
  98. package/dist/code-agents/executor.d.ts +5 -0
  99. package/dist/code-agents/executor.js +463 -0
  100. package/dist/code-agents/index.d.ts +22 -0
  101. package/dist/code-agents/index.js +199 -0
  102. package/dist/code-agents/orchestrator.d.ts +23 -0
  103. package/dist/code-agents/orchestrator.js +403 -0
  104. package/dist/code-agents/parser.d.ts +21 -0
  105. package/dist/code-agents/parser.js +197 -0
  106. package/dist/code-agents/registry.d.ts +27 -0
  107. package/dist/code-agents/registry.js +147 -0
  108. package/dist/code-agents/types.d.ts +66 -0
  109. package/dist/code-agents/types.js +4 -0
  110. package/dist/code-agents/utils.d.ts +36 -0
  111. package/dist/code-agents/utils.js +236 -0
  112. package/dist/config.d.ts +19 -0
  113. package/dist/config.js +123 -0
  114. package/dist/cron.d.ts +49 -0
  115. package/dist/cron.js +400 -0
  116. package/dist/dashboard/assets/index-CZJCvMSN.js +65 -0
  117. package/dist/dashboard/assets/index-EAg6lqF5.css +1 -0
  118. package/dist/dashboard/favicon.svg +3 -0
  119. package/dist/dashboard/index.html +21 -0
  120. package/dist/dashboard-frontend.d.ts +7 -0
  121. package/dist/dashboard-frontend.js +86 -0
  122. package/dist/dashboard.d.ts +8 -0
  123. package/dist/dashboard.js +4071 -0
  124. package/dist/digests.d.ts +36 -0
  125. package/dist/digests.js +338 -0
  126. package/dist/discord.d.ts +8 -0
  127. package/dist/discord.js +828 -0
  128. package/dist/doctor/checks.d.ts +18 -0
  129. package/dist/doctor/checks.js +368 -0
  130. package/dist/doctor/formatters.d.ts +3 -0
  131. package/dist/doctor/formatters.js +44 -0
  132. package/dist/doctor/index.d.ts +8 -0
  133. package/dist/doctor/index.js +7 -0
  134. package/dist/doctor/runner.d.ts +3 -0
  135. package/dist/doctor/runner.js +109 -0
  136. package/dist/doctor/types.d.ts +20 -0
  137. package/dist/doctor/types.js +1 -0
  138. package/dist/exec-approval.d.ts +101 -0
  139. package/dist/exec-approval.js +432 -0
  140. package/dist/file-lock.d.ts +34 -0
  141. package/dist/file-lock.js +81 -0
  142. package/dist/gateway.d.ts +8 -0
  143. package/dist/gateway.js +114 -0
  144. package/dist/heartbeat.d.ts +4 -0
  145. package/dist/heartbeat.js +101 -0
  146. package/dist/index.d.ts +1 -0
  147. package/dist/index.js +75 -0
  148. package/dist/langfuse.d.ts +34 -0
  149. package/dist/langfuse.js +145 -0
  150. package/dist/mcp-context-a8c.d.ts +13 -0
  151. package/dist/mcp-context-a8c.js +34 -0
  152. package/dist/model-selection.d.ts +18 -0
  153. package/dist/model-selection.js +50 -0
  154. package/dist/orchestrator.d.ts +15 -0
  155. package/dist/orchestrator.js +676 -0
  156. package/dist/providers/anthropic.d.ts +7 -0
  157. package/dist/providers/anthropic.js +319 -0
  158. package/dist/providers/codex.d.ts +17 -0
  159. package/dist/providers/codex.js +508 -0
  160. package/dist/providers/content.d.ts +21 -0
  161. package/dist/providers/content.js +55 -0
  162. package/dist/providers/index.d.ts +13 -0
  163. package/dist/providers/index.js +138 -0
  164. package/dist/providers/observability.d.ts +19 -0
  165. package/dist/providers/observability.js +94 -0
  166. package/dist/providers/openai.d.ts +10 -0
  167. package/dist/providers/openai.js +310 -0
  168. package/dist/providers/tool-guard.d.ts +30 -0
  169. package/dist/providers/tool-guard.js +89 -0
  170. package/dist/providers/types.d.ts +34 -0
  171. package/dist/providers/types.js +2 -0
  172. package/dist/providers/utils.d.ts +65 -0
  173. package/dist/providers/utils.js +199 -0
  174. package/dist/security.d.ts +8 -0
  175. package/dist/security.js +113 -0
  176. package/dist/service.d.ts +8 -0
  177. package/dist/service.js +38 -0
  178. package/dist/sessions.d.ts +35 -0
  179. package/dist/sessions.js +142 -0
  180. package/dist/setup.d.ts +36 -0
  181. package/dist/setup.js +821 -0
  182. package/dist/skills-types.d.ts +65 -0
  183. package/dist/skills-types.js +2 -0
  184. package/dist/skills.d.ts +32 -0
  185. package/dist/skills.js +260 -0
  186. package/dist/subagent.d.ts +19 -0
  187. package/dist/subagent.js +376 -0
  188. package/dist/telegram.d.ts +2 -0
  189. package/dist/telegram.js +11 -0
  190. package/dist/tools/bash-tool.d.ts +3 -0
  191. package/dist/tools/bash-tool.js +59 -0
  192. package/dist/tools/browser-tool.d.ts +3 -0
  193. package/dist/tools/browser-tool.js +265 -0
  194. package/dist/tools/definitions.d.ts +432 -0
  195. package/dist/tools/definitions.js +181 -0
  196. package/dist/tools/execute-context.d.ts +26 -0
  197. package/dist/tools/execute-context.js +1 -0
  198. package/dist/tools/file-tools.d.ts +8 -0
  199. package/dist/tools/file-tools.js +67 -0
  200. package/dist/tools/path-utils.d.ts +1 -0
  201. package/dist/tools/path-utils.js +8 -0
  202. package/dist/tools.d.ts +24 -0
  203. package/dist/tools.js +281 -0
  204. package/dist/types.d.ts +259 -0
  205. package/dist/types.js +2 -0
  206. package/dist/usage.d.ts +76 -0
  207. package/dist/usage.js +150 -0
  208. package/dist/voice.d.ts +37 -0
  209. package/dist/voice.js +461 -0
  210. package/package.json +70 -0
  211. package/templates/AGENTS.md +38 -0
  212. package/templates/BOOT.md +23 -0
  213. package/templates/BOOTSTRAP.md +26 -0
  214. package/templates/HEARTBEAT.md +5 -0
  215. package/templates/IDENTITY.md +5 -0
  216. package/templates/MEMORY.md +24 -0
  217. package/templates/SOUL.md +92 -0
  218. package/templates/TOOLS.md +30 -0
  219. package/templates/USER.md +31 -0
package/dist/setup.js ADDED
@@ -0,0 +1,821 @@
1
+ // Interactive setup wizard for SkimpyClaw
2
+ import * as readline from 'readline';
3
+ import { writeFileSync, mkdirSync, existsSync, copyFileSync, readdirSync, readFileSync } from 'fs';
4
+ import { join, dirname } from 'path';
5
+ import { homedir } from 'os';
6
+ import { fileURLToPath } from 'url';
7
+ import { spawnSync } from 'child_process';
8
+ import { randomUUID } from 'crypto';
9
+ import { runDoctor as runDoctorChecks } from './doctor/runner.js';
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = dirname(__filename);
12
+ // ANSI color helpers (no chalk dependency)
13
+ const c = {
14
+ green: (s) => `\x1b[32m${s}\x1b[0m`,
15
+ red: (s) => `\x1b[31m${s}\x1b[0m`,
16
+ yellow: (s) => `\x1b[33m${s}\x1b[0m`,
17
+ cyan: (s) => `\x1b[36m${s}\x1b[0m`,
18
+ bold: (s) => `\x1b[1m${s}\x1b[0m`,
19
+ dim: (s) => `\x1b[2m${s}\x1b[0m`,
20
+ };
21
+ function sectionHeader(title) {
22
+ console.log(`\n${c.bold(c.cyan(`─── ${title} ───`))}`);
23
+ }
24
+ function statusOk(msg) {
25
+ console.log(` ${c.green('✓')} ${msg}`);
26
+ }
27
+ function statusFail(msg) {
28
+ console.log(` ${c.red('✗')} ${msg}`);
29
+ }
30
+ function statusWarn(msg) {
31
+ console.log(` ${c.yellow('⚠')} ${msg}`);
32
+ }
33
+ const CONFIG_DIR = join(homedir(), '.skimpyclaw');
34
+ const AGENTS_DIR = join(CONFIG_DIR, 'agents', 'main');
35
+ const TEMPLATES_DIR = join(__dirname, '..', 'templates');
36
+ const GATEWAY_PLIST_LABEL = 'com.skimpyclaw.gateway';
37
+ const GATEWAY_PLIST_TEMPLATE = join(__dirname, '..', 'com.skimpyclaw.gateway.plist.example');
38
+ function loadExistingSetup() {
39
+ let config = null;
40
+ const env = {};
41
+ const configPath = join(CONFIG_DIR, 'config.json');
42
+ if (existsSync(configPath)) {
43
+ try {
44
+ config = JSON.parse(readFileSync(configPath, 'utf-8'));
45
+ }
46
+ catch { /* ignore bad config */ }
47
+ }
48
+ const envPath = join(CONFIG_DIR, '.env');
49
+ if (existsSync(envPath)) {
50
+ const lines = readFileSync(envPath, 'utf-8').split('\n');
51
+ for (const line of lines) {
52
+ const match = line.match(/^([A-Z_]+)=(.*)$/);
53
+ if (match)
54
+ env[match[1]] = match[2];
55
+ }
56
+ }
57
+ return { config, env };
58
+ }
59
+ function ask(rl, question) {
60
+ return new Promise((resolve) => {
61
+ rl.question(question, (answer) => {
62
+ resolve(answer.trim());
63
+ });
64
+ });
65
+ }
66
+ function maskInput(input) {
67
+ if (input.length <= 8)
68
+ return '****';
69
+ return input.slice(0, 4) + '****' + input.slice(-4);
70
+ }
71
+ function renderGatewayPlist(workspaceDir) {
72
+ if (!existsSync(GATEWAY_PLIST_TEMPLATE)) {
73
+ throw new Error(`Gateway launchd template not found: ${GATEWAY_PLIST_TEMPLATE}`);
74
+ }
75
+ const nodeBin = process.execPath;
76
+ const nodeBinDir = dirname(nodeBin);
77
+ const homeDir = homedir();
78
+ const pnpmBinDir = process.env.PNPM_HOME || join(homeDir, 'Library', 'pnpm');
79
+ const systemPath = process.env.PATH || '/usr/local/bin:/usr/bin:/bin';
80
+ return readFileSync(GATEWAY_PLIST_TEMPLATE, 'utf-8')
81
+ .replaceAll('__NODE_BIN__', nodeBin)
82
+ .replaceAll('__NODE_BIN_DIR__', nodeBinDir)
83
+ .replaceAll('__PNPM_BIN_DIR__', pnpmBinDir)
84
+ .replaceAll('__SYSTEM_PATH__', systemPath)
85
+ .replaceAll('__REPO_DIR__', workspaceDir)
86
+ .replaceAll('__HOME_DIR__', homeDir);
87
+ }
88
+ const PROVIDER_OPTIONS = [
89
+ { key: 'anthropic-api', label: 'Anthropic API key' },
90
+ { key: 'anthropic-oauth', label: 'Anthropic OAuth (Claude Code)' },
91
+ { key: 'openai-api', label: 'OpenAI API key' },
92
+ { key: 'codex-oauth', label: 'OpenAI Codex OAuth' },
93
+ { key: 'minimax-api', label: 'MiniMax API key' },
94
+ { key: 'kimi-api', label: 'Kimi (Moonshot) API key' },
95
+ ];
96
+ function detectExistingProviders(config) {
97
+ const existing = new Set();
98
+ if (!config?.models?.providers)
99
+ return existing;
100
+ const providers = config.models.providers;
101
+ if (providers.anthropic?.authToken)
102
+ existing.add('anthropic-oauth');
103
+ else if (providers.anthropic?.apiKey)
104
+ existing.add('anthropic-api');
105
+ if (providers.openai?.apiKey)
106
+ existing.add('openai-api');
107
+ if (providers.codex || providers.openai?.authToken === 'codex')
108
+ existing.add('codex-oauth');
109
+ if (providers.minimax)
110
+ existing.add('minimax-api');
111
+ if (providers.kimi)
112
+ existing.add('kimi-api');
113
+ return existing;
114
+ }
115
+ async function askProviders(rl, existingProviders) {
116
+ const hasExisting = existingProviders && existingProviders.size > 0;
117
+ while (true) {
118
+ sectionHeader('3. Model Providers');
119
+ if (hasExisting) {
120
+ console.log(' Currently configured:');
121
+ for (const opt of PROVIDER_OPTIONS) {
122
+ if (existingProviders.has(opt.key)) {
123
+ console.log(` ${c.green('✓')} ${opt.label}`);
124
+ }
125
+ }
126
+ console.log('');
127
+ }
128
+ console.log(' Pick providers (pick one or more):');
129
+ for (let i = 0; i < PROVIDER_OPTIONS.length; i++) {
130
+ const marker = hasExisting && existingProviders.has(PROVIDER_OPTIONS[i].key) ? c.green('*') : ' ';
131
+ console.log(` ${marker}${i + 1}. ${PROVIDER_OPTIONS[i].label}`);
132
+ }
133
+ if (hasExisting) {
134
+ console.log(` ${c.dim('Press Enter to keep current providers')}`);
135
+ }
136
+ const input = await ask(rl, ' Enter numbers separated by commas (e.g. 1,3): ');
137
+ if (input.trim() === '' && hasExisting) {
138
+ return new Set(existingProviders);
139
+ }
140
+ const nums = input.split(',').map((s) => parseInt(s.trim(), 10)).filter((n) => !isNaN(n));
141
+ const choices = new Set();
142
+ for (const n of nums) {
143
+ if (n >= 1 && n <= PROVIDER_OPTIONS.length) {
144
+ choices.add(PROVIDER_OPTIONS[n - 1].key);
145
+ }
146
+ }
147
+ if (choices.size === 0) {
148
+ console.log(' ✗ Pick at least one provider.\n');
149
+ continue;
150
+ }
151
+ // Can't pick both Anthropic API and Anthropic OAuth
152
+ if (choices.has('anthropic-api') && choices.has('anthropic-oauth')) {
153
+ console.log(' ✗ Pick either Anthropic API key or Anthropic OAuth, not both.\n');
154
+ continue;
155
+ }
156
+ return choices;
157
+ }
158
+ }
159
+ async function collectProviderSecrets(rl, providers, existingEnv) {
160
+ const secrets = {};
161
+ const env = existingEnv || {};
162
+ if (providers.has('anthropic-api')) {
163
+ const existing = env.ANTHROPIC_API_KEY || '';
164
+ console.log('\n Anthropic API Key');
165
+ if (existing) {
166
+ const input = await ask(rl, ` Enter key [${maskInput(existing)}]: `);
167
+ secrets.anthropicKey = input || existing;
168
+ }
169
+ else {
170
+ console.log(' Get one from: https://console.anthropic.com/');
171
+ secrets.anthropicKey = await ask(rl, ' Enter key: ');
172
+ }
173
+ console.log(` ✓ ${maskInput(secrets.anthropicKey)}`);
174
+ }
175
+ if (providers.has('anthropic-oauth')) {
176
+ const existing = env.CLAUDE_CODE_OAUTH_TOKEN || '';
177
+ console.log('\n Anthropic OAuth (Claude Code)');
178
+ if (existing) {
179
+ const input = await ask(rl, ` Enter token [${maskInput(existing)}]: `);
180
+ secrets.oauthToken = input || existing;
181
+ }
182
+ else {
183
+ console.log(' Run `claude setup-token` to get your token, then paste it here.');
184
+ console.log(' (The daemon can\'t read .zshrc — the token must be in ~/.skimpyclaw/.env)');
185
+ const detected = process.env.CLAUDE_CODE_OAUTH_TOKEN || '';
186
+ if (detected) {
187
+ console.log(` ${c.dim(`Detected in current shell: ${maskInput(detected)}`)}`);
188
+ }
189
+ const oauthInput = await ask(rl, detected ? ` Enter token [${maskInput(detected)}]: ` : ' Enter token: ');
190
+ secrets.oauthToken = oauthInput || detected;
191
+ }
192
+ if (secrets.oauthToken) {
193
+ console.log(` ✓ ${maskInput(secrets.oauthToken)}`);
194
+ }
195
+ else {
196
+ console.log(` ${c.yellow('⚠')} Skipped — run \`claude setup-token\`, then add CLAUDE_CODE_OAUTH_TOKEN to ~/.skimpyclaw/.env`);
197
+ }
198
+ }
199
+ if (providers.has('openai-api')) {
200
+ const existing = env.OPENAI_API_KEY || '';
201
+ console.log('\n OpenAI API Key');
202
+ if (existing) {
203
+ const input = await ask(rl, ` Enter key [${maskInput(existing)}]: `);
204
+ secrets.openaiKey = input || existing;
205
+ }
206
+ else {
207
+ console.log(' Get one from: https://platform.openai.com/api-keys');
208
+ secrets.openaiKey = await ask(rl, ' Enter key: ');
209
+ }
210
+ console.log(` ✓ ${maskInput(secrets.openaiKey)}`);
211
+ }
212
+ if (providers.has('minimax-api')) {
213
+ const existing = env.MINIMAX_API_KEY || '';
214
+ console.log('\n MiniMax API Key');
215
+ if (existing) {
216
+ const input = await ask(rl, ` Enter key [${maskInput(existing)}]: `);
217
+ secrets.minimaxKey = input || existing;
218
+ }
219
+ else {
220
+ console.log(' Get one from: https://platform.minimax.io/user-center/basic-information/interface-key');
221
+ secrets.minimaxKey = await ask(rl, ' Enter key: ');
222
+ }
223
+ console.log(` ✓ ${maskInput(secrets.minimaxKey)}`);
224
+ }
225
+ if (providers.has('kimi-api')) {
226
+ const existing = env.KIMI_API_KEY || '';
227
+ console.log('\n Kimi (Moonshot) API Key');
228
+ if (existing) {
229
+ const input = await ask(rl, ` Enter key [${maskInput(existing)}]: `);
230
+ secrets.kimiKey = input || existing;
231
+ }
232
+ else {
233
+ console.log(' Get one from: https://platform.moonshot.cn/console/api-keys');
234
+ secrets.kimiKey = await ask(rl, ' Enter key: ');
235
+ }
236
+ console.log(` ✓ ${maskInput(secrets.kimiKey)}`);
237
+ }
238
+ if (providers.has('codex-oauth')) {
239
+ console.log('\n OpenAI Codex OAuth');
240
+ console.log(' No key needed — uses ~/.codex/auth.json at runtime.');
241
+ console.log(' ✓ Will use codex auth');
242
+ }
243
+ console.log('');
244
+ return secrets;
245
+ }
246
+ function buildProviders(providers) {
247
+ const result = {};
248
+ if (providers.has('anthropic-api')) {
249
+ result.anthropic = { apiKey: '${ANTHROPIC_API_KEY}' };
250
+ }
251
+ else if (providers.has('anthropic-oauth')) {
252
+ result.anthropic = { authToken: '${CLAUDE_CODE_OAUTH_TOKEN}' };
253
+ }
254
+ if (providers.has('openai-api')) {
255
+ result.openai = { apiKey: '${OPENAI_API_KEY}', baseURL: 'https://api.openai.com/v1' };
256
+ }
257
+ if (providers.has('minimax-api')) {
258
+ result.minimax = { apiKey: '${MINIMAX_API_KEY}', baseURL: 'https://api.minimax.io/v1' };
259
+ }
260
+ if (providers.has('kimi-api')) {
261
+ result.kimi = { apiKey: '${KIMI_API_KEY}', baseURL: 'https://api.kimi.com/coding/v1' };
262
+ }
263
+ if (providers.has('codex-oauth')) {
264
+ result.codex = {
265
+ authToken: 'codex',
266
+ authPath: '${HOME}/.codex/auth.json',
267
+ baseURL: 'https://chatgpt.com/backend-api',
268
+ };
269
+ }
270
+ return result;
271
+ }
272
+ function buildDefaultModel(providers) {
273
+ const hasAnthropic = providers.has('anthropic-api') || providers.has('anthropic-oauth');
274
+ if (hasAnthropic)
275
+ return 'anthropic/claude-opus-4';
276
+ if (providers.has('codex-oauth'))
277
+ return 'codex/gpt-5.3-codex';
278
+ if (providers.has('kimi-api'))
279
+ return 'kimi/kimi-for-coding';
280
+ if (providers.has('minimax-api'))
281
+ return 'minimax/MiniMax-M2.1';
282
+ return 'openai/gpt-4o';
283
+ }
284
+ function buildAliases(providers) {
285
+ // Always include well-known aliases so users can switch models easily
286
+ const aliases = {
287
+ 'claude-fast': 'anthropic/claude-haiku-4-5',
288
+ 'claude-think': 'anthropic/claude-sonnet-4-5',
289
+ 'claude-opus': 'anthropic/claude-opus-4',
290
+ 'codex5.1': 'codex/gpt-5.1-codex',
291
+ 'codex5.2': 'codex/gpt-5.2-codex',
292
+ 'codex5.3': 'codex/gpt-5.3-codex',
293
+ 'minimax': 'minimax/MiniMax-M2.5',
294
+ 'kimi': 'kimi/kimi-for-coding',
295
+ };
296
+ if (providers.has('openai-api')) {
297
+ aliases['gpt-fast'] = 'openai/gpt-4o-mini';
298
+ aliases.gpt = 'openai/gpt-4o';
299
+ }
300
+ if (providers.has('codex-oauth')) {
301
+ aliases.codex = 'codex/gpt-5.3-codex';
302
+ }
303
+ if (providers.has('minimax-api')) {
304
+ aliases.minimax = 'minimax/MiniMax-M2.1';
305
+ }
306
+ if (providers.has('kimi-api')) {
307
+ aliases.kimi = 'kimi/kimi-for-coding';
308
+ }
309
+ return aliases;
310
+ }
311
+ function buildEnvContent(telegramToken, providers, secrets, discordToken) {
312
+ const lines = ['# SkimpyClaw secrets'];
313
+ if (providers.has('anthropic-api') && secrets.anthropicKey) {
314
+ lines.push(`ANTHROPIC_API_KEY=${secrets.anthropicKey}`);
315
+ }
316
+ if (providers.has('anthropic-oauth')) {
317
+ if (secrets.oauthToken) {
318
+ lines.push(`CLAUDE_CODE_OAUTH_TOKEN=${secrets.oauthToken}`);
319
+ }
320
+ else {
321
+ lines.push('# Anthropic OAuth — paste token here (from .zshrc or `echo $CLAUDE_CODE_OAUTH_TOKEN`)');
322
+ lines.push('CLAUDE_CODE_OAUTH_TOKEN=');
323
+ }
324
+ }
325
+ if (providers.has('openai-api') && secrets.openaiKey) {
326
+ lines.push(`OPENAI_API_KEY=${secrets.openaiKey}`);
327
+ }
328
+ if (providers.has('minimax-api') && secrets.minimaxKey) {
329
+ lines.push(`MINIMAX_API_KEY=${secrets.minimaxKey}`);
330
+ }
331
+ if (providers.has('kimi-api') && secrets.kimiKey) {
332
+ lines.push(`KIMI_API_KEY=${secrets.kimiKey}`);
333
+ }
334
+ lines.push(`TELEGRAM_BOT_TOKEN=${telegramToken}`);
335
+ if (discordToken) {
336
+ lines.push(`DISCORD_BOT_TOKEN=${discordToken}`);
337
+ }
338
+ lines.push('');
339
+ return lines.join('\n');
340
+ }
341
+ export function buildSetupConfig(input) {
342
+ const useDiscord = Boolean(input.discordToken);
343
+ const features = input.features ?? { browser: false, voice: false, mcp: false };
344
+ return {
345
+ gateway: {
346
+ port: 18790,
347
+ host: '127.0.0.1',
348
+ mode: 'local',
349
+ },
350
+ agents: {
351
+ default: 'main',
352
+ list: {
353
+ main: {
354
+ identity: {
355
+ name: input.agentName,
356
+ emoji: '👙🦞',
357
+ },
358
+ model: buildDefaultModel(input.selectedProviders),
359
+ thinking: 'low',
360
+ },
361
+ },
362
+ },
363
+ models: {
364
+ providers: buildProviders(input.selectedProviders),
365
+ aliases: buildAliases(input.selectedProviders),
366
+ },
367
+ channels: {
368
+ active: useDiscord ? 'discord' : 'telegram',
369
+ telegram: {
370
+ enabled: true,
371
+ token: '${TELEGRAM_BOT_TOKEN}',
372
+ allowFrom: [parseInt(input.telegramId, 10) || input.telegramId],
373
+ dailyNotesDir: '${HOME}/Daily Notes',
374
+ defaultAllowedPaths: [
375
+ '${HOME}/.skimpyclaw',
376
+ input.workspaceDir,
377
+ ],
378
+ },
379
+ discord: {
380
+ enabled: useDiscord,
381
+ token: useDiscord ? '${DISCORD_BOT_TOKEN}' : '',
382
+ allowFrom: useDiscord ? [input.discordUserId || ''] : [],
383
+ defaultAllowedPaths: [
384
+ '${HOME}/.skimpyclaw',
385
+ input.workspaceDir,
386
+ ],
387
+ ...(input.discordDefaultChannelId ? { defaultChannelId: input.discordDefaultChannelId } : {}),
388
+ },
389
+ },
390
+ cron: {
391
+ jobs: [],
392
+ },
393
+ heartbeat: {
394
+ intervalMs: 1800000,
395
+ prompt: 'Read HEARTBEAT.md. Follow it strictly. If nothing needs attention, reply HEARTBEAT_OK.',
396
+ tools: {
397
+ enabled: true,
398
+ allowedPaths: [
399
+ '${HOME}/.skimpyclaw',
400
+ input.workspaceDir,
401
+ ],
402
+ maxIterations: 10,
403
+ bashTimeout: 15000,
404
+ ...(features.browser ? { browser: { enabled: true } } : { browser: { enabled: false } }),
405
+ },
406
+ },
407
+ ...(features.voice ? {
408
+ voice: {
409
+ enabled: true,
410
+ defaultProvider: 'macos',
411
+ providers: {
412
+ macos: { tts: { voice: 'Samantha' } },
413
+ },
414
+ channels: {
415
+ telegram: { enabled: true, acceptVoice: true, sendVoice: true },
416
+ discord: { enabled: true, acceptVoice: true, sendVoice: true },
417
+ },
418
+ },
419
+ } : {}),
420
+ dashboard: {
421
+ token: randomUUID(),
422
+ },
423
+ };
424
+ }
425
+ export function buildSetupArtifacts(input) {
426
+ const config = buildSetupConfig(input);
427
+ return {
428
+ configJson: JSON.stringify(config, null, 2),
429
+ envContent: buildEnvContent(input.telegramToken, input.selectedProviders, input.providerSecrets, input.discordToken),
430
+ config,
431
+ };
432
+ }
433
+ const REQUIRED_TEMPLATE_DEFAULTS = {
434
+ 'SOUL.md': '# SOUL\n\nBe direct, resourceful, and helpful. Keep it concise.\n',
435
+ 'IDENTITY.md': '# IDENTITY\n\nName: Claw\nEmoji: 👙🦞\n',
436
+ 'USER.md': '# USER\n\nName: User\n',
437
+ 'HEARTBEAT.md': '# HEARTBEAT\n\nIf nothing needs attention, reply HEARTBEAT_OK.\n',
438
+ };
439
+ function ensureCoreTemplates(agentDir) {
440
+ const created = [];
441
+ for (const [file, content] of Object.entries(REQUIRED_TEMPLATE_DEFAULTS)) {
442
+ const dst = join(agentDir, file);
443
+ if (!existsSync(dst)) {
444
+ writeFileSync(dst, content, 'utf-8');
445
+ created.push(file);
446
+ }
447
+ }
448
+ return created;
449
+ }
450
+ async function quickFetch(url, init) {
451
+ return await fetch(url, { ...init, signal: AbortSignal.timeout(12000) });
452
+ }
453
+ async function validateTelegramToken(token) {
454
+ try {
455
+ const res = await quickFetch(`https://api.telegram.org/bot${token}/getMe`);
456
+ const text = await res.text();
457
+ if (!res.ok)
458
+ return { ok: false, detail: `HTTP ${res.status}: ${text.slice(0, 140)}` };
459
+ const body = JSON.parse(text);
460
+ if (!body.ok)
461
+ return { ok: false, detail: text.slice(0, 140) };
462
+ return { ok: true, detail: body.result?.username ? `@${body.result.username}` : 'valid token' };
463
+ }
464
+ catch (err) {
465
+ return { ok: false, detail: err instanceof Error ? err.message : String(err) };
466
+ }
467
+ }
468
+ async function validateProviderAuth(providers, secrets) {
469
+ const checks = [];
470
+ if (providers.has('anthropic-api') && secrets.anthropicKey) {
471
+ try {
472
+ const res = await quickFetch('https://api.anthropic.com/v1/messages', {
473
+ method: 'POST',
474
+ headers: {
475
+ 'content-type': 'application/json',
476
+ 'x-api-key': secrets.anthropicKey,
477
+ 'anthropic-version': '2023-06-01',
478
+ },
479
+ body: JSON.stringify({ model: 'claude-3-5-haiku-20241022', max_tokens: 8, messages: [{ role: 'user', content: 'ping' }] }),
480
+ });
481
+ checks.push({ name: 'Anthropic API', ok: res.ok, detail: res.ok ? 'auth ok' : `HTTP ${res.status}` });
482
+ }
483
+ catch (err) {
484
+ checks.push({ name: 'Anthropic API', ok: false, detail: err instanceof Error ? err.message : String(err) });
485
+ }
486
+ }
487
+ if (providers.has('openai-api') && secrets.openaiKey) {
488
+ try {
489
+ const res = await quickFetch('https://api.openai.com/v1/models', {
490
+ headers: { authorization: `Bearer ${secrets.openaiKey}` },
491
+ });
492
+ checks.push({ name: 'OpenAI API', ok: res.ok, detail: res.ok ? 'auth ok' : `HTTP ${res.status}` });
493
+ }
494
+ catch (err) {
495
+ checks.push({ name: 'OpenAI API', ok: false, detail: err instanceof Error ? err.message : String(err) });
496
+ }
497
+ }
498
+ if (providers.has('minimax-api') && secrets.minimaxKey) {
499
+ try {
500
+ const res = await quickFetch('https://api.minimax.io/anthropic/v1/messages', {
501
+ method: 'POST',
502
+ headers: {
503
+ authorization: `Bearer ${secrets.minimaxKey}`,
504
+ 'content-type': 'application/json',
505
+ 'anthropic-version': '2023-06-01',
506
+ },
507
+ body: JSON.stringify({ model: 'MiniMax-M2.1', max_tokens: 8, messages: [{ role: 'user', content: 'ping' }] }),
508
+ });
509
+ checks.push({ name: 'MiniMax API', ok: res.ok, detail: res.ok ? 'auth ok' : `HTTP ${res.status}` });
510
+ }
511
+ catch (err) {
512
+ checks.push({ name: 'MiniMax API', ok: false, detail: err instanceof Error ? err.message : String(err) });
513
+ }
514
+ }
515
+ if (providers.has('codex-oauth')) {
516
+ const authPath = join(homedir(), '.codex', 'auth.json');
517
+ checks.push({ name: 'Codex OAuth', ok: existsSync(authPath), detail: existsSync(authPath) ? authPath : `missing ${authPath}` });
518
+ }
519
+ return checks;
520
+ }
521
+ export async function runSetup(options = {}) {
522
+ const dryRun = options.dryRun ?? false;
523
+ if (dryRun) {
524
+ const workspaceDir = process.cwd();
525
+ if (!existsSync(TEMPLATES_DIR)) {
526
+ throw new Error(`Templates directory not found: ${TEMPLATES_DIR}`);
527
+ }
528
+ const templates = readdirSync(TEMPLATES_DIR).filter((f) => f.endsWith('.md'));
529
+ if (templates.length === 0) {
530
+ throw new Error(`No markdown templates found in ${TEMPLATES_DIR}`);
531
+ }
532
+ // Validate launchd template rendering with current environment and workspace.
533
+ renderGatewayPlist(workspaceDir);
534
+ console.log('✅ Onboarding dry run successful.');
535
+ console.log(`Would create config under: ${CONFIG_DIR}`);
536
+ console.log(`Would copy ${templates.length} templates to: ${AGENTS_DIR}`);
537
+ console.log(`Would render launchd plist from: ${GATEWAY_PLIST_TEMPLATE}`);
538
+ return;
539
+ }
540
+ const rl = readline.createInterface({
541
+ input: process.stdin,
542
+ output: process.stdout,
543
+ });
544
+ try {
545
+ const existing = loadExistingSetup();
546
+ const isReconfigure = existing.config !== null;
547
+ if (isReconfigure) {
548
+ console.log(`\n${c.bold('👙🦞 SkimpyClaw Setup')} ${c.dim('(reconfigure — press Enter to keep current values)')}\n`);
549
+ }
550
+ else {
551
+ console.log(`\n${c.bold('👙🦞 SkimpyClaw Setup')}\n`);
552
+ }
553
+ // 1. Telegram Bot Token
554
+ const existingTgToken = existing.env.TELEGRAM_BOT_TOKEN || '';
555
+ sectionHeader('1. Telegram Bot Token');
556
+ console.log(' Get one from @BotFather: https://t.me/BotFather');
557
+ let telegramToken;
558
+ if (existingTgToken) {
559
+ const input = await ask(rl, ` Enter token [${maskInput(existingTgToken)}]: `);
560
+ telegramToken = input || existingTgToken;
561
+ }
562
+ else {
563
+ telegramToken = await ask(rl, ' Enter token: ');
564
+ }
565
+ console.log(` ✓ ${maskInput(telegramToken)}\n`);
566
+ // 2. Telegram ID
567
+ const existingTgId = String(existing.config?.channels?.telegram?.allowFrom?.[0] || '');
568
+ sectionHeader('2. Your Telegram ID');
569
+ console.log(' Get it from @userinfobot: https://t.me/userinfobot');
570
+ let telegramId;
571
+ while (true) {
572
+ const defaultHint = existingTgId ? ` [${existingTgId}]` : '';
573
+ const input = await ask(rl, ` Enter ID${defaultHint}: `);
574
+ telegramId = input || existingTgId;
575
+ if (/^\d+$/.test(telegramId))
576
+ break;
577
+ console.log(' ✗ Telegram user IDs are numbers. Send /start to @userinfobot to find yours.');
578
+ }
579
+ console.log(` ✓ ${telegramId}\n`);
580
+ // 2b. Optional Discord
581
+ const existingDiscord = existing.config?.channels?.discord?.enabled === true;
582
+ sectionHeader('2b. Discord Bot (optional)');
583
+ const discordDefault = existingDiscord ? 'Y' : 'N';
584
+ const useDiscord = /^y(es)?$/i.test(await ask(rl, ` Enable Discord channel? [${existingDiscord ? 'Y/n' : 'y/N'}]: `) || discordDefault);
585
+ let discordToken = '';
586
+ let discordUserId = '';
587
+ let discordDefaultChannelId = '';
588
+ if (useDiscord) {
589
+ const existingDiscordToken = existing.env.DISCORD_BOT_TOKEN || '';
590
+ const existingDiscordUserId = String(existing.config?.channels?.discord?.allowFrom?.[0] || '');
591
+ const existingDiscordChannelId = existing.config?.channels?.discord?.defaultChannelId || '';
592
+ console.log(' Create bot in Discord Developer Portal, then copy token and user ID.');
593
+ const dtInput = await ask(rl, existingDiscordToken ? ` Enter Discord bot token [${maskInput(existingDiscordToken)}]: ` : ' Enter Discord bot token: ');
594
+ discordToken = dtInput || existingDiscordToken;
595
+ const duInput = await ask(rl, existingDiscordUserId ? ` Enter your Discord user ID [${existingDiscordUserId}]: ` : ' Enter your Discord user ID: ');
596
+ discordUserId = duInput || existingDiscordUserId;
597
+ const dcInput = await ask(rl, existingDiscordChannelId ? ` Optional default channel ID [${existingDiscordChannelId}]: ` : ' Optional default channel ID for proactive alerts: ');
598
+ discordDefaultChannelId = dcInput || existingDiscordChannelId;
599
+ console.log(` ✓ ${maskInput(discordToken)}\n`);
600
+ }
601
+ else {
602
+ console.log(' ✓ skipped\n');
603
+ }
604
+ // 3. Model Providers
605
+ const existingProviders = isReconfigure ? detectExistingProviders(existing.config) : undefined;
606
+ const selectedProviders = await askProviders(rl, existingProviders);
607
+ const providerSecrets = await collectProviderSecrets(rl, selectedProviders, isReconfigure ? existing.env : undefined);
608
+ // 4. Agent Name
609
+ const existingAgentName = existing.config?.agents?.list?.main?.identity?.name || '';
610
+ sectionHeader('4. Agent Name');
611
+ const agentNameDefault = existingAgentName || 'SkimpyClaw';
612
+ const agentName = (await ask(rl, ` What should I call myself? [${agentNameDefault}]: `)) || agentNameDefault;
613
+ console.log(` ✓ ${agentName}\n`);
614
+ // 5. Your Name
615
+ sectionHeader('5. Your Name');
616
+ const userName = (await ask(rl, ' What should I call you? ')) || 'User';
617
+ statusOk(userName);
618
+ // 6. Optional Features
619
+ const existingBrowser = existing.config?.heartbeat?.tools?.browser?.enabled === true
620
+ || existing.config?.channels?.telegram?.tools?.browser?.enabled === true;
621
+ const existingVoice = existing.config?.voice?.enabled === true;
622
+ sectionHeader('Optional Features');
623
+ // 6a. Browser tool
624
+ const browserDefault = existingBrowser ? 'Y' : 'N';
625
+ const enableBrowser = /^y(es)?$/i.test(await ask(rl, ` Enable browser tool? (requires Chrome) [${existingBrowser ? 'Y/n' : 'y/N'}]: `) || browserDefault);
626
+ if (enableBrowser) {
627
+ const macChrome = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
628
+ const which = spawnSync('which', ['google-chrome'], { encoding: 'utf-8' });
629
+ if (which.status === 0 || existsSync(macChrome)) {
630
+ statusOk('Chrome detected');
631
+ }
632
+ else {
633
+ statusWarn('Chrome not found — browser tool may not work until Chrome is installed');
634
+ }
635
+ }
636
+ else {
637
+ statusOk('browser disabled');
638
+ }
639
+ // 6b. Voice/TTS
640
+ const voiceDefault = existingVoice ? 'Y' : 'N';
641
+ const enableVoice = /^y(es)?$/i.test(await ask(rl, ` Enable voice/TTS? (requires ffmpeg) [${existingVoice ? 'Y/n' : 'y/N'}]: `) || voiceDefault);
642
+ if (enableVoice) {
643
+ const ffmpeg = spawnSync('which', ['ffmpeg'], { encoding: 'utf-8' });
644
+ if (ffmpeg.status === 0) {
645
+ statusOk('ffmpeg detected');
646
+ }
647
+ else {
648
+ statusWarn('ffmpeg not found — voice features may not work until ffmpeg is installed');
649
+ }
650
+ }
651
+ else {
652
+ statusOk('voice disabled');
653
+ }
654
+ // 6c. MCP tools
655
+ console.log(' Install mcporter: https://github.com/steipete/mcporter');
656
+ const enableMcp = /^y(es)?$/i.test(await ask(rl, ' Enable MCP tools? (requires mcporter at ~/.mcporter/) [y/N]: '));
657
+ if (enableMcp) {
658
+ const mcporterConfig = join(homedir(), '.mcporter', 'mcporter.json');
659
+ if (existsSync(mcporterConfig)) {
660
+ statusOk('mcporter config found');
661
+ }
662
+ else {
663
+ statusWarn(`mcporter config not found at ${mcporterConfig} — MCP tools won't load until configured`);
664
+ }
665
+ }
666
+ else {
667
+ statusOk('MCP tools disabled');
668
+ }
669
+ const features = {
670
+ browser: enableBrowser,
671
+ voice: enableVoice,
672
+ mcp: enableMcp,
673
+ };
674
+ const workspaceDir = process.cwd();
675
+ const { configJson: rawConfigJson, envContent, config: generatedConfig } = buildSetupArtifacts({
676
+ workspaceDir,
677
+ telegramId,
678
+ telegramToken,
679
+ discordToken: useDiscord ? discordToken : undefined,
680
+ discordUserId: useDiscord ? discordUserId : undefined,
681
+ discordDefaultChannelId: useDiscord ? discordDefaultChannelId : undefined,
682
+ agentName,
683
+ selectedProviders,
684
+ providerSecrets,
685
+ features,
686
+ });
687
+ // On reconfigure, preserve dashboard token, cron jobs, subagents, security, langfuse
688
+ if (isReconfigure && existing.config) {
689
+ if (existing.config.dashboard?.token) {
690
+ generatedConfig.dashboard = existing.config.dashboard;
691
+ }
692
+ if (Array.isArray(existing.config.cron?.jobs) && existing.config.cron.jobs.length > 0) {
693
+ generatedConfig.cron = existing.config.cron;
694
+ }
695
+ if (existing.config.subagents) {
696
+ generatedConfig.subagents = existing.config.subagents;
697
+ }
698
+ if (existing.config.security) {
699
+ generatedConfig.security = existing.config.security;
700
+ }
701
+ if (existing.config.langfuse) {
702
+ generatedConfig.langfuse = existing.config.langfuse;
703
+ }
704
+ // Preserve voice provider config if voice was already configured
705
+ if (existing.config.voice?.providers && Object.keys(existing.config.voice.providers).length > 0) {
706
+ generatedConfig.voice = existing.config.voice;
707
+ }
708
+ }
709
+ const configJson = JSON.stringify(generatedConfig, null, 2);
710
+ // Create directories
711
+ console.log('Creating directories...');
712
+ mkdirSync(CONFIG_DIR, { recursive: true });
713
+ mkdirSync(join(CONFIG_DIR, 'logs'), { recursive: true });
714
+ mkdirSync(join(CONFIG_DIR, 'sessions'), { recursive: true });
715
+ mkdirSync(join(CONFIG_DIR, 'cron'), { recursive: true });
716
+ mkdirSync(AGENTS_DIR, { recursive: true });
717
+ mkdirSync(join(AGENTS_DIR, 'memory'), { recursive: true });
718
+ const configPath = join(CONFIG_DIR, 'config.json');
719
+ writeFileSync(configPath, configJson);
720
+ console.log(`✓ Config written to ${configPath}`);
721
+ // Copy templates
722
+ if (existsSync(TEMPLATES_DIR)) {
723
+ const templates = readdirSync(TEMPLATES_DIR).filter((f) => f.endsWith('.md'));
724
+ for (const template of templates) {
725
+ const src = join(TEMPLATES_DIR, template);
726
+ const dst = join(AGENTS_DIR, template);
727
+ if (!existsSync(dst)) {
728
+ copyFileSync(src, dst);
729
+ }
730
+ }
731
+ console.log(`✓ Templates copied to ${AGENTS_DIR}`);
732
+ }
733
+ else {
734
+ console.log(`⚠ Templates directory not found. Create templates manually in ${AGENTS_DIR}`);
735
+ }
736
+ const createdFallbackTemplates = ensureCoreTemplates(AGENTS_DIR);
737
+ if (createdFallbackTemplates.length > 0) {
738
+ console.log(`✓ Added missing core templates: ${createdFallbackTemplates.join(', ')}`);
739
+ }
740
+ // Merge secrets into .env (preserve existing keys not in new content)
741
+ const envPath = join(CONFIG_DIR, '.env');
742
+ if (isReconfigure && existsSync(envPath)) {
743
+ const existingLines = readFileSync(envPath, 'utf-8').split('\n');
744
+ const newKeys = new Set();
745
+ for (const line of envContent.split('\n')) {
746
+ const m = line.match(/^([A-Z_]+)=/);
747
+ if (m)
748
+ newKeys.add(m[1]);
749
+ }
750
+ // Keep existing keys that aren't being replaced
751
+ const preserved = existingLines.filter((line) => {
752
+ const m = line.match(/^([A-Z_]+)=/);
753
+ return m && !newKeys.has(m[1]);
754
+ });
755
+ const merged = envContent.trim() + (preserved.length ? '\n' + preserved.join('\n') : '') + '\n';
756
+ writeFileSync(envPath, merged);
757
+ console.log(`✓ Secrets merged into ${envPath}`);
758
+ }
759
+ else {
760
+ writeFileSync(envPath, envContent);
761
+ console.log(`✓ Secrets written to ${envPath}`);
762
+ }
763
+ // Update USER.md with name
764
+ writeFileSync(join(AGENTS_DIR, 'USER.md'), `# USER.md - About ${userName}\n\nName: ${userName}\n\n## Preferences\n\n- Direct communication, no fluff\n\n## Routines\n\n- Morning: Review tasks and messages\n- EOD: Review completed work, plan tomorrow\n`);
765
+ // Create launchd plist from template
766
+ const plistPath = join(homedir(), 'Library', 'LaunchAgents', `${GATEWAY_PLIST_LABEL}.plist`);
767
+ const plistContent = renderGatewayPlist(workspaceDir);
768
+ mkdirSync(dirname(plistPath), { recursive: true });
769
+ writeFileSync(plistPath, plistContent);
770
+ console.log(`✓ Daemon plist written to ${plistPath}`);
771
+ sectionHeader('Post-Setup Validation');
772
+ console.log(' Running doctor checks...\n');
773
+ const { report } = await runDoctorChecks();
774
+ let failCount = 0;
775
+ for (const check of report.checks) {
776
+ if (check.ok) {
777
+ console.log(` ${c.green('PASS')} ${check.name} ${c.dim(`— ${check.detail}`)}`);
778
+ }
779
+ else {
780
+ failCount++;
781
+ console.log(` ${c.red('FAIL')} ${check.name} — ${check.detail}`);
782
+ if (check.remedy) {
783
+ console.log(` ${c.dim(check.remedy)}`);
784
+ }
785
+ }
786
+ }
787
+ if (failCount === 0) {
788
+ console.log(`\n${c.green('✅ Setup complete. Run `skimpyclaw start` to begin.')}\n`);
789
+ }
790
+ else {
791
+ console.log(`\n${c.yellow(`⚠ Setup complete with ${failCount} warning${failCount > 1 ? 's' : ''}. Run \`skimpyclaw doctor\` for details.`)}\n`);
792
+ }
793
+ const dashboardToken = generatedConfig.dashboard?.token || 'unknown';
794
+ console.log(`${c.bold('Dashboard')}`);
795
+ console.log(` URL: http://localhost:18790/dashboard`);
796
+ console.log(` Token: ${c.cyan(dashboardToken)}`);
797
+ console.log(` ${c.dim('(also available via: skimpyclaw status)')}`);
798
+ console.log('\nNext steps:');
799
+ console.log('1. Review templates in ~/.skimpyclaw/agents/main/');
800
+ console.log('2. Start the daemon:');
801
+ console.log(` launchctl load ~/Library/LaunchAgents/${GATEWAY_PLIST_LABEL}.plist`);
802
+ console.log('3. Check health:');
803
+ console.log(' curl http://localhost:18790/health');
804
+ console.log(`4. Send /help in your ${useDiscord ? 'Discord bot DM/server' : 'Telegram bot'}`);
805
+ console.log('\n👙🦞 Enjoy!');
806
+ }
807
+ finally {
808
+ rl.close();
809
+ }
810
+ }
811
+ async function main() {
812
+ const dryRun = process.argv.includes('--dry-run');
813
+ await runSetup({ dryRun });
814
+ }
815
+ const isDirectExecution = process.argv[1] === fileURLToPath(import.meta.url);
816
+ if (isDirectExecution) {
817
+ main().catch((error) => {
818
+ console.error('Setup failed:', error);
819
+ process.exit(1);
820
+ });
821
+ }