afk-code 0.1.1 → 0.1.4
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 +86 -87
- package/dist/cli/index.js +578 -11
- package/package.json +6 -3
- package/slack-manifest.json +12 -2
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Colin Harman
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,129 +1,128 @@
|
|
|
1
1
|
# AFK Code
|
|
2
2
|
|
|
3
|
-
Monitor
|
|
3
|
+
Monitor and interact with Claude Code sessions from Slack, Discord, or Telegram. Respond from your phone while AFK.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+

|
|
6
6
|
|
|
7
|
-
|
|
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
|
|
7
|
+
## Quick Start (Slack)
|
|
13
8
|
|
|
14
9
|
```bash
|
|
15
|
-
#
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
# Or run directly with npx
|
|
19
|
-
npx afk-code help
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
Requires Node.js 18+.
|
|
10
|
+
# 1. Create a Slack app at https://api.slack.com/apps
|
|
11
|
+
# Click "Create New App" → "From manifest" → paste slack-manifest.json
|
|
23
12
|
|
|
24
|
-
|
|
13
|
+
# 2. Install to your workspace and get credentials:
|
|
14
|
+
# - Bot Token (xoxb-...) from OAuth & Permissions
|
|
15
|
+
# - App Token (xapp-...) from Basic Information → App-Level Tokens (needs connections:write)
|
|
16
|
+
# - Your User ID from your Slack profile → "..." → Copy member ID
|
|
25
17
|
|
|
26
|
-
|
|
18
|
+
# 3. Configure and run
|
|
19
|
+
npx afk-code slack setup # Enter your credentials
|
|
20
|
+
npx afk-code slack # Start the bot
|
|
27
21
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
### 2. Install to Workspace
|
|
33
|
-
|
|
34
|
-
Click **Install to Workspace** and authorize the app.
|
|
35
|
-
|
|
36
|
-
### 3. Get Your Credentials
|
|
22
|
+
# 4. In another terminal, start a monitored Claude session
|
|
23
|
+
npx afk-code run -- claude
|
|
24
|
+
```
|
|
37
25
|
|
|
38
|
-
|
|
39
|
-
- **App Token**: Basic Information → App-Level Tokens → Generate Token with `connections:write` scope (`xapp-...`)
|
|
40
|
-
- **Signing Secret**: Basic Information → Signing Secret
|
|
41
|
-
- **Your User ID**: In Slack, click your profile → three dots → Copy member ID
|
|
26
|
+
A new channel is created for each session. Messages relay bidirectionally.
|
|
42
27
|
|
|
43
|
-
|
|
28
|
+
## Quick Start (Discord)
|
|
44
29
|
|
|
45
30
|
```bash
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
31
|
+
# 1. Create a Discord app at https://discord.com/developers/applications
|
|
32
|
+
# - Go to Bot → Reset Token → copy it
|
|
33
|
+
# - Enable "Message Content Intent"
|
|
34
|
+
# - Go to OAuth2 → URL Generator → select "bot" scope
|
|
35
|
+
# - Select permissions: Send Messages, Manage Channels, Read Message History
|
|
36
|
+
# - Open the generated URL to invite the bot
|
|
50
37
|
|
|
51
|
-
|
|
38
|
+
# 2. Get your User ID (enable Developer Mode, right-click your name → Copy User ID)
|
|
52
39
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
afk-code
|
|
40
|
+
# 3. Configure and run
|
|
41
|
+
npx afk-code discord setup # Enter your credentials
|
|
42
|
+
npx afk-code discord # Start the bot
|
|
56
43
|
|
|
57
|
-
#
|
|
58
|
-
afk-code run -- claude
|
|
44
|
+
# 4. In another terminal, start a monitored Claude session
|
|
45
|
+
npx afk-code run -- claude
|
|
59
46
|
```
|
|
60
47
|
|
|
61
|
-
|
|
48
|
+
## Quick Start (Telegram)
|
|
62
49
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
50
|
+
```bash
|
|
51
|
+
# 1. Create a bot with @BotFather on Telegram
|
|
52
|
+
# - Send /newbot and follow the prompts
|
|
53
|
+
# - Copy the bot token
|
|
66
54
|
|
|
67
|
-
|
|
55
|
+
# 2. Get your Chat ID
|
|
56
|
+
# - Message your bot, then visit:
|
|
57
|
+
# - https://api.telegram.org/bot<TOKEN>/getUpdates
|
|
58
|
+
# - Find "chat":{"id":YOUR_CHAT_ID}
|
|
68
59
|
|
|
69
|
-
|
|
60
|
+
# 3. Configure and run
|
|
61
|
+
npx afk-code telegram setup # Enter your credentials
|
|
62
|
+
npx afk-code telegram # Start the bot
|
|
70
63
|
|
|
71
|
-
|
|
72
|
-
-
|
|
73
|
-
|
|
64
|
+
# 4. In another terminal, start a monitored Claude session
|
|
65
|
+
npx afk-code run -- claude
|
|
66
|
+
```
|
|
74
67
|
|
|
75
|
-
|
|
68
|
+
## Commands
|
|
76
69
|
|
|
77
|
-
|
|
78
|
-
-
|
|
79
|
-
-
|
|
80
|
-
-
|
|
70
|
+
```
|
|
71
|
+
afk-code slack setup Configure Slack credentials
|
|
72
|
+
afk-code slack Run the Slack bot
|
|
73
|
+
afk-code discord setup Configure Discord credentials
|
|
74
|
+
afk-code discord Run the Discord bot
|
|
75
|
+
afk-code telegram setup Configure Telegram credentials
|
|
76
|
+
afk-code telegram Run the Telegram bot
|
|
77
|
+
afk-code run -- <command> Start a monitored session
|
|
78
|
+
afk-code help Show help
|
|
79
|
+
```
|
|
81
80
|
|
|
82
|
-
###
|
|
81
|
+
### Slash Commands
|
|
83
82
|
|
|
84
|
-
|
|
83
|
+
| Command | Slack | Discord | Telegram | Description |
|
|
84
|
+
|---------|:-----:|:-------:|:--------:|-------------|
|
|
85
|
+
| `/sessions` | ✓ | ✓ | ✓ | List active sessions |
|
|
86
|
+
| `/switch <name>` | - | - | ✓ | Switch session (Telegram only) |
|
|
87
|
+
| `/model <name>` | ✓ | ✓ | ✓ | Switch model (opus, sonnet, haiku) |
|
|
88
|
+
| `/compact` | ✓ | ✓ | ✓ | Compact the conversation |
|
|
89
|
+
| `/background` | ✓ | ✓ | ✓ | Send Ctrl+B (background mode) |
|
|
90
|
+
| `/interrupt` | ✓ | ✓ | ✓ | Send Escape (interrupt) |
|
|
91
|
+
| `/mode` | ✓ | ✓ | ✓ | Toggle mode (Shift+Tab) |
|
|
85
92
|
|
|
86
|
-
|
|
93
|
+
## Installation Options
|
|
87
94
|
|
|
88
95
|
```bash
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
Enter your bot token and user ID. Config is saved to `~/.afk-code/discord.env`.
|
|
96
|
+
# Global install
|
|
97
|
+
npm install -g afk-code
|
|
93
98
|
|
|
94
|
-
|
|
99
|
+
# Or use npx (no install)
|
|
100
|
+
npx afk-code <command>
|
|
95
101
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
afk-code
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
afk-code run -- claude
|
|
102
|
+
# Or run from source
|
|
103
|
+
git clone https://github.com/clharman/afk-code.git
|
|
104
|
+
cd afk-code && npm install
|
|
105
|
+
npm run dev -- slack
|
|
106
|
+
npm run dev -- run -- claude
|
|
102
107
|
```
|
|
103
108
|
|
|
104
|
-
|
|
109
|
+
Requires Node.js 18+.
|
|
105
110
|
|
|
106
|
-
##
|
|
111
|
+
## How It Works
|
|
107
112
|
|
|
108
|
-
|
|
109
|
-
afk-code run --
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
afk-code discord Run the Discord bot
|
|
113
|
-
afk-code discord setup Configure Discord credentials
|
|
114
|
-
afk-code help Show help
|
|
115
|
-
```
|
|
113
|
+
1. `afk-code slack`, `afk-code discord`, or `afk-code telegram` starts a bot that listens for sessions
|
|
114
|
+
2. `afk-code run -- claude` spawns Claude in a PTY and connects to the bot via Unix socket
|
|
115
|
+
3. The bot watches Claude's JSONL files for messages and relays them to chat
|
|
116
|
+
4. Messages you send in chat are forwarded to the terminal
|
|
116
117
|
|
|
117
|
-
##
|
|
118
|
+
## Limitations
|
|
118
119
|
|
|
119
|
-
|
|
120
|
+
- Does not support plan mode or responding to Claude Code's form-based questions (AskUserQuestion)
|
|
121
|
+
- Does not send tool calls or results
|
|
120
122
|
|
|
121
|
-
|
|
122
|
-
2. Connects to the running Slack/Discord bot via Unix socket
|
|
123
|
-
3. Watches the Claude Code JSONL file for new messages
|
|
124
|
-
4. Relays messages bidirectionally between terminal and chat
|
|
123
|
+
## Disclaimer
|
|
125
124
|
|
|
126
|
-
|
|
125
|
+
This project is not affiliated with Anthropic. Use at your own risk.
|
|
127
126
|
|
|
128
127
|
## License
|
|
129
128
|
|
package/dist/cli/index.js
CHANGED
|
@@ -889,7 +889,7 @@ ${todosText}`,
|
|
|
889
889
|
await say(":warning: Failed to send input - session not connected.");
|
|
890
890
|
}
|
|
891
891
|
});
|
|
892
|
-
app.command("/
|
|
892
|
+
app.command("/sessions", async ({ command: command2, ack, respond }) => {
|
|
893
893
|
await ack();
|
|
894
894
|
const subcommand = command2.text.trim().split(" ")[0];
|
|
895
895
|
if (subcommand === "sessions" || !subcommand) {
|
|
@@ -905,7 +905,7 @@ ${text}`,
|
|
|
905
905
|
mrkdwn: true
|
|
906
906
|
});
|
|
907
907
|
} else {
|
|
908
|
-
await respond("Unknown command.
|
|
908
|
+
await respond("Unknown command. Try `/sessions`");
|
|
909
909
|
}
|
|
910
910
|
});
|
|
911
911
|
app.command("/background", async ({ command: command2, ack, respond }) => {
|
|
@@ -965,6 +965,50 @@ ${text}`,
|
|
|
965
965
|
await respond(":warning: Failed to send command - session not connected.");
|
|
966
966
|
}
|
|
967
967
|
});
|
|
968
|
+
app.command("/compact", async ({ command: command2, ack, respond }) => {
|
|
969
|
+
await ack();
|
|
970
|
+
const sessionId = channelManager.getSessionByChannel(command2.channel_id);
|
|
971
|
+
if (!sessionId) {
|
|
972
|
+
await respond(":warning: This channel is not associated with an active session.");
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
const channel = channelManager.getChannel(sessionId);
|
|
976
|
+
if (!channel || channel.status === "ended") {
|
|
977
|
+
await respond(":warning: This session has ended.");
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
const sent = sessionManager.sendInput(sessionId, "/compact\n");
|
|
981
|
+
if (sent) {
|
|
982
|
+
await respond(":compression: Sent /compact");
|
|
983
|
+
} else {
|
|
984
|
+
await respond(":warning: Failed to send command - session not connected.");
|
|
985
|
+
}
|
|
986
|
+
});
|
|
987
|
+
app.command("/model", async ({ command: command2, ack, respond }) => {
|
|
988
|
+
await ack();
|
|
989
|
+
const sessionId = channelManager.getSessionByChannel(command2.channel_id);
|
|
990
|
+
if (!sessionId) {
|
|
991
|
+
await respond(":warning: This channel is not associated with an active session.");
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
const channel = channelManager.getChannel(sessionId);
|
|
995
|
+
if (!channel || channel.status === "ended") {
|
|
996
|
+
await respond(":warning: This session has ended.");
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
const modelArg = command2.text.trim();
|
|
1000
|
+
if (!modelArg) {
|
|
1001
|
+
await respond("Usage: `/model <opus|sonnet|haiku>`");
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
const sent = sessionManager.sendInput(sessionId, `/model ${modelArg}
|
|
1005
|
+
`);
|
|
1006
|
+
if (sent) {
|
|
1007
|
+
await respond(`:brain: Sent /model ${modelArg}`);
|
|
1008
|
+
} else {
|
|
1009
|
+
await respond(":warning: Failed to send command - session not connected.");
|
|
1010
|
+
}
|
|
1011
|
+
});
|
|
968
1012
|
app.event("app_home_opened", async ({ event, client }) => {
|
|
969
1013
|
const active = channelManager.getAllActive();
|
|
970
1014
|
const blocks = [
|
|
@@ -1413,7 +1457,9 @@ ${content}
|
|
|
1413
1457
|
new SlashCommandBuilder().setName("background").setDescription("Send Claude to background mode (Ctrl+B)"),
|
|
1414
1458
|
new SlashCommandBuilder().setName("interrupt").setDescription("Interrupt Claude (Escape)"),
|
|
1415
1459
|
new SlashCommandBuilder().setName("mode").setDescription("Toggle Claude mode (Shift+Tab)"),
|
|
1416
|
-
new SlashCommandBuilder().setName("
|
|
1460
|
+
new SlashCommandBuilder().setName("sessions").setDescription("List active Claude Code sessions"),
|
|
1461
|
+
new SlashCommandBuilder().setName("compact").setDescription("Compact the conversation (/compact)"),
|
|
1462
|
+
new SlashCommandBuilder().setName("model").setDescription("Switch Claude model").addStringOption((option) => option.setName("name").setDescription("Model name (opus, sonnet, haiku)").setRequired(true))
|
|
1417
1463
|
];
|
|
1418
1464
|
try {
|
|
1419
1465
|
const rest = new REST({ version: "10" }).setToken(config.botToken);
|
|
@@ -1428,7 +1474,7 @@ ${content}
|
|
|
1428
1474
|
client.on(Events.InteractionCreate, async (interaction) => {
|
|
1429
1475
|
if (!interaction.isChatInputCommand()) return;
|
|
1430
1476
|
const { commandName, channelId } = interaction;
|
|
1431
|
-
if (commandName === "
|
|
1477
|
+
if (commandName === "sessions") {
|
|
1432
1478
|
const active = channelManager.getAllActive();
|
|
1433
1479
|
if (active.length === 0) {
|
|
1434
1480
|
await interaction.reply("No active sessions. Start a session with `afk-code run -- claude`");
|
|
@@ -1469,6 +1515,44 @@ ${text}`);
|
|
|
1469
1515
|
await interaction.reply("\u26A0\uFE0F Failed to send command - session not connected.");
|
|
1470
1516
|
}
|
|
1471
1517
|
}
|
|
1518
|
+
if (commandName === "compact") {
|
|
1519
|
+
const sessionId = channelManager.getSessionByChannel(channelId);
|
|
1520
|
+
if (!sessionId) {
|
|
1521
|
+
await interaction.reply("\u26A0\uFE0F This channel is not associated with an active session.");
|
|
1522
|
+
return;
|
|
1523
|
+
}
|
|
1524
|
+
const channel = channelManager.getChannel(sessionId);
|
|
1525
|
+
if (!channel || channel.status === "ended") {
|
|
1526
|
+
await interaction.reply("\u26A0\uFE0F This session has ended.");
|
|
1527
|
+
return;
|
|
1528
|
+
}
|
|
1529
|
+
const sent = sessionManager.sendInput(sessionId, "/compact\n");
|
|
1530
|
+
if (sent) {
|
|
1531
|
+
await interaction.reply("\u{1F5DC}\uFE0F Sent /compact");
|
|
1532
|
+
} else {
|
|
1533
|
+
await interaction.reply("\u26A0\uFE0F Failed to send command - session not connected.");
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
if (commandName === "model") {
|
|
1537
|
+
const sessionId = channelManager.getSessionByChannel(channelId);
|
|
1538
|
+
if (!sessionId) {
|
|
1539
|
+
await interaction.reply("\u26A0\uFE0F This channel is not associated with an active session.");
|
|
1540
|
+
return;
|
|
1541
|
+
}
|
|
1542
|
+
const channel = channelManager.getChannel(sessionId);
|
|
1543
|
+
if (!channel || channel.status === "ended") {
|
|
1544
|
+
await interaction.reply("\u26A0\uFE0F This session has ended.");
|
|
1545
|
+
return;
|
|
1546
|
+
}
|
|
1547
|
+
const modelArg = interaction.options.getString("name", true);
|
|
1548
|
+
const sent = sessionManager.sendInput(sessionId, `/model ${modelArg}
|
|
1549
|
+
`);
|
|
1550
|
+
if (sent) {
|
|
1551
|
+
await interaction.reply(`\u{1F9E0} Sent /model ${modelArg}`);
|
|
1552
|
+
} else {
|
|
1553
|
+
await interaction.reply("\u26A0\uFE0F Failed to send command - session not connected.");
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1472
1556
|
});
|
|
1473
1557
|
return { client, sessionManager, channelManager };
|
|
1474
1558
|
}
|
|
@@ -1482,6 +1566,345 @@ var init_discord_app = __esm({
|
|
|
1482
1566
|
}
|
|
1483
1567
|
});
|
|
1484
1568
|
|
|
1569
|
+
// src/telegram/telegram-app.ts
|
|
1570
|
+
var telegram_app_exports = {};
|
|
1571
|
+
__export(telegram_app_exports, {
|
|
1572
|
+
createTelegramApp: () => createTelegramApp
|
|
1573
|
+
});
|
|
1574
|
+
import { Bot } from "grammy";
|
|
1575
|
+
function createTelegramApp(config) {
|
|
1576
|
+
const bot = new Bot(config.botToken);
|
|
1577
|
+
const activeSessions = /* @__PURE__ */ new Map();
|
|
1578
|
+
const telegramSentMessages = /* @__PURE__ */ new Set();
|
|
1579
|
+
let currentSessionId = null;
|
|
1580
|
+
const messageQueue = [];
|
|
1581
|
+
let processingQueue = false;
|
|
1582
|
+
async function processQueue() {
|
|
1583
|
+
if (processingQueue) return;
|
|
1584
|
+
processingQueue = true;
|
|
1585
|
+
while (messageQueue.length > 0) {
|
|
1586
|
+
const fn = messageQueue.shift();
|
|
1587
|
+
if (fn) {
|
|
1588
|
+
try {
|
|
1589
|
+
await fn();
|
|
1590
|
+
} catch (err) {
|
|
1591
|
+
console.error("[Telegram] Error sending message:", err);
|
|
1592
|
+
}
|
|
1593
|
+
if (messageQueue.length > 0) {
|
|
1594
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
processingQueue = false;
|
|
1599
|
+
}
|
|
1600
|
+
async function sendMessage(text, parseMode = "Markdown") {
|
|
1601
|
+
messageQueue.push(async () => {
|
|
1602
|
+
try {
|
|
1603
|
+
await bot.api.sendMessage(config.chatId, text, { parse_mode: parseMode });
|
|
1604
|
+
} catch (err) {
|
|
1605
|
+
if (parseMode && err.message?.includes("parse")) {
|
|
1606
|
+
await bot.api.sendMessage(config.chatId, text);
|
|
1607
|
+
} else {
|
|
1608
|
+
throw err;
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
});
|
|
1612
|
+
processQueue();
|
|
1613
|
+
}
|
|
1614
|
+
async function sendChunkedMessage(text, prefix) {
|
|
1615
|
+
const chunks = chunkMessage(text, MAX_MESSAGE_LENGTH);
|
|
1616
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
1617
|
+
const chunk = prefix && i === 0 ? `${prefix}
|
|
1618
|
+
|
|
1619
|
+
${chunks[i]}` : chunks[i];
|
|
1620
|
+
await sendMessage(chunk);
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
const sessionManager = new SessionManager({
|
|
1624
|
+
onSessionStart: async (session) => {
|
|
1625
|
+
activeSessions.set(session.id, {
|
|
1626
|
+
sessionId: session.id,
|
|
1627
|
+
sessionName: session.name,
|
|
1628
|
+
lastActivity: /* @__PURE__ */ new Date()
|
|
1629
|
+
});
|
|
1630
|
+
await sendMessage(
|
|
1631
|
+
`*[${session.name}]* ${formatSessionStatus(session.status)}
|
|
1632
|
+
Session started
|
|
1633
|
+
\`${session.cwd}\``
|
|
1634
|
+
);
|
|
1635
|
+
},
|
|
1636
|
+
onSessionEnd: async (sessionId) => {
|
|
1637
|
+
const tracking = activeSessions.get(sessionId);
|
|
1638
|
+
const name = tracking?.sessionName || sessionId;
|
|
1639
|
+
activeSessions.delete(sessionId);
|
|
1640
|
+
await sendMessage(`*[${name}]* Session ended`);
|
|
1641
|
+
},
|
|
1642
|
+
onSessionUpdate: async (sessionId, name) => {
|
|
1643
|
+
const tracking = activeSessions.get(sessionId);
|
|
1644
|
+
if (tracking) {
|
|
1645
|
+
tracking.sessionName = name;
|
|
1646
|
+
tracking.lastActivity = /* @__PURE__ */ new Date();
|
|
1647
|
+
}
|
|
1648
|
+
},
|
|
1649
|
+
onSessionStatus: async (sessionId, _status) => {
|
|
1650
|
+
const tracking = activeSessions.get(sessionId);
|
|
1651
|
+
if (tracking) {
|
|
1652
|
+
tracking.lastActivity = /* @__PURE__ */ new Date();
|
|
1653
|
+
}
|
|
1654
|
+
},
|
|
1655
|
+
onMessage: async (sessionId, role, content) => {
|
|
1656
|
+
const tracking = activeSessions.get(sessionId);
|
|
1657
|
+
if (!tracking) return;
|
|
1658
|
+
tracking.lastActivity = /* @__PURE__ */ new Date();
|
|
1659
|
+
if (role === "user") {
|
|
1660
|
+
const contentKey = content.trim();
|
|
1661
|
+
if (telegramSentMessages.has(contentKey)) {
|
|
1662
|
+
telegramSentMessages.delete(contentKey);
|
|
1663
|
+
return;
|
|
1664
|
+
}
|
|
1665
|
+
await sendChunkedMessage(content, `*[${tracking.sessionName}]* *User:*`);
|
|
1666
|
+
} else {
|
|
1667
|
+
await sendChunkedMessage(content, `*[${tracking.sessionName}]* *Claude:*`);
|
|
1668
|
+
}
|
|
1669
|
+
},
|
|
1670
|
+
onTodos: async (sessionId, todos) => {
|
|
1671
|
+
const tracking = activeSessions.get(sessionId);
|
|
1672
|
+
if (!tracking || todos.length === 0) return;
|
|
1673
|
+
const todosText = formatTodos(todos);
|
|
1674
|
+
await sendMessage(`*[${tracking.sessionName}]* *Tasks:*
|
|
1675
|
+
${todosText}`);
|
|
1676
|
+
},
|
|
1677
|
+
onToolCall: async (_sessionId, _tool) => {
|
|
1678
|
+
},
|
|
1679
|
+
onToolResult: async (_sessionId, _result) => {
|
|
1680
|
+
},
|
|
1681
|
+
onPlanModeChange: async (sessionId, inPlanMode) => {
|
|
1682
|
+
const tracking = activeSessions.get(sessionId);
|
|
1683
|
+
if (!tracking) return;
|
|
1684
|
+
const status = inPlanMode ? "Planning mode - Claude is designing a solution" : "Execution mode - Claude is implementing";
|
|
1685
|
+
await sendMessage(`*[${tracking.sessionName}]* ${status}`);
|
|
1686
|
+
}
|
|
1687
|
+
});
|
|
1688
|
+
function getCurrentSession() {
|
|
1689
|
+
if (currentSessionId) {
|
|
1690
|
+
const session = activeSessions.get(currentSessionId);
|
|
1691
|
+
if (session) return session;
|
|
1692
|
+
currentSessionId = null;
|
|
1693
|
+
}
|
|
1694
|
+
if (activeSessions.size === 1) {
|
|
1695
|
+
return activeSessions.values().next().value;
|
|
1696
|
+
}
|
|
1697
|
+
return null;
|
|
1698
|
+
}
|
|
1699
|
+
function getSessionByName(name) {
|
|
1700
|
+
const nameLower = name.toLowerCase();
|
|
1701
|
+
for (const tracking of activeSessions.values()) {
|
|
1702
|
+
if (tracking.sessionName.toLowerCase().startsWith(nameLower)) {
|
|
1703
|
+
return tracking;
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
return null;
|
|
1707
|
+
}
|
|
1708
|
+
function getMostRecentSession() {
|
|
1709
|
+
let mostRecent = null;
|
|
1710
|
+
for (const tracking of activeSessions.values()) {
|
|
1711
|
+
if (!mostRecent || tracking.lastActivity > mostRecent.lastActivity) {
|
|
1712
|
+
mostRecent = tracking;
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
return mostRecent;
|
|
1716
|
+
}
|
|
1717
|
+
bot.on("message:text", async (ctx) => {
|
|
1718
|
+
if (ctx.chat.id.toString() !== config.chatId) return;
|
|
1719
|
+
const text = ctx.message.text;
|
|
1720
|
+
if (text.startsWith("/")) {
|
|
1721
|
+
await handleCommand(ctx, text.trim());
|
|
1722
|
+
return;
|
|
1723
|
+
}
|
|
1724
|
+
const current = getCurrentSession();
|
|
1725
|
+
if (!current) {
|
|
1726
|
+
if (activeSessions.size === 0) {
|
|
1727
|
+
await ctx.reply("No active sessions. Start one with:\n`afk-code run -- claude`", { parse_mode: "Markdown" });
|
|
1728
|
+
} else {
|
|
1729
|
+
const list = Array.from(activeSessions.values()).map((s) => `\u2022 \`${s.sessionName}\``).join("\n");
|
|
1730
|
+
await ctx.reply(
|
|
1731
|
+
`Multiple sessions active. Select one first:
|
|
1732
|
+
|
|
1733
|
+
${list}
|
|
1734
|
+
|
|
1735
|
+
Use: \`/switch <name>\``,
|
|
1736
|
+
{ parse_mode: "Markdown" }
|
|
1737
|
+
);
|
|
1738
|
+
}
|
|
1739
|
+
return;
|
|
1740
|
+
}
|
|
1741
|
+
telegramSentMessages.add(text.trim());
|
|
1742
|
+
const sent = sessionManager.sendInput(current.sessionId, text);
|
|
1743
|
+
if (!sent) {
|
|
1744
|
+
telegramSentMessages.delete(text.trim());
|
|
1745
|
+
await ctx.reply("Failed to send input - session not connected.");
|
|
1746
|
+
}
|
|
1747
|
+
});
|
|
1748
|
+
async function handleCommand(ctx, text) {
|
|
1749
|
+
const [command2, ...args2] = text.split(" ");
|
|
1750
|
+
const sessionArg = args2[0];
|
|
1751
|
+
let targetSession = null;
|
|
1752
|
+
if (sessionArg && !sessionArg.startsWith("/")) {
|
|
1753
|
+
for (const tracking of activeSessions.values()) {
|
|
1754
|
+
if (tracking.sessionName.toLowerCase().startsWith(sessionArg.toLowerCase())) {
|
|
1755
|
+
targetSession = tracking;
|
|
1756
|
+
break;
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
if (!targetSession) {
|
|
1761
|
+
targetSession = getMostRecentSession();
|
|
1762
|
+
}
|
|
1763
|
+
switch (command2.toLowerCase()) {
|
|
1764
|
+
case "/start": {
|
|
1765
|
+
await ctx.reply(
|
|
1766
|
+
`*AFK Code Telegram Bot*
|
|
1767
|
+
|
|
1768
|
+
This bot lets you monitor and interact with Claude Code sessions.
|
|
1769
|
+
|
|
1770
|
+
Start a session with:
|
|
1771
|
+
\`afk-code run -- claude\`
|
|
1772
|
+
|
|
1773
|
+
Type /help for available commands.`,
|
|
1774
|
+
{ parse_mode: "Markdown" }
|
|
1775
|
+
);
|
|
1776
|
+
break;
|
|
1777
|
+
}
|
|
1778
|
+
case "/sessions": {
|
|
1779
|
+
if (activeSessions.size === 0) {
|
|
1780
|
+
await ctx.reply("No active sessions. Start one with `afk-code run -- claude`");
|
|
1781
|
+
return;
|
|
1782
|
+
}
|
|
1783
|
+
const current = getCurrentSession();
|
|
1784
|
+
const list = Array.from(activeSessions.values()).map((s) => {
|
|
1785
|
+
const isCurrent = current && s.sessionId === current.sessionId;
|
|
1786
|
+
return isCurrent ? `\u2022 *${s.sessionName}* \u2190 current` : `\u2022 ${s.sessionName}`;
|
|
1787
|
+
}).join("\n");
|
|
1788
|
+
await ctx.reply(`*Active Sessions:*
|
|
1789
|
+
${list}
|
|
1790
|
+
|
|
1791
|
+
Use \`/switch <name>\` to change`, { parse_mode: "Markdown" });
|
|
1792
|
+
break;
|
|
1793
|
+
}
|
|
1794
|
+
case "/switch":
|
|
1795
|
+
case "/select": {
|
|
1796
|
+
if (!sessionArg) {
|
|
1797
|
+
if (activeSessions.size === 0) {
|
|
1798
|
+
await ctx.reply("No active sessions.");
|
|
1799
|
+
return;
|
|
1800
|
+
}
|
|
1801
|
+
const current = getCurrentSession();
|
|
1802
|
+
const list = Array.from(activeSessions.values()).map((s) => {
|
|
1803
|
+
const isCurrent = current && s.sessionId === current.sessionId;
|
|
1804
|
+
return isCurrent ? `\u2022 *${s.sessionName}* \u2190 current` : `\u2022 ${s.sessionName}`;
|
|
1805
|
+
}).join("\n");
|
|
1806
|
+
await ctx.reply(`*Sessions:*
|
|
1807
|
+
${list}
|
|
1808
|
+
|
|
1809
|
+
Use: \`/switch <name>\``, { parse_mode: "Markdown" });
|
|
1810
|
+
return;
|
|
1811
|
+
}
|
|
1812
|
+
const session = getSessionByName(sessionArg);
|
|
1813
|
+
if (session) {
|
|
1814
|
+
currentSessionId = session.sessionId;
|
|
1815
|
+
await ctx.reply(`Switched to: *${session.sessionName}*`, { parse_mode: "Markdown" });
|
|
1816
|
+
} else {
|
|
1817
|
+
await ctx.reply(`Session not found: ${sessionArg}`);
|
|
1818
|
+
}
|
|
1819
|
+
break;
|
|
1820
|
+
}
|
|
1821
|
+
case "/background":
|
|
1822
|
+
case "/bg": {
|
|
1823
|
+
if (!targetSession) {
|
|
1824
|
+
await ctx.reply("No active session.");
|
|
1825
|
+
return;
|
|
1826
|
+
}
|
|
1827
|
+
const sent = sessionManager.sendInput(targetSession.sessionId, "");
|
|
1828
|
+
await ctx.reply(sent ? "Sent background command (Ctrl+B)" : "Failed - session not connected.");
|
|
1829
|
+
break;
|
|
1830
|
+
}
|
|
1831
|
+
case "/interrupt":
|
|
1832
|
+
case "/stop": {
|
|
1833
|
+
if (!targetSession) {
|
|
1834
|
+
await ctx.reply("No active session.");
|
|
1835
|
+
return;
|
|
1836
|
+
}
|
|
1837
|
+
const sent = sessionManager.sendInput(targetSession.sessionId, "\x1B");
|
|
1838
|
+
await ctx.reply(sent ? "Sent interrupt (Escape)" : "Failed - session not connected.");
|
|
1839
|
+
break;
|
|
1840
|
+
}
|
|
1841
|
+
case "/mode": {
|
|
1842
|
+
if (!targetSession) {
|
|
1843
|
+
await ctx.reply("No active session.");
|
|
1844
|
+
return;
|
|
1845
|
+
}
|
|
1846
|
+
const sent = sessionManager.sendInput(targetSession.sessionId, "\x1B[Z");
|
|
1847
|
+
await ctx.reply(sent ? "Sent mode toggle (Shift+Tab)" : "Failed - session not connected.");
|
|
1848
|
+
break;
|
|
1849
|
+
}
|
|
1850
|
+
case "/compact": {
|
|
1851
|
+
if (!targetSession) {
|
|
1852
|
+
await ctx.reply("No active session.");
|
|
1853
|
+
return;
|
|
1854
|
+
}
|
|
1855
|
+
const sent = sessionManager.sendInput(targetSession.sessionId, "/compact\n");
|
|
1856
|
+
await ctx.reply(sent ? "Sent /compact" : "Failed - session not connected.");
|
|
1857
|
+
break;
|
|
1858
|
+
}
|
|
1859
|
+
case "/model": {
|
|
1860
|
+
if (!targetSession) {
|
|
1861
|
+
await ctx.reply("No active session.");
|
|
1862
|
+
return;
|
|
1863
|
+
}
|
|
1864
|
+
const modelArg = args2.slice(targetSession === getSessionByName(args2[0] || "") ? 1 : 0).join(" ");
|
|
1865
|
+
if (!modelArg) {
|
|
1866
|
+
await ctx.reply("Usage: `/model <opus|sonnet|haiku>`", { parse_mode: "Markdown" });
|
|
1867
|
+
return;
|
|
1868
|
+
}
|
|
1869
|
+
const sent = sessionManager.sendInput(targetSession.sessionId, `/model ${modelArg}
|
|
1870
|
+
`);
|
|
1871
|
+
await ctx.reply(sent ? `Sent /model ${modelArg}` : "Failed - session not connected.");
|
|
1872
|
+
break;
|
|
1873
|
+
}
|
|
1874
|
+
case "/help": {
|
|
1875
|
+
await ctx.reply(
|
|
1876
|
+
`*AFK Code Commands:*
|
|
1877
|
+
|
|
1878
|
+
/sessions - List active sessions
|
|
1879
|
+
/switch <name> - Switch to a session
|
|
1880
|
+
/model <name> - Switch model
|
|
1881
|
+
/compact - Compact conversation
|
|
1882
|
+
/background - Send Ctrl+B
|
|
1883
|
+
/interrupt - Send Escape
|
|
1884
|
+
/mode - Toggle mode (Shift+Tab)
|
|
1885
|
+
/help - Show this message
|
|
1886
|
+
|
|
1887
|
+
_Messages go to the current session (auto-selected if only one)._`,
|
|
1888
|
+
{ parse_mode: "Markdown" }
|
|
1889
|
+
);
|
|
1890
|
+
break;
|
|
1891
|
+
}
|
|
1892
|
+
default:
|
|
1893
|
+
break;
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
return { bot, sessionManager };
|
|
1897
|
+
}
|
|
1898
|
+
var MAX_MESSAGE_LENGTH;
|
|
1899
|
+
var init_telegram_app = __esm({
|
|
1900
|
+
"src/telegram/telegram-app.ts"() {
|
|
1901
|
+
"use strict";
|
|
1902
|
+
init_session_manager();
|
|
1903
|
+
init_message_formatter();
|
|
1904
|
+
MAX_MESSAGE_LENGTH = 4e3;
|
|
1905
|
+
}
|
|
1906
|
+
});
|
|
1907
|
+
|
|
1485
1908
|
// src/cli/run.ts
|
|
1486
1909
|
import { randomUUID } from "crypto";
|
|
1487
1910
|
import { homedir } from "os";
|
|
@@ -1573,7 +1996,9 @@ async function run(command2) {
|
|
|
1573
1996
|
if (process.stdin.isTTY) {
|
|
1574
1997
|
process.stdin.setRawMode(false);
|
|
1575
1998
|
}
|
|
1576
|
-
process.stdin.unref
|
|
1999
|
+
if (typeof process.stdin.unref === "function") {
|
|
2000
|
+
process.stdin.unref();
|
|
2001
|
+
}
|
|
1577
2002
|
daemon?.close();
|
|
1578
2003
|
resolve2();
|
|
1579
2004
|
});
|
|
@@ -1897,6 +2322,136 @@ async function discordRun() {
|
|
|
1897
2322
|
}
|
|
1898
2323
|
}
|
|
1899
2324
|
|
|
2325
|
+
// src/cli/telegram.ts
|
|
2326
|
+
import { homedir as homedir5 } from "os";
|
|
2327
|
+
import { mkdir as mkdir3, writeFile as writeFile3, readFile as readFile4, access as access3 } from "fs/promises";
|
|
2328
|
+
import * as readline3 from "readline";
|
|
2329
|
+
var CONFIG_DIR3 = `${homedir5()}/.afk-code`;
|
|
2330
|
+
var TELEGRAM_CONFIG_FILE = `${CONFIG_DIR3}/telegram.env`;
|
|
2331
|
+
function prompt3(question) {
|
|
2332
|
+
const rl = readline3.createInterface({
|
|
2333
|
+
input: process.stdin,
|
|
2334
|
+
output: process.stdout
|
|
2335
|
+
});
|
|
2336
|
+
return new Promise((resolve2) => {
|
|
2337
|
+
rl.question(question, (answer) => {
|
|
2338
|
+
rl.close();
|
|
2339
|
+
resolve2(answer.trim());
|
|
2340
|
+
});
|
|
2341
|
+
});
|
|
2342
|
+
}
|
|
2343
|
+
async function fileExists3(path) {
|
|
2344
|
+
try {
|
|
2345
|
+
await access3(path);
|
|
2346
|
+
return true;
|
|
2347
|
+
} catch {
|
|
2348
|
+
return false;
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
async function telegramSetup() {
|
|
2352
|
+
console.log(`
|
|
2353
|
+
\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510
|
|
2354
|
+
\u2502 AFK Code Telegram Setup \u2502
|
|
2355
|
+
\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518
|
|
2356
|
+
|
|
2357
|
+
This will configure a Telegram bot for monitoring Claude Code sessions.
|
|
2358
|
+
|
|
2359
|
+
Step 1: Create a Telegram Bot
|
|
2360
|
+
\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2361
|
+
1. Open Telegram and search for @BotFather
|
|
2362
|
+
2. Send /newbot and follow the prompts
|
|
2363
|
+
3. Choose a name (e.g., "AFK Code")
|
|
2364
|
+
4. Choose a username (e.g., "my_afk_code_bot")
|
|
2365
|
+
5. Copy the bot token BotFather gives you
|
|
2366
|
+
`);
|
|
2367
|
+
const botToken = await prompt3("Bot Token: ");
|
|
2368
|
+
if (!botToken || !botToken.includes(":")) {
|
|
2369
|
+
console.error("Invalid bot token. It should look like: 123456789:ABCdefGHIjklMNOpqrsTUVwxyz");
|
|
2370
|
+
process.exit(1);
|
|
2371
|
+
}
|
|
2372
|
+
console.log(`
|
|
2373
|
+
Step 2: Get Your Chat ID
|
|
2374
|
+
\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2375
|
+
1. Start a chat with your new bot in Telegram
|
|
2376
|
+
2. Send it any message (e.g., "hello")
|
|
2377
|
+
3. Visit this URL in your browser:
|
|
2378
|
+
https://api.telegram.org/bot${botToken}/getUpdates
|
|
2379
|
+
4. Find "chat":{"id":YOUR_CHAT_ID} in the response
|
|
2380
|
+
5. Copy the numeric chat ID
|
|
2381
|
+
`);
|
|
2382
|
+
const chatId = await prompt3("Chat ID: ");
|
|
2383
|
+
if (!chatId || !/^-?\d+$/.test(chatId)) {
|
|
2384
|
+
console.error("Invalid chat ID. It should be a number (can be negative for groups).");
|
|
2385
|
+
process.exit(1);
|
|
2386
|
+
}
|
|
2387
|
+
await mkdir3(CONFIG_DIR3, { recursive: true });
|
|
2388
|
+
const envContent = `# AFK Code Telegram Configuration
|
|
2389
|
+
TELEGRAM_BOT_TOKEN=${botToken}
|
|
2390
|
+
TELEGRAM_CHAT_ID=${chatId}
|
|
2391
|
+
`;
|
|
2392
|
+
await writeFile3(TELEGRAM_CONFIG_FILE, envContent);
|
|
2393
|
+
console.log(`
|
|
2394
|
+
Configuration saved to ${TELEGRAM_CONFIG_FILE}
|
|
2395
|
+
|
|
2396
|
+
To start the Telegram bot, run:
|
|
2397
|
+
afk-code telegram
|
|
2398
|
+
|
|
2399
|
+
Then start a Claude Code session with:
|
|
2400
|
+
afk-code run -- claude
|
|
2401
|
+
|
|
2402
|
+
Your bot will send session updates to your Telegram chat!
|
|
2403
|
+
`);
|
|
2404
|
+
}
|
|
2405
|
+
async function loadEnvFile3(path) {
|
|
2406
|
+
if (!await fileExists3(path)) return {};
|
|
2407
|
+
const content = await readFile4(path, "utf-8");
|
|
2408
|
+
const config = {};
|
|
2409
|
+
for (const line of content.split("\n")) {
|
|
2410
|
+
if (line.startsWith("#") || !line.includes("=")) continue;
|
|
2411
|
+
const [key, ...valueParts] = line.split("=");
|
|
2412
|
+
config[key.trim()] = valueParts.join("=").trim();
|
|
2413
|
+
}
|
|
2414
|
+
return config;
|
|
2415
|
+
}
|
|
2416
|
+
async function telegramRun() {
|
|
2417
|
+
const globalConfig = await loadEnvFile3(TELEGRAM_CONFIG_FILE);
|
|
2418
|
+
const localConfig = await loadEnvFile3(`${process.cwd()}/.env`);
|
|
2419
|
+
const config = {
|
|
2420
|
+
...globalConfig,
|
|
2421
|
+
...localConfig
|
|
2422
|
+
};
|
|
2423
|
+
if (process.env.TELEGRAM_BOT_TOKEN) config.TELEGRAM_BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN;
|
|
2424
|
+
if (process.env.TELEGRAM_CHAT_ID) config.TELEGRAM_CHAT_ID = process.env.TELEGRAM_CHAT_ID;
|
|
2425
|
+
const required = ["TELEGRAM_BOT_TOKEN", "TELEGRAM_CHAT_ID"];
|
|
2426
|
+
const missing = required.filter((key) => !config[key]);
|
|
2427
|
+
if (missing.length > 0) {
|
|
2428
|
+
console.error(`Missing config: ${missing.join(", ")}`);
|
|
2429
|
+
console.error("");
|
|
2430
|
+
console.error('Run "afk-code telegram setup" for guided configuration.');
|
|
2431
|
+
process.exit(1);
|
|
2432
|
+
}
|
|
2433
|
+
console.log("[AFK Code] Starting Telegram bot...");
|
|
2434
|
+
const { createTelegramApp: createTelegramApp2 } = await Promise.resolve().then(() => (init_telegram_app(), telegram_app_exports));
|
|
2435
|
+
const telegramConfig = {
|
|
2436
|
+
botToken: config.TELEGRAM_BOT_TOKEN,
|
|
2437
|
+
chatId: config.TELEGRAM_CHAT_ID
|
|
2438
|
+
};
|
|
2439
|
+
const { bot, sessionManager } = createTelegramApp2(telegramConfig);
|
|
2440
|
+
try {
|
|
2441
|
+
await sessionManager.start();
|
|
2442
|
+
} catch (err) {
|
|
2443
|
+
console.error("[AFK Code] Failed to start session manager:", err);
|
|
2444
|
+
process.exit(1);
|
|
2445
|
+
}
|
|
2446
|
+
bot.start({
|
|
2447
|
+
onStart: (botInfo) => {
|
|
2448
|
+
console.log(`[AFK Code] Telegram bot @${botInfo.username} is running!`);
|
|
2449
|
+
console.log("");
|
|
2450
|
+
console.log("Start a Claude Code session with: afk-code run -- claude");
|
|
2451
|
+
}
|
|
2452
|
+
});
|
|
2453
|
+
}
|
|
2454
|
+
|
|
1900
2455
|
// src/cli/index.ts
|
|
1901
2456
|
var args = process.argv.slice(2);
|
|
1902
2457
|
var command = args[0];
|
|
@@ -1933,27 +2488,39 @@ async function main() {
|
|
|
1933
2488
|
}
|
|
1934
2489
|
break;
|
|
1935
2490
|
}
|
|
2491
|
+
case "telegram": {
|
|
2492
|
+
if (args[1] === "setup") {
|
|
2493
|
+
await telegramSetup();
|
|
2494
|
+
} else {
|
|
2495
|
+
await telegramRun();
|
|
2496
|
+
}
|
|
2497
|
+
break;
|
|
2498
|
+
}
|
|
1936
2499
|
case "help":
|
|
1937
2500
|
case "--help":
|
|
1938
2501
|
case "-h":
|
|
1939
2502
|
case void 0: {
|
|
1940
2503
|
console.log(`
|
|
1941
|
-
AFK Code - Monitor Claude Code sessions from Slack/Discord
|
|
2504
|
+
AFK Code - Monitor Claude Code sessions from Slack/Discord/Telegram
|
|
1942
2505
|
|
|
1943
2506
|
Commands:
|
|
1944
2507
|
slack Run the Slack bot
|
|
1945
2508
|
slack setup Configure Slack integration
|
|
1946
2509
|
discord Run the Discord bot
|
|
1947
2510
|
discord setup Configure Discord integration
|
|
2511
|
+
telegram Run the Telegram bot
|
|
2512
|
+
telegram setup Configure Telegram integration
|
|
1948
2513
|
run -- <command> Start a monitored session
|
|
1949
2514
|
help Show this help message
|
|
1950
2515
|
|
|
1951
2516
|
Examples:
|
|
1952
|
-
afk-code slack setup
|
|
1953
|
-
afk-code slack
|
|
1954
|
-
afk-code discord setup
|
|
1955
|
-
afk-code discord
|
|
1956
|
-
afk-code
|
|
2517
|
+
afk-code slack setup # First-time Slack configuration
|
|
2518
|
+
afk-code slack # Start the Slack bot
|
|
2519
|
+
afk-code discord setup # First-time Discord configuration
|
|
2520
|
+
afk-code discord # Start the Discord bot
|
|
2521
|
+
afk-code telegram setup # First-time Telegram configuration
|
|
2522
|
+
afk-code telegram # Start the Telegram bot
|
|
2523
|
+
afk-code run -- claude # Start a Claude Code session
|
|
1957
2524
|
`);
|
|
1958
2525
|
break;
|
|
1959
2526
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "afk-code",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Monitor and interact with Claude Code sessions from Slack/Discord",
|
|
3
|
+
"version": "0.1.4",
|
|
4
|
+
"description": "Monitor and interact with Claude Code sessions from Slack/Discord/Telegram",
|
|
5
5
|
"author": "Colin Harman",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
"scripts": {
|
|
15
15
|
"build": "tsup",
|
|
16
16
|
"dev": "tsx src/cli/index.ts",
|
|
17
|
-
"prepublishOnly": "npm run build"
|
|
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); }); }\""
|
|
18
19
|
},
|
|
19
20
|
"files": [
|
|
20
21
|
"dist",
|
|
@@ -29,6 +30,7 @@
|
|
|
29
30
|
"claude-code",
|
|
30
31
|
"slack",
|
|
31
32
|
"discord",
|
|
33
|
+
"telegram",
|
|
32
34
|
"bot",
|
|
33
35
|
"ai",
|
|
34
36
|
"cli"
|
|
@@ -37,6 +39,7 @@
|
|
|
37
39
|
"dependencies": {
|
|
38
40
|
"@slack/bolt": "^4.6.0",
|
|
39
41
|
"discord.js": "^14.25.1",
|
|
42
|
+
"grammy": "^1.35.0",
|
|
40
43
|
"node-pty": "^1.0.0"
|
|
41
44
|
},
|
|
42
45
|
"devDependencies": {
|
package/slack-manifest.json
CHANGED
|
@@ -16,9 +16,8 @@
|
|
|
16
16
|
},
|
|
17
17
|
"slash_commands": [
|
|
18
18
|
{
|
|
19
|
-
"command": "/
|
|
19
|
+
"command": "/sessions",
|
|
20
20
|
"description": "List active Claude Code sessions",
|
|
21
|
-
"usage_hint": "[sessions]",
|
|
22
21
|
"should_escape": false
|
|
23
22
|
},
|
|
24
23
|
{
|
|
@@ -35,6 +34,17 @@
|
|
|
35
34
|
"command": "/mode",
|
|
36
35
|
"description": "Toggle Claude Code mode (Shift+Tab)",
|
|
37
36
|
"should_escape": false
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"command": "/compact",
|
|
40
|
+
"description": "Compact the Claude Code conversation",
|
|
41
|
+
"should_escape": false
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"command": "/model",
|
|
45
|
+
"description": "Switch Claude model (opus, sonnet, haiku)",
|
|
46
|
+
"usage_hint": "<model>",
|
|
47
|
+
"should_escape": false
|
|
38
48
|
}
|
|
39
49
|
]
|
|
40
50
|
},
|