afk-code 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.
package/README.md ADDED
@@ -0,0 +1,134 @@
1
+ # AFK Code
2
+
3
+ Monitor your Claude Code sessions from Slack or Discord. Get notified when Claude needs input, and respond without leaving your chat app.
4
+
5
+ ## How it works
6
+
7
+ 1. Run `afk-code slack` or `afk-code discord` to start the bot
8
+ 2. Run `afk-code run -- claude` to start a monitored Claude Code session
9
+ 3. A new thread (Slack) or channel (Discord) is created for the session
10
+ 4. All messages are relayed bidirectionally - respond from your phone while AFK
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ # Clone the repo
16
+ git clone https://github.com/clharman/afk-code.git
17
+ cd afk-code
18
+
19
+ # Install dependencies
20
+ bun install
21
+
22
+ # Link the CLI globally (optional)
23
+ bun link
24
+ ```
25
+
26
+ Requires [Bun](https://bun.sh) runtime.
27
+
28
+ ## Slack Setup
29
+
30
+ ### 1. Create a Slack App
31
+
32
+ Go to [api.slack.com/apps](https://api.slack.com/apps) and click **Create New App** → **From manifest**.
33
+
34
+ Paste the contents of `slack-manifest.json` from this repo, then click **Create**.
35
+
36
+ ### 2. Install to Workspace
37
+
38
+ Click **Install to Workspace** and authorize the app.
39
+
40
+ ### 3. Get Your Credentials
41
+
42
+ - **Bot Token**: OAuth & Permissions → Bot User OAuth Token (`xoxb-...`)
43
+ - **App Token**: Basic Information → App-Level Tokens → Generate Token with `connections:write` scope (`xapp-...`)
44
+ - **Signing Secret**: Basic Information → Signing Secret
45
+ - **Your User ID**: In Slack, click your profile → three dots → Copy member ID
46
+
47
+ ### 4. Configure AFK Code
48
+
49
+ ```bash
50
+ afk-code slack setup
51
+ ```
52
+
53
+ Follow the prompts to enter your credentials. Config is saved to `~/.afk-code/slack.env`.
54
+
55
+ ### 5. Run
56
+
57
+ ```bash
58
+ # Terminal 1: Start the Slack bot
59
+ afk-code slack
60
+
61
+ # Terminal 2: Start a Claude Code session
62
+ afk-code run -- claude
63
+ ```
64
+
65
+ A new thread will appear in your Slack channel for each session.
66
+
67
+ ## Discord Setup
68
+
69
+ ### 1. Create a Discord Application
70
+
71
+ Go to [discord.com/developers/applications](https://discord.com/developers/applications) and click **New Application**.
72
+
73
+ ### 2. Create a Bot
74
+
75
+ - Go to **Bot** in the sidebar
76
+ - Click **Reset Token** and copy it
77
+ - Enable **Message Content Intent** under Privileged Gateway Intents
78
+
79
+ ### 3. Invite the Bot
80
+
81
+ - Go to **OAuth2** → **URL Generator**
82
+ - Select scopes: `bot`
83
+ - Select permissions: `Send Messages`, `Manage Channels`, `Read Message History`
84
+ - Open the generated URL to invite the bot to your server
85
+
86
+ ### 4. Get Your User ID
87
+
88
+ Enable Developer Mode in Discord settings, then right-click your name → **Copy User ID**.
89
+
90
+ ### 5. Configure AFK Code
91
+
92
+ ```bash
93
+ afk-code discord setup
94
+ ```
95
+
96
+ Enter your bot token and user ID. Config is saved to `~/.afk-code/discord.env`.
97
+
98
+ ### 6. Run
99
+
100
+ ```bash
101
+ # Terminal 1: Start the Discord bot
102
+ afk-code discord
103
+
104
+ # Terminal 2: Start a Claude Code session
105
+ afk-code run -- claude
106
+ ```
107
+
108
+ An "AFK Code Sessions" category will be created with a channel for each session.
109
+
110
+ ## Commands
111
+
112
+ ```
113
+ afk-code run -- <command> Start a monitored session (e.g., afk-code run -- claude)
114
+ afk-code slack Run the Slack bot
115
+ afk-code slack setup Configure Slack credentials
116
+ afk-code discord Run the Discord bot
117
+ afk-code discord setup Configure Discord credentials
118
+ afk-code help Show help
119
+ ```
120
+
121
+ ## How It Works
122
+
123
+ AFK Code watches Claude Code's JSONL output files to capture messages in real-time. When you start a session with `afk-code run`, it:
124
+
125
+ 1. Spawns the command in a PTY (pseudo-terminal)
126
+ 2. Connects to the running Slack/Discord bot via Unix socket
127
+ 3. Watches the Claude Code JSONL file for new messages
128
+ 4. Relays messages bidirectionally between terminal and chat
129
+
130
+ Messages you send in Slack/Discord threads are forwarded to the terminal as if you typed them.
131
+
132
+ ## License
133
+
134
+ MIT
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "afk-code",
3
+ "version": "0.1.0",
4
+ "description": "Monitor and interact with Claude Code sessions from Slack/Discord",
5
+ "author": "Colin Harman",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/clharman/afk-code.git"
9
+ },
10
+ "type": "module",
11
+ "bin": {
12
+ "afk-code": "./src/cli/index.ts"
13
+ },
14
+ "scripts": {
15
+ "dev": "bun run src/cli/index.ts",
16
+ "test": "bun test"
17
+ },
18
+ "files": [
19
+ "src",
20
+ "slack-manifest.json",
21
+ "README.md"
22
+ ],
23
+ "engines": {
24
+ "bun": ">=1.0.0"
25
+ },
26
+ "keywords": [
27
+ "claude",
28
+ "claude-code",
29
+ "slack",
30
+ "discord",
31
+ "bot",
32
+ "ai",
33
+ "cli"
34
+ ],
35
+ "license": "MIT",
36
+ "dependencies": {
37
+ "@slack/bolt": "^4.6.0",
38
+ "@zenyr/bun-pty": "^0.4.4",
39
+ "discord.js": "^14.25.1"
40
+ },
41
+ "devDependencies": {
42
+ "@types/bun": "latest",
43
+ "typescript": "^5.0.0"
44
+ }
45
+ }
@@ -0,0 +1,70 @@
1
+ {
2
+ "display_information": {
3
+ "name": "AFK Code",
4
+ "description": "Monitor and interact with Claude Code sessions from Slack",
5
+ "background_color": "#4a154b"
6
+ },
7
+ "features": {
8
+ "app_home": {
9
+ "home_tab_enabled": true,
10
+ "messages_tab_enabled": false,
11
+ "messages_tab_read_only_enabled": false
12
+ },
13
+ "bot_user": {
14
+ "display_name": "AFK Code",
15
+ "always_online": true
16
+ },
17
+ "slash_commands": [
18
+ {
19
+ "command": "/afk",
20
+ "description": "List active Claude Code sessions",
21
+ "usage_hint": "[sessions]",
22
+ "should_escape": false
23
+ },
24
+ {
25
+ "command": "/background",
26
+ "description": "Send Claude to background mode (Ctrl+B)",
27
+ "should_escape": false
28
+ },
29
+ {
30
+ "command": "/interrupt",
31
+ "description": "Interrupt Claude (Escape)",
32
+ "should_escape": false
33
+ },
34
+ {
35
+ "command": "/mode",
36
+ "description": "Toggle Claude mode (Shift+Tab)",
37
+ "should_escape": false
38
+ }
39
+ ]
40
+ },
41
+ "oauth_config": {
42
+ "scopes": {
43
+ "bot": [
44
+ "channels:history",
45
+ "channels:read",
46
+ "chat:write",
47
+ "chat:write.customize",
48
+ "commands",
49
+ "groups:history",
50
+ "groups:write",
51
+ "users:read"
52
+ ]
53
+ }
54
+ },
55
+ "settings": {
56
+ "event_subscriptions": {
57
+ "bot_events": [
58
+ "app_home_opened",
59
+ "message.channels",
60
+ "message.groups"
61
+ ]
62
+ },
63
+ "interactivity": {
64
+ "is_enabled": false
65
+ },
66
+ "org_deploy_enabled": false,
67
+ "socket_mode_enabled": true,
68
+ "token_rotation_enabled": false
69
+ }
70
+ }
@@ -0,0 +1,183 @@
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
+ }
@@ -0,0 +1,83 @@
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 ADDED
@@ -0,0 +1,126 @@
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
+ }