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.
Files changed (39) hide show
  1. package/dist/acp/commands.d.ts +15 -0
  2. package/dist/acp/commands.js +39 -5
  3. package/dist/acp/server.d.ts +13 -0
  4. package/dist/acp/server.js +283 -27
  5. package/dist/acp/serverHandlers.js +10 -10
  6. package/dist/acp/session.d.ts +13 -2
  7. package/dist/acp/transport.d.ts +6 -0
  8. package/dist/acp/transport.js +98 -3
  9. package/dist/api/index.js +6 -3
  10. package/dist/config/index.js +12 -4
  11. package/dist/config/providers.d.ts +48 -4
  12. package/dist/config/providers.js +325 -88
  13. package/dist/renderer/agentExecution.js +116 -69
  14. package/dist/renderer/commands.js +36 -11
  15. package/dist/renderer/main.d.ts +24 -0
  16. package/dist/renderer/main.js +57 -2
  17. package/dist/utils/agent.d.ts +33 -2
  18. package/dist/utils/agent.js +86 -8
  19. package/dist/utils/agentChat.js +22 -10
  20. package/dist/utils/checkpoints.js +3 -0
  21. package/dist/utils/codeReview.js +28 -23
  22. package/dist/utils/git.d.ts +262 -4
  23. package/dist/utils/git.js +1928 -61
  24. package/dist/utils/gitHookInstaller.d.ts +32 -1
  25. package/dist/utils/gitHookInstaller.js +76 -8
  26. package/dist/utils/headlessReview.js +26 -5
  27. package/dist/utils/personalities.js +8 -2
  28. package/dist/utils/shell.d.ts +108 -0
  29. package/dist/utils/shell.js +364 -5
  30. package/dist/utils/taskPlanner.js +12 -4
  31. package/dist/utils/telegramApproval.d.ts +10 -2
  32. package/dist/utils/telegramApproval.js +22 -4
  33. package/dist/utils/tokenTracker.d.ts +13 -5
  34. package/dist/utils/tokenTracker.js +163 -34
  35. package/dist/utils/toolExecution.d.ts +41 -0
  36. package/dist/utils/toolExecution.js +357 -1
  37. package/dist/version.d.ts +1 -1
  38. package/dist/version.js +1 -1
  39. package/package.json +1 -1
@@ -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
- appendFileSync(ACP_DEBUG_PATH, `${new Date().toISOString()} [ACP${direction}client] ${payload}\n`);
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
- const maxTokens = config.get('maxTokens');
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
- const maxTokens = config.get('maxTokens');
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) {
@@ -280,10 +280,18 @@ if (currentMigrationVersion < 1) {
280
280
  config.set('rateLimitCommands', 10000);
281
281
  }
282
282
  }
283
- if (currentMigrationVersion < 4) {
284
- // Vendor aliases below were removed from Codeep's curated catalogue after
285
- // their replacements became available. Migrate only exact known aliases:
286
- // OpenRouter/Ollama/custom model ids remain user-controlled.
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 rejects "medium"; OpenAI has no "max").
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): Record<string, unknown>;
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/DeepSeek grade only high|max; Gemini via the OpenAI-compat layer only
136
- * low|high). Always leads with 'auto'. `[]` for models with no graded knob.
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