agent-gauntlet 0.1.10 → 0.1.12
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/README.md +55 -87
- package/package.json +4 -2
- package/src/bun-plugins.d.ts +4 -0
- package/src/cli-adapters/claude.ts +139 -108
- package/src/cli-adapters/codex.ts +141 -117
- package/src/cli-adapters/cursor.ts +152 -0
- package/src/cli-adapters/gemini.ts +171 -139
- package/src/cli-adapters/github-copilot.ts +153 -0
- package/src/cli-adapters/index.ts +77 -48
- package/src/commands/check.test.ts +24 -20
- package/src/commands/check.ts +86 -59
- package/src/commands/ci/index.ts +15 -0
- package/src/commands/ci/init.ts +96 -0
- package/src/commands/ci/list-jobs.ts +78 -0
- package/src/commands/detect.test.ts +38 -32
- package/src/commands/detect.ts +89 -61
- package/src/commands/health.test.ts +67 -53
- package/src/commands/health.ts +167 -145
- package/src/commands/help.test.ts +37 -37
- package/src/commands/help.ts +31 -22
- package/src/commands/index.ts +10 -9
- package/src/commands/init.test.ts +120 -107
- package/src/commands/init.ts +514 -417
- package/src/commands/list.test.ts +87 -70
- package/src/commands/list.ts +28 -24
- package/src/commands/rerun.ts +157 -119
- package/src/commands/review.test.ts +26 -20
- package/src/commands/review.ts +86 -59
- package/src/commands/run.test.ts +22 -20
- package/src/commands/run.ts +85 -58
- package/src/commands/shared.ts +44 -35
- package/src/config/ci-loader.ts +33 -0
- package/src/config/ci-schema.ts +52 -0
- package/src/config/loader.test.ts +112 -90
- package/src/config/loader.ts +132 -123
- package/src/config/schema.ts +48 -47
- package/src/config/types.ts +28 -13
- package/src/config/validator.ts +521 -454
- package/src/core/change-detector.ts +122 -104
- package/src/core/entry-point.test.ts +60 -62
- package/src/core/entry-point.ts +120 -74
- package/src/core/job.ts +69 -59
- package/src/core/runner.ts +264 -230
- package/src/gates/check.ts +78 -69
- package/src/gates/result.ts +7 -7
- package/src/gates/review.test.ts +277 -138
- package/src/gates/review.ts +724 -561
- package/src/index.ts +18 -15
- package/src/output/console.ts +253 -214
- package/src/output/logger.ts +66 -52
- package/src/templates/run_gauntlet.template.md +18 -0
- package/src/templates/workflow.yml +77 -0
- package/src/utils/diff-parser.ts +64 -62
- package/src/utils/log-parser.ts +227 -206
- package/src/utils/sanitizer.ts +1 -1
|
@@ -1,149 +1,181 @@
|
|
|
1
|
-
import { exec } from
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import
|
|
1
|
+
import { exec } from "node:child_process";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
import { type CLIAdapter, isUsageLimit } from "./index.js";
|
|
7
7
|
|
|
8
8
|
const execAsync = promisify(exec);
|
|
9
9
|
const MAX_BUFFER_BYTES = 10 * 1024 * 1024;
|
|
10
10
|
|
|
11
11
|
export class GeminiAdapter implements CLIAdapter {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
12
|
+
name = "gemini";
|
|
13
|
+
|
|
14
|
+
async isAvailable(): Promise<boolean> {
|
|
15
|
+
try {
|
|
16
|
+
await execAsync("which gemini");
|
|
17
|
+
return true;
|
|
18
|
+
} catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async checkHealth(options?: { checkUsageLimit?: boolean }): Promise<{
|
|
24
|
+
available: boolean;
|
|
25
|
+
status: "healthy" | "missing" | "unhealthy";
|
|
26
|
+
message?: string;
|
|
27
|
+
}> {
|
|
28
|
+
const available = await this.isAvailable();
|
|
29
|
+
if (!available) {
|
|
30
|
+
return {
|
|
31
|
+
available: false,
|
|
32
|
+
status: "missing",
|
|
33
|
+
message: "Command not found",
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (options?.checkUsageLimit) {
|
|
38
|
+
try {
|
|
39
|
+
const { stdout, stderr } = await execAsync(
|
|
40
|
+
'echo "hello" | gemini --sandbox --output-format text',
|
|
41
|
+
{ timeout: 10000 },
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const combined = (stdout || "") + (stderr || "");
|
|
45
|
+
if (isUsageLimit(combined)) {
|
|
46
|
+
return {
|
|
47
|
+
available: true,
|
|
48
|
+
status: "unhealthy",
|
|
49
|
+
message: "Usage limit exceeded",
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return { available: true, status: "healthy", message: "Installed" };
|
|
54
|
+
} catch (error: unknown) {
|
|
55
|
+
const execError = error as {
|
|
56
|
+
stderr?: string;
|
|
57
|
+
stdout?: string;
|
|
58
|
+
message?: string;
|
|
59
|
+
};
|
|
60
|
+
const stderr = execError.stderr || "";
|
|
61
|
+
const stdout = execError.stdout || "";
|
|
62
|
+
const combined = stderr + stdout;
|
|
63
|
+
|
|
64
|
+
if (isUsageLimit(combined)) {
|
|
65
|
+
return {
|
|
66
|
+
available: true,
|
|
67
|
+
status: "unhealthy",
|
|
68
|
+
message: "Usage limit exceeded",
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Since we sent a valid prompt ("hello"), any other error implies the tool is broken
|
|
73
|
+
const cleanError =
|
|
74
|
+
combined.split("\n")[0]?.trim() ||
|
|
75
|
+
execError.message ||
|
|
76
|
+
"Command failed";
|
|
77
|
+
return {
|
|
78
|
+
available: true,
|
|
79
|
+
status: "unhealthy",
|
|
80
|
+
message: `Error: ${cleanError}`,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
available,
|
|
87
|
+
status: available ? "healthy" : "missing",
|
|
88
|
+
message: available ? "Installed" : "Command not found",
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
getProjectCommandDir(): string | null {
|
|
93
|
+
return ".gemini/commands";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
getUserCommandDir(): string | null {
|
|
97
|
+
// Gemini supports user-level commands at ~/.gemini/commands
|
|
98
|
+
return path.join(os.homedir(), ".gemini", "commands");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
getCommandExtension(): string {
|
|
102
|
+
return ".toml";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
canUseSymlink(): boolean {
|
|
106
|
+
// Gemini uses TOML format, needs transformation
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
transformCommand(markdownContent: string): string {
|
|
111
|
+
// Transform Markdown with YAML frontmatter to Gemini's TOML format
|
|
112
|
+
const { frontmatter, body } =
|
|
113
|
+
this.parseMarkdownWithFrontmatter(markdownContent);
|
|
114
|
+
|
|
115
|
+
const description =
|
|
116
|
+
frontmatter.description || "Run the gauntlet verification suite";
|
|
117
|
+
// Escape the body for TOML multi-line string
|
|
118
|
+
const escapedBody = body.trim();
|
|
119
|
+
|
|
120
|
+
return `description = ${JSON.stringify(description)}
|
|
100
121
|
prompt = """
|
|
101
122
|
${escapedBody}
|
|
102
123
|
"""
|
|
103
124
|
`;
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private parseMarkdownWithFrontmatter(content: string): {
|
|
128
|
+
frontmatter: Record<string, string>;
|
|
129
|
+
body: string;
|
|
130
|
+
} {
|
|
131
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
132
|
+
if (!frontmatterMatch) {
|
|
133
|
+
return { frontmatter: {}, body: content };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const frontmatterStr = frontmatterMatch[1] ?? "";
|
|
137
|
+
const body = frontmatterMatch[2] ?? "";
|
|
138
|
+
|
|
139
|
+
// Simple YAML parsing for key: value pairs
|
|
140
|
+
const frontmatter: Record<string, string> = {};
|
|
141
|
+
for (const line of frontmatterStr.split("\n")) {
|
|
142
|
+
const kvMatch = line.match(/^([^:]+):\s*(.*)$/);
|
|
143
|
+
if (kvMatch?.[1] && kvMatch[2] !== undefined) {
|
|
144
|
+
frontmatter[kvMatch[1].trim()] = kvMatch[2].trim();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return { frontmatter, body };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async execute(opts: {
|
|
152
|
+
prompt: string;
|
|
153
|
+
diff: string;
|
|
154
|
+
model?: string;
|
|
155
|
+
timeoutMs?: number;
|
|
156
|
+
}): Promise<string> {
|
|
157
|
+
// Construct the full prompt content
|
|
158
|
+
const fullContent = `${opts.prompt}\n\n--- DIFF ---\n${opts.diff}`;
|
|
159
|
+
|
|
160
|
+
// Write to a temporary file to avoid shell escaping issues
|
|
161
|
+
const tmpDir = os.tmpdir();
|
|
162
|
+
const tmpFile = path.join(tmpDir, `gauntlet-gemini-${Date.now()}.txt`);
|
|
163
|
+
await fs.writeFile(tmpFile, fullContent);
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
// Use gemini CLI with file input
|
|
167
|
+
// --sandbox: enables the execution sandbox
|
|
168
|
+
// --allowed-tools: whitelists read-only tools for non-interactive execution
|
|
169
|
+
// --output-format text: ensures plain text output
|
|
170
|
+
// Use < for stdin redirection instead of cat pipe (cleaner)
|
|
171
|
+
const cmd = `gemini --sandbox --allowed-tools read_file,list_directory,glob,search_file_content --output-format text < "${tmpFile}"`;
|
|
172
|
+
const { stdout } = await execAsync(cmd, {
|
|
173
|
+
timeout: opts.timeoutMs,
|
|
174
|
+
maxBuffer: MAX_BUFFER_BYTES,
|
|
175
|
+
});
|
|
176
|
+
return stdout;
|
|
177
|
+
} finally {
|
|
178
|
+
await fs.unlink(tmpFile).catch(() => {});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
149
181
|
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { exec } from "node:child_process";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
import { type CLIAdapter, isUsageLimit } from "./index.js";
|
|
7
|
+
|
|
8
|
+
const execAsync = promisify(exec);
|
|
9
|
+
const MAX_BUFFER_BYTES = 10 * 1024 * 1024;
|
|
10
|
+
|
|
11
|
+
export class GitHubCopilotAdapter implements CLIAdapter {
|
|
12
|
+
name = "github-copilot";
|
|
13
|
+
|
|
14
|
+
async isAvailable(): Promise<boolean> {
|
|
15
|
+
try {
|
|
16
|
+
await execAsync("which copilot");
|
|
17
|
+
return true;
|
|
18
|
+
} catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async checkHealth(options?: { checkUsageLimit?: boolean }): Promise<{
|
|
24
|
+
available: boolean;
|
|
25
|
+
status: "healthy" | "missing" | "unhealthy";
|
|
26
|
+
message?: string;
|
|
27
|
+
}> {
|
|
28
|
+
const available = await this.isAvailable();
|
|
29
|
+
if (!available) {
|
|
30
|
+
return {
|
|
31
|
+
available: false,
|
|
32
|
+
status: "missing",
|
|
33
|
+
message: "Command not found",
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (options?.checkUsageLimit) {
|
|
38
|
+
try {
|
|
39
|
+
// Try a lightweight command to check if we're rate limited
|
|
40
|
+
// Use minimal tool permissions for health check
|
|
41
|
+
const { stdout, stderr } = await execAsync(
|
|
42
|
+
'echo "hello" | copilot --allow-tool "shell(echo)"',
|
|
43
|
+
{ timeout: 10000 },
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const combined = (stdout || "") + (stderr || "");
|
|
47
|
+
if (isUsageLimit(combined)) {
|
|
48
|
+
return {
|
|
49
|
+
available: true,
|
|
50
|
+
status: "unhealthy",
|
|
51
|
+
message: "Usage limit exceeded",
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return { available: true, status: "healthy", message: "Ready" };
|
|
56
|
+
} catch (error: unknown) {
|
|
57
|
+
const execError = error as {
|
|
58
|
+
stderr?: string;
|
|
59
|
+
stdout?: string;
|
|
60
|
+
message?: string;
|
|
61
|
+
};
|
|
62
|
+
const stderr = execError.stderr || "";
|
|
63
|
+
const stdout = execError.stdout || "";
|
|
64
|
+
const combined = stderr + stdout;
|
|
65
|
+
|
|
66
|
+
if (isUsageLimit(combined)) {
|
|
67
|
+
return {
|
|
68
|
+
available: true,
|
|
69
|
+
status: "unhealthy",
|
|
70
|
+
message: "Usage limit exceeded",
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Since we sent a valid prompt ("hello"), any other error implies the tool is broken
|
|
75
|
+
const cleanError =
|
|
76
|
+
combined.split("\n")[0]?.trim() ||
|
|
77
|
+
execError.message ||
|
|
78
|
+
"Command failed";
|
|
79
|
+
return {
|
|
80
|
+
available: true,
|
|
81
|
+
status: "unhealthy",
|
|
82
|
+
message: `Error: ${cleanError}`,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return { available: true, status: "healthy", message: "Ready" };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
getProjectCommandDir(): string | null {
|
|
91
|
+
// GitHub Copilot CLI does not support custom commands (feature request #618)
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
getUserCommandDir(): string | null {
|
|
96
|
+
// GitHub Copilot CLI does not support custom commands (feature request #618)
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
getCommandExtension(): string {
|
|
101
|
+
return ".md";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
canUseSymlink(): boolean {
|
|
105
|
+
// Not applicable - no command directory support
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
transformCommand(markdownContent: string): string {
|
|
110
|
+
// Not applicable - no command directory support
|
|
111
|
+
return markdownContent;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async execute(opts: {
|
|
115
|
+
prompt: string;
|
|
116
|
+
diff: string;
|
|
117
|
+
model?: string;
|
|
118
|
+
timeoutMs?: number;
|
|
119
|
+
}): Promise<string> {
|
|
120
|
+
const fullContent = `${opts.prompt}\n\n--- DIFF ---\n${opts.diff}`;
|
|
121
|
+
|
|
122
|
+
const tmpDir = os.tmpdir();
|
|
123
|
+
// Include process.pid for uniqueness across concurrent processes
|
|
124
|
+
const tmpFile = path.join(
|
|
125
|
+
tmpDir,
|
|
126
|
+
`gauntlet-copilot-${process.pid}-${Date.now()}.txt`,
|
|
127
|
+
);
|
|
128
|
+
await fs.writeFile(tmpFile, fullContent);
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
// Copilot reads from stdin when no -p flag is provided
|
|
132
|
+
// Tool whitelist: cat/grep/ls/find/head/tail are required for the AI to read
|
|
133
|
+
// and analyze code files during review. While these tools can access files,
|
|
134
|
+
// they are read-only and necessary for code review functionality.
|
|
135
|
+
// The copilot CLI is scoped to the repo directory by default.
|
|
136
|
+
// git is excluded to prevent access to commit history (review should only see diff).
|
|
137
|
+
//
|
|
138
|
+
// Shell command construction: We use exec() with shell piping instead of execFile()
|
|
139
|
+
// because copilot requires stdin input. The tmpFile path is system-controlled
|
|
140
|
+
// (os.tmpdir() + Date.now() + process.pid), not user-supplied, eliminating injection risk.
|
|
141
|
+
// Double quotes handle paths with spaces. This pattern matches claude.ts:131.
|
|
142
|
+
const cmd = `cat "${tmpFile}" | copilot --allow-tool "shell(cat)" --allow-tool "shell(grep)" --allow-tool "shell(ls)" --allow-tool "shell(find)" --allow-tool "shell(head)" --allow-tool "shell(tail)"`;
|
|
143
|
+
const { stdout } = await execAsync(cmd, {
|
|
144
|
+
timeout: opts.timeoutMs,
|
|
145
|
+
maxBuffer: MAX_BUFFER_BYTES,
|
|
146
|
+
});
|
|
147
|
+
return stdout;
|
|
148
|
+
} finally {
|
|
149
|
+
// Cleanup errors are intentionally ignored - the tmp file will be cleaned up by OS
|
|
150
|
+
await fs.unlink(tmpFile).catch(() => {});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -1,79 +1,108 @@
|
|
|
1
1
|
export interface CLIAdapterHealth {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
available: boolean;
|
|
3
|
+
status: "healthy" | "missing" | "unhealthy";
|
|
4
|
+
message?: string;
|
|
5
5
|
}
|
|
6
6
|
|
|
7
7
|
export function isUsageLimit(output: string): boolean {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
8
|
+
const lower = output.toLowerCase();
|
|
9
|
+
return (
|
|
10
|
+
lower.includes("usage limit") ||
|
|
11
|
+
lower.includes("quota exceeded") ||
|
|
12
|
+
lower.includes("quota will reset") ||
|
|
13
|
+
lower.includes("credit balance is too low") ||
|
|
14
|
+
lower.includes("out of extra usage") ||
|
|
15
|
+
lower.includes("out of usage")
|
|
16
|
+
);
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
export interface CLIAdapter {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
20
|
+
name: string;
|
|
21
|
+
isAvailable(): Promise<boolean>;
|
|
22
|
+
checkHealth(options?: {
|
|
23
|
+
checkUsageLimit?: boolean;
|
|
24
|
+
}): Promise<CLIAdapterHealth>;
|
|
25
|
+
execute(opts: {
|
|
26
|
+
prompt: string;
|
|
27
|
+
diff: string;
|
|
28
|
+
model?: string;
|
|
29
|
+
timeoutMs?: number;
|
|
30
|
+
}): Promise<string>;
|
|
31
|
+
/**
|
|
32
|
+
* Returns the project-scoped command directory path (relative to project root).
|
|
33
|
+
* Returns null if the CLI only supports user-level commands.
|
|
34
|
+
*/
|
|
35
|
+
getProjectCommandDir(): string | null;
|
|
36
|
+
/**
|
|
37
|
+
* Returns the user-level command directory path (absolute path).
|
|
38
|
+
* Returns null if the CLI doesn't support user-level commands.
|
|
39
|
+
*/
|
|
40
|
+
getUserCommandDir(): string | null;
|
|
41
|
+
/**
|
|
42
|
+
* Returns the command file extension used by this CLI.
|
|
43
|
+
*/
|
|
44
|
+
getCommandExtension(): string;
|
|
45
|
+
/**
|
|
46
|
+
* Returns true if this adapter can use symlinks (same format as source Markdown).
|
|
47
|
+
*/
|
|
48
|
+
canUseSymlink(): boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Transforms gauntlet command content to this CLI's format.
|
|
51
|
+
* The source content is always Markdown with YAML frontmatter.
|
|
52
|
+
*/
|
|
53
|
+
transformCommand(markdownContent: string): string;
|
|
45
54
|
}
|
|
46
55
|
|
|
47
|
-
import {
|
|
48
|
-
import { CodexAdapter } from
|
|
49
|
-
import {
|
|
56
|
+
import { ClaudeAdapter } from "./claude.js";
|
|
57
|
+
import { CodexAdapter } from "./codex.js";
|
|
58
|
+
import { CursorAdapter } from "./cursor.js";
|
|
59
|
+
import { GeminiAdapter } from "./gemini.js";
|
|
60
|
+
import { GitHubCopilotAdapter } from "./github-copilot.js";
|
|
50
61
|
|
|
51
|
-
export {
|
|
62
|
+
export {
|
|
63
|
+
GeminiAdapter,
|
|
64
|
+
CodexAdapter,
|
|
65
|
+
ClaudeAdapter,
|
|
66
|
+
GitHubCopilotAdapter,
|
|
67
|
+
CursorAdapter,
|
|
68
|
+
};
|
|
52
69
|
|
|
70
|
+
// Adapter registry: keys should use lowercase with hyphens for multi-word names
|
|
53
71
|
const adapters: Record<string, CLIAdapter> = {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
72
|
+
gemini: new GeminiAdapter(),
|
|
73
|
+
codex: new CodexAdapter(),
|
|
74
|
+
claude: new ClaudeAdapter(),
|
|
75
|
+
"github-copilot": new GitHubCopilotAdapter(),
|
|
76
|
+
cursor: new CursorAdapter(),
|
|
57
77
|
};
|
|
58
78
|
|
|
59
79
|
export function getAdapter(name: string): CLIAdapter | undefined {
|
|
60
|
-
|
|
80
|
+
return adapters[name];
|
|
61
81
|
}
|
|
62
82
|
|
|
63
83
|
export function getAllAdapters(): CLIAdapter[] {
|
|
64
|
-
|
|
84
|
+
return Object.values(adapters);
|
|
65
85
|
}
|
|
66
86
|
|
|
67
87
|
/**
|
|
68
88
|
* Returns all adapters that support project-scoped commands.
|
|
69
89
|
*/
|
|
70
90
|
export function getProjectCommandAdapters(): CLIAdapter[] {
|
|
71
|
-
|
|
91
|
+
return Object.values(adapters).filter(
|
|
92
|
+
(a) => a.getProjectCommandDir() !== null,
|
|
93
|
+
);
|
|
72
94
|
}
|
|
73
95
|
|
|
74
96
|
/**
|
|
75
97
|
* Returns all adapters that support user-level commands.
|
|
76
98
|
*/
|
|
77
99
|
export function getUserCommandAdapters(): CLIAdapter[] {
|
|
78
|
-
|
|
100
|
+
return Object.values(adapters).filter((a) => a.getUserCommandDir() !== null);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Returns all valid CLI tool names (adapter registry keys).
|
|
105
|
+
*/
|
|
106
|
+
export function getValidCLITools(): string[] {
|
|
107
|
+
return Object.keys(adapters);
|
|
79
108
|
}
|