feat: add dev mode with DEV_ env prefix

Support dual configuration: PROXY_TOKEN/SERVER_URL for production,
DEV_PROXY_TOKEN/DEV_SERVER_URL for local development.

- npm start → production
- npm run start:dev → dev (PROXY_MODE=dev)
- npm run dev → dev with hot-reload
- Restore PTY auto-creation on sync_state for existing chats

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-22 01:19:21 +03:00
parent 4a91896732
commit c127faccef
4 changed files with 31 additions and 17 deletions

View File

@ -1,8 +1,7 @@
# Токен устройства (получить в simple-chat: Настройки → Устройство) # === Production ===
PROXY_TOKEN= PROXY_TOKEN=
SERVER_URL=wss://ai-chat.vigdorov.ru/ws/agent-proxy
# WebSocket URL бэкенда simple-chat # === Dev (используется при npm run start:dev / npm run dev) ===
# Локальная разработка: DEV_PROXY_TOKEN=
SERVER_URL=ws://localhost:3000/ws/agent-proxy DEV_SERVER_URL=ws://localhost:3000/ws/agent-proxy
# Продакшен:
# SERVER_URL=wss://ai-chat.vigdorov.ru/ws/agent-proxy

View File

@ -5,7 +5,8 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"start": "tsx src/main.ts", "start": "tsx src/main.ts",
"dev": "tsx watch src/main.ts", "start:dev": "PROXY_MODE=dev tsx src/main.ts",
"dev": "PROXY_MODE=dev tsx watch src/main.ts",
"lint": "eslint src/", "lint": "eslint src/",
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
}, },

View File

@ -6,6 +6,7 @@ import { homedir } from 'os';
export interface ProxyConfig { export interface ProxyConfig {
proxyToken: string; proxyToken: string;
serverUrl: string; serverUrl: string;
isDev: boolean;
} }
export function loadConfig(): ProxyConfig { export function loadConfig(): ProxyConfig {
@ -16,12 +17,15 @@ export function loadConfig(): ProxyConfig {
} }
config(); // local .env (won't override existing vars) config(); // local .env (won't override existing vars)
const proxyToken = process.env.PROXY_TOKEN; const isDev = process.env.PROXY_MODE === 'dev';
const prefix = isDev ? 'DEV_' : '';
const proxyToken = process.env[`${prefix}PROXY_TOKEN`];
if (!proxyToken) { if (!proxyToken) {
throw new Error('PROXY_TOKEN is required. Set it in .env or ~/.claude-proxy/.env'); throw new Error(`${prefix}PROXY_TOKEN is required. Set it in .env`);
} }
const serverUrl = process.env.SERVER_URL || 'wss://ai-chat.vigdorov.ru/ws/agent-proxy'; const serverUrl = process.env[`${prefix}SERVER_URL`] || 'wss://ai-chat.vigdorov.ru/ws/agent-proxy';
return { proxyToken, serverUrl }; return { proxyToken, serverUrl, isDev };
} }

View File

@ -23,15 +23,16 @@ import { existsSync, readdirSync, statSync, readFileSync } from 'fs';
class ClaudeCliProxy { class ClaudeCliProxy {
private ws: WsClient; private ws: WsClient;
private pty: PtyManager; private pty: PtyManager;
private config: ReturnType<typeof loadConfig>;
constructor() { constructor() {
const config = loadConfig(); this.config = loadConfig();
this.pty = new PtyManager(); this.pty = new PtyManager();
this.ws = new WsClient({ this.ws = new WsClient({
url: config.serverUrl, url: this.config.serverUrl,
token: config.proxyToken, token: this.config.proxyToken,
onMessage: (msg) => this.handleMessage(msg), onMessage: (msg) => this.handleMessage(msg),
onBinary: (data) => this.handleBinaryFrame(data), onBinary: (data) => this.handleBinaryFrame(data),
onConnected: () => this.onConnected(), onConnected: () => this.onConnected(),
@ -40,7 +41,8 @@ class ClaudeCliProxy {
} }
async start(): Promise<void> { async start(): Promise<void> {
console.log('[proxy] Starting claude-cli-proxy (xterm mode)...'); const mode = this.config.isDev ? 'DEV' : 'PROD';
console.log(`[proxy] Starting claude-cli-proxy (${mode}) → ${this.config.serverUrl}`);
this.ws.connect(); this.ws.connect();
@ -114,15 +116,23 @@ class ClaudeCliProxy {
const activePtys = new Set(this.pty.listPtys()); const activePtys = new Set(this.pty.listPtys());
const chatIds = new Set(payload.chats.map((c) => c.id)); const chatIds = new Set(payload.chats.map((c) => c.id));
// Only send replay + pty_ready for PTYs that already exist
// Do NOT auto-create PTYs — they should only be created via explicit create_pty
for (const chat of payload.chats) { for (const chat of payload.chats) {
if (activePtys.has(chat.id)) { if (activePtys.has(chat.id)) {
// PTY exists — send replay + ready
const replay = this.pty.getReplayBuffer(chat.id); const replay = this.pty.getReplayBuffer(chat.id);
if (replay) { if (replay) {
this.ws.sendBinary(encodeBinaryFrame(DIR_PTY_OUTPUT, chat.id, replay)); this.ws.sendBinary(encodeBinaryFrame(DIR_PTY_OUTPUT, chat.id, replay));
} }
this.ws.send({ type: 'pty_ready', payload: { chatId: chat.id } }); this.ws.send({ type: 'pty_ready', payload: { chatId: chat.id } });
} else if (chat.workDir) {
// PTY not running — create it
this.handleCreatePty({
chatId: chat.id,
dir: chat.workDir,
resumeSessionId: chat.sessionId ?? undefined,
cols: chat.cols ?? 120,
rows: chat.rows ?? 40,
});
} }
} }