diff --git a/bun.lock b/bun.lock index 2453ed7d9f..9fb62d4cd3 100644 --- a/bun.lock +++ b/bun.lock @@ -334,6 +334,7 @@ "opentui-spinner": "0.0.6", "partial-json": "0.1.7", "remeda": "catalog:", + "simple-git": "3.31.1", "solid-js": "catalog:", "strip-ansi": "7.1.2", "tree-sitter-bash": "0.25.0", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 3e4d6abd16..ba8f3a07c3 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -114,6 +114,7 @@ "opentui-spinner": "0.0.6", "partial-json": "0.1.7", "remeda": "catalog:", + "simple-git": "3.31.1", "solid-js": "catalog:", "strip-ansi": "7.1.2", "tree-sitter-bash": "0.25.0", diff --git a/packages/opencode/src/kilo-sessions/ingest-queue.ts b/packages/opencode/src/kilo-sessions/ingest-queue.ts index fefcd45327..1ec105ea4a 100644 --- a/packages/opencode/src/kilo-sessions/ingest-queue.ts +++ b/packages/opencode/src/kilo-sessions/ingest-queue.ts @@ -16,6 +16,8 @@ export namespace IngestQueue { data: { platform: string orgId?: string + gitUrl?: string + gitBranch?: string } } | { diff --git a/packages/opencode/src/kilo-sessions/kilo-sessions.ts b/packages/opencode/src/kilo-sessions/kilo-sessions.ts index 5f00adb63f..b6c8f59788 100644 --- a/packages/opencode/src/kilo-sessions/kilo-sessions.ts +++ b/packages/opencode/src/kilo-sessions/kilo-sessions.ts @@ -10,6 +10,9 @@ import { clearInFlightCache, withInFlightCache } from "@/kilo-sessions/inflight- import type * as SDK from "@kilocode/sdk/v2" import z from "zod" import { KILO_API_BASE } from "@kilocode/kilo-gateway" +import { Instance } from "@/project/instance" +import { Vcs } from "@/project/vcs" +import simpleGit from "simple-git" export namespace KiloSessions { const log = Log.create({ service: "kilo-sessions" }) @@ -23,6 +26,7 @@ export namespace KiloSessions { const tokenKey = "kilo-sessions:token" const orgKey = "kilo-sessions:org" const clientKey = "kilo-sessions:client" + const gitUrlKeyPrefix = "kilo-sessions:git-url:" const ttlMs = 10_000 @@ -31,6 +35,7 @@ export namespace KiloSessions { clearInFlightCache(tokenValidKey) clearInFlightCache(clientKey) clearInFlightCache(orgKey) + clearInFlightCache(gitUrlKeyPrefix + Instance.worktree) } async function authValid(token: string) { @@ -370,13 +375,56 @@ export namespace KiloSessions { ]) } + /** Normalize a git remote URL: strip credentials, query params, and hash. Returns undefined for unrecognized formats. */ + function normalizeGitUrl(raw: string): string | undefined { + const ssh = raw.match(/^git@([^:]+):(.+)$/) + if (ssh) return `git@${ssh[1]}:${ssh[2].split("?")[0]}` + try { + const parsed = new URL(raw) + if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return undefined + parsed.username = "" + parsed.password = "" + parsed.search = "" + parsed.hash = "" + return parsed.toString() + } catch { + return undefined + } + } + + async function getGitUrl(): Promise { + return withInFlightCache(gitUrlKeyPrefix + Instance.worktree, ttlMs, async () => { + const repo = simpleGit(Instance.worktree) + const remotes = await repo.getRemotes(true).catch(() => []) + if (remotes.length === 0) return undefined + + const names = remotes.map((r) => r.name) + const remote = names.includes("origin") + ? "origin" + : remotes.length === 1 + ? names[0] + : names.includes("upstream") + ? "upstream" + : undefined + + if (!remote) return undefined + + const url = remotes.find((r) => r.name === remote)?.refs.fetch ?? "" + return url ? normalizeGitUrl(url) : undefined + }) + } + async function meta() { const platform = process.env["KILO_PLATFORM"] || "cli" const orgId = await getOrgId() + const gitBranch = await Vcs.branch().catch(() => undefined) + const gitUrl = await getGitUrl().catch(() => undefined) return { platform, ...(orgId ? { orgId } : {}), + ...(gitUrl ? { gitUrl } : {}), + ...(gitBranch ? { gitBranch } : {}), } } diff --git a/packages/opencode/test/kilo-sessions/ingest-queue.test.ts b/packages/opencode/test/kilo-sessions/ingest-queue.test.ts index 7c09477179..529be44b21 100644 --- a/packages/opencode/test/kilo-sessions/ingest-queue.test.ts +++ b/packages/opencode/test/kilo-sessions/ingest-queue.test.ts @@ -137,9 +137,16 @@ describe("share ingest queue", () => { }), }) - await q.sync("s7", [{ type: "kilo_meta", data: { platform: "cli" } }]) + await q.sync("s7", [ + { type: "kilo_meta", data: { platform: "cli", gitUrl: "https://github.com/old/repo.git", gitBranch: "main" } }, + ]) clock.now = 100 - await q.sync("s7", [{ type: "kilo_meta", data: { platform: "vscode", orgId: "org-1" } }]) + await q.sync("s7", [ + { + type: "kilo_meta", + data: { platform: "vscode", orgId: "org-1", gitUrl: "https://github.com/new/repo.git", gitBranch: "feature" }, + }, + ]) clock.now = 1000 sched.run() @@ -149,6 +156,8 @@ describe("share ingest queue", () => { expect((sent[0] as any).data[0].type).toBe("kilo_meta") expect((sent[0] as any).data[0].data.platform).toBe("vscode") expect((sent[0] as any).data[0].data.orgId).toBe("org-1") + expect((sent[0] as any).data[0].data.gitUrl).toBe("https://github.com/new/repo.git") + expect((sent[0] as any).data[0].data.gitBranch).toBe("feature") }) test("network failure retries and fill preserves newer updates", async () => {