codeep 3.4.0 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/acp/commands.d.ts +15 -0
- package/dist/acp/commands.js +39 -5
- package/dist/acp/server.d.ts +13 -0
- package/dist/acp/server.js +283 -27
- package/dist/acp/serverHandlers.js +10 -10
- package/dist/acp/session.d.ts +13 -2
- package/dist/acp/transport.d.ts +6 -0
- package/dist/acp/transport.js +98 -3
- package/dist/api/index.js +6 -3
- package/dist/config/index.js +12 -4
- package/dist/config/providers.d.ts +48 -4
- package/dist/config/providers.js +325 -88
- package/dist/renderer/agentExecution.js +116 -69
- package/dist/renderer/commands.js +36 -11
- package/dist/renderer/main.d.ts +24 -0
- package/dist/renderer/main.js +57 -2
- package/dist/utils/agent.d.ts +33 -2
- package/dist/utils/agent.js +86 -8
- package/dist/utils/agentChat.js +22 -10
- package/dist/utils/checkpoints.js +3 -0
- package/dist/utils/codeReview.js +28 -23
- 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/headlessReview.js +26 -5
- package/dist/utils/personalities.js +8 -2
- package/dist/utils/shell.d.ts +108 -0
- package/dist/utils/shell.js +364 -5
- package/dist/utils/taskPlanner.js +12 -4
- package/dist/utils/telegramApproval.d.ts +10 -2
- package/dist/utils/telegramApproval.js +22 -4
- package/dist/utils/tokenTracker.d.ts +13 -5
- package/dist/utils/tokenTracker.js +163 -34
- package/dist/utils/toolExecution.d.ts +41 -0
- package/dist/utils/toolExecution.js +357 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/acp/transport.js
CHANGED
|
@@ -1,29 +1,124 @@
|
|
|
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
|
|
29
124
|
/** The client answered one of our requests with a JSON-RPC error. */
|
package/dist/api/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import * as https from 'node:https';
|
|
|
3
3
|
import { config, getApiKey, resolveBaseUrl, describeUnsendableKey } from '../config/index.js';
|
|
4
4
|
import { withRetry, isNetworkError } from '../utils/retry.js';
|
|
5
5
|
import { checkApiRateLimit } from '../utils/ratelimit.js';
|
|
6
|
-
import { getProvider, getProviderBaseUrl, getProviderAuthHeader, usesMaxCompletionTokens, requiresDefaultTemperature, modelRejectsSamplingParams, reasoningParamsFor } from '../config/providers.js';
|
|
6
|
+
import { getProvider, getProviderBaseUrl, getProviderAuthHeader, usesMaxCompletionTokens, requiresDefaultTemperature, modelRejectsSamplingParams, reasoningParamsFor, minResponseTokensFor } from '../config/providers.js';
|
|
7
7
|
import { logApiRequest, logApiResponse } from '../utils/logger.js';
|
|
8
8
|
import { loadProjectIntelligence, generateContextFromIntelligence } from '../utils/projectIntelligence.js';
|
|
9
9
|
import { loadProjectRules } from '../utils/agent.js';
|
|
@@ -366,7 +366,8 @@ async function chatOpenAI(message, history, model, apiKey, onChunk, abortSignal)
|
|
|
366
366
|
const stream = Boolean(onChunk);
|
|
367
367
|
const timeout = config.get('apiTimeout');
|
|
368
368
|
const temperature = config.get('temperature');
|
|
369
|
-
|
|
369
|
+
// Never below the model's floor (Opus 5.5 on OpenRouter — see minResponseTokensFor).
|
|
370
|
+
const maxTokens = Math.max(config.get('maxTokens'), minResponseTokensFor(model, config.get('reasoningEffort')));
|
|
370
371
|
// Get provider-specific URL and auth. resolveBaseUrl applies user
|
|
371
372
|
// overrides: Ollama (ollamaUrl), Custom (customBaseUrl), and OpenAI
|
|
372
373
|
// (OPENAI_BASE_URL env) — so self-hosted / OpenAI-compatible endpoints work.
|
|
@@ -647,7 +648,9 @@ async function chatAnthropic(message, history, model, apiKey, onChunk, abortSign
|
|
|
647
648
|
const stream = Boolean(onChunk);
|
|
648
649
|
const timeout = config.get('apiTimeout');
|
|
649
650
|
const temperature = config.get('temperature');
|
|
650
|
-
|
|
651
|
+
// Never below the model's floor: Opus 5.5's always-on thinking spends the
|
|
652
|
+
// same limit as the answer (see minResponseTokensFor).
|
|
653
|
+
const maxTokens = Math.max(config.get('maxTokens'), minResponseTokensFor(model, config.get('reasoningEffort')));
|
|
651
654
|
const baseUrl = getProviderBaseUrl(providerId, 'anthropic');
|
|
652
655
|
const authHeader = getProviderAuthHeader(providerId, 'anthropic');
|
|
653
656
|
if (!baseUrl) {
|
package/dist/config/index.js
CHANGED
|
@@ -280,10 +280,18 @@ if (currentMigrationVersion < 1) {
|
|
|
280
280
|
config.set('rateLimitCommands', 10000);
|
|
281
281
|
}
|
|
282
282
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
283
|
+
// Retired model ids: on EVERY load, not once. This sat inside
|
|
284
|
+
// `currentMigrationVersion < 4` until 2026-09-23, and MIGRATION_VERSION had
|
|
285
|
+
// been 4 since 2026-08-15 — so every RETIRED_MODEL_REPLACEMENTS entry added
|
|
286
|
+
// after that (the GPT-5.5/5.4, Grok, DeepSeek and Gemini ones) reached only
|
|
287
|
+
// fresh configs and profile loads, never an existing user's active model. The
|
|
288
|
+
// lookup is exact and idempotent, it touches only ids a vendor retired or
|
|
289
|
+
// rerouted (never OpenRouter/Ollama/custom ids, which the map does not key),
|
|
290
|
+
// and it is what applyProfile and the macOS app already do on every load. The
|
|
291
|
+
// one-shot rule above protects settings a user chooses; the ids in that map are
|
|
292
|
+
// ones the vendor retired or reroutes, or that cannot work from Codeep (GPT-6
|
|
293
|
+
// Astra's tool calls), so rewriting them again cannot undo a working choice.
|
|
294
|
+
{
|
|
287
295
|
const provider = config.get('provider');
|
|
288
296
|
const model = config.get('model');
|
|
289
297
|
const replacement = replacementModelFor(provider, model);
|
|
@@ -49,7 +49,14 @@ export interface ProviderConfig {
|
|
|
49
49
|
export declare const PROVIDERS: Record<string, ProviderConfig>;
|
|
50
50
|
export type ProviderId = keyof typeof PROVIDERS;
|
|
51
51
|
export declare function getProvider(id: string): ProviderConfig | null;
|
|
52
|
+
/**
|
|
53
|
+
* One exact lookup, never followed further: every target must already be a
|
|
54
|
+
* model its own provider offers (providers.test.ts holds the map to that), so a
|
|
55
|
+
* chain can never be needed and the macOS mirror stays a flat table too.
|
|
56
|
+
*/
|
|
52
57
|
export declare function replacementModelFor(providerId: string, modelId: string): string | undefined;
|
|
58
|
+
/** The whole map, read-only — for the invariant test and nothing else. */
|
|
59
|
+
export declare function retiredModelReplacements(): Readonly<Record<string, Readonly<Record<string, string>>>>;
|
|
53
60
|
export declare function getProviderList(): {
|
|
54
61
|
id: string;
|
|
55
62
|
name: string;
|
|
@@ -94,6 +101,21 @@ export declare function modelRejectsSamplingParams(model: string): boolean;
|
|
|
94
101
|
* Falls back to the requested value if no provider limit is set.
|
|
95
102
|
*/
|
|
96
103
|
export declare function getEffectiveMaxTokens(providerId: string, requested: number): number;
|
|
104
|
+
/**
|
|
105
|
+
* The smallest response budget (`max_tokens`) worth sending this model, or 0
|
|
106
|
+
* for no floor. Callers take the larger of this and whatever they would have
|
|
107
|
+
* sent, then apply getEffectiveMaxTokens as usual.
|
|
108
|
+
*
|
|
109
|
+
* Claude Opus 5.5 thinks on every request — adaptive thinking cannot be turned
|
|
110
|
+
* off — and "tends to think more per turn than Claude Opus 5" at the same
|
|
111
|
+
* effort; Anthropic's notes say to "leave room in max_tokens for the thinking",
|
|
112
|
+
* which spends the same limit as the answer. The task planner's 2048, or a
|
|
113
|
+
* maxTokens lowered in /settings, could go on thinking alone and cut the reply
|
|
114
|
+
* off. 32K is Codeep's own default; the Max tier gets 64K, where Anthropic's
|
|
115
|
+
* advice for max effort on Opus 5 is "starting at 64k tokens". Matched on the
|
|
116
|
+
* canonical id, so `anthropic/claude-opus-5.5` on OpenRouter is covered too.
|
|
117
|
+
*/
|
|
118
|
+
export declare function minResponseTokensFor(model: string, tier: ReasoningTier | undefined): number;
|
|
97
119
|
/**
|
|
98
120
|
* Unified, user-facing thinking-effort tiers (the `/thinking` setting).
|
|
99
121
|
*
|
|
@@ -102,7 +124,7 @@ export declare function getEffectiveMaxTokens(providerId: string, requested: num
|
|
|
102
124
|
*
|
|
103
125
|
* The four tiers are CONCEPTUAL. `reasoningParamsFor()` clamps each one to the
|
|
104
126
|
* nearest level the active provider+model actually accepts, so we never send a
|
|
105
|
-
* value that would 400 (e.g. Gemini
|
|
127
|
+
* value that would 400 (e.g. Gemini has no "max"; GPT-5.5 tops out at "xhigh").
|
|
106
128
|
* The control is a pure DEPTH knob on models that already think — it never
|
|
107
129
|
* toggles thinking on/off, which keeps us clear of the reasoning_content-replay
|
|
108
130
|
* contract that DeepSeek/GLM impose when thinking mode is flipped.
|
|
@@ -116,6 +138,21 @@ export declare const REASONING_TIERS: ReasoningTier[];
|
|
|
116
138
|
* `claude-opus-4.8` → `claude-opus-4-8`). Mirrors macOS `ModelTuning.canonicalModelID`.
|
|
117
139
|
*/
|
|
118
140
|
export declare function canonicalModelId(model: string): string;
|
|
141
|
+
/**
|
|
142
|
+
* GPT-6 Sol and Luna on Chat Completions support function calling "only with
|
|
143
|
+
* `reasoning_effort` set to `none`" (developers.openai.com models/gpt-6-sol,
|
|
144
|
+
* guides/latest-model). So a request that carries tools to them must send
|
|
145
|
+
* "none", whatever /thinking says. Direct `openai` only: OpenRouter may reach
|
|
146
|
+
* OpenAI through the Responses API, where the rule does not apply, and Astra
|
|
147
|
+
* rejects "none" with a 400 (it is not offered on `openai` at all).
|
|
148
|
+
*/
|
|
149
|
+
export declare function toolsForceReasoningOff(providerId: string, model: string): boolean;
|
|
150
|
+
/**
|
|
151
|
+
* What /thinking must tell the user when the tier does not reach every request
|
|
152
|
+
* for this model, or null. Without it the setting would look applied while
|
|
153
|
+
* agent turns quietly ran at "none".
|
|
154
|
+
*/
|
|
155
|
+
export declare function agentTurnReasoningNote(providerId: string, model: string): string | null;
|
|
119
156
|
/**
|
|
120
157
|
* Does this provider+model expose a GRADED thinking-effort control we can drive?
|
|
121
158
|
* Used to gate the `/thinking` UI — hidden entirely for models without one.
|
|
@@ -128,12 +165,19 @@ export declare function modelSupportsReasoningEffort(providerId: string, model:
|
|
|
128
165
|
* models, or providers without a graded knob — so callers can spread it
|
|
129
166
|
* unconditionally. Keep in lockstep with macOS `ModelTuning.reasoningParams`.
|
|
130
167
|
*/
|
|
131
|
-
export declare function reasoningParamsFor(providerId: string, model: string, tier: ReasoningTier
|
|
168
|
+
export declare function reasoningParamsFor(providerId: string, model: string, tier: ReasoningTier,
|
|
169
|
+
/** The request carries a non-empty `tools` array (native tool calling). */
|
|
170
|
+
opts?: {
|
|
171
|
+
tools?: boolean;
|
|
172
|
+
}): Record<string, unknown>;
|
|
132
173
|
/**
|
|
133
174
|
* The DISTINCT tiers a given provider+model actually exposes — used to build a
|
|
134
175
|
* per-model picker that only offers levels the model can tell apart (e.g.
|
|
135
|
-
* GLM-5.2
|
|
136
|
-
*
|
|
176
|
+
* GLM-5.2 grades only high|max; Gemini via the OpenAI-compat layer has no max).
|
|
177
|
+
* Always leads with 'auto'. `[]` for models with no graded knob.
|
|
178
|
+
*
|
|
179
|
+
* GPT-6 Sol/Luna list their full set: it is what they run on plain chat. Agent
|
|
180
|
+
* turns send "none" regardless (toolsForceReasoningOff), and /thinking says so.
|
|
137
181
|
*
|
|
138
182
|
* Kept in lockstep with `reasoningParamsFor` (the providers-test asserts every
|
|
139
183
|
* listed tier yields a DISTINCT param, so this can't silently drift). Mirrors
|