afk-code 0.1.3 → 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/README.md +36 -8
- package/dist/cli/index.js +575 -10
- package/package.json +4 -2
- package/slack-manifest.json +12 -2
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# AFK Code
|
|
2
2
|
|
|
3
|
-
Monitor and interact with Claude Code sessions from Slack or
|
|
3
|
+
Monitor and interact with Claude Code sessions from Slack, Discord, or Telegram. Respond from your phone while AFK.
|
|
4
|
+
|
|
5
|
+

|
|
4
6
|
|
|
5
7
|
## Quick Start (Slack)
|
|
6
8
|
|
|
@@ -43,6 +45,26 @@ npx afk-code discord # Start the bot
|
|
|
43
45
|
npx afk-code run -- claude
|
|
44
46
|
```
|
|
45
47
|
|
|
48
|
+
## Quick Start (Telegram)
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# 1. Create a bot with @BotFather on Telegram
|
|
52
|
+
# - Send /newbot and follow the prompts
|
|
53
|
+
# - Copy the bot token
|
|
54
|
+
|
|
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}
|
|
59
|
+
|
|
60
|
+
# 3. Configure and run
|
|
61
|
+
npx afk-code telegram setup # Enter your credentials
|
|
62
|
+
npx afk-code telegram # Start the bot
|
|
63
|
+
|
|
64
|
+
# 4. In another terminal, start a monitored Claude session
|
|
65
|
+
npx afk-code run -- claude
|
|
66
|
+
```
|
|
67
|
+
|
|
46
68
|
## Commands
|
|
47
69
|
|
|
48
70
|
```
|
|
@@ -50,17 +72,23 @@ afk-code slack setup Configure Slack credentials
|
|
|
50
72
|
afk-code slack Run the Slack bot
|
|
51
73
|
afk-code discord setup Configure Discord credentials
|
|
52
74
|
afk-code discord Run the Discord bot
|
|
75
|
+
afk-code telegram setup Configure Telegram credentials
|
|
76
|
+
afk-code telegram Run the Telegram bot
|
|
53
77
|
afk-code run -- <command> Start a monitored session
|
|
54
78
|
afk-code help Show help
|
|
55
79
|
```
|
|
56
80
|
|
|
57
|
-
###
|
|
81
|
+
### Slash Commands
|
|
58
82
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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) |
|
|
64
92
|
|
|
65
93
|
## Installation Options
|
|
66
94
|
|
|
@@ -82,7 +110,7 @@ Requires Node.js 18+.
|
|
|
82
110
|
|
|
83
111
|
## How It Works
|
|
84
112
|
|
|
85
|
-
1. `afk-code slack` or `afk-code
|
|
113
|
+
1. `afk-code slack`, `afk-code discord`, or `afk-code telegram` starts a bot that listens for sessions
|
|
86
114
|
2. `afk-code run -- claude` spawns Claude in a PTY and connects to the bot via Unix socket
|
|
87
115
|
3. The bot watches Claude's JSONL files for messages and relays them to chat
|
|
88
116
|
4. Messages you send in chat are forwarded to the terminal
|
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";
|
|
@@ -1899,6 +2322,136 @@ async function discordRun() {
|
|
|
1899
2322
|
}
|
|
1900
2323
|
}
|
|
1901
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
|
+
|
|
1902
2455
|
// src/cli/index.ts
|
|
1903
2456
|
var args = process.argv.slice(2);
|
|
1904
2457
|
var command = args[0];
|
|
@@ -1935,27 +2488,39 @@ async function main() {
|
|
|
1935
2488
|
}
|
|
1936
2489
|
break;
|
|
1937
2490
|
}
|
|
2491
|
+
case "telegram": {
|
|
2492
|
+
if (args[1] === "setup") {
|
|
2493
|
+
await telegramSetup();
|
|
2494
|
+
} else {
|
|
2495
|
+
await telegramRun();
|
|
2496
|
+
}
|
|
2497
|
+
break;
|
|
2498
|
+
}
|
|
1938
2499
|
case "help":
|
|
1939
2500
|
case "--help":
|
|
1940
2501
|
case "-h":
|
|
1941
2502
|
case void 0: {
|
|
1942
2503
|
console.log(`
|
|
1943
|
-
AFK Code - Monitor Claude Code sessions from Slack/Discord
|
|
2504
|
+
AFK Code - Monitor Claude Code sessions from Slack/Discord/Telegram
|
|
1944
2505
|
|
|
1945
2506
|
Commands:
|
|
1946
2507
|
slack Run the Slack bot
|
|
1947
2508
|
slack setup Configure Slack integration
|
|
1948
2509
|
discord Run the Discord bot
|
|
1949
2510
|
discord setup Configure Discord integration
|
|
2511
|
+
telegram Run the Telegram bot
|
|
2512
|
+
telegram setup Configure Telegram integration
|
|
1950
2513
|
run -- <command> Start a monitored session
|
|
1951
2514
|
help Show this help message
|
|
1952
2515
|
|
|
1953
2516
|
Examples:
|
|
1954
|
-
afk-code slack setup
|
|
1955
|
-
afk-code slack
|
|
1956
|
-
afk-code discord setup
|
|
1957
|
-
afk-code discord
|
|
1958
|
-
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
|
|
1959
2524
|
`);
|
|
1960
2525
|
break;
|
|
1961
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",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"claude-code",
|
|
31
31
|
"slack",
|
|
32
32
|
"discord",
|
|
33
|
+
"telegram",
|
|
33
34
|
"bot",
|
|
34
35
|
"ai",
|
|
35
36
|
"cli"
|
|
@@ -38,6 +39,7 @@
|
|
|
38
39
|
"dependencies": {
|
|
39
40
|
"@slack/bolt": "^4.6.0",
|
|
40
41
|
"discord.js": "^14.25.1",
|
|
42
|
+
"grammy": "^1.35.0",
|
|
41
43
|
"node-pty": "^1.0.0"
|
|
42
44
|
},
|
|
43
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
|
},
|