@mrpatronz/nexusflow 2.0.0 → 2.1.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/README.md +2 -1
- package/dist/agent/AcpCliAdapter.d.ts +75 -0
- package/dist/agent/AcpCliAdapter.d.ts.map +1 -0
- package/dist/agent/AcpCliAdapter.js +292 -0
- package/dist/agent/AcpCliAdapter.js.map +1 -0
- package/dist/agent/AcpCliAdapter.test.d.ts +2 -0
- package/dist/agent/AcpCliAdapter.test.d.ts.map +1 -0
- package/dist/agent/AcpCliAdapter.test.js +235 -0
- package/dist/agent/AcpCliAdapter.test.js.map +1 -0
- package/dist/agent/ClaudeCliAdapter.d.ts +40 -5
- package/dist/agent/ClaudeCliAdapter.d.ts.map +1 -1
- package/dist/agent/ClaudeCliAdapter.js +212 -15
- package/dist/agent/ClaudeCliAdapter.js.map +1 -1
- package/dist/agent/ClaudeCliAdapter.test.d.ts +2 -0
- package/dist/agent/ClaudeCliAdapter.test.d.ts.map +1 -0
- package/dist/agent/ClaudeCliAdapter.test.js +320 -0
- package/dist/agent/ClaudeCliAdapter.test.js.map +1 -0
- package/dist/agent/CliAdapterBase.d.ts +29 -12
- package/dist/agent/CliAdapterBase.d.ts.map +1 -1
- package/dist/agent/CliAdapterBase.js +76 -36
- package/dist/agent/CliAdapterBase.js.map +1 -1
- package/dist/agent/CodexCliAdapter.d.ts +51 -0
- package/dist/agent/CodexCliAdapter.d.ts.map +1 -0
- package/dist/agent/CodexCliAdapter.js +226 -0
- package/dist/agent/CodexCliAdapter.js.map +1 -0
- package/dist/agent/CodexCliAdapter.test.d.ts +2 -0
- package/dist/agent/CodexCliAdapter.test.d.ts.map +1 -0
- package/dist/agent/CodexCliAdapter.test.js +316 -0
- package/dist/agent/CodexCliAdapter.test.js.map +1 -0
- package/dist/agent/CopilotAcpAdapter.d.ts +7 -0
- package/dist/agent/CopilotAcpAdapter.d.ts.map +1 -0
- package/dist/agent/CopilotAcpAdapter.js +29 -0
- package/dist/agent/CopilotAcpAdapter.js.map +1 -0
- package/dist/agent/ProviderRegistry.d.ts +39 -3
- package/dist/agent/ProviderRegistry.d.ts.map +1 -1
- package/dist/agent/ProviderRegistry.js +43 -8
- package/dist/agent/ProviderRegistry.js.map +1 -1
- package/dist/agent/ProviderRegistry.test.js +52 -0
- package/dist/agent/ProviderRegistry.test.js.map +1 -1
- package/dist/agent/adapters.d.ts +7 -5
- package/dist/agent/adapters.d.ts.map +1 -1
- package/dist/agent/adapters.js +92 -17
- package/dist/agent/adapters.js.map +1 -1
- package/dist/agent/adapters.test.js +22 -0
- package/dist/agent/adapters.test.js.map +1 -1
- package/dist/agent/cliAvailability.d.ts +51 -13
- package/dist/agent/cliAvailability.d.ts.map +1 -1
- package/dist/agent/cliAvailability.js +163 -44
- package/dist/agent/cliAvailability.js.map +1 -1
- package/dist/agent/cliAvailability.test.js +140 -53
- package/dist/agent/cliAvailability.test.js.map +1 -1
- package/dist/agent/session.d.ts +2 -1
- package/dist/agent/session.d.ts.map +1 -1
- package/dist/agent/session.js +15 -4
- package/dist/agent/session.js.map +1 -1
- package/dist/agent/session.test.js +29 -8
- package/dist/agent/session.test.js.map +1 -1
- package/dist/gui/assets/DashboardPage-BeAHHUtn.js +1 -0
- package/dist/gui/assets/GettingStartedPage-CFAyG0ar.js +1 -0
- package/dist/gui/assets/ProjectsPage-C0SmbBqW.js +1 -0
- package/dist/gui/assets/{ScaffoldRepoInline-D-2ZeuDK.js → ScaffoldRepoInline-lfFwOnQu.js} +1 -1
- package/dist/gui/assets/SettingsPage-DIBk9fu8.js +1 -0
- package/dist/gui/assets/StartWorkPage-CWC5OujI.js +1 -0
- package/dist/gui/assets/{StrategiesPage-Cizm0GWk.js → StrategiesPage-Dcq10WZS.js} +2 -2
- package/dist/gui/assets/WorkspacesPage-BqOx6Gbe.js +6 -0
- package/dist/gui/assets/index-B5IYXvr6.css +2 -0
- package/dist/gui/assets/{index-BqeoAIB1.js → index-BCfJkb0T.js} +3 -3
- package/dist/gui/index.html +2 -2
- package/dist/server.d.ts +13 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +153 -21
- package/dist/server.js.map +1 -1
- package/dist/server.test.js +143 -1
- package/dist/server.test.js.map +1 -1
- package/dist/utils/session-finder.d.ts +3 -0
- package/dist/utils/session-finder.d.ts.map +1 -1
- package/dist/utils/session-finder.js +54 -10
- package/dist/utils/session-finder.js.map +1 -1
- package/dist/utils/session-finder.test.js +61 -2
- package/dist/utils/session-finder.test.js.map +1 -1
- package/package.json +4 -2
- package/dist/gui/assets/DashboardPage-HNC3-OP0.js +0 -1
- package/dist/gui/assets/GettingStartedPage-Dguoya_Z.js +0 -1
- package/dist/gui/assets/ProjectsPage-DVR4j4sw.js +0 -1
- package/dist/gui/assets/SettingsPage-Bauo77q3.js +0 -1
- package/dist/gui/assets/StartWorkPage-DvuFEa5Y.js +0 -1
- package/dist/gui/assets/WorkspacesPage-DYOVfZqy.js +0 -6
- package/dist/gui/assets/index-CsA2SueF.css +0 -2
package/README.md
CHANGED
|
@@ -322,6 +322,7 @@ The dashboard integrates an **AI Strategy Analysis** inspector:
|
|
|
322
322
|
The primary GUI is the Electron desktop app in `desktop/` (`npm install && npm start` there). It embeds the same dashboard the browser sees. For browser access, run `nexusflow dashboard`; `nexusflow ui` starts the underlying server without opening anything (add `--open` to launch a browser). For the platform rationale (Electron vs Tauri and friends), see [docs/desktop-platform.md](docs/desktop-platform.md). The dashboard is a full-featured dark-themed GUI:
|
|
323
323
|
|
|
324
324
|
- **Workspaces tab** — create, browse, and manage feature workspaces
|
|
325
|
+
- **Chat tab** — use local CLI harnesses directly; Codex reuses `codex login` for workspace-write chat and GitHub Copilot reuses `copilot login` for read-only ACP chat, so NexusFlow does not need their API keys
|
|
325
326
|
- **Sessions tab** — view past AI conversation transcripts and resume sessions
|
|
326
327
|
- **Logs panel** — real-time aggregated service log output
|
|
327
328
|
- **Config panel** — edit NexusFlow settings from the browser
|
|
@@ -475,4 +476,4 @@ npm run test:watch
|
|
|
475
476
|
|
|
476
477
|
## 📄 License
|
|
477
478
|
|
|
478
|
-
MIT — see [LICENSE](LICENSE) for details.
|
|
479
|
+
MIT — see [LICENSE](LICENSE) for details.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import * as acp from '@agentclientprotocol/sdk';
|
|
2
|
+
import { type ChildProcess } from 'node:child_process';
|
|
3
|
+
import { EventEmitter } from 'node:events';
|
|
4
|
+
import type { AgentHarness } from './ProviderRegistry.js';
|
|
5
|
+
import type { AgentSession } from './session.js';
|
|
6
|
+
export interface AcpConnection {
|
|
7
|
+
initialize(params: acp.InitializeRequest): Promise<acp.InitializeResponse>;
|
|
8
|
+
newSession(params: acp.NewSessionRequest): Promise<acp.NewSessionResponse>;
|
|
9
|
+
loadSession(params: acp.LoadSessionRequest): Promise<acp.LoadSessionResponse>;
|
|
10
|
+
prompt(params: acp.PromptRequest): Promise<acp.PromptResponse>;
|
|
11
|
+
cancel(params: acp.CancelNotification): Promise<void>;
|
|
12
|
+
close(): Promise<void>;
|
|
13
|
+
readonly closed: Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
export interface AcpTransport {
|
|
16
|
+
process: ChildProcess;
|
|
17
|
+
connection: AcpConnection;
|
|
18
|
+
}
|
|
19
|
+
export interface AcpTransportOptions {
|
|
20
|
+
executable: string;
|
|
21
|
+
args: string[];
|
|
22
|
+
cwd: string;
|
|
23
|
+
client: acp.Client;
|
|
24
|
+
}
|
|
25
|
+
export type AcpTransportFactory = (options: AcpTransportOptions) => AcpTransport;
|
|
26
|
+
export interface AcpCliAdapterOptions {
|
|
27
|
+
executable: string;
|
|
28
|
+
args: string[];
|
|
29
|
+
label: string;
|
|
30
|
+
loginCommand?: string;
|
|
31
|
+
validateSessionId?: (id: string) => boolean;
|
|
32
|
+
transportFactory?: AcpTransportFactory;
|
|
33
|
+
}
|
|
34
|
+
/** Start an ACP subprocess using NDJSON over stdio and the current fluent SDK. */
|
|
35
|
+
export declare function createAcpTransport(options: AcpTransportOptions): AcpTransport;
|
|
36
|
+
/**
|
|
37
|
+
* NexusFlow's first ACP surface is deliberately read-only. Tool visibility is
|
|
38
|
+
* also restricted by each harness' startup flags, but permission requests are
|
|
39
|
+
* boundary input and must still be checked independently.
|
|
40
|
+
*/
|
|
41
|
+
export declare function decideReadOnlyPermission(params: acp.RequestPermissionRequest): acp.RequestPermissionResponse;
|
|
42
|
+
/** ACP ids are opaque JSON values, but control characters and unbounded input are never accepted. */
|
|
43
|
+
export declare function isSafeAcpSessionId(id: string): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Shared Agent Client Protocol lifecycle for local harnesses.
|
|
46
|
+
*
|
|
47
|
+
* ACP keeps prompts and streamed responses on stdin/stdout as structured
|
|
48
|
+
* messages, avoiding shell quoting differences and providing one transport for
|
|
49
|
+
* multiple harnesses. The subprocess remains alive for the chat session.
|
|
50
|
+
*/
|
|
51
|
+
export declare class AcpCliAdapter extends EventEmitter implements AgentHarness {
|
|
52
|
+
private readonly options;
|
|
53
|
+
private readonly transportFactory;
|
|
54
|
+
private transport;
|
|
55
|
+
private ready;
|
|
56
|
+
private sessionId;
|
|
57
|
+
private processing;
|
|
58
|
+
private stopped;
|
|
59
|
+
private reportedProcessFailure;
|
|
60
|
+
private acceptAgentMessages;
|
|
61
|
+
private turnProducedMessage;
|
|
62
|
+
private stderrTail;
|
|
63
|
+
constructor(options: AcpCliAdapterOptions);
|
|
64
|
+
start(cwd: string, session?: AgentSession): Promise<void>;
|
|
65
|
+
private connect;
|
|
66
|
+
private handleSessionUpdate;
|
|
67
|
+
send(data: string): Promise<void>;
|
|
68
|
+
stop(): void;
|
|
69
|
+
private observeProcess;
|
|
70
|
+
private reportConnectionFailure;
|
|
71
|
+
private toActionableError;
|
|
72
|
+
private stopTransport;
|
|
73
|
+
private shutdownTransport;
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=AcpCliAdapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AcpCliAdapter.d.ts","sourceRoot":"","sources":["../../src/agent/AcpCliAdapter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import * as acp from '@agentclientprotocol/sdk';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { EventEmitter } from 'node:events';
|
|
4
|
+
import { Readable, Writable } from 'node:stream';
|
|
5
|
+
import { killTree } from './CliAdapterBase.js';
|
|
6
|
+
class FluentAcpConnection {
|
|
7
|
+
connection;
|
|
8
|
+
closed;
|
|
9
|
+
constructor(client, stream) {
|
|
10
|
+
const app = acp.client({ name: 'NexusFlow' })
|
|
11
|
+
.onRequest(acp.methods.client.session.requestPermission, ({ params }) => (client.requestPermission(params)))
|
|
12
|
+
.onNotification(acp.methods.client.session.update, ({ params }) => (client.sessionUpdate(params)));
|
|
13
|
+
this.connection = app.connect(stream);
|
|
14
|
+
this.closed = this.connection.closed;
|
|
15
|
+
}
|
|
16
|
+
async request(method, params) {
|
|
17
|
+
return this.connection.agent.request(method, params);
|
|
18
|
+
}
|
|
19
|
+
initialize(params) {
|
|
20
|
+
return this.request(acp.methods.agent.initialize, params);
|
|
21
|
+
}
|
|
22
|
+
newSession(params) {
|
|
23
|
+
return this.request(acp.methods.agent.session.new, params);
|
|
24
|
+
}
|
|
25
|
+
loadSession(params) {
|
|
26
|
+
return this.request(acp.methods.agent.session.load, params);
|
|
27
|
+
}
|
|
28
|
+
prompt(params) {
|
|
29
|
+
return this.request(acp.methods.agent.session.prompt, params);
|
|
30
|
+
}
|
|
31
|
+
async cancel(params) {
|
|
32
|
+
await this.connection.agent.notify(acp.methods.agent.session.cancel, params);
|
|
33
|
+
}
|
|
34
|
+
async close() {
|
|
35
|
+
this.connection.close();
|
|
36
|
+
await this.closed;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** Start an ACP subprocess using NDJSON over stdio and the current fluent SDK. */
|
|
40
|
+
export function createAcpTransport(options) {
|
|
41
|
+
const child = spawn(options.executable, options.args, {
|
|
42
|
+
cwd: options.cwd,
|
|
43
|
+
env: { ...process.env, FORCE_COLOR: '0' },
|
|
44
|
+
// npm installs expose a .cmd shim on Windows. All argv here is provider
|
|
45
|
+
// configuration; user prompts and session ids travel inside ACP JSON.
|
|
46
|
+
shell: process.platform === 'win32' && /\.(cmd|bat)$/i.test(options.executable),
|
|
47
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
48
|
+
windowsHide: true,
|
|
49
|
+
});
|
|
50
|
+
if (!child.stdin || !child.stdout) {
|
|
51
|
+
throw new Error('ACP subprocess did not expose stdin/stdout.');
|
|
52
|
+
}
|
|
53
|
+
const output = Writable.toWeb(child.stdin);
|
|
54
|
+
const input = Readable.toWeb(child.stdout);
|
|
55
|
+
const stream = acp.ndJsonStream(output, input);
|
|
56
|
+
const connection = new FluentAcpConnection(options.client, stream);
|
|
57
|
+
return { process: child, connection };
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* NexusFlow's first ACP surface is deliberately read-only. Tool visibility is
|
|
61
|
+
* also restricted by each harness' startup flags, but permission requests are
|
|
62
|
+
* boundary input and must still be checked independently.
|
|
63
|
+
*/
|
|
64
|
+
export function decideReadOnlyPermission(params) {
|
|
65
|
+
// `toolCall.kind` is presentation metadata, not an authorization claim.
|
|
66
|
+
// In particular, an outside-workspace path can still be labelled `read`.
|
|
67
|
+
// Normal reads under the session cwd do not need escalation; every request
|
|
68
|
+
// that reaches this callback is therefore rejected fail-closed.
|
|
69
|
+
const option = ['reject_once', 'reject_always']
|
|
70
|
+
.map((kind) => params.options.find((candidate) => candidate.kind === kind))
|
|
71
|
+
.find(Boolean);
|
|
72
|
+
return option
|
|
73
|
+
? { outcome: { outcome: 'selected', optionId: option.optionId } }
|
|
74
|
+
: { outcome: { outcome: 'cancelled' } };
|
|
75
|
+
}
|
|
76
|
+
/** ACP ids are opaque JSON values, but control characters and unbounded input are never accepted. */
|
|
77
|
+
export function isSafeAcpSessionId(id) {
|
|
78
|
+
return id.length > 0 && id.length <= 200 && !/[\u0000-\u001f\u007f]/.test(id);
|
|
79
|
+
}
|
|
80
|
+
function withTimeout(promise, timeoutMs, operation) {
|
|
81
|
+
let timer;
|
|
82
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
83
|
+
timer = setTimeout(() => reject(new Error(`${operation} timed out after ${timeoutMs}ms.`)), timeoutMs);
|
|
84
|
+
timer.unref?.();
|
|
85
|
+
});
|
|
86
|
+
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Shared Agent Client Protocol lifecycle for local harnesses.
|
|
90
|
+
*
|
|
91
|
+
* ACP keeps prompts and streamed responses on stdin/stdout as structured
|
|
92
|
+
* messages, avoiding shell quoting differences and providing one transport for
|
|
93
|
+
* multiple harnesses. The subprocess remains alive for the chat session.
|
|
94
|
+
*/
|
|
95
|
+
export class AcpCliAdapter extends EventEmitter {
|
|
96
|
+
options;
|
|
97
|
+
transportFactory;
|
|
98
|
+
transport = null;
|
|
99
|
+
ready = Promise.resolve();
|
|
100
|
+
sessionId = null;
|
|
101
|
+
processing = false;
|
|
102
|
+
stopped = false;
|
|
103
|
+
reportedProcessFailure = false;
|
|
104
|
+
acceptAgentMessages = false;
|
|
105
|
+
turnProducedMessage = false;
|
|
106
|
+
stderrTail = '';
|
|
107
|
+
constructor(options) {
|
|
108
|
+
super();
|
|
109
|
+
this.options = options;
|
|
110
|
+
this.transportFactory = options.transportFactory ?? createAcpTransport;
|
|
111
|
+
}
|
|
112
|
+
async start(cwd, session) {
|
|
113
|
+
this.stopTransport(false);
|
|
114
|
+
this.stopped = false;
|
|
115
|
+
this.processing = false;
|
|
116
|
+
this.reportedProcessFailure = false;
|
|
117
|
+
this.acceptAgentMessages = false;
|
|
118
|
+
this.stderrTail = '';
|
|
119
|
+
this.sessionId = null;
|
|
120
|
+
this.turnProducedMessage = false;
|
|
121
|
+
this.ready = this.connect(cwd, session).catch((error) => {
|
|
122
|
+
if (!this.stopped)
|
|
123
|
+
this.reportConnectionFailure(error);
|
|
124
|
+
});
|
|
125
|
+
await this.ready;
|
|
126
|
+
}
|
|
127
|
+
async connect(cwd, requestedSession) {
|
|
128
|
+
const client = {
|
|
129
|
+
requestPermission: async (params) => decideReadOnlyPermission(params),
|
|
130
|
+
sessionUpdate: async (params) => this.handleSessionUpdate(params),
|
|
131
|
+
};
|
|
132
|
+
const transport = this.transportFactory({
|
|
133
|
+
executable: this.options.executable,
|
|
134
|
+
args: [...this.options.args],
|
|
135
|
+
cwd,
|
|
136
|
+
client,
|
|
137
|
+
});
|
|
138
|
+
this.transport = transport;
|
|
139
|
+
this.observeProcess(transport.process);
|
|
140
|
+
void transport.connection.closed.catch((error) => {
|
|
141
|
+
if (!this.stopped)
|
|
142
|
+
this.reportConnectionFailure(error);
|
|
143
|
+
});
|
|
144
|
+
const initialized = await withTimeout(transport.connection.initialize({
|
|
145
|
+
protocolVersion: acp.PROTOCOL_VERSION,
|
|
146
|
+
clientCapabilities: {},
|
|
147
|
+
clientInfo: { name: 'NexusFlow', version: '2.0.0' },
|
|
148
|
+
}), 10_000, `${this.options.label} ACP initialization`);
|
|
149
|
+
if (initialized.protocolVersion !== acp.PROTOCOL_VERSION) {
|
|
150
|
+
throw new Error(`${this.options.label} negotiated unsupported ACP version ${initialized.protocolVersion}.`);
|
|
151
|
+
}
|
|
152
|
+
if (requestedSession?.resume) {
|
|
153
|
+
if (initialized.agentCapabilities?.loadSession) {
|
|
154
|
+
await withTimeout(transport.connection.loadSession({
|
|
155
|
+
sessionId: requestedSession.id,
|
|
156
|
+
cwd,
|
|
157
|
+
mcpServers: [],
|
|
158
|
+
}), 10_000, `${this.options.label} session resume`);
|
|
159
|
+
this.sessionId = requestedSession.id;
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
this.emit('system', `${this.options.label} cannot resume this ACP session; starting a new one.`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (!this.sessionId) {
|
|
166
|
+
const created = await withTimeout(transport.connection.newSession({ cwd, mcpServers: [] }), 10_000, `${this.options.label} session creation`);
|
|
167
|
+
const validateSessionId = this.options.validateSessionId ?? isSafeAcpSessionId;
|
|
168
|
+
if (!validateSessionId(created.sessionId)) {
|
|
169
|
+
throw new Error(`${this.options.label} returned an invalid session id.`);
|
|
170
|
+
}
|
|
171
|
+
this.sessionId = created.sessionId;
|
|
172
|
+
}
|
|
173
|
+
// session/load replays history by protocol design. The renderer already has
|
|
174
|
+
// its persisted transcript, so only forward chunks from subsequent turns.
|
|
175
|
+
this.acceptAgentMessages = true;
|
|
176
|
+
this.emit('session', this.sessionId);
|
|
177
|
+
}
|
|
178
|
+
handleSessionUpdate(params) {
|
|
179
|
+
const update = params.update;
|
|
180
|
+
if (this.acceptAgentMessages
|
|
181
|
+
&& update.sessionUpdate === 'agent_message_chunk'
|
|
182
|
+
&& update.content.type === 'text'
|
|
183
|
+
&& update.content.text) {
|
|
184
|
+
this.turnProducedMessage = true;
|
|
185
|
+
this.emit('data', update.content.text);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
async send(data) {
|
|
189
|
+
if (this.processing || this.stopped || !data)
|
|
190
|
+
return;
|
|
191
|
+
this.processing = true;
|
|
192
|
+
try {
|
|
193
|
+
await this.ready;
|
|
194
|
+
const connection = this.transport?.connection;
|
|
195
|
+
if (!connection || !this.sessionId || this.stopped)
|
|
196
|
+
return;
|
|
197
|
+
this.turnProducedMessage = false;
|
|
198
|
+
const result = await connection.prompt({
|
|
199
|
+
sessionId: this.sessionId,
|
|
200
|
+
prompt: [{ type: 'text', text: data }],
|
|
201
|
+
});
|
|
202
|
+
if (result.stopReason !== 'end_turn' && result.stopReason !== 'cancelled') {
|
|
203
|
+
this.emit('system', `${this.options.label} stopped the turn: ${result.stopReason}.`);
|
|
204
|
+
}
|
|
205
|
+
else if (result.stopReason === 'end_turn' && !this.turnProducedMessage) {
|
|
206
|
+
this.emit('error', new Error(`${this.options.label} ended without a recognizable response. Update the harness and try again.`));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
if (!this.stopped && !this.reportedProcessFailure) {
|
|
211
|
+
this.emit('error', this.toActionableError(error));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
finally {
|
|
215
|
+
this.processing = false;
|
|
216
|
+
if (!this.stopped)
|
|
217
|
+
this.emit('idle');
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
stop() {
|
|
221
|
+
if (this.stopped)
|
|
222
|
+
return;
|
|
223
|
+
this.stopped = true;
|
|
224
|
+
const transport = this.transport;
|
|
225
|
+
const sessionId = this.sessionId;
|
|
226
|
+
this.transport = null;
|
|
227
|
+
this.sessionId = null;
|
|
228
|
+
if (transport)
|
|
229
|
+
void this.shutdownTransport(transport, this.processing ? sessionId : null);
|
|
230
|
+
this.processing = false;
|
|
231
|
+
this.emit('close', 0);
|
|
232
|
+
}
|
|
233
|
+
observeProcess(child) {
|
|
234
|
+
child.stderr?.on('data', (chunk) => {
|
|
235
|
+
const text = chunk.toString();
|
|
236
|
+
this.stderrTail = (this.stderrTail + text).slice(-2_000);
|
|
237
|
+
console.warn(`[${this.options.label} stderr]`, text.trimEnd());
|
|
238
|
+
});
|
|
239
|
+
child.once('error', (error) => {
|
|
240
|
+
if (!this.stopped)
|
|
241
|
+
this.reportConnectionFailure(error);
|
|
242
|
+
});
|
|
243
|
+
child.once('close', (code) => {
|
|
244
|
+
if (!this.stopped && !this.reportedProcessFailure) {
|
|
245
|
+
const suffix = this.stderrTail ? `: ${this.stderrTail.trim()}` : '';
|
|
246
|
+
this.reportConnectionFailure(new Error(`${this.options.label} ACP process exited with code ${code}${suffix}`));
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
reportConnectionFailure(error) {
|
|
251
|
+
if (this.reportedProcessFailure)
|
|
252
|
+
return;
|
|
253
|
+
this.reportedProcessFailure = true;
|
|
254
|
+
this.emit('error', this.toActionableError(error));
|
|
255
|
+
// An in-flight prompt owns the idle transition in send(). Publishing idle
|
|
256
|
+
// here would let the server admit a second prompt while processing is true.
|
|
257
|
+
if (!this.processing)
|
|
258
|
+
this.emit('idle');
|
|
259
|
+
this.stopTransport(true);
|
|
260
|
+
}
|
|
261
|
+
toActionableError(error) {
|
|
262
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
263
|
+
if (/auth(?:entication)?[_ -]?required|not authenticated|not logged in|unauthorized/i.test(message)) {
|
|
264
|
+
const action = this.options.loginCommand
|
|
265
|
+
? ` Run \`${this.options.loginCommand}\` in a terminal, then try again.`
|
|
266
|
+
: '';
|
|
267
|
+
return new Error(`${this.options.label} is not signed in.${action}`);
|
|
268
|
+
}
|
|
269
|
+
return new Error(`${this.options.label} failed: ${message}`);
|
|
270
|
+
}
|
|
271
|
+
stopTransport(kill) {
|
|
272
|
+
const child = this.transport?.process ?? null;
|
|
273
|
+
child?.stdin?.end();
|
|
274
|
+
if (kill)
|
|
275
|
+
killTree(child);
|
|
276
|
+
this.transport = null;
|
|
277
|
+
this.sessionId = null;
|
|
278
|
+
}
|
|
279
|
+
async shutdownTransport(transport, sessionId) {
|
|
280
|
+
if (sessionId) {
|
|
281
|
+
await Promise.race([
|
|
282
|
+
transport.connection.cancel({ sessionId }).catch(() => { }),
|
|
283
|
+
new Promise((resolve) => setTimeout(resolve, 250)),
|
|
284
|
+
]);
|
|
285
|
+
}
|
|
286
|
+
transport.process.stdin?.end();
|
|
287
|
+
void transport.connection.close().catch(() => { });
|
|
288
|
+
const forceKill = setTimeout(() => killTree(transport.process), 500);
|
|
289
|
+
forceKill.unref?.();
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
//# sourceMappingURL=AcpCliAdapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AcpCliAdapter.js","sourceRoot":"","sources":["../../src/agent/AcpCliAdapter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AcpCliAdapter.test.d.ts","sourceRoot":"","sources":["../../src/agent/AcpCliAdapter.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { AcpCliAdapter, decideReadOnlyPermission, isSafeAcpSessionId, } from './AcpCliAdapter.js';
|
|
5
|
+
import { buildCopilotAcpArgs } from './CopilotAcpAdapter.js';
|
|
6
|
+
const SESSION_ID = '123e4567-e89b-12d3-a456-426614174000';
|
|
7
|
+
class FakeChild extends EventEmitter {
|
|
8
|
+
stdin = { end: vi.fn() };
|
|
9
|
+
stdout = new EventEmitter();
|
|
10
|
+
stderr = new EventEmitter();
|
|
11
|
+
killed = false;
|
|
12
|
+
exitCode = null;
|
|
13
|
+
pid;
|
|
14
|
+
}
|
|
15
|
+
function makeConnection(overrides = {}) {
|
|
16
|
+
return {
|
|
17
|
+
initialize: vi.fn().mockResolvedValue({
|
|
18
|
+
protocolVersion: 1,
|
|
19
|
+
agentCapabilities: { loadSession: true },
|
|
20
|
+
}),
|
|
21
|
+
newSession: vi.fn().mockResolvedValue({ sessionId: SESSION_ID }),
|
|
22
|
+
loadSession: vi.fn().mockResolvedValue({}),
|
|
23
|
+
prompt: vi.fn().mockResolvedValue({ stopReason: 'end_turn' }),
|
|
24
|
+
cancel: vi.fn().mockResolvedValue(undefined),
|
|
25
|
+
close: vi.fn().mockResolvedValue(undefined),
|
|
26
|
+
closed: new Promise(() => { }),
|
|
27
|
+
...overrides,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function makeHarness(connection) {
|
|
31
|
+
const child = new FakeChild();
|
|
32
|
+
let client;
|
|
33
|
+
const factory = vi.fn((options) => {
|
|
34
|
+
client = options.client;
|
|
35
|
+
return { process: child, connection };
|
|
36
|
+
});
|
|
37
|
+
const harness = new AcpCliAdapter({
|
|
38
|
+
executable: 'copilot',
|
|
39
|
+
args: buildCopilotAcpArgs(),
|
|
40
|
+
label: 'GitHub Copilot CLI',
|
|
41
|
+
loginCommand: 'copilot login',
|
|
42
|
+
validateSessionId: (id) => id === SESSION_ID,
|
|
43
|
+
transportFactory: factory,
|
|
44
|
+
});
|
|
45
|
+
return { harness, child, factory, getClient: () => client };
|
|
46
|
+
}
|
|
47
|
+
describe('Copilot ACP profile', () => {
|
|
48
|
+
it('starts stdio ACP with only read/search tools and no remote or MCP surface', () => {
|
|
49
|
+
expect(buildCopilotAcpArgs()).toEqual([
|
|
50
|
+
'--acp',
|
|
51
|
+
'--stdio',
|
|
52
|
+
'--available-tools=view,glob,grep',
|
|
53
|
+
'--disable-builtin-mcps',
|
|
54
|
+
'--no-ask-user',
|
|
55
|
+
'--no-auto-update',
|
|
56
|
+
'--no-remote',
|
|
57
|
+
'--no-remote-export',
|
|
58
|
+
]);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
describe('read-only ACP permissions', () => {
|
|
62
|
+
const options = [
|
|
63
|
+
{ optionId: 'allow', name: 'Allow once', kind: 'allow_once' },
|
|
64
|
+
{ optionId: 'reject', name: 'Reject once', kind: 'reject_once' },
|
|
65
|
+
];
|
|
66
|
+
it.each(['read', 'search', 'edit', 'delete', 'move', 'execute', 'fetch', 'other'])('rejects every escalated %s operation', (kind) => {
|
|
67
|
+
expect(decideReadOnlyPermission({
|
|
68
|
+
sessionId: SESSION_ID,
|
|
69
|
+
toolCall: { toolCallId: 'tool', kind },
|
|
70
|
+
options,
|
|
71
|
+
})).toEqual({ outcome: { outcome: 'selected', optionId: 'reject' } });
|
|
72
|
+
});
|
|
73
|
+
it('cancels when the provider offers no policy-compatible option', () => {
|
|
74
|
+
expect(decideReadOnlyPermission({
|
|
75
|
+
sessionId: SESSION_ID,
|
|
76
|
+
toolCall: { toolCallId: 'tool', kind: 'edit' },
|
|
77
|
+
options: [{ optionId: 'allow', name: 'Allow', kind: 'allow_always' }],
|
|
78
|
+
})).toEqual({ outcome: { outcome: 'cancelled' } });
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
describe('AcpCliAdapter', () => {
|
|
82
|
+
it('exchanges a streamed turn with a real ACP subprocess', async () => {
|
|
83
|
+
const fixturePath = fileURLToPath(new URL('./fixtures/fake-acp-agent.mjs', import.meta.url));
|
|
84
|
+
const harness = new AcpCliAdapter({
|
|
85
|
+
executable: process.execPath,
|
|
86
|
+
args: [fixturePath],
|
|
87
|
+
label: 'Fake ACP',
|
|
88
|
+
validateSessionId: (id) => id === SESSION_ID,
|
|
89
|
+
});
|
|
90
|
+
const output = [];
|
|
91
|
+
harness.on('data', (text) => output.push(text));
|
|
92
|
+
await harness.start(process.cwd());
|
|
93
|
+
await harness.send('hello');
|
|
94
|
+
harness.stop();
|
|
95
|
+
expect(output).toEqual(['real ACP response']);
|
|
96
|
+
});
|
|
97
|
+
it('creates a provider session and streams only agent text', async () => {
|
|
98
|
+
let client;
|
|
99
|
+
const connection = makeConnection({
|
|
100
|
+
prompt: vi.fn(async () => {
|
|
101
|
+
await client.sessionUpdate({
|
|
102
|
+
sessionId: SESSION_ID,
|
|
103
|
+
update: {
|
|
104
|
+
sessionUpdate: 'agent_message_chunk',
|
|
105
|
+
content: { type: 'text', text: 'Hello from Copilot' },
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
return { stopReason: 'end_turn' };
|
|
109
|
+
}),
|
|
110
|
+
});
|
|
111
|
+
const fixture = makeHarness(connection);
|
|
112
|
+
const sessions = [];
|
|
113
|
+
const output = [];
|
|
114
|
+
fixture.harness.on('session', (id) => sessions.push(id));
|
|
115
|
+
fixture.harness.on('data', (text) => output.push(text));
|
|
116
|
+
const start = fixture.harness.start('C:\\workspace');
|
|
117
|
+
client = fixture.getClient();
|
|
118
|
+
await start;
|
|
119
|
+
await fixture.harness.send('Explain this repo');
|
|
120
|
+
expect(sessions).toEqual([SESSION_ID]);
|
|
121
|
+
expect(output).toEqual(['Hello from Copilot']);
|
|
122
|
+
expect(connection.newSession).toHaveBeenCalledWith({ cwd: 'C:\\workspace', mcpServers: [] });
|
|
123
|
+
expect(connection.prompt).toHaveBeenCalledWith({
|
|
124
|
+
sessionId: SESSION_ID,
|
|
125
|
+
prompt: [{ type: 'text', text: 'Explain this repo' }],
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
it('loads an existing session without replaying its history into the persisted chat', async () => {
|
|
129
|
+
let client;
|
|
130
|
+
const connection = makeConnection({
|
|
131
|
+
loadSession: vi.fn(async () => {
|
|
132
|
+
await client.sessionUpdate({
|
|
133
|
+
sessionId: SESSION_ID,
|
|
134
|
+
update: {
|
|
135
|
+
sessionUpdate: 'agent_message_chunk',
|
|
136
|
+
content: { type: 'text', text: 'old replayed answer' },
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
return {};
|
|
140
|
+
}),
|
|
141
|
+
});
|
|
142
|
+
const fixture = makeHarness(connection);
|
|
143
|
+
const output = [];
|
|
144
|
+
fixture.harness.on('data', (text) => output.push(text));
|
|
145
|
+
const start = fixture.harness.start('C:\\workspace', { id: SESSION_ID, resume: true });
|
|
146
|
+
client = fixture.getClient();
|
|
147
|
+
await start;
|
|
148
|
+
expect(connection.loadSession).toHaveBeenCalledWith({
|
|
149
|
+
sessionId: SESSION_ID,
|
|
150
|
+
cwd: 'C:\\workspace',
|
|
151
|
+
mcpServers: [],
|
|
152
|
+
});
|
|
153
|
+
expect(connection.newSession).not.toHaveBeenCalled();
|
|
154
|
+
expect(output).toEqual([]);
|
|
155
|
+
});
|
|
156
|
+
it('turns ACP authentication failures into login guidance', async () => {
|
|
157
|
+
const connection = makeConnection({
|
|
158
|
+
newSession: vi.fn().mockRejectedValue(new Error('auth_required')),
|
|
159
|
+
});
|
|
160
|
+
const { harness } = makeHarness(connection);
|
|
161
|
+
const errors = [];
|
|
162
|
+
harness.on('error', (error) => errors.push(error.message));
|
|
163
|
+
await harness.start('C:\\workspace');
|
|
164
|
+
expect(errors).toEqual([
|
|
165
|
+
'GitHub Copilot CLI is not signed in. Run `copilot login` in a terminal, then try again.',
|
|
166
|
+
]);
|
|
167
|
+
});
|
|
168
|
+
it('rejects invalid provider session ids before exposing them to the renderer', async () => {
|
|
169
|
+
const connection = makeConnection({
|
|
170
|
+
newSession: vi.fn().mockResolvedValue({ sessionId: 'not-the-expected-id' }),
|
|
171
|
+
});
|
|
172
|
+
const { harness } = makeHarness(connection);
|
|
173
|
+
const errors = [];
|
|
174
|
+
harness.on('error', (error) => errors.push(error.message));
|
|
175
|
+
await harness.start('C:\\workspace');
|
|
176
|
+
expect(errors[0]).toMatch(/invalid session id/i);
|
|
177
|
+
});
|
|
178
|
+
it('reports a successful turn with no recognizable message', async () => {
|
|
179
|
+
const { harness } = makeHarness(makeConnection());
|
|
180
|
+
const errors = [];
|
|
181
|
+
harness.on('error', (error) => errors.push(error.message));
|
|
182
|
+
await harness.start('C:\\workspace');
|
|
183
|
+
await harness.send('Say something');
|
|
184
|
+
expect(errors).toEqual([
|
|
185
|
+
'GitHub Copilot CLI ended without a recognizable response. Update the harness and try again.',
|
|
186
|
+
]);
|
|
187
|
+
});
|
|
188
|
+
it('cancels the active ACP turn before closing the transport', async () => {
|
|
189
|
+
let finishPrompt;
|
|
190
|
+
const connection = makeConnection({
|
|
191
|
+
prompt: vi.fn(() => new Promise((resolve) => {
|
|
192
|
+
finishPrompt = resolve;
|
|
193
|
+
})),
|
|
194
|
+
});
|
|
195
|
+
const { harness } = makeHarness(connection);
|
|
196
|
+
await harness.start('C:\\workspace');
|
|
197
|
+
const sending = harness.send('Long task');
|
|
198
|
+
await vi.waitFor(() => expect(connection.prompt).toHaveBeenCalled());
|
|
199
|
+
harness.stop();
|
|
200
|
+
await vi.waitFor(() => expect(connection.cancel).toHaveBeenCalledWith({ sessionId: SESSION_ID }));
|
|
201
|
+
finishPrompt({ stopReason: 'cancelled' });
|
|
202
|
+
await sending;
|
|
203
|
+
});
|
|
204
|
+
it('does not publish idle for a process failure until the active prompt settles', async () => {
|
|
205
|
+
let rejectPrompt;
|
|
206
|
+
const connection = makeConnection({
|
|
207
|
+
prompt: vi.fn(() => new Promise((_resolve, reject) => {
|
|
208
|
+
rejectPrompt = reject;
|
|
209
|
+
})),
|
|
210
|
+
});
|
|
211
|
+
const { harness, child } = makeHarness(connection);
|
|
212
|
+
const errors = [];
|
|
213
|
+
const idle = [];
|
|
214
|
+
harness.on('error', (error) => errors.push(error.message));
|
|
215
|
+
harness.on('idle', () => idle.push(1));
|
|
216
|
+
await harness.start('C:\\workspace');
|
|
217
|
+
const sending = harness.send('Long task');
|
|
218
|
+
await vi.waitFor(() => expect(connection.prompt).toHaveBeenCalled());
|
|
219
|
+
child.emit('error', new Error('transport broke'));
|
|
220
|
+
expect(errors).toEqual(['GitHub Copilot CLI failed: transport broke']);
|
|
221
|
+
expect(idle).toEqual([]);
|
|
222
|
+
rejectPrompt(new Error('connection closed'));
|
|
223
|
+
await sending;
|
|
224
|
+
expect(errors).toEqual(['GitHub Copilot CLI failed: transport broke']);
|
|
225
|
+
expect(idle).toHaveLength(1);
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
describe('isSafeAcpSessionId', () => {
|
|
229
|
+
it('accepts bounded opaque ids and rejects control characters', () => {
|
|
230
|
+
expect(isSafeAcpSessionId('ses_abc-123')).toBe(true);
|
|
231
|
+
expect(isSafeAcpSessionId('ses_abc\n123')).toBe(false);
|
|
232
|
+
expect(isSafeAcpSessionId('x'.repeat(201))).toBe(false);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
//# sourceMappingURL=AcpCliAdapter.test.js.map
|