Compare commits

...
Author SHA1 Message Date
Saoud Rizwan 8cf5a3c412 fix: bake resolved config path into hook scripts at install time
Uses getDocumentsPath() to resolve the actual Documents folder,
writes webhook_config.json there, and embeds the full resolved
path in the generated hook scripts. This way hooks work correctly
even if Documents is in a non-standard location (OneDrive, etc.).
2026-02-25 14:09:14 -08:00
Saoud Rizwan fb6c499c86 fix: use ~/.cline/ for webhook config instead of ~/Documents/Cline/
The Documents folder can be in a non-standard location on Windows
(OneDrive, different drive, etc.). Using ~/.cline/ is stable and
platform-independent, matching getClineHomePath().
2026-02-25 14:06:28 -08:00
Saoud Rizwan 4266221a07 Pass spec file path to agent instead of reading contents inline 2026-02-25 13:59:47 -08:00
Saoud Rizwan 64026a6f1a refactor: deduplicate hook scripts with shared template function 2026-02-25 13:55:48 -08:00
Saoud Rizwan e706e83362 feat: add /lg-task URI handler for LG CNS dashboard integration
Adds a new URI path (/lg-task) that enables the LG CNS web dashboard
to launch Cline tasks directly from the browser. The dashboard opens
a URI with prompt-file, webhook-url, and webhook-token parameters.

The handler reads the spec file from disk, installs PowerShell webhook
hook scripts to ~/Documents/Cline/Hooks/, writes webhook config, and
starts the task. The hooks POST progress events (task_started,
tool_executed, task_completed) back to the dashboard as Cline works.

All webhook integration code lives in src/services/lg-cns-integration/
for clear separation of concerns.
2026-02-25 13:53:24 -08:00
3 changed files with 153 additions and 2 deletions
+3 -2
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 { SharedUriHandler, TASK_URI_PATH } from "./services/uri/SharedUriHandler"
import { LG_TASK_URI_PATH, SharedUriHandler, TASK_URI_PATH } from "./services/uri/SharedUriHandler"
import { ShowMessageType } from "./shared/proto/host/window"
import { fileExistsAtPath } from "./utils/fs"
@@ -160,7 +160,8 @@ export async function activate(context: vscode.ExtensionContext) {
const handleUri = async (uri: vscode.Uri) => {
const url = decodeURIComponent(uri.toString())
const isTaskUri = getUriPath(url) === TASK_URI_PATH
const uriPath = getUriPath(url)
const isTaskUri = uriPath === TASK_URI_PATH || uriPath === LG_TASK_URI_PATH
if (isTaskUri) {
await openClineSidebarForTaskUri()
@@ -0,0 +1,126 @@
import fs from "fs/promises"
import path from "path"
import { ensureHooksDirectoryExists, getDocumentsPath } from "@/core/storage/disk"
import { Logger } from "@/shared/services/Logger"
/**
* Sets up webhook hooks for LG CNS dashboard integration.
*
* Writes a webhook config file and installs PowerShell hook scripts
* to ~/Documents/Cline/Hooks/ that POST progress events back to
* the LG web dashboard.
*/
export async function setupLgWebhooks(webhookUrl: string, webhookToken: string): Promise<void> {
const documentsPath = await getDocumentsPath()
const clineDir = path.join(documentsPath, "Cline")
await fs.mkdir(clineDir, { recursive: true })
// Write webhook config next to hooks in ~/Documents/Cline/
const configPath = path.join(clineDir, "webhook_config.json")
const config = {
webhook_url: webhookUrl,
webhook_token: webhookToken,
created_at: new Date().toISOString(),
}
await fs.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8")
// Write hook scripts with the resolved config path baked in,
// so hooks don't need to guess the Documents folder location at runtime
const hooksDir = await ensureHooksDirectoryExists()
await Promise.all([
fs.writeFile(path.join(hooksDir, "TaskStart"), makeHookScript("task_started", TASK_START_DATA, configPath), "utf-8"),
fs.writeFile(
path.join(hooksDir, "PostToolUse"),
makeHookScript("tool_executed", POST_TOOL_USE_DATA, configPath),
"utf-8",
),
fs.writeFile(
path.join(hooksDir, "TaskComplete"),
makeHookScript("task_completed", TASK_COMPLETE_DATA, configPath),
"utf-8",
),
])
Logger.info(`LG webhooks configured: ${webhookUrl}`)
}
// -- Hook script generation --
/**
* Generates a PowerShell hook script that reads stdin JSON from Cline,
* extracts event-specific data, and POSTs a webhook event.
*
* The config path is resolved at generation time and embedded in the script,
* so hooks work correctly even if the Documents folder is in a non-standard
* location (e.g., OneDrive, different drive on Windows).
*/
function makeHookScript(eventName: string, dataExtraction: string, configPath: string): string {
// Escape backslashes for embedding in the PowerShell string
const escapedConfigPath = configPath.replace(/\\/g, "\\\\")
return `#!/usr/bin/env pwsh
$ErrorActionPreference = "SilentlyContinue"
# Read hook input from stdin (Cline sends JSON)
$hookInput = $input | Out-String | ConvertFrom-Json
# Load webhook config (path resolved at install time by the Cline extension)
$configPath = "${escapedConfigPath}"
if (-not (Test-Path $configPath)) {
Write-Output '{"cancel": false}'
exit 0
}
$config = Get-Content $configPath -Raw | ConvertFrom-Json
# Extract event-specific data
${dataExtraction}
$payload = @{
event = "${eventName}"
timestamp = (Get-Date).ToUniversalTime().ToString("o")
data = $eventData
} | ConvertTo-Json -Depth 5 -Compress
# POST to webhook (fire-and-forget, 5 second timeout)
try {
$headers = @{
"Content-Type" = "application/json"
"Authorization" = "Bearer $($config.webhook_token)"
}
Invoke-RestMethod -Uri $config.webhook_url -Method Post -Body $payload -Headers $headers -TimeoutSec 5 | Out-Null
} catch {}
Write-Output '{"cancel": false}'
`
}
// Each extraction block sets $eventData from $hookInput
const TASK_START_DATA = `$taskMetadata = @{}
if ($hookInput.taskStart -and $hookInput.taskStart.taskMetadata) {
$taskMetadata = $hookInput.taskStart.taskMetadata
}
$eventData = @{
task_id = if ($hookInput.taskId) { $hookInput.taskId } else { "" }
cline_version = if ($hookInput.clineVersion) { $hookInput.clineVersion } else { "" }
workspace_roots = if ($hookInput.workspaceRoots) { $hookInput.workspaceRoots } else { @() }
task_metadata = $taskMetadata
}`
const POST_TOOL_USE_DATA = `$toolData = if ($hookInput.postToolUse) { $hookInput.postToolUse } else { @{} }
$eventData = @{
task_id = if ($hookInput.taskId) { $hookInput.taskId } else { "" }
tool_name = if ($toolData.toolName) { $toolData.toolName } else { "" }
parameters = if ($toolData.parameters) { $toolData.parameters } else { @{} }
success = if ($null -ne $toolData.success) { $toolData.success } else { $false }
execution_time_ms = if ($toolData.executionTimeMs) { $toolData.executionTimeMs } else { 0 }
}`
const TASK_COMPLETE_DATA = `$taskData = if ($hookInput.taskComplete) { $hookInput.taskComplete } else { @{} }
$taskMetadata = if ($taskData.taskMetadata) { $taskData.taskMetadata } else { @{} }
$eventData = @{
task_id = if ($hookInput.taskId) { $hookInput.taskId } else { "" }
task_metadata = $taskMetadata
}`
+24
View File
@@ -1,7 +1,9 @@
import { WebviewProvider } from "@/core/webview"
import { setupLgWebhooks } 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
@@ -92,6 +94,28 @@ 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")
if (!promptFile) {
Logger.warn("SharedUriHandler: Missing prompt-file parameter for LG task creation")
return false
}
const webhookUrl = query.get("webhook-url")
const webhookToken = query.get("webhook-token")
if (webhookUrl && webhookToken) {
await setupLgWebhooks(webhookUrl, webhookToken)
}
const prompt =
`The following file contains a development specification for you to implement: ${promptFile}\n\n` +
`Start by reading this file. As you work through the task, re-read the file whenever its contents ` +
`are lost during context compaction (when the context window limit is reached), so you can keep ` +
`track of your progress against the spec requirements.`
await visibleWebview.controller.handleTaskCreation(prompt)
return true
}
// Match /mcp-auth/callback/{hash}
case path.match(/^\/mcp-auth\/callback\/[^/]+$/)?.input: {
const serverHash = path.split("/").pop()