codeep 3.3.3 → 3.4.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/dist/acp/commands.d.ts +50 -1
- package/dist/acp/commands.js +545 -109
- package/dist/acp/protocol.d.ts +14 -5
- package/dist/acp/server.d.ts +36 -1
- package/dist/acp/server.js +581 -155
- package/dist/acp/serverHandlers.d.ts +2 -1
- package/dist/acp/serverHandlers.js +3 -0
- package/dist/acp/session.d.ts +28 -2
- package/dist/acp/session.js +25 -6
- package/dist/acp/transport.d.ts +40 -4
- package/dist/acp/transport.js +218 -25
- package/dist/acp/turns.d.ts +20 -0
- package/dist/acp/turns.js +30 -0
- package/dist/api/index.js +2 -0
- package/dist/api/ollamaNative.d.ts +3 -0
- package/dist/api/ollamaNative.js +35 -3
- package/dist/config/index.d.ts +21 -4
- package/dist/config/index.js +178 -123
- package/dist/renderer/agentExecution.d.ts +30 -2
- package/dist/renderer/agentExecution.js +248 -92
- package/dist/renderer/commands/helpers.d.ts +18 -2
- package/dist/renderer/commands/helpers.js +28 -5
- package/dist/renderer/commands.d.ts +2 -0
- package/dist/renderer/commands.js +180 -64
- package/dist/renderer/main.d.ts +41 -0
- package/dist/renderer/main.js +181 -80
- package/dist/utils/agent.d.ts +69 -4
- package/dist/utils/agent.js +416 -248
- package/dist/utils/agentChat.js +82 -10
- package/dist/utils/agents.d.ts +2 -1
- package/dist/utils/agents.js +100 -29
- package/dist/utils/auditLog.d.ts +4 -3
- package/dist/utils/auditLog.js +92 -9
- package/dist/utils/checkpoints.js +11 -6
- package/dist/utils/codeReview.js +28 -23
- package/dist/utils/codeepCloud.d.ts +14 -2
- package/dist/utils/codeepCloud.js +56 -20
- package/dist/utils/customCommands.js +7 -2
- package/dist/utils/git.d.ts +262 -4
- package/dist/utils/git.js +1928 -61
- package/dist/utils/gitHookInstaller.d.ts +32 -1
- package/dist/utils/gitHookInstaller.js +76 -8
- package/dist/utils/gitignore.d.ts +8 -0
- package/dist/utils/gitignore.js +41 -10
- package/dist/utils/headlessReview.d.ts +11 -0
- package/dist/utils/headlessReview.js +33 -5
- package/dist/utils/history.d.ts +22 -6
- package/dist/utils/history.js +140 -26
- package/dist/utils/logger.js +6 -7
- package/dist/utils/mcpConfig.d.ts +24 -0
- package/dist/utils/mcpConfig.js +36 -5
- package/dist/utils/mentions.d.ts +28 -5
- package/dist/utils/mentions.js +253 -45
- package/dist/utils/personalities.js +16 -6
- package/dist/utils/planMode.d.ts +13 -7
- package/dist/utils/planMode.js +32 -12
- package/dist/utils/projectIntelligence.d.ts +2 -0
- package/dist/utils/projectIntelligence.js +27 -8
- package/dist/utils/projectPaths.d.ts +53 -0
- package/dist/utils/projectPaths.js +146 -0
- package/dist/utils/shell.d.ts +119 -0
- package/dist/utils/shell.js +417 -45
- package/dist/utils/skillBundles.js +17 -7
- package/dist/utils/skillBundlesCloud.js +20 -3
- package/dist/utils/skills.d.ts +24 -2
- package/dist/utils/skills.js +235 -43
- package/dist/utils/smartContext.js +97 -23
- package/dist/utils/telegramApproval.d.ts +10 -2
- package/dist/utils/telegramApproval.js +22 -4
- package/dist/utils/toolExecution.d.ts +50 -2
- package/dist/utils/toolExecution.js +418 -16
- package/dist/utils/toolParsing.d.ts +7 -1
- package/dist/utils/toolParsing.js +12 -3
- package/dist/utils/userProfile.js +58 -16
- package/dist/utils/verify.d.ts +25 -4
- package/dist/utils/verify.js +259 -74
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -3,7 +3,8 @@ import type { AcpSession } from './commands.js';
|
|
|
3
3
|
/** Session record held by the running server. Lives in server.ts; re-exposed
|
|
4
4
|
* here so handler signatures can name it without re-declaring the shape. */
|
|
5
5
|
export interface AcpServerSession extends AcpSession {
|
|
6
|
-
|
|
6
|
+
/** Controllers of the prompts still running; session/cancel aborts all. */
|
|
7
|
+
activePrompts: Set<AbortController>;
|
|
7
8
|
currentModeId: string;
|
|
8
9
|
titleSent: boolean;
|
|
9
10
|
hadHistory: boolean;
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
import { AGENT_MODES, buildConfigOptions } from './server.js';
|
|
19
19
|
import { config, setProvider, setApiKey, listSessionsWithInfo, deleteSession as deleteSessionFile, } from '../config/index.js';
|
|
20
20
|
import { disposeSession as disposeMcpSession } from '../utils/mcpRegistry.js';
|
|
21
|
+
import { clearPendingPlan } from '../utils/planMode.js';
|
|
21
22
|
// ─── session/set_mode ─────────────────────────────────────────────────────────
|
|
22
23
|
/**
|
|
23
24
|
* Switch the agent confirmation mode for a session (auto = no prompts,
|
|
@@ -192,6 +193,8 @@ export function handleSessionDelete(msg, deps) {
|
|
|
192
193
|
const { sessionId, cwd } = (msg.params ?? {});
|
|
193
194
|
// Remove from in-memory sessions map if present
|
|
194
195
|
sessions.delete(sessionId);
|
|
196
|
+
// Its pending /plan goes with it.
|
|
197
|
+
clearPendingPlan(sessionId);
|
|
195
198
|
// Tear down any MCP server processes attached to this session — leaks
|
|
196
199
|
// children otherwise. Fire-and-forget; client doesn't wait on stop().
|
|
197
200
|
disposeMcpSession(sessionId).catch(() => { });
|
package/dist/acp/session.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { PermissionOutcome } from '../utils/agent.js';
|
|
2
2
|
import { ProjectContext } from '../utils/project.js';
|
|
3
3
|
import { ToolCall } from '../utils/tools.js';
|
|
4
|
-
import type { FsCallbacks } from '../utils/toolExecution.js';
|
|
4
|
+
import type { FsCallbacks, TrustBearingWrite } from '../utils/toolExecution.js';
|
|
5
|
+
import type { Message } from '../config/index.js';
|
|
5
6
|
export interface AgentSessionOptions {
|
|
6
7
|
prompt: string;
|
|
7
8
|
workspaceRoot: string;
|
|
@@ -10,7 +11,18 @@ export interface AgentSessionOptions {
|
|
|
10
11
|
onChunk: (text: string) => void;
|
|
11
12
|
onThought?: (text: string) => void;
|
|
12
13
|
onToolCall?: (toolCallId: string, toolName: string, kind: string, title: string, status: 'pending' | 'running' | 'finished' | 'error', locations?: string[], rawOutput?: string) => void;
|
|
13
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Ask the user about one tool call. Handed straight to runAgent, so the
|
|
16
|
+
* signature is runAgent's: `trustBearing` is what the agent's own gate
|
|
17
|
+
* already worked out about the call — the file it would write that decides
|
|
18
|
+
* what runs later, or null when it writes no such file — and the dialog
|
|
19
|
+
* words itself from that instead of resolving the path a second time.
|
|
20
|
+
*
|
|
21
|
+
* Declared with one parameter, this type said the second argument did not
|
|
22
|
+
* exist while runAgent passed it on every call, so the one caller that
|
|
23
|
+
* needs it had to cast its way back to the truth.
|
|
24
|
+
*/
|
|
25
|
+
onRequestPermission?: (toolCall: ToolCall, trustBearing?: TrustBearingWrite | null) => Promise<PermissionOutcome>;
|
|
14
26
|
/** Tools to force into the per-run dangerous set (ACP manual mode). */
|
|
15
27
|
extraDangerousTools?: string[];
|
|
16
28
|
onExecuteCommand?: (command: string, args: string[], cwd: string) => Promise<{
|
|
@@ -20,7 +32,21 @@ export interface AgentSessionOptions {
|
|
|
20
32
|
}>;
|
|
21
33
|
/** Optional fs delegation when the ACP client advertises `fs` capability. */
|
|
22
34
|
fs?: FsCallbacks;
|
|
35
|
+
/**
|
|
36
|
+
* Earlier turns of this conversation. ACP clients send only the new
|
|
37
|
+
* message, so without this the agent would start every turn blind to a
|
|
38
|
+
* loaded, rewound or compacted session.
|
|
39
|
+
*/
|
|
40
|
+
chatHistory?: Message[];
|
|
23
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* The part of a session's history the agent sees: user and assistant turns,
|
|
44
|
+
* as the TUI passes them (`App.getChatHistory`).
|
|
45
|
+
*/
|
|
46
|
+
export declare function toAgentChatHistory(history: Message[]): Array<{
|
|
47
|
+
role: 'user' | 'assistant';
|
|
48
|
+
content: string;
|
|
49
|
+
}>;
|
|
24
50
|
/**
|
|
25
51
|
* Build a ProjectContext from a workspace root directory.
|
|
26
52
|
* Falls back to a minimal synthetic context if scanning fails.
|
package/dist/acp/session.js
CHANGED
|
@@ -3,6 +3,15 @@
|
|
|
3
3
|
import { join, isAbsolute } from 'path';
|
|
4
4
|
import { runAgent } from '../utils/agent.js';
|
|
5
5
|
import { getProjectContext } from '../utils/project.js';
|
|
6
|
+
/**
|
|
7
|
+
* The part of a session's history the agent sees: user and assistant turns,
|
|
8
|
+
* as the TUI passes them (`App.getChatHistory`).
|
|
9
|
+
*/
|
|
10
|
+
export function toAgentChatHistory(history) {
|
|
11
|
+
return history
|
|
12
|
+
.filter((m) => m.role === 'user' || m.role === 'assistant')
|
|
13
|
+
.map(m => ({ role: m.role, content: m.content }));
|
|
14
|
+
}
|
|
6
15
|
/**
|
|
7
16
|
* Build a ProjectContext from a workspace root directory.
|
|
8
17
|
* Falls back to a minimal synthetic context if scanning fails.
|
|
@@ -184,18 +193,28 @@ export async function runAgentSession(opts) {
|
|
|
184
193
|
// `conversationId` is the ACP session id, which is what
|
|
185
194
|
// registerSessionServers keyed by in server.ts handleSessionNew.
|
|
186
195
|
mcpSessionId: opts.conversationId,
|
|
196
|
+
chatHistory: opts.chatHistory ? toAgentChatHistory(opts.chatHistory) : undefined,
|
|
187
197
|
});
|
|
188
|
-
// result.finalResponse is
|
|
189
|
-
//
|
|
190
|
-
// —
|
|
198
|
+
// result.finalResponse is mostly emitted via onChunk streaming above;
|
|
199
|
+
// emit all of it here if nothing was streamed (e.g. non-streaming fallback path)
|
|
200
|
+
// — or a paused/interrupted run, whose finalResponse is a fresh "say
|
|
191
201
|
// continue" notice that was never streamed and must always reach the client.
|
|
202
|
+
// Otherwise send what the loop wrote itself and never streamed (the
|
|
203
|
+
// verification result, a reviewer's notes, a notice that replaced the
|
|
204
|
+
// answer): without it an editor never hears that verification failed.
|
|
192
205
|
if (result.finalResponse && (chunksEmitted === 0 || result.interrupted)) {
|
|
193
206
|
opts.onChunk(result.finalResponse);
|
|
194
207
|
}
|
|
208
|
+
else if (result.finalResponse && !result.aborted) {
|
|
209
|
+
const tail = result.unstreamedText ?? '';
|
|
210
|
+
if (tail)
|
|
211
|
+
opts.onChunk(`\n\n${tail}`);
|
|
212
|
+
}
|
|
195
213
|
// Surface errors as thrown exceptions so the ACP server can handle them correctly.
|
|
196
|
-
// Exception:
|
|
197
|
-
//
|
|
198
|
-
//
|
|
214
|
+
// Exception: once chunks were streamed, the rest of finalResponse was sent
|
|
215
|
+
// just above (e.g. "✗ Verification failed", "Agent stopped due to repeated
|
|
216
|
+
// API timeouts"), so don't also throw — the user already received the
|
|
217
|
+
// explanation and Zed would show a confusing second error.
|
|
199
218
|
if (!result.success && !result.aborted) {
|
|
200
219
|
const alreadyExplained = result.finalResponse && chunksEmitted > 0;
|
|
201
220
|
if (!alreadyExplained) {
|
package/dist/acp/transport.d.ts
CHANGED
|
@@ -1,12 +1,43 @@
|
|
|
1
1
|
import { JsonRpcRequest, JsonRpcResponse, JsonRpcNotification } from './protocol.js';
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* A frame with its obvious credentials blanked.
|
|
4
|
+
*
|
|
5
|
+
* Exported for unit testing (see transport.test.ts).
|
|
6
|
+
*/
|
|
7
|
+
export declare function redactCredentials(frame: string): string;
|
|
8
|
+
type MessageHandler = (msg: JsonRpcRequest | JsonRpcNotification) => void | Promise<unknown>;
|
|
9
|
+
export interface RequestOptions {
|
|
10
|
+
/**
|
|
11
|
+
* How long to wait for the client's answer. Defaults to 30s; 0 waits
|
|
12
|
+
* indefinitely, for calls that wait on a person (permission dialogs) or
|
|
13
|
+
* on a running process (terminal/wait_for_exit).
|
|
14
|
+
*/
|
|
15
|
+
timeoutMs?: number;
|
|
16
|
+
/** Stop waiting when this fires (e.g. the prompt was cancelled). */
|
|
17
|
+
signal?: AbortSignal;
|
|
18
|
+
}
|
|
19
|
+
/** The client answered one of our requests with a JSON-RPC error. */
|
|
20
|
+
export declare class AcpRequestError extends Error {
|
|
21
|
+
readonly method: string;
|
|
22
|
+
readonly code: number;
|
|
23
|
+
constructor(method: string, code: number, message: string);
|
|
24
|
+
}
|
|
25
|
+
/** The client did not answer one of our requests in time. */
|
|
26
|
+
export declare class AcpRequestTimeoutError extends Error {
|
|
27
|
+
readonly method: string;
|
|
28
|
+
readonly timeoutMs: number;
|
|
29
|
+
constructor(method: string, timeoutMs: number);
|
|
30
|
+
}
|
|
3
31
|
export declare class StdioTransport {
|
|
4
32
|
private buffer;
|
|
5
|
-
|
|
33
|
+
protected handler: MessageHandler | null;
|
|
6
34
|
private pendingRequests;
|
|
7
35
|
private requestIdCounter;
|
|
36
|
+
private unanswered;
|
|
8
37
|
start(handler: MessageHandler): void;
|
|
9
|
-
|
|
38
|
+
protected onData(chunk: string): void;
|
|
39
|
+
private dispatch;
|
|
40
|
+
protected write(line: string): void;
|
|
10
41
|
send(msg: JsonRpcResponse | JsonRpcNotification): void;
|
|
11
42
|
respond(id: number | string, result: unknown): void;
|
|
12
43
|
error(id: number | string, code: number, message: string): void;
|
|
@@ -14,7 +45,12 @@ export declare class StdioTransport {
|
|
|
14
45
|
/**
|
|
15
46
|
* Send a JSON-RPC request to the client and wait for the response.
|
|
16
47
|
* Used for agent-initiated requests like session/request_permission.
|
|
48
|
+
*
|
|
49
|
+
* Rejects with AcpRequestError when the client answers with an error,
|
|
50
|
+
* with AcpRequestTimeoutError when it does not answer in time, and with
|
|
51
|
+
* an AbortError when `signal` fires. A result is never invented: callers
|
|
52
|
+
* that treat a missing answer as "no" must say so with their own catch.
|
|
17
53
|
*/
|
|
18
|
-
request(method: string, params: unknown): Promise<unknown>;
|
|
54
|
+
request(method: string, params: unknown, options?: RequestOptions): Promise<unknown>;
|
|
19
55
|
}
|
|
20
56
|
export {};
|
package/dist/acp/transport.js
CHANGED
|
@@ -1,36 +1,157 @@
|
|
|
1
1
|
// acp/transport.ts
|
|
2
2
|
// Newline-delimited JSON-RPC over stdio
|
|
3
|
-
import { appendFileSync, mkdirSync } from 'node:fs';
|
|
3
|
+
import { appendFileSync, chmodSync, mkdirSync, renameSync, statSync } from 'node:fs';
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
5
|
import { join, dirname } from 'node:path';
|
|
6
6
|
// Debug log destination — when CODEEP_ACP_DEBUG is set we mirror every
|
|
7
7
|
// inbound and outbound JSON-RPC frame here. Using a file (not stderr) because
|
|
8
8
|
// most ACP clients (Zed included) do not pipe agent stderr to anywhere the
|
|
9
9
|
// user can easily read; a known on-disk path is reliable everywhere.
|
|
10
|
+
//
|
|
11
|
+
// WHAT IS IN IT, because a user asked for it in a bug report will attach the
|
|
12
|
+
// whole file: every frame of the session. That is the prompts, the model's
|
|
13
|
+
// replies, the contents of every file read or written through fs/*, the
|
|
14
|
+
// commands run in the client's terminal and their output, and the `env` of
|
|
15
|
+
// every terminal/create. redactCredentials() blanks the obvious credential
|
|
16
|
+
// shapes on the way in, but it is a filter over text and not a guarantee — a
|
|
17
|
+
// secret that does not look like one survives it. So: session-private, 0600
|
|
18
|
+
// in a 0700 directory, and not something to paste anywhere unread.
|
|
10
19
|
const ACP_DEBUG_PATH = process.env.CODEEP_ACP_DEBUG_FILE
|
|
11
20
|
|| join(homedir(), '.cache', 'codeep', 'acp-debug.log');
|
|
12
21
|
const ACP_DEBUG = !!process.env.CODEEP_ACP_DEBUG;
|
|
22
|
+
/**
|
|
23
|
+
* Roll the log over at 8MB, keeping one previous file.
|
|
24
|
+
*
|
|
25
|
+
* It had no limit at all: every frame was appended and nothing ever truncated
|
|
26
|
+
* or removed the file, and a frame carries whole file contents and whole
|
|
27
|
+
* command outputs — so a user who left CODEEP_ACP_DEBUG set grew it until the
|
|
28
|
+
* disk stopped them. One previous file rather than a truncate because the
|
|
29
|
+
* frames that explain a broken session are usually the handshake at the top,
|
|
30
|
+
* which is exactly what a truncate throws away. Bounded at twice this, then.
|
|
31
|
+
*/
|
|
32
|
+
const ACP_DEBUG_MAX_BYTES = 8 * 1024 * 1024;
|
|
13
33
|
if (ACP_DEBUG) {
|
|
34
|
+
// 0700: the directory holds a file with the whole session in it.
|
|
14
35
|
try {
|
|
15
|
-
mkdirSync(dirname(ACP_DEBUG_PATH), { recursive: true });
|
|
36
|
+
mkdirSync(dirname(ACP_DEBUG_PATH), { recursive: true, mode: 0o700 });
|
|
16
37
|
}
|
|
17
38
|
catch { /* ignore */ }
|
|
18
39
|
}
|
|
40
|
+
/** Bytes written so far, so the size check costs no syscall per frame. Null
|
|
41
|
+
* until the first write reads what an earlier run left on disk. */
|
|
42
|
+
let acpDebugBytes = null;
|
|
19
43
|
function debugLog(direction, payload) {
|
|
20
44
|
if (!ACP_DEBUG)
|
|
21
45
|
return;
|
|
46
|
+
const line = `${new Date().toISOString()} [ACP${direction}client] ${redactCredentials(payload)}\n`;
|
|
47
|
+
const bytes = Buffer.byteLength(line);
|
|
22
48
|
try {
|
|
23
|
-
|
|
49
|
+
if (acpDebugBytes === null)
|
|
50
|
+
acpDebugBytes = adoptExistingLog();
|
|
51
|
+
if (acpDebugBytes > 0 && acpDebugBytes + bytes > ACP_DEBUG_MAX_BYTES) {
|
|
52
|
+
renameSync(ACP_DEBUG_PATH, `${ACP_DEBUG_PATH}.1`);
|
|
53
|
+
acpDebugBytes = 0;
|
|
54
|
+
}
|
|
55
|
+
// `mode` applies only when the file is created, which after the rename
|
|
56
|
+
// above is every rollover as well as the first frame of the first run.
|
|
57
|
+
appendFileSync(ACP_DEBUG_PATH, line, { mode: 0o600 });
|
|
58
|
+
acpDebugBytes += bytes;
|
|
24
59
|
}
|
|
25
60
|
catch { /* swallow — never break the protocol over a logging failure */ }
|
|
26
61
|
}
|
|
62
|
+
/** The size of the log already on disk, 0 when there is none. */
|
|
63
|
+
function adoptExistingLog() {
|
|
64
|
+
try {
|
|
65
|
+
const stat = statSync(ACP_DEBUG_PATH);
|
|
66
|
+
// A log this build did not create is one an older Codeep created 0644 —
|
|
67
|
+
// world-readable, with everything listed above in it. Tighten it, but
|
|
68
|
+
// only at our own path: CODEEP_ACP_DEBUG_FILE may name something whose
|
|
69
|
+
// mode is not ours to change (a fifo, a tty, a shared file).
|
|
70
|
+
if (!process.env.CODEEP_ACP_DEBUG_FILE && (stat.mode & 0o077) !== 0) {
|
|
71
|
+
chmodSync(ACP_DEBUG_PATH, 0o600);
|
|
72
|
+
}
|
|
73
|
+
return stat.size;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return 0;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Credential shapes blanked before a frame is mirrored to the debug log.
|
|
81
|
+
*
|
|
82
|
+
* The log exists to debug the protocol, so this is deliberately narrow: it
|
|
83
|
+
* blanks what is unmistakably a secret and leaves everything else readable.
|
|
84
|
+
* Matched on the frame TEXT rather than on a parsed object because an inbound
|
|
85
|
+
* frame is logged before it is parsed and may not be JSON at all.
|
|
86
|
+
*
|
|
87
|
+
* Nothing here changes the frame on the wire — only the copy on disk.
|
|
88
|
+
*/
|
|
89
|
+
const ACP_DEBUG_REDACTIONS = [
|
|
90
|
+
// `"apiKey": "…"`, `"authorization": "…"` — the MEMBER NAME says it is a
|
|
91
|
+
// secret, whatever the value looks like.
|
|
92
|
+
[/("[A-Za-z0-9_.-]*(?:api[_-]?key|access[_-]?key|secret|token|password|passwd|credential|authorization|cookie|private[_-]?key)[A-Za-z0-9_.-]*"\s*:\s*)"(?:[^"\\]|\\.)*"/gi, '$1"[redacted]"'],
|
|
93
|
+
// ACP spells an environment as `{"name":…,"value":…}`, so the secret-looking
|
|
94
|
+
// string is the VALUE of `name` and the rule above cannot see it. This is
|
|
95
|
+
// the shape terminal/create used to leak the whole of process.env in.
|
|
96
|
+
[/("name"\s*:\s*"[A-Za-z0-9_.-]*(?:KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|AUTH|COOKIE)[A-Za-z0-9_.-]*"\s*,\s*"value"\s*:\s*)"(?:[^"\\]|\\.)*"/gi, '$1"[redacted]"'],
|
|
97
|
+
// And the shapes that are a credential wherever they turn up — a command
|
|
98
|
+
// line the agent ran, a terminal's own output, a file it read.
|
|
99
|
+
[/\bsk-[A-Za-z0-9_-]{16,}/g, '[redacted]'], // OpenAI / Anthropic
|
|
100
|
+
[/\bgh[pousr]_[A-Za-z0-9]{20,}/g, '[redacted]'], // GitHub
|
|
101
|
+
[/\bgithub_pat_[A-Za-z0-9_]{20,}/g, '[redacted]'],
|
|
102
|
+
[/\bglpat-[A-Za-z0-9_-]{16,}/g, '[redacted]'], // GitLab
|
|
103
|
+
[/\bxox[abprs]-[A-Za-z0-9-]{10,}/g, '[redacted]'], // Slack
|
|
104
|
+
[/\bAKIA[0-9A-Z]{16}\b/g, '[redacted]'], // AWS access key id
|
|
105
|
+
[/\bAIza[0-9A-Za-z_-]{20,}/g, '[redacted]'], // Google
|
|
106
|
+
[/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g, '[redacted]'], // JWT
|
|
107
|
+
[/\bBearer\s+[A-Za-z0-9._~+/-]{16,}={0,2}/gi, 'Bearer [redacted]'],
|
|
108
|
+
// `https://user:password@host` — keep the structure, drop the password.
|
|
109
|
+
[/((?:https?|ssh|git):\/\/[^\s"'/@]+:)[^\s"'/@]+@/g, '$1[redacted]@'],
|
|
110
|
+
];
|
|
111
|
+
/**
|
|
112
|
+
* A frame with its obvious credentials blanked.
|
|
113
|
+
*
|
|
114
|
+
* Exported for unit testing (see transport.test.ts).
|
|
115
|
+
*/
|
|
116
|
+
export function redactCredentials(frame) {
|
|
117
|
+
let out = frame;
|
|
118
|
+
for (const [pattern, replacement] of ACP_DEBUG_REDACTIONS)
|
|
119
|
+
out = out.replace(pattern, replacement);
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
27
122
|
const MAX_BUFFER_SIZE = 10 * 1024 * 1024; // 10MB
|
|
28
123
|
const REQUEST_TIMEOUT_MS = 30_000; // 30s
|
|
124
|
+
/** The client answered one of our requests with a JSON-RPC error. */
|
|
125
|
+
export class AcpRequestError extends Error {
|
|
126
|
+
method;
|
|
127
|
+
code;
|
|
128
|
+
constructor(method, code, message) {
|
|
129
|
+
super(`${method} failed: ${message}`);
|
|
130
|
+
this.method = method;
|
|
131
|
+
this.code = code;
|
|
132
|
+
this.name = 'AcpRequestError';
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/** The client did not answer one of our requests in time. */
|
|
136
|
+
export class AcpRequestTimeoutError extends Error {
|
|
137
|
+
method;
|
|
138
|
+
timeoutMs;
|
|
139
|
+
constructor(method, timeoutMs) {
|
|
140
|
+
super(`${method} got no response within ${timeoutMs}ms`);
|
|
141
|
+
this.method = method;
|
|
142
|
+
this.timeoutMs = timeoutMs;
|
|
143
|
+
this.name = 'AcpRequestTimeoutError';
|
|
144
|
+
}
|
|
145
|
+
}
|
|
29
146
|
export class StdioTransport {
|
|
30
147
|
buffer = '';
|
|
31
148
|
handler = null;
|
|
32
149
|
pendingRequests = new Map();
|
|
33
150
|
requestIdCounter = 1000;
|
|
151
|
+
// Ids of inbound requests we have not answered yet. A handler that fails
|
|
152
|
+
// before answering gets an error reply; one that fails after answering
|
|
153
|
+
// must not produce a second response for the same id.
|
|
154
|
+
unanswered = new Set();
|
|
34
155
|
start(handler) {
|
|
35
156
|
this.handler = handler;
|
|
36
157
|
process.stdin.setEncoding('utf8');
|
|
@@ -49,31 +170,67 @@ export class StdioTransport {
|
|
|
49
170
|
const trimmed = line.trim();
|
|
50
171
|
if (!trimmed)
|
|
51
172
|
continue;
|
|
173
|
+
debugLog('←', trimmed);
|
|
174
|
+
let msg;
|
|
52
175
|
try {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
176
|
+
msg = JSON.parse(trimmed);
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
continue; // ignore malformed messages
|
|
180
|
+
}
|
|
181
|
+
if (!msg || typeof msg !== 'object')
|
|
182
|
+
continue;
|
|
183
|
+
// A frame without a method answers one of our outbound requests.
|
|
184
|
+
if (!('method' in msg) && ('result' in msg || 'error' in msg)) {
|
|
185
|
+
const response = msg;
|
|
186
|
+
const pending = this.pendingRequests.get(response.id);
|
|
187
|
+
if (pending) {
|
|
188
|
+
this.pendingRequests.delete(response.id);
|
|
189
|
+
if (response.error) {
|
|
190
|
+
pending.reject(new AcpRequestError(pending.method, response.error.code, response.error.message));
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
pending.resolve(response.result ?? null);
|
|
63
194
|
}
|
|
64
195
|
}
|
|
65
|
-
|
|
196
|
+
// Nobody is waiting for it any more (timed out or cancelled). It is
|
|
197
|
+
// a response, so answering it would break JSON-RPC: drop it.
|
|
198
|
+
continue;
|
|
66
199
|
}
|
|
67
|
-
|
|
68
|
-
|
|
200
|
+
this.dispatch(msg);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
dispatch(msg) {
|
|
204
|
+
const id = 'id' in msg ? msg.id : undefined;
|
|
205
|
+
if (id !== undefined)
|
|
206
|
+
this.unanswered.add(id);
|
|
207
|
+
const fail = (err) => {
|
|
208
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
209
|
+
// stderr only: stdout carries nothing but JSON-RPC frames.
|
|
210
|
+
process.stderr.write(`[codeep-acp] ${msg.method} failed: ${message}\n`);
|
|
211
|
+
if (id !== undefined && this.unanswered.has(id)) {
|
|
212
|
+
this.error(id, -32603, message);
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
try {
|
|
216
|
+
const out = this.handler?.(msg);
|
|
217
|
+
if (out && typeof out.then === 'function') {
|
|
218
|
+
out.then(undefined, fail);
|
|
69
219
|
}
|
|
70
220
|
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
fail(err);
|
|
223
|
+
}
|
|
71
224
|
}
|
|
72
|
-
|
|
73
|
-
const line = JSON.stringify(msg);
|
|
225
|
+
write(line) {
|
|
74
226
|
debugLog('→', line);
|
|
75
227
|
process.stdout.write(line + '\n');
|
|
76
228
|
}
|
|
229
|
+
send(msg) {
|
|
230
|
+
if ('id' in msg)
|
|
231
|
+
this.unanswered.delete(msg.id);
|
|
232
|
+
this.write(JSON.stringify(msg));
|
|
233
|
+
}
|
|
77
234
|
respond(id, result) {
|
|
78
235
|
this.send({ jsonrpc: '2.0', id, result });
|
|
79
236
|
}
|
|
@@ -86,17 +243,53 @@ export class StdioTransport {
|
|
|
86
243
|
/**
|
|
87
244
|
* Send a JSON-RPC request to the client and wait for the response.
|
|
88
245
|
* Used for agent-initiated requests like session/request_permission.
|
|
246
|
+
*
|
|
247
|
+
* Rejects with AcpRequestError when the client answers with an error,
|
|
248
|
+
* with AcpRequestTimeoutError when it does not answer in time, and with
|
|
249
|
+
* an AbortError when `signal` fires. A result is never invented: callers
|
|
250
|
+
* that treat a missing answer as "no" must say so with their own catch.
|
|
89
251
|
*/
|
|
90
|
-
request(method, params) {
|
|
252
|
+
request(method, params, options = {}) {
|
|
91
253
|
const id = ++this.requestIdCounter;
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
254
|
+
const timeoutMs = options.timeoutMs ?? REQUEST_TIMEOUT_MS;
|
|
255
|
+
const { signal } = options;
|
|
256
|
+
return new Promise((resolve, reject) => {
|
|
257
|
+
if (signal?.aborted) {
|
|
258
|
+
reject(abortError(method));
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
let timer;
|
|
262
|
+
const settle = () => {
|
|
263
|
+
if (timer)
|
|
264
|
+
clearTimeout(timer);
|
|
265
|
+
signal?.removeEventListener('abort', onAbort);
|
|
266
|
+
};
|
|
267
|
+
const onAbort = () => {
|
|
96
268
|
if (this.pendingRequests.delete(id)) {
|
|
97
|
-
|
|
269
|
+
settle();
|
|
270
|
+
reject(abortError(method));
|
|
98
271
|
}
|
|
99
|
-
}
|
|
272
|
+
};
|
|
273
|
+
this.pendingRequests.set(id, {
|
|
274
|
+
method,
|
|
275
|
+
resolve: (result) => { settle(); resolve(result); },
|
|
276
|
+
reject: (err) => { settle(); reject(err); },
|
|
277
|
+
});
|
|
278
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
279
|
+
if (timeoutMs > 0) {
|
|
280
|
+
timer = setTimeout(() => {
|
|
281
|
+
if (this.pendingRequests.delete(id)) {
|
|
282
|
+
settle();
|
|
283
|
+
reject(new AcpRequestTimeoutError(method, timeoutMs));
|
|
284
|
+
}
|
|
285
|
+
}, timeoutMs);
|
|
286
|
+
}
|
|
287
|
+
this.write(JSON.stringify({ jsonrpc: '2.0', id, method, params }));
|
|
100
288
|
});
|
|
101
289
|
}
|
|
102
290
|
}
|
|
291
|
+
function abortError(method) {
|
|
292
|
+
const err = new Error(`${method} was cancelled`);
|
|
293
|
+
err.name = 'AbortError';
|
|
294
|
+
return err;
|
|
295
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recording a finished turn in an ACP thread's conversation.
|
|
3
|
+
*/
|
|
4
|
+
import { type Message } from '../config/index.js';
|
|
5
|
+
/** The part of a session a turn needs. */
|
|
6
|
+
export interface TurnSession {
|
|
7
|
+
workspaceRoot: string;
|
|
8
|
+
history: Message[];
|
|
9
|
+
codeepSessionId: string;
|
|
10
|
+
/** Bumped whenever the thread moves to another conversation. */
|
|
11
|
+
conversation?: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Start a turn: returns the function that records its messages once it ran.
|
|
15
|
+
* If the thread moved to another conversation meanwhile, the turn belongs to
|
|
16
|
+
* the one it started in — it is saved there, not appended to the new one —
|
|
17
|
+
* and after a /rewind of that same conversation it is dropped, since saving
|
|
18
|
+
* it would undo the rewind.
|
|
19
|
+
*/
|
|
20
|
+
export declare function beginTurn(session: TurnSession): (entries: Message[]) => void;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recording a finished turn in an ACP thread's conversation.
|
|
3
|
+
*/
|
|
4
|
+
import { config, saveSession } from '../config/index.js';
|
|
5
|
+
/**
|
|
6
|
+
* Start a turn: returns the function that records its messages once it ran.
|
|
7
|
+
* If the thread moved to another conversation meanwhile, the turn belongs to
|
|
8
|
+
* the one it started in — it is saved there, not appended to the new one —
|
|
9
|
+
* and after a /rewind of that same conversation it is dropped, since saving
|
|
10
|
+
* it would undo the rewind.
|
|
11
|
+
*/
|
|
12
|
+
export function beginTurn(session) {
|
|
13
|
+
const conversation = session.conversation ?? 0;
|
|
14
|
+
const id = session.codeepSessionId;
|
|
15
|
+
const history = session.history;
|
|
16
|
+
return (entries) => {
|
|
17
|
+
const autoSave = config.get('autoSave');
|
|
18
|
+
if ((session.conversation ?? 0) === conversation) {
|
|
19
|
+
session.history.push(...entries);
|
|
20
|
+
if (autoSave && session.history.length > 0)
|
|
21
|
+
saveSession(session.codeepSessionId, session.history, session.workspaceRoot);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (id === session.codeepSessionId)
|
|
25
|
+
return;
|
|
26
|
+
history.push(...entries);
|
|
27
|
+
if (autoSave)
|
|
28
|
+
saveSession(id, history, session.workspaceRoot);
|
|
29
|
+
};
|
|
30
|
+
}
|
package/dist/api/index.js
CHANGED
|
@@ -445,6 +445,8 @@ async function chatOpenAI(message, history, model, apiKey, onChunk, abortSignal)
|
|
|
445
445
|
temperature: omitTemperature ? undefined : temperature,
|
|
446
446
|
timeoutMs: timeout,
|
|
447
447
|
onChunk: stream ? onChunk : undefined,
|
|
448
|
+
// Carries the user's Stop as well as this request's own timeout.
|
|
449
|
+
signal: controller.signal,
|
|
448
450
|
});
|
|
449
451
|
if (result.promptTokens != null && result.completionTokens != null) {
|
|
450
452
|
recordTokenUsage({ promptTokens: result.promptTokens, completionTokens: result.completionTokens, totalTokens: result.promptTokens + result.completionTokens }, model, providerId);
|
|
@@ -94,6 +94,9 @@ export interface OllamaChatOptions {
|
|
|
94
94
|
keepAlive?: string;
|
|
95
95
|
temperature?: number;
|
|
96
96
|
timeoutMs?: number;
|
|
97
|
+
/** Stops the request when it fires (Stop / cancel). The promise then
|
|
98
|
+
* rejects with an error named 'AbortError', as fetch does. */
|
|
99
|
+
signal?: AbortSignal;
|
|
97
100
|
onChunk?: (text: string) => void;
|
|
98
101
|
/** Tool definitions in OpenAI function format. Ollama's /api/chat accepts the
|
|
99
102
|
* same `{type:'function',function:{...}}` shape and returns `tool_calls`. */
|
package/dist/api/ollamaNative.js
CHANGED
|
@@ -179,10 +179,40 @@ export function streamOllamaNativeChat(opts) {
|
|
|
179
179
|
...(Object.keys(options).length ? { options } : {}),
|
|
180
180
|
...(opts.keepAlive ? { keep_alive: opts.keepAlive } : {}),
|
|
181
181
|
});
|
|
182
|
-
return new Promise((
|
|
182
|
+
return new Promise((resolveChat, rejectChat) => {
|
|
183
|
+
const signal = opts.signal;
|
|
184
|
+
// Settles once: a request destroyed on abort goes on to emit errors of
|
|
185
|
+
// its own, which must not replace the abort.
|
|
186
|
+
let settled = false;
|
|
187
|
+
const settle = () => {
|
|
188
|
+
if (settled)
|
|
189
|
+
return false;
|
|
190
|
+
settled = true;
|
|
191
|
+
signal?.removeEventListener('abort', onAbort);
|
|
192
|
+
return true;
|
|
193
|
+
};
|
|
194
|
+
const resolve = (result) => { if (settle())
|
|
195
|
+
resolveChat(result); };
|
|
196
|
+
const reject = (error) => { if (settle())
|
|
197
|
+
rejectChat(error); };
|
|
198
|
+
const abortError = () => {
|
|
199
|
+
const error = new Error('The operation was aborted');
|
|
200
|
+
error.name = 'AbortError';
|
|
201
|
+
return error;
|
|
202
|
+
};
|
|
203
|
+
const onAbort = () => {
|
|
204
|
+
reject(abortError());
|
|
205
|
+
req?.destroy();
|
|
206
|
+
};
|
|
207
|
+
let req;
|
|
208
|
+
if (signal?.aborted) {
|
|
209
|
+
reject(abortError());
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
183
213
|
const u = new URL(url);
|
|
184
214
|
const lib = u.protocol === 'https:' ? https : http;
|
|
185
|
-
|
|
215
|
+
req = lib.request({
|
|
186
216
|
hostname: u.hostname,
|
|
187
217
|
port: u.port,
|
|
188
218
|
path: u.pathname + u.search,
|
|
@@ -200,6 +230,8 @@ export function streamOllamaNativeChat(opts) {
|
|
|
200
230
|
let acc = initialOllamaAccumulator();
|
|
201
231
|
const decoder = new TextDecoder();
|
|
202
232
|
res.on('data', (chunk) => {
|
|
233
|
+
if (settled)
|
|
234
|
+
return; // stopped: nothing more reaches the caller
|
|
203
235
|
buffer += decoder.decode(chunk, { stream: true });
|
|
204
236
|
const { lines, rest } = splitOllamaLines(buffer);
|
|
205
237
|
buffer = rest;
|
|
@@ -221,7 +253,7 @@ export function streamOllamaNativeChat(opts) {
|
|
|
221
253
|
res.on('error', reject);
|
|
222
254
|
});
|
|
223
255
|
req.on('error', reject);
|
|
224
|
-
req.on('timeout', () => { req
|
|
256
|
+
req.on('timeout', () => { req?.destroy(new Error('Ollama request timed out')); });
|
|
225
257
|
req.write(body);
|
|
226
258
|
req.end();
|
|
227
259
|
});
|