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 +134 -0
- package/package.json +45 -0
- package/slack-manifest.json +70 -0
- package/src/cli/discord.ts +183 -0
- package/src/cli/index.ts +83 -0
- package/src/cli/run.ts +126 -0
- package/src/cli/slack.ts +193 -0
- package/src/discord/channel-manager.ts +191 -0
- package/src/discord/discord-app.ts +359 -0
- package/src/discord/types.ts +4 -0
- package/src/slack/channel-manager.ts +175 -0
- package/src/slack/index.ts +58 -0
- package/src/slack/message-formatter.ts +91 -0
- package/src/slack/session-manager.ts +567 -0
- package/src/slack/slack-app.ts +443 -0
- package/src/slack/types.ts +6 -0
- package/src/types/index.ts +6 -0
- package/src/utils/image-extractor.ts +72 -0
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
|
+
}
|
package/src/cli/index.ts
ADDED
|
@@ -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
|
+
}
|