Skip to content

Commit 03b648e

Browse files
committed
feat: add gitclaw runner for run command
Creates temp workspace with full gitclaw directory structure (agent.yaml, SOUL.md, RULES.md, skills/, tools/, hooks/, knowledge/) and launches gitclaw CLI. Supports interactive and single-shot mode. Uses process.exitCode (not process.exit) for proper cleanup.
1 parent a69de38 commit 03b648e

File tree

2 files changed

+111
-2
lines changed

2 files changed

+111
-2
lines changed

src/commands/run.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { runWithGitHub } from '../runners/github.js';
1414
import { runWithGit } from '../runners/git.js';
1515
import { runWithOpenCode } from '../runners/opencode.js';
1616
import { runWithGemini } from '../runners/gemini.js';
17+
import { runWithGitclaw } from '../runners/gitclaw.js';
1718

1819
interface RunOptions {
1920
repo?: string;
@@ -28,7 +29,7 @@ interface RunOptions {
2829
export const runCommand = new Command('run')
2930
.description('Run an agent from a git repository or local directory')
3031
.option('-r, --repo <url>', 'Git repository URL')
31-
.option('-a, --adapter <name>', 'Adapter: claude, openai, crewai, openclaw, nanobot, lyzr, github, opencode, gemini, git, prompt', 'claude')
32+
.option('-a, --adapter <name>', 'Adapter: claude, openai, crewai, openclaw, nanobot, lyzr, github, opencode, gemini, gitclaw, git, prompt', 'claude')
3233
.option('-b, --branch <branch>', 'Git branch/tag to clone', 'main')
3334
.option('--refresh', 'Force re-clone (pull latest)', false)
3435
.option('--no-cache', 'Clone to temp dir, delete on exit')
@@ -120,6 +121,9 @@ export const runCommand = new Command('run')
120121
case 'gemini':
121122
runWithGemini(agentDir, manifest, { prompt: options.prompt });
122123
break;
124+
case 'gitclaw':
125+
runWithGitclaw(agentDir, manifest, { prompt: options.prompt });
126+
break;
123127
case 'git':
124128
if (!options.repo) {
125129
error('The git adapter requires --repo (-r)');
@@ -138,7 +142,7 @@ export const runCommand = new Command('run')
138142
break;
139143
default:
140144
error(`Unknown adapter: ${options.adapter}`);
141-
info('Supported adapters: claude, openai, crewai, openclaw, nanobot, lyzr, github, opencode, gemini, git, prompt');
145+
info('Supported adapters: claude, openai, crewai, openclaw, nanobot, lyzr, github, opencode, gemini, gitclaw, git, prompt');
142146
process.exit(1);
143147
}
144148
} catch (e) {

src/runners/gitclaw.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { writeFileSync, mkdirSync, rmSync } from 'node:fs';
2+
import { join } from 'node:path';
3+
import { tmpdir } from 'node:os';
4+
import { spawnSync } from 'node:child_process';
5+
import { randomBytes } from 'node:crypto';
6+
import { exportToGitclaw } from '../adapters/gitclaw.js';
7+
import { AgentManifest } from '../utils/loader.js';
8+
import { error, info } from '../utils/format.js';
9+
10+
export interface GitclawRunOptions {
11+
prompt?: string;
12+
}
13+
14+
/**
15+
* Run a gitagent agent using the gitclaw SDK.
16+
*
17+
* Creates a temporary workspace with the full gitclaw directory structure:
18+
* - agent.yaml (provider:model format)
19+
* - SOUL.md, RULES.md, DUTIES.md
20+
* - skills/, tools/, hooks/, knowledge/
21+
*
22+
* Then launches `gitclaw` CLI in that workspace.
23+
* Supports both interactive mode (no prompt) and single-shot mode (`gitclaw run -p`).
24+
*/
25+
export function runWithGitclaw(agentDir: string, manifest: AgentManifest, options: GitclawRunOptions = {}): void {
26+
const exp = exportToGitclaw(agentDir);
27+
28+
// Create a temporary workspace
29+
const workspaceDir = join(tmpdir(), `gitagent-gitclaw-${randomBytes(4).toString('hex')}`);
30+
mkdirSync(workspaceDir, { recursive: true });
31+
32+
// Write agent.yaml
33+
writeFileSync(join(workspaceDir, 'agent.yaml'), exp.agentYaml, 'utf-8');
34+
35+
// Write identity files
36+
if (exp.soulMd) writeFileSync(join(workspaceDir, 'SOUL.md'), exp.soulMd, 'utf-8');
37+
if (exp.rulesMd) writeFileSync(join(workspaceDir, 'RULES.md'), exp.rulesMd, 'utf-8');
38+
if (exp.dutiesMd) writeFileSync(join(workspaceDir, 'DUTIES.md'), exp.dutiesMd, 'utf-8');
39+
40+
// Write knowledge index
41+
if (exp.knowledgeIndex) {
42+
mkdirSync(join(workspaceDir, 'knowledge'), { recursive: true });
43+
writeFileSync(join(workspaceDir, 'knowledge', 'index.yaml'), exp.knowledgeIndex, 'utf-8');
44+
}
45+
46+
// Write skills
47+
for (const skill of exp.skills) {
48+
const skillDir = join(workspaceDir, 'skills', skill.name);
49+
mkdirSync(skillDir, { recursive: true });
50+
writeFileSync(join(skillDir, 'SKILL.md'), skill.content, 'utf-8');
51+
}
52+
53+
// Write tools
54+
if (exp.tools.length > 0) {
55+
mkdirSync(join(workspaceDir, 'tools'), { recursive: true });
56+
for (const tool of exp.tools) {
57+
writeFileSync(join(workspaceDir, 'tools', tool.name), tool.content, 'utf-8');
58+
}
59+
}
60+
61+
// Write hooks
62+
if (exp.hooks) {
63+
mkdirSync(join(workspaceDir, 'hooks'), { recursive: true });
64+
writeFileSync(join(workspaceDir, 'hooks', 'hooks.yaml'), exp.hooks, 'utf-8');
65+
}
66+
67+
info(`Workspace prepared at ${workspaceDir}`);
68+
info(` agent.yaml${exp.soulMd ? ', SOUL.md' : ''}${exp.rulesMd ? ', RULES.md' : ''}`);
69+
if (exp.skills.length > 0) {
70+
info(` Skills: ${exp.skills.map(s => s.name).join(', ')}`);
71+
}
72+
if (manifest.model?.preferred) {
73+
info(` Model: ${manifest.model.preferred}`);
74+
}
75+
76+
// Build gitclaw CLI args
77+
const args: string[] = [];
78+
79+
if (options.prompt) {
80+
args.push('run', '-p', options.prompt);
81+
}
82+
83+
info(`Launching gitclaw agent "${manifest.name}"...`);
84+
if (!options.prompt) {
85+
info('Starting interactive mode. Type your messages to chat.');
86+
}
87+
88+
const result = spawnSync('gitclaw', args, {
89+
stdio: 'inherit',
90+
cwd: workspaceDir,
91+
env: { ...process.env },
92+
});
93+
94+
// Cleanup temp workspace before exiting
95+
try { rmSync(workspaceDir, { recursive: true, force: true }); } catch { /* ignore */ }
96+
97+
if (result.error) {
98+
error(`Failed to launch gitclaw: ${result.error.message}`);
99+
info('Make sure gitclaw is installed: npm install -g gitclaw');
100+
process.exitCode = 1;
101+
return;
102+
}
103+
104+
process.exitCode = result.status ?? 0;
105+
}

0 commit comments

Comments
 (0)