11import * as fs from "node:fs" ;
22import * as path from "node:path" ;
33import { createGitClient } from "@posthog/git/client" ;
4+ import { getCleanEnv } from "@posthog/git/operation-manager" ;
45import { getCurrentBranch } from "@posthog/git/queries" ;
56import { CloneSaga } from "@posthog/git/sagas/clone" ;
67import { parseGithubUrl } from "@posthog/git/utils" ;
@@ -26,6 +27,28 @@ function fail(text: string): LocalToolResult {
2627 return { content : [ { type : "text" , text } ] , isError : true } ;
2728}
2829
30+ function githubAuthEnv ( token : string | undefined ) : Record < string , string > {
31+ if ( ! token ) return { } ;
32+ const basicAuth = Buffer . from ( `x-access-token:${ token } ` ) . toString ( "base64" ) ;
33+ return {
34+ GIT_CONFIG_COUNT : "1" ,
35+ GIT_CONFIG_KEY_0 : "http.extraHeader" ,
36+ GIT_CONFIG_VALUE_0 : `AUTHORIZATION: basic ${ basicAuth } ` ,
37+ } ;
38+ }
39+
40+ function hasHttpCredentials ( remoteUrl : string ) : boolean {
41+ try {
42+ const url = new URL ( remoteUrl ) ;
43+ return (
44+ ( url . protocol === "http:" || url . protocol === "https:" ) &&
45+ Boolean ( url . username || url . password )
46+ ) ;
47+ } catch {
48+ return false ;
49+ }
50+ }
51+
2952/**
3053 * Lazily brings a repo into a repo-less channel session's scratch workspace.
3154 * Clones into `<cwd>/repos/<repo>` (a subdir of the session cwd, so no session
@@ -61,6 +84,11 @@ export const cloneRepoTool = defineLocalTool({
6184 const slug = `${ parsed . owner } /${ parsed . repo } ` ;
6285 const repoName = parsed . repo ;
6386 const targetPath = path . join ( ctx . cwd , "repos" , slug ) ;
87+ const cloneUrl = `https://github.com/${ slug } .git` ;
88+ const authenticatedGitEnv = {
89+ ...getCleanEnv ( ) ,
90+ ...githubAuthEnv ( token ) ,
91+ } ;
6492
6593 const done = async ( note ?: string ) : Promise < LocalToolResult > => {
6694 const checkedOut = ( await getCurrentBranch ( targetPath ) ) ?? branch ?? null ;
@@ -78,7 +106,7 @@ export const cloneRepoTool = defineLocalTool({
78106
79107 const checkout = async ( ) : Promise < LocalToolResult | null > => {
80108 if ( ! branch ) return null ;
81- const git = createGitClient ( targetPath ) ;
109+ const git = createGitClient ( targetPath ) . env ( authenticatedGitEnv ) ;
82110 try {
83111 await git . checkout ( branch ) ;
84112 return null ;
@@ -115,24 +143,35 @@ export const cloneRepoTool = defineLocalTool({
115143 // the repo in place. Reuse it instead of letting git abort on a non-empty
116144 // destination, which the agent would receive as an opaque error.
117145 if ( fs . existsSync ( path . join ( targetPath , ".git" ) ) ) {
146+ const git = createGitClient ( targetPath ) ;
147+ try {
148+ const originUrl = await git . remote ( [ "get-url" , "origin" ] ) ;
149+ if (
150+ typeof originUrl === "string" &&
151+ hasHttpCredentials ( originUrl . trim ( ) )
152+ ) {
153+ await git . remote ( [ "set-url" , "origin" , cloneUrl ] ) ;
154+ }
155+ } catch ( err ) {
156+ return fail (
157+ `clone_repo couldn't secure the existing origin: ${ redact (
158+ err instanceof Error ? err . message : String ( err ) ,
159+ ) } `,
160+ ) ;
161+ }
118162 return (
119163 ( await checkout ( ) ) ??
120164 ( await done ( `${ slug } already cloned at ${ targetPath } ` ) )
121165 ) ;
122166 }
123167
124- // GitHub accepts a token as the basic-auth username for https clones; this
125- // covers private repos. Public repos clone fine without it.
126- const cloneUrl = token
127- ? `https://x-access-token:${ token } @github.com/${ slug } .git`
128- : `https://github.com/${ slug } .git` ;
129-
130168 try {
131169 const result = await new CloneSaga ( ) . run ( {
132170 repoUrl : cloneUrl ,
133171 targetPath,
134172 branch,
135173 shallow : true ,
174+ env : githubAuthEnv ( token ) ,
136175 } ) ;
137176 if ( ! result . success ) {
138177 return fail ( `clone_repo failed: ${ redact ( result . error ) } ` ) ;
0 commit comments