@arvorco/relentless 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/relentless.analyze.md +20 -0
- package/.claude/commands/relentless.checklist.md +15 -0
- package/.claude/commands/relentless.clarify.md +19 -0
- package/.claude/commands/relentless.constitution.md +78 -0
- package/.claude/commands/relentless.implement.md +15 -0
- package/.claude/commands/relentless.plan.md +22 -0
- package/.claude/commands/relentless.plan.old.md +89 -0
- package/.claude/commands/relentless.specify.md +254 -0
- package/.claude/commands/relentless.tasks.md +25 -0
- package/.claude/commands/relentless.taskstoissues.md +15 -0
- package/.claude/settings.local.json +23 -0
- package/.claude/skills/analyze/SKILL.md +149 -0
- package/.claude/skills/checklist/SKILL.md +173 -0
- package/.claude/skills/checklist/templates/checklist-template.md +40 -0
- package/.claude/skills/clarify/SKILL.md +174 -0
- package/.claude/skills/constitution/SKILL.md +150 -0
- package/.claude/skills/constitution/templates/constitution-template.md +228 -0
- package/.claude/skills/implement/SKILL.md +141 -0
- package/.claude/skills/plan/SKILL.md +179 -0
- package/.claude/skills/plan/templates/plan-template.md +104 -0
- package/.claude/skills/prd/SKILL.md +242 -0
- package/.claude/skills/relentless/SKILL.md +265 -0
- package/.claude/skills/specify/SKILL.md +220 -0
- package/.claude/skills/specify/scripts/bash/check-prerequisites.sh +166 -0
- package/.claude/skills/specify/scripts/bash/common.sh +156 -0
- package/.claude/skills/specify/scripts/bash/create-new-feature.sh +305 -0
- package/.claude/skills/specify/scripts/bash/setup-plan.sh +61 -0
- package/.claude/skills/specify/scripts/bash/update-agent-context.sh +799 -0
- package/.claude/skills/specify/templates/spec-template.md +115 -0
- package/.claude/skills/tasks/SKILL.md +202 -0
- package/.claude/skills/tasks/templates/tasks-template.md +251 -0
- package/.claude/skills/taskstoissues/SKILL.md +97 -0
- package/.specify/memory/constitution.md +50 -0
- package/.specify/scripts/bash/check-prerequisites.sh +166 -0
- package/.specify/scripts/bash/common.sh +156 -0
- package/.specify/scripts/bash/create-new-feature.sh +297 -0
- package/.specify/scripts/bash/setup-plan.sh +61 -0
- package/.specify/scripts/bash/update-agent-context.sh +799 -0
- package/.specify/templates/agent-file-template.md +28 -0
- package/.specify/templates/checklist-template.md +40 -0
- package/.specify/templates/plan-template.md +104 -0
- package/.specify/templates/spec-template.md +115 -0
- package/.specify/templates/tasks-template.md +251 -0
- package/CHANGES_SUMMARY.md +255 -0
- package/CLAUDE.md +92 -0
- package/GEMINI_SETUP.md +256 -0
- package/LICENSE +21 -0
- package/README.md +1171 -0
- package/REFACTOR_SUMMARY.md +267 -0
- package/bin/relentless.ts +536 -0
- package/bun.lock +352 -0
- package/eslint.config.js +37 -0
- package/package.json +61 -0
- package/prd.json.example +64 -0
- package/prompt.md +108 -0
- package/ralph.sh +80 -0
- package/relentless/config.json +38 -0
- package/relentless/features/.gitkeep +0 -0
- package/relentless/features/ghsk-ideas/prd.json +229 -0
- package/relentless/features/ghsk-ideas/prd.md +191 -0
- package/relentless/features/ghsk-ideas/progress.txt +408 -0
- package/relentless/prompt.md +79 -0
- package/skills/checklist/SKILL.md +349 -0
- package/skills/clarify/SKILL.md +476 -0
- package/skills/prd/SKILL.md +242 -0
- package/skills/relentless/SKILL.md +268 -0
- package/skills/tasks/SKILL.md +577 -0
- package/src/agents/amp.ts +115 -0
- package/src/agents/claude.ts +185 -0
- package/src/agents/codex.ts +89 -0
- package/src/agents/droid.ts +90 -0
- package/src/agents/gemini.ts +109 -0
- package/src/agents/index.ts +16 -0
- package/src/agents/opencode.ts +88 -0
- package/src/agents/registry.ts +95 -0
- package/src/agents/types.ts +101 -0
- package/src/config/index.ts +8 -0
- package/src/config/loader.ts +237 -0
- package/src/config/schema.ts +115 -0
- package/src/execution/index.ts +8 -0
- package/src/execution/router.ts +49 -0
- package/src/execution/runner.ts +512 -0
- package/src/index.ts +11 -0
- package/src/init/index.ts +7 -0
- package/src/init/scaffolder.ts +377 -0
- package/src/prd/analyzer.ts +512 -0
- package/src/prd/index.ts +11 -0
- package/src/prd/issues.ts +249 -0
- package/src/prd/parser.ts +281 -0
- package/src/prd/progress.ts +198 -0
- package/src/prd/types.ts +170 -0
- package/src/tui/App.tsx +85 -0
- package/src/tui/TUIRunner.tsx +400 -0
- package/src/tui/components/AgentOutput.tsx +45 -0
- package/src/tui/components/AgentStatus.tsx +64 -0
- package/src/tui/components/CurrentStory.tsx +66 -0
- package/src/tui/components/Header.tsx +49 -0
- package/src/tui/components/ProgressBar.tsx +39 -0
- package/src/tui/components/StoryGrid.tsx +86 -0
- package/src/tui/hooks/useTUI.ts +147 -0
- package/src/tui/hooks/useTimer.ts +51 -0
- package/src/tui/index.tsx +17 -0
- package/src/tui/theme.ts +41 -0
- package/src/tui/types.ts +77 -0
- package/templates/constitution.md +228 -0
- package/templates/plan.md +273 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code Agent Adapter
|
|
3
|
+
*
|
|
4
|
+
* Adapter for Anthropic's Claude Code CLI
|
|
5
|
+
* https://docs.anthropic.com/claude-code
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { AgentAdapter, AgentResult, InvokeOptions, RateLimitInfo } from "./types.js";
|
|
9
|
+
|
|
10
|
+
export const claudeAdapter: AgentAdapter = {
|
|
11
|
+
name: "claude",
|
|
12
|
+
displayName: "Claude Code",
|
|
13
|
+
hasSkillSupport: true,
|
|
14
|
+
skillInstallCommand: "/plugin install github:ArvorCo/Relentless",
|
|
15
|
+
|
|
16
|
+
async isInstalled(): Promise<boolean> {
|
|
17
|
+
try {
|
|
18
|
+
const path = await this.getExecutablePath();
|
|
19
|
+
return path !== null;
|
|
20
|
+
} catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
async getExecutablePath(): Promise<string | null> {
|
|
26
|
+
try {
|
|
27
|
+
const proc = Bun.spawn(["which", "claude"], { stdout: "pipe" });
|
|
28
|
+
const output = await new Response(proc.stdout).text();
|
|
29
|
+
const exitCode = await proc.exited;
|
|
30
|
+
if (exitCode === 0 && output.trim()) {
|
|
31
|
+
return output.trim();
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
async invoke(prompt: string, options?: InvokeOptions): Promise<AgentResult> {
|
|
40
|
+
const startTime = Date.now();
|
|
41
|
+
const args = ["-p"];
|
|
42
|
+
|
|
43
|
+
if (options?.dangerouslyAllowAll) {
|
|
44
|
+
args.push("--dangerously-skip-permissions");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (options?.model) {
|
|
48
|
+
args.push("--model", options.model);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const proc = Bun.spawn(["claude", ...args], {
|
|
52
|
+
cwd: options?.workingDirectory,
|
|
53
|
+
stdin: new Blob([prompt]),
|
|
54
|
+
stdout: "pipe",
|
|
55
|
+
stderr: "pipe",
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Collect output
|
|
59
|
+
const stdout = await new Response(proc.stdout).text();
|
|
60
|
+
const stderr = await new Response(proc.stderr).text();
|
|
61
|
+
const exitCode = await proc.exited;
|
|
62
|
+
|
|
63
|
+
const output = stdout + (stderr ? `\n${stderr}` : "");
|
|
64
|
+
const duration = Date.now() - startTime;
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
output,
|
|
68
|
+
exitCode,
|
|
69
|
+
isComplete: this.detectCompletion(output),
|
|
70
|
+
duration,
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
async *invokeStream(
|
|
75
|
+
prompt: string,
|
|
76
|
+
options?: InvokeOptions
|
|
77
|
+
): AsyncGenerator<string, AgentResult, unknown> {
|
|
78
|
+
const startTime = Date.now();
|
|
79
|
+
const args = ["-p"];
|
|
80
|
+
|
|
81
|
+
if (options?.dangerouslyAllowAll) {
|
|
82
|
+
args.push("--dangerously-skip-permissions");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (options?.model) {
|
|
86
|
+
args.push("--model", options.model);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const proc = Bun.spawn(["claude", ...args], {
|
|
90
|
+
cwd: options?.workingDirectory,
|
|
91
|
+
stdin: new Blob([prompt]),
|
|
92
|
+
stdout: "pipe",
|
|
93
|
+
stderr: "pipe",
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const decoder = new TextDecoder();
|
|
97
|
+
let fullOutput = "";
|
|
98
|
+
|
|
99
|
+
// Stream stdout
|
|
100
|
+
const reader = proc.stdout.getReader();
|
|
101
|
+
try {
|
|
102
|
+
while (true) {
|
|
103
|
+
const { done, value } = await reader.read();
|
|
104
|
+
if (done) break;
|
|
105
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
106
|
+
fullOutput += chunk;
|
|
107
|
+
yield chunk;
|
|
108
|
+
}
|
|
109
|
+
} finally {
|
|
110
|
+
reader.releaseLock();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Collect any stderr
|
|
114
|
+
const stderr = await new Response(proc.stderr).text();
|
|
115
|
+
const exitCode = await proc.exited;
|
|
116
|
+
|
|
117
|
+
const output = fullOutput + (stderr ? `\n${stderr}` : "");
|
|
118
|
+
const duration = Date.now() - startTime;
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
output,
|
|
122
|
+
exitCode,
|
|
123
|
+
isComplete: this.detectCompletion(output),
|
|
124
|
+
duration,
|
|
125
|
+
};
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
detectCompletion(output: string): boolean {
|
|
129
|
+
return output.includes("<promise>COMPLETE</promise>");
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
detectRateLimit(output: string): RateLimitInfo {
|
|
133
|
+
// Pattern: "You've hit your limit · resets 12am (America/Sao_Paulo)"
|
|
134
|
+
if (output.includes("You've hit your limit") || output.includes("you've hit your limit")) {
|
|
135
|
+
const resetMatch = output.match(/resets\s+(\d{1,2})(am|pm)/i);
|
|
136
|
+
let resetTime: Date | undefined;
|
|
137
|
+
|
|
138
|
+
if (resetMatch) {
|
|
139
|
+
const hour = parseInt(resetMatch[1], 10);
|
|
140
|
+
const isPM = resetMatch[2].toLowerCase() === "pm";
|
|
141
|
+
const now = new Date();
|
|
142
|
+
|
|
143
|
+
resetTime = new Date(now);
|
|
144
|
+
resetTime.setHours(isPM && hour !== 12 ? hour + 12 : hour === 12 && !isPM ? 0 : hour);
|
|
145
|
+
resetTime.setMinutes(0);
|
|
146
|
+
resetTime.setSeconds(0);
|
|
147
|
+
resetTime.setMilliseconds(0);
|
|
148
|
+
|
|
149
|
+
// If reset time is in the past, move to tomorrow
|
|
150
|
+
if (resetTime <= now) {
|
|
151
|
+
resetTime.setDate(resetTime.getDate() + 1);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
limited: true,
|
|
157
|
+
resetTime,
|
|
158
|
+
message: "Claude Code rate limit exceeded",
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return { limited: false };
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
async installSkills(projectPath: string): Promise<void> {
|
|
166
|
+
// Claude Code reads skills from .claude/skills/ in the project
|
|
167
|
+
const skillsDir = `${projectPath}/.claude/skills`;
|
|
168
|
+
await Bun.spawn(["mkdir", "-p", skillsDir]).exited;
|
|
169
|
+
|
|
170
|
+
// Copy our skills to the project
|
|
171
|
+
const relentlessRoot = import.meta.dir.replace("/src/agents", "");
|
|
172
|
+
await Bun.spawn([
|
|
173
|
+
"cp",
|
|
174
|
+
"-r",
|
|
175
|
+
`${relentlessRoot}/skills/prd`,
|
|
176
|
+
`${skillsDir}/`,
|
|
177
|
+
]).exited;
|
|
178
|
+
await Bun.spawn([
|
|
179
|
+
"cp",
|
|
180
|
+
"-r",
|
|
181
|
+
`${relentlessRoot}/skills/relentless`,
|
|
182
|
+
`${skillsDir}/`,
|
|
183
|
+
]).exited;
|
|
184
|
+
},
|
|
185
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex Agent Adapter
|
|
3
|
+
*
|
|
4
|
+
* Adapter for OpenAI's Codex CLI
|
|
5
|
+
* https://developers.openai.com/codex/cli/
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { AgentAdapter, AgentResult, InvokeOptions, RateLimitInfo } from "./types";
|
|
9
|
+
|
|
10
|
+
export const codexAdapter: AgentAdapter = {
|
|
11
|
+
name: "codex",
|
|
12
|
+
displayName: "OpenAI Codex",
|
|
13
|
+
hasSkillSupport: false, // Uses SKILL.md but requires manual setup
|
|
14
|
+
|
|
15
|
+
async isInstalled(): Promise<boolean> {
|
|
16
|
+
try {
|
|
17
|
+
const path = await this.getExecutablePath();
|
|
18
|
+
return path !== null;
|
|
19
|
+
} catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
async getExecutablePath(): Promise<string | null> {
|
|
25
|
+
try {
|
|
26
|
+
const proc = Bun.spawn(["which", "codex"], { stdout: "pipe" });
|
|
27
|
+
const output = await new Response(proc.stdout).text();
|
|
28
|
+
const exitCode = await proc.exited;
|
|
29
|
+
if (exitCode === 0 && output.trim()) {
|
|
30
|
+
return output.trim();
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
async invoke(prompt: string, options?: InvokeOptions): Promise<AgentResult> {
|
|
39
|
+
const startTime = Date.now();
|
|
40
|
+
|
|
41
|
+
// Codex uses `codex exec -` to read from stdin
|
|
42
|
+
const proc = Bun.spawn(["codex", "exec", "-"], {
|
|
43
|
+
cwd: options?.workingDirectory,
|
|
44
|
+
stdin: new Blob([prompt]),
|
|
45
|
+
stdout: "pipe",
|
|
46
|
+
stderr: "pipe",
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Collect output
|
|
50
|
+
const stdout = await new Response(proc.stdout).text();
|
|
51
|
+
const stderr = await new Response(proc.stderr).text();
|
|
52
|
+
const exitCode = await proc.exited;
|
|
53
|
+
|
|
54
|
+
const output = stdout + (stderr ? `\n${stderr}` : "");
|
|
55
|
+
const duration = Date.now() - startTime;
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
output,
|
|
59
|
+
exitCode,
|
|
60
|
+
isComplete: this.detectCompletion(output),
|
|
61
|
+
duration,
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
detectCompletion(output: string): boolean {
|
|
66
|
+
return output.includes("<promise>COMPLETE</promise>");
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
detectRateLimit(output: string): RateLimitInfo {
|
|
70
|
+
// Codex/OpenAI rate limit patterns
|
|
71
|
+
const patterns = [
|
|
72
|
+
/rate limit exceeded/i,
|
|
73
|
+
/\b429\b/,
|
|
74
|
+
/too many requests/i,
|
|
75
|
+
/quota exceeded/i,
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
for (const pattern of patterns) {
|
|
79
|
+
if (pattern.test(output)) {
|
|
80
|
+
return {
|
|
81
|
+
limited: true,
|
|
82
|
+
message: "OpenAI Codex rate limit exceeded",
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return { limited: false };
|
|
88
|
+
},
|
|
89
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Droid Agent Adapter
|
|
3
|
+
*
|
|
4
|
+
* Adapter for Factory's Droid CLI
|
|
5
|
+
* https://factory.ai
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { AgentAdapter, AgentResult, InvokeOptions, RateLimitInfo } from "./types";
|
|
9
|
+
|
|
10
|
+
export const droidAdapter: AgentAdapter = {
|
|
11
|
+
name: "droid",
|
|
12
|
+
displayName: "Factory Droid",
|
|
13
|
+
hasSkillSupport: false, // No skill system, uses prompting
|
|
14
|
+
|
|
15
|
+
async isInstalled(): Promise<boolean> {
|
|
16
|
+
try {
|
|
17
|
+
const path = await this.getExecutablePath();
|
|
18
|
+
return path !== null;
|
|
19
|
+
} catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
async getExecutablePath(): Promise<string | null> {
|
|
25
|
+
try {
|
|
26
|
+
const proc = Bun.spawn(["which", "droid"], { stdout: "pipe" });
|
|
27
|
+
const output = await new Response(proc.stdout).text();
|
|
28
|
+
const exitCode = await proc.exited;
|
|
29
|
+
if (exitCode === 0 && output.trim()) {
|
|
30
|
+
return output.trim();
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
async invoke(prompt: string, options?: InvokeOptions): Promise<AgentResult> {
|
|
39
|
+
const startTime = Date.now();
|
|
40
|
+
|
|
41
|
+
// Droid reads from stdin when piped or redirected.
|
|
42
|
+
// Use --auto high for high risk tolerance by default
|
|
43
|
+
const proc = Bun.spawn(["droid", "exec", "--auto", "high"], {
|
|
44
|
+
cwd: options?.workingDirectory,
|
|
45
|
+
stdin: new Blob([prompt]),
|
|
46
|
+
stdout: "pipe",
|
|
47
|
+
stderr: "pipe",
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Collect output
|
|
51
|
+
const stdout = await new Response(proc.stdout).text();
|
|
52
|
+
const stderr = await new Response(proc.stderr).text();
|
|
53
|
+
const exitCode = await proc.exited;
|
|
54
|
+
|
|
55
|
+
const output = stdout + (stderr ? `\n${stderr}` : "");
|
|
56
|
+
const duration = Date.now() - startTime;
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
output,
|
|
60
|
+
exitCode,
|
|
61
|
+
isComplete: this.detectCompletion(output),
|
|
62
|
+
duration,
|
|
63
|
+
};
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
detectCompletion(output: string): boolean {
|
|
67
|
+
return output.includes("<promise>COMPLETE</promise>");
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
detectRateLimit(output: string): RateLimitInfo {
|
|
71
|
+
// Droid rate limit patterns
|
|
72
|
+
const patterns = [
|
|
73
|
+
/rate limit/i,
|
|
74
|
+
/\b429\b/,
|
|
75
|
+
/too many requests/i,
|
|
76
|
+
/quota exceeded/i,
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
for (const pattern of patterns) {
|
|
80
|
+
if (pattern.test(output)) {
|
|
81
|
+
return {
|
|
82
|
+
limited: true,
|
|
83
|
+
message: "Factory Droid rate limit exceeded",
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return { limited: false };
|
|
89
|
+
},
|
|
90
|
+
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gemini Agent Adapter
|
|
3
|
+
*
|
|
4
|
+
* Adapter for Google's Gemini CLI
|
|
5
|
+
* https://github.com/google-gemini/gemini-cli
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { AgentAdapter, AgentResult, InvokeOptions, RateLimitInfo } from "./types";
|
|
9
|
+
|
|
10
|
+
export const geminiAdapter: AgentAdapter = {
|
|
11
|
+
name: "gemini",
|
|
12
|
+
displayName: "Gemini CLI",
|
|
13
|
+
hasSkillSupport: true, // Uses extension system
|
|
14
|
+
skillInstallCommand: "gemini extensions install https://github.com/ArvorCo/Relentless",
|
|
15
|
+
|
|
16
|
+
async isInstalled(): Promise<boolean> {
|
|
17
|
+
try {
|
|
18
|
+
const path = await this.getExecutablePath();
|
|
19
|
+
return path !== null;
|
|
20
|
+
} catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
async getExecutablePath(): Promise<string | null> {
|
|
26
|
+
try {
|
|
27
|
+
const proc = Bun.spawn(["which", "gemini"], { stdout: "pipe" });
|
|
28
|
+
const output = await new Response(proc.stdout).text();
|
|
29
|
+
const exitCode = await proc.exited;
|
|
30
|
+
if (exitCode === 0 && output.trim()) {
|
|
31
|
+
return output.trim();
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
async invoke(prompt: string, options?: InvokeOptions): Promise<AgentResult> {
|
|
40
|
+
const startTime = Date.now();
|
|
41
|
+
const args: string[] = [];
|
|
42
|
+
|
|
43
|
+
if (options?.dangerouslyAllowAll) {
|
|
44
|
+
args.push("--yolo");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (options?.model) {
|
|
48
|
+
args.push("--model", options.model);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Gemini CLI accepts prompt as positional argument
|
|
52
|
+
args.push(prompt);
|
|
53
|
+
|
|
54
|
+
const proc = Bun.spawn(["gemini", ...args], {
|
|
55
|
+
cwd: options?.workingDirectory,
|
|
56
|
+
stdout: "pipe",
|
|
57
|
+
stderr: "pipe",
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Collect output
|
|
61
|
+
const stdout = await new Response(proc.stdout).text();
|
|
62
|
+
const stderr = await new Response(proc.stderr).text();
|
|
63
|
+
const exitCode = await proc.exited;
|
|
64
|
+
|
|
65
|
+
const output = stdout + (stderr ? `\n${stderr}` : "");
|
|
66
|
+
const duration = Date.now() - startTime;
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
output,
|
|
70
|
+
exitCode,
|
|
71
|
+
isComplete: this.detectCompletion(output),
|
|
72
|
+
duration,
|
|
73
|
+
};
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
detectCompletion(output: string): boolean {
|
|
77
|
+
return output.includes("<promise>COMPLETE</promise>");
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
detectRateLimit(output: string): RateLimitInfo {
|
|
81
|
+
// Gemini rate limit patterns
|
|
82
|
+
const patterns = [
|
|
83
|
+
/quota exceeded/i,
|
|
84
|
+
/resource exhausted/i,
|
|
85
|
+
/rate limit/i,
|
|
86
|
+
/\b429\b/,
|
|
87
|
+
/too many requests/i,
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
for (const pattern of patterns) {
|
|
91
|
+
if (pattern.test(output)) {
|
|
92
|
+
return {
|
|
93
|
+
limited: true,
|
|
94
|
+
message: "Gemini rate limit exceeded",
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return { limited: false };
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
async installSkills(_projectPath: string): Promise<void> {
|
|
103
|
+
// Gemini uses extensions, we can't install project-local
|
|
104
|
+
// Users need to run: gemini extensions install <url>
|
|
105
|
+
console.log(
|
|
106
|
+
`To install Relentless skills for Gemini, run:\n ${this.skillInstallCommand}`
|
|
107
|
+
);
|
|
108
|
+
},
|
|
109
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Adapters Module
|
|
3
|
+
*
|
|
4
|
+
* Re-exports all agent-related types and functions
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export * from "./types";
|
|
8
|
+
export * from "./registry";
|
|
9
|
+
|
|
10
|
+
// Individual adapters (for direct access if needed)
|
|
11
|
+
export { claudeAdapter } from "./claude";
|
|
12
|
+
export { ampAdapter } from "./amp";
|
|
13
|
+
export { opencodeAdapter } from "./opencode";
|
|
14
|
+
export { codexAdapter } from "./codex";
|
|
15
|
+
export { droidAdapter } from "./droid";
|
|
16
|
+
export { geminiAdapter } from "./gemini";
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCode Agent Adapter
|
|
3
|
+
*
|
|
4
|
+
* Adapter for the OpenCode CLI
|
|
5
|
+
* https://opencode.ai
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { AgentAdapter, AgentResult, InvokeOptions, RateLimitInfo } from "./types";
|
|
9
|
+
|
|
10
|
+
export const opencodeAdapter: AgentAdapter = {
|
|
11
|
+
name: "opencode",
|
|
12
|
+
displayName: "OpenCode",
|
|
13
|
+
hasSkillSupport: false, // Uses agent system, different from skills
|
|
14
|
+
|
|
15
|
+
async isInstalled(): Promise<boolean> {
|
|
16
|
+
try {
|
|
17
|
+
const path = await this.getExecutablePath();
|
|
18
|
+
return path !== null;
|
|
19
|
+
} catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
async getExecutablePath(): Promise<string | null> {
|
|
25
|
+
try {
|
|
26
|
+
const proc = Bun.spawn(["which", "opencode"], { stdout: "pipe" });
|
|
27
|
+
const output = await new Response(proc.stdout).text();
|
|
28
|
+
const exitCode = await proc.exited;
|
|
29
|
+
if (exitCode === 0 && output.trim()) {
|
|
30
|
+
return output.trim();
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
async invoke(prompt: string, options?: InvokeOptions): Promise<AgentResult> {
|
|
39
|
+
const startTime = Date.now();
|
|
40
|
+
|
|
41
|
+
// OpenCode uses `opencode run "message"` for non-interactive mode
|
|
42
|
+
const proc = Bun.spawn(["opencode", "run", prompt], {
|
|
43
|
+
cwd: options?.workingDirectory,
|
|
44
|
+
stdout: "pipe",
|
|
45
|
+
stderr: "pipe",
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Collect output
|
|
49
|
+
const stdout = await new Response(proc.stdout).text();
|
|
50
|
+
const stderr = await new Response(proc.stderr).text();
|
|
51
|
+
const exitCode = await proc.exited;
|
|
52
|
+
|
|
53
|
+
const output = stdout + (stderr ? `\n${stderr}` : "");
|
|
54
|
+
const duration = Date.now() - startTime;
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
output,
|
|
58
|
+
exitCode,
|
|
59
|
+
isComplete: this.detectCompletion(output),
|
|
60
|
+
duration,
|
|
61
|
+
};
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
detectCompletion(output: string): boolean {
|
|
65
|
+
return output.includes("<promise>COMPLETE</promise>");
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
detectRateLimit(output: string): RateLimitInfo {
|
|
69
|
+
// OpenCode rate limit patterns
|
|
70
|
+
const patterns = [
|
|
71
|
+
/rate limited/i,
|
|
72
|
+
/try again later/i,
|
|
73
|
+
/quota exceeded/i,
|
|
74
|
+
/\b429\b/,
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
for (const pattern of patterns) {
|
|
78
|
+
if (pattern.test(output)) {
|
|
79
|
+
return {
|
|
80
|
+
limited: true,
|
|
81
|
+
message: "OpenCode rate limit exceeded",
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return { limited: false };
|
|
87
|
+
},
|
|
88
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Registry
|
|
3
|
+
*
|
|
4
|
+
* Central registry for all supported AI coding agents.
|
|
5
|
+
* Provides agent discovery, selection, and health checks.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { AgentAdapter, AgentName } from "./types";
|
|
9
|
+
import { claudeAdapter } from "./claude";
|
|
10
|
+
import { ampAdapter } from "./amp";
|
|
11
|
+
import { opencodeAdapter } from "./opencode";
|
|
12
|
+
import { codexAdapter } from "./codex";
|
|
13
|
+
import { droidAdapter } from "./droid";
|
|
14
|
+
import { geminiAdapter } from "./gemini";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Map of all registered agent adapters
|
|
18
|
+
*/
|
|
19
|
+
export const AGENTS: Record<AgentName, AgentAdapter> = {
|
|
20
|
+
claude: claudeAdapter,
|
|
21
|
+
amp: ampAdapter,
|
|
22
|
+
opencode: opencodeAdapter,
|
|
23
|
+
codex: codexAdapter,
|
|
24
|
+
droid: droidAdapter,
|
|
25
|
+
gemini: geminiAdapter,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get an agent adapter by name
|
|
30
|
+
*/
|
|
31
|
+
export function getAgent(name: AgentName): AgentAdapter {
|
|
32
|
+
const agent = AGENTS[name];
|
|
33
|
+
if (!agent) {
|
|
34
|
+
throw new Error(`Unknown agent: ${name}`);
|
|
35
|
+
}
|
|
36
|
+
return agent;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Get all agent names
|
|
41
|
+
*/
|
|
42
|
+
export function getAllAgentNames(): AgentName[] {
|
|
43
|
+
return Object.keys(AGENTS) as AgentName[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Check which agents are installed on the system
|
|
48
|
+
*/
|
|
49
|
+
export async function getInstalledAgents(): Promise<AgentAdapter[]> {
|
|
50
|
+
const results = await Promise.all(
|
|
51
|
+
Object.values(AGENTS).map(async (agent) => ({
|
|
52
|
+
agent,
|
|
53
|
+
installed: await agent.isInstalled(),
|
|
54
|
+
}))
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
return results.filter((r) => r.installed).map((r) => r.agent);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Agent health check result
|
|
62
|
+
*/
|
|
63
|
+
export interface AgentHealthResult {
|
|
64
|
+
name: AgentName;
|
|
65
|
+
displayName: string;
|
|
66
|
+
installed: boolean;
|
|
67
|
+
executablePath: string | null;
|
|
68
|
+
hasSkillSupport: boolean;
|
|
69
|
+
skillInstallCommand?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Run health checks on all agents
|
|
74
|
+
*/
|
|
75
|
+
export async function checkAgentHealth(): Promise<AgentHealthResult[]> {
|
|
76
|
+
const results = await Promise.all(
|
|
77
|
+
Object.values(AGENTS).map(async (agent) => ({
|
|
78
|
+
name: agent.name,
|
|
79
|
+
displayName: agent.displayName,
|
|
80
|
+
installed: await agent.isInstalled(),
|
|
81
|
+
executablePath: await agent.getExecutablePath(),
|
|
82
|
+
hasSkillSupport: agent.hasSkillSupport,
|
|
83
|
+
skillInstallCommand: agent.skillInstallCommand,
|
|
84
|
+
}))
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
return results;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Validate that an agent name is valid
|
|
92
|
+
*/
|
|
93
|
+
export function isValidAgentName(name: string): name is AgentName {
|
|
94
|
+
return name in AGENTS;
|
|
95
|
+
}
|