afk-code 0.1.3 → 0.2.0
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 +64 -26
- package/dist/cli/index.js +586 -16
- package/package.json +5 -3
- package/slack-manifest.json +12 -2
package/README.md
CHANGED
|
@@ -1,28 +1,39 @@
|
|
|
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
4
|
|
|
5
|
-
|
|
5
|
+
<img src="https://github.com/user-attachments/assets/83083b63-9ca2-4ef0-b83d-fcc51bd2fff9" alt="AFK Code iPhone Slack screenshot" width="400">
|
|
6
|
+
|
|
7
|
+
## Client Comparison
|
|
8
|
+
|
|
9
|
+
Telegram and Discord are recommended.
|
|
10
|
+
|
|
11
|
+
| | Telegram | Discord | Slack |
|
|
12
|
+
|---|---|---|---|
|
|
13
|
+
| Siri integration | Receive & Send | Receive only | Receive only |
|
|
14
|
+
| Multi-session support | One at a time (switchable) | Yes | Yes |
|
|
15
|
+
| Permissions required | Personal | Personal | Admin |
|
|
16
|
+
|
|
17
|
+
## Quick Start (Telegram)
|
|
6
18
|
|
|
7
19
|
```bash
|
|
8
|
-
# 1. Create a
|
|
9
|
-
#
|
|
20
|
+
# 1. Create a bot with @BotFather on Telegram
|
|
21
|
+
# - Send /newbot and follow the prompts
|
|
22
|
+
# - Copy the bot token
|
|
10
23
|
|
|
11
|
-
# 2.
|
|
12
|
-
# -
|
|
13
|
-
# -
|
|
14
|
-
# -
|
|
24
|
+
# 2. Get your Chat ID
|
|
25
|
+
# - Message your bot, then visit:
|
|
26
|
+
# - https://api.telegram.org/bot<TOKEN>/getUpdates
|
|
27
|
+
# - Find "chat":{"id":YOUR_CHAT_ID}
|
|
15
28
|
|
|
16
29
|
# 3. Configure and run
|
|
17
|
-
npx afk-code
|
|
18
|
-
npx afk-code
|
|
30
|
+
npx afk-code telegram setup # Enter your credentials
|
|
31
|
+
npx afk-code telegram # Start the bot
|
|
19
32
|
|
|
20
33
|
# 4. In another terminal, start a monitored Claude session
|
|
21
|
-
npx afk-code
|
|
34
|
+
npx afk-code claude
|
|
22
35
|
```
|
|
23
36
|
|
|
24
|
-
A new channel is created for each session. Messages relay bidirectionally.
|
|
25
|
-
|
|
26
37
|
## Quick Start (Discord)
|
|
27
38
|
|
|
28
39
|
```bash
|
|
@@ -40,27 +51,54 @@ npx afk-code discord setup # Enter your credentials
|
|
|
40
51
|
npx afk-code discord # Start the bot
|
|
41
52
|
|
|
42
53
|
# 4. In another terminal, start a monitored Claude session
|
|
43
|
-
npx afk-code
|
|
54
|
+
npx afk-code claude
|
|
44
55
|
```
|
|
45
56
|
|
|
57
|
+
## Quick Start (Slack)
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# 1. Create a Slack app at https://api.slack.com/apps
|
|
61
|
+
# Click "Create New App" → "From manifest" → paste slack-manifest.json
|
|
62
|
+
|
|
63
|
+
# 2. Install to your workspace and get credentials:
|
|
64
|
+
# - Bot Token (xoxb-...) from OAuth & Permissions
|
|
65
|
+
# - App Token (xapp-...) from Basic Information → App-Level Tokens (needs connections:write)
|
|
66
|
+
# - Your User ID from your Slack profile → "..." → Copy member ID
|
|
67
|
+
|
|
68
|
+
# 3. Configure and run
|
|
69
|
+
npx afk-code slack setup # Enter your credentials
|
|
70
|
+
npx afk-code slack # Start the bot
|
|
71
|
+
|
|
72
|
+
# 4. In another terminal, start a monitored Claude session
|
|
73
|
+
npx afk-code claude
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
A new channel is created for each session. Messages relay bidirectionally.
|
|
77
|
+
|
|
46
78
|
## Commands
|
|
47
79
|
|
|
48
80
|
```
|
|
49
|
-
afk-code
|
|
50
|
-
afk-code
|
|
81
|
+
afk-code telegram setup Configure Telegram credentials
|
|
82
|
+
afk-code telegram Run the Telegram bot
|
|
51
83
|
afk-code discord setup Configure Discord credentials
|
|
52
84
|
afk-code discord Run the Discord bot
|
|
53
|
-
afk-code
|
|
85
|
+
afk-code slack setup Configure Slack credentials
|
|
86
|
+
afk-code slack Run the Slack bot
|
|
87
|
+
afk-code <command> [args] Start a monitored session
|
|
54
88
|
afk-code help Show help
|
|
55
89
|
```
|
|
56
90
|
|
|
57
|
-
###
|
|
91
|
+
### Slash Commands
|
|
58
92
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
93
|
+
| Command | Slack | Discord | Telegram | Description |
|
|
94
|
+
|---------|:-----:|:-------:|:--------:|-------------|
|
|
95
|
+
| `/sessions` | ✓ | ✓ | ✓ | List active sessions |
|
|
96
|
+
| `/switch <name>` | - | - | ✓ | Switch session (Telegram only) |
|
|
97
|
+
| `/model <name>` | ✓ | ✓ | ✓ | Switch model (opus, sonnet, haiku) |
|
|
98
|
+
| `/compact` | ✓ | ✓ | ✓ | Compact the conversation |
|
|
99
|
+
| `/background` | ✓ | ✓ | ✓ | Send Ctrl+B (background mode) |
|
|
100
|
+
| `/interrupt` | ✓ | ✓ | ✓ | Send Escape (interrupt) |
|
|
101
|
+
| `/mode` | ✓ | ✓ | ✓ | Toggle mode (Shift+Tab) |
|
|
64
102
|
|
|
65
103
|
## Installation Options
|
|
66
104
|
|
|
@@ -75,15 +113,15 @@ npx afk-code <command>
|
|
|
75
113
|
git clone https://github.com/clharman/afk-code.git
|
|
76
114
|
cd afk-code && npm install
|
|
77
115
|
npm run dev -- slack
|
|
78
|
-
npm run dev --
|
|
116
|
+
npm run dev -- claude
|
|
79
117
|
```
|
|
80
118
|
|
|
81
119
|
Requires Node.js 18+.
|
|
82
120
|
|
|
83
121
|
## How It Works
|
|
84
122
|
|
|
85
|
-
1. `afk-code slack` or `afk-code
|
|
86
|
-
2. `afk-code
|
|
123
|
+
1. `afk-code slack`, `afk-code discord`, or `afk-code telegram` starts a bot that listens for sessions
|
|
124
|
+
2. `afk-code claude` spawns Claude in a PTY and connects to the bot via Unix socket
|
|
87
125
|
3. The bot watches Claude's JSONL files for messages and relays them to chat
|
|
88
126
|
4. Messages you send in chat are forwarded to the terminal
|
|
89
127
|
|
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,351 @@ 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", options) {
|
|
1601
|
+
messageQueue.push(async () => {
|
|
1602
|
+
try {
|
|
1603
|
+
await bot.api.sendMessage(config.chatId, text, {
|
|
1604
|
+
parse_mode: parseMode,
|
|
1605
|
+
disable_notification: options?.disable_notification
|
|
1606
|
+
});
|
|
1607
|
+
} catch (err) {
|
|
1608
|
+
if (parseMode && err.message?.includes("parse")) {
|
|
1609
|
+
await bot.api.sendMessage(config.chatId, text, {
|
|
1610
|
+
disable_notification: options?.disable_notification
|
|
1611
|
+
});
|
|
1612
|
+
} else {
|
|
1613
|
+
throw err;
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
});
|
|
1617
|
+
processQueue();
|
|
1618
|
+
}
|
|
1619
|
+
async function sendChunkedMessage(text, prefix, options) {
|
|
1620
|
+
const chunks = chunkMessage(text, MAX_MESSAGE_LENGTH);
|
|
1621
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
1622
|
+
const chunk = prefix && i === 0 ? `${prefix} ${chunks[i]}` : chunks[i];
|
|
1623
|
+
await sendMessage(chunk, "Markdown", options);
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
const sessionManager = new SessionManager({
|
|
1627
|
+
onSessionStart: async (session) => {
|
|
1628
|
+
activeSessions.set(session.id, {
|
|
1629
|
+
sessionId: session.id,
|
|
1630
|
+
sessionName: session.name,
|
|
1631
|
+
lastActivity: /* @__PURE__ */ new Date()
|
|
1632
|
+
});
|
|
1633
|
+
const parts = session.name.split(" ");
|
|
1634
|
+
const cmd = parts[0];
|
|
1635
|
+
const args2 = parts.slice(1).map((a) => a.replace(/^-+/, ""));
|
|
1636
|
+
const sessionLabel = args2.length > 0 ? `${cmd} (${args2.join(", ")})` : cmd;
|
|
1637
|
+
await sendMessage(
|
|
1638
|
+
`Session started: ${sessionLabel}
|
|
1639
|
+
Directory: \`${session.cwd}\``
|
|
1640
|
+
);
|
|
1641
|
+
},
|
|
1642
|
+
onSessionEnd: async (sessionId) => {
|
|
1643
|
+
const tracking = activeSessions.get(sessionId);
|
|
1644
|
+
const name = tracking?.sessionName || sessionId;
|
|
1645
|
+
activeSessions.delete(sessionId);
|
|
1646
|
+
await sendMessage(`Session ended: ${name}`);
|
|
1647
|
+
},
|
|
1648
|
+
onSessionUpdate: async (sessionId, name) => {
|
|
1649
|
+
const tracking = activeSessions.get(sessionId);
|
|
1650
|
+
if (tracking) {
|
|
1651
|
+
tracking.sessionName = name;
|
|
1652
|
+
tracking.lastActivity = /* @__PURE__ */ new Date();
|
|
1653
|
+
}
|
|
1654
|
+
},
|
|
1655
|
+
onSessionStatus: async (sessionId, _status) => {
|
|
1656
|
+
const tracking = activeSessions.get(sessionId);
|
|
1657
|
+
if (tracking) {
|
|
1658
|
+
tracking.lastActivity = /* @__PURE__ */ new Date();
|
|
1659
|
+
}
|
|
1660
|
+
},
|
|
1661
|
+
onMessage: async (sessionId, role, content) => {
|
|
1662
|
+
const tracking = activeSessions.get(sessionId);
|
|
1663
|
+
if (!tracking) return;
|
|
1664
|
+
tracking.lastActivity = /* @__PURE__ */ new Date();
|
|
1665
|
+
if (role === "user") {
|
|
1666
|
+
const contentKey = content.trim();
|
|
1667
|
+
if (telegramSentMessages.has(contentKey)) {
|
|
1668
|
+
telegramSentMessages.delete(contentKey);
|
|
1669
|
+
return;
|
|
1670
|
+
}
|
|
1671
|
+
await sendChunkedMessage(content, `_User (terminal):_`, { disable_notification: true });
|
|
1672
|
+
} else {
|
|
1673
|
+
await sendChunkedMessage(content, `_Claude Code:_`);
|
|
1674
|
+
}
|
|
1675
|
+
},
|
|
1676
|
+
onTodos: async (sessionId, todos) => {
|
|
1677
|
+
const tracking = activeSessions.get(sessionId);
|
|
1678
|
+
if (!tracking || todos.length === 0) return;
|
|
1679
|
+
const todosText = formatTodos(todos);
|
|
1680
|
+
await sendMessage(`_Claude Code:_ *Tasks:*
|
|
1681
|
+
${todosText}`);
|
|
1682
|
+
},
|
|
1683
|
+
onToolCall: async (_sessionId, _tool) => {
|
|
1684
|
+
},
|
|
1685
|
+
onToolResult: async (_sessionId, _result) => {
|
|
1686
|
+
},
|
|
1687
|
+
onPlanModeChange: async (sessionId, inPlanMode) => {
|
|
1688
|
+
const tracking = activeSessions.get(sessionId);
|
|
1689
|
+
if (!tracking) return;
|
|
1690
|
+
const status = inPlanMode ? "Planning mode - Claude is designing a solution" : "Execution mode - Claude is implementing";
|
|
1691
|
+
await sendMessage(`_Claude Code:_ ${status}`);
|
|
1692
|
+
}
|
|
1693
|
+
});
|
|
1694
|
+
function getCurrentSession() {
|
|
1695
|
+
if (currentSessionId) {
|
|
1696
|
+
const session = activeSessions.get(currentSessionId);
|
|
1697
|
+
if (session) return session;
|
|
1698
|
+
currentSessionId = null;
|
|
1699
|
+
}
|
|
1700
|
+
if (activeSessions.size === 1) {
|
|
1701
|
+
return activeSessions.values().next().value;
|
|
1702
|
+
}
|
|
1703
|
+
return null;
|
|
1704
|
+
}
|
|
1705
|
+
function getSessionByName(name) {
|
|
1706
|
+
const nameLower = name.toLowerCase();
|
|
1707
|
+
for (const tracking of activeSessions.values()) {
|
|
1708
|
+
if (tracking.sessionName.toLowerCase().startsWith(nameLower)) {
|
|
1709
|
+
return tracking;
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
return null;
|
|
1713
|
+
}
|
|
1714
|
+
function getMostRecentSession() {
|
|
1715
|
+
let mostRecent = null;
|
|
1716
|
+
for (const tracking of activeSessions.values()) {
|
|
1717
|
+
if (!mostRecent || tracking.lastActivity > mostRecent.lastActivity) {
|
|
1718
|
+
mostRecent = tracking;
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
return mostRecent;
|
|
1722
|
+
}
|
|
1723
|
+
bot.on("message:text", async (ctx) => {
|
|
1724
|
+
if (ctx.chat.id.toString() !== config.chatId) return;
|
|
1725
|
+
const text = ctx.message.text;
|
|
1726
|
+
if (text.startsWith("/")) {
|
|
1727
|
+
await handleCommand(ctx, text.trim());
|
|
1728
|
+
return;
|
|
1729
|
+
}
|
|
1730
|
+
const current = getCurrentSession();
|
|
1731
|
+
if (!current) {
|
|
1732
|
+
if (activeSessions.size === 0) {
|
|
1733
|
+
await ctx.reply("No active sessions. Start one with:\n`afk-code run -- claude`", { parse_mode: "Markdown" });
|
|
1734
|
+
} else {
|
|
1735
|
+
const list = Array.from(activeSessions.values()).map((s) => `\u2022 \`${s.sessionName}\``).join("\n");
|
|
1736
|
+
await ctx.reply(
|
|
1737
|
+
`Multiple sessions active. Select one first:
|
|
1738
|
+
|
|
1739
|
+
${list}
|
|
1740
|
+
|
|
1741
|
+
Use: \`/switch <name>\``,
|
|
1742
|
+
{ parse_mode: "Markdown" }
|
|
1743
|
+
);
|
|
1744
|
+
}
|
|
1745
|
+
return;
|
|
1746
|
+
}
|
|
1747
|
+
telegramSentMessages.add(text.trim());
|
|
1748
|
+
const sent = sessionManager.sendInput(current.sessionId, text);
|
|
1749
|
+
if (!sent) {
|
|
1750
|
+
telegramSentMessages.delete(text.trim());
|
|
1751
|
+
await ctx.reply("Failed to send input - session not connected.");
|
|
1752
|
+
}
|
|
1753
|
+
});
|
|
1754
|
+
async function handleCommand(ctx, text) {
|
|
1755
|
+
const [command2, ...args2] = text.split(" ");
|
|
1756
|
+
const sessionArg = args2[0];
|
|
1757
|
+
let targetSession = null;
|
|
1758
|
+
if (sessionArg && !sessionArg.startsWith("/")) {
|
|
1759
|
+
for (const tracking of activeSessions.values()) {
|
|
1760
|
+
if (tracking.sessionName.toLowerCase().startsWith(sessionArg.toLowerCase())) {
|
|
1761
|
+
targetSession = tracking;
|
|
1762
|
+
break;
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
if (!targetSession) {
|
|
1767
|
+
targetSession = getMostRecentSession();
|
|
1768
|
+
}
|
|
1769
|
+
switch (command2.toLowerCase()) {
|
|
1770
|
+
case "/start": {
|
|
1771
|
+
await ctx.reply(
|
|
1772
|
+
`*AFK Code Telegram Bot*
|
|
1773
|
+
|
|
1774
|
+
This bot lets you monitor and interact with Claude Code sessions.
|
|
1775
|
+
|
|
1776
|
+
Start a session with:
|
|
1777
|
+
\`afk-code run -- claude\`
|
|
1778
|
+
|
|
1779
|
+
Type /help for available commands.`,
|
|
1780
|
+
{ parse_mode: "Markdown" }
|
|
1781
|
+
);
|
|
1782
|
+
break;
|
|
1783
|
+
}
|
|
1784
|
+
case "/sessions": {
|
|
1785
|
+
if (activeSessions.size === 0) {
|
|
1786
|
+
await ctx.reply("No active sessions. Start one with `afk-code run -- claude`");
|
|
1787
|
+
return;
|
|
1788
|
+
}
|
|
1789
|
+
const current = getCurrentSession();
|
|
1790
|
+
const list = Array.from(activeSessions.values()).map((s) => {
|
|
1791
|
+
const isCurrent = current && s.sessionId === current.sessionId;
|
|
1792
|
+
return isCurrent ? `\u2022 *${s.sessionName}* \u2190 current` : `\u2022 ${s.sessionName}`;
|
|
1793
|
+
}).join("\n");
|
|
1794
|
+
await ctx.reply(`*Active Sessions:*
|
|
1795
|
+
${list}
|
|
1796
|
+
|
|
1797
|
+
Use \`/switch <name>\` to change`, { parse_mode: "Markdown" });
|
|
1798
|
+
break;
|
|
1799
|
+
}
|
|
1800
|
+
case "/switch":
|
|
1801
|
+
case "/select": {
|
|
1802
|
+
if (!sessionArg) {
|
|
1803
|
+
if (activeSessions.size === 0) {
|
|
1804
|
+
await ctx.reply("No active sessions.");
|
|
1805
|
+
return;
|
|
1806
|
+
}
|
|
1807
|
+
const current = getCurrentSession();
|
|
1808
|
+
const list = Array.from(activeSessions.values()).map((s) => {
|
|
1809
|
+
const isCurrent = current && s.sessionId === current.sessionId;
|
|
1810
|
+
return isCurrent ? `\u2022 *${s.sessionName}* \u2190 current` : `\u2022 ${s.sessionName}`;
|
|
1811
|
+
}).join("\n");
|
|
1812
|
+
await ctx.reply(`*Sessions:*
|
|
1813
|
+
${list}
|
|
1814
|
+
|
|
1815
|
+
Use: \`/switch <name>\``, { parse_mode: "Markdown" });
|
|
1816
|
+
return;
|
|
1817
|
+
}
|
|
1818
|
+
const session = getSessionByName(sessionArg);
|
|
1819
|
+
if (session) {
|
|
1820
|
+
currentSessionId = session.sessionId;
|
|
1821
|
+
await ctx.reply(`Switched to: *${session.sessionName}*`, { parse_mode: "Markdown" });
|
|
1822
|
+
} else {
|
|
1823
|
+
await ctx.reply(`Session not found: ${sessionArg}`);
|
|
1824
|
+
}
|
|
1825
|
+
break;
|
|
1826
|
+
}
|
|
1827
|
+
case "/background":
|
|
1828
|
+
case "/bg": {
|
|
1829
|
+
if (!targetSession) {
|
|
1830
|
+
await ctx.reply("No active session.");
|
|
1831
|
+
return;
|
|
1832
|
+
}
|
|
1833
|
+
const sent = sessionManager.sendInput(targetSession.sessionId, "");
|
|
1834
|
+
await ctx.reply(sent ? "Sent background command (Ctrl+B)" : "Failed - session not connected.");
|
|
1835
|
+
break;
|
|
1836
|
+
}
|
|
1837
|
+
case "/interrupt":
|
|
1838
|
+
case "/stop": {
|
|
1839
|
+
if (!targetSession) {
|
|
1840
|
+
await ctx.reply("No active session.");
|
|
1841
|
+
return;
|
|
1842
|
+
}
|
|
1843
|
+
const sent = sessionManager.sendInput(targetSession.sessionId, "\x1B");
|
|
1844
|
+
await ctx.reply(sent ? "Sent interrupt (Escape)" : "Failed - session not connected.");
|
|
1845
|
+
break;
|
|
1846
|
+
}
|
|
1847
|
+
case "/mode": {
|
|
1848
|
+
if (!targetSession) {
|
|
1849
|
+
await ctx.reply("No active session.");
|
|
1850
|
+
return;
|
|
1851
|
+
}
|
|
1852
|
+
const sent = sessionManager.sendInput(targetSession.sessionId, "\x1B[Z");
|
|
1853
|
+
await ctx.reply(sent ? "Sent mode toggle (Shift+Tab)" : "Failed - session not connected.");
|
|
1854
|
+
break;
|
|
1855
|
+
}
|
|
1856
|
+
case "/compact": {
|
|
1857
|
+
if (!targetSession) {
|
|
1858
|
+
await ctx.reply("No active session.");
|
|
1859
|
+
return;
|
|
1860
|
+
}
|
|
1861
|
+
const sent = sessionManager.sendInput(targetSession.sessionId, "/compact\n");
|
|
1862
|
+
await ctx.reply(sent ? "Sent /compact" : "Failed - session not connected.");
|
|
1863
|
+
break;
|
|
1864
|
+
}
|
|
1865
|
+
case "/model": {
|
|
1866
|
+
if (!targetSession) {
|
|
1867
|
+
await ctx.reply("No active session.");
|
|
1868
|
+
return;
|
|
1869
|
+
}
|
|
1870
|
+
const modelArg = args2.slice(targetSession === getSessionByName(args2[0] || "") ? 1 : 0).join(" ");
|
|
1871
|
+
if (!modelArg) {
|
|
1872
|
+
await ctx.reply("Usage: `/model <opus|sonnet|haiku>`", { parse_mode: "Markdown" });
|
|
1873
|
+
return;
|
|
1874
|
+
}
|
|
1875
|
+
const sent = sessionManager.sendInput(targetSession.sessionId, `/model ${modelArg}
|
|
1876
|
+
`);
|
|
1877
|
+
await ctx.reply(sent ? `Sent /model ${modelArg}` : "Failed - session not connected.");
|
|
1878
|
+
break;
|
|
1879
|
+
}
|
|
1880
|
+
case "/help": {
|
|
1881
|
+
await ctx.reply(
|
|
1882
|
+
`*AFK Code Commands:*
|
|
1883
|
+
|
|
1884
|
+
/sessions - List active sessions
|
|
1885
|
+
/switch <name> - Switch to a session
|
|
1886
|
+
/model <name> - Switch model
|
|
1887
|
+
/compact - Compact conversation
|
|
1888
|
+
/background - Send Ctrl+B
|
|
1889
|
+
/interrupt - Send Escape
|
|
1890
|
+
/mode - Toggle mode (Shift+Tab)
|
|
1891
|
+
/help - Show this message
|
|
1892
|
+
|
|
1893
|
+
_Messages go to the current session (auto-selected if only one)._`,
|
|
1894
|
+
{ parse_mode: "Markdown" }
|
|
1895
|
+
);
|
|
1896
|
+
break;
|
|
1897
|
+
}
|
|
1898
|
+
default:
|
|
1899
|
+
break;
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
return { bot, sessionManager };
|
|
1903
|
+
}
|
|
1904
|
+
var MAX_MESSAGE_LENGTH;
|
|
1905
|
+
var init_telegram_app = __esm({
|
|
1906
|
+
"src/telegram/telegram-app.ts"() {
|
|
1907
|
+
"use strict";
|
|
1908
|
+
init_session_manager();
|
|
1909
|
+
init_message_formatter();
|
|
1910
|
+
MAX_MESSAGE_LENGTH = 4e3;
|
|
1911
|
+
}
|
|
1912
|
+
});
|
|
1913
|
+
|
|
1485
1914
|
// src/cli/run.ts
|
|
1486
1915
|
import { randomUUID } from "crypto";
|
|
1487
1916
|
import { homedir } from "os";
|
|
@@ -1899,6 +2328,136 @@ async function discordRun() {
|
|
|
1899
2328
|
}
|
|
1900
2329
|
}
|
|
1901
2330
|
|
|
2331
|
+
// src/cli/telegram.ts
|
|
2332
|
+
import { homedir as homedir5 } from "os";
|
|
2333
|
+
import { mkdir as mkdir3, writeFile as writeFile3, readFile as readFile4, access as access3 } from "fs/promises";
|
|
2334
|
+
import * as readline3 from "readline";
|
|
2335
|
+
var CONFIG_DIR3 = `${homedir5()}/.afk-code`;
|
|
2336
|
+
var TELEGRAM_CONFIG_FILE = `${CONFIG_DIR3}/telegram.env`;
|
|
2337
|
+
function prompt3(question) {
|
|
2338
|
+
const rl = readline3.createInterface({
|
|
2339
|
+
input: process.stdin,
|
|
2340
|
+
output: process.stdout
|
|
2341
|
+
});
|
|
2342
|
+
return new Promise((resolve2) => {
|
|
2343
|
+
rl.question(question, (answer) => {
|
|
2344
|
+
rl.close();
|
|
2345
|
+
resolve2(answer.trim());
|
|
2346
|
+
});
|
|
2347
|
+
});
|
|
2348
|
+
}
|
|
2349
|
+
async function fileExists3(path) {
|
|
2350
|
+
try {
|
|
2351
|
+
await access3(path);
|
|
2352
|
+
return true;
|
|
2353
|
+
} catch {
|
|
2354
|
+
return false;
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
async function telegramSetup() {
|
|
2358
|
+
console.log(`
|
|
2359
|
+
\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
|
|
2360
|
+
\u2502 AFK Code Telegram Setup \u2502
|
|
2361
|
+
\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
|
|
2362
|
+
|
|
2363
|
+
This will configure a Telegram bot for monitoring Claude Code sessions.
|
|
2364
|
+
|
|
2365
|
+
Step 1: Create a Telegram Bot
|
|
2366
|
+
\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2367
|
+
1. Open Telegram and search for @BotFather
|
|
2368
|
+
2. Send /newbot and follow the prompts
|
|
2369
|
+
3. Choose a name (e.g., "AFK Code")
|
|
2370
|
+
4. Choose a username (e.g., "my_afk_code_bot")
|
|
2371
|
+
5. Copy the bot token BotFather gives you
|
|
2372
|
+
`);
|
|
2373
|
+
const botToken = await prompt3("Bot Token: ");
|
|
2374
|
+
if (!botToken || !botToken.includes(":")) {
|
|
2375
|
+
console.error("Invalid bot token. It should look like: 123456789:ABCdefGHIjklMNOpqrsTUVwxyz");
|
|
2376
|
+
process.exit(1);
|
|
2377
|
+
}
|
|
2378
|
+
console.log(`
|
|
2379
|
+
Step 2: Get Your Chat ID
|
|
2380
|
+
\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2381
|
+
1. Start a chat with your new bot in Telegram
|
|
2382
|
+
2. Send it any message (e.g., "hello")
|
|
2383
|
+
3. Visit this URL in your browser:
|
|
2384
|
+
https://api.telegram.org/bot${botToken}/getUpdates
|
|
2385
|
+
4. Find "chat":{"id":YOUR_CHAT_ID} in the response
|
|
2386
|
+
5. Copy the numeric chat ID
|
|
2387
|
+
`);
|
|
2388
|
+
const chatId = await prompt3("Chat ID: ");
|
|
2389
|
+
if (!chatId || !/^-?\d+$/.test(chatId)) {
|
|
2390
|
+
console.error("Invalid chat ID. It should be a number (can be negative for groups).");
|
|
2391
|
+
process.exit(1);
|
|
2392
|
+
}
|
|
2393
|
+
await mkdir3(CONFIG_DIR3, { recursive: true });
|
|
2394
|
+
const envContent = `# AFK Code Telegram Configuration
|
|
2395
|
+
TELEGRAM_BOT_TOKEN=${botToken}
|
|
2396
|
+
TELEGRAM_CHAT_ID=${chatId}
|
|
2397
|
+
`;
|
|
2398
|
+
await writeFile3(TELEGRAM_CONFIG_FILE, envContent);
|
|
2399
|
+
console.log(`
|
|
2400
|
+
Configuration saved to ${TELEGRAM_CONFIG_FILE}
|
|
2401
|
+
|
|
2402
|
+
To start the Telegram bot, run:
|
|
2403
|
+
afk-code telegram
|
|
2404
|
+
|
|
2405
|
+
Then start a Claude Code session with:
|
|
2406
|
+
afk-code run -- claude
|
|
2407
|
+
|
|
2408
|
+
Your bot will send session updates to your Telegram chat!
|
|
2409
|
+
`);
|
|
2410
|
+
}
|
|
2411
|
+
async function loadEnvFile3(path) {
|
|
2412
|
+
if (!await fileExists3(path)) return {};
|
|
2413
|
+
const content = await readFile4(path, "utf-8");
|
|
2414
|
+
const config = {};
|
|
2415
|
+
for (const line of content.split("\n")) {
|
|
2416
|
+
if (line.startsWith("#") || !line.includes("=")) continue;
|
|
2417
|
+
const [key, ...valueParts] = line.split("=");
|
|
2418
|
+
config[key.trim()] = valueParts.join("=").trim();
|
|
2419
|
+
}
|
|
2420
|
+
return config;
|
|
2421
|
+
}
|
|
2422
|
+
async function telegramRun() {
|
|
2423
|
+
const globalConfig = await loadEnvFile3(TELEGRAM_CONFIG_FILE);
|
|
2424
|
+
const localConfig = await loadEnvFile3(`${process.cwd()}/.env`);
|
|
2425
|
+
const config = {
|
|
2426
|
+
...globalConfig,
|
|
2427
|
+
...localConfig
|
|
2428
|
+
};
|
|
2429
|
+
if (process.env.TELEGRAM_BOT_TOKEN) config.TELEGRAM_BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN;
|
|
2430
|
+
if (process.env.TELEGRAM_CHAT_ID) config.TELEGRAM_CHAT_ID = process.env.TELEGRAM_CHAT_ID;
|
|
2431
|
+
const required = ["TELEGRAM_BOT_TOKEN", "TELEGRAM_CHAT_ID"];
|
|
2432
|
+
const missing = required.filter((key) => !config[key]);
|
|
2433
|
+
if (missing.length > 0) {
|
|
2434
|
+
console.error(`Missing config: ${missing.join(", ")}`);
|
|
2435
|
+
console.error("");
|
|
2436
|
+
console.error('Run "afk-code telegram setup" for guided configuration.');
|
|
2437
|
+
process.exit(1);
|
|
2438
|
+
}
|
|
2439
|
+
console.log("[AFK Code] Starting Telegram bot...");
|
|
2440
|
+
const { createTelegramApp: createTelegramApp2 } = await Promise.resolve().then(() => (init_telegram_app(), telegram_app_exports));
|
|
2441
|
+
const telegramConfig = {
|
|
2442
|
+
botToken: config.TELEGRAM_BOT_TOKEN,
|
|
2443
|
+
chatId: config.TELEGRAM_CHAT_ID
|
|
2444
|
+
};
|
|
2445
|
+
const { bot, sessionManager } = createTelegramApp2(telegramConfig);
|
|
2446
|
+
try {
|
|
2447
|
+
await sessionManager.start();
|
|
2448
|
+
} catch (err) {
|
|
2449
|
+
console.error("[AFK Code] Failed to start session manager:", err);
|
|
2450
|
+
process.exit(1);
|
|
2451
|
+
}
|
|
2452
|
+
bot.start({
|
|
2453
|
+
onStart: (botInfo) => {
|
|
2454
|
+
console.log(`[AFK Code] Telegram bot @${botInfo.username} is running!`);
|
|
2455
|
+
console.log("");
|
|
2456
|
+
console.log("Start a Claude Code session with: afk-code run -- claude");
|
|
2457
|
+
}
|
|
2458
|
+
});
|
|
2459
|
+
}
|
|
2460
|
+
|
|
1902
2461
|
// src/cli/index.ts
|
|
1903
2462
|
var args = process.argv.slice(2);
|
|
1904
2463
|
var command = args[0];
|
|
@@ -1935,34 +2494,45 @@ async function main() {
|
|
|
1935
2494
|
}
|
|
1936
2495
|
break;
|
|
1937
2496
|
}
|
|
2497
|
+
case "telegram": {
|
|
2498
|
+
if (args[1] === "setup") {
|
|
2499
|
+
await telegramSetup();
|
|
2500
|
+
} else {
|
|
2501
|
+
await telegramRun();
|
|
2502
|
+
}
|
|
2503
|
+
break;
|
|
2504
|
+
}
|
|
1938
2505
|
case "help":
|
|
1939
2506
|
case "--help":
|
|
1940
2507
|
case "-h":
|
|
1941
2508
|
case void 0: {
|
|
1942
2509
|
console.log(`
|
|
1943
|
-
AFK Code - Monitor Claude Code sessions from Slack/Discord
|
|
2510
|
+
AFK Code - Monitor Claude Code sessions from Slack/Discord/Telegram
|
|
1944
2511
|
|
|
1945
2512
|
Commands:
|
|
1946
|
-
|
|
1947
|
-
|
|
2513
|
+
telegram Run the Telegram bot
|
|
2514
|
+
telegram setup Configure Telegram integration
|
|
1948
2515
|
discord Run the Discord bot
|
|
1949
2516
|
discord setup Configure Discord integration
|
|
1950
|
-
|
|
2517
|
+
slack Run the Slack bot
|
|
2518
|
+
slack setup Configure Slack integration
|
|
2519
|
+
<command> [args] Start a monitored session
|
|
1951
2520
|
help Show this help message
|
|
1952
2521
|
|
|
1953
2522
|
Examples:
|
|
1954
|
-
afk-code
|
|
1955
|
-
afk-code
|
|
1956
|
-
afk-code discord setup
|
|
1957
|
-
afk-code discord
|
|
1958
|
-
afk-code
|
|
2523
|
+
afk-code telegram setup # First-time Telegram configuration
|
|
2524
|
+
afk-code telegram # Start the Telegram bot
|
|
2525
|
+
afk-code discord setup # First-time Discord configuration
|
|
2526
|
+
afk-code discord # Start the Discord bot
|
|
2527
|
+
afk-code slack setup # First-time Slack configuration
|
|
2528
|
+
afk-code slack # Start the Slack bot
|
|
2529
|
+
afk-code claude # Start a Claude Code session
|
|
1959
2530
|
`);
|
|
1960
2531
|
break;
|
|
1961
2532
|
}
|
|
1962
2533
|
default: {
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
process.exit(1);
|
|
2534
|
+
await run(args);
|
|
2535
|
+
break;
|
|
1966
2536
|
}
|
|
1967
2537
|
}
|
|
1968
2538
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "afk-code",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Monitor and interact with Claude Code sessions from Slack/Discord",
|
|
3
|
+
"version": "0.2.0",
|
|
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,14 +30,16 @@
|
|
|
30
30
|
"claude-code",
|
|
31
31
|
"slack",
|
|
32
32
|
"discord",
|
|
33
|
+
"telegram",
|
|
33
34
|
"bot",
|
|
34
35
|
"ai",
|
|
35
36
|
"cli"
|
|
36
37
|
],
|
|
37
38
|
"license": "MIT",
|
|
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
|
},
|