Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ The server loads the plugin at startup. The plugin starts a grammY long-polling
src/
main.ts -- plugin entry: env validation, bot start, event hook
telegram.ts -- grammY bot: auth middleware, commands, prompt dispatch, streaming edits
opencode.ts -- SDK client wrapper: session CRUD, prompt, abort, permissions, auto-approve
opencode.ts -- SDK client wrapper: session CRUD, prompt, abort, permissions
format.ts -- MarkdownV2 escaping, code block preservation, tool summaries, message chunking
events.ts -- event router: message.part.updated, permission.asked, streaming dispatch
memory.ts -- exchange logging: saves every Telegram exchange as markdown to exchanges/
Expand All @@ -35,7 +35,7 @@ skills/
- Session-to-chat mapping persisted to `sessions.json` (resolved relative to `directory` from plugin context)
- Streaming uses edit-in-place with 2s throttle to stay under Telegram rate limits
- Prompt is fired non-blocking (`.then/.catch`) so grammY can process permission callbacks while waiting
- Permission requests surface as inline keyboards (Allow/Session/Deny); auto-approve is per-session toggle
- Permission requests surface as inline keyboards (Allow/Session/Deny)
- Permission callback data uses short counter keys (Telegram 64-byte limit on callback data)
- Message chunking respects code block boundaries at 4096 char Telegram limit
- Server sends `permission.asked` with a shape that diverges from the SDK's v1 `Permission` type.
Expand All @@ -51,6 +51,7 @@ skills/
## Config

Environment variables (set in server's environment or systemd `EnvironmentFile=`):

- `TELEGRAM_BOT_TOKEN` -- required
- `TELEGRAM_ALLOWED_USERS` -- required, comma-separated Telegram user IDs
- `OPENCODE_WORKSPACE` -- optional, server working directory (used by Makefile at install time)
Expand Down Expand Up @@ -78,7 +79,7 @@ Install the plugin into the OpenCode workspace's plugin directory (symlink or co
| Telegram lib | grammY | Lightweight, TypeScript-native |
| Runtime | bun | No build step, runs TS directly |
| Bot mode | Long polling | Simpler than webhooks for single-user |
| Session mapping | File-persisted Map | Survives restarts; auto-approve is in-memory only |
| Session mapping | File-persisted Map | Survives restarts |
| Deployment | systemd user service | Reliable, auto-restart, journald logging |
| Streaming | Edit-in-place messages | ChatGPT-style UX with 2s throttle |
| Prompt execution | Non-blocking `.then/.catch` | Avoids deadlock: grammY must process permission callbacks while prompt blocks |
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ make uninstall
| `/new` | New session |
| `/sessions` | List and switch sessions |
| `/abort` | Abort current session |
| `/autoapprove on\|off` | Toggle auto-approve for permissions |
| `/history` | Recent messages from current session |
| `/remember <text>` | Save a memory to MEMORY.md via OpenCode |
| `/start_llama` | Start llama systemd service |
Expand Down
2 changes: 0 additions & 2 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,9 @@ export const ClawCode: Plugin = async ({ client, directory }) => {
const bot = createBot(token, allowedUsers);

await bot.api.setMyCommands([
{ command: "start", description: "Welcome message" },
{ command: "new", description: "New session" },
{ command: "sessions", description: "List and switch sessions" },
{ command: "abort", description: "Abort current session" },
{ command: "autoapprove", description: "Toggle auto-approve (on|off)" },
{ command: "history", description: "Recent messages from current session" },
{ command: "remember", description: "Save a memory (/remember <text>)" },
{ command: "start_llama", description: "Start llama service" },
Expand Down
10 changes: 0 additions & 10 deletions src/opencode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ let client: Client;
let sessionsFile: string;

const chatSessions = new Map<number, string>();
const autoApprove = new Set<string>();

export function init(c: Client, directory: string): void {
client = c;
Expand All @@ -32,15 +31,6 @@ function saveSessions(): void {
writeFileSync(sessionsFile, JSON.stringify(obj, null, 2));
}

export function setAutoApprove(sessionId: string, enabled: boolean): void {
if (enabled) autoApprove.add(sessionId);
else autoApprove.delete(sessionId);
}

export function isAutoApprove(sessionId: string): boolean {
return autoApprove.has(sessionId);
}

export async function getOrCreateSession(
chatId: number,
): Promise<{ sessionId: string; fallback: boolean }> {
Expand Down
43 changes: 1 addition & 42 deletions src/telegram.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ import {
abortSession,
sendPrompt,
replyPermission,
setAutoApprove,
isAutoApprove,
} from "./opencode.js";
import { registerSession, unregisterSession } from "./events.js";
import { saveExchange } from "./memory.js";
Expand Down Expand Up @@ -106,14 +104,7 @@ export function createBot(token: string, allowedUsers: number[]): Bot {
}
});

bot.command("start", async (ctx) => {
const sessionId = getSessionId(ctx.chat.id);
const autoApproveStatus = sessionId && isAutoApprove(sessionId) ? "on" : "off";
await ctx.reply(
`Welcome to ClawCode\\! Send me a message and I'll forward it to OpenCode\\.\n\nAuto\\-approve: *${autoApproveStatus}*`,
{ parse_mode: "MarkdownV2" },
);
});


bot.command("new", async (ctx) => {
const chatId = ctx.chat.id;
Expand Down Expand Up @@ -181,29 +172,6 @@ export function createBot(token: string, allowedUsers: number[]): Bot {
}
});

bot.command("autoapprove", async (ctx) => {
log.info(`[cmd] /autoapprove chat=${ctx.chat.id}`);
const arg = ctx.match?.trim().toLowerCase();
if (arg !== "on" && arg !== "off") {
await ctx.reply("Usage: /autoapprove on \\| off", { parse_mode: "MarkdownV2" });
return;
}
const sessionId = getSessionId(ctx.chat.id);
if (!sessionId) {
await ctx.reply("No active session\\. Use /new to create one\\.", {
parse_mode: "MarkdownV2",
});
return;
}
const enabled = arg === "on";
setAutoApprove(sessionId, enabled);
log.info(`[session] autoapprove=${enabled} session=${sessionId}`);
await ctx.reply(
escapeMarkdownV2(`Auto-approve ${enabled ? "enabled" : "disabled"} for current session.`),
{ parse_mode: "MarkdownV2" },
);
});

bot.command("history", async (ctx) => {
log.info(`[cmd] /history chat=${ctx.chat.id}`);
const sessionId = getSessionId(ctx.chat.id);
Expand Down Expand Up @@ -380,15 +348,6 @@ export function createBot(token: string, allowedUsers: number[]): Bot {
// onPermission
async (perm: PermissionEvent) => {
log.info(`[permission] request permission=${perm.permission} session=${perm.sessionID} perm=${perm.id}`);
if (isAutoApprove(perm.sessionID)) {
log.info(`[permission] auto-approving perm=${perm.id}`);
await replyPermission(perm.sessionID, perm.id, "once");
await ctx.api.sendMessage(chatId,
escapeMarkdownV2(`Auto-approved: ${perm.permission} ${perm.patterns.join(", ")}`),
{ parse_mode: "MarkdownV2" },
);
return;
}
const key = String(++permCounter);
pendingPerms.set(key, { sessionId: perm.sessionID, permissionId: perm.id });
const keyboard = new InlineKeyboard()
Expand Down
Loading