@@ -14,8 +14,9 @@ import type { HooksConfig } from "./hooks.js";
1414import { loadDeclarativeTools } from "./tool-loader.js" ;
1515import { AuditLogger , isAuditEnabled } from "./audit.js" ;
1616import { formatComplianceWarnings } from "./compliance.js" ;
17- import { readFile } from "fs/promises" ;
18- import { join } from "path" ;
17+ import { readFile , mkdir , writeFile , stat , access } from "fs/promises" ;
18+ import { join , resolve } from "path" ;
19+ import { execSync } from "child_process" ;
1920
2021// ANSI helpers
2122const dim = ( s : string ) => `\x1b[2m${ s } \x1b[0m` ;
@@ -122,8 +123,131 @@ function summarizeArgs(args: any): string {
122123 . join ( ", " ) ;
123124}
124125
126+ function askQuestion ( question : string ) : Promise < string > {
127+ const rl = createInterface ( { input : process . stdin , output : process . stdout } ) ;
128+ return new Promise ( ( res ) => {
129+ rl . question ( question , ( answer ) => {
130+ rl . close ( ) ;
131+ res ( answer . trim ( ) ) ;
132+ } ) ;
133+ } ) ;
134+ }
135+
136+ function isGitRepo ( dir : string ) : boolean {
137+ try {
138+ execSync ( "git rev-parse --is-inside-work-tree" , { cwd : dir , stdio : "pipe" } ) ;
139+ return true ;
140+ } catch {
141+ return false ;
142+ }
143+ }
144+
145+ async function fileExists ( path : string ) : Promise < boolean > {
146+ try {
147+ await access ( path ) ;
148+ return true ;
149+ } catch {
150+ return false ;
151+ }
152+ }
153+
154+ async function ensureRepo ( dir : string , model ?: string ) : Promise < string > {
155+ const absDir = resolve ( dir ) ;
156+
157+ // Create directory if it doesn't exist
158+ if ( ! ( await fileExists ( absDir ) ) ) {
159+ console . log ( dim ( `Creating directory: ${ absDir } ` ) ) ;
160+ await mkdir ( absDir , { recursive : true } ) ;
161+ }
162+
163+ // Git init if not a repo
164+ if ( ! isGitRepo ( absDir ) ) {
165+ console . log ( dim ( "Initializing git repository..." ) ) ;
166+ execSync ( "git init" , { cwd : absDir , stdio : "pipe" } ) ;
167+
168+ // Create .gitignore
169+ const gitignorePath = join ( absDir , ".gitignore" ) ;
170+ if ( ! ( await fileExists ( gitignorePath ) ) ) {
171+ await writeFile ( gitignorePath , "node_modules/\ndist/\n.gitagent/\n" , "utf-8" ) ;
172+ }
173+
174+ // Initial commit so memory saves work
175+ execSync ( "git add -A && git commit -m 'Initial commit' --allow-empty" , {
176+ cwd : absDir ,
177+ stdio : "pipe" ,
178+ } ) ;
179+ }
180+
181+ // Scaffold agent.yaml if missing
182+ const agentYamlPath = join ( absDir , "agent.yaml" ) ;
183+ if ( ! ( await fileExists ( agentYamlPath ) ) ) {
184+ const defaultModel = model || "openai:gpt-4o-mini" ;
185+ const agentName = absDir . split ( "/" ) . pop ( ) || "my-agent" ;
186+ const yaml = [
187+ 'spec_version: "0.1.0"' ,
188+ `name: ${ agentName } ` ,
189+ "version: 0.1.0" ,
190+ `description: Gitclaw agent for ${ agentName } ` ,
191+ "model:" ,
192+ ` preferred: "${ defaultModel } "` ,
193+ " fallback: []" ,
194+ "tools: [cli, read, write, memory]" ,
195+ "runtime:" ,
196+ " max_turns: 50" ,
197+ "" ,
198+ ] . join ( "\n" ) ;
199+ await writeFile ( agentYamlPath , yaml , "utf-8" ) ;
200+ console . log ( dim ( `Created agent.yaml (model: ${ defaultModel } )` ) ) ;
201+ }
202+
203+ // Scaffold memory if missing
204+ const memoryDir = join ( absDir , "memory" ) ;
205+ const memoryFile = join ( memoryDir , "MEMORY.md" ) ;
206+ if ( ! ( await fileExists ( memoryFile ) ) ) {
207+ await mkdir ( memoryDir , { recursive : true } ) ;
208+ await writeFile ( memoryFile , "# Memory\n" , "utf-8" ) ;
209+ }
210+
211+ // Scaffold SOUL.md if missing
212+ const soulPath = join ( absDir , "SOUL.md" ) ;
213+ if ( ! ( await fileExists ( soulPath ) ) ) {
214+ await writeFile ( soulPath , [
215+ "# Identity" ,
216+ "" ,
217+ "You are a helpful AI agent. You live inside a git repository." ,
218+ "You can run commands, read and write files, and remember things." ,
219+ "Be concise and action-oriented." ,
220+ "" ,
221+ ] . join ( "\n" ) , "utf-8" ) ;
222+ }
223+
224+ // Stage new scaffolded files
225+ try {
226+ execSync ( "git add -A && git diff --cached --quiet || git commit -m 'Scaffold gitclaw agent'" , {
227+ cwd : absDir ,
228+ stdio : "pipe" ,
229+ } ) ;
230+ } catch {
231+ // ok if nothing to commit
232+ }
233+
234+ return absDir ;
235+ }
236+
125237async function main ( ) : Promise < void > {
126- const { model, dir, prompt, env } = parseArgs ( process . argv ) ;
238+ const { model, dir : rawDir , prompt, env } = parseArgs ( process . argv ) ;
239+
240+ // If no --dir given interactively, ask for it
241+ let dir = rawDir ;
242+ if ( dir === process . cwd ( ) && ! prompt ) {
243+ const answer = await askQuestion ( green ( "? " ) + bold ( "Repository path" ) + dim ( " (. for current dir)" ) + green ( ": " ) ) ;
244+ if ( answer ) {
245+ dir = resolve ( answer === "." ? process . cwd ( ) : answer ) ;
246+ }
247+ }
248+
249+ // Ensure the target is a valid gitclaw repo
250+ dir = await ensureRepo ( dir , model ) ;
127251
128252 let loaded ;
129253 try {
0 commit comments