diff --git a/extension/package-lock.json b/extension/package-lock.json index 1b36257..8af4ff4 100644 --- a/extension/package-lock.json +++ b/extension/package-lock.json @@ -1,12 +1,12 @@ { "name": "gravity-bridge", - "version": "0.5.59", + "version": "0.5.60", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "gravity-bridge", - "version": "0.5.59", + "version": "0.5.60", "dependencies": { "cheerio": "^1.2.0", "ws": "^8.19.0" diff --git a/extension/package.json b/extension/package.json index fa3759b..d532a47 100644 --- a/extension/package.json +++ b/extension/package.json @@ -2,7 +2,7 @@ "name": "gravity-bridge", "displayName": "Gravity Bridge", "description": "Discord-based unified approval system for Antigravity AI interactions.", - "version": "0.5.59", + "version": "0.5.60", "publisher": "variet", "engines": { "vscode": "^1.100.0" diff --git a/extension/src/http-bridge.ts b/extension/src/http-bridge.ts index 036adb8..55d57a7 100644 --- a/extension/src/http-bridge.ts +++ b/extension/src/http-bridge.ts @@ -266,6 +266,64 @@ function _handlePending(req: any, res: any, ctx: HttpBridgeContext) { const GENERIC_BTN_RE = /^(?:Always\s*)?(?:Run|Allow|Accept|Approve)$/i; let enrichedCmd = rawCmd; let enrichedDesc = rawDesc; + + // v19: "Always run" auto-approve — MUST run BEFORE any filter can reject it + // Detects from rawCmd OR from buttons array (Observer may detect sibling first) + let alwaysRunDetected = /^Always\s+run$/i.test(rawCmd); + let alwaysRunBtnIndex = alwaysRunDetected ? 0 : -1; + if (!alwaysRunDetected && Array.isArray(data.buttons)) { + for (let bi = 0; bi < data.buttons.length; bi++) { + if (/^Always\s+run$/i.test((data.buttons[bi].text || '').trim())) { + alwaysRunDetected = true; + alwaysRunBtnIndex = bi; + break; + } + } + } + if (alwaysRunDetected) { + // Try enrichment for better Discord display text + let displayCmd = rawCmd; + if (GENERIC_BTN_RE.test(rawCmd) && rawDesc.length > 10 && rawDesc !== rawCmd) { + const promptMatch = rawDesc.match(/[>»]\s*(.+)/); + if (promptMatch && promptMatch[1].trim().length > 3) { + displayCmd = promptMatch[1].trim().substring(0, 200); + } + } + const rid = data.request_id || Date.now().toString(); + ctx.logToFile(`[HTTP] AUTO-APPROVE "Always run" (btnIdx=${alwaysRunBtnIndex}): cmd="${displayCmd.substring(0, 80)}"`); + // Write response file so observer's pollResponseGroup picks it up and clicks the button + const responseDir = path.join(ctx.bridgePath, 'response'); + if (!fs.existsSync(responseDir)) { + fs.mkdirSync(responseDir, { recursive: true }); + } + const respPayload = { + request_id: rid, + approved: true, + button_index: alwaysRunBtnIndex >= 0 ? alwaysRunBtnIndex : 0, + step_type: data.step_type || 'command', + project_name: ctx.projectName, + }; + fs.writeFileSync( + path.join(responseDir, `${rid}.json`), + JSON.stringify(respPayload), + 'utf-8' + ); + // Notify Discord (non-interactive "자동 승인" embed) + if (ctx.wsBridge && ctx.wsBridge.isConnected()) { + ctx.wsBridge.sendPending({ + request_id: rid, + command: displayCmd, + description: rawDesc ? `[${rawCmd}] ${rawDesc}` : rawCmd, + step_type: data.step_type || 'command', + status: 'auto_approved', + buttons: data.buttons, + project_name: ctx.projectName, + }); + } + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ ok: true, request_id: rid, auto_approved: true })); + return; + } if (GENERIC_BTN_RE.test(rawCmd) && rawDesc.length > 10 && rawDesc !== rawCmd) { // Extract the actual command from description (often includes terminal prompt) // Pattern: "…\project_name > actual_command" @@ -361,55 +419,8 @@ function _handlePending(req: any, res: any, ctx: HttpBridgeContext) { buttons: data.buttons, step_index: ctx.lastPendingStepIndex >= 0 ? ctx.lastPendingStepIndex : undefined, }; - // v17+: "Always run" auto-approve — click button immediately without Discord roundtrip - // Check rawCmd first, then fall back to scanning the buttons array - // (Observer may detect "Run" first while "Always run" is a sibling button) - let alwaysRunIndex = -1; - if (/^Always\s+run$/i.test(rawCmd)) { - alwaysRunIndex = 0; - } else if (Array.isArray(data.buttons)) { - for (let bi = 0; bi < data.buttons.length; bi++) { - if (/^Always\s+run$/i.test((data.buttons[bi].text || '').trim())) { - alwaysRunIndex = bi; - break; - } - } - } - if (alwaysRunIndex >= 0) { - ctx.logToFile(`[HTTP] AUTO-APPROVE "Always run" (btnIdx=${alwaysRunIndex}): enriched="${enrichedCmd.substring(0, 80)}"`); - // Write response file so observer's pollResponseGroup picks it up and clicks the button - const responseDir = path.join(ctx.bridgePath, 'response'); - if (!fs.existsSync(responseDir)) { - fs.mkdirSync(responseDir, { recursive: true }); - } - const respPayload = { - request_id: rid, - approved: true, - button_index: alwaysRunIndex, - step_type: data.step_type || 'command', - project_name: ctx.projectName, - }; - fs.writeFileSync( - path.join(responseDir, `${rid}.json`), - JSON.stringify(respPayload), - 'utf-8' - ); - // Notify Discord (non-interactive "자동 승인" embed) - if (ctx.wsBridge && ctx.wsBridge.isConnected()) { - ctx.wsBridge.sendPending({ - request_id: rid, - command: enrichedCmd || rawCmd, - description: enrichedDesc, - step_type: pending.step_type, - status: 'auto_approved', - buttons: pending.buttons, - project_name: ctx.projectName, - }); - } - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ ok: true, request_id: rid, auto_approved: true })); - return; - } + // v19: "Always run" auto-approve was already handled above (before filter chain) + // No need for duplicate check here. // File permission: inject multi-choice buttons const cmdLower = enrichedCmd.toLowerCase(); if (cmdLower.includes('allow') && !pending.buttons) {