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:
2026-03-02 20:00:45 +00:00
co-authored by Claude Opus 4.6
parent 192ae63c0e
commit 0068244356
20 changed files with 1168 additions and 73 deletions
+139
View File
@@ -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;
}
}