Files
claude-telegram-bot/docs/superpowers/plans/2026-04-10-telegram-bot-plan.md
T

1037 lines
25 KiB
Markdown

# Claude Telegram Bot Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Build a Telegram bot that manages Claude Code sessions on a local server — start/stop/switch sessions per project and chat with Claude through Telegram.
**Architecture:** Single Bun process with grammy for Telegram, Claude Agent SDK for session management, JSON file for state persistence. Owner-only access via chat_id allowlist.
**Tech Stack:** TypeScript, Bun, grammy, @anthropic-ai/claude-agent-sdk
---
## File Structure
| File | Responsibility |
|------|----------------|
| `src/index.ts` | Entry point: load env, init store, create bot, start |
| `src/bot.ts` | grammy Bot instance, owner middleware, register handlers |
| `src/handlers/commands.ts` | All `/command` handlers with inline keyboard support |
| `src/handlers/message.ts` | Plain text → Claude session routing |
| `src/services/session-manager.ts` | Session CRUD, switching, project bookmarks, name generation |
| `src/services/claude.ts` | Claude Agent SDK wrapper: createSession, sendMessage |
| `src/state/store.ts` | Load/save state.json, AppState type definitions |
| `src/utils/telegram.ts` | Split long messages, format markdown for Telegram |
| `package.json` | Dependencies and scripts |
| `tsconfig.json` | TypeScript config for Bun |
| `.env` | Tokens and config (not committed) |
| `.gitignore` | Ignore node_modules, .env, dist |
---
### Task 1: Project Scaffolding
**Files:**
- Create: `package.json`
- Create: `tsconfig.json`
- Create: `.gitignore`
- Create: `.env`
- Create: `src/index.ts` (placeholder)
- [ ] **Step 1: Initialize git repository**
```bash
cd /d/claude_telegram_bot
git init
```
- [ ] **Step 2: Create package.json**
```json
{
"name": "claude-telegram-bot",
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "bun run src/index.ts",
"dev": "bun --watch run src/index.ts"
},
"dependencies": {
"grammy": "^1.35.0",
"@anthropic-ai/claude-agent-sdk": "^0.1.0",
"dotenv": "^16.4.0"
}
}
```
- [ ] **Step 3: Create tsconfig.json**
```json
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"outDir": "dist",
"rootDir": "src",
"types": ["bun-types"]
},
"include": ["src"]
}
```
- [ ] **Step 4: Create .gitignore**
```
node_modules/
dist/
.env
```
- [ ] **Step 5: Create .env with placeholder values**
```
TELEGRAM_BOT_TOKEN=your-telegram-bot-token
TELEGRAM_OWNER_ID=your-telegram-user-id
ANTHROPIC_API_KEY=your-anthropic-api-key
```
- [ ] **Step 6: Create placeholder entry point**
Create `src/index.ts`:
```typescript
console.log("Claude Telegram Bot starting...");
```
- [ ] **Step 7: Install dependencies**
```bash
cd /d/claude_telegram_bot
bun install
```
- [ ] **Step 8: Verify it runs**
```bash
bun run src/index.ts
```
Expected: prints "Claude Telegram Bot starting..."
- [ ] **Step 9: Commit**
```bash
git add package.json tsconfig.json .gitignore src/index.ts bun.lock
git commit -m "feat: scaffold project with bun, grammy, claude-agent-sdk"
```
---
### Task 2: State Store
**Files:**
- Create: `src/state/store.ts`
- [ ] **Step 1: Implement Store class**
Create `src/state/store.ts`:
```typescript
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
import { dirname } from "path";
import { homedir } from "os";
import { join } from "path";
export interface SessionInfo {
sessionId: string;
cwd: string;
status: "idle" | "busy";
}
export interface AppState {
projects: Record<string, string>;
sessions: Record<string, SessionInfo>;
activeSession: string | null;
}
const STATE_DIR = join(homedir(), ".claude-telegram-bot");
const STATE_FILE = join(STATE_DIR, "state.json");
const DEFAULT_STATE: AppState = {
projects: {},
sessions: {},
activeSession: null,
};
export class Store {
private state: AppState;
constructor() {
this.state = this.load();
}
private load(): AppState {
if (!existsSync(STATE_FILE)) {
return { ...DEFAULT_STATE, projects: {}, sessions: {} };
}
const raw = readFileSync(STATE_FILE, "utf-8");
return JSON.parse(raw) as AppState;
}
save(): void {
if (!existsSync(STATE_DIR)) {
mkdirSync(STATE_DIR, { recursive: true });
}
writeFileSync(STATE_FILE, JSON.stringify(this.state, null, 2));
}
get data(): AppState {
return this.state;
}
}
```
- [ ] **Step 2: Verify it compiles**
```bash
bun build src/state/store.ts --outdir /tmp/check --no-bundle 2>&1 | head -5
```
Expected: no errors
- [ ] **Step 3: Commit**
```bash
git add src/state/store.ts
git commit -m "feat: add state store with JSON persistence"
```
---
### Task 3: Session Manager
**Files:**
- Create: `src/services/session-manager.ts`
- [ ] **Step 1: Implement SessionManager**
Create `src/services/session-manager.ts`:
```typescript
import { Store, type SessionInfo } from "../state/store.js";
import { basename } from "path";
export class SessionManager {
constructor(private store: Store) {}
get activeSession(): string | null {
return this.store.data.activeSession;
}
get activeSessionInfo(): SessionInfo | null {
const name = this.store.data.activeSession;
if (!name) return null;
return this.store.data.sessions[name] ?? null;
}
getAllSessions(): Record<string, SessionInfo> {
return this.store.data.sessions;
}
resolveProjectPath(nameOrPath: string): { name: string; path: string } | null {
// Check bookmarks first
const bookmarked = this.store.data.projects[nameOrPath];
if (bookmarked) {
return { name: nameOrPath, path: bookmarked };
}
// Treat as path
const name = this.generateName(basename(nameOrPath));
return { name, path: nameOrPath };
}
private generateName(base: string): string {
if (!this.store.data.sessions[base]) return base;
let i = 2;
while (this.store.data.sessions[`${base}-${i}`]) i++;
return `${base}-${i}`;
}
createSession(name: string, cwd: string, sessionId: string): void {
this.store.data.sessions[name] = { sessionId, cwd, status: "idle" };
this.store.data.activeSession = name;
this.store.save();
}
stopSession(name: string): boolean {
if (!this.store.data.sessions[name]) return false;
delete this.store.data.sessions[name];
if (this.store.data.activeSession === name) {
const remaining = Object.keys(this.store.data.sessions);
this.store.data.activeSession = remaining.length > 0 ? remaining[0] : null;
}
this.store.save();
return true;
}
stopAll(): number {
const count = Object.keys(this.store.data.sessions).length;
this.store.data.sessions = {};
this.store.data.activeSession = null;
this.store.save();
return count;
}
switchSession(name: string): boolean {
if (!this.store.data.sessions[name]) return false;
this.store.data.activeSession = name;
this.store.save();
return true;
}
setStatus(name: string, status: "idle" | "busy"): void {
const session = this.store.data.sessions[name];
if (session) {
session.status = status;
this.store.save();
}
}
// Project bookmarks
saveProject(name: string, path: string): void {
this.store.data.projects[name] = path;
this.store.save();
}
removeProject(name: string): boolean {
if (!this.store.data.projects[name]) return false;
delete this.store.data.projects[name];
this.store.save();
return true;
}
getProjects(): Record<string, string> {
return this.store.data.projects;
}
}
```
- [ ] **Step 2: Verify it compiles**
```bash
bun build src/services/session-manager.ts --outdir /tmp/check --no-bundle 2>&1 | head -5
```
Expected: no errors
- [ ] **Step 3: Commit**
```bash
git add src/services/session-manager.ts
git commit -m "feat: add session manager with CRUD, switching, bookmarks"
```
---
### Task 4: Claude Agent SDK Wrapper
**Files:**
- Create: `src/services/claude.ts`
- [ ] **Step 1: Implement ClaudeService**
Create `src/services/claude.ts`:
```typescript
import { query } from "@anthropic-ai/claude-agent-sdk";
export interface ClaudeResult {
sessionId: string;
result: string;
}
export class ClaudeService {
async createSession(prompt: string, cwd: string): Promise<ClaudeResult> {
let sessionId = "";
let result = "";
for await (const message of query({
prompt,
options: {
cwd,
permissionMode: "bypassPermissions",
allowedTools: [
"Read", "Write", "Edit", "Bash", "Glob", "Grep",
"WebSearch", "WebFetch", "Agent",
],
},
})) {
if (message.type === "system" && message.subtype === "init") {
sessionId = message.session_id;
}
if ("result" in message && typeof message.result === "string") {
result = message.result;
}
}
return { sessionId, result };
}
async sendMessage(prompt: string, sessionId: string): Promise<string> {
let result = "";
for await (const message of query({
prompt,
options: {
resume: sessionId,
permissionMode: "bypassPermissions",
},
})) {
if ("result" in message && typeof message.result === "string") {
result = message.result;
}
}
return result;
}
}
```
- [ ] **Step 2: Verify it compiles**
```bash
bun build src/services/claude.ts --outdir /tmp/check --no-bundle 2>&1 | head -5
```
Expected: no errors
- [ ] **Step 3: Commit**
```bash
git add src/services/claude.ts
git commit -m "feat: add Claude Agent SDK wrapper with session create/resume"
```
---
### Task 5: Telegram Utilities
**Files:**
- Create: `src/utils/telegram.ts`
- [ ] **Step 1: Implement message splitting and formatting**
Create `src/utils/telegram.ts`:
```typescript
const MAX_LENGTH = 4096;
export function splitMessage(text: string): string[] {
if (text.length <= MAX_LENGTH) return [text];
const chunks: string[] = [];
let remaining = text;
while (remaining.length > 0) {
if (remaining.length <= MAX_LENGTH) {
chunks.push(remaining);
break;
}
// Try to split at paragraph boundary
let splitAt = remaining.lastIndexOf("\n\n", MAX_LENGTH);
if (splitAt === -1 || splitAt < MAX_LENGTH / 2) {
// Try line boundary
splitAt = remaining.lastIndexOf("\n", MAX_LENGTH);
}
if (splitAt === -1 || splitAt < MAX_LENGTH / 2) {
// Hard split
splitAt = MAX_LENGTH;
}
chunks.push(remaining.slice(0, splitAt));
remaining = remaining.slice(splitAt).trimStart();
}
return chunks;
}
export function escapeMarkdown(text: string): string {
// grammy supports Markdown parse mode — minimal escaping needed
return text;
}
```
- [ ] **Step 2: Verify it compiles**
```bash
bun build src/utils/telegram.ts --outdir /tmp/check --no-bundle 2>&1 | head -5
```
Expected: no errors
- [ ] **Step 3: Commit**
```bash
git add src/utils/telegram.ts
git commit -m "feat: add telegram message splitting utility"
```
---
### Task 6: Command Handlers
**Files:**
- Create: `src/handlers/commands.ts`
- [ ] **Step 1: Implement all command handlers**
Create `src/handlers/commands.ts`:
```typescript
import { type Context, InlineKeyboard } from "grammy";
import { SessionManager } from "../services/session-manager.js";
import { ClaudeService } from "../services/claude.js";
export function registerCommands(
manager: SessionManager,
claude: ClaudeService,
onCommand: Record<string, (ctx: Context) => Promise<void>>,
): void {
onCommand["start"] = async (ctx) => {
const arg = ctx.match as string;
if (!arg) {
await ctx.reply("Usage: /start <path or project name>");
return;
}
const resolved = manager.resolveProjectPath(arg);
if (!resolved) {
await ctx.reply("Could not resolve project path.");
return;
}
// Check if session with this name already exists
if (manager.getAllSessions()[resolved.name]) {
await ctx.reply(
`Session "${resolved.name}" already exists. Use /switch ${resolved.name}`,
);
return;
}
await ctx.reply(`Starting session "${resolved.name}" in ${resolved.path}...`);
try {
const { sessionId, result } = await claude.createSession(
"You are now connected via Telegram. Briefly confirm you're ready and state the project directory.",
resolved.path,
);
manager.createSession(resolved.name, resolved.path, sessionId);
await ctx.reply(`Session "${resolved.name}" created.\n\n${result}`);
} catch (err) {
await ctx.reply(`Failed to create session: ${err}`);
}
};
onCommand["stop"] = async (ctx) => {
const arg = ctx.match as string;
const name = arg || manager.activeSession;
if (!name) {
await ctx.reply("No active session. Nothing to stop.");
return;
}
if (manager.stopSession(name)) {
const active = manager.activeSession;
let msg = `Session "${name}" stopped.`;
if (active) msg += ` Switched to "${active}".`;
await ctx.reply(msg);
} else {
await ctx.reply(`Session "${name}" not found.`);
}
};
onCommand["stopall"] = async (ctx) => {
const count = manager.stopAll();
await ctx.reply(`Stopped ${count} session(s).`);
};
onCommand["switch"] = async (ctx) => {
const arg = ctx.match as string;
if (!arg) {
await ctx.reply("Usage: /switch <session name>");
return;
}
if (manager.switchSession(arg)) {
await ctx.reply(`Switched to "${arg}".`);
} else {
await ctx.reply(`Session "${arg}" not found. Use /sessions to see active sessions.`);
}
};
onCommand["sessions"] = async (ctx) => {
const sessions = manager.getAllSessions();
const names = Object.keys(sessions);
if (names.length === 0) {
await ctx.reply("No active sessions.");
return;
}
const active = manager.activeSession;
const lines = names.map((name) => {
const s = sessions[name];
const marker = name === active ? "►" : " ";
return `${marker} ${name}${s.cwd} (${s.status})`;
});
const keyboard = new InlineKeyboard();
for (const name of names) {
keyboard.text(name, `switch:${name}`);
}
await ctx.reply(lines.join("\n"), { reply_markup: keyboard });
};
onCommand["status"] = async (ctx) => {
const info = manager.activeSessionInfo;
if (!info) {
await ctx.reply("No active session.");
return;
}
const name = manager.activeSession!;
await ctx.reply(
`Session: ${name}\nPath: ${info.cwd}\nStatus: ${info.status}`,
);
};
onCommand["save"] = async (ctx) => {
const arg = ctx.match as string;
const parts = arg.split(/\s+/, 2);
if (parts.length < 2) {
await ctx.reply("Usage: /save <name> <path>");
return;
}
manager.saveProject(parts[0], parts[1]);
await ctx.reply(`Project "${parts[0]}" saved → ${parts[1]}`);
};
onCommand["remove"] = async (ctx) => {
const arg = ctx.match as string;
if (!arg) {
await ctx.reply("Usage: /remove <name>");
return;
}
if (manager.removeProject(arg)) {
await ctx.reply(`Project "${arg}" removed.`);
} else {
await ctx.reply(`Project "${arg}" not found.`);
}
};
onCommand["projects"] = async (ctx) => {
const projects = manager.getProjects();
const names = Object.keys(projects);
if (names.length === 0) {
await ctx.reply("No saved projects.");
return;
}
const lines = names.map((n) => `${n}${projects[n]}`);
const keyboard = new InlineKeyboard();
for (const name of names) {
keyboard.text(`Start ${name}`, `start:${name}`);
}
await ctx.reply(lines.join("\n"), { reply_markup: keyboard });
};
onCommand["help"] = async (ctx) => {
await ctx.reply(
[
"Session commands:",
" /start <path|name> — start new session",
" /stop [name] — stop session",
" /stopall — stop all sessions",
" /switch <name> — switch active session",
" /sessions — list sessions",
" /status — current session info",
"",
"Project bookmarks:",
" /save <name> <path> — save bookmark",
" /remove <name> — remove bookmark",
" /projects — list bookmarks",
"",
"Any other text is sent to the active session.",
].join("\n"),
);
};
}
```
- [ ] **Step 2: Verify it compiles**
```bash
bun build src/handlers/commands.ts --outdir /tmp/check --no-bundle 2>&1 | head -5
```
Expected: no errors
- [ ] **Step 3: Commit**
```bash
git add src/handlers/commands.ts
git commit -m "feat: add all command handlers with inline keyboards"
```
---
### Task 7: Message Handler
**Files:**
- Create: `src/handlers/message.ts`
- [ ] **Step 1: Implement text message handler**
Create `src/handlers/message.ts`:
```typescript
import { type Context } from "grammy";
import { SessionManager } from "../services/session-manager.js";
import { ClaudeService } from "../services/claude.js";
import { splitMessage } from "../utils/telegram.js";
export function createMessageHandler(
manager: SessionManager,
claude: ClaudeService,
) {
return async (ctx: Context) => {
const text = ctx.message?.text;
if (!text) return;
const activeName = manager.activeSession;
if (!activeName) {
await ctx.reply("No active session. Use /start <path or name> to create one.");
return;
}
const session = manager.activeSessionInfo;
if (!session) return;
if (session.status === "busy") {
await ctx.reply("Session is busy, wait for the current request to finish.");
return;
}
manager.setStatus(activeName, "busy");
await ctx.replyWithChatAction("typing");
try {
const result = await claude.sendMessage(text, session.sessionId);
const response = result || "(empty response)";
const chunks = splitMessage(response);
for (const chunk of chunks) {
await ctx.reply(chunk);
}
} catch (err) {
await ctx.reply(`Error: ${err}`);
} finally {
manager.setStatus(activeName, "idle");
}
};
}
```
- [ ] **Step 2: Verify it compiles**
```bash
bun build src/handlers/message.ts --outdir /tmp/check --no-bundle 2>&1 | head -5
```
Expected: no errors
- [ ] **Step 3: Commit**
```bash
git add src/handlers/message.ts
git commit -m "feat: add message handler routing text to active Claude session"
```
---
### Task 8: Bot Setup
**Files:**
- Create: `src/bot.ts`
- [ ] **Step 1: Implement bot with middleware and handler registration**
Create `src/bot.ts`:
```typescript
import { Bot, type Context } from "grammy";
import { SessionManager } from "./services/session-manager.js";
import { ClaudeService } from "./services/claude.js";
import { registerCommands } from "./handlers/commands.js";
import { createMessageHandler } from "./handlers/message.js";
export function createBot(token: string, ownerId: number): Bot {
const bot = new Bot(token);
// Owner-only middleware
bot.use(async (ctx, next) => {
if (ctx.from?.id !== ownerId) return;
await next();
});
const store = (await import("./state/store.js")).Store;
const storeInstance = new store();
const manager = new SessionManager(storeInstance);
const claude = new ClaudeService();
// Register command handlers
const commands: Record<string, (ctx: Context) => Promise<void>> = {};
registerCommands(manager, claude, commands);
for (const [name, handler] of Object.entries(commands)) {
bot.command(name, handler);
}
// Callback queries for inline buttons
bot.on("callback_query:data", async (ctx) => {
const data = ctx.callbackQuery.data;
if (data.startsWith("switch:")) {
const name = data.slice("switch:".length);
if (manager.switchSession(name)) {
await ctx.answerCallbackQuery({ text: `Switched to "${name}"` });
await ctx.reply(`Switched to "${name}".`);
} else {
await ctx.answerCallbackQuery({ text: `Session "${name}" not found` });
}
} else if (data.startsWith("start:")) {
const name = data.slice("start:".length);
const resolved = manager.resolveProjectPath(name);
if (!resolved) {
await ctx.answerCallbackQuery({ text: "Project not found" });
return;
}
if (manager.getAllSessions()[resolved.name]) {
manager.switchSession(resolved.name);
await ctx.answerCallbackQuery({ text: `Switched to "${resolved.name}"` });
await ctx.reply(`Session "${resolved.name}" already exists. Switched to it.`);
return;
}
await ctx.answerCallbackQuery({ text: `Starting "${resolved.name}"...` });
await ctx.reply(`Starting session "${resolved.name}" in ${resolved.path}...`);
try {
const { sessionId, result } = await claude.createSession(
"You are now connected via Telegram. Briefly confirm you're ready and state the project directory.",
resolved.path,
);
manager.createSession(resolved.name, resolved.path, sessionId);
await ctx.reply(`Session "${resolved.name}" created.\n\n${result}`);
} catch (err) {
await ctx.reply(`Failed to create session: ${err}`);
}
}
});
// Plain text messages
bot.on("message:text", createMessageHandler(manager, claude));
return bot;
}
```
- [ ] **Step 2: Verify it compiles**
```bash
bun build src/bot.ts --outdir /tmp/check --no-bundle 2>&1 | head -5
```
Expected: no errors
- [ ] **Step 3: Commit**
```bash
git add src/bot.ts
git commit -m "feat: add bot setup with owner middleware, commands, callbacks"
```
---
### Task 9: Entry Point
**Files:**
- Modify: `src/index.ts`
- [ ] **Step 1: Implement entry point**
Replace `src/index.ts` with:
```typescript
import "dotenv/config";
import { createBot } from "./bot.js";
const token = process.env.TELEGRAM_BOT_TOKEN;
const ownerId = process.env.TELEGRAM_OWNER_ID;
const apiKey = process.env.ANTHROPIC_API_KEY;
if (!token) throw new Error("TELEGRAM_BOT_TOKEN is required");
if (!ownerId) throw new Error("TELEGRAM_OWNER_ID is required");
if (!apiKey) throw new Error("ANTHROPIC_API_KEY is required");
const bot = createBot(token, Number(ownerId));
bot.start({
onStart: () => console.log("Bot started successfully"),
});
// Graceful shutdown
const shutdown = () => {
console.log("Shutting down...");
bot.stop();
process.exit(0);
};
process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);
```
- [ ] **Step 2: Verify it compiles**
```bash
bun build src/index.ts --outdir /tmp/check --no-bundle 2>&1 | head -5
```
Expected: no errors
- [ ] **Step 3: Commit**
```bash
git add src/index.ts
git commit -m "feat: add entry point with env validation and graceful shutdown"
```
---
### Task 10: Fix Bot Initialization (async import)
**Files:**
- Modify: `src/bot.ts`
The `createBot` function uses a top-level `await import()` inside a non-async function. This needs to be restructured so that `Store` is imported statically and bot creation is clean.
- [ ] **Step 1: Fix bot.ts to use static imports**
Replace the dynamic import in `src/bot.ts`. Change:
```typescript
const store = (await import("./state/store.js")).Store;
const storeInstance = new store();
```
to use a static import at the top of the file and make `createBot` synchronous:
```typescript
import { Store } from "./state/store.js";
```
And inside `createBot`:
```typescript
const storeInstance = new Store();
```
- [ ] **Step 2: Verify full project compiles**
```bash
bun build src/index.ts --outdir /tmp/check 2>&1 | head -5
```
Expected: no errors
- [ ] **Step 3: Commit**
```bash
git add src/bot.ts
git commit -m "fix: use static import for Store in bot.ts"
```
---
### Task 11: Integration Test — Manual Smoke Test
- [ ] **Step 1: Fill in real .env values**
Edit `.env` with actual tokens:
```
TELEGRAM_BOT_TOKEN=<real token from BotFather>
TELEGRAM_OWNER_ID=<your Telegram user ID>
ANTHROPIC_API_KEY=<your Anthropic API key>
```
- [ ] **Step 2: Start the bot**
```bash
cd /d/claude_telegram_bot
bun run start
```
Expected: "Bot started successfully"
- [ ] **Step 3: Test in Telegram**
Send these messages to the bot in order:
1. `/help` — should show command list
2. `/start /d/claude_telegram_bot` — should create session
3. `What files are in this directory?` — should get Claude response
4. `/status` — should show session info
5. `/sessions` — should show list with inline button
6. `/save mybot /d/claude_telegram_bot` — should save bookmark
7. `/projects` — should show bookmark with "Start" button
8. `/stop` — should stop session
9. Verify `/sessions` shows empty list
- [ ] **Step 4: Commit any fixes from smoke test**
```bash
git add -A
git commit -m "fix: address issues found in smoke test"
```
(Skip this step if no fixes needed.)
---
### Task 12: Push to Git Remote
- [ ] **Step 1: Create repository on Gitea**
```bash
curl -X POST "http://localhost:3000/api/v1/user/repos" \
-H "Content-Type: application/json" \
-u "admin:gowtep-Wovdek-hehpu5" \
-d '{"name":"claude-telegram-bot","description":"Telegram bot for managing Claude Code sessions","private":false}'
```
- [ ] **Step 2: Add remote and push**
```bash
cd /d/claude_telegram_bot
git remote add origin http://admin:gowtep-Wovdek-hehpu5@localhost:3000/admin/claude-telegram-bot.git
git push -u origin main
```
Expected: successful push
- [ ] **Step 3: Verify in browser**
Open `http://localhost:3000/admin/claude-telegram-bot` — repository should show all files and commits.