codeep 2.4.1 → 2.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/README.md +1 -0
- package/dist/acp/session.js +3 -1
- package/dist/renderer/agentExecution.js +8 -0
- package/dist/renderer/main.js +45 -0
- package/dist/utils/agent.d.ts +17 -0
- package/dist/utils/agent.js +28 -28
- package/dist/utils/codeepCloud.js +6 -3
- package/dist/utils/diffPreview.js +3 -1
- package/dist/utils/headlessReview.d.ts +24 -0
- package/dist/utils/headlessReview.js +91 -0
- package/dist/utils/skills.js +5 -1
- package/dist/utils/toolExecution.js +4 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1050,6 +1050,7 @@ In `dangerous` mode, configure which tools require confirmation via `/settings`:
|
|
|
1050
1050
|
| `codeep account` | Link CLI to codeep.dev (GitHub OAuth) |
|
|
1051
1051
|
| `codeep account sync` | Pull API keys + personalities + commands from codeep.dev → local |
|
|
1052
1052
|
| `codeep account push` | Push local API keys + personalities + commands → codeep.dev |
|
|
1053
|
+
| `codeep review [files…]` | Offline, deterministic code review for CI — markdown or `--json`, `--fail-on <error\|warning\|info\|none>` sets the exit code. No API key needed. |
|
|
1053
1054
|
|
|
1054
1055
|
### Authentication
|
|
1055
1056
|
|
package/dist/acp/session.js
CHANGED
|
@@ -154,7 +154,9 @@ export async function runAgentSession(opts) {
|
|
|
154
154
|
});
|
|
155
155
|
// result.finalResponse is already emitted via onChunk streaming above;
|
|
156
156
|
// only emit it here if nothing was streamed (e.g. non-streaming fallback path)
|
|
157
|
-
|
|
157
|
+
// — except a paused/interrupted run, whose finalResponse is a fresh "say
|
|
158
|
+
// continue" notice that was never streamed and must always reach the client.
|
|
159
|
+
if (result.finalResponse && (chunksEmitted === 0 || result.interrupted)) {
|
|
158
160
|
opts.onChunk(result.finalResponse);
|
|
159
161
|
}
|
|
160
162
|
// Surface errors as thrown exceptions so the ACP server can handle them correctly.
|
|
@@ -343,6 +343,14 @@ export async function executeAgentTask(task, dryRun, ctx) {
|
|
|
343
343
|
else if (result.aborted) {
|
|
344
344
|
app.addMessage({ role: 'assistant', content: 'Agent stopped by user.' });
|
|
345
345
|
}
|
|
346
|
+
else if (result.interrupted) {
|
|
347
|
+
// Paused at a step/time safety limit — resumable, not a failure. Show the
|
|
348
|
+
// agent's partial summary and nudge the user to resume.
|
|
349
|
+
if (result.finalResponse) {
|
|
350
|
+
app.addMessage({ role: 'assistant', content: result.finalResponse });
|
|
351
|
+
}
|
|
352
|
+
app.notify('Paused at the safety limit — say "continue" to keep going');
|
|
353
|
+
}
|
|
346
354
|
else {
|
|
347
355
|
// Show the agent's summary if available, with error details below
|
|
348
356
|
if (result.finalResponse) {
|
package/dist/renderer/main.js
CHANGED
|
@@ -375,6 +375,13 @@ function showSessionPickerInline() {
|
|
|
375
375
|
// ─── Main ─────────────────────────────────────────────────────────────────────
|
|
376
376
|
async function main() {
|
|
377
377
|
const args = process.argv.slice(2);
|
|
378
|
+
// Headless, deterministic code review for CI (no API key, no TUI). Handled
|
|
379
|
+
// before the global --help/--version checks so `codeep review --help` shows
|
|
380
|
+
// the review usage rather than the top-level help.
|
|
381
|
+
if (args[0] === 'review') {
|
|
382
|
+
const { runHeadlessReview } = await import('../utils/headlessReview.js');
|
|
383
|
+
process.exit(runHeadlessReview(args.slice(1)));
|
|
384
|
+
}
|
|
378
385
|
if (args.includes('--version') || args.includes('-v')) {
|
|
379
386
|
console.log(`Codeep v${getCurrentVersion()}`);
|
|
380
387
|
process.exit(0);
|
|
@@ -389,6 +396,7 @@ Usage:
|
|
|
389
396
|
codeep account sync Pull keys + personalities + commands + profile from codeep.dev
|
|
390
397
|
codeep account push Push local keys + personalities + commands + profile to codeep.dev
|
|
391
398
|
codeep acp Start ACP server (for Zed editor integration)
|
|
399
|
+
codeep review Offline code review for CI (--json, --fail-on <level>)
|
|
392
400
|
codeep --version Show version
|
|
393
401
|
codeep --help Show this help
|
|
394
402
|
|
|
@@ -825,6 +833,43 @@ async function gracefulShutdown() {
|
|
|
825
833
|
}),
|
|
826
834
|
]);
|
|
827
835
|
}
|
|
836
|
+
// ─── Last-resort crash handlers ───────────────────────────────────────────────
|
|
837
|
+
// Without these, a stray throw or rejected promise (deep in the agent loop or a
|
|
838
|
+
// background cloud sync) crashes Node with the terminal still in raw mode +
|
|
839
|
+
// alternate screen — leaving the user's shell garbled — or vanishes silently.
|
|
840
|
+
process.on('uncaughtException', (error) => {
|
|
841
|
+
logAppError(error instanceof Error ? error : new Error(String(error)), 'uncaughtException');
|
|
842
|
+
// After an uncaught exception the process state is undefined; Node's guidance
|
|
843
|
+
// is to clean up synchronously and exit rather than limp on. Restore the
|
|
844
|
+
// terminal and best-effort save the conversation so the crash doesn't lose it.
|
|
845
|
+
try {
|
|
846
|
+
if (app)
|
|
847
|
+
app.stop();
|
|
848
|
+
}
|
|
849
|
+
catch { /* ignore */ }
|
|
850
|
+
try {
|
|
851
|
+
process.stdout.write('\x1b[2J\x1b[3J\x1b[H');
|
|
852
|
+
}
|
|
853
|
+
catch { /* ignore */ }
|
|
854
|
+
console.error('Fatal error:', error);
|
|
855
|
+
try {
|
|
856
|
+
if (app)
|
|
857
|
+
autoSaveSession(app.getMessages(), projectPath);
|
|
858
|
+
}
|
|
859
|
+
catch { /* ignore */ }
|
|
860
|
+
process.exit(1);
|
|
861
|
+
});
|
|
862
|
+
process.on('unhandledRejection', (reason) => {
|
|
863
|
+
logAppError(reason instanceof Error ? reason : new Error(String(reason)), 'unhandledRejection');
|
|
864
|
+
// A rejected promise is usually recoverable (failed background sync, network
|
|
865
|
+
// blip), so surface it and keep the TUI alive instead of tearing it down.
|
|
866
|
+
// Fall back to stderr if the app isn't up yet.
|
|
867
|
+
const message = reason instanceof Error ? reason.message : String(reason);
|
|
868
|
+
if (app)
|
|
869
|
+
app.notifyWarn(`Background error: ${message}`);
|
|
870
|
+
else
|
|
871
|
+
console.error('Unhandled rejection:', reason);
|
|
872
|
+
});
|
|
828
873
|
process.on('SIGINT', () => {
|
|
829
874
|
gracefulShutdown().finally(() => process.exit(0));
|
|
830
875
|
});
|
package/dist/utils/agent.d.ts
CHANGED
|
@@ -71,6 +71,8 @@ export interface AgentOptions {
|
|
|
71
71
|
/** Role system-prompt addendum injected for a delegated sub-agent. */
|
|
72
72
|
roleAddendum?: string;
|
|
73
73
|
}
|
|
74
|
+
/** Why a run stopped early at a safety limit — both are resumable, not errors. */
|
|
75
|
+
export type InterruptKind = 'iteration_limit' | 'time_limit';
|
|
74
76
|
export interface AgentResult {
|
|
75
77
|
success: boolean;
|
|
76
78
|
iterations: number;
|
|
@@ -78,7 +80,22 @@ export interface AgentResult {
|
|
|
78
80
|
finalResponse: string;
|
|
79
81
|
error?: string;
|
|
80
82
|
aborted?: boolean;
|
|
83
|
+
/** Set when the run paused at a step/time safety limit. The caller can offer
|
|
84
|
+
* a "continue" affordance instead of treating it as a failure. */
|
|
85
|
+
interrupted?: InterruptKind;
|
|
81
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* Build the result for a run that paused at a safety limit. Pausing is a normal,
|
|
89
|
+
* resumable state — not an error — so the summary tells the user how to resume.
|
|
90
|
+
* Shared by both limit checks in the loop so the wording + `interrupted` signal
|
|
91
|
+
* stay in sync.
|
|
92
|
+
*/
|
|
93
|
+
export declare function buildPausedResult(kind: InterruptKind, ctx: {
|
|
94
|
+
iterations: number;
|
|
95
|
+
actions: ActionLog[];
|
|
96
|
+
maxIterations?: number;
|
|
97
|
+
durationMin?: number;
|
|
98
|
+
}): AgentResult;
|
|
82
99
|
/**
|
|
83
100
|
* Run the agent loop
|
|
84
101
|
*/
|
package/dist/utils/agent.js
CHANGED
|
@@ -103,6 +103,32 @@ function compressMessages(messages, actions) {
|
|
|
103
103
|
debug(`Context compressed: ${totalChars} chars → keeping first + summary + last ${keep} messages`);
|
|
104
104
|
return [firstMessage, summaryMessage, ...recentMessages];
|
|
105
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Build the result for a run that paused at a safety limit. Pausing is a normal,
|
|
108
|
+
* resumable state — not an error — so the summary tells the user how to resume.
|
|
109
|
+
* Shared by both limit checks in the loop so the wording + `interrupted` signal
|
|
110
|
+
* stay in sync.
|
|
111
|
+
*/
|
|
112
|
+
export function buildPausedResult(kind, ctx) {
|
|
113
|
+
const editedFiles = [...new Set(ctx.actions.filter(a => a.type === 'write' || a.type === 'edit').map(a => a.target))];
|
|
114
|
+
const head = kind === 'time_limit'
|
|
115
|
+
? `⏸ Paused after the ${ctx.durationMin}-minute time limit.`
|
|
116
|
+
: `⏸ Paused after ${ctx.maxIterations} tool steps (the safety limit).`;
|
|
117
|
+
const lines = [head, '', 'This is a safety limit, not an error — say **continue** to pick up where it left off.'];
|
|
118
|
+
if (editedFiles.length > 0) {
|
|
119
|
+
lines.push('', '**Progress so far — files written/edited:**', ...editedFiles.map(f => ` ✓ \`${f}\``));
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
success: false,
|
|
123
|
+
iterations: ctx.iterations,
|
|
124
|
+
actions: ctx.actions,
|
|
125
|
+
finalResponse: lines.join('\n'),
|
|
126
|
+
error: kind === 'time_limit'
|
|
127
|
+
? `Exceeded maximum duration of ${ctx.durationMin} min`
|
|
128
|
+
: `Exceeded maximum of ${ctx.maxIterations} iterations`,
|
|
129
|
+
interrupted: kind,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
106
132
|
const DEFAULT_OPTIONS = {
|
|
107
133
|
// Modern models (GLM-5.1, Claude 4.5, GPT-4.1) complete typical coding tasks in
|
|
108
134
|
// 3–8 iterations. The old cap of 100 mostly let broken loops wander for minutes
|
|
@@ -452,21 +478,8 @@ export async function runAgent(prompt, projectContext, options = {}) {
|
|
|
452
478
|
while (iteration < opts.maxIterations) {
|
|
453
479
|
// Check timeout
|
|
454
480
|
if (Date.now() - startTime > opts.maxDuration) {
|
|
455
|
-
const filesDone = actions.filter(a => a.type === 'write' || a.type === 'edit').map(a => a.target);
|
|
456
481
|
const durationMin = Math.round(opts.maxDuration / 60000);
|
|
457
|
-
|
|
458
|
-
if (filesDone.length > 0) {
|
|
459
|
-
partialLines.push(`\n**Partial progress — files written/edited:**`);
|
|
460
|
-
[...new Set(filesDone)].forEach(f => partialLines.push(` ✓ \`${f}\``));
|
|
461
|
-
partialLines.push(`\nYou can continue by running the agent again.`);
|
|
462
|
-
}
|
|
463
|
-
result = {
|
|
464
|
-
success: false,
|
|
465
|
-
iterations: iteration,
|
|
466
|
-
actions,
|
|
467
|
-
finalResponse: partialLines.join('\n'),
|
|
468
|
-
error: `Exceeded maximum duration of ${durationMin} min`,
|
|
469
|
-
};
|
|
482
|
+
result = buildPausedResult('time_limit', { iterations: iteration, actions, durationMin });
|
|
470
483
|
if (!opts.nested)
|
|
471
484
|
writeProgressLog(projectContext.root || '', prompt, result, projectContext.name);
|
|
472
485
|
return result;
|
|
@@ -894,20 +907,7 @@ export async function runAgent(prompt, projectContext, options = {}) {
|
|
|
894
907
|
}
|
|
895
908
|
// Check if we hit max iterations — build partial summary from actions log
|
|
896
909
|
if (iteration >= opts.maxIterations && !finalResponse) {
|
|
897
|
-
|
|
898
|
-
const partialLines = [`Agent reached the iteration limit (${opts.maxIterations} steps).`];
|
|
899
|
-
if (filesDone.length > 0) {
|
|
900
|
-
partialLines.push(`\n**Partial progress — files written/edited:**`);
|
|
901
|
-
[...new Set(filesDone)].forEach(f => partialLines.push(` ✓ \`${f}\``));
|
|
902
|
-
partialLines.push(`\nThe task may be incomplete. You can continue by running the agent again.`);
|
|
903
|
-
}
|
|
904
|
-
result = {
|
|
905
|
-
success: false,
|
|
906
|
-
iterations: iteration,
|
|
907
|
-
actions,
|
|
908
|
-
finalResponse: partialLines.join('\n'),
|
|
909
|
-
error: `Exceeded maximum of ${opts.maxIterations} iterations`,
|
|
910
|
-
};
|
|
910
|
+
result = buildPausedResult('iteration_limit', { iterations: iteration, actions, maxIterations: opts.maxIterations });
|
|
911
911
|
if (!opts.nested)
|
|
912
912
|
writeProgressLog(projectContext.root || '', prompt, result, projectContext.name);
|
|
913
913
|
return result;
|
|
@@ -68,10 +68,13 @@ export async function runAccountFlow() {
|
|
|
68
68
|
if (!res.ok)
|
|
69
69
|
continue;
|
|
70
70
|
const data = await res.json();
|
|
71
|
-
|
|
71
|
+
// Require the sync token, not just github_id: the server can briefly
|
|
72
|
+
// report authorized after claiming the code but before the token is
|
|
73
|
+
// issued. Linking without it leaves `account sync` broken ("Not linked"),
|
|
74
|
+
// so keep polling until the token is present.
|
|
75
|
+
if (data.status === 'authorized' && data.github_id && data.sync_token) {
|
|
72
76
|
setGithubAccount(data.github_id, data.username ?? '');
|
|
73
|
-
|
|
74
|
-
setSyncToken(data.sync_token);
|
|
77
|
+
setSyncToken(data.sync_token);
|
|
75
78
|
// Register device info
|
|
76
79
|
try {
|
|
77
80
|
await fetch(`${API_BASE}/api/auth/cli/device`, {
|
|
@@ -283,7 +283,9 @@ export function createEditDiff(path, oldText, newText, projectRoot) {
|
|
|
283
283
|
if (!content.includes(oldText)) {
|
|
284
284
|
return null;
|
|
285
285
|
}
|
|
286
|
-
|
|
286
|
+
// Literal replacement ($-safe): a plain-string replace would interpret
|
|
287
|
+
// $&, $1, $$ in newText and mis-render the diff.
|
|
288
|
+
const newContent = content.replace(oldText, () => newText);
|
|
287
289
|
const hunks = generateDiff(content, newContent);
|
|
288
290
|
return {
|
|
289
291
|
path,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ReviewResult } from './codeReview.js';
|
|
2
|
+
export type FailOn = 'error' | 'warning' | 'info' | 'none';
|
|
3
|
+
export interface ReviewArgs {
|
|
4
|
+
files: string[];
|
|
5
|
+
json: boolean;
|
|
6
|
+
failOn: FailOn;
|
|
7
|
+
help: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare const REVIEW_HELP = "Usage: codeep review [options] [files...]\n\nRun a deterministic, offline code review (no API key required). With no files,\nreviews your unstaged git changes, falling back to a src/ scan when the tree is\nclean. Pass files (or let your CI pass the PR's changed files) to scope it.\n\nOptions:\n --json Print the result as JSON instead of the markdown report\n --fail-on <level> Exit non-zero when an issue at or above <level> is found:\n error | warning | info | none (default: error)\n -h, --help Show this help\n\nExit code: 0 when nothing at/above --fail-on is found, 1 otherwise.";
|
|
10
|
+
/** Parse `codeep review` argv (everything after the subcommand). Pure. */
|
|
11
|
+
export declare function parseReviewArgs(argv: string[]): ReviewArgs;
|
|
12
|
+
/** Exit code for a result under a fail-on threshold. Pure. */
|
|
13
|
+
export declare function exitCodeForResult(result: ReviewResult, failOn: FailOn): number;
|
|
14
|
+
export interface ReviewDeps {
|
|
15
|
+
/** Run the review over optional specific files. */
|
|
16
|
+
review: (files?: string[]) => ReviewResult;
|
|
17
|
+
/** Sink for the report (one call). */
|
|
18
|
+
write: (text: string) => void;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Orchestrate a headless review and return the process exit code. Side effects
|
|
22
|
+
* (filesystem, stdout) live behind `deps` so the flow is unit-testable.
|
|
23
|
+
*/
|
|
24
|
+
export declare function runHeadlessReview(argv: string[], deps?: ReviewDeps): number;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// Headless `codeep review` — a non-interactive entry point around the
|
|
2
|
+
// deterministic reviewer in codeReview.ts. No API key, no TUI: it scans, prints
|
|
3
|
+
// a report (markdown or JSON), and exits non-zero when issues at/above a chosen
|
|
4
|
+
// severity are found, so it drops cleanly into CI (e.g. a GitHub Action).
|
|
5
|
+
import { performCodeReview, formatReviewResult } from './codeReview.js';
|
|
6
|
+
const FAIL_ON_VALUES = ['error', 'warning', 'info', 'none'];
|
|
7
|
+
// Higher = more severe. `suggestion` sits below `info` so `--fail-on info`
|
|
8
|
+
// never trips on a mere suggestion.
|
|
9
|
+
const SEVERITY_RANK = { suggestion: 0, info: 1, warning: 2, error: 3 };
|
|
10
|
+
export const REVIEW_HELP = `Usage: codeep review [options] [files...]
|
|
11
|
+
|
|
12
|
+
Run a deterministic, offline code review (no API key required). With no files,
|
|
13
|
+
reviews your unstaged git changes, falling back to a src/ scan when the tree is
|
|
14
|
+
clean. Pass files (or let your CI pass the PR's changed files) to scope it.
|
|
15
|
+
|
|
16
|
+
Options:
|
|
17
|
+
--json Print the result as JSON instead of the markdown report
|
|
18
|
+
--fail-on <level> Exit non-zero when an issue at or above <level> is found:
|
|
19
|
+
error | warning | info | none (default: error)
|
|
20
|
+
-h, --help Show this help
|
|
21
|
+
|
|
22
|
+
Exit code: 0 when nothing at/above --fail-on is found, 1 otherwise.`;
|
|
23
|
+
/** Parse `codeep review` argv (everything after the subcommand). Pure. */
|
|
24
|
+
export function parseReviewArgs(argv) {
|
|
25
|
+
const out = { files: [], json: false, failOn: 'error', help: false };
|
|
26
|
+
for (let i = 0; i < argv.length; i++) {
|
|
27
|
+
const arg = argv[i];
|
|
28
|
+
if (arg === '--json') {
|
|
29
|
+
out.json = true;
|
|
30
|
+
}
|
|
31
|
+
else if (arg === '-h' || arg === '--help') {
|
|
32
|
+
out.help = true;
|
|
33
|
+
}
|
|
34
|
+
else if (arg === '--fail-on') {
|
|
35
|
+
const v = argv[++i];
|
|
36
|
+
if (FAIL_ON_VALUES.includes(v))
|
|
37
|
+
out.failOn = v;
|
|
38
|
+
}
|
|
39
|
+
else if (arg.startsWith('--fail-on=')) {
|
|
40
|
+
const v = arg.slice('--fail-on='.length);
|
|
41
|
+
if (FAIL_ON_VALUES.includes(v))
|
|
42
|
+
out.failOn = v;
|
|
43
|
+
}
|
|
44
|
+
else if (!arg.startsWith('-')) {
|
|
45
|
+
out.files.push(arg);
|
|
46
|
+
}
|
|
47
|
+
// Unknown flags are ignored so a future flag doesn't hard-fail old clients.
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
/** Exit code for a result under a fail-on threshold. Pure. */
|
|
52
|
+
export function exitCodeForResult(result, failOn) {
|
|
53
|
+
if (failOn === 'none')
|
|
54
|
+
return 0;
|
|
55
|
+
const threshold = SEVERITY_RANK[failOn];
|
|
56
|
+
const tripped = result.issues.some((i) => (SEVERITY_RANK[i.severity] ?? 0) >= threshold);
|
|
57
|
+
return tripped ? 1 : 0;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Orchestrate a headless review and return the process exit code. Side effects
|
|
61
|
+
* (filesystem, stdout) live behind `deps` so the flow is unit-testable.
|
|
62
|
+
*/
|
|
63
|
+
export function runHeadlessReview(argv, deps = defaultDeps()) {
|
|
64
|
+
const args = parseReviewArgs(argv);
|
|
65
|
+
if (args.help) {
|
|
66
|
+
deps.write(REVIEW_HELP);
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
const result = deps.review(args.files.length ? args.files : undefined);
|
|
70
|
+
deps.write(args.json ? JSON.stringify(result, null, 2) : formatReviewResult(result));
|
|
71
|
+
return exitCodeForResult(result, args.failOn);
|
|
72
|
+
}
|
|
73
|
+
// Only the reviewer's `.root` is read, so a minimal context rooted at cwd is
|
|
74
|
+
// enough — no need for a full (slower) project scan just to lint.
|
|
75
|
+
function minimalContext(root) {
|
|
76
|
+
return {
|
|
77
|
+
root,
|
|
78
|
+
name: root.split('/').pop() ?? 'workspace',
|
|
79
|
+
type: 'Unknown',
|
|
80
|
+
structure: '',
|
|
81
|
+
keyFiles: [],
|
|
82
|
+
fileCount: 0,
|
|
83
|
+
summary: `Workspace at ${root}`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function defaultDeps() {
|
|
87
|
+
return {
|
|
88
|
+
review: (files) => performCodeReview(minimalContext(process.cwd()), files),
|
|
89
|
+
write: (text) => process.stdout.write(text + '\n'),
|
|
90
|
+
};
|
|
91
|
+
}
|
package/dist/utils/skills.js
CHANGED
|
@@ -741,7 +741,11 @@ function sanitizeForShell(text) {
|
|
|
741
741
|
export function interpolateParams(content, params) {
|
|
742
742
|
let result = content;
|
|
743
743
|
for (const [key, value] of Object.entries(params)) {
|
|
744
|
-
|
|
744
|
+
// Literal find/replace-all via split+join. Avoids two bugs of the old
|
|
745
|
+
// regex form: a regex-meta char in `key` (e.g. `.`, `(`) breaking the match
|
|
746
|
+
// or throwing, and `$`-sequences in `value` ($&, $$, $1) being interpreted
|
|
747
|
+
// as replacement patterns and corrupting the output.
|
|
748
|
+
result = result.split('${' + key + '}').join(value);
|
|
745
749
|
}
|
|
746
750
|
return result;
|
|
747
751
|
}
|
|
@@ -456,7 +456,10 @@ async function dispatchTool(tool, parameters, projectRoot, fs, toolCall) {
|
|
|
456
456
|
return { success: false, output: '', error: `old_text matches ${matchCount} locations in the file. Provide more surrounding context to make it unique (only 1 match allowed).`, tool, parameters };
|
|
457
457
|
}
|
|
458
458
|
recordEdit(validation.absolutePath);
|
|
459
|
-
|
|
459
|
+
// Function replacer so newText is written literally — a plain-string
|
|
460
|
+
// replacement interprets $&, $1, $$ etc., which silently corrupts any
|
|
461
|
+
// edit whose new_text contains `$` (shell vars, template literals, regex).
|
|
462
|
+
const updated = content.replace(oldText, () => newText);
|
|
460
463
|
if (fs?.writeTextFile) {
|
|
461
464
|
try {
|
|
462
465
|
await fs.writeTextFile(validation.absolutePath, updated);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codeep",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.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",
|