From 113a1d3bde4844386d648a55038006f34b812461 Mon Sep 17 00:00:00 2001 From: Bernardo Ferrari Date: Mon, 16 Feb 2026 10:32:58 -0300 Subject: [PATCH 1/8] split(vscode): foundation utils and gateway bridge --- packages/kilo-gateway/src/api/profile.ts | 30 ++++ .../kilo-gateway/src/api/remote-sessions.ts | 91 ++++++++++ packages/kilo-gateway/src/server/routes.ts | 156 +++++++++++++++++- packages/kilo-ui/src/styles/vscode-bridge.css | 31 +++- packages/kilo-vscode/src/utils/logger.ts | 54 ++++++ .../kilo-vscode/src/utils/open-external.ts | 23 +++ .../kilo-vscode/src/utils/path-security.ts | 31 ++++ packages/kilo-vscode/src/utils/telemetry.ts | 32 ++++ packages/kilo-vscode/src/utils/webview-csp.ts | 15 ++ .../tests/unit/open-external-url.test.ts | 24 +++ .../tests/unit/path-security.test.ts | 47 ++++++ .../tests/unit/webview-csp.test.ts | 19 +++ 12 files changed, 548 insertions(+), 5 deletions(-) create mode 100644 packages/kilo-gateway/src/api/remote-sessions.ts create mode 100644 packages/kilo-vscode/src/utils/logger.ts create mode 100644 packages/kilo-vscode/src/utils/open-external.ts create mode 100644 packages/kilo-vscode/src/utils/path-security.ts create mode 100644 packages/kilo-vscode/src/utils/telemetry.ts create mode 100644 packages/kilo-vscode/src/utils/webview-csp.ts create mode 100644 packages/kilo-vscode/tests/unit/open-external-url.test.ts create mode 100644 packages/kilo-vscode/tests/unit/path-security.test.ts create mode 100644 packages/kilo-vscode/tests/unit/webview-csp.test.ts diff --git a/packages/kilo-gateway/src/api/profile.ts b/packages/kilo-gateway/src/api/profile.ts index aa717a64c4..baba44c5d0 100644 --- a/packages/kilo-gateway/src/api/profile.ts +++ b/packages/kilo-gateway/src/api/profile.ts @@ -113,6 +113,36 @@ export async function fetchDefaultModel(token?: string, organizationId?: string) */ export const getKiloDefaultModel = fetchDefaultModel +/** + * Fetch extension settings for the authenticated user/organization. + * These settings include org-managed Marketplace policy fields. + */ +export async function fetchExtensionSettings( + token: string, + organizationId?: string, +): Promise<{ organization?: unknown; user?: unknown }> { + const headers: Record = { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + } + + if (organizationId) { + headers["x-kilocode-organizationid"] = organizationId + } + + const response = await fetch(`${KILO_API_BASE}/api/extension-settings`, { headers }) + + if (!response.ok) { + throw new Error(`Failed to fetch extension settings: ${response.status}`) + } + + const data = (await response.json()) as { organization?: unknown; user?: unknown } + return { + organization: data.organization, + user: data.user, + } +} + /** * Fetch both profile and balance in parallel */ diff --git a/packages/kilo-gateway/src/api/remote-sessions.ts b/packages/kilo-gateway/src/api/remote-sessions.ts new file mode 100644 index 0000000000..ee52857063 --- /dev/null +++ b/packages/kilo-gateway/src/api/remote-sessions.ts @@ -0,0 +1,91 @@ +import { KILO_API_BASE } from "./constants.js" + +export interface RemoteSessionInfo { + session_id: string + title: string + created_at: string + updated_at: string + git_url: string | null + organization_id: string | null + last_mode: string | null + last_model: string | null + cloud_agent_session_id: string | null +} + +type TrpcEnvelope = { + result?: { + data?: T + } +} + +type CliSessionsListResult = { + cliSessions?: RemoteSessionInfo[] +} + +type CliSessionWithBlobResult = { + ui_messages_blob_url?: string | null +} + +async function trpcGet( + procedure: string, + input: Record, + token: string, + organizationId?: string, +): Promise { + const url = new URL(`${KILO_API_BASE}/api/trpc/${procedure}`) + url.searchParams.set("input", JSON.stringify(input)) + + const headers: Record = { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + } + if (organizationId) { + headers["x-kilocode-organizationid"] = organizationId + } + + const response = await fetch(url, { method: "GET", headers }) + if (!response.ok) { + throw new Error(`tRPC ${procedure} failed: ${response.status}`) + } + + const payload = (await response.json()) as TrpcEnvelope + if (!payload.result || payload.result.data === undefined) { + throw new Error(`tRPC ${procedure} returned invalid payload`) + } + return payload.result.data +} + +export async function fetchRemoteSessions( + token: string, + limit = 50, + organizationId?: string, +): Promise { + const data = await trpcGet("cliSessions.list", { limit }, token, organizationId) + return Array.isArray(data.cliSessions) ? data.cliSessions : [] +} + +export async function fetchRemoteSessionMessages( + token: string, + sessionId: string, + organizationId?: string, +): Promise { + const data = await trpcGet( + "cliSessions.get", + { session_id: sessionId, include_blob_urls: true }, + token, + organizationId, + ) + + const blobUrl = data.ui_messages_blob_url + if (!blobUrl) { + return [] + } + + const response = await fetch(blobUrl) + if (!response.ok) { + throw new Error(`Failed to fetch remote session messages: ${response.status}`) + } + + const payload = (await response.json()) as unknown + return Array.isArray(payload) ? payload : [] +} diff --git a/packages/kilo-gateway/src/server/routes.ts b/packages/kilo-gateway/src/server/routes.ts index 0d5b7d371d..6416aa7e73 100644 --- a/packages/kilo-gateway/src/server/routes.ts +++ b/packages/kilo-gateway/src/server/routes.ts @@ -6,8 +6,9 @@ * This factory function accepts OpenCode dependencies to create Kilo-specific routes */ -import { fetchProfile, fetchBalance } from "../api/profile.js" +import { fetchProfile, fetchBalance, fetchExtensionSettings } from "../api/profile.js" import { fetchKilocodeNotifications, KilocodeNotificationSchema } from "../api/notifications.js" +import { fetchRemoteSessionMessages, fetchRemoteSessions } from "../api/remote-sessions.js" import { KILO_API_BASE } from "../api/constants.js" // Type definitions for OpenCode dependencies (injected at runtime) @@ -55,6 +56,26 @@ interface KiloRoutesDeps { export function createKiloRoutes(deps: KiloRoutesDeps) { const { Hono, describeRoute, validator, resolver, errors, Auth, z } = deps + function getToken(auth: any): string | undefined { + if (!auth) { + return undefined + } + if (auth.type === "api") { + return auth.key + } + if (auth.type === "oauth") { + return auth.access + } + if (auth.type === "wellknown") { + return auth.token + } + return undefined + } + + function getOrganizationId(auth: any): string | undefined { + return auth?.type === "oauth" ? auth.accountId : undefined + } + const Organization = z.object({ id: z.string(), name: z.string(), @@ -242,6 +263,135 @@ export function createKiloRoutes(deps: KiloRoutesDeps) { }) }, ) + .get( + "/extension-settings", + describeRoute({ + summary: "Get extension settings", + description: "Fetch organization/user extension settings for cloud-managed policy behavior", + operationId: "kilo.extensionSettings", + responses: { + 200: { + description: "Extension settings", + content: { + "application/json": { + schema: resolver( + z.object({ + organization: z.unknown().optional(), + user: z.unknown().optional(), + }), + ), + }, + }, + }, + ...errors(400, 401), + }, + }), + async (c: any) => { + const auth = await Auth.get("kilo") + if (!auth) { + return c.json({ error: "Not authenticated with Kilo Gateway" }, 401) + } + + const token = getToken(auth) + if (!token) { + return c.json({ error: "No valid token found" }, 401) + } + + const organizationId = getOrganizationId(auth) + const settings = await fetchExtensionSettings(token, organizationId) + return c.json(settings) + }, + ) + .get( + "/remote-sessions", + describeRoute({ + summary: "List remote sessions", + description: "Fetch cloud-synced remote sessions for Agent Manager", + operationId: "kilo.remoteSessions.list", + responses: { + 200: { + description: "Remote sessions", + content: { + "application/json": { + schema: resolver( + z.object({ + sessions: z.array(z.unknown()), + }), + ), + }, + }, + }, + ...errors(400, 401), + }, + }), + validator( + "query", + z.object({ + limit: z.coerce.number().int().min(1).max(100).optional(), + }), + ), + async (c: any) => { + const auth = await Auth.get("kilo") + if (!auth) { + return c.json({ error: "Not authenticated with Kilo Gateway" }, 401) + } + + const token = getToken(auth) + if (!token) { + return c.json({ error: "No valid token found" }, 401) + } + + const organizationId = getOrganizationId(auth) + const limit = c.req.valid("query").limit ?? 50 + const sessions = await fetchRemoteSessions(token, limit, organizationId) + return c.json({ sessions }) + }, + ) + .get( + "/remote-sessions/:sessionID/messages", + describeRoute({ + summary: "Get remote session messages", + description: "Fetch cloud session transcript messages for Agent Manager local continuation", + operationId: "kilo.remoteSessions.messages", + responses: { + 200: { + description: "Remote session messages", + content: { + "application/json": { + schema: resolver( + z.object({ + messages: z.array(z.unknown()), + }), + ), + }, + }, + }, + ...errors(400, 401), + }, + }), + validator( + "param", + z.object({ + sessionID: z.string().min(1), + }), + ), + async (c: any) => { + const auth = await Auth.get("kilo") + if (!auth) { + return c.json({ error: "Not authenticated with Kilo Gateway" }, 401) + } + + const token = getToken(auth) + if (!token) { + return c.json({ error: "No valid token found" }, 401) + } + + const organizationId = getOrganizationId(auth) + const sessionID = c.req.valid("param").sessionID + const messages = await fetchRemoteSessionMessages(token, sessionID, organizationId) + return c.json({ messages }) + }, + ) .get( "/notifications", describeRoute({ @@ -264,10 +414,10 @@ export function createKiloRoutes(deps: KiloRoutesDeps) { const auth = await Auth.get("kilo") if (!auth) return c.json([]) - const token = auth.type === "api" ? auth.key : auth.type === "oauth" ? auth.access : undefined + const token = getToken(auth) if (!token) return c.json([]) - const organizationId = auth.type === "oauth" ? auth.accountId : undefined + const organizationId = getOrganizationId(auth) const notifications = await fetchKilocodeNotifications({ kilocodeToken: token, kilocodeOrganizationId: organizationId, diff --git a/packages/kilo-ui/src/styles/vscode-bridge.css b/packages/kilo-ui/src/styles/vscode-bridge.css index 9e047eace2..c28c79ccf3 100644 --- a/packages/kilo-ui/src/styles/vscode-bridge.css +++ b/packages/kilo-ui/src/styles/vscode-bridge.css @@ -11,8 +11,35 @@ */ html[data-theme="kilo-vscode"] { - /* Let VS Code control the color scheme instead of the OS preference */ - color-scheme: normal; + /* ===== Typography ===== + * opencode UI components use --font-family-sans/mono variables. + * Bridge them to VS Code-native font settings inside the extension. + */ + --font-family-sans: var(--vscode-font-family, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif); + --font-family-sans--font-feature-settings: normal; + --font-family-sans--font-variation-settings: normal; + --font-family-mono: var( + --vscode-editor-font-family, + ui-monospace, + SFMono-Regular, + Menlo, + Monaco, + Consolas, + "Liberation Mono", + "Courier New", + monospace + ); + --font-family-mono--font-feature-settings: normal; + --font-family-mono--font-variation-settings: normal; + --font-size-small: calc(var(--vscode-font-size, 13px) * 0.85); + --font-size-base: var(--vscode-font-size, 13px); + --font-size-large: calc(var(--vscode-font-size, 13px) * 1.15); + --font-size-x-large: calc(var(--vscode-font-size, 13px) * 1.35); + --font-weight-regular: var(--vscode-font-weight, 400); + --font-weight-medium: 500; + --line-height-large: 150%; + --line-height-x-large: 170%; + --line-height-2x-large: 190%; /* ===== Backgrounds ===== */ --background-base: var(--vscode-editor-background); diff --git a/packages/kilo-vscode/src/utils/logger.ts b/packages/kilo-vscode/src/utils/logger.ts new file mode 100644 index 0000000000..fda2f72998 --- /dev/null +++ b/packages/kilo-vscode/src/utils/logger.ts @@ -0,0 +1,54 @@ +import * as vscode from "vscode" +import { inspect } from "node:util" + +type LogLevel = "debug" | "info" | "warn" | "error" + +let outputChannel: vscode.OutputChannel | undefined +let debugEnabled = false + +export function initializeLogger(channel: vscode.OutputChannel): void { + outputChannel = channel +} + +export function setLoggerDebugEnabled(enabled: boolean): void { + debugEnabled = enabled +} + +function formatArg(value: unknown): string { + if (typeof value === "string") { + return value + } + return inspect(value, { depth: 6, colors: false, compact: true, breakLength: 120 }) +} + +function consoleMethod(level: LogLevel): (...data: unknown[]) => void { + switch (level) { + case "error": + return console.error + case "warn": + return console.warn + default: + return console.log + } +} + +function write(level: LogLevel, ...args: unknown[]): void { + if (args.length === 0) { + return + } + if (level === "debug" && !debugEnabled) { + return + } + + const timestamp = new Date().toISOString() + const message = args.map((arg) => formatArg(arg)).join(" ") + outputChannel?.appendLine(`[${timestamp}] [${level.toUpperCase()}] ${message}`) + consoleMethod(level)(...args) +} + +export const logger = { + debug: (...args: unknown[]) => write("debug", ...args), + info: (...args: unknown[]) => write("info", ...args), + warn: (...args: unknown[]) => write("warn", ...args), + error: (...args: unknown[]) => write("error", ...args), +} diff --git a/packages/kilo-vscode/src/utils/open-external.ts b/packages/kilo-vscode/src/utils/open-external.ts new file mode 100644 index 0000000000..2cb2c0ca04 --- /dev/null +++ b/packages/kilo-vscode/src/utils/open-external.ts @@ -0,0 +1,23 @@ +import { z } from "zod" + +export const ALLOWED_OPEN_EXTERNAL_SCHEMES = new Set(["https:", "vscode:"]) + +export function parseAllowedOpenExternalUrl(rawUrl: unknown): string | null { + const parsedInput = z.string().trim().min(1).safeParse(rawUrl) + if (!parsedInput.success) { + return null + } + + let parsedUrl: URL + try { + parsedUrl = new URL(parsedInput.data) + } catch { + return null + } + + if (!ALLOWED_OPEN_EXTERNAL_SCHEMES.has(parsedUrl.protocol)) { + return null + } + + return parsedUrl.toString() +} diff --git a/packages/kilo-vscode/src/utils/path-security.ts b/packages/kilo-vscode/src/utils/path-security.ts new file mode 100644 index 0000000000..be844b1307 --- /dev/null +++ b/packages/kilo-vscode/src/utils/path-security.ts @@ -0,0 +1,31 @@ +import fs from "node:fs/promises" +import path from "node:path" + +export async function realpathOrResolved(targetPath: string): Promise { + try { + return await fs.realpath(targetPath) + } catch { + return path.resolve(targetPath) + } +} + +export function normalizePathForCompare(targetPath: string): string { + const resolved = path.resolve(targetPath) + return process.platform === "win32" ? resolved.toLowerCase() : resolved +} + +export async function isPathInsideAnyRoot(candidatePath: string, roots: readonly string[]): Promise { + const candidateCanonical = normalizePathForCompare(await realpathOrResolved(candidatePath)) + + for (const root of roots) { + if (!root) { + continue + } + const rootCanonical = normalizePathForCompare(await realpathOrResolved(root)) + if (candidateCanonical === rootCanonical || candidateCanonical.startsWith(`${rootCanonical}${path.sep}`)) { + return true + } + } + + return false +} diff --git a/packages/kilo-vscode/src/utils/telemetry.ts b/packages/kilo-vscode/src/utils/telemetry.ts new file mode 100644 index 0000000000..b5ae1be656 --- /dev/null +++ b/packages/kilo-vscode/src/utils/telemetry.ts @@ -0,0 +1,32 @@ +import * as vscode from "vscode" +import { z } from "zod" +import { logger } from "./logger" + +export const telemetryEventNameSchema = z.enum([ + "Marketplace Tab Viewed", + "Marketplace Install Button Clicked", + "Marketplace Item Installed", + "Marketplace Item Removed", + "Agent Manager Opened", + "Agent Manager Session Started", + "Agent Manager Session Completed", + "Agent Manager Session Stopped", + "Agent Manager Session Error", + "Agent Manager Login Issue", +]) + +export type TelemetryEventName = z.infer + +export function parseTelemetryProperties(raw: unknown): Record | undefined { + const parsed = z.record(z.unknown()).safeParse(raw) + return parsed.success ? parsed.data : undefined +} + +export function captureTelemetryEvent(event: TelemetryEventName, properties?: Record): void { + if (!vscode.env.isTelemetryEnabled) { + logger.debug("[Kilo New] Telemetry skipped (disabled):", event) + return + } + + logger.debug("[Kilo New] Telemetry:", event, properties ?? {}) +} diff --git a/packages/kilo-vscode/src/utils/webview-csp.ts b/packages/kilo-vscode/src/utils/webview-csp.ts new file mode 100644 index 0000000000..57f35f04c7 --- /dev/null +++ b/packages/kilo-vscode/src/utils/webview-csp.ts @@ -0,0 +1,15 @@ +export interface WebviewCspInput { + cspSource: string + nonce: string +} + +export function buildWebviewCsp(input: WebviewCspInput): string { + return [ + "default-src 'none'", + `style-src 'unsafe-inline' ${input.cspSource}`, + `script-src 'nonce-${input.nonce}' 'wasm-unsafe-eval'`, + `font-src ${input.cspSource}`, + `connect-src ${input.cspSource}`, + `img-src ${input.cspSource} data: blob:`, + ].join("; ") +} diff --git a/packages/kilo-vscode/tests/unit/open-external-url.test.ts b/packages/kilo-vscode/tests/unit/open-external-url.test.ts new file mode 100644 index 0000000000..63b3dd0248 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/open-external-url.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from "bun:test" +import { parseAllowedOpenExternalUrl } from "../../src/utils/open-external" + +describe("parseAllowedOpenExternalUrl", () => { + it("accepts https urls", () => { + expect(parseAllowedOpenExternalUrl("https://example.com/path?q=1")).toBe("https://example.com/path?q=1") + }) + + it("accepts vscode urls", () => { + expect(parseAllowedOpenExternalUrl("vscode://file/c:/tmp/foo.ts")).toBe("vscode://file/c:/tmp/foo.ts") + }) + + it("rejects unsupported schemes", () => { + expect(parseAllowedOpenExternalUrl("javascript:alert(1)")).toBeNull() + expect(parseAllowedOpenExternalUrl("file:///tmp/a.txt")).toBeNull() + }) + + it("rejects invalid payloads", () => { + expect(parseAllowedOpenExternalUrl("")).toBeNull() + expect(parseAllowedOpenExternalUrl("not-a-url")).toBeNull() + expect(parseAllowedOpenExternalUrl(undefined)).toBeNull() + expect(parseAllowedOpenExternalUrl(42)).toBeNull() + }) +}) diff --git a/packages/kilo-vscode/tests/unit/path-security.test.ts b/packages/kilo-vscode/tests/unit/path-security.test.ts new file mode 100644 index 0000000000..3fb707dfe4 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/path-security.test.ts @@ -0,0 +1,47 @@ +import fs from "node:fs/promises" +import os from "node:os" +import path from "node:path" +import { describe, expect, it } from "bun:test" +import { isPathInsideAnyRoot } from "../../src/utils/path-security" + +describe("isPathInsideAnyRoot", () => { + it("allows paths inside the declared root", async () => { + const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "kilo-path-security-")) + const nested = path.join(tempRoot, "a", "b", "file.txt") + await fs.mkdir(path.dirname(nested), { recursive: true }) + await fs.writeFile(nested, "ok", "utf8") + + expect(await isPathInsideAnyRoot(nested, [tempRoot])).toBe(true) + }) + + it("rejects traversal outside the declared root", async () => { + const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "kilo-path-security-")) + const outsideRoot = await fs.mkdtemp(path.join(os.tmpdir(), "kilo-path-security-outside-")) + const outsideFile = path.join(outsideRoot, "outside.txt") + await fs.writeFile(outsideFile, "outside", "utf8") + + const traversal = path.join(tempRoot, "..", path.basename(outsideRoot), "outside.txt") + expect(await isPathInsideAnyRoot(traversal, [tempRoot])).toBe(false) + }) + + it("rejects symlink escapes", async () => { + const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "kilo-path-security-")) + const outsideRoot = await fs.mkdtemp(path.join(os.tmpdir(), "kilo-path-security-outside-")) + const outsideFile = path.join(outsideRoot, "outside.txt") + await fs.writeFile(outsideFile, "outside", "utf8") + + const symlinkPath = path.join(tempRoot, "escape-link") + try { + await fs.symlink(outsideRoot, symlinkPath) + } catch (error) { + if (process.platform === "win32") { + // Windows CI environments may block symlink creation depending on privileges. + return + } + throw error + } + const escapedFile = path.join(symlinkPath, "outside.txt") + + expect(await isPathInsideAnyRoot(escapedFile, [tempRoot])).toBe(false) + }) +}) diff --git a/packages/kilo-vscode/tests/unit/webview-csp.test.ts b/packages/kilo-vscode/tests/unit/webview-csp.test.ts new file mode 100644 index 0000000000..fd0f2daafd --- /dev/null +++ b/packages/kilo-vscode/tests/unit/webview-csp.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from "bun:test" +import { buildWebviewCsp } from "../../src/utils/webview-csp" + +describe("buildWebviewCsp", () => { + it("includes strict defaults and nonce-scoped scripts", () => { + const csp = buildWebviewCsp({ cspSource: "vscode-webview://abc", nonce: "nonce123" }) + + expect(csp).toContain("default-src 'none'") + expect(csp).toContain("script-src 'nonce-nonce123' 'wasm-unsafe-eval'") + expect(csp).toContain("connect-src vscode-webview://abc") + }) + + it("does not allow arbitrary https image loading", () => { + const csp = buildWebviewCsp({ cspSource: "vscode-webview://abc", nonce: "nonce123" }) + + expect(csp).toContain("img-src vscode-webview://abc data: blob:") + expect(csp).not.toContain("https:") + }) +}) From c8b5f1f65016f46fd9bdb7e739efc994a2e16607 Mon Sep 17 00:00:00 2001 From: Bernardo Ferrari Date: Mon, 16 Feb 2026 14:20:12 -0300 Subject: [PATCH 2/8] split(vscode): scope part 1 to foundation and defer remote/session utils --- .../kilo-gateway/src/api/remote-sessions.ts | 91 ------------------ packages/kilo-gateway/src/server/routes.ts | 92 +------------------ packages/kilo-vscode/src/utils/logger.ts | 1 + packages/kilo-vscode/src/utils/telemetry.ts | 32 ------- packages/kilo-vscode/src/utils/webview-csp.ts | 15 --- .../tests/unit/webview-csp.test.ts | 19 ---- 6 files changed, 2 insertions(+), 248 deletions(-) delete mode 100644 packages/kilo-gateway/src/api/remote-sessions.ts delete mode 100644 packages/kilo-vscode/src/utils/telemetry.ts delete mode 100644 packages/kilo-vscode/src/utils/webview-csp.ts delete mode 100644 packages/kilo-vscode/tests/unit/webview-csp.test.ts diff --git a/packages/kilo-gateway/src/api/remote-sessions.ts b/packages/kilo-gateway/src/api/remote-sessions.ts deleted file mode 100644 index ee52857063..0000000000 --- a/packages/kilo-gateway/src/api/remote-sessions.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { KILO_API_BASE } from "./constants.js" - -export interface RemoteSessionInfo { - session_id: string - title: string - created_at: string - updated_at: string - git_url: string | null - organization_id: string | null - last_mode: string | null - last_model: string | null - cloud_agent_session_id: string | null -} - -type TrpcEnvelope = { - result?: { - data?: T - } -} - -type CliSessionsListResult = { - cliSessions?: RemoteSessionInfo[] -} - -type CliSessionWithBlobResult = { - ui_messages_blob_url?: string | null -} - -async function trpcGet( - procedure: string, - input: Record, - token: string, - organizationId?: string, -): Promise { - const url = new URL(`${KILO_API_BASE}/api/trpc/${procedure}`) - url.searchParams.set("input", JSON.stringify(input)) - - const headers: Record = { - Authorization: `Bearer ${token}`, - "Content-Type": "application/json", - } - if (organizationId) { - headers["x-kilocode-organizationid"] = organizationId - } - - const response = await fetch(url, { method: "GET", headers }) - if (!response.ok) { - throw new Error(`tRPC ${procedure} failed: ${response.status}`) - } - - const payload = (await response.json()) as TrpcEnvelope - if (!payload.result || payload.result.data === undefined) { - throw new Error(`tRPC ${procedure} returned invalid payload`) - } - return payload.result.data -} - -export async function fetchRemoteSessions( - token: string, - limit = 50, - organizationId?: string, -): Promise { - const data = await trpcGet("cliSessions.list", { limit }, token, organizationId) - return Array.isArray(data.cliSessions) ? data.cliSessions : [] -} - -export async function fetchRemoteSessionMessages( - token: string, - sessionId: string, - organizationId?: string, -): Promise { - const data = await trpcGet( - "cliSessions.get", - { session_id: sessionId, include_blob_urls: true }, - token, - organizationId, - ) - - const blobUrl = data.ui_messages_blob_url - if (!blobUrl) { - return [] - } - - const response = await fetch(blobUrl) - if (!response.ok) { - throw new Error(`Failed to fetch remote session messages: ${response.status}`) - } - - const payload = (await response.json()) as unknown - return Array.isArray(payload) ? payload : [] -} diff --git a/packages/kilo-gateway/src/server/routes.ts b/packages/kilo-gateway/src/server/routes.ts index 6416aa7e73..6f3262b00f 100644 --- a/packages/kilo-gateway/src/server/routes.ts +++ b/packages/kilo-gateway/src/server/routes.ts @@ -8,7 +8,6 @@ import { fetchProfile, fetchBalance, fetchExtensionSettings } from "../api/profile.js" import { fetchKilocodeNotifications, KilocodeNotificationSchema } from "../api/notifications.js" -import { fetchRemoteSessionMessages, fetchRemoteSessions } from "../api/remote-sessions.js" import { KILO_API_BASE } from "../api/constants.js" // Type definitions for OpenCode dependencies (injected at runtime) @@ -73,6 +72,7 @@ export function createKiloRoutes(deps: KiloRoutesDeps) { } function getOrganizationId(auth: any): string | undefined { + // OAuth stores the currently selected organization context in accountId. return auth?.type === "oauth" ? auth.accountId : undefined } @@ -302,96 +302,6 @@ export function createKiloRoutes(deps: KiloRoutesDeps) { return c.json(settings) }, ) - .get( - "/remote-sessions", - describeRoute({ - summary: "List remote sessions", - description: "Fetch cloud-synced remote sessions for Agent Manager", - operationId: "kilo.remoteSessions.list", - responses: { - 200: { - description: "Remote sessions", - content: { - "application/json": { - schema: resolver( - z.object({ - sessions: z.array(z.unknown()), - }), - ), - }, - }, - }, - ...errors(400, 401), - }, - }), - validator( - "query", - z.object({ - limit: z.coerce.number().int().min(1).max(100).optional(), - }), - ), - async (c: any) => { - const auth = await Auth.get("kilo") - if (!auth) { - return c.json({ error: "Not authenticated with Kilo Gateway" }, 401) - } - - const token = getToken(auth) - if (!token) { - return c.json({ error: "No valid token found" }, 401) - } - - const organizationId = getOrganizationId(auth) - const limit = c.req.valid("query").limit ?? 50 - const sessions = await fetchRemoteSessions(token, limit, organizationId) - return c.json({ sessions }) - }, - ) - .get( - "/remote-sessions/:sessionID/messages", - describeRoute({ - summary: "Get remote session messages", - description: "Fetch cloud session transcript messages for Agent Manager local continuation", - operationId: "kilo.remoteSessions.messages", - responses: { - 200: { - description: "Remote session messages", - content: { - "application/json": { - schema: resolver( - z.object({ - messages: z.array(z.unknown()), - }), - ), - }, - }, - }, - ...errors(400, 401), - }, - }), - validator( - "param", - z.object({ - sessionID: z.string().min(1), - }), - ), - async (c: any) => { - const auth = await Auth.get("kilo") - if (!auth) { - return c.json({ error: "Not authenticated with Kilo Gateway" }, 401) - } - - const token = getToken(auth) - if (!token) { - return c.json({ error: "No valid token found" }, 401) - } - - const organizationId = getOrganizationId(auth) - const sessionID = c.req.valid("param").sessionID - const messages = await fetchRemoteSessionMessages(token, sessionID, organizationId) - return c.json({ messages }) - }, - ) .get( "/notifications", describeRoute({ diff --git a/packages/kilo-vscode/src/utils/logger.ts b/packages/kilo-vscode/src/utils/logger.ts index fda2f72998..a812a466df 100644 --- a/packages/kilo-vscode/src/utils/logger.ts +++ b/packages/kilo-vscode/src/utils/logger.ts @@ -3,6 +3,7 @@ import { inspect } from "node:util" type LogLevel = "debug" | "info" | "warn" | "error" +// Central logger keeps console output while also writing to a VS Code output channel. let outputChannel: vscode.OutputChannel | undefined let debugEnabled = false diff --git a/packages/kilo-vscode/src/utils/telemetry.ts b/packages/kilo-vscode/src/utils/telemetry.ts deleted file mode 100644 index b5ae1be656..0000000000 --- a/packages/kilo-vscode/src/utils/telemetry.ts +++ /dev/null @@ -1,32 +0,0 @@ -import * as vscode from "vscode" -import { z } from "zod" -import { logger } from "./logger" - -export const telemetryEventNameSchema = z.enum([ - "Marketplace Tab Viewed", - "Marketplace Install Button Clicked", - "Marketplace Item Installed", - "Marketplace Item Removed", - "Agent Manager Opened", - "Agent Manager Session Started", - "Agent Manager Session Completed", - "Agent Manager Session Stopped", - "Agent Manager Session Error", - "Agent Manager Login Issue", -]) - -export type TelemetryEventName = z.infer - -export function parseTelemetryProperties(raw: unknown): Record | undefined { - const parsed = z.record(z.unknown()).safeParse(raw) - return parsed.success ? parsed.data : undefined -} - -export function captureTelemetryEvent(event: TelemetryEventName, properties?: Record): void { - if (!vscode.env.isTelemetryEnabled) { - logger.debug("[Kilo New] Telemetry skipped (disabled):", event) - return - } - - logger.debug("[Kilo New] Telemetry:", event, properties ?? {}) -} diff --git a/packages/kilo-vscode/src/utils/webview-csp.ts b/packages/kilo-vscode/src/utils/webview-csp.ts deleted file mode 100644 index 57f35f04c7..0000000000 --- a/packages/kilo-vscode/src/utils/webview-csp.ts +++ /dev/null @@ -1,15 +0,0 @@ -export interface WebviewCspInput { - cspSource: string - nonce: string -} - -export function buildWebviewCsp(input: WebviewCspInput): string { - return [ - "default-src 'none'", - `style-src 'unsafe-inline' ${input.cspSource}`, - `script-src 'nonce-${input.nonce}' 'wasm-unsafe-eval'`, - `font-src ${input.cspSource}`, - `connect-src ${input.cspSource}`, - `img-src ${input.cspSource} data: blob:`, - ].join("; ") -} diff --git a/packages/kilo-vscode/tests/unit/webview-csp.test.ts b/packages/kilo-vscode/tests/unit/webview-csp.test.ts deleted file mode 100644 index fd0f2daafd..0000000000 --- a/packages/kilo-vscode/tests/unit/webview-csp.test.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { describe, expect, it } from "bun:test" -import { buildWebviewCsp } from "../../src/utils/webview-csp" - -describe("buildWebviewCsp", () => { - it("includes strict defaults and nonce-scoped scripts", () => { - const csp = buildWebviewCsp({ cspSource: "vscode-webview://abc", nonce: "nonce123" }) - - expect(csp).toContain("default-src 'none'") - expect(csp).toContain("script-src 'nonce-nonce123' 'wasm-unsafe-eval'") - expect(csp).toContain("connect-src vscode-webview://abc") - }) - - it("does not allow arbitrary https image loading", () => { - const csp = buildWebviewCsp({ cspSource: "vscode-webview://abc", nonce: "nonce123" }) - - expect(csp).toContain("img-src vscode-webview://abc data: blob:") - expect(csp).not.toContain("https:") - }) -}) From 112742121c5874beb129ba58b04936c4744717b7 Mon Sep 17 00:00:00 2001 From: Bernardo Ferrari Date: Mon, 16 Feb 2026 18:03:49 -0300 Subject: [PATCH 3/8] fix(split-01): clarify org context and drop unused logger util --- packages/kilo-gateway/src/server/routes.ts | 14 ++++-- packages/kilo-vscode/src/utils/logger.ts | 55 ---------------------- 2 files changed, 9 insertions(+), 60 deletions(-) delete mode 100644 packages/kilo-vscode/src/utils/logger.ts diff --git a/packages/kilo-gateway/src/server/routes.ts b/packages/kilo-gateway/src/server/routes.ts index 6f3262b00f..01410bc79e 100644 --- a/packages/kilo-gateway/src/server/routes.ts +++ b/packages/kilo-gateway/src/server/routes.ts @@ -71,9 +71,13 @@ export function createKiloRoutes(deps: KiloRoutesDeps) { return undefined } - function getOrganizationId(auth: any): string | undefined { - // OAuth stores the currently selected organization context in accountId. - return auth?.type === "oauth" ? auth.accountId : undefined + function getSelectedOrganizationId(auth: any): string | undefined { + if (auth?.type !== "oauth") { + return undefined + } + // OAuth auth can represent users that belong to multiple orgs. + // accountId stores the currently selected org context. + return typeof auth.accountId === "string" && auth.accountId.length > 0 ? auth.accountId : undefined } const Organization = z.object({ @@ -297,7 +301,7 @@ export function createKiloRoutes(deps: KiloRoutesDeps) { return c.json({ error: "No valid token found" }, 401) } - const organizationId = getOrganizationId(auth) + const organizationId = getSelectedOrganizationId(auth) const settings = await fetchExtensionSettings(token, organizationId) return c.json(settings) }, @@ -327,7 +331,7 @@ export function createKiloRoutes(deps: KiloRoutesDeps) { const token = getToken(auth) if (!token) return c.json([]) - const organizationId = getOrganizationId(auth) + const organizationId = getSelectedOrganizationId(auth) const notifications = await fetchKilocodeNotifications({ kilocodeToken: token, kilocodeOrganizationId: organizationId, diff --git a/packages/kilo-vscode/src/utils/logger.ts b/packages/kilo-vscode/src/utils/logger.ts deleted file mode 100644 index a812a466df..0000000000 --- a/packages/kilo-vscode/src/utils/logger.ts +++ /dev/null @@ -1,55 +0,0 @@ -import * as vscode from "vscode" -import { inspect } from "node:util" - -type LogLevel = "debug" | "info" | "warn" | "error" - -// Central logger keeps console output while also writing to a VS Code output channel. -let outputChannel: vscode.OutputChannel | undefined -let debugEnabled = false - -export function initializeLogger(channel: vscode.OutputChannel): void { - outputChannel = channel -} - -export function setLoggerDebugEnabled(enabled: boolean): void { - debugEnabled = enabled -} - -function formatArg(value: unknown): string { - if (typeof value === "string") { - return value - } - return inspect(value, { depth: 6, colors: false, compact: true, breakLength: 120 }) -} - -function consoleMethod(level: LogLevel): (...data: unknown[]) => void { - switch (level) { - case "error": - return console.error - case "warn": - return console.warn - default: - return console.log - } -} - -function write(level: LogLevel, ...args: unknown[]): void { - if (args.length === 0) { - return - } - if (level === "debug" && !debugEnabled) { - return - } - - const timestamp = new Date().toISOString() - const message = args.map((arg) => formatArg(arg)).join(" ") - outputChannel?.appendLine(`[${timestamp}] [${level.toUpperCase()}] ${message}`) - consoleMethod(level)(...args) -} - -export const logger = { - debug: (...args: unknown[]) => write("debug", ...args), - info: (...args: unknown[]) => write("info", ...args), - warn: (...args: unknown[]) => write("warn", ...args), - error: (...args: unknown[]) => write("error", ...args), -} From 6f9aac9c83a3512c4e1c905350e6076db1ab4a0b Mon Sep 17 00:00:00 2001 From: Bernardo Ferrari Date: Mon, 16 Feb 2026 18:06:20 -0300 Subject: [PATCH 4/8] chore(split-01): restore logger utility --- packages/kilo-vscode/src/utils/logger.ts | 55 ++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 packages/kilo-vscode/src/utils/logger.ts diff --git a/packages/kilo-vscode/src/utils/logger.ts b/packages/kilo-vscode/src/utils/logger.ts new file mode 100644 index 0000000000..a812a466df --- /dev/null +++ b/packages/kilo-vscode/src/utils/logger.ts @@ -0,0 +1,55 @@ +import * as vscode from "vscode" +import { inspect } from "node:util" + +type LogLevel = "debug" | "info" | "warn" | "error" + +// Central logger keeps console output while also writing to a VS Code output channel. +let outputChannel: vscode.OutputChannel | undefined +let debugEnabled = false + +export function initializeLogger(channel: vscode.OutputChannel): void { + outputChannel = channel +} + +export function setLoggerDebugEnabled(enabled: boolean): void { + debugEnabled = enabled +} + +function formatArg(value: unknown): string { + if (typeof value === "string") { + return value + } + return inspect(value, { depth: 6, colors: false, compact: true, breakLength: 120 }) +} + +function consoleMethod(level: LogLevel): (...data: unknown[]) => void { + switch (level) { + case "error": + return console.error + case "warn": + return console.warn + default: + return console.log + } +} + +function write(level: LogLevel, ...args: unknown[]): void { + if (args.length === 0) { + return + } + if (level === "debug" && !debugEnabled) { + return + } + + const timestamp = new Date().toISOString() + const message = args.map((arg) => formatArg(arg)).join(" ") + outputChannel?.appendLine(`[${timestamp}] [${level.toUpperCase()}] ${message}`) + consoleMethod(level)(...args) +} + +export const logger = { + debug: (...args: unknown[]) => write("debug", ...args), + info: (...args: unknown[]) => write("info", ...args), + warn: (...args: unknown[]) => write("warn", ...args), + error: (...args: unknown[]) => write("error", ...args), +} From f6b0461aed5d21b728b51efad1a4ea256d8a1b05 Mon Sep 17 00:00:00 2001 From: Bernardo Ferrari Date: Mon, 16 Feb 2026 18:10:09 -0300 Subject: [PATCH 5/8] fix(split-01): use selected-org helper in profile route --- packages/kilo-gateway/src/server/routes.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/kilo-gateway/src/server/routes.ts b/packages/kilo-gateway/src/server/routes.ts index 01410bc79e..66b9a44ea0 100644 --- a/packages/kilo-gateway/src/server/routes.ts +++ b/packages/kilo-gateway/src/server/routes.ts @@ -130,7 +130,7 @@ export function createKiloRoutes(deps: KiloRoutesDeps) { } const token = auth.access - const currentOrgId = auth.accountId ?? null + const currentOrgId = getSelectedOrganizationId(auth) ?? null // Fetch profile and balance in parallel // Pass organizationId to fetchBalance to get team balance when in org context @@ -177,6 +177,7 @@ export function createKiloRoutes(deps: KiloRoutesDeps) { } // Update auth with new organization ID + // OpenCode's OAuth auth shape stores the active org context in `accountId`. await Auth.set("kilo", { type: "oauth", refresh: auth.refresh, From 2912ab3826a7167f14dc4f94a426068ade6f86e2 Mon Sep 17 00:00:00 2001 From: Bernardo Ferrari Date: Mon, 16 Feb 2026 18:11:21 -0300 Subject: [PATCH 6/8] chore(split-01): keep profile currentOrgId assignment minimal --- packages/kilo-gateway/src/server/routes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kilo-gateway/src/server/routes.ts b/packages/kilo-gateway/src/server/routes.ts index 66b9a44ea0..21fd66c054 100644 --- a/packages/kilo-gateway/src/server/routes.ts +++ b/packages/kilo-gateway/src/server/routes.ts @@ -130,7 +130,7 @@ export function createKiloRoutes(deps: KiloRoutesDeps) { } const token = auth.access - const currentOrgId = getSelectedOrganizationId(auth) ?? null + const currentOrgId = auth.accountId ?? null // Fetch profile and balance in parallel // Pass organizationId to fetchBalance to get team balance when in org context From b2118dcdb4fc2431d81f8bad045fabcae5ca98ff Mon Sep 17 00:00:00 2001 From: Bernardo Ferrari Date: Mon, 16 Feb 2026 18:13:12 -0300 Subject: [PATCH 7/8] refactor(split-01): simplify gateway auth helpers and settings parsing --- packages/kilo-gateway/src/api/profile.ts | 6 +---- packages/kilo-gateway/src/server/routes.ts | 31 +++++++++------------- 2 files changed, 13 insertions(+), 24 deletions(-) diff --git a/packages/kilo-gateway/src/api/profile.ts b/packages/kilo-gateway/src/api/profile.ts index baba44c5d0..427add4bd1 100644 --- a/packages/kilo-gateway/src/api/profile.ts +++ b/packages/kilo-gateway/src/api/profile.ts @@ -136,11 +136,7 @@ export async function fetchExtensionSettings( throw new Error(`Failed to fetch extension settings: ${response.status}`) } - const data = (await response.json()) as { organization?: unknown; user?: unknown } - return { - organization: data.organization, - user: data.user, - } + return (await response.json()) as { organization?: unknown; user?: unknown } } /** diff --git a/packages/kilo-gateway/src/server/routes.ts b/packages/kilo-gateway/src/server/routes.ts index 21fd66c054..57e1b47f13 100644 --- a/packages/kilo-gateway/src/server/routes.ts +++ b/packages/kilo-gateway/src/server/routes.ts @@ -56,19 +56,17 @@ export function createKiloRoutes(deps: KiloRoutesDeps) { const { Hono, describeRoute, validator, resolver, errors, Auth, z } = deps function getToken(auth: any): string | undefined { - if (!auth) { - return undefined - } - if (auth.type === "api") { - return auth.key - } - if (auth.type === "oauth") { - return auth.access + if (!auth) return undefined + switch (auth.type) { + case "api": + return auth.key + case "oauth": + return auth.access + case "wellknown": + return auth.token + default: + return undefined } - if (auth.type === "wellknown") { - return auth.token - } - return undefined } function getSelectedOrganizationId(auth: any): string | undefined { @@ -293,14 +291,9 @@ export function createKiloRoutes(deps: KiloRoutesDeps) { }), async (c: any) => { const auth = await Auth.get("kilo") - if (!auth) { - return c.json({ error: "Not authenticated with Kilo Gateway" }, 401) - } - + if (!auth) return c.json({ error: "Not authenticated with Kilo Gateway" }, 401) const token = getToken(auth) - if (!token) { - return c.json({ error: "No valid token found" }, 401) - } + if (!token) return c.json({ error: "No valid token found" }, 401) const organizationId = getSelectedOrganizationId(auth) const settings = await fetchExtensionSettings(token, organizationId) From 437426907804ec20b432fef482af02bbcbfe996d Mon Sep 17 00:00:00 2001 From: Bernardo Ferrari Date: Mon, 16 Feb 2026 18:17:49 -0300 Subject: [PATCH 8/8] chore(split-01): remove extension-settings gateway changes --- packages/kilo-gateway/src/api/profile.ts | 26 --------- packages/kilo-gateway/src/server/routes.ts | 64 +--------------------- 2 files changed, 3 insertions(+), 87 deletions(-) diff --git a/packages/kilo-gateway/src/api/profile.ts b/packages/kilo-gateway/src/api/profile.ts index 427add4bd1..aa717a64c4 100644 --- a/packages/kilo-gateway/src/api/profile.ts +++ b/packages/kilo-gateway/src/api/profile.ts @@ -113,32 +113,6 @@ export async function fetchDefaultModel(token?: string, organizationId?: string) */ export const getKiloDefaultModel = fetchDefaultModel -/** - * Fetch extension settings for the authenticated user/organization. - * These settings include org-managed Marketplace policy fields. - */ -export async function fetchExtensionSettings( - token: string, - organizationId?: string, -): Promise<{ organization?: unknown; user?: unknown }> { - const headers: Record = { - Authorization: `Bearer ${token}`, - "Content-Type": "application/json", - } - - if (organizationId) { - headers["x-kilocode-organizationid"] = organizationId - } - - const response = await fetch(`${KILO_API_BASE}/api/extension-settings`, { headers }) - - if (!response.ok) { - throw new Error(`Failed to fetch extension settings: ${response.status}`) - } - - return (await response.json()) as { organization?: unknown; user?: unknown } -} - /** * Fetch both profile and balance in parallel */ diff --git a/packages/kilo-gateway/src/server/routes.ts b/packages/kilo-gateway/src/server/routes.ts index 57e1b47f13..0d5b7d371d 100644 --- a/packages/kilo-gateway/src/server/routes.ts +++ b/packages/kilo-gateway/src/server/routes.ts @@ -6,7 +6,7 @@ * This factory function accepts OpenCode dependencies to create Kilo-specific routes */ -import { fetchProfile, fetchBalance, fetchExtensionSettings } from "../api/profile.js" +import { fetchProfile, fetchBalance } from "../api/profile.js" import { fetchKilocodeNotifications, KilocodeNotificationSchema } from "../api/notifications.js" import { KILO_API_BASE } from "../api/constants.js" @@ -55,29 +55,6 @@ interface KiloRoutesDeps { export function createKiloRoutes(deps: KiloRoutesDeps) { const { Hono, describeRoute, validator, resolver, errors, Auth, z } = deps - function getToken(auth: any): string | undefined { - if (!auth) return undefined - switch (auth.type) { - case "api": - return auth.key - case "oauth": - return auth.access - case "wellknown": - return auth.token - default: - return undefined - } - } - - function getSelectedOrganizationId(auth: any): string | undefined { - if (auth?.type !== "oauth") { - return undefined - } - // OAuth auth can represent users that belong to multiple orgs. - // accountId stores the currently selected org context. - return typeof auth.accountId === "string" && auth.accountId.length > 0 ? auth.accountId : undefined - } - const Organization = z.object({ id: z.string(), name: z.string(), @@ -175,7 +152,6 @@ export function createKiloRoutes(deps: KiloRoutesDeps) { } // Update auth with new organization ID - // OpenCode's OAuth auth shape stores the active org context in `accountId`. await Auth.set("kilo", { type: "oauth", refresh: auth.refresh, @@ -266,40 +242,6 @@ export function createKiloRoutes(deps: KiloRoutesDeps) { }) }, ) - .get( - "/extension-settings", - describeRoute({ - summary: "Get extension settings", - description: "Fetch organization/user extension settings for cloud-managed policy behavior", - operationId: "kilo.extensionSettings", - responses: { - 200: { - description: "Extension settings", - content: { - "application/json": { - schema: resolver( - z.object({ - organization: z.unknown().optional(), - user: z.unknown().optional(), - }), - ), - }, - }, - }, - ...errors(400, 401), - }, - }), - async (c: any) => { - const auth = await Auth.get("kilo") - if (!auth) return c.json({ error: "Not authenticated with Kilo Gateway" }, 401) - const token = getToken(auth) - if (!token) return c.json({ error: "No valid token found" }, 401) - - const organizationId = getSelectedOrganizationId(auth) - const settings = await fetchExtensionSettings(token, organizationId) - return c.json(settings) - }, - ) .get( "/notifications", describeRoute({ @@ -322,10 +264,10 @@ export function createKiloRoutes(deps: KiloRoutesDeps) { const auth = await Auth.get("kilo") if (!auth) return c.json([]) - const token = getToken(auth) + const token = auth.type === "api" ? auth.key : auth.type === "oauth" ? auth.access : undefined if (!token) return c.json([]) - const organizationId = getSelectedOrganizationId(auth) + const organizationId = auth.type === "oauth" ? auth.accountId : undefined const notifications = await fetchKilocodeNotifications({ kilocodeToken: token, kilocodeOrganizationId: organizationId,