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 CHANGED
@@ -1,6 +1,8 @@
1
1
  # AFK Code
2
2
 
3
- Monitor and interact with Claude Code sessions from Slack or Discord. Respond from your phone while AFK.
3
+ Monitor and interact with Claude Code sessions from Slack, Discord, or Telegram. Respond from your phone while AFK.
4
+
5
+ ![square-image](https://github.com/user-attachments/assets/83083b63-9ca2-4ef0-b83d-fcc51bd2fff9)
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
- ### Slack Slash Commands
81
+ ### Slash Commands
58
82
 
59
- - `/afk` - List active sessions
60
- - `/background` - Send Ctrl+B (background signal)
61
- - `/interrupt` - Send Escape (interrupt signal)
62
- - `/mode` - Send Shift+Tab (toggle mode)
63
- - Not recommended since you don't get feedback on what mode you're in
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 discord` starts a bot that listens for sessions
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("/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";
@@ -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 # First-time Slack configuration
1955
- afk-code slack # Start the Slack bot
1956
- afk-code discord setup # First-time Discord configuration
1957
- afk-code discord # Start the Discord bot
1958
- 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
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.3",
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": {
@@ -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
  },