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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "afk-code",
3
- "version": "0.1.0",
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": "./src/cli/index.ts"
12
+ "afk-code": "./dist/cli/index.js"
13
13
  },
14
14
  "scripts": {
15
- "dev": "bun run src/cli/index.ts",
16
- "test": "bun test"
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
- "src",
21
+ "dist",
20
22
  "slack-manifest.json",
21
23
  "README.md"
22
24
  ],
23
25
  "engines": {
24
- "bun": ">=1.0.0"
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
- "@zenyr/bun-pty": "^0.4.4",
39
- "discord.js": "^14.25.1"
40
+ "discord.js": "^14.25.1",
41
+ "node-pty": "^1.0.0"
40
42
  },
41
43
  "devDependencies": {
42
- "@types/bun": "latest",
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
  }
@@ -23,17 +23,17 @@
23
23
  },
24
24
  {
25
25
  "command": "/background",
26
- "description": "Send Claude to background mode (Ctrl+B)",
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
  ]
@@ -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
- }