Compare commits

...
Author SHA1 Message Date
Saoud Rizwan e4c4cc6260 Revert "feat(uri): add /lg-task webhook integration for LG dashboard flow (#1…"
This reverts commit a7ba25d6e1.
2026-05-22 10:18:58 -07:00
4 changed files with 2 additions and 405 deletions
+2 -3
View File
@@ -53,7 +53,7 @@ import { ExtensionRegistryInfo } from "./registry"
import { AuthService } from "./services/auth/AuthService"
import { LogoutReason } from "./services/auth/types"
import { telemetryService } from "./services/telemetry"
import { LG_TASK_URI_PATH, SharedUriHandler, TASK_URI_PATH } from "./services/uri/SharedUriHandler"
import { SharedUriHandler, TASK_URI_PATH } from "./services/uri/SharedUriHandler"
import { ShowMessageType } from "./shared/proto/host/window"
import { fileExistsAtPath } from "./utils/fs"
@@ -160,8 +160,7 @@ export async function activate(context: vscode.ExtensionContext) {
const handleUri = async (uri: vscode.Uri) => {
const url = decodeURIComponent(uri.toString())
const uriPath = getUriPath(url)
const isTaskUri = uriPath === TASK_URI_PATH || uriPath === LG_TASK_URI_PATH
const isTaskUri = getUriPath(url) === TASK_URI_PATH
if (isTaskUri) {
await openClineSidebarForTaskUri()
@@ -1,322 +0,0 @@
import fs from "fs/promises"
import path from "path"
import { ensureHooksDirectoryExists, getDocumentsPath } from "@/core/storage/disk"
type LgHookScript = {
fileName: string
content: string
mode?: number
}
export async function writeLgWebhookConfig(webhookUrl: string, webhookToken: string): Promise<void> {
const documentsPath = await getDocumentsPath()
const clineDir = path.join(documentsPath, "Cline")
const configPath = path.join(clineDir, "webhook_config.json")
await fs.mkdir(clineDir, { recursive: true })
await fs.writeFile(
configPath,
JSON.stringify(
{
webhook_url: webhookUrl,
webhook_token: webhookToken,
created_at: new Date().toISOString(),
},
null,
2,
),
"utf-8",
)
}
export async function writeLgWebhookHooks(): Promise<void> {
const hooksDir = await ensureHooksDirectoryExists()
const hooks = getLgWebhookHookScripts()
for (const hook of hooks) {
const hookPath = path.join(hooksDir, hook.fileName)
await fs.writeFile(hookPath, hook.content, "utf-8")
if (hook.mode !== undefined) {
await fs.chmod(hookPath, hook.mode)
}
}
}
function getLgWebhookHookScripts(): LgHookScript[] {
if (process.platform === "win32") {
return [
{ fileName: "TaskStart.ps1", content: TASK_START_POWERSHELL },
{ fileName: "PostToolUse.ps1", content: POST_TOOL_USE_POWERSHELL },
{ fileName: "TaskComplete.ps1", content: TASK_COMPLETE_POWERSHELL },
]
}
return [
{ fileName: "TaskStart", content: TASK_START_NODE, mode: 0o755 },
{ fileName: "PostToolUse", content: POST_TOOL_USE_NODE, mode: 0o755 },
{ fileName: "TaskComplete", content: TASK_COMPLETE_NODE, mode: 0o755 },
]
}
const TASK_START_POWERSHELL = `try {
$rawInput = [Console]::In.ReadToEnd()
$inputData = $null
if ($rawInput) {
$inputData = $rawInput | ConvertFrom-Json -Depth 100
}
} catch {
$inputData = $null
}
try {
$documentsPath = [System.Environment]::GetFolderPath([System.Environment+SpecialFolder]::MyDocuments)
if (-not $documentsPath) {
$documentsPath = Join-Path $HOME "Documents"
}
$configPath = Join-Path (Join-Path $documentsPath "Cline") "webhook_config.json"
if (-not (Test-Path $configPath)) {
@{ cancel = $false } | ConvertTo-Json -Compress
exit 0
}
$config = Get-Content $configPath -Raw | ConvertFrom-Json -Depth 100
if (-not $config.webhook_url -or -not $config.webhook_token) {
@{ cancel = $false } | ConvertTo-Json -Compress
exit 0
}
$workspaceRoots = @()
if ($inputData -and $null -ne $inputData.workspaceRoots) {
$workspaceRoots = $inputData.workspaceRoots
}
$taskMetadata = @{}
if ($inputData -and $inputData.taskStart -and $null -ne $inputData.taskStart.taskMetadata) {
$taskMetadata = $inputData.taskStart.taskMetadata
}
$payload = @{
event = "task_started"
timestamp = (Get-Date).ToUniversalTime().ToString("o")
data = @{
task_id = if ($inputData) { $inputData.taskId } else { $null }
cline_version = if ($inputData) { $inputData.clineVersion } else { $null }
workspace_roots = $workspaceRoots
task_metadata = $taskMetadata
}
}
$headers = @{
Authorization = "Bearer $($config.webhook_token)"
"Content-Type" = "application/json"
}
Invoke-RestMethod -Method Post -Uri $config.webhook_url -Headers $headers -Body ($payload | ConvertTo-Json -Depth 100) -ContentType "application/json" -TimeoutSec 5 | Out-Null
} catch {}
@{ cancel = $false } | ConvertTo-Json -Compress
`
const POST_TOOL_USE_POWERSHELL = `try {
$rawInput = [Console]::In.ReadToEnd()
$inputData = $null
if ($rawInput) {
$inputData = $rawInput | ConvertFrom-Json -Depth 100
}
} catch {
$inputData = $null
}
try {
$documentsPath = [System.Environment]::GetFolderPath([System.Environment+SpecialFolder]::MyDocuments)
if (-not $documentsPath) {
$documentsPath = Join-Path $HOME "Documents"
}
$configPath = Join-Path (Join-Path $documentsPath "Cline") "webhook_config.json"
if (-not (Test-Path $configPath)) {
@{ cancel = $false } | ConvertTo-Json -Compress
exit 0
}
$config = Get-Content $configPath -Raw | ConvertFrom-Json -Depth 100
if (-not $config.webhook_url -or -not $config.webhook_token) {
@{ cancel = $false } | ConvertTo-Json -Compress
exit 0
}
$toolData = $null
if ($inputData -and $inputData.postToolUse) {
$toolData = $inputData.postToolUse
}
$payload = @{
event = "tool_executed"
timestamp = (Get-Date).ToUniversalTime().ToString("o")
data = @{
task_id = if ($inputData) { $inputData.taskId } else { $null }
tool_name = if ($toolData) { $toolData.toolName } else { $null }
parameters = if ($toolData -and $null -ne $toolData.parameters) { $toolData.parameters } else { @{} }
success = if ($toolData) { [bool]$toolData.success } else { $false }
execution_time_ms = if ($toolData) { $toolData.executionTimeMs } else { $null }
}
}
$headers = @{
Authorization = "Bearer $($config.webhook_token)"
"Content-Type" = "application/json"
}
Invoke-RestMethod -Method Post -Uri $config.webhook_url -Headers $headers -Body ($payload | ConvertTo-Json -Depth 100) -ContentType "application/json" -TimeoutSec 5 | Out-Null
} catch {}
@{ cancel = $false } | ConvertTo-Json -Compress
`
const TASK_COMPLETE_POWERSHELL = `try {
$rawInput = [Console]::In.ReadToEnd()
$inputData = $null
if ($rawInput) {
$inputData = $rawInput | ConvertFrom-Json -Depth 100
}
} catch {
$inputData = $null
}
try {
$documentsPath = [System.Environment]::GetFolderPath([System.Environment+SpecialFolder]::MyDocuments)
if (-not $documentsPath) {
$documentsPath = Join-Path $HOME "Documents"
}
$configPath = Join-Path (Join-Path $documentsPath "Cline") "webhook_config.json"
if (-not (Test-Path $configPath)) {
@{ cancel = $false } | ConvertTo-Json -Compress
exit 0
}
$config = Get-Content $configPath -Raw | ConvertFrom-Json -Depth 100
if (-not $config.webhook_url -or -not $config.webhook_token) {
@{ cancel = $false } | ConvertTo-Json -Compress
exit 0
}
$taskMetadata = @{}
if ($inputData -and $inputData.taskComplete -and $null -ne $inputData.taskComplete.taskMetadata) {
$taskMetadata = $inputData.taskComplete.taskMetadata
}
$payload = @{
event = "task_completed"
timestamp = (Get-Date).ToUniversalTime().ToString("o")
data = @{
task_id = if ($inputData) { $inputData.taskId } else { $null }
task_metadata = $taskMetadata
}
}
$headers = @{
Authorization = "Bearer $($config.webhook_token)"
"Content-Type" = "application/json"
}
Invoke-RestMethod -Method Post -Uri $config.webhook_url -Headers $headers -Body ($payload | ConvertTo-Json -Depth 100) -ContentType "application/json" -TimeoutSec 5 | Out-Null
} catch {}
@{ cancel = $false } | ConvertTo-Json -Compress
`
const NODE_HOOK_SHARED = `#!/usr/bin/env node
const fs = require("fs/promises")
const os = require("os")
const path = require("path")
async function readConfig() {
const configPath = path.join(os.homedir(), "Documents", "Cline", "webhook_config.json")
try {
const rawConfig = await fs.readFile(configPath, "utf-8")
const config = JSON.parse(rawConfig)
if (!config.webhook_url || !config.webhook_token) {
return null
}
return config
} catch {
return null
}
}
async function postEvent(config, payload) {
let timeout
try {
const controller = new AbortController()
timeout = setTimeout(() => controller.abort(), 5000)
await fetch(config.webhook_url, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: \`Bearer \${config.webhook_token}\`,
},
body: JSON.stringify(payload),
signal: controller.signal,
})
} catch {} finally {
if (timeout) {
clearTimeout(timeout)
}
}
}
async function main(buildPayload) {
let input = {}
try {
const rawInput = await fs.readFile(0, "utf-8")
input = rawInput ? JSON.parse(rawInput) : {}
} catch {}
const config = await readConfig()
if (config) {
await postEvent(config, buildPayload(input))
}
process.stdout.write(JSON.stringify({ cancel: false }))
}
`
const TASK_START_NODE = `${NODE_HOOK_SHARED}
main((input) => ({
event: "task_started",
timestamp: new Date().toISOString(),
data: {
task_id: input.taskId ?? null,
cline_version: input.clineVersion ?? null,
workspace_roots: input.workspaceRoots ?? [],
task_metadata: input.taskStart?.taskMetadata ?? {},
},
}))
`
const POST_TOOL_USE_NODE = `${NODE_HOOK_SHARED}
main((input) => ({
event: "tool_executed",
timestamp: new Date().toISOString(),
data: {
task_id: input.taskId ?? null,
tool_name: input.postToolUse?.toolName ?? null,
parameters: input.postToolUse?.parameters ?? {},
success: Boolean(input.postToolUse?.success),
execution_time_ms: input.postToolUse?.executionTimeMs ?? null,
},
}))
`
const TASK_COMPLETE_NODE = `${NODE_HOOK_SHARED}
main((input) => ({
event: "task_completed",
timestamp: new Date().toISOString(),
data: {
task_id: input.taskId ?? null,
task_metadata: input.taskComplete?.taskMetadata ?? {},
},
}))
`
-52
View File
@@ -1,11 +1,7 @@
import { expect } from "chai"
import * as fs from "fs/promises"
import { afterEach, beforeEach, describe, it } from "mocha"
import os from "os"
import path from "path"
import * as sinon from "sinon"
import { WebviewProvider } from "@/core/webview"
import * as webhookHooks from "@/services/lg-cns-integration/webhook-hooks"
import { Logger } from "@/shared/services/Logger"
import { ErrorService } from "../error"
import { SharedUriHandler } from "./SharedUriHandler"
@@ -14,7 +10,6 @@ describe("SharedUriHandler", () => {
let sandbox: sinon.SinonSandbox
let handleOpenRouterCallbackStub: sinon.SinonStub
let handleAuthCallbackStub: sinon.SinonStub
let handleTaskCreationStub: sinon.SinonStub
beforeEach(async () => {
sandbox = sinon.createSandbox()
@@ -39,12 +34,10 @@ describe("SharedUriHandler", () => {
handleOpenRouterCallbackStub = sandbox.stub().resolves()
handleAuthCallbackStub = sandbox.stub().resolves()
handleTaskCreationStub = sandbox.stub().resolves()
const mockWebviewProvider = {
controller: {
handleOpenRouterCallback: handleOpenRouterCallbackStub,
handleAuthCallback: handleAuthCallbackStub,
handleTaskCreation: handleTaskCreationStub,
},
} as any
sandbox.stub(WebviewProvider, "getVisibleInstance").returns(mockWebviewProvider)
@@ -112,51 +105,6 @@ describe("SharedUriHandler", () => {
})
})
describe("LG task URI handling", () => {
it("should setup webhook files and create task from prompt-file", async () => {
const webhookUrl = "https://example.com/api/updates"
const webhookToken = "token-123"
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "lg-task-uri-"))
try {
const promptFilePath = path.join(tempDir, "lg-spec.md")
await fs.writeFile(promptFilePath, "Implement user registration flow", "utf-8")
const writeConfigStub = sandbox.stub(webhookHooks, "writeLgWebhookConfig").resolves()
const writeHooksStub = sandbox.stub(webhookHooks, "writeLgWebhookHooks").resolves()
const result = await SharedUriHandler.handleUri(
`vscode://cline.cline/lg-task?prompt-file=${encodeURIComponent(
promptFilePath,
)}&webhook-url=${encodeURIComponent(webhookUrl)}&webhook-token=${encodeURIComponent(webhookToken)}`,
)
expect(result).to.be.true
sinon.assert.calledOnce(handleTaskCreationStub)
const taskPrompt = handleTaskCreationStub.firstCall.args[0] as string
expect(taskPrompt).to.contain(promptFilePath)
expect(taskPrompt).to.contain("Implement user registration flow")
expect(taskPrompt).to.contain("re-read")
sinon.assert.calledOnceWithExactly(writeConfigStub, webhookUrl, webhookToken)
sinon.assert.calledOnce(writeHooksStub)
} finally {
await fs.rm(tempDir, { recursive: true, force: true })
}
})
it("should return false when LG task parameters are missing", async () => {
const writeConfigStub = sandbox.stub(webhookHooks, "writeLgWebhookConfig").resolves()
const writeHooksStub = sandbox.stub(webhookHooks, "writeLgWebhookHooks").resolves()
const result = await SharedUriHandler.handleUri(
"vscode://cline.cline/lg-task?prompt-file=%2Ftmp%2Fspec.md&webhook-url=https%3A%2F%2Fexample.com",
)
expect(result).to.be.false
expect(handleTaskCreationStub.called).to.be.false
expect(writeConfigStub.called).to.be.false
expect(writeHooksStub.called).to.be.false
})
})
describe("Error handling", () => {
it("should catch and log errors from controller methods", async () => {
handleOpenRouterCallbackStub.rejects(new Error("Controller error"))
-28
View File
@@ -1,10 +1,7 @@
import fs from "fs/promises"
import { WebviewProvider } from "@/core/webview"
import { writeLgWebhookConfig, writeLgWebhookHooks } from "@/services/lg-cns-integration/webhook-hooks"
import { Logger } from "@/shared/services/Logger"
export const TASK_URI_PATH = "/task"
export const LG_TASK_URI_PATH = "/lg-task"
/**
* Shared URI handler that processes both VSCode URI events and HTTP server callbacks
@@ -95,31 +92,6 @@ export class SharedUriHandler {
Logger.warn("SharedUriHandler: Missing prompt parameter for task creation")
return false
}
case LG_TASK_URI_PATH: {
const promptFile = query.get("prompt-file")
const webhookUrl = query.get("webhook-url")
const webhookToken = query.get("webhook-token")
if (!promptFile || !webhookUrl || !webhookToken) {
Logger.warn("SharedUriHandler: Missing required parameters for LG task creation")
return false
}
const specContents = await fs.readFile(promptFile, "utf-8")
const prompt = [
`The following file contains the development specification you must implement: ${promptFile}`,
"",
"Start by reading that file from disk. If context compaction happens later, re-read the same file path so you can continue tracking progress against the original requirements.",
"",
"For convenience, here is the current file content:",
"",
specContents,
].join("\n")
await writeLgWebhookConfig(webhookUrl, webhookToken)
await writeLgWebhookHooks()
await visibleWebview.controller.handleTaskCreation(prompt)
return true
}
// Match /mcp-auth/callback/{hash}
case path.match(/^\/mcp-auth\/callback\/[^/]+$/)?.input: {
const serverHash = path.split("/").pop()