mirror of
https://github.com/cline/cline.git
synced 2026-09-15 13:02:17 +08:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e4c4cc6260 |
+2
-3
@@ -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 ?? {},
|
||||
},
|
||||
}))
|
||||
`
|
||||
@@ -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"))
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user