@nathapp/nax 0.35.0 → 0.36.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/bin/nax.ts +18 -9
- package/dist/nax.js +1064 -560
- package/package.json +1 -1
- package/src/agents/adapters/aider.ts +135 -0
- package/src/agents/adapters/gemini.ts +177 -0
- package/src/agents/adapters/opencode.ts +106 -0
- package/src/agents/index.ts +2 -0
- package/src/agents/registry.ts +6 -2
- package/src/agents/version-detection.ts +109 -0
- package/src/cli/agents.ts +87 -0
- package/src/cli/config.ts +28 -14
- package/src/cli/generate.ts +1 -1
- package/src/cli/index.ts +1 -0
- package/src/context/generator.ts +4 -0
- package/src/context/generators/codex.ts +28 -0
- package/src/context/generators/gemini.ts +28 -0
- package/src/context/types.ts +1 -1
- package/src/pipeline/stages/execution.ts +2 -39
- package/src/pipeline/stages/routing.ts +8 -2
- package/src/precheck/checks-agents.ts +63 -0
- package/src/precheck/checks.ts +3 -0
- package/src/precheck/index.ts +2 -0
- package/src/tdd/rectification-gate.ts +2 -46
- package/src/tdd/session-runner.ts +2 -49
- package/src/tdd/verdict.ts +135 -8
- package/src/utils/git.ts +49 -0
package/package.json
CHANGED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Aider Agent Adapter — implements AgentAdapter interface
|
|
3
|
+
*
|
|
4
|
+
* Provides uniform interface for spawning Aider agent processes,
|
|
5
|
+
* supporting one-shot completions in headless mode.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
AgentAdapter,
|
|
10
|
+
AgentCapabilities,
|
|
11
|
+
AgentResult,
|
|
12
|
+
AgentRunOptions,
|
|
13
|
+
CompleteOptions,
|
|
14
|
+
DecomposeOptions,
|
|
15
|
+
DecomposeResult,
|
|
16
|
+
PlanOptions,
|
|
17
|
+
PlanResult,
|
|
18
|
+
} from "../types";
|
|
19
|
+
import { CompleteError } from "../types";
|
|
20
|
+
|
|
21
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
22
|
+
// Injectable dependencies — matches the _deps pattern used in other adapters
|
|
23
|
+
// These are replaced in unit tests to intercept Bun.spawn and Bun.which calls.
|
|
24
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
export const _aiderCompleteDeps = {
|
|
27
|
+
which(name: string): string | null {
|
|
28
|
+
return Bun.which(name);
|
|
29
|
+
},
|
|
30
|
+
spawn(
|
|
31
|
+
cmd: string[],
|
|
32
|
+
opts: { stdout: "pipe"; stderr: "pipe" | "inherit" },
|
|
33
|
+
): {
|
|
34
|
+
stdout: ReadableStream<Uint8Array>;
|
|
35
|
+
stderr: ReadableStream<Uint8Array>;
|
|
36
|
+
exited: Promise<number>;
|
|
37
|
+
pid: number;
|
|
38
|
+
} {
|
|
39
|
+
return Bun.spawn(cmd, opts) as unknown as {
|
|
40
|
+
stdout: ReadableStream<Uint8Array>;
|
|
41
|
+
stderr: ReadableStream<Uint8Array>;
|
|
42
|
+
exited: Promise<number>;
|
|
43
|
+
pid: number;
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
49
|
+
// AiderAdapter implementation
|
|
50
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Maximum characters to capture from agent stdout.
|
|
54
|
+
*/
|
|
55
|
+
const MAX_AGENT_OUTPUT_CHARS = 5000;
|
|
56
|
+
|
|
57
|
+
export class AiderAdapter implements AgentAdapter {
|
|
58
|
+
readonly name = "aider";
|
|
59
|
+
readonly displayName = "Aider";
|
|
60
|
+
readonly binary = "aider";
|
|
61
|
+
|
|
62
|
+
readonly capabilities: AgentCapabilities = {
|
|
63
|
+
supportedTiers: ["balanced"],
|
|
64
|
+
maxContextTokens: 20_000,
|
|
65
|
+
features: new Set<"tdd" | "review" | "refactor" | "batch">(["refactor"]),
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
async isInstalled(): Promise<boolean> {
|
|
69
|
+
const path = _aiderCompleteDeps.which("aider");
|
|
70
|
+
return path !== null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
buildCommand(options: AgentRunOptions): string[] {
|
|
74
|
+
return ["aider", "--message", options.prompt, "--yes"];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async run(options: AgentRunOptions): Promise<AgentResult> {
|
|
78
|
+
const cmd = this.buildCommand(options);
|
|
79
|
+
const startTime = Date.now();
|
|
80
|
+
|
|
81
|
+
const proc = _aiderCompleteDeps.spawn(cmd, {
|
|
82
|
+
stdout: "pipe",
|
|
83
|
+
stderr: "inherit",
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const exitCode = await proc.exited;
|
|
87
|
+
const stdout = await new Response(proc.stdout).text();
|
|
88
|
+
const durationMs = Date.now() - startTime;
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
success: exitCode === 0,
|
|
92
|
+
exitCode,
|
|
93
|
+
output: stdout.slice(-MAX_AGENT_OUTPUT_CHARS),
|
|
94
|
+
rateLimited: false,
|
|
95
|
+
durationMs,
|
|
96
|
+
estimatedCost: 0,
|
|
97
|
+
pid: proc.pid,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async complete(prompt: string, options?: CompleteOptions): Promise<string> {
|
|
102
|
+
const cmd = ["aider", "--message", prompt, "--yes"];
|
|
103
|
+
|
|
104
|
+
if (options?.model) {
|
|
105
|
+
cmd.push("--model", options.model);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const proc = _aiderCompleteDeps.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
|
|
109
|
+
const exitCode = await proc.exited;
|
|
110
|
+
|
|
111
|
+
const stdout = await new Response(proc.stdout).text();
|
|
112
|
+
const stderr = await new Response(proc.stderr).text();
|
|
113
|
+
const trimmed = stdout.trim();
|
|
114
|
+
|
|
115
|
+
if (exitCode !== 0) {
|
|
116
|
+
const errorDetails = stderr.trim() || trimmed;
|
|
117
|
+
const errorMessage = errorDetails || `complete() failed with exit code ${exitCode}`;
|
|
118
|
+
throw new CompleteError(errorMessage, exitCode);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!trimmed) {
|
|
122
|
+
throw new CompleteError("complete() returned empty output");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return trimmed;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async plan(_options: PlanOptions): Promise<PlanResult> {
|
|
129
|
+
throw new Error("AiderAdapter.plan() not implemented");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async decompose(_options: DecomposeOptions): Promise<DecomposeResult> {
|
|
133
|
+
throw new Error("AiderAdapter.decompose() not implemented");
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gemini CLI Agent Adapter — implements AgentAdapter interface
|
|
3
|
+
*
|
|
4
|
+
* Provides uniform interface for spawning Gemini CLI processes,
|
|
5
|
+
* supporting one-shot completions via 'gemini -p' and Google auth detection.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
AgentAdapter,
|
|
10
|
+
AgentCapabilities,
|
|
11
|
+
AgentResult,
|
|
12
|
+
AgentRunOptions,
|
|
13
|
+
CompleteOptions,
|
|
14
|
+
DecomposeOptions,
|
|
15
|
+
DecomposeResult,
|
|
16
|
+
PlanOptions,
|
|
17
|
+
PlanResult,
|
|
18
|
+
} from "../types";
|
|
19
|
+
import { CompleteError } from "../types";
|
|
20
|
+
|
|
21
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
22
|
+
// Injectable dependencies — follows the _deps pattern
|
|
23
|
+
// Replaced in unit tests to intercept Bun.spawn/Bun.which calls.
|
|
24
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
export const _geminiRunDeps = {
|
|
27
|
+
which(name: string): string | null {
|
|
28
|
+
return Bun.which(name);
|
|
29
|
+
},
|
|
30
|
+
spawn(
|
|
31
|
+
cmd: string[],
|
|
32
|
+
opts: { cwd?: string; stdout: "pipe"; stderr: "pipe" | "inherit"; env?: Record<string, string | undefined> },
|
|
33
|
+
): {
|
|
34
|
+
stdout: ReadableStream<Uint8Array>;
|
|
35
|
+
stderr: ReadableStream<Uint8Array>;
|
|
36
|
+
exited: Promise<number>;
|
|
37
|
+
pid: number;
|
|
38
|
+
kill(signal?: number | NodeJS.Signals): void;
|
|
39
|
+
} {
|
|
40
|
+
return Bun.spawn(cmd, opts) as unknown as {
|
|
41
|
+
stdout: ReadableStream<Uint8Array>;
|
|
42
|
+
stderr: ReadableStream<Uint8Array>;
|
|
43
|
+
exited: Promise<number>;
|
|
44
|
+
pid: number;
|
|
45
|
+
kill(signal?: number | NodeJS.Signals): void;
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const _geminiCompleteDeps = {
|
|
51
|
+
spawn(
|
|
52
|
+
cmd: string[],
|
|
53
|
+
opts: { stdout: "pipe"; stderr: "pipe" | "inherit" },
|
|
54
|
+
): {
|
|
55
|
+
stdout: ReadableStream<Uint8Array>;
|
|
56
|
+
stderr: ReadableStream<Uint8Array>;
|
|
57
|
+
exited: Promise<number>;
|
|
58
|
+
pid: number;
|
|
59
|
+
} {
|
|
60
|
+
return Bun.spawn(cmd, opts) as unknown as {
|
|
61
|
+
stdout: ReadableStream<Uint8Array>;
|
|
62
|
+
stderr: ReadableStream<Uint8Array>;
|
|
63
|
+
exited: Promise<number>;
|
|
64
|
+
pid: number;
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
70
|
+
// GeminiAdapter implementation
|
|
71
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
const MAX_AGENT_OUTPUT_CHARS = 5000;
|
|
74
|
+
|
|
75
|
+
export class GeminiAdapter implements AgentAdapter {
|
|
76
|
+
readonly name = "gemini";
|
|
77
|
+
readonly displayName = "Gemini CLI";
|
|
78
|
+
readonly binary = "gemini";
|
|
79
|
+
|
|
80
|
+
readonly capabilities: AgentCapabilities = {
|
|
81
|
+
supportedTiers: ["fast", "balanced", "powerful"],
|
|
82
|
+
maxContextTokens: 1_000_000,
|
|
83
|
+
features: new Set<"tdd" | "review" | "refactor" | "batch">(["tdd", "review", "refactor"]),
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
async isInstalled(): Promise<boolean> {
|
|
87
|
+
const path = _geminiRunDeps.which("gemini");
|
|
88
|
+
if (path === null) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Check Google auth — run 'gemini' with a flag that shows auth status
|
|
93
|
+
try {
|
|
94
|
+
const proc = _geminiRunDeps.spawn(["gemini", "--version"], {
|
|
95
|
+
stdout: "pipe",
|
|
96
|
+
stderr: "pipe",
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const exitCode = await proc.exited;
|
|
100
|
+
if (exitCode !== 0) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const stdout = await new Response(proc.stdout).text();
|
|
105
|
+
const lowerOut = stdout.toLowerCase();
|
|
106
|
+
|
|
107
|
+
// If output explicitly says "not logged in", auth has failed
|
|
108
|
+
if (lowerOut.includes("not logged in")) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return true;
|
|
113
|
+
} catch {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
buildCommand(options: AgentRunOptions): string[] {
|
|
119
|
+
return ["gemini", "-p", options.prompt];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async run(options: AgentRunOptions): Promise<AgentResult> {
|
|
123
|
+
const cmd = this.buildCommand(options);
|
|
124
|
+
const startTime = Date.now();
|
|
125
|
+
|
|
126
|
+
const proc = _geminiRunDeps.spawn(cmd, {
|
|
127
|
+
cwd: options.workdir,
|
|
128
|
+
stdout: "pipe",
|
|
129
|
+
stderr: "inherit",
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const exitCode = await proc.exited;
|
|
133
|
+
const stdout = await new Response(proc.stdout).text();
|
|
134
|
+
const durationMs = Date.now() - startTime;
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
success: exitCode === 0,
|
|
138
|
+
exitCode,
|
|
139
|
+
output: stdout.slice(-MAX_AGENT_OUTPUT_CHARS),
|
|
140
|
+
rateLimited: false,
|
|
141
|
+
durationMs,
|
|
142
|
+
estimatedCost: 0,
|
|
143
|
+
pid: proc.pid,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async complete(prompt: string, _options?: CompleteOptions): Promise<string> {
|
|
148
|
+
const cmd = ["gemini", "-p", prompt];
|
|
149
|
+
|
|
150
|
+
const proc = _geminiCompleteDeps.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
|
|
151
|
+
const exitCode = await proc.exited;
|
|
152
|
+
|
|
153
|
+
const stdout = await new Response(proc.stdout).text();
|
|
154
|
+
const stderr = await new Response(proc.stderr).text();
|
|
155
|
+
const trimmed = stdout.trim();
|
|
156
|
+
|
|
157
|
+
if (exitCode !== 0) {
|
|
158
|
+
const errorDetails = stderr.trim() || trimmed;
|
|
159
|
+
const errorMessage = errorDetails || `complete() failed with exit code ${exitCode}`;
|
|
160
|
+
throw new CompleteError(errorMessage, exitCode);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (!trimmed) {
|
|
164
|
+
throw new CompleteError("complete() returned empty output");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return trimmed;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async plan(_options: PlanOptions): Promise<PlanResult> {
|
|
171
|
+
throw new Error("GeminiAdapter.plan() not implemented");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async decompose(_options: DecomposeOptions): Promise<DecomposeResult> {
|
|
175
|
+
throw new Error("GeminiAdapter.decompose() not implemented");
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCode Agent Adapter — implements AgentAdapter interface
|
|
3
|
+
*
|
|
4
|
+
* Provides uniform interface for spawning OpenCode agent processes,
|
|
5
|
+
* supporting one-shot completions.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
AgentAdapter,
|
|
10
|
+
AgentCapabilities,
|
|
11
|
+
AgentResult,
|
|
12
|
+
AgentRunOptions,
|
|
13
|
+
CompleteOptions,
|
|
14
|
+
DecomposeOptions,
|
|
15
|
+
DecomposeResult,
|
|
16
|
+
PlanOptions,
|
|
17
|
+
PlanResult,
|
|
18
|
+
} from "../types";
|
|
19
|
+
import { CompleteError } from "../types";
|
|
20
|
+
|
|
21
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
22
|
+
// Injectable dependencies — matches the _deps pattern used in claude.ts
|
|
23
|
+
// These are replaced in unit tests to intercept Bun.spawn calls.
|
|
24
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
export const _opencodeCompleteDeps = {
|
|
27
|
+
which(name: string): string | null {
|
|
28
|
+
return Bun.which(name);
|
|
29
|
+
},
|
|
30
|
+
spawn(
|
|
31
|
+
cmd: string[],
|
|
32
|
+
opts: { stdout: "pipe"; stderr: "pipe" | "inherit" },
|
|
33
|
+
): {
|
|
34
|
+
stdout: ReadableStream<Uint8Array>;
|
|
35
|
+
stderr: ReadableStream<Uint8Array>;
|
|
36
|
+
exited: Promise<number>;
|
|
37
|
+
pid: number;
|
|
38
|
+
} {
|
|
39
|
+
return Bun.spawn(cmd, opts) as unknown as {
|
|
40
|
+
stdout: ReadableStream<Uint8Array>;
|
|
41
|
+
stderr: ReadableStream<Uint8Array>;
|
|
42
|
+
exited: Promise<number>;
|
|
43
|
+
pid: number;
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
49
|
+
// OpenCodeAdapter implementation
|
|
50
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
export class OpenCodeAdapter implements AgentAdapter {
|
|
53
|
+
readonly name = "opencode";
|
|
54
|
+
readonly displayName = "OpenCode";
|
|
55
|
+
readonly binary = "opencode";
|
|
56
|
+
|
|
57
|
+
readonly capabilities: AgentCapabilities = {
|
|
58
|
+
supportedTiers: ["fast", "balanced"],
|
|
59
|
+
maxContextTokens: 8_000,
|
|
60
|
+
features: new Set<"tdd" | "review" | "refactor" | "batch">(["tdd", "refactor"]),
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
async isInstalled(): Promise<boolean> {
|
|
64
|
+
const path = _opencodeCompleteDeps.which("opencode");
|
|
65
|
+
return path !== null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
buildCommand(_options: AgentRunOptions): string[] {
|
|
69
|
+
throw new Error("OpenCodeAdapter.buildCommand() not implemented");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async run(_options: AgentRunOptions): Promise<AgentResult> {
|
|
73
|
+
throw new Error("OpenCodeAdapter.run() not implemented");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async complete(prompt: string, _options?: CompleteOptions): Promise<string> {
|
|
77
|
+
const cmd = ["opencode", "--prompt", prompt];
|
|
78
|
+
|
|
79
|
+
const proc = _opencodeCompleteDeps.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
|
|
80
|
+
const exitCode = await proc.exited;
|
|
81
|
+
|
|
82
|
+
const stdout = await new Response(proc.stdout).text();
|
|
83
|
+
const stderr = await new Response(proc.stderr).text();
|
|
84
|
+
const trimmed = stdout.trim();
|
|
85
|
+
|
|
86
|
+
if (exitCode !== 0) {
|
|
87
|
+
const errorDetails = stderr.trim() || trimmed;
|
|
88
|
+
const errorMessage = errorDetails || `complete() failed with exit code ${exitCode}`;
|
|
89
|
+
throw new CompleteError(errorMessage, exitCode);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!trimmed) {
|
|
93
|
+
throw new CompleteError("complete() returned empty output");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return trimmed;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async plan(_options: PlanOptions): Promise<PlanResult> {
|
|
100
|
+
throw new Error("OpenCodeAdapter.plan() not implemented");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async decompose(_options: DecomposeOptions): Promise<DecomposeResult> {
|
|
104
|
+
throw new Error("OpenCodeAdapter.decompose() not implemented");
|
|
105
|
+
}
|
|
106
|
+
}
|
package/src/agents/index.ts
CHANGED
|
@@ -12,3 +12,5 @@ export {
|
|
|
12
12
|
formatCostWithConfidence,
|
|
13
13
|
} from "./cost";
|
|
14
14
|
export { validateAgentForTier, validateAgentFeature, describeAgentCapabilities } from "./validation";
|
|
15
|
+
export type { AgentVersionInfo } from "./version-detection";
|
|
16
|
+
export { getAgentVersion, getAgentVersions } from "./version-detection";
|
package/src/agents/registry.ts
CHANGED
|
@@ -4,7 +4,10 @@
|
|
|
4
4
|
* Discovers and manages available coding agents.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { AiderAdapter } from "./adapters/aider";
|
|
7
8
|
import { CodexAdapter } from "./adapters/codex";
|
|
9
|
+
import { GeminiAdapter } from "./adapters/gemini";
|
|
10
|
+
import { OpenCodeAdapter } from "./adapters/opencode";
|
|
8
11
|
import { ClaudeCodeAdapter } from "./claude";
|
|
9
12
|
import type { AgentAdapter } from "./types";
|
|
10
13
|
|
|
@@ -12,8 +15,9 @@ import type { AgentAdapter } from "./types";
|
|
|
12
15
|
export const ALL_AGENTS: AgentAdapter[] = [
|
|
13
16
|
new ClaudeCodeAdapter(),
|
|
14
17
|
new CodexAdapter(),
|
|
15
|
-
|
|
16
|
-
|
|
18
|
+
new OpenCodeAdapter(),
|
|
19
|
+
new GeminiAdapter(),
|
|
20
|
+
new AiderAdapter(),
|
|
17
21
|
];
|
|
18
22
|
|
|
19
23
|
/** Get all registered agent names */
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent version detection utilities
|
|
3
|
+
*
|
|
4
|
+
* Extracts version information from installed agent binaries
|
|
5
|
+
* by running `<agent> --version` and parsing the output.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { getInstalledAgents } from "./registry";
|
|
9
|
+
import type { AgentAdapter } from "./types";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Information about an installed agent including its version
|
|
13
|
+
*/
|
|
14
|
+
export interface AgentVersionInfo {
|
|
15
|
+
/** Agent name (e.g., "codex", "aider") */
|
|
16
|
+
name: string;
|
|
17
|
+
/** Human-readable display name */
|
|
18
|
+
displayName: string;
|
|
19
|
+
/** Agent version or null if not installed/unable to detect */
|
|
20
|
+
version: string | null;
|
|
21
|
+
/** Whether the agent binary is installed */
|
|
22
|
+
installed: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Dependency injection for testability
|
|
27
|
+
*/
|
|
28
|
+
export const _versionDetectionDeps = {
|
|
29
|
+
spawn(
|
|
30
|
+
cmd: string[],
|
|
31
|
+
opts: { stdout: "pipe"; stderr: "pipe" },
|
|
32
|
+
): {
|
|
33
|
+
stdout: ReadableStream<Uint8Array>;
|
|
34
|
+
stderr: ReadableStream<Uint8Array>;
|
|
35
|
+
exited: Promise<number>;
|
|
36
|
+
} {
|
|
37
|
+
return Bun.spawn(cmd, opts) as unknown as {
|
|
38
|
+
stdout: ReadableStream<Uint8Array>;
|
|
39
|
+
stderr: ReadableStream<Uint8Array>;
|
|
40
|
+
exited: Promise<number>;
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get version for a single agent binary
|
|
47
|
+
*
|
|
48
|
+
* Runs `<agent> --version` and extracts version string.
|
|
49
|
+
* Returns null if agent not found or version detection fails.
|
|
50
|
+
*/
|
|
51
|
+
export async function getAgentVersion(binaryName: string): Promise<string | null> {
|
|
52
|
+
try {
|
|
53
|
+
const proc = _versionDetectionDeps.spawn([binaryName, "--version"], {
|
|
54
|
+
stdout: "pipe",
|
|
55
|
+
stderr: "pipe",
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const exitCode = await proc.exited;
|
|
59
|
+
if (exitCode !== 0) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const stdout = await new Response(proc.stdout).text();
|
|
64
|
+
const versionLine = stdout.trim().split("\n")[0];
|
|
65
|
+
|
|
66
|
+
// Extract version from common formats:
|
|
67
|
+
// "tool version 1.2.3"
|
|
68
|
+
// "v1.2.3"
|
|
69
|
+
// "1.2.3"
|
|
70
|
+
const versionMatch = versionLine.match(/v?(\d+\.\d+(?:\.\d+)?(?:[-+][\w.]+)?)/);
|
|
71
|
+
if (versionMatch) {
|
|
72
|
+
return versionMatch[0];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// If no version pattern matched, return the first line as-is
|
|
76
|
+
return versionLine || null;
|
|
77
|
+
} catch {
|
|
78
|
+
// Bun.spawn throws ENOENT if binary not found
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Get version information for all configured agents
|
|
85
|
+
*
|
|
86
|
+
* Returns list of agents with their installation status and version info.
|
|
87
|
+
*/
|
|
88
|
+
export async function getAgentVersions(): Promise<AgentVersionInfo[]> {
|
|
89
|
+
const agents = await getInstalledAgents();
|
|
90
|
+
const agentsByName = new Map(agents.map((a) => [a.name, a]));
|
|
91
|
+
|
|
92
|
+
// Import ALL_AGENTS to include non-installed ones
|
|
93
|
+
const { ALL_AGENTS } = await import("./registry");
|
|
94
|
+
|
|
95
|
+
const versions = await Promise.all(
|
|
96
|
+
ALL_AGENTS.map(async (agent: AgentAdapter): Promise<AgentVersionInfo> => {
|
|
97
|
+
const version = agentsByName.has(agent.name) ? await getAgentVersion(agent.binary) : null;
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
name: agent.name,
|
|
101
|
+
displayName: agent.displayName,
|
|
102
|
+
version,
|
|
103
|
+
installed: agentsByName.has(agent.name),
|
|
104
|
+
};
|
|
105
|
+
}),
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
return versions;
|
|
109
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agents Command
|
|
3
|
+
*
|
|
4
|
+
* Lists available agents with their binary paths, versions, and health status.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { ALL_AGENTS } from "../agents/registry";
|
|
8
|
+
import { getAgentVersion } from "../agents/version-detection";
|
|
9
|
+
import type { NaxConfig } from "../config/schema";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* List all agents with status, version, and capabilities.
|
|
13
|
+
*
|
|
14
|
+
* @param config - nax configuration
|
|
15
|
+
* @param _workdir - Working directory (for consistency with other commands)
|
|
16
|
+
*/
|
|
17
|
+
export async function agentsListCommand(config: NaxConfig, _workdir: string): Promise<void> {
|
|
18
|
+
// Get version info for all agents
|
|
19
|
+
const agentVersions = await Promise.all(
|
|
20
|
+
ALL_AGENTS.map(async (agent) => ({
|
|
21
|
+
name: agent.name,
|
|
22
|
+
displayName: agent.displayName,
|
|
23
|
+
binary: agent.binary,
|
|
24
|
+
version: await getAgentVersion(agent.binary),
|
|
25
|
+
installed: await agent.isInstalled(),
|
|
26
|
+
capabilities: agent.capabilities,
|
|
27
|
+
isDefault: config.autoMode.defaultAgent === agent.name,
|
|
28
|
+
})),
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
// Build table rows
|
|
32
|
+
const rows = agentVersions.map((info) => {
|
|
33
|
+
const status = info.installed ? "installed" : "unavailable";
|
|
34
|
+
const versionStr = info.version || "-";
|
|
35
|
+
const defaultMarker = info.isDefault ? " (default)" : "";
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
name: info.displayName + defaultMarker,
|
|
39
|
+
status,
|
|
40
|
+
version: versionStr,
|
|
41
|
+
binary: info.binary,
|
|
42
|
+
tiers: info.capabilities.supportedTiers.join(", "),
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (rows.length === 0) {
|
|
47
|
+
console.log("No agents available.");
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Calculate column widths
|
|
52
|
+
const widths = {
|
|
53
|
+
name: Math.max(5, ...rows.map((r) => r.name.length)),
|
|
54
|
+
status: Math.max(6, ...rows.map((r) => r.status.length)),
|
|
55
|
+
version: Math.max(7, ...rows.map((r) => r.version.length)),
|
|
56
|
+
binary: Math.max(6, ...rows.map((r) => r.binary.length)),
|
|
57
|
+
tiers: Math.max(5, ...rows.map((r) => r.tiers.length)),
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// Display table
|
|
61
|
+
console.log("\nAvailable Agents:\n");
|
|
62
|
+
console.log(
|
|
63
|
+
`${pad("Agent", widths.name)} ${pad("Status", widths.status)} ${pad("Version", widths.version)} ${pad("Binary", widths.binary)} ${pad("Tiers", widths.tiers)}`,
|
|
64
|
+
);
|
|
65
|
+
console.log(
|
|
66
|
+
`${"-".repeat(widths.name)} ${"-".repeat(widths.status)} ${"-".repeat(widths.version)} ${"-".repeat(widths.binary)} ${"-".repeat(widths.tiers)}`,
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
for (const row of rows) {
|
|
70
|
+
console.log(
|
|
71
|
+
`${pad(row.name, widths.name)} ${pad(row.status, widths.status)} ${pad(row.version, widths.version)} ${pad(row.binary, widths.binary)} ${pad(row.tiers, widths.tiers)}`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
console.log();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Pad string to width.
|
|
80
|
+
*
|
|
81
|
+
* @param str - String to pad
|
|
82
|
+
* @param width - Target width
|
|
83
|
+
* @returns Padded string
|
|
84
|
+
*/
|
|
85
|
+
function pad(str: string, width: number): string {
|
|
86
|
+
return str.padEnd(width);
|
|
87
|
+
}
|