@buihongduc132/pi-acp-agents 0.3.1
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/CHANGELOG.md +45 -0
- package/LICENSE +21 -0
- package/README.md +359 -0
- package/index.ts +1521 -0
- package/package.json +103 -0
- package/skills/pi-acp-agents/SKILL.md +112 -0
- package/src/acp-widget.ts +379 -0
- package/src/adapter-factory.ts +55 -0
- package/src/adapters/acpx.ts +215 -0
- package/src/adapters/base.ts +117 -0
- package/src/adapters/codex.ts +77 -0
- package/src/adapters/custom.ts +14 -0
- package/src/adapters/gemini.ts +66 -0
- package/src/adapters/opencode.ts +101 -0
- package/src/config/config.ts +312 -0
- package/src/config/types.ts +203 -0
- package/src/coordination/alias-resolver.ts +208 -0
- package/src/coordination/coordinator.ts +266 -0
- package/src/coordination/worker-dispatcher.ts +191 -0
- package/src/core/async-executor.ts +149 -0
- package/src/core/circuit-breaker.ts +254 -0
- package/src/core/client.ts +661 -0
- package/src/core/health-monitor.ts +200 -0
- package/src/core/protocol-validator.ts +259 -0
- package/src/core/session-lifecycle.ts +46 -0
- package/src/core/session-manager.ts +64 -0
- package/src/extension-safety.ts +200 -0
- package/src/logger.ts +92 -0
- package/src/management/event-log.ts +31 -0
- package/src/management/governance-store.ts +123 -0
- package/src/management/heartbeat-parser.ts +92 -0
- package/src/management/mailbox-manager.ts +95 -0
- package/src/management/runtime-paths.ts +34 -0
- package/src/management/safe-mkdir.ts +78 -0
- package/src/management/session-archive-store.ts +136 -0
- package/src/management/session-name-store.ts +88 -0
- package/src/management/task-store.ts +260 -0
- package/src/management/worker-store.ts +164 -0
- package/src/public-api.ts +72 -0
- package/src/settings/agent-config-tui.ts +456 -0
- package/src/settings/agents-command.ts +138 -0
- package/src/settings/config.ts +201 -0
- package/src/settings/configure-tui.ts +135 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-acp-agents — ACPX CLI adapter.
|
|
3
|
+
*
|
|
4
|
+
* Delegates ACP agent interaction to the `acpx` CLI instead of managing
|
|
5
|
+
* a subprocess directly. Session lifecycle is handled via CLI commands:
|
|
6
|
+
* - spawn → acpx sessions create
|
|
7
|
+
* - prompt → acpx sessions prompt
|
|
8
|
+
* - cancel → acpx sessions cancel
|
|
9
|
+
* - dispose → acpx sessions close
|
|
10
|
+
*/
|
|
11
|
+
import { spawnSync } from "node:child_process";
|
|
12
|
+
import { platform } from "node:os";
|
|
13
|
+
import type { AcpAdapterOptions, AcpPromptResult } from "../config/types.js";
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Constants
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
const DEFAULT_TIMEOUT_SEC = 3600; // 1 hour
|
|
20
|
+
const ACX_BINARY = "acpx";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Escape a string for safe use as a cmd.exe argument on Windows.
|
|
24
|
+
* When shell:true is used, cmd.exe interprets metacharacters like & | < > ^ %.
|
|
25
|
+
* This wraps the arg in double quotes and escapes internal quotes and carets.
|
|
26
|
+
*/
|
|
27
|
+
function escapeWindowsArg(arg: string): string {
|
|
28
|
+
// cmd.exe metacharacters: & | < > ^ % ( ) "
|
|
29
|
+
// Wrap in quotes and escape internal quotes with ^"
|
|
30
|
+
const escaped = arg.replace(/"/g, '""');
|
|
31
|
+
return `"${escaped}"`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Internal state
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
interface AcpxSessionState {
|
|
39
|
+
sessionId: string | null;
|
|
40
|
+
connected: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Adapter
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
export class AcpxAdapter {
|
|
48
|
+
public readonly name = "acpx";
|
|
49
|
+
private state: AcpxSessionState = { sessionId: null, connected: false };
|
|
50
|
+
private cwd: string;
|
|
51
|
+
private agentName: string;
|
|
52
|
+
|
|
53
|
+
constructor(opts: AcpAdapterOptions) {
|
|
54
|
+
this.cwd = opts.cwd ?? process.cwd();
|
|
55
|
+
this.agentName = opts.agentName ?? (opts.config.command
|
|
56
|
+
? opts.config.command.split("/").pop() ?? "acpx"
|
|
57
|
+
: "acpx");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// -----------------------------------------------------------------------
|
|
61
|
+
// Public API — matches AcpAgentAdapter surface
|
|
62
|
+
// -----------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
async spawn(): Promise<void> {
|
|
65
|
+
let result: ReturnType<typeof spawnSync>;
|
|
66
|
+
try {
|
|
67
|
+
result = this._runAcpx(["sessions", "create", this.agentName, "--format", "json"]);
|
|
68
|
+
} catch (err: unknown) {
|
|
69
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
70
|
+
throw new Error(`AcpxAdapter spawn failed: ${msg}`);
|
|
71
|
+
}
|
|
72
|
+
if (result.error) {
|
|
73
|
+
throw new Error(`AcpxAdapter spawn failed: ${result.error.message}`);
|
|
74
|
+
}
|
|
75
|
+
if (result.status !== 0) {
|
|
76
|
+
const err = String(result.stderr ?? "").trim() || "acpx sessions create failed";
|
|
77
|
+
throw new Error(`AcpxAdapter spawn failed: ${err}`);
|
|
78
|
+
}
|
|
79
|
+
let parsed: Record<string, unknown>;
|
|
80
|
+
try {
|
|
81
|
+
parsed = JSON.parse(String(result.stdout));
|
|
82
|
+
} catch {
|
|
83
|
+
throw new Error(`AcpxAdapter spawn failed: invalid JSON response`);
|
|
84
|
+
}
|
|
85
|
+
const sessionId = parsed["sessionId"];
|
|
86
|
+
this.state.sessionId = typeof sessionId === "string" ? sessionId : null;
|
|
87
|
+
this.state.connected = true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async initialize(): Promise<void> {
|
|
91
|
+
// acpx manages its own init; no separate handshake needed
|
|
92
|
+
if (!this.state.connected) {
|
|
93
|
+
throw new Error("Not spawned — call spawn() first");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async newSession(_cwd?: string): Promise<string> {
|
|
98
|
+
if (!this.state.connected) throw new Error("Not spawned");
|
|
99
|
+
// acpx creates a new session per spawn; return current ID
|
|
100
|
+
if (!this.state.sessionId) {
|
|
101
|
+
throw new Error("No session ID after spawn");
|
|
102
|
+
}
|
|
103
|
+
return this.state.sessionId;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async prompt(message: string): Promise<AcpPromptResult> {
|
|
107
|
+
if (!this.state.connected) throw new Error("Not spawned — call spawn() first");
|
|
108
|
+
if (!this.state.sessionId) throw new Error("No session ID");
|
|
109
|
+
|
|
110
|
+
const timeout = DEFAULT_TIMEOUT_SEC;
|
|
111
|
+
const args = [
|
|
112
|
+
"sessions", "prompt",
|
|
113
|
+
"--session", this.state.sessionId,
|
|
114
|
+
"--format", "json",
|
|
115
|
+
"--approve-all",
|
|
116
|
+
"--timeout", String(timeout),
|
|
117
|
+
"--",
|
|
118
|
+
message,
|
|
119
|
+
];
|
|
120
|
+
let result: ReturnType<typeof spawnSync>;
|
|
121
|
+
try {
|
|
122
|
+
result = this._runAcpx(args);
|
|
123
|
+
} catch (err: unknown) {
|
|
124
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
125
|
+
throw new Error(`AcpxAdapter prompt failed: ${msg}`);
|
|
126
|
+
}
|
|
127
|
+
if (result.error) {
|
|
128
|
+
throw new Error(`AcpxAdapter prompt failed: ${result.error.message}`);
|
|
129
|
+
}
|
|
130
|
+
if (result.status !== 0) {
|
|
131
|
+
const err = String(result.stderr ?? "").trim() || "acpx prompt failed";
|
|
132
|
+
throw new Error(`AcpxAdapter prompt failed: ${err}`);
|
|
133
|
+
}
|
|
134
|
+
let parsed: Record<string, unknown>;
|
|
135
|
+
try {
|
|
136
|
+
parsed = JSON.parse(String(result.stdout));
|
|
137
|
+
} catch {
|
|
138
|
+
throw new Error(`AcpxAdapter prompt failed: invalid JSON response`);
|
|
139
|
+
}
|
|
140
|
+
const text = typeof parsed["text"] === "string" ? parsed["text"] : "";
|
|
141
|
+
const stopReason = typeof parsed["stopReason"] === "string"
|
|
142
|
+
? parsed["stopReason"]
|
|
143
|
+
: typeof parsed["stop_reason"] === "string" ? parsed["stop_reason"] : "end_turn";
|
|
144
|
+
const sessionId = typeof parsed["sessionId"] === "string"
|
|
145
|
+
? parsed["sessionId"]
|
|
146
|
+
: typeof parsed["session_id"] === "string" ? parsed["session_id"] : this.state.sessionId!;
|
|
147
|
+
return {
|
|
148
|
+
text,
|
|
149
|
+
stopReason,
|
|
150
|
+
sessionId,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async cancel(): Promise<void> {
|
|
155
|
+
if (!this.state.sessionId) return;
|
|
156
|
+
try {
|
|
157
|
+
this._runAcpx(["sessions", "cancel", "--session", this.state.sessionId]);
|
|
158
|
+
} catch {
|
|
159
|
+
// best-effort — cancel must not throw
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async loadSession(sessionId: string): Promise<string> {
|
|
164
|
+
this.state.sessionId = sessionId;
|
|
165
|
+
this.state.connected = true;
|
|
166
|
+
return sessionId;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async setModel(_modelId: string): Promise<void> {
|
|
170
|
+
// acpx doesn't support per-session model switching via CLI
|
|
171
|
+
// Model is configured at the acpx profile level
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async setMode(_modeId: string): Promise<void> {
|
|
175
|
+
// acpx doesn't support per-session mode switching via CLI
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
getSessionId(): string | null {
|
|
179
|
+
return this.state.sessionId;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
get connected(): boolean {
|
|
183
|
+
return this.state.connected;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
dispose(): void {
|
|
187
|
+
if (this.state.sessionId) {
|
|
188
|
+
try {
|
|
189
|
+
this._runAcpx(["sessions", "close", this.state.sessionId]);
|
|
190
|
+
} catch {
|
|
191
|
+
// best-effort — dispose must not throw
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
this.state.sessionId = null;
|
|
195
|
+
this.state.connected = false;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// -----------------------------------------------------------------------
|
|
199
|
+
// Internal
|
|
200
|
+
// -----------------------------------------------------------------------
|
|
201
|
+
|
|
202
|
+
private _runAcpx(args: string[]) {
|
|
203
|
+
const isWindows = platform() === "win32";
|
|
204
|
+
// When shell:true on Windows, cmd.exe interprets metacharacters (&, |, <, >, ^, %).
|
|
205
|
+
// Escape args that may contain user-controlled content (e.g. prompt messages).
|
|
206
|
+
const safeArgs = isWindows ? args.map(escapeWindowsArg) : args;
|
|
207
|
+
return spawnSync(ACX_BINARY, safeArgs, {
|
|
208
|
+
cwd: this.cwd,
|
|
209
|
+
encoding: "utf-8",
|
|
210
|
+
timeout: 120_000, // 2 min for CLI itself; prompt timeout is passed to acpx
|
|
211
|
+
maxBuffer: 10 * 1024 * 1024, // 10 MB
|
|
212
|
+
shell: isWindows,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base ACP Agent Adapter — abstract class for all ACP agent adapters.
|
|
3
|
+
*
|
|
4
|
+
* Subclasses provide agent-specific defaults via applyDefaults() and get name().
|
|
5
|
+
* The base class handles the ACP lifecycle via AcpClient.
|
|
6
|
+
*/
|
|
7
|
+
import { AcpClient, type AcpClientOptions } from "../core/client.js";
|
|
8
|
+
import type { AcpAgentConfig, AcpAdapterOptions, AcpPromptResult } from "../config/types.js";
|
|
9
|
+
import type { Logger } from "../logger.js";
|
|
10
|
+
import { createNoopLogger } from "../logger.js";
|
|
11
|
+
|
|
12
|
+
export type { AcpAdapterOptions };
|
|
13
|
+
|
|
14
|
+
export abstract class AcpAgentAdapter {
|
|
15
|
+
protected config: AcpAgentConfig;
|
|
16
|
+
protected clientInfo: { name: string; version: string };
|
|
17
|
+
protected logger: Logger;
|
|
18
|
+
protected cwd: string;
|
|
19
|
+
protected client: AcpClient | null = null;
|
|
20
|
+
protected onActivity?: (sessionId: string) => void;
|
|
21
|
+
protected onSessionUpdate?: (sessionId: string, update: import("@agentclientprotocol/sdk").SessionUpdate) => void;
|
|
22
|
+
|
|
23
|
+
constructor(opts: AcpAdapterOptions) {
|
|
24
|
+
this.config = this.applyDefaults(opts.config);
|
|
25
|
+
this.clientInfo = opts.clientInfo ?? { name: "pi-acp-agents", version: "0.1.0" };
|
|
26
|
+
this.logger = opts.logger ?? createNoopLogger();
|
|
27
|
+
this.cwd = opts.cwd ?? process.cwd();
|
|
28
|
+
this.onActivity = opts.onActivity;
|
|
29
|
+
this.onSessionUpdate = opts.onSessionUpdate;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Subclasses override to provide agent-specific default config values */
|
|
33
|
+
protected applyDefaults(config: AcpAgentConfig): AcpAgentConfig {
|
|
34
|
+
return { ...config };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Adapter name (e.g., "gemini", "custom") */
|
|
38
|
+
abstract get name(): string;
|
|
39
|
+
|
|
40
|
+
/** Spawn the agent process and establish ACP connection */
|
|
41
|
+
async spawn(): Promise<void> {
|
|
42
|
+
const client = new AcpClient({
|
|
43
|
+
agentName: this.name,
|
|
44
|
+
config: this.config,
|
|
45
|
+
cwd: this.cwd,
|
|
46
|
+
clientInfo: this.clientInfo,
|
|
47
|
+
onActivity: this.onActivity,
|
|
48
|
+
onSessionUpdate: this.onSessionUpdate,
|
|
49
|
+
});
|
|
50
|
+
await client.connect();
|
|
51
|
+
// Only assign after successful connect
|
|
52
|
+
this.client = client;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** ACP initialize handshake */
|
|
56
|
+
async initialize(): Promise<void> {
|
|
57
|
+
if (!this.client) throw new Error("Not spawned — call spawn() first");
|
|
58
|
+
await this.client.initialize();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Create a new session, returns sessionId */
|
|
62
|
+
async newSession(_cwd?: string): Promise<string> {
|
|
63
|
+
if (!this.client) throw new Error("Not spawned");
|
|
64
|
+
return this.client.newSession();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Send a prompt and get the result */
|
|
68
|
+
async prompt(message: string): Promise<AcpPromptResult> {
|
|
69
|
+
if (!this.client) throw new Error("Not spawned");
|
|
70
|
+
return this.client.quickPrompt(message);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Cancel ongoing prompt */
|
|
74
|
+
async cancel(): Promise<void> {
|
|
75
|
+
await this.client?.cancel();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Load an existing session by ID */
|
|
79
|
+
async loadSession(sessionId: string): Promise<string> {
|
|
80
|
+
if (!this.client) throw new Error("Not spawned");
|
|
81
|
+
return this.client.loadSession(sessionId);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Set the model for the current session */
|
|
85
|
+
async setModel(modelId: string): Promise<void> {
|
|
86
|
+
if (!this.client) throw new Error("Not spawned");
|
|
87
|
+
await this.client.setModel(modelId);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Set the mode (thinking level) for the current session */
|
|
91
|
+
async setMode(modeId: string): Promise<void> {
|
|
92
|
+
if (!this.client) throw new Error("Not spawned");
|
|
93
|
+
await this.client.setMode(modeId);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Get current session ID */
|
|
97
|
+
getSessionId(): string | null {
|
|
98
|
+
return this.client?.sessionId ?? null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Check if connected */
|
|
102
|
+
get connected(): boolean {
|
|
103
|
+
return this.client?.connected ?? false;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Clean up — kill process and release resources */
|
|
107
|
+
dispose(): void {
|
|
108
|
+
if (this.client) {
|
|
109
|
+
try {
|
|
110
|
+
this.client.dispose();
|
|
111
|
+
} catch {
|
|
112
|
+
// best-effort — dispose must not throw
|
|
113
|
+
}
|
|
114
|
+
this.client = null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-acp-agents — Codex ACP adapter
|
|
3
|
+
*
|
|
4
|
+
* Protocol: stdio nd-JSON, standard ACP.
|
|
5
|
+
* Uses the third-party codex-acp bridge (cola-io/codex-acp) which wraps
|
|
6
|
+
* the OpenAI Codex runtime with ACP protocol.
|
|
7
|
+
*
|
|
8
|
+
* No provider-specific features — pure ACP protocol compliance.
|
|
9
|
+
*/
|
|
10
|
+
import { AcpAgentAdapter } from "./base.js";
|
|
11
|
+
import type { AcpAgentConfig } from "../config/types.js";
|
|
12
|
+
import type { Logger } from "../logger.js";
|
|
13
|
+
import { createNoopLogger } from "../logger.js";
|
|
14
|
+
import { execSync } from "node:child_process";
|
|
15
|
+
|
|
16
|
+
const log = createNoopLogger();
|
|
17
|
+
|
|
18
|
+
export interface CodexAdapterOptions {
|
|
19
|
+
config?: Partial<AcpAgentConfig>;
|
|
20
|
+
clientInfo?: { name: string; version: string };
|
|
21
|
+
logger?: Logger;
|
|
22
|
+
cwd?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class CodexAcpAdapter extends AcpAgentAdapter {
|
|
26
|
+
constructor(opts: Partial<CodexAdapterOptions> = {}) {
|
|
27
|
+
super({
|
|
28
|
+
config: {
|
|
29
|
+
command: "codex-acp",
|
|
30
|
+
args: [],
|
|
31
|
+
...opts.config,
|
|
32
|
+
} as AcpAgentConfig,
|
|
33
|
+
clientInfo: opts.clientInfo,
|
|
34
|
+
logger: opts.logger,
|
|
35
|
+
cwd: opts.cwd,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
get name(): string {
|
|
40
|
+
return "codex";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
protected applyDefaults(config: AcpAgentConfig): AcpAgentConfig {
|
|
44
|
+
return {
|
|
45
|
+
...config,
|
|
46
|
+
command: config.command || "codex-acp",
|
|
47
|
+
args: config.args ?? [],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Check if codex-acp binary is available */
|
|
52
|
+
static isAvailable(): boolean {
|
|
53
|
+
try {
|
|
54
|
+
execSync("which codex-acp", { stdio: "pipe" });
|
|
55
|
+
return true;
|
|
56
|
+
} catch (e) {
|
|
57
|
+
// codex-acp binary not found on PATH
|
|
58
|
+
log.debug("codex-acp not found", e);
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Get codex-acp version */
|
|
64
|
+
static getVersion(): string | null {
|
|
65
|
+
try {
|
|
66
|
+
const output = execSync("codex-acp --version", {
|
|
67
|
+
encoding: "utf-8",
|
|
68
|
+
stdio: "pipe",
|
|
69
|
+
});
|
|
70
|
+
return output.trim();
|
|
71
|
+
} catch (e) {
|
|
72
|
+
// codex-acp version check failed
|
|
73
|
+
log.debug("codex-acp version check failed", e);
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-acp-agents — Custom ACP adapter for user-defined commands
|
|
3
|
+
*/
|
|
4
|
+
import { AcpAgentAdapter, type AcpAdapterOptions } from "./base.js";
|
|
5
|
+
|
|
6
|
+
export class CustomAcpAdapter extends AcpAgentAdapter {
|
|
7
|
+
get name(): string {
|
|
8
|
+
return "custom";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
constructor(opts: AcpAdapterOptions) {
|
|
12
|
+
super(opts);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-acp-agents — Gemini-specific ACP adapter
|
|
3
|
+
*/
|
|
4
|
+
import { AcpAgentAdapter } from "./base.js";
|
|
5
|
+
import type { AcpAgentConfig } from "../config/types.js";
|
|
6
|
+
import type { Logger } from "../logger.js";
|
|
7
|
+
import { createNoopLogger } from "../logger.js";
|
|
8
|
+
import { execSync } from "node:child_process";
|
|
9
|
+
|
|
10
|
+
export interface GeminiAdapterOptions {
|
|
11
|
+
config?: Partial<AcpAgentConfig>;
|
|
12
|
+
clientInfo?: { name: string; version: string };
|
|
13
|
+
logger?: Logger;
|
|
14
|
+
cwd?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class GeminiAcpAdapter extends AcpAgentAdapter {
|
|
18
|
+
constructor(opts: Partial<GeminiAdapterOptions> = {}) {
|
|
19
|
+
super({
|
|
20
|
+
config: {
|
|
21
|
+
command: "gemini",
|
|
22
|
+
args: ["--acp"],
|
|
23
|
+
...opts.config,
|
|
24
|
+
} as AcpAgentConfig,
|
|
25
|
+
clientInfo: opts.clientInfo,
|
|
26
|
+
logger: opts.logger,
|
|
27
|
+
cwd: opts.cwd,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
get name(): string {
|
|
32
|
+
return "gemini";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
protected applyDefaults(config: AcpAgentConfig): AcpAgentConfig {
|
|
36
|
+
return {
|
|
37
|
+
...config,
|
|
38
|
+
command: config.command || "gemini",
|
|
39
|
+
args: config.args ?? ["--acp"],
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Check if gemini CLI is available */
|
|
44
|
+
static isAvailable(): boolean {
|
|
45
|
+
try {
|
|
46
|
+
execSync("which gemini", { stdio: "pipe" });
|
|
47
|
+
return true;
|
|
48
|
+
} catch (err) {
|
|
49
|
+
// gemini CLI not found on PATH
|
|
50
|
+
createNoopLogger().debug("gemini not available", err);
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Get gemini CLI version */
|
|
56
|
+
static getVersion(): string | null {
|
|
57
|
+
try {
|
|
58
|
+
const output = execSync("gemini --version", { encoding: "utf-8", stdio: "pipe" });
|
|
59
|
+
return output.trim();
|
|
60
|
+
} catch (err) {
|
|
61
|
+
// gemini version check failed
|
|
62
|
+
createNoopLogger().debug("gemini version check failed", err);
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-acp-agents — OpenCode ACP adapter
|
|
3
|
+
*
|
|
4
|
+
* Protocol: stdio nd-JSON, standard ACP.
|
|
5
|
+
* Command: `opencode acp` (or `ocxo acp` when installed via alias)
|
|
6
|
+
*
|
|
7
|
+
* No provider-specific features — pure ACP protocol compliance.
|
|
8
|
+
*/
|
|
9
|
+
import { AcpAgentAdapter } from "./base.js";
|
|
10
|
+
import type { AcpAgentConfig } from "../config/types.js";
|
|
11
|
+
import type { Logger } from "../logger.js";
|
|
12
|
+
import { createNoopLogger } from "../logger.js";
|
|
13
|
+
import { execSync } from "node:child_process";
|
|
14
|
+
|
|
15
|
+
const log = createNoopLogger();
|
|
16
|
+
|
|
17
|
+
export interface OpenCodeAdapterOptions {
|
|
18
|
+
config?: Partial<AcpAgentConfig>;
|
|
19
|
+
clientInfo?: { name: string; version: string };
|
|
20
|
+
logger?: Logger;
|
|
21
|
+
cwd?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class OpenCodeAcpAdapter extends AcpAgentAdapter {
|
|
25
|
+
constructor(opts: Partial<OpenCodeAdapterOptions> = {}) {
|
|
26
|
+
// Auto-resolve binary: prefer explicit config, then ocxo, then opencode
|
|
27
|
+
const resolvedBinary = opts.config?.command || OpenCodeAcpAdapter.resolveBinary() || "opencode";
|
|
28
|
+
super({
|
|
29
|
+
config: {
|
|
30
|
+
command: resolvedBinary,
|
|
31
|
+
args: ["acp"],
|
|
32
|
+
...opts.config,
|
|
33
|
+
} as AcpAgentConfig,
|
|
34
|
+
clientInfo: opts.clientInfo,
|
|
35
|
+
logger: opts.logger,
|
|
36
|
+
cwd: opts.cwd,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
get name(): string {
|
|
41
|
+
return "opencode";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
protected applyDefaults(config: AcpAgentConfig): AcpAgentConfig {
|
|
45
|
+
return {
|
|
46
|
+
...config,
|
|
47
|
+
command: config.command || "opencode",
|
|
48
|
+
args: config.args ?? ["acp"],
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Check if opencode CLI is available (checks opencode, ocxo) */
|
|
53
|
+
static isAvailable(binary?: string): boolean {
|
|
54
|
+
const candidates = binary ? [binary] : ["opencode", "ocxo"];
|
|
55
|
+
for (const cmd of candidates) {
|
|
56
|
+
try {
|
|
57
|
+
execSync(`which ${cmd}`, { stdio: "pipe" });
|
|
58
|
+
return true;
|
|
59
|
+
} catch (e) {
|
|
60
|
+
// Binary not found on PATH
|
|
61
|
+
log.debug(`opencode '${cmd}' not found`, e);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Get CLI version */
|
|
69
|
+
static getVersion(binary?: string): string | null {
|
|
70
|
+
const candidates = binary ? [binary] : ["opencode", "ocxo"];
|
|
71
|
+
for (const cmd of candidates) {
|
|
72
|
+
try {
|
|
73
|
+
const output = execSync(`${cmd} --version`, {
|
|
74
|
+
encoding: "utf-8",
|
|
75
|
+
stdio: "pipe",
|
|
76
|
+
});
|
|
77
|
+
return `${cmd}: ${output.trim()}`;
|
|
78
|
+
} catch (e) {
|
|
79
|
+
// Version check failed for this binary
|
|
80
|
+
log.debug(`opencode '${cmd}' version check failed`, e);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Resolve the actual binary name to use */
|
|
88
|
+
static resolveBinary(): string | null {
|
|
89
|
+
for (const cmd of ["opencode", "ocxo"]) {
|
|
90
|
+
try {
|
|
91
|
+
execSync(`which ${cmd}`, { stdio: "pipe" });
|
|
92
|
+
return cmd;
|
|
93
|
+
} catch (e) {
|
|
94
|
+
// Binary not found on PATH
|
|
95
|
+
log.debug(`opencode '${cmd}' resolve failed`, e);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|