afk-code 0.1.0 → 0.1.3
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.
- package/LICENSE +21 -0
- package/README.md +64 -97
- package/dist/cli/index.js +1972 -0
- package/package.json +13 -9
- package/slack-manifest.json +3 -3
- package/src/cli/discord.ts +0 -183
- package/src/cli/index.ts +0 -83
- package/src/cli/run.ts +0 -126
- package/src/cli/slack.ts +0 -193
- package/src/discord/channel-manager.ts +0 -191
- package/src/discord/discord-app.ts +0 -359
- package/src/discord/types.ts +0 -4
- package/src/slack/channel-manager.ts +0 -175
- package/src/slack/index.ts +0 -58
- package/src/slack/message-formatter.ts +0 -91
- package/src/slack/session-manager.ts +0 -567
- package/src/slack/slack-app.ts +0 -443
- package/src/slack/types.ts +0 -6
- package/src/types/index.ts +0 -6
- package/src/utils/image-extractor.ts +0 -72
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "afk-code",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Monitor and interact with Claude Code sessions from Slack/Discord",
|
|
5
5
|
"author": "Colin Harman",
|
|
6
6
|
"repository": {
|
|
@@ -9,19 +9,21 @@
|
|
|
9
9
|
},
|
|
10
10
|
"type": "module",
|
|
11
11
|
"bin": {
|
|
12
|
-
"afk-code": "./
|
|
12
|
+
"afk-code": "./dist/cli/index.js"
|
|
13
13
|
},
|
|
14
14
|
"scripts": {
|
|
15
|
-
"
|
|
16
|
-
"
|
|
15
|
+
"build": "tsup",
|
|
16
|
+
"dev": "tsx src/cli/index.ts",
|
|
17
|
+
"prepublishOnly": "npm run build",
|
|
18
|
+
"postinstall": "node -e \"const fs = require('fs'); const path = require('path'); const dir = path.dirname(path.dirname(require.resolve('node-pty'))); const prebuilds = path.join(dir, 'prebuilds'); if (fs.existsSync(prebuilds)) { fs.readdirSync(prebuilds).forEach(p => { const helper = path.join(prebuilds, p, 'spawn-helper'); if (fs.existsSync(helper)) fs.chmodSync(helper, 0o755); }); }\""
|
|
17
19
|
},
|
|
18
20
|
"files": [
|
|
19
|
-
"
|
|
21
|
+
"dist",
|
|
20
22
|
"slack-manifest.json",
|
|
21
23
|
"README.md"
|
|
22
24
|
],
|
|
23
25
|
"engines": {
|
|
24
|
-
"
|
|
26
|
+
"node": ">=18.0.0"
|
|
25
27
|
},
|
|
26
28
|
"keywords": [
|
|
27
29
|
"claude",
|
|
@@ -35,11 +37,13 @@
|
|
|
35
37
|
"license": "MIT",
|
|
36
38
|
"dependencies": {
|
|
37
39
|
"@slack/bolt": "^4.6.0",
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
+
"discord.js": "^14.25.1",
|
|
41
|
+
"node-pty": "^1.0.0"
|
|
40
42
|
},
|
|
41
43
|
"devDependencies": {
|
|
42
|
-
"@types/
|
|
44
|
+
"@types/node": "^20.0.0",
|
|
45
|
+
"tsup": "^8.0.0",
|
|
46
|
+
"tsx": "^4.0.0",
|
|
43
47
|
"typescript": "^5.0.0"
|
|
44
48
|
}
|
|
45
49
|
}
|
package/slack-manifest.json
CHANGED
|
@@ -23,17 +23,17 @@
|
|
|
23
23
|
},
|
|
24
24
|
{
|
|
25
25
|
"command": "/background",
|
|
26
|
-
"description": "Send
|
|
26
|
+
"description": "Send background signal to Claude Code (Ctrl+B)",
|
|
27
27
|
"should_escape": false
|
|
28
28
|
},
|
|
29
29
|
{
|
|
30
30
|
"command": "/interrupt",
|
|
31
|
-
"description": "Interrupt Claude (Escape)",
|
|
31
|
+
"description": "Interrupt Claude Code (Escape)",
|
|
32
32
|
"should_escape": false
|
|
33
33
|
},
|
|
34
34
|
{
|
|
35
35
|
"command": "/mode",
|
|
36
|
-
"description": "Toggle Claude mode (Shift+Tab)",
|
|
36
|
+
"description": "Toggle Claude Code mode (Shift+Tab)",
|
|
37
37
|
"should_escape": false
|
|
38
38
|
}
|
|
39
39
|
]
|
package/src/cli/discord.ts
DELETED
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
import { homedir } from 'os';
|
|
2
|
-
import { mkdir } from 'fs/promises';
|
|
3
|
-
import * as readline from 'readline';
|
|
4
|
-
|
|
5
|
-
const CONFIG_DIR = `${homedir()}/.afk-code`;
|
|
6
|
-
const DISCORD_CONFIG_FILE = `${CONFIG_DIR}/discord.env`;
|
|
7
|
-
|
|
8
|
-
function prompt(question: string): Promise<string> {
|
|
9
|
-
const rl = readline.createInterface({
|
|
10
|
-
input: process.stdin,
|
|
11
|
-
output: process.stdout,
|
|
12
|
-
});
|
|
13
|
-
return new Promise((resolve) => {
|
|
14
|
-
rl.question(question, (answer) => {
|
|
15
|
-
rl.close();
|
|
16
|
-
resolve(answer.trim());
|
|
17
|
-
});
|
|
18
|
-
});
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export async function discordSetup(): Promise<void> {
|
|
22
|
-
console.log(`
|
|
23
|
-
┌─────────────────────────────────────────────────────────────┐
|
|
24
|
-
│ AFK Code Discord Setup │
|
|
25
|
-
└─────────────────────────────────────────────────────────────┘
|
|
26
|
-
|
|
27
|
-
This will guide you through setting up the Discord bot for
|
|
28
|
-
monitoring Claude Code sessions.
|
|
29
|
-
|
|
30
|
-
Step 1: Create a Discord Application
|
|
31
|
-
────────────────────────────────────
|
|
32
|
-
1. Go to: https://discord.com/developers/applications
|
|
33
|
-
2. Click "New Application"
|
|
34
|
-
3. Give it a name (e.g., "AFK Code")
|
|
35
|
-
4. Click "Create"
|
|
36
|
-
|
|
37
|
-
Step 2: Create a Bot
|
|
38
|
-
────────────────────
|
|
39
|
-
1. Go to "Bot" in the sidebar
|
|
40
|
-
2. Click "Add Bot" → "Yes, do it!"
|
|
41
|
-
3. Under "Privileged Gateway Intents", enable:
|
|
42
|
-
• MESSAGE CONTENT INTENT
|
|
43
|
-
4. Click "Reset Token" and copy the token
|
|
44
|
-
|
|
45
|
-
Step 3: Invite the Bot
|
|
46
|
-
──────────────────────
|
|
47
|
-
1. Go to "OAuth2" → "URL Generator"
|
|
48
|
-
2. Select scopes: "bot"
|
|
49
|
-
3. Select permissions:
|
|
50
|
-
• Send Messages
|
|
51
|
-
• Manage Channels
|
|
52
|
-
• Read Message History
|
|
53
|
-
4. Copy the URL and open it to invite the bot to your server
|
|
54
|
-
`);
|
|
55
|
-
|
|
56
|
-
await prompt('Press Enter when you have created and invited the bot...');
|
|
57
|
-
|
|
58
|
-
console.log(`
|
|
59
|
-
Now let's collect your credentials:
|
|
60
|
-
|
|
61
|
-
• Bot Token: "Bot" → "Token" (click "Reset Token" if needed)
|
|
62
|
-
• Your User ID: Enable Developer Mode in Discord settings,
|
|
63
|
-
then right-click your name → "Copy User ID"
|
|
64
|
-
`);
|
|
65
|
-
|
|
66
|
-
const botToken = await prompt('Bot Token: ');
|
|
67
|
-
if (!botToken || botToken.length < 50) {
|
|
68
|
-
console.error('Invalid bot token.');
|
|
69
|
-
process.exit(1);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const userId = await prompt('Your Discord User ID: ');
|
|
73
|
-
if (!userId || !/^\d+$/.test(userId)) {
|
|
74
|
-
console.error('Invalid user ID. Should be a number.');
|
|
75
|
-
process.exit(1);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Save configuration
|
|
79
|
-
await mkdir(CONFIG_DIR, { recursive: true });
|
|
80
|
-
|
|
81
|
-
const envContent = `# AFK Code Discord Configuration
|
|
82
|
-
DISCORD_BOT_TOKEN=${botToken}
|
|
83
|
-
DISCORD_USER_ID=${userId}
|
|
84
|
-
`;
|
|
85
|
-
|
|
86
|
-
await Bun.write(DISCORD_CONFIG_FILE, envContent);
|
|
87
|
-
console.log(`
|
|
88
|
-
✓ Configuration saved to ${DISCORD_CONFIG_FILE}
|
|
89
|
-
|
|
90
|
-
To start the Discord bot, run:
|
|
91
|
-
afk-code discord
|
|
92
|
-
|
|
93
|
-
Then start a Claude Code session with:
|
|
94
|
-
afk-code run -- claude
|
|
95
|
-
`);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
async function loadEnvFile(path: string): Promise<Record<string, string>> {
|
|
99
|
-
const file = Bun.file(path);
|
|
100
|
-
if (!(await file.exists())) return {};
|
|
101
|
-
|
|
102
|
-
const content = await file.text();
|
|
103
|
-
const config: Record<string, string> = {};
|
|
104
|
-
|
|
105
|
-
for (const line of content.split('\n')) {
|
|
106
|
-
if (line.startsWith('#') || !line.includes('=')) continue;
|
|
107
|
-
const [key, ...valueParts] = line.split('=');
|
|
108
|
-
config[key.trim()] = valueParts.join('=').trim();
|
|
109
|
-
}
|
|
110
|
-
return config;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export async function discordRun(): Promise<void> {
|
|
114
|
-
// Load config from multiple sources (in order of precedence):
|
|
115
|
-
// 1. Environment variables (highest priority)
|
|
116
|
-
// 2. Local .env file
|
|
117
|
-
// 3. ~/.afk-code/discord.env (lowest priority)
|
|
118
|
-
|
|
119
|
-
const globalConfig = await loadEnvFile(DISCORD_CONFIG_FILE);
|
|
120
|
-
const localConfig = await loadEnvFile(`${process.cwd()}/.env`);
|
|
121
|
-
|
|
122
|
-
// Merge configs (local overrides global, env vars override both)
|
|
123
|
-
const config: Record<string, string> = {
|
|
124
|
-
...globalConfig,
|
|
125
|
-
...localConfig,
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
// Environment variables take highest precedence
|
|
129
|
-
if (process.env.DISCORD_BOT_TOKEN) config.DISCORD_BOT_TOKEN = process.env.DISCORD_BOT_TOKEN;
|
|
130
|
-
if (process.env.DISCORD_USER_ID) config.DISCORD_USER_ID = process.env.DISCORD_USER_ID;
|
|
131
|
-
|
|
132
|
-
// Validate required config
|
|
133
|
-
const required = ['DISCORD_BOT_TOKEN', 'DISCORD_USER_ID'];
|
|
134
|
-
const missing = required.filter((key) => !config[key]);
|
|
135
|
-
|
|
136
|
-
if (missing.length > 0) {
|
|
137
|
-
console.error(`Missing config: ${missing.join(', ')}`);
|
|
138
|
-
console.error('');
|
|
139
|
-
console.error('Provide tokens via:');
|
|
140
|
-
console.error(' - Environment variables (DISCORD_BOT_TOKEN, DISCORD_USER_ID)');
|
|
141
|
-
console.error(' - Local .env file');
|
|
142
|
-
console.error(' - Run "afk-code discord setup" for guided configuration');
|
|
143
|
-
process.exit(1);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// Import and run the discord bot
|
|
147
|
-
const { createDiscordApp } = await import('../discord/discord-app');
|
|
148
|
-
|
|
149
|
-
// Show where config was loaded from
|
|
150
|
-
const localEnvExists = await Bun.file(`${process.cwd()}/.env`).exists();
|
|
151
|
-
const globalEnvExists = await Bun.file(DISCORD_CONFIG_FILE).exists();
|
|
152
|
-
const source = localEnvExists ? '.env' : globalEnvExists ? DISCORD_CONFIG_FILE : 'environment';
|
|
153
|
-
console.log(`[AFK Code] Loaded config from ${source}`);
|
|
154
|
-
console.log('[AFK Code] Starting Discord bot...');
|
|
155
|
-
|
|
156
|
-
const discordConfig = {
|
|
157
|
-
botToken: config.DISCORD_BOT_TOKEN,
|
|
158
|
-
userId: config.DISCORD_USER_ID,
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
const { client, sessionManager } = createDiscordApp(discordConfig);
|
|
162
|
-
|
|
163
|
-
// Start session manager (Unix socket server for CLI connections)
|
|
164
|
-
try {
|
|
165
|
-
await sessionManager.start();
|
|
166
|
-
console.log('[AFK Code] Session manager started');
|
|
167
|
-
} catch (err) {
|
|
168
|
-
console.error('[AFK Code] Failed to start session manager:', err);
|
|
169
|
-
process.exit(1);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// Start Discord bot
|
|
173
|
-
try {
|
|
174
|
-
await client.login(config.DISCORD_BOT_TOKEN);
|
|
175
|
-
console.log('[AFK Code] Discord bot is running!');
|
|
176
|
-
console.log('');
|
|
177
|
-
console.log('Start a Claude Code session with: afk-code run -- claude');
|
|
178
|
-
console.log('Each session will create an #afk-* channel');
|
|
179
|
-
} catch (err) {
|
|
180
|
-
console.error('[AFK Code] Failed to start Discord bot:', err);
|
|
181
|
-
process.exit(1);
|
|
182
|
-
}
|
|
183
|
-
}
|
package/src/cli/index.ts
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
|
|
3
|
-
import { run } from './run';
|
|
4
|
-
import { slackSetup, slackRun } from './slack';
|
|
5
|
-
import { discordSetup, discordRun } from './discord';
|
|
6
|
-
|
|
7
|
-
const args = process.argv.slice(2);
|
|
8
|
-
const command = args[0];
|
|
9
|
-
|
|
10
|
-
async function main() {
|
|
11
|
-
switch (command) {
|
|
12
|
-
case 'run': {
|
|
13
|
-
// Find -- separator and get command after it
|
|
14
|
-
const separatorIndex = args.indexOf('--');
|
|
15
|
-
if (separatorIndex === -1) {
|
|
16
|
-
console.error('Usage: afk-code run -- <command> [args...]');
|
|
17
|
-
console.error('Example: afk-code run -- claude');
|
|
18
|
-
process.exit(1);
|
|
19
|
-
}
|
|
20
|
-
const cmd = args.slice(separatorIndex + 1);
|
|
21
|
-
if (cmd.length === 0) {
|
|
22
|
-
console.error('No command specified after --');
|
|
23
|
-
process.exit(1);
|
|
24
|
-
}
|
|
25
|
-
await run(cmd);
|
|
26
|
-
break;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
case 'slack': {
|
|
30
|
-
if (args[1] === 'setup') {
|
|
31
|
-
await slackSetup();
|
|
32
|
-
} else {
|
|
33
|
-
await slackRun();
|
|
34
|
-
}
|
|
35
|
-
break;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
case 'discord': {
|
|
39
|
-
if (args[1] === 'setup') {
|
|
40
|
-
await discordSetup();
|
|
41
|
-
} else {
|
|
42
|
-
await discordRun();
|
|
43
|
-
}
|
|
44
|
-
break;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
case 'help':
|
|
48
|
-
case '--help':
|
|
49
|
-
case '-h':
|
|
50
|
-
case undefined: {
|
|
51
|
-
console.log(`
|
|
52
|
-
AFK Code - Monitor Claude Code sessions from Slack/Discord
|
|
53
|
-
|
|
54
|
-
Commands:
|
|
55
|
-
slack Run the Slack bot
|
|
56
|
-
slack setup Configure Slack integration
|
|
57
|
-
discord Run the Discord bot
|
|
58
|
-
discord setup Configure Discord integration
|
|
59
|
-
run -- <command> Start a monitored session
|
|
60
|
-
help Show this help message
|
|
61
|
-
|
|
62
|
-
Examples:
|
|
63
|
-
afk-code slack setup # First-time Slack configuration
|
|
64
|
-
afk-code slack # Start the Slack bot
|
|
65
|
-
afk-code discord setup # First-time Discord configuration
|
|
66
|
-
afk-code discord # Start the Discord bot
|
|
67
|
-
afk-code run -- claude # Start a Claude Code session
|
|
68
|
-
`);
|
|
69
|
-
break;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
default: {
|
|
73
|
-
console.error(`Unknown command: ${command}`);
|
|
74
|
-
console.error('Run "afk-code help" for usage');
|
|
75
|
-
process.exit(1);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
main().catch((err) => {
|
|
81
|
-
console.error('Error:', err.message);
|
|
82
|
-
process.exit(1);
|
|
83
|
-
});
|
package/src/cli/run.ts
DELETED
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from 'crypto';
|
|
2
|
-
import { spawn } from 'bun';
|
|
3
|
-
import { spawn as spawnPty } from '@zenyr/bun-pty';
|
|
4
|
-
import type { Socket } from 'bun';
|
|
5
|
-
import { homedir } from 'os';
|
|
6
|
-
|
|
7
|
-
const DAEMON_SOCKET = '/tmp/afk-code-daemon.sock';
|
|
8
|
-
|
|
9
|
-
// Get Claude's project directory for the current working directory
|
|
10
|
-
function getClaudeProjectDir(cwd: string): string {
|
|
11
|
-
// Claude encodes paths by replacing / with -
|
|
12
|
-
const encodedPath = cwd.replace(/\//g, '-');
|
|
13
|
-
return `${homedir()}/.claude/projects/${encodedPath}`;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
// Connect to daemon and maintain bidirectional communication
|
|
17
|
-
async function connectToDaemon(
|
|
18
|
-
sessionId: string,
|
|
19
|
-
projectDir: string,
|
|
20
|
-
cwd: string,
|
|
21
|
-
command: string[],
|
|
22
|
-
onInput: (text: string) => void
|
|
23
|
-
): Promise<{ close: () => void } | null> {
|
|
24
|
-
try {
|
|
25
|
-
let messageBuffer = '';
|
|
26
|
-
|
|
27
|
-
const socket = await Bun.connect({
|
|
28
|
-
unix: DAEMON_SOCKET,
|
|
29
|
-
socket: {
|
|
30
|
-
data(socket, data) {
|
|
31
|
-
messageBuffer += data.toString();
|
|
32
|
-
|
|
33
|
-
const lines = messageBuffer.split('\n');
|
|
34
|
-
messageBuffer = lines.pop() || '';
|
|
35
|
-
|
|
36
|
-
for (const line of lines) {
|
|
37
|
-
if (!line.trim()) continue;
|
|
38
|
-
try {
|
|
39
|
-
const msg = JSON.parse(line);
|
|
40
|
-
if (msg.type === 'input' && msg.text) {
|
|
41
|
-
onInput(msg.text);
|
|
42
|
-
}
|
|
43
|
-
} catch {}
|
|
44
|
-
}
|
|
45
|
-
},
|
|
46
|
-
error(socket, error) {
|
|
47
|
-
console.error('[Session] Daemon connection error:', error);
|
|
48
|
-
},
|
|
49
|
-
close(socket) {},
|
|
50
|
-
},
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
// Tell daemon about this session
|
|
54
|
-
socket.write(JSON.stringify({
|
|
55
|
-
type: 'session_start',
|
|
56
|
-
id: sessionId,
|
|
57
|
-
projectDir,
|
|
58
|
-
cwd,
|
|
59
|
-
command,
|
|
60
|
-
name: command.join(' '),
|
|
61
|
-
}) + '\n');
|
|
62
|
-
|
|
63
|
-
return {
|
|
64
|
-
close: () => {
|
|
65
|
-
socket.write(JSON.stringify({ type: 'session_end', sessionId }) + '\n');
|
|
66
|
-
socket.end();
|
|
67
|
-
},
|
|
68
|
-
};
|
|
69
|
-
} catch {
|
|
70
|
-
return null;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export async function run(command: string[]): Promise<void> {
|
|
75
|
-
const sessionId = randomUUID().slice(0, 8);
|
|
76
|
-
const cwd = process.cwd();
|
|
77
|
-
const projectDir = getClaudeProjectDir(cwd);
|
|
78
|
-
|
|
79
|
-
// Use bun-pty for full terminal features + remote input
|
|
80
|
-
const cols = process.stdout.columns || 80;
|
|
81
|
-
const rows = process.stdout.rows || 24;
|
|
82
|
-
|
|
83
|
-
const pty = spawnPty(command[0], command.slice(1), {
|
|
84
|
-
name: process.env.TERM || 'xterm-256color',
|
|
85
|
-
cols,
|
|
86
|
-
rows,
|
|
87
|
-
cwd,
|
|
88
|
-
env: process.env as Record<string, string>,
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
const daemon = await connectToDaemon(
|
|
92
|
-
sessionId,
|
|
93
|
-
projectDir,
|
|
94
|
-
cwd,
|
|
95
|
-
command,
|
|
96
|
-
(text) => {
|
|
97
|
-
pty.write(text);
|
|
98
|
-
}
|
|
99
|
-
);
|
|
100
|
-
|
|
101
|
-
if (process.stdin.isTTY) {
|
|
102
|
-
process.stdin.setRawMode(true);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
pty.onData((data: string) => {
|
|
106
|
-
process.stdout.write(data);
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
process.stdin.on('data', (data: Buffer) => {
|
|
110
|
-
pty.write(data.toString());
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
process.stdout.on('resize', () => {
|
|
114
|
-
pty.resize(process.stdout.columns || 80, process.stdout.rows || 24);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
await new Promise<void>((resolve) => {
|
|
118
|
-
pty.onExit(() => {
|
|
119
|
-
if (process.stdin.isTTY) {
|
|
120
|
-
process.stdin.setRawMode(false);
|
|
121
|
-
}
|
|
122
|
-
daemon?.close();
|
|
123
|
-
resolve();
|
|
124
|
-
});
|
|
125
|
-
});
|
|
126
|
-
}
|
package/src/cli/slack.ts
DELETED
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
import { homedir } from 'os';
|
|
2
|
-
import { mkdir } from 'fs/promises';
|
|
3
|
-
import * as readline from 'readline';
|
|
4
|
-
|
|
5
|
-
const CONFIG_DIR = `${homedir()}/.afk-code`;
|
|
6
|
-
const SLACK_CONFIG_FILE = `${CONFIG_DIR}/slack.env`;
|
|
7
|
-
const MANIFEST_URL = 'https://github.com/clharman/afk-code/blob/main/slack-manifest.json';
|
|
8
|
-
|
|
9
|
-
function prompt(question: string): Promise<string> {
|
|
10
|
-
const rl = readline.createInterface({
|
|
11
|
-
input: process.stdin,
|
|
12
|
-
output: process.stdout,
|
|
13
|
-
});
|
|
14
|
-
return new Promise((resolve) => {
|
|
15
|
-
rl.question(question, (answer) => {
|
|
16
|
-
rl.close();
|
|
17
|
-
resolve(answer.trim());
|
|
18
|
-
});
|
|
19
|
-
});
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export async function slackSetup(): Promise<void> {
|
|
23
|
-
console.log(`
|
|
24
|
-
┌─────────────────────────────────────────────────────────────┐
|
|
25
|
-
│ AFK Code Slack Setup │
|
|
26
|
-
└─────────────────────────────────────────────────────────────┘
|
|
27
|
-
|
|
28
|
-
This will guide you through setting up the Slack bot for
|
|
29
|
-
monitoring Claude Code sessions.
|
|
30
|
-
|
|
31
|
-
Step 1: Create a Slack App
|
|
32
|
-
──────────────────────────
|
|
33
|
-
1. Go to: https://api.slack.com/apps
|
|
34
|
-
2. Click "Create New App" → "From manifest"
|
|
35
|
-
3. Select your workspace
|
|
36
|
-
4. Paste the manifest from: ${MANIFEST_URL}
|
|
37
|
-
(Or copy from slack-manifest.json in this repo)
|
|
38
|
-
5. Click "Create"
|
|
39
|
-
|
|
40
|
-
Step 2: Install the App
|
|
41
|
-
───────────────────────
|
|
42
|
-
1. Go to "Install App" in the sidebar
|
|
43
|
-
2. Click "Install to Workspace"
|
|
44
|
-
3. Authorize the app
|
|
45
|
-
|
|
46
|
-
Step 3: Get Your Tokens
|
|
47
|
-
───────────────────────
|
|
48
|
-
`);
|
|
49
|
-
|
|
50
|
-
await prompt('Press Enter when you have created and installed the app...');
|
|
51
|
-
|
|
52
|
-
console.log(`
|
|
53
|
-
Now let's collect your tokens:
|
|
54
|
-
|
|
55
|
-
• Bot Token: "OAuth & Permissions" → "Bot User OAuth Token" (starts with xoxb-)
|
|
56
|
-
• App Token: "Basic Information" → "App-Level Tokens" → Generate one with
|
|
57
|
-
"connections:write" scope (starts with xapp-)
|
|
58
|
-
• User ID: Click your profile in Slack → "..." → "Copy member ID"
|
|
59
|
-
`);
|
|
60
|
-
|
|
61
|
-
const botToken = await prompt('Bot Token (xoxb-...): ');
|
|
62
|
-
if (!botToken.startsWith('xoxb-')) {
|
|
63
|
-
console.error('Invalid bot token. Should start with xoxb-');
|
|
64
|
-
process.exit(1);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const appToken = await prompt('App Token (xapp-...): ');
|
|
68
|
-
if (!appToken.startsWith('xapp-')) {
|
|
69
|
-
console.error('Invalid app token. Should start with xapp-');
|
|
70
|
-
process.exit(1);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const userId = await prompt('Your Slack User ID (U...): ');
|
|
74
|
-
if (!userId.startsWith('U')) {
|
|
75
|
-
console.error('Invalid user ID. Should start with U');
|
|
76
|
-
process.exit(1);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Save configuration
|
|
80
|
-
await mkdir(CONFIG_DIR, { recursive: true });
|
|
81
|
-
|
|
82
|
-
const envContent = `# AFK Code Slack Configuration
|
|
83
|
-
SLACK_BOT_TOKEN=${botToken}
|
|
84
|
-
SLACK_APP_TOKEN=${appToken}
|
|
85
|
-
SLACK_USER_ID=${userId}
|
|
86
|
-
`;
|
|
87
|
-
|
|
88
|
-
await Bun.write(SLACK_CONFIG_FILE, envContent);
|
|
89
|
-
console.log(`
|
|
90
|
-
✓ Configuration saved to ${SLACK_CONFIG_FILE}
|
|
91
|
-
|
|
92
|
-
To start the Slack bot, run:
|
|
93
|
-
afk-code slack
|
|
94
|
-
|
|
95
|
-
Then start a Claude Code session with:
|
|
96
|
-
afk-code run -- claude
|
|
97
|
-
`);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
async function loadEnvFile(path: string): Promise<Record<string, string>> {
|
|
101
|
-
const file = Bun.file(path);
|
|
102
|
-
if (!(await file.exists())) return {};
|
|
103
|
-
|
|
104
|
-
const content = await file.text();
|
|
105
|
-
const config: Record<string, string> = {};
|
|
106
|
-
|
|
107
|
-
for (const line of content.split('\n')) {
|
|
108
|
-
if (line.startsWith('#') || !line.includes('=')) continue;
|
|
109
|
-
const [key, ...valueParts] = line.split('=');
|
|
110
|
-
config[key.trim()] = valueParts.join('=').trim();
|
|
111
|
-
}
|
|
112
|
-
return config;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export async function slackRun(): Promise<void> {
|
|
116
|
-
// Load config from multiple sources (in order of precedence):
|
|
117
|
-
// 1. Environment variables (highest priority)
|
|
118
|
-
// 2. Local .env file
|
|
119
|
-
// 3. ~/.afk-code/slack.env (lowest priority)
|
|
120
|
-
|
|
121
|
-
const globalConfig = await loadEnvFile(SLACK_CONFIG_FILE);
|
|
122
|
-
const localConfig = await loadEnvFile(`${process.cwd()}/.env`);
|
|
123
|
-
|
|
124
|
-
// Merge configs (local overrides global, env vars override both)
|
|
125
|
-
const config: Record<string, string> = {
|
|
126
|
-
...globalConfig,
|
|
127
|
-
...localConfig,
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
// Environment variables take highest precedence
|
|
131
|
-
if (process.env.SLACK_BOT_TOKEN) config.SLACK_BOT_TOKEN = process.env.SLACK_BOT_TOKEN;
|
|
132
|
-
if (process.env.SLACK_APP_TOKEN) config.SLACK_APP_TOKEN = process.env.SLACK_APP_TOKEN;
|
|
133
|
-
if (process.env.SLACK_USER_ID) config.SLACK_USER_ID = process.env.SLACK_USER_ID;
|
|
134
|
-
|
|
135
|
-
// Validate required config
|
|
136
|
-
const required = ['SLACK_BOT_TOKEN', 'SLACK_APP_TOKEN', 'SLACK_USER_ID'];
|
|
137
|
-
const missing = required.filter((key) => !config[key]);
|
|
138
|
-
|
|
139
|
-
if (missing.length > 0) {
|
|
140
|
-
console.error(`Missing config: ${missing.join(', ')}`);
|
|
141
|
-
console.error('');
|
|
142
|
-
console.error('Provide tokens via:');
|
|
143
|
-
console.error(' - Environment variables (SLACK_BOT_TOKEN, SLACK_APP_TOKEN, SLACK_USER_ID)');
|
|
144
|
-
console.error(' - Local .env file');
|
|
145
|
-
console.error(' - Run "afk-code slack setup" for guided configuration');
|
|
146
|
-
process.exit(1);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Set environment variables and start the bot
|
|
150
|
-
process.env.SLACK_BOT_TOKEN = config.SLACK_BOT_TOKEN;
|
|
151
|
-
process.env.SLACK_APP_TOKEN = config.SLACK_APP_TOKEN;
|
|
152
|
-
process.env.SLACK_USER_ID = config.SLACK_USER_ID;
|
|
153
|
-
|
|
154
|
-
// Import and run the slack bot
|
|
155
|
-
const { createSlackApp } = await import('../slack/slack-app');
|
|
156
|
-
|
|
157
|
-
// Show where config was loaded from
|
|
158
|
-
const localEnvExists = await Bun.file(`${process.cwd()}/.env`).exists();
|
|
159
|
-
const globalEnvExists = await Bun.file(SLACK_CONFIG_FILE).exists();
|
|
160
|
-
const source = localEnvExists ? '.env' : globalEnvExists ? SLACK_CONFIG_FILE : 'environment';
|
|
161
|
-
console.log(`[AFK Code] Loaded config from ${source}`);
|
|
162
|
-
console.log('[AFK Code] Starting Slack bot...');
|
|
163
|
-
|
|
164
|
-
const slackConfig = {
|
|
165
|
-
botToken: config.SLACK_BOT_TOKEN,
|
|
166
|
-
appToken: config.SLACK_APP_TOKEN,
|
|
167
|
-
signingSecret: '',
|
|
168
|
-
userId: config.SLACK_USER_ID,
|
|
169
|
-
};
|
|
170
|
-
|
|
171
|
-
const { app, sessionManager } = createSlackApp(slackConfig);
|
|
172
|
-
|
|
173
|
-
// Start session manager (Unix socket server for CLI connections)
|
|
174
|
-
try {
|
|
175
|
-
await sessionManager.start();
|
|
176
|
-
console.log('[AFK Code] Session manager started');
|
|
177
|
-
} catch (err) {
|
|
178
|
-
console.error('[AFK Code] Failed to start session manager:', err);
|
|
179
|
-
process.exit(1);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// Start Slack app
|
|
183
|
-
try {
|
|
184
|
-
await app.start();
|
|
185
|
-
console.log('[AFK Code] Slack bot is running!');
|
|
186
|
-
console.log('');
|
|
187
|
-
console.log('Start a Claude Code session with: afk-code run -- claude');
|
|
188
|
-
console.log('Each session will create a private #afk-* channel');
|
|
189
|
-
} catch (err) {
|
|
190
|
-
console.error('[AFK Code] Failed to start Slack app:', err);
|
|
191
|
-
process.exit(1);
|
|
192
|
-
}
|
|
193
|
-
}
|