astro-claw 1.0.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.
@@ -0,0 +1,16 @@
1
+ {
2
+ "apify": {
3
+ "command": "npx",
4
+ "args": ["-y", "@apify/actors-mcp-server"],
5
+ "env": {
6
+ "APIFY_TOKEN": "your-apify-token-here"
7
+ }
8
+ },
9
+ "airtable": {
10
+ "command": "npx",
11
+ "args": ["-y", "airtable-mcp-server"],
12
+ "env": {
13
+ "AIRTABLE_API_KEY": "your-airtable-pat-here"
14
+ }
15
+ }
16
+ }
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "astro-claw",
3
+ "version": "1.0.0",
4
+ "description": "Claude Code over Slack — your AI crew member. One command: npx astro-claw",
5
+ "main": "index.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "astro-claw": "./start.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node start.js",
12
+ "dev": "nodemon --watch index.js --watch start.js --watch setup.js start.js",
13
+ "setup": "node start.js --setup"
14
+ },
15
+ "keywords": [
16
+ "slack",
17
+ "claude",
18
+ "ai",
19
+ "agent",
20
+ "claude-code",
21
+ "bot",
22
+ "astronaut"
23
+ ],
24
+ "author": "Taxflow <felipe@jointaxflow.ai>",
25
+ "license": "MIT",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://github.com/felipesalinasr/astro-claw"
29
+ },
30
+ "engines": {
31
+ "node": ">=18.0.0"
32
+ },
33
+ "files": [
34
+ "start.js",
35
+ "setup.js",
36
+ "slack-setup.js",
37
+ "index.js",
38
+ "slack-manifest.json",
39
+ "slack-manifest.yml",
40
+ "astronaut-icon.png",
41
+ "workspace/CLAUDE.md",
42
+ "mcp-servers.example.json",
43
+ ".env.example"
44
+ ],
45
+ "dependencies": {
46
+ "@anthropic-ai/claude-agent-sdk": "^0.2.85",
47
+ "@slack/bolt": "^4.1.0",
48
+ "dotenv": "^16.4.0"
49
+ },
50
+ "optionalDependencies": {
51
+ "puppeteer-core": "^24.40.0"
52
+ },
53
+ "devDependencies": {
54
+ "nodemon": "^3.1.0"
55
+ }
56
+ }
package/setup.js ADDED
@@ -0,0 +1,316 @@
1
+ import { createInterface } from "node:readline/promises";
2
+ import { execSync, spawnSync } from "node:child_process";
3
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
4
+ import { resolve, dirname } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+
9
+ // Config paths — use ASTRO_CLAW_HOME (set by start.js) or fall back to __dirname
10
+ const CONFIG_HOME = process.env.ASTRO_CLAW_HOME || __dirname;
11
+ const ENV_PATH = resolve(CONFIG_HOME, ".env");
12
+ const MCP_PATH = resolve(CONFIG_HOME, "mcp-servers.json");
13
+ const MANIFEST_PATH = resolve(__dirname, "slack-manifest.json"); // always from package
14
+
15
+ // ─── Terminal Colors ────────────────────────────────────────────────
16
+ const bold = (s) => `\x1b[1m${s}\x1b[0m`;
17
+ const green = (s) => `\x1b[32m${s}\x1b[0m`;
18
+ const red = (s) => `\x1b[31m${s}\x1b[0m`;
19
+ const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
20
+ const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
21
+ const dim = (s) => `\x1b[2m${s}\x1b[0m`;
22
+
23
+ const CHECK = green("✓");
24
+ const CROSS = red("✗");
25
+ const WARN = yellow("!");
26
+
27
+ function step(num, total, label) {
28
+ console.log(`\n ${bold(`Step ${num}/${total}:`)} ${label}`);
29
+ }
30
+
31
+ // ─── Step 1: Node Version ───────────────────────────────────────────
32
+ function checkNodeVersion() {
33
+ const major = parseInt(process.versions.node.split(".")[0], 10);
34
+ if (major < 18) {
35
+ console.log(` ${CROSS} Node.js ${process.versions.node} — need v18 or higher`);
36
+ console.log(` Download: https://nodejs.org/`);
37
+ process.exit(1);
38
+ }
39
+ console.log(` ${CHECK} Node.js v${process.versions.node}`);
40
+ }
41
+
42
+ // ─── Step 2: Claude Code CLI ────────────────────────────────────────
43
+ async function checkClaudeCli(rl) {
44
+ let version;
45
+ try {
46
+ version = execSync("claude --version", { encoding: "utf-8", stdio: "pipe" }).trim();
47
+ } catch {
48
+ version = null;
49
+ }
50
+
51
+ if (version) {
52
+ console.log(` ${CHECK} Claude Code CLI ${version}`);
53
+ return;
54
+ }
55
+
56
+ console.log(` ${WARN} Claude Code CLI not found`);
57
+ const answer = await rl.question(` Install it now? ${dim("[Y/n]")} `);
58
+
59
+ if (answer.toLowerCase() === "n") {
60
+ console.log(`\n Install manually: ${cyan("npm install -g @anthropic-ai/claude-code")}`);
61
+ console.log(` Then run ${cyan("npx astro-claw")} again.\n`);
62
+ process.exit(0);
63
+ }
64
+
65
+ console.log(` Installing Claude Code CLI...`);
66
+ try {
67
+ execSync("npm install -g @anthropic-ai/claude-code", { stdio: "inherit" });
68
+ const v = execSync("claude --version", { encoding: "utf-8", stdio: "pipe" }).trim();
69
+ console.log(` ${CHECK} Claude Code CLI ${v}`);
70
+ } catch {
71
+ console.log(` ${CROSS} Installation failed.`);
72
+ console.log(` Try: ${cyan("sudo npm install -g @anthropic-ai/claude-code")}`);
73
+ console.log(` Then run ${cyan("npx astro-claw")} again.\n`);
74
+ process.exit(1);
75
+ }
76
+ }
77
+
78
+ // ─── Step 3: Claude Authentication ──────────────────────────────────
79
+ async function checkClaudeAuth(rl) {
80
+ function isLoggedIn() {
81
+ try {
82
+ const out = execSync("claude auth status", { encoding: "utf-8", stdio: "pipe" });
83
+ const data = JSON.parse(out);
84
+ return data.loggedIn ? data : null;
85
+ } catch {
86
+ return null;
87
+ }
88
+ }
89
+
90
+ const auth = isLoggedIn();
91
+ if (auth) {
92
+ const who = auth.email || auth.authMethod || "authenticated";
93
+ console.log(` ${CHECK} Claude authenticated (${who})`);
94
+ return;
95
+ }
96
+
97
+ console.log(` ${WARN} Not logged in to Claude`);
98
+ console.log(` A browser window will open for you to log in.`);
99
+ await rl.question(` Press Enter to continue... `);
100
+
101
+ try {
102
+ spawnSync("claude", ["auth", "login"], { stdio: "inherit" });
103
+ } catch {
104
+ // check status next
105
+ }
106
+
107
+ if (isLoggedIn()) {
108
+ console.log(` ${CHECK} Claude authenticated`);
109
+ } else {
110
+ console.log(` ${CROSS} Authentication failed.`);
111
+ console.log(` Run ${cyan("claude auth login")} manually, then ${cyan("npx astro-claw")} again.\n`);
112
+ process.exit(1);
113
+ }
114
+ }
115
+
116
+ // ─── Step 4: Slack App Setup ────────────────────────────────────────
117
+ // Returns { botToken, appToken, signingSecret } — some may be null
118
+ async function setupSlackApp(rl) {
119
+ // Try self-driving Chrome first
120
+ let autoTokens = null;
121
+ try {
122
+ const { findChrome } = await import("./slack-setup.js");
123
+ if (findChrome()) {
124
+ console.log(`\n Chrome detected — launching self-driving setup...`);
125
+ const { default: selfDrivingSlackSetup } = await import("./slack-setup.js");
126
+ autoTokens = await selfDrivingSlackSetup();
127
+ }
128
+ } catch (err) {
129
+ console.log(` ${WARN} Auto-setup unavailable: ${err.message}`);
130
+ }
131
+
132
+ // If auto-setup got all tokens, we're done
133
+ if (autoTokens?.botToken && autoTokens?.appToken && autoTokens?.signingSecret) {
134
+ return autoTokens;
135
+ }
136
+
137
+ // Fall back to manual flow for any missing tokens
138
+ if (!autoTokens) {
139
+ // Full manual flow — open browser with manifest
140
+ let manifest;
141
+ try { manifest = readFileSync(MANIFEST_PATH, "utf-8"); } catch { manifest = null; }
142
+
143
+ if (manifest) {
144
+ const slackUrl = `https://api.slack.com/apps?new_app=1&manifest_json=${encodeURIComponent(manifest)}`;
145
+ console.log(`\n Opening your browser with the app manifest pre-filled...\n`);
146
+ try {
147
+ const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
148
+ execSync(`${openCmd} "${slackUrl}"`, { stdio: "ignore" });
149
+ console.log(` ${CHECK} Browser opened`);
150
+ } catch {
151
+ console.log(` ${WARN} Couldn't open browser. Go to: ${cyan(slackUrl)}`);
152
+ }
153
+ } else {
154
+ console.log(`\n Go to: ${cyan("https://api.slack.com/apps?new_app=1")}`);
155
+ console.log(` Choose "From a manifest" → paste the JSON from slack-manifest.json`);
156
+ }
157
+
158
+ console.log(`\n ${bold("1.")} Select your workspace → click ${bold("Next")} → ${bold("Create")}`);
159
+ console.log(` ${bold("2.")} Go to ${bold("Basic Information")} → ${bold("App-Level Tokens")} → ${bold("Generate Token")}`);
160
+ console.log(` Name it anything → add scope ${bold("connections:write")} → ${bold("Generate")}`);
161
+ console.log(` ${bold("3.")} Go to ${bold("OAuth & Permissions")} → ${bold("Install to Workspace")} → ${bold("Allow")}`);
162
+ console.log(` ${bold("4.")} ${dim("(Optional)")} Set the bot icon: ${bold("Basic Information")} → ${bold("Display Information")}`);
163
+ console.log(` Upload the ${cyan("astronaut-icon.png")} file from this package\n`);
164
+ await rl.question(` Press Enter when done... `);
165
+ } else {
166
+ // Partial auto — tell user which ones were captured
167
+ const missing = [];
168
+ if (!autoTokens.botToken) missing.push("Bot Token");
169
+ if (!autoTokens.appToken) missing.push("App-Level Token");
170
+ if (!autoTokens.signingSecret) missing.push("Signing Secret");
171
+ console.log(` ${WARN} Still need: ${bold(missing.join(", "))}`);
172
+ console.log(` Grab them from your Slack app dashboard.\n`);
173
+ }
174
+
175
+ // Collect any missing tokens manually
176
+ return await collectMissingTokens(rl, autoTokens || {});
177
+ }
178
+
179
+ // ─── Step 5: Collect Missing Tokens ─────────────────────────────────
180
+ async function collectMissingTokens(rl, existing) {
181
+ async function askToken(label, hint, validator, errorMsg) {
182
+ while (true) {
183
+ const value = (await rl.question(` ${label} ${dim(hint)} `)).trim();
184
+ if (!value) { console.log(` ${CROSS} Required.`); continue; }
185
+ if (validator(value)) return value;
186
+ console.log(` ${CROSS} ${errorMsg}`);
187
+ }
188
+ }
189
+
190
+ const needAny = !existing.botToken || !existing.appToken || !existing.signingSecret;
191
+ if (needAny) {
192
+ console.log(`\n Paste your tokens from the Slack app dashboard:\n`);
193
+ }
194
+
195
+ const botToken = existing.botToken || await askToken("Bot Token:", "(OAuth & Permissions > Bot User OAuth Token)", (v) => v.startsWith("xoxb-"), "Must start with xoxb-");
196
+ const appToken = existing.appToken || await askToken("App-Level Token:", "(Basic Information > App-Level Tokens)", (v) => v.startsWith("xapp-"), "Must start with xapp-");
197
+ const signingSecret = existing.signingSecret || await askToken("Signing Secret:", "(Basic Information > Signing Secret)", (v) => /^[0-9a-f]{20,}$/i.test(v), "Must be a hex string (20+ chars)");
198
+
199
+ return { botToken, appToken, signingSecret };
200
+ }
201
+
202
+ // ─── Step 6: Validate Tokens ────────────────────────────────────────
203
+ async function validateTokens(tokens) {
204
+ console.log(`\n Validating...`);
205
+ try {
206
+ const { WebClient } = await import("@slack/web-api");
207
+ const client = new WebClient(tokens.botToken);
208
+ const result = await client.auth.test();
209
+ console.log(` ${CHECK} Connected to workspace ${bold(`"${result.team}"`)} as ${bold(result.user)}`);
210
+ return true;
211
+ } catch (err) {
212
+ console.log(` ${CROSS} Bot token validation failed: ${err?.data?.error || err.message || "unknown"}`);
213
+ return false;
214
+ }
215
+ }
216
+
217
+ // ─── Step 7: Optional Config ────────────────────────────────────────
218
+ async function collectOptionalConfig(rl) {
219
+ console.log(`\n Optional configuration:\n`);
220
+ console.log(` ${dim("Find your Slack user ID: click your profile > ⋯ > Copy member ID")}`);
221
+ const adminIds = (await rl.question(` Admin User ID(s) ${dim("(comma-separated, blank to skip)")} `)).trim();
222
+
223
+ if (adminIds) {
224
+ const valid = adminIds.split(",").map((s) => s.trim()).every((id) => /^[UWB][A-Z0-9]{6,15}$/.test(id));
225
+ if (!valid) console.log(` ${WARN} Some IDs look invalid, saving anyway.`);
226
+ }
227
+
228
+ return { adminIds };
229
+ }
230
+
231
+ // ─── Step 8: Write .env ─────────────────────────────────────────────
232
+ function writeEnvFile(tokens, optional) {
233
+ const content = `# Astro Claw — generated by setup wizard
234
+ # Config home: ${CONFIG_HOME}
235
+ # Re-run setup: npx astro-claw --setup
236
+
237
+ # Slack tokens
238
+ SLACK_BOT_TOKEN=${tokens.botToken}
239
+ SLACK_APP_TOKEN=${tokens.appToken}
240
+ SLACK_SIGNING_SECRET=${tokens.signingSecret}
241
+
242
+ # No API key needed — uses your Claude CLI login.
243
+ # Verify: claude auth status
244
+
245
+ # Admin user IDs (comma-separated, blank = all users)
246
+ ADMIN_USER_IDS=${optional.adminIds || ""}
247
+ `;
248
+ writeFileSync(ENV_PATH, content);
249
+ console.log(` ${CHECK} Configuration saved to ${ENV_PATH}`);
250
+ }
251
+
252
+ // ─── Step 9: MCP Servers ────────────────────────────────────────────
253
+ function setupMcpServers() {
254
+ if (!existsSync(MCP_PATH)) {
255
+ writeFileSync(MCP_PATH, "{}\n");
256
+ console.log(` ${CHECK} MCP servers config created ${dim("(add servers later with !mcp in Slack)")}`);
257
+ } else {
258
+ console.log(` ${CHECK} MCP servers config exists`);
259
+ }
260
+ }
261
+
262
+ // ─── Main ───────────────────────────────────────────────────────────
263
+ export default async function runSetup() {
264
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
265
+
266
+ console.log("");
267
+ console.log(bold(" 🚀 Astro Claw Setup"));
268
+ console.log(dim(" ─────────────────────────────────────────"));
269
+ console.log("");
270
+
271
+ try {
272
+ step(1, 4, "Checking Node.js");
273
+ checkNodeVersion();
274
+
275
+ step(2, 4, "Claude Code CLI");
276
+ await checkClaudeCli(rl);
277
+
278
+ step(3, 4, "Claude authentication");
279
+ await checkClaudeAuth(rl);
280
+
281
+ step(4, 4, "Slack app setup");
282
+ let tokens = await setupSlackApp(rl);
283
+
284
+ // Validate tokens
285
+ let validated = await validateTokens(tokens);
286
+ while (!validated) {
287
+ const retry = await rl.question(`\n Re-enter tokens? ${dim("[Y/n]")} `);
288
+ if (retry.toLowerCase() === "n") { rl.close(); return false; }
289
+ tokens = await collectMissingTokens(rl, {});
290
+ validated = await validateTokens(tokens);
291
+ }
292
+
293
+ // Optional config + save
294
+ const optional = await collectOptionalConfig(rl);
295
+ writeEnvFile(tokens, optional);
296
+ setupMcpServers();
297
+
298
+ rl.close();
299
+
300
+ console.log("");
301
+ console.log(bold(green(" ✓ Setup complete!")));
302
+ console.log(dim(" ─────────────────────────────────────────"));
303
+ console.log(` Config saved to: ${cyan(CONFIG_HOME)}`);
304
+ console.log(` Starting Astro Claw...\n`);
305
+
306
+ return true;
307
+ } catch (err) {
308
+ rl.close();
309
+ if (err.code === "ERR_USE_AFTER_CLOSE" || err.message?.includes("readline was closed")) {
310
+ console.log(`\n\n Setup cancelled. Run ${cyan("npx astro-claw")} when ready.\n`);
311
+ } else {
312
+ console.error(`\n Setup error: ${err.message}\n`);
313
+ }
314
+ return false;
315
+ }
316
+ }
@@ -0,0 +1,47 @@
1
+ {
2
+ "display_information": {
3
+ "name": "Astronaut",
4
+ "description": "AI crew member powered by Claude Code",
5
+ "background_color": "#1a1a2e"
6
+ },
7
+ "features": {
8
+ "app_home": {
9
+ "home_tab_enabled": false,
10
+ "messages_tab_enabled": true,
11
+ "messages_tab_read_only_enabled": false
12
+ },
13
+ "bot_user": {
14
+ "display_name": "Astronaut",
15
+ "always_online": true
16
+ }
17
+ },
18
+ "oauth_config": {
19
+ "scopes": {
20
+ "bot": [
21
+ "app_mentions:read",
22
+ "channels:history",
23
+ "chat:write",
24
+ "files:read",
25
+ "groups:history",
26
+ "im:history",
27
+ "im:read",
28
+ "im:write",
29
+ "mpim:history"
30
+ ]
31
+ }
32
+ },
33
+ "settings": {
34
+ "event_subscriptions": {
35
+ "bot_events": [
36
+ "app_mention",
37
+ "message.im"
38
+ ]
39
+ },
40
+ "interactivity": {
41
+ "is_enabled": false
42
+ },
43
+ "org_deploy_enabled": false,
44
+ "socket_mode_enabled": true,
45
+ "token_rotation_enabled": false
46
+ }
47
+ }
@@ -0,0 +1,34 @@
1
+ display_information:
2
+ name: Astronaut
3
+ description: AI crew member powered by Claude Code
4
+ background_color: "#1a1a2e"
5
+ features:
6
+ app_home:
7
+ home_tab_enabled: false
8
+ messages_tab_enabled: true
9
+ messages_tab_read_only_enabled: false
10
+ bot_user:
11
+ display_name: Astronaut
12
+ always_online: true
13
+ oauth_config:
14
+ scopes:
15
+ bot:
16
+ - app_mentions:read
17
+ - channels:history
18
+ - chat:write
19
+ - files:read
20
+ - groups:history
21
+ - im:history
22
+ - im:read
23
+ - im:write
24
+ - mpim:history
25
+ settings:
26
+ event_subscriptions:
27
+ bot_events:
28
+ - app_mention
29
+ - message.im
30
+ interactivity:
31
+ is_enabled: false
32
+ org_deploy_enabled: false
33
+ socket_mode_enabled: true
34
+ token_rotation_enabled: false