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 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 your Claude Code sessions from Slack or Discord. Get notified when Claude needs input, and respond without leaving your chat app.
3
+ Monitor and interact with Claude Code sessions from Slack, Discord, or Telegram. Respond from your phone while AFK.
4
4
 
5
- ## How it works
5
+ ![square-image](https://github.com/user-attachments/assets/83083b63-9ca2-4ef0-b83d-fcc51bd2fff9)
6
6
 
7
- 1. Run `afk-code slack` or `afk-code discord` to start the bot
8
- 2. Run `afk-code run -- claude` to start a monitored Claude Code session
9
- 3. A new thread (Slack) or channel (Discord) is created for the session
10
- 4. All messages are relayed bidirectionally - respond from your phone while AFK
11
-
12
- ## Installation
7
+ ## Quick Start (Slack)
13
8
 
14
9
  ```bash
15
- # Install globally via npm
16
- npm install -g afk-code
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
- ## Slack Setup
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
- ### 1. Create a Slack App
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
- Go to [api.slack.com/apps](https://api.slack.com/apps) and click **Create New App** **From manifest**.
29
-
30
- Paste the contents of `slack-manifest.json` from this repo, then click **Create**.
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
- - **Bot Token**: OAuth & Permissions Bot User OAuth Token (`xoxb-...`)
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
- ### 4. Configure AFK Code
28
+ ## Quick Start (Discord)
44
29
 
45
30
  ```bash
46
- afk-code slack setup
47
- ```
48
-
49
- Follow the prompts to enter your credentials. Config is saved to `~/.afk-code/slack.env`.
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
- ### 5. Run
38
+ # 2. Get your User ID (enable Developer Mode, right-click your name → Copy User ID)
52
39
 
53
- ```bash
54
- # Terminal 1: Start the Slack bot
55
- afk-code slack
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
- # Terminal 2: Start a Claude Code session
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
- A new thread will appear in your Slack channel for each session.
48
+ ## Quick Start (Telegram)
62
49
 
63
- ## Discord Setup
64
-
65
- ### 1. Create a Discord Application
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
- Go to [discord.com/developers/applications](https://discord.com/developers/applications) and click **New Application**.
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
- ### 2. Create a Bot
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
- - Go to **Bot** in the sidebar
72
- - Click **Reset Token** and copy it
73
- - Enable **Message Content Intent** under Privileged Gateway Intents
64
+ # 4. In another terminal, start a monitored Claude session
65
+ npx afk-code run -- claude
66
+ ```
74
67
 
75
- ### 3. Invite the Bot
68
+ ## Commands
76
69
 
77
- - Go to **OAuth2** → **URL Generator**
78
- - Select scopes: `bot`
79
- - Select permissions: `Send Messages`, `Manage Channels`, `Read Message History`
80
- - Open the generated URL to invite the bot to your server
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
- ### 4. Get Your User ID
81
+ ### Slash Commands
83
82
 
84
- Enable Developer Mode in Discord settings, then right-click your name → **Copy User ID**.
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
- ### 5. Configure AFK Code
93
+ ## Installation Options
87
94
 
88
95
  ```bash
89
- afk-code discord setup
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
- ### 6. Run
99
+ # Or use npx (no install)
100
+ npx afk-code <command>
95
101
 
96
- ```bash
97
- # Terminal 1: Start the Discord bot
98
- afk-code discord
99
-
100
- # Terminal 2: Start a Claude Code session
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
- An "AFK Code Sessions" category will be created with a channel for each session.
109
+ Requires Node.js 18+.
105
110
 
106
- ## Commands
111
+ ## How It Works
107
112
 
108
- ```
109
- afk-code run -- <command> Start a monitored session (e.g., afk-code run -- claude)
110
- afk-code slack Run the Slack bot
111
- afk-code slack setup Configure Slack credentials
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
- ## How It Works
118
+ ## Limitations
118
119
 
119
- AFK Code watches Claude Code's JSONL output files to capture messages in real-time. When you start a session with `afk-code run`, it:
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
- 1. Spawns the command in a PTY (pseudo-terminal)
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
- Messages you send in Slack/Discord threads are forwarded to the terminal as if you typed them.
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("/afk", async ({ command: command2, ack, respond }) => {
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. Available: `/afk sessions`");
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("afk").setDescription("List active Claude Code sessions")
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 === "afk") {
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 # First-time Slack configuration
1953
- afk-code slack # Start the Slack bot
1954
- afk-code discord setup # First-time Discord configuration
1955
- afk-code discord # Start the Discord bot
1956
- afk-code run -- claude # Start a Claude Code session
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.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": {
@@ -16,9 +16,8 @@
16
16
  },
17
17
  "slash_commands": [
18
18
  {
19
- "command": "/afk",
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
  },