claude-code as channel model, container trust/permissions fixes, terminal cwd fix
- add claude-code as virtual model in channel messaging (telegram/discord/whatsapp) - new send-claude-code.ts: docker exec claude -p with session resumption - route claude-code model in sendAndAwait before Pi pipeline - append claude-code to listPiModels output - fix container .claude mount (rw for sub-mounts), hooks format (matcher-based) - pre-seed hasTrustDialogAccepted and bypassPermissions in container settings - git init in entrypoint to skip workspace trust prompt - fix ~/~ double-tilde in CommandTerminalWrapper cwd resolution - remove --continue from claude-code panel command Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@ import * as piBridge from '@@/api/pi/pi-bridge';
|
||||
import { getHomeDir } from '@@/data-path';
|
||||
import { getUserSettings } from 'officerdb';
|
||||
import { logger } from '@@/api/pi/logger';
|
||||
import { sendClaudeCode, clearClaudeCodeSession } from './send-claude-code';
|
||||
|
||||
const DEFAULT_MODEL = 'opencode/big-pickle';
|
||||
const IDLE_TIMEOUT_MS = 60 * 60 * 1000;
|
||||
@@ -63,6 +64,7 @@ export function setSessionModel(context: string, userId: number, contextId: stri
|
||||
const sessionId = buildSessionId(context, userId, contextId);
|
||||
// Store override independently of session — survives idle eviction
|
||||
channelModelOverrides.set(sessionId, model);
|
||||
clearClaudeCodeSession(sessionId);
|
||||
|
||||
const session = sessionManager.getSession(sessionId);
|
||||
if (session) {
|
||||
@@ -94,6 +96,20 @@ export async function sendAndAwait(params: SendAndAwaitParams): Promise<SendAndA
|
||||
await existing;
|
||||
|
||||
try {
|
||||
// Resolve model early to check for claude-code routing
|
||||
const override = channelModelOverrides.get(sessionId);
|
||||
const resolvedModel = params.model ?? override ?? (await getUserDefaultModel(params.userId)) ?? DEFAULT_MODEL;
|
||||
|
||||
if (resolvedModel === 'claude-code') {
|
||||
return await sendClaudeCode({
|
||||
userId: params.userId,
|
||||
email: params.email,
|
||||
username: params.username,
|
||||
prompt: params.prompt,
|
||||
sessionKey: sessionId,
|
||||
});
|
||||
}
|
||||
|
||||
return await doSend(sessionId, params);
|
||||
} finally {
|
||||
releaseLock!();
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
import { ensureDockerContainer } from '@@/api/terminal/websocket';
|
||||
import { getHomeDir } from '@@/data-path';
|
||||
import { logger } from '@@/api/pi/logger';
|
||||
import type { MessageCost } from '@@/api/pi/types';
|
||||
|
||||
const SEND_TIMEOUT_MS = 5 * 60 * 1000;
|
||||
|
||||
type ClaudeCodeParams = {
|
||||
userId: number;
|
||||
email: string;
|
||||
username: string;
|
||||
prompt: string;
|
||||
sessionKey: string;
|
||||
};
|
||||
|
||||
type ClaudeCodeResult = {
|
||||
text: string;
|
||||
sessionId: string;
|
||||
model: string;
|
||||
cost: MessageCost;
|
||||
};
|
||||
|
||||
type ClaudeCodeOutput = {
|
||||
result: string;
|
||||
session_id: string;
|
||||
cost_usd: number;
|
||||
input_tokens: number;
|
||||
output_tokens: number;
|
||||
is_error: boolean;
|
||||
};
|
||||
|
||||
// Map channel session key → Claude Code session ID for --resume
|
||||
const claudeCodeSessions = new Map<string, string>();
|
||||
|
||||
export function clearClaudeCodeSession(sessionKey: string): void {
|
||||
claudeCodeSessions.delete(sessionKey);
|
||||
}
|
||||
|
||||
export async function sendClaudeCode(params: ClaudeCodeParams): Promise<ClaudeCodeResult> {
|
||||
const { userId, email, username, prompt, sessionKey } = params;
|
||||
const homeDir = getHomeDir(email);
|
||||
|
||||
const container = await ensureDockerContainer(email, userId, homeDir, username);
|
||||
const dockerPath = Bun.which('docker') ?? 'docker';
|
||||
const containerId = container.dockerId;
|
||||
const containerHome = `/home/${username}`;
|
||||
|
||||
const args = [
|
||||
dockerPath, 'exec', '-i',
|
||||
'-u', username,
|
||||
'-w', containerHome,
|
||||
'-e', `HOME=${containerHome}`,
|
||||
containerId,
|
||||
'claude', '-p', prompt,
|
||||
'--dangerously-skip-permissions',
|
||||
'--output-format', 'json',
|
||||
];
|
||||
|
||||
const existingSession = claudeCodeSessions.get(sessionKey);
|
||||
if (existingSession) {
|
||||
args.push('--resume', existingSession);
|
||||
}
|
||||
|
||||
logger.info('Claude Code exec', { sessionKey, containerId, resume: existingSession ?? null });
|
||||
|
||||
const proc = Bun.spawn(args, {
|
||||
stdin: 'pipe',
|
||||
stdout: 'pipe',
|
||||
stderr: 'pipe',
|
||||
});
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
try { proc.kill(); } catch { /* already dead */ }
|
||||
}, SEND_TIMEOUT_MS);
|
||||
|
||||
try {
|
||||
const [stdout, stderr] = await Promise.all([
|
||||
new Response(proc.stdout).text(),
|
||||
new Response(proc.stderr).text(),
|
||||
]);
|
||||
|
||||
const exitCode = await proc.exited;
|
||||
clearTimeout(timeout);
|
||||
|
||||
if (stderr.trim()) {
|
||||
logger.info('Claude Code stderr', { text: stderr.trim().slice(0, 500) });
|
||||
}
|
||||
|
||||
if (exitCode !== 0 && !stdout.trim()) {
|
||||
throw new Error(`claude exited with code ${exitCode}: ${stderr.trim().slice(0, 200)}`);
|
||||
}
|
||||
|
||||
// Parse JSON output
|
||||
let output: ClaudeCodeOutput;
|
||||
try {
|
||||
output = JSON.parse(stdout) as ClaudeCodeOutput;
|
||||
} catch {
|
||||
// Non-JSON output — treat raw stdout as result text
|
||||
return {
|
||||
text: stdout.trim() || '(no response)',
|
||||
sessionId: sessionKey,
|
||||
model: 'claude-code',
|
||||
cost: { inputTokens: 0, outputTokens: 0, totalUSD: 0 },
|
||||
};
|
||||
}
|
||||
|
||||
if (output.is_error) {
|
||||
throw new Error(output.result || 'Claude Code returned an error');
|
||||
}
|
||||
|
||||
// Store session for --resume on next message
|
||||
if (output.session_id) {
|
||||
claudeCodeSessions.set(sessionKey, output.session_id);
|
||||
}
|
||||
|
||||
const cost: MessageCost = {
|
||||
inputTokens: output.input_tokens ?? 0,
|
||||
outputTokens: output.output_tokens ?? 0,
|
||||
totalUSD: output.cost_usd ?? 0,
|
||||
};
|
||||
|
||||
logger.info('Claude Code result', {
|
||||
sessionKey,
|
||||
sessionId: output.session_id,
|
||||
cost: cost.totalUSD,
|
||||
tokens: cost.inputTokens + cost.outputTokens,
|
||||
});
|
||||
|
||||
return {
|
||||
text: output.result || '(no response)',
|
||||
sessionId: sessionKey,
|
||||
model: 'claude-code',
|
||||
cost,
|
||||
};
|
||||
} catch (err) {
|
||||
clearTimeout(timeout);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user