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
@@ -5,9 +5,10 @@
5
5
  * executeTool() dispatches to individual tool handlers.
6
6
  * listDirectory() and htmlToText() are private helpers.
7
7
  * createActionLog() converts a ToolCall+ToolResult into a history ActionLog.
8
+ * trustBearingWrite() names the writes that decide what runs later.
8
9
  */
9
10
  import { existsSync, readdirSync, statSync, lstatSync, readFileSync, writeFileSync, unlinkSync, mkdirSync, rmSync, realpathSync } from 'fs';
10
- import { join, dirname, relative, resolve, isAbsolute, sep } from 'path';
11
+ import { join, dirname, basename, relative, resolve, isAbsolute, sep } from 'path';
11
12
  import { executeCommandAsync } from './shell.js';
12
13
  import { recordWrite, recordEdit, recordDelete, recordMkdir, recordCommand, discardAction, recordResult } from './history.js';
13
14
  import { loadIgnoreRules, isIgnored } from './gitignore.js';
@@ -17,6 +18,7 @@ import { logger } from './logger.js';
17
18
  import { runHook } from './hooks.js';
18
19
  import { checkCommandRateLimit } from './ratelimit.js';
19
20
  import { isMcpToolName, callSessionTool, isVirtualMcpToolName, callSessionVirtualTool } from './mcpRegistry.js';
21
+ import { resolveHooksDirResult } from './gitHookInstaller.js';
20
22
  // SSRF guard (isBlockedIp / assertFetchUrlAllowed) moved to ./ssrfGuard —
21
23
  // shared with shell.ts for curl/wget URL checks. Re-exported here so the
22
24
  // existing tests that import it from toolExecution keep working.
@@ -79,6 +81,354 @@ export function validatePath(path, projectRoot) {
79
81
  }
80
82
  return { valid: true, absolutePath };
81
83
  }
84
+ // ── Files that decide what runs later ────────────────────────────────────────
85
+ //
86
+ // Writing one of these is not an edit, it is code execution on a delay:
87
+ //
88
+ // - `.git/` — git honours `core.fsmonitor`, `core.pager`, `diff.external`,
89
+ // `core.hooksPath`, `core.sshCommand` and `credential.helper` by RUNNING
90
+ // the command they name, so the next `git status` Codeep makes for the
91
+ // status line executes whatever `.git/config` says. Scripts in
92
+ // `.git/hooks/` run on the next commit. A worktree's `.git` is a file
93
+ // pointing at the real directory, so the whole name is off limits and not
94
+ // only what sits beneath it.
95
+ // - the repository's hook directory — `.git/hooks/` by default, but
96
+ // `core.hooksPath` moves it, and `.githooks/` and husky's `.husky/` are
97
+ // exactly that convention. A hook there runs on the user's own next
98
+ // `git commit` in their own terminal, long after the agent stopped.
99
+ // - `.codeep/hooks/` — scripts Codeep itself runs around every tool call.
100
+ // - `.codeep/skills/` — a skill's steps are commands Codeep runs when the
101
+ // skill is used (see skillBundles.ts, which loads project skills).
102
+ // - `.codeep/agents/` — a sub-agent definition. Its `tools:` is the
103
+ // allowlist the nested run is checked against and REPLACES the parent's,
104
+ // so it can hand a delegated run a tool this run was restricted from; its
105
+ // `model:` decides which provider the code in that run's context is sent
106
+ // to. Both take effect the next time anything delegates to that name.
107
+ // - `.codeep/mcp_servers.json`, `.mcp.json` — every entry is a command
108
+ // Codeep spawns.
109
+ // - `.codeep/config.json` — Codeep's own settings for this project.
110
+ //
111
+ // `.codeep/commands/` is deliberately NOT here, and must stay out: a custom
112
+ // command is prompt text, expanded into a message to the model, and the model's
113
+ // tool calls then go through every gate in this file. Gating it would put a
114
+ // confirmation in front of writing a prompt. That is also the line the agents
115
+ // directory falls on the other side of: an agent file is prompt text PLUS a
116
+ // tool allowlist and a model, which is the part a prompt cannot change.
117
+ //
118
+ // WHAT THIS DOES NOT COVER, so nobody reads the list above as a boundary:
119
+ // only the tools that take a `path` are gated. `execute_command` reaches the
120
+ // same files through any program that writes one — reviewers proved it with
121
+ // git 2.54 by having the agent write an ordinary `setup.cjs` (not a name on
122
+ // this list, so not gated) and then run `node setup.cjs`, which wrote
123
+ // `.git/config`; `cp`, `tee` and `mv` do it in one step and need no file
124
+ // written first. There is no fix for that here, because a gate that asked
125
+ // about every command able to write a file would be asking about every
126
+ // command.
127
+ //
128
+ // Nor is the classification a property of the file: it is decided at the
129
+ // moment of the write, from where the repository keeps its hooks AT THAT
130
+ // MOMENT. `write_file ci/deploy.sh` in a repository with the default hook
131
+ // directory is an ordinary file and goes through unasked; a later `git config
132
+ // core.hooksPath ci` makes that same file a live `pre-commit` without any
133
+ // further write for this gate to see. The cache below is dropped before every
134
+ // command line so the NEXT write is judged against the new hook directory,
135
+ // but nothing re-judges the writes that already happened — reclassifying them
136
+ // would mean keeping every path a run has written and re-running the gate
137
+ // after each command, and there is still nothing to do about a file already
138
+ // on disk. What remains covered is the write that installs the hook itself:
139
+ // `core.hooksPath` lives in `.git/config`, which this gate confirms.
140
+ //
141
+ // What it IS worth is the case it was built for: a model that writes a hook
142
+ // or a `.git/config` as part of an ordinary-looking edit, with no shell
143
+ // involved at all, which is what a prompt injection reaches for because
144
+ // execute_command is the tool users already watch. That write now stops for a
145
+ // confirmation in every mode. Someone who has approved a shell command has
146
+ // approved a shell command.
147
+ //
148
+ // The reasons are written for the person answering the confirmation prompt:
149
+ // "a config file" tells them nothing, "this decides what git runs" does.
150
+ const GIT_REASON = 'This file controls what commands git runs — core.fsmonitor, core.pager and diff.external are commands git executes for you.';
151
+ const GIT_HOOK_REASON = 'This is a git hook — git runs it on your next commit or push, in your own terminal.';
152
+ /** The same gate, for the repository that pointed `core.hooksPath` at its own
153
+ * root. Every top-level file matches there, so the reason may not say "this
154
+ * is a git hook": it would tell someone editing `package.json` that their
155
+ * package manifest is a hook. What is true of all of them is the directory
156
+ * they sit in, so that is what this says. */
157
+ const HOOKS_AT_ROOT_REASON = "This repository has named its own top level as its git hook directory (core.hooksPath), so git runs files " +
158
+ 'from here by name — writing one can install a hook that runs on your next commit.';
159
+ const CODEEP_HOOK_REASON = 'This file runs on every tool call.';
160
+ const SKILL_REASON = 'This is a skill — its steps are commands Codeep runs whenever the skill is used.';
161
+ const MCP_SERVERS_REASON = 'This file starts MCP servers — every entry is a command Codeep spawns.';
162
+ const AGENT_REASON = 'This file defines a sub-agent — the tools it may use and the model it runs on, every time something delegates to it.';
163
+ const CODEEP_CONFIG_REASON = "This file is Codeep's own configuration for this project.";
164
+ /** The one reason that is not a constant: it carries git's own refusal with
165
+ * it, because over ACP this prompt is the only place the user is ever told
166
+ * which key made Codeep refuse and how to clear it. */
167
+ const unknownHooksReason = (why) => 'Codeep could not ask git where this repository keeps its hooks, so it cannot tell whether this write installs ' +
168
+ `one that runs on your next commit. ${why}`;
169
+ /** Directory names that are a git hook directory by convention, so a repo
170
+ * using one is covered before git is asked anything. `.githooks` is the bare
171
+ * `core.hooksPath` convention and `.husky` is husky's. */
172
+ const HOOK_DIRECTORY_NAMES = new Set(['.githooks', '.husky']);
173
+ /** Tools whose `path` parameter names a file they create, change or remove. */
174
+ const PATH_WRITING_TOOLS = new Set(['write_file', 'edit_file', 'delete_file', 'create_directory']);
175
+ /**
176
+ * A path's segments as the filesystem will match them: lowercased, because
177
+ * macOS and Windows both hand `.GIT/config` to the same file git reads, and
178
+ * without the trailing dots and spaces Windows silently drops — `.git./config`
179
+ * and `.git /config` are two literal directories on POSIX but land in the real
180
+ * `.git` on Windows, which is the whole point of writing them that way. A
181
+ * segment that is nothing but dots or spaces keeps its own spelling, so `..`
182
+ * stays `..` instead of collapsing to nothing.
183
+ */
184
+ function pathSegments(path) {
185
+ return path
186
+ .split(/[\\/]+/)
187
+ .filter(s => s && s !== '.')
188
+ .map(s => (s.replace(/[. ]+$/, '') || s).toLowerCase());
189
+ }
190
+ /**
191
+ * The answer per project, for one run.
192
+ *
193
+ * The comment that used to sit here refused a cache, and it was right about
194
+ * the risk and wrong about the cost: `core.hooksPath` really can change
195
+ * mid-run without any write this gate sees — `execute_command` running
196
+ * `git config core.hooksPath .evil` needs no path-writing tool — but paying
197
+ * for that with a fresh resolution on EVERY path-writing call meant two git
198
+ * subprocesses per tool call. Measured over 100 ordinary writes in a real
199
+ * repository: 2484ms without the cache, 4.7ms with it (24.8ms → 0.05ms per
200
+ * call), all of it on the event loop and almost all of it on the common path
201
+ * where no name matches anything.
202
+ *
203
+ * So the answer is cached and thrown away the moment a command runs, which is
204
+ * the only in-run way the answer can change. Every caller that spawns a
205
+ * command line calls forgetHooksDirectory() first: the execute_command tool
206
+ * below, the ACP path that delegates that tool to the client's terminal (see
207
+ * agent.ts), and the two skill runners. `git config core.hooksPath .evil`
208
+ * followed by `write_file .evil/pre-commit` therefore still finds a cold
209
+ * cache, which is the case the old comment was protecting.
210
+ *
211
+ * Keyed by project root because one process serves several workspaces over
212
+ * ACP, and cleared whole rather than per root because a command line can `cd`
213
+ * into any of them.
214
+ */
215
+ const hooksDirectoryCache = new Map();
216
+ /** Drop the cached hook directories. Call before spawning a command line. */
217
+ export function forgetHooksDirectory() {
218
+ hooksDirectoryCache.clear();
219
+ }
220
+ /**
221
+ * Where this repository's hooks live, as segments relative to the project
222
+ * root — see HooksDirectory.
223
+ *
224
+ * Asking git is the only way to learn where a repo actually keeps them, and
225
+ * gitHookInstaller already does it with a hardened environment: every
226
+ * command-executing key is neutralised, `core.hooksPath` alone is left under
227
+ * the repo's control, and `git rev-parse` only prints a path. So the
228
+ * resolution cannot run anything the repository chose.
229
+ *
230
+ * resolveHooksDirResult() is the variant that does not throw, and the reason
231
+ * it exists: `none` and `unknown` used to be the same null, and reading a
232
+ * refusal as "no hook directory" switched this gate off in exactly the
233
+ * repositories that earned the refusal. It is still wrapped in a try/catch,
234
+ * and ANY throw counts as unknown — the fail-closed answer must not depend on
235
+ * a promise made by another module.
236
+ */
237
+ function hooksDirectorySegments(projectRoot, realRoot) {
238
+ const cached = hooksDirectoryCache.get(projectRoot);
239
+ if (cached !== undefined)
240
+ return cached;
241
+ const answer = resolveHooksDirectory(projectRoot, realRoot);
242
+ hooksDirectoryCache.set(projectRoot, answer);
243
+ return answer;
244
+ }
245
+ function resolveHooksDirectory(projectRoot, realRoot) {
246
+ let result;
247
+ try {
248
+ result = resolveHooksDirResult(projectRoot);
249
+ }
250
+ catch (error) {
251
+ return { kind: 'unknown', reason: error instanceof Error ? error.message : String(error) };
252
+ }
253
+ if (result.kind !== 'hooks')
254
+ return result;
255
+ // Both spellings of the root, as below: git may print the hooks path
256
+ // canonicalised (an absolute `core.hooksPath`, a symlinked checkout), and on
257
+ // macOS that is `/private/var/…` where the root arrived as `/var/…`.
258
+ const rel = insideRoot(projectRoot, result.dir) ?? insideRoot(realRoot, result.dir);
259
+ // `??` and not `||`: '' is the hook directory BEING the project root, which
260
+ // is a repository this has to cover, and pathSegments('') is the empty
261
+ // prefix that says so.
262
+ return rel === null ? { kind: 'none' } : { kind: 'segments', segments: pathSegments(rel) };
263
+ }
264
+ /**
265
+ * What a project-relative path controls, or null when it controls nothing.
266
+ *
267
+ * Matched on path segments rather than on a prefix, so a nested checkout's
268
+ * `vendor/lib/.git/config` is covered the same as the top-level one.
269
+ *
270
+ * `hooksDir` is the repository's own hook directory. It costs a `git
271
+ * rev-parse` the first time it is asked in a run, so it is asked for only once
272
+ * a name has failed to answer.
273
+ */
274
+ function reasonForSegments(relativePath, hooksDir) {
275
+ const segments = pathSegments(relativePath);
276
+ if (segments.includes('.git'))
277
+ return GIT_REASON;
278
+ if (segments.some(s => HOOK_DIRECTORY_NAMES.has(s)))
279
+ return GIT_HOOK_REASON;
280
+ const last = segments[segments.length - 1];
281
+ if (last === '.mcp.json')
282
+ return MCP_SERVERS_REASON;
283
+ for (let i = 0; i < segments.length - 1; i++) {
284
+ if (segments[i] !== '.codeep')
285
+ continue;
286
+ if (segments[i + 1] === 'hooks')
287
+ return CODEEP_HOOK_REASON;
288
+ if (segments[i + 1] === 'skills')
289
+ return SKILL_REASON;
290
+ if (segments[i + 1] === 'agents')
291
+ return AGENT_REASON;
292
+ if (i + 2 !== segments.length)
293
+ continue; // `.codeep/<file>` only, not deeper
294
+ if (segments[i + 1] === 'mcp_servers.json')
295
+ return MCP_SERVERS_REASON;
296
+ if (segments[i + 1] === 'config.json')
297
+ return CODEEP_CONFIG_REASON;
298
+ }
299
+ const hooks = hooksDir();
300
+ // Git could not be asked, so "this is not a hook" is not something anyone
301
+ // knows. A repository gets that answer when its own config names a program
302
+ // git would run, which is the last repository to hand a write through
303
+ // unasked — so every write in it is confirmed until the config is fixed.
304
+ // Noisy, and that is the trade: the alternative is a `core.hooksPath` this
305
+ // cannot see, pointed anywhere, written into silently.
306
+ if (hooks.kind === 'unknown')
307
+ return unknownHooksReason(hooks.reason);
308
+ if (hooks.kind === 'none')
309
+ return null;
310
+ // `every` over the empty prefix is trivially true, which is right: a repo
311
+ // whose `core.hooksPath` IS its root runs `<root>/pre-commit` on the next
312
+ // commit (verified with git 2.54), and that file was going unasked. What
313
+ // would NOT be right is reading that empty prefix as "every path in the
314
+ // project is a hook" — it would put a confirmation in front of every write
315
+ // in the repository. git looks for hooks DIRECTLY in its hook directory, so
316
+ // at the root that is the top level and nothing under it. A hook directory
317
+ // of its own keeps the whole subtree: a hook sources its helpers from
318
+ // beside it, the way husky's `pre-commit` sources `_/husky.sh`.
319
+ const prefix = hooks.segments;
320
+ if (prefix.every((s, i) => segments[i] === s) && (prefix.length > 0 || segments.length === 1)) {
321
+ // Which reason depends on which of those two shapes matched: a hook
322
+ // directory of its own means this file IS one, the root means only that
323
+ // this is the directory git looks in — and that one reports `package.json`
324
+ // along with everything else at the top level.
325
+ return prefix.length > 0 ? GIT_HOOK_REASON : HOOKS_AT_ROOT_REASON;
326
+ }
327
+ return null;
328
+ }
329
+ /** The path with a symlinked ancestor resolved, or null if it cannot be. */
330
+ function realPathThroughLinks(absolutePath) {
331
+ let existing = absolutePath;
332
+ const rest = [];
333
+ for (;;) {
334
+ try {
335
+ lstatSync(existing); // lstat, so a symlink counts as existing
336
+ break;
337
+ }
338
+ catch {
339
+ const parent = dirname(existing);
340
+ if (parent === existing)
341
+ return null;
342
+ rest.unshift(basename(existing));
343
+ existing = parent;
344
+ }
345
+ }
346
+ try {
347
+ return join(realpathSync(existing), ...rest);
348
+ }
349
+ catch {
350
+ return null;
351
+ }
352
+ }
353
+ /**
354
+ * The path relative to `root`, or null when it is not inside it.
355
+ *
356
+ * The empty string is a RESULT, not a miss: it means the path is the root
357
+ * itself. Folding it into null cost the hook gate a real repository — one
358
+ * with `core.hooksPath` set to its own root, where git runs `<root>/pre-commit`
359
+ * on the next commit and the relative path of the hook directory is ''.
360
+ */
361
+ function insideRoot(root, absolutePath) {
362
+ const rel = relative(root, absolutePath);
363
+ return !rel.startsWith('..') && !isAbsolute(rel) ? rel : null;
364
+ }
365
+ /**
366
+ * The tail of the refusal one of these writes gets when the run has nobody to
367
+ * ask — no permission callback at all, which is how `codeep review --fix`
368
+ * runs in CI. agent.ts builds the refusal; headlessReview.ts recognises it by
369
+ * this text so it can say so in the run output rather than leave it buried in
370
+ * the agent's tool log.
371
+ *
372
+ * One constant, in the module that owns the classification rather than in
373
+ * agent.ts, for two reasons: a reworded refusal that stopped matching would
374
+ * put CI back to failing silently, and agent.js is a module the fix-run tests
375
+ * replace wholesale — a constant read from there would have been undefined in
376
+ * exactly the test that guards this.
377
+ */
378
+ export const NO_CONFIRMER_REFUSAL = 'Nobody could be asked to confirm it, so nothing was written.';
379
+ /**
380
+ * What a tool call would write that decides what runs later, or null.
381
+ *
382
+ * Callers use this for two things: to force a confirmation the mode would
383
+ * otherwise skip (see agent.ts), and to tell the person answering it what
384
+ * they are approving.
385
+ *
386
+ * A symlink inside the project can point at one of these names — `ln -s .git
387
+ * tools/cfg` makes a write to `tools/cfg/config` land in the real `.git`, and
388
+ * validatePath allows it because it never leaves the project — so the
389
+ * resolved path is classified alongside the one the model asked for.
390
+ */
391
+ export function trustBearingWrite(toolCall, projectRoot) {
392
+ if (!PATH_WRITING_TOOLS.has(normalizeToolName(toolCall.tool)))
393
+ return null;
394
+ const path = toolCall.parameters?.path;
395
+ if (typeof path !== 'string' || !path)
396
+ return null;
397
+ const absolute = isAbsolute(path) ? resolve(path) : resolve(projectRoot, path);
398
+ let realRoot = projectRoot;
399
+ try {
400
+ realRoot = realpathSync(projectRoot);
401
+ }
402
+ catch {
403
+ // Unreadable root: the given path is all there is to go on.
404
+ }
405
+ const candidates = [absolute];
406
+ const resolved = realPathThroughLinks(absolute);
407
+ if (resolved && resolved !== absolute)
408
+ candidates.push(resolved);
409
+ // Looked up at most once per call, and only if a candidate gets far enough
410
+ // to need it. The cache behind it spans the run; this spans the call, so
411
+ // the two candidates below share one lookup.
412
+ let hooksDir;
413
+ const hooksDirOnce = () => {
414
+ if (hooksDir === undefined)
415
+ hooksDir = hooksDirectorySegments(projectRoot, realRoot);
416
+ return hooksDir;
417
+ };
418
+ for (const candidate of candidates) {
419
+ // Only the part below the project root is classified. The root's own
420
+ // path is not the agent's doing, and on macOS it usually arrives
421
+ // unresolved (/var/folders/… for /private/var/folders/…), so both
422
+ // spellings get a chance to match.
423
+ const rel = insideRoot(projectRoot, candidate) ?? insideRoot(realRoot, candidate);
424
+ const reason = rel && reasonForSegments(rel, hooksDirOnce);
425
+ // The resolved candidate is the stable name for this file, so it keys the
426
+ // answer when there is one.
427
+ if (reason)
428
+ return { path, file: resolved ?? candidate, reason };
429
+ }
430
+ return null;
431
+ }
82
432
  /**
83
433
  * List directory contents, respecting .gitignore rules.
84
434
  * Tracks visited inodes to prevent infinite loops caused by circular symlinks.
@@ -530,6 +880,12 @@ async function dispatchTool(tool, parameters, projectRoot, fs, toolCall, signal)
530
880
  const args = parameters.args || [];
531
881
  if (!command)
532
882
  return { success: false, output: '', error: 'Missing required parameter: command', tool, parameters };
883
+ // A command is the one thing in a run that can move this repository's
884
+ // hooks — `git config core.hooksPath .evil` — without any write the
885
+ // gate above sees. Drop the cached answer before it runs, so the next
886
+ // `write_file .evil/pre-commit` asks git again rather than trusting
887
+ // where the hooks were a moment ago.
888
+ forgetHooksDirectory();
533
889
  // Command throttle — guards against agent loops that spawn commands
534
890
  // every iteration (each can be up to 2 minutes of subprocess time).
535
891
  // Rate-limited *after* permission resolution: an allowed command
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "3.4.0";
1
+ export declare const VERSION = "3.5.0";
package/dist/version.js CHANGED
@@ -1,4 +1,4 @@
1
1
  // AUTO-GENERATED by scripts/gen-version.js — do not edit by hand.
2
2
  // Baked from package.json at build time so the bun-compiled binary reports
3
3
  // the right version (it has no package.json on disk to read at runtime).
4
- export const VERSION = '3.4.0';
4
+ export const VERSION = '3.5.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeep",
3
- "version": "3.4.0",
3
+ "version": "3.5.0",
4
4
  "description": "AI-powered coding assistant built for the terminal. Multiple LLM providers, project-aware context, and a seamless development workflow.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",