Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/hooks/useSSEStream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ interface ToolUseInfo {
interface ToolResultInfo {
tool_use_id: string;
content: string;
is_error?: boolean;
}

export interface SSECallbacks {
Expand Down Expand Up @@ -73,6 +74,7 @@ function handleSSEEvent(
callbacks.onToolResult({
tool_use_id: resultData.tool_use_id,
content: resultData.content,
is_error: resultData.is_error,
});
} catch {
// skip malformed tool_result data
Expand Down
10 changes: 9 additions & 1 deletion src/lib/bridge/permission-broker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import type { PermissionUpdate } from '@anthropic-ai/claude-agent-sdk';
import type { ChannelAddress, OutboundMessage } from './types';
import type { BaseChannelAdapter } from './channel-adapter';
import { deliver } from './delivery-layer';
import { insertPermissionLink, getPermissionLink, markPermissionLinkResolved, getSession, getDb } from '../db';
import { insertPermissionLink, getPermissionLink, markPermissionLinkResolved, getSession, getDb, getSetting } from '../db';
import { resolvePendingPermission } from '../permission-registry';
import { escapeHtml } from './adapters/telegram-utils';

Expand All @@ -36,6 +36,14 @@ export async function forwardPermissionRequest(
suggestions?: unknown[],
replyToMessageId?: string,
): Promise<void> {
// Check if auto-approval is enabled globally — auto-approve without IM notification
const globalAutoApprove = getSetting('dangerously_skip_permissions') === 'true';
if (globalAutoApprove) {
console.log(`[bridge] Auto-approved permission ${permissionRequestId} (tool=${toolName}) due to global auto-approval setting`);
resolvePendingPermission(permissionRequestId, { behavior: 'allow' });
return;
}

// Check if this session uses full_access permission profile — auto-approve without IM notification
if (sessionId) {
const session = getSession(sessionId);
Expand Down
47 changes: 33 additions & 14 deletions src/lib/claude-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { registerPendingPermission } from './permission-registry';
import { registerConversation, unregisterConversation } from './conversation-registry';
import { captureCapabilities, setCachedPlugins } from './agent-sdk-capabilities';
import { getSetting, updateSdkSessionId, createPermissionRequest } from './db';
// Auto-approval is now handled at the CodePilot level, not via SDK bypassPermissions
import { resolveForClaudeCode, toClaudeCodeEnv } from './provider-resolver';
import { findClaudeBinary, findGitBash, getExpandedPath, invalidateClaudePathCache } from './platform';
import { notifyPermissionRequest, notifyGeneric } from './telegram-bot';
Expand Down Expand Up @@ -316,14 +317,26 @@ export async function generateTextViaSdk(params: {
const queryOptions: Options = {
cwd: os.homedir(),
abortController,
permissionMode: 'bypassPermissions',
allowDangerouslySkipPermissions: true,
permissionMode: 'acceptEdits',
env: sanitizeEnv(sdkEnv),
settingSources: resolved.settingSources as Options['settingSources'],
systemPrompt: params.system,
maxTurns: 1,
};

// Add auto-approval handler for this simple query
const globalAutoApprove = getSetting('dangerously_skip_permissions') === 'true';
if (globalAutoApprove) {
queryOptions.canUseTool = async (toolName, _input, opts) => {
console.log(`[claude-client] Auto-approved ${toolName} (auto-approval enabled)`);
return {
behavior: 'allow',
updatedInput: _input,
...(opts.suggestions ? { updatedPermissions: opts.suggestions } : {}),
};
};
}

if (params.model) {
queryOptions.model = params.model;
}
Expand Down Expand Up @@ -445,18 +458,15 @@ export function streamClaude(options: ClaudeStreamOptions): ReadableStream<strin
console.warn('[claude-client] No API key found: no active provider, no legacy settings, and no ANTHROPIC_API_KEY/ANTHROPIC_AUTH_TOKEN in environment');
}


// Check if dangerously_skip_permissions is enabled globally or per-session
const globalSkip = getSetting('dangerously_skip_permissions') === 'true';
const skipPermissions = globalSkip || !!sessionBypassPermissions;
// Check if auto-approval is enabled globally or per-session
const globalAutoApprove = getSetting('dangerously_skip_permissions') === 'true';
const shouldAutoApprove = globalAutoApprove || !!sessionBypassPermissions;

const queryOptions: Options = {
cwd: workingDirectory || os.homedir(),
abortController,
includePartialMessages: true,
permissionMode: skipPermissions
? 'bypassPermissions'
: ((permissionMode as Options['permissionMode']) || 'acceptEdits'),
permissionMode: (permissionMode as Options['permissionMode']) || 'acceptEdits',
env: sanitizeEnv(sdkEnv),
// Load settings so the SDK behaves like the CLI (tool permissions,
// CLAUDE.md, etc.). When an active provider is configured in
Expand All @@ -466,10 +476,6 @@ export function streamClaude(options: ClaudeStreamOptions): ReadableStream<strin
settingSources: resolved.settingSources as Options['settingSources'],
};

if (skipPermissions) {
queryOptions.allowDangerouslySkipPermissions = true;
}

// Find claude binary for packaged app where PATH is limited.
// On Windows, npm installs Claude CLI as a .cmd wrapper which cannot
// be spawned directly without shell:true. Parse the wrapper to
Expand Down Expand Up @@ -594,10 +600,20 @@ export function streamClaude(options: ClaudeStreamOptions): ReadableStream<strin
queryOptions.resume = sdkSessionId;
}

// Permission handler: sends SSE event and waits for user response
// Permission handler: auto-approve if enabled, or sends SSE event and waits for user response
queryOptions.canUseTool = async (toolName, input, opts) => {
const permissionRequestId = `perm-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;

// If auto-approval is enabled, approve immediately without user interaction
if (shouldAutoApprove) {
console.log(`[claude-client] Auto-approved ${toolName} (auto-approval enabled)`);
return {
behavior: 'allow',
updatedInput: input,
...(opts.suggestions ? { updatedPermissions: opts.suggestions } : {}),
};
}

const permEvent: PermissionRequestEvent = {
permissionRequestId,
toolName,
Expand Down Expand Up @@ -888,6 +904,9 @@ export function streamClaude(options: ClaudeStreamOptions): ReadableStream<strin
is_error: block.is_error || false,
}),
}));
if (block.is_error) {
console.warn(`[claude-client] Tool ${block.tool_use_id} failed: ${resultContent}`);
}

// Deferred TodoWrite sync: only emit task_update after successful execution
if (!block.is_error && pendingTodoWrites.has(block.tool_use_id)) {
Expand Down
1 change: 1 addition & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -910,6 +910,7 @@ export interface ToolUseInfo {
export interface ToolResultInfo {
tool_use_id: string;
content: string;
is_error?: boolean;
}

export type StreamPhase = 'active' | 'completed' | 'error' | 'stopped';
Expand Down
183 changes: 183 additions & 0 deletions start.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
#!/bin/bash

# CodePilot 后台启动脚本

APP_DIR="$(cd "$(dirname "$0")" && pwd)"
LOG_FILE="$APP_DIR/app.log"
PID_FILE="$APP_DIR/app.pid"

# 显示菜单
show_menu() {
clear
echo "========================================"
echo " CodePilot 管理菜单"
echo "========================================"
echo ""

# 检查状态
if [ -f "$PID_FILE" ] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then
echo " 当前状态: 运行中 (PID: $(cat "$PID_FILE"))"
else
echo " 当前状态: 未运行"
fi

echo ""
echo " [1] 启动服务"
echo " [2] 停止服务"
echo " [3] 重启服务"
echo " [4] 查看实时日志"
echo " [5] 查看状态"
echo " [6] 查看日志 (最后50行)"
echo " [0] 退出"
echo ""
echo "========================================"
}

# 启动服务
do_start() {
if [ -f "$PID_FILE" ] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then
echo "服务已在运行 (PID: $(cat "$PID_FILE"))"
return
fi

cd "$APP_DIR" || exit 1
nohup npm run start > "$LOG_FILE" 2>&1 &
echo $! > "$PID_FILE"
echo "启动成功,PID: $!"
}

# 停止服务
do_stop() {
# 先尝试停止 PID 文件中的进程
if [ -f "$PID_FILE" ]; then
PID=$(cat "$PID_FILE")
if kill -0 "$PID" 2>/dev/null; then
kill "$PID" 2>/dev/null
sleep 1
# 如果进程还在,强制杀掉
if kill -0 "$PID" 2>/dev/null; then
kill -9 "$PID" 2>/dev/null
fi
echo "已停止 (PID: $PID)"
else
echo "进程不存在"
fi
rm -f "$PID_FILE"
else
echo "服务未运行"
fi

# 强制清理占用端口 3000 的进程
if ss -tlnp 2>/dev/null | grep -q ":3000 "; then
echo "清理残留进程..."
fuser -k 3000/tcp 2>/dev/null
sleep 1
# 再次强制清理
lsof -ti:3000 | xargs -r kill -9 2>/dev/null
fi
}

# 查看状态
do_status() {
if [ -f "$PID_FILE" ] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then
echo "运行中 (PID: $(cat "$PID_FILE"))"
else
echo "未运行"
fi
}

# 查看日志
do_log() {
tail -f "$LOG_FILE"
}

# 菜单模式
menu_mode() {
while true; do
show_menu
read -p "请选择操作 [0-6]: " choice

case $choice in
1)
echo ""
do_start
read -p "按回车键继续..."
;;
2)
echo ""
do_stop
read -p "按回车键继续..."
;;
3)
echo ""
do_stop
sleep 1
do_start
read -p "按回车键继续..."
;;
4)
echo ""
echo "按 Ctrl+C 退出日志查看"
do_log
;;
5)
echo ""
do_status
read -p "按回车键继续..."
;;
6)
echo ""
if [ -f "$LOG_FILE" ]; then
tail -n 50 "$LOG_FILE"
else
echo "暂无日志文件"
fi
read -p "按回车键继续..."
;;
0)
echo ""
echo "再见!"
exit 0
;;
*)
echo ""
echo "无效选项"
read -p "按回车键继续..."
;;
esac
done
}

# 命令行模式
case "${1:-menu}" in
start)
do_start
;;
stop)
do_stop
;;
restart)
do_stop
sleep 1
do_start
;;
status)
do_status
;;
log)
do_log
;;
menu)
menu_mode
;;
*)
echo "用法: $0 {start|stop|restart|status|log|menu}"
echo ""
echo " start - 后台启动服务"
echo " stop - 停止服务"
echo " restart- 重启服务"
echo " status - 查看运行状态"
echo " log - 查看实时日志"
echo " menu - 显示交互菜单(默认)"
;;
esac