mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:32:32 +00:00
- Fix typos: parellel→parallel, neccessary→necessary, mulitple→multiple, responible→responsible, geniune→genuine, yeild→yield - Add ignore words: Nd, nd, edn, lightening (valid terms) - Run prettier --write to fix all formatting issues
239 lines
7.4 KiB
JavaScript
239 lines
7.4 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Claude CLI PTY Proxy
|
|
*
|
|
* Uses PTY to interact with Claude CLI in interactive mode,
|
|
* which uses your Claude subscription instead of API credits.
|
|
*/
|
|
|
|
const http = require("http");
|
|
const pty = require("node-pty");
|
|
|
|
const PORT = parseInt(process.env.PORT || "9999");
|
|
|
|
// Track active PTY sessions
|
|
const sessions = new Map();
|
|
|
|
/**
|
|
* Send a message to Claude via PTY and get response
|
|
*/
|
|
function sendToClaude(prompt) {
|
|
return new Promise((resolve, reject) => {
|
|
const timeout = 120000; // 2 minutes
|
|
let output = "";
|
|
let responseStarted = false;
|
|
let lastDataTime = Date.now();
|
|
|
|
// Spawn Claude in interactive mode
|
|
const proc = pty.spawn("claude", ["--dangerously-skip-permissions"], {
|
|
name: "xterm-256color",
|
|
cols: 120,
|
|
rows: 40,
|
|
cwd: process.cwd(),
|
|
env: {
|
|
...process.env,
|
|
TERM: "xterm-256color",
|
|
NO_COLOR: "1", // Disable colors for easier parsing
|
|
},
|
|
});
|
|
|
|
const timeoutId = setTimeout(() => {
|
|
proc.kill();
|
|
reject(new Error("Timeout waiting for Claude response"));
|
|
}, timeout);
|
|
|
|
// Detect when Claude is ready for input
|
|
let ready = false;
|
|
let sentPrompt = false;
|
|
|
|
proc.onData((data) => {
|
|
lastDataTime = Date.now();
|
|
output += data;
|
|
|
|
// Debug logging
|
|
if (process.env.DEBUG) {
|
|
console.log("[PTY DATA]:", JSON.stringify(data.substring(0, 100)));
|
|
}
|
|
|
|
// Detect ready state (Claude shows prompt)
|
|
if (!ready && (output.includes(">") || output.includes("Claude"))) {
|
|
ready = true;
|
|
}
|
|
|
|
// Send prompt once ready
|
|
if (ready && !sentPrompt) {
|
|
sentPrompt = true;
|
|
setTimeout(() => {
|
|
proc.write(prompt + "\n");
|
|
}, 500);
|
|
}
|
|
|
|
// Detect response completion (new prompt appears after response)
|
|
if (sentPrompt && output.split(prompt).length > 1) {
|
|
// Look for completion indicators
|
|
const afterPrompt = output.split(prompt).pop();
|
|
if (afterPrompt && afterPrompt.includes(">")) {
|
|
// Response complete
|
|
clearTimeout(timeoutId);
|
|
proc.kill();
|
|
|
|
// Extract response between prompt and next >
|
|
const response = extractResponse(output, prompt);
|
|
resolve(response);
|
|
}
|
|
}
|
|
});
|
|
|
|
proc.onExit(({ exitCode }) => {
|
|
clearTimeout(timeoutId);
|
|
if (exitCode !== 0 && !output) {
|
|
reject(new Error(`Claude exited with code ${exitCode}`));
|
|
} else {
|
|
const response = extractResponse(output, prompt);
|
|
resolve(response || "No response received");
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Extract Claude's response from PTY output
|
|
*/
|
|
function extractResponse(output, prompt) {
|
|
// Remove ANSI escape codes
|
|
const clean = output
|
|
.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, "")
|
|
.replace(/\x1B\][^\x07]*\x07/g, "")
|
|
.replace(/[\x00-\x1F]/g, " ");
|
|
|
|
// Find the response after the prompt
|
|
const promptIndex = clean.indexOf(prompt);
|
|
if (promptIndex === -1) return clean.trim();
|
|
|
|
let response = clean.substring(promptIndex + prompt.length);
|
|
|
|
// Remove trailing prompt indicator
|
|
response = response.replace(/>\s*$/, "").trim();
|
|
|
|
// Remove any leading/trailing whitespace and common artifacts
|
|
response = response
|
|
.split("\n")
|
|
.filter(
|
|
(line) =>
|
|
line.trim() && !line.includes("Claude") && !line.startsWith(">"),
|
|
)
|
|
.join("\n")
|
|
.trim();
|
|
|
|
return response || "Response received";
|
|
}
|
|
|
|
/**
|
|
* Build prompt from messages
|
|
*/
|
|
function buildPrompt(messages) {
|
|
const lastUserMsg = messages.filter((m) => m.role === "user").pop();
|
|
return lastUserMsg?.content || messages[messages.length - 1]?.content || "";
|
|
}
|
|
|
|
/**
|
|
* HTTP request handler
|
|
*/
|
|
async function handleRequest(req, res) {
|
|
const url = new URL(req.url, `http://localhost:${PORT}`);
|
|
|
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
|
|
if (req.method === "OPTIONS") {
|
|
res.writeHead(200);
|
|
res.end();
|
|
return;
|
|
}
|
|
|
|
if (url.pathname === "/health" || url.pathname === "/") {
|
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
res.end(JSON.stringify({ status: "ok", mode: "pty-interactive" }));
|
|
return;
|
|
}
|
|
|
|
if (url.pathname === "/v1/chat/completions" && req.method === "POST") {
|
|
let body = "";
|
|
req.on("data", (chunk) => {
|
|
body += chunk.toString();
|
|
});
|
|
req.on("end", async () => {
|
|
try {
|
|
const request = JSON.parse(body);
|
|
const prompt = buildPrompt(request.messages || []);
|
|
|
|
console.log(
|
|
`[${new Date().toISOString()}] PTY Request: ${prompt.substring(0, 50)}...`,
|
|
);
|
|
|
|
const response = await sendToClaude(prompt);
|
|
|
|
console.log(
|
|
`[${new Date().toISOString()}] PTY Response: ${response.substring(0, 100)}...`,
|
|
);
|
|
|
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
res.end(
|
|
JSON.stringify({
|
|
id: `chatcmpl-${Date.now()}`,
|
|
object: "chat.completion",
|
|
created: Math.floor(Date.now() / 1000),
|
|
model: request.model || "claude-interactive",
|
|
choices: [
|
|
{
|
|
index: 0,
|
|
message: { role: "assistant", content: response },
|
|
finish_reason: "stop",
|
|
},
|
|
],
|
|
usage: {
|
|
prompt_tokens: Math.ceil(prompt.length / 4),
|
|
completion_tokens: Math.ceil(response.length / 4),
|
|
total_tokens: Math.ceil((prompt.length + response.length) / 4),
|
|
},
|
|
}),
|
|
);
|
|
} catch (error) {
|
|
console.error(`[${new Date().toISOString()}] Error:`, error.message);
|
|
res.writeHead(500, { "Content-Type": "application/json" });
|
|
res.end(
|
|
JSON.stringify({
|
|
error: { message: error.message, type: "server_error" },
|
|
}),
|
|
);
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
|
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
res.end(JSON.stringify({ error: { message: "Not found" } }));
|
|
}
|
|
|
|
const server = http.createServer(handleRequest);
|
|
|
|
server.listen(PORT, () => {
|
|
console.log(`
|
|
╔══════════════════════════════════════════════════════════════╗
|
|
║ Claude CLI PTY Proxy (Interactive Mode) ║
|
|
╠══════════════════════════════════════════════════════════════╣
|
|
║ Listening: http://localhost:${PORT} ║
|
|
║ Mode: PTY Interactive (uses subscription, not API) ║
|
|
╠══════════════════════════════════════════════════════════════╣
|
|
║ This proxy spawns Claude CLI in interactive mode via PTY, ║
|
|
║ which uses your Claude subscription instead of API credits. ║
|
|
╚══════════════════════════════════════════════════════════════╝
|
|
`);
|
|
});
|
|
|
|
process.on("SIGINT", () => {
|
|
console.log("\nShutting down...");
|
|
server.close(() => process.exit(0));
|
|
});
|