codeep 2.20.0 → 2.22.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/server.js +8 -4
- package/dist/api/index.js +26 -19
- package/dist/config/index.d.ts +20 -0
- package/dist/config/index.js +32 -0
- package/dist/renderer/App.js +0 -1
- package/dist/renderer/Input.d.ts +0 -1
- package/dist/renderer/Input.js +0 -1
- package/dist/renderer/commands/registry.js +1 -0
- package/dist/renderer/commands.js +19 -3
- package/dist/renderer/components/Export.js +0 -2
- package/dist/renderer/components/Login.d.ts +0 -1
- package/dist/renderer/components/Login.js +0 -2
- package/dist/renderer/components/Logout.js +0 -2
- package/dist/renderer/components/Settings.d.ts +3 -0
- package/dist/renderer/components/Settings.js +0 -12
- package/dist/renderer/main.js +35 -56
- package/dist/utils/agent.d.ts +7 -0
- package/dist/utils/agent.js +102 -6
- package/dist/utils/auditLog.d.ts +93 -0
- package/dist/utils/auditLog.js +217 -0
- package/dist/utils/codeepCloud.d.ts +30 -4
- package/dist/utils/codeepCloud.js +71 -19
- package/dist/utils/diffPreview.js +0 -1
- package/dist/utils/git.js +0 -1
- package/dist/utils/headlessReview.d.ts +9 -1
- package/dist/utils/headlessReview.js +77 -3
- package/dist/utils/mcpStreamableHttp.d.ts +0 -1
- package/dist/utils/mcpStreamableHttp.js +0 -3
- package/dist/utils/personalities.js +0 -1
- package/dist/utils/reviewFix.d.ts +65 -0
- package/dist/utils/reviewFix.js +141 -0
- package/dist/utils/skillBundles.js +0 -4
- package/dist/utils/smartContext.js +0 -18
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
- package/dist/renderer/components/Permission.d.ts +0 -24
- package/dist/renderer/components/Permission.js +0 -113
|
@@ -301,6 +301,52 @@ function writeFileBundle(kind, items) {
|
|
|
301
301
|
* file so web edits actually take effect, but every divergent local body is
|
|
302
302
|
* first copied to ~/.codeep/backups/personalities/. */
|
|
303
303
|
let lastPersonalityPullBackupCount = 0;
|
|
304
|
+
/** Copy an about-to-be-replaced-or-removed personality into the backup dir.
|
|
305
|
+
* Shared so a deletion is backed up by exactly the same rules as an overwrite
|
|
306
|
+
* — nothing local is ever lost without a copy first. */
|
|
307
|
+
function backupLocalPersonality(name, body) {
|
|
308
|
+
const backupDir = join(homedir(), '.codeep', 'backups', 'personalities');
|
|
309
|
+
if (!existsSync(backupDir))
|
|
310
|
+
mkdirSync(backupDir, { recursive: true });
|
|
311
|
+
const suffix = new Date().toISOString().replace(/[:.]/g, '-');
|
|
312
|
+
let backupPath = join(backupDir, `${name}-${suffix}.md`);
|
|
313
|
+
let collision = 1;
|
|
314
|
+
while (existsSync(backupPath)) {
|
|
315
|
+
backupPath = join(backupDir, `${name}-${suffix}-${collision++}.md`);
|
|
316
|
+
}
|
|
317
|
+
writeFileSync(backupPath, body);
|
|
318
|
+
lastPersonalityPullBackupCount++;
|
|
319
|
+
}
|
|
320
|
+
/** Apply the server's explicit deletion list.
|
|
321
|
+
*
|
|
322
|
+
* Only names the server named. Absence from `items` is deliberately NOT a
|
|
323
|
+
* deletion signal: an expired session, the wrong account, or a truncated
|
|
324
|
+
* response all yield an empty `items`, and deleting on absence would wipe
|
|
325
|
+
* every local agent. Project-scoped agents in `.codeep/personalities/` are
|
|
326
|
+
* not cloud-owned and are never touched — only the global directory is.
|
|
327
|
+
* Every removal is backed up first, and a failed backup cancels the delete. */
|
|
328
|
+
function applyPersonalityTombstones(deleted) {
|
|
329
|
+
const dir = globalDir('personalities');
|
|
330
|
+
if (!existsSync(dir))
|
|
331
|
+
return 0;
|
|
332
|
+
let removed = 0;
|
|
333
|
+
for (const name of deleted) {
|
|
334
|
+
if (typeof name !== 'string' || !/^[a-z0-9][a-z0-9-]*$/.test(name) || name.length > 64)
|
|
335
|
+
continue;
|
|
336
|
+
const filePath = join(dir, `${name}.md`);
|
|
337
|
+
if (!existsSync(filePath))
|
|
338
|
+
continue;
|
|
339
|
+
try {
|
|
340
|
+
backupLocalPersonality(name, readFileSync(filePath, 'utf8'));
|
|
341
|
+
unlinkSync(filePath);
|
|
342
|
+
removed++;
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
// A failed backup must not become a deletion — leave the file alone.
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return removed;
|
|
349
|
+
}
|
|
304
350
|
function writePulledPersonalityBundle(items) {
|
|
305
351
|
lastPersonalityPullBackupCount = 0;
|
|
306
352
|
const dir = globalDir('personalities');
|
|
@@ -317,17 +363,7 @@ function writePulledPersonalityBundle(items) {
|
|
|
317
363
|
const local = readFileSync(filePath, 'utf8');
|
|
318
364
|
if (local === body)
|
|
319
365
|
continue;
|
|
320
|
-
|
|
321
|
-
if (!existsSync(backupDir))
|
|
322
|
-
mkdirSync(backupDir, { recursive: true });
|
|
323
|
-
let suffix = new Date().toISOString().replace(/[:.]/g, '-');
|
|
324
|
-
let backupPath = join(backupDir, `${name}-${suffix}.md`);
|
|
325
|
-
let collision = 1;
|
|
326
|
-
while (existsSync(backupPath)) {
|
|
327
|
-
backupPath = join(backupDir, `${name}-${suffix}-${collision++}.md`);
|
|
328
|
-
}
|
|
329
|
-
writeFileSync(backupPath, local);
|
|
330
|
-
lastPersonalityPullBackupCount++;
|
|
366
|
+
backupLocalPersonality(name, local);
|
|
331
367
|
}
|
|
332
368
|
// Same-directory rename is atomic on supported local filesystems: a
|
|
333
369
|
// crash cannot leave a half-written active personality.
|
|
@@ -348,39 +384,54 @@ function writePulledPersonalityBundle(items) {
|
|
|
348
384
|
}
|
|
349
385
|
return written;
|
|
350
386
|
}
|
|
387
|
+
export function describeSyncFailure(reason) {
|
|
388
|
+
switch (reason) {
|
|
389
|
+
case 'not-linked': return 'not linked to codeep.dev — run: codeep account';
|
|
390
|
+
case 'unreachable': return "couldn't reach codeep.dev";
|
|
391
|
+
case 'rejected': return 'codeep.dev refused the request — try signing in again';
|
|
392
|
+
case 'malformed': return 'codeep.dev sent a response this version cannot read';
|
|
393
|
+
}
|
|
394
|
+
}
|
|
351
395
|
async function pullBundle(kind) {
|
|
352
396
|
const syncToken = getSyncToken();
|
|
353
397
|
if (!syncToken)
|
|
354
|
-
return
|
|
398
|
+
return { ok: false, reason: 'not-linked' };
|
|
355
399
|
const res = await fetchWithRetry(`${API_BASE}/api/${kind}`, { headers: { 'x-sync-token': syncToken } });
|
|
356
400
|
if (!res?.ok)
|
|
357
|
-
return
|
|
401
|
+
return { ok: false, reason: 'unreachable' };
|
|
358
402
|
try {
|
|
359
403
|
const data = await res.json();
|
|
360
404
|
if (!data.ok)
|
|
361
|
-
return
|
|
362
|
-
|
|
405
|
+
return { ok: false, reason: 'rejected' };
|
|
406
|
+
const count = kind === 'personalities'
|
|
363
407
|
? writePulledPersonalityBundle(data.items ?? {})
|
|
364
408
|
: writeFileBundle(kind, data.items ?? {});
|
|
409
|
+
// A missing `deleted` field means "no deletions" — never "delete
|
|
410
|
+
// everything". Older servers simply omit it, and a client that treated the
|
|
411
|
+
// omission as a full tombstone list would empty the user's agent folder.
|
|
412
|
+
const removed = kind === 'personalities' && Array.isArray(data.deleted)
|
|
413
|
+
? applyPersonalityTombstones(data.deleted)
|
|
414
|
+
: 0;
|
|
415
|
+
return { ok: true, count, removed };
|
|
365
416
|
}
|
|
366
417
|
catch {
|
|
367
|
-
return
|
|
418
|
+
return { ok: false, reason: 'malformed' };
|
|
368
419
|
}
|
|
369
420
|
}
|
|
370
421
|
async function pushBundle(kind) {
|
|
371
422
|
const syncToken = getSyncToken();
|
|
372
423
|
if (!syncToken)
|
|
373
|
-
return
|
|
424
|
+
return { ok: false, reason: 'not-linked' };
|
|
374
425
|
const items = readFileBundle(kind);
|
|
375
426
|
const count = Object.keys(items).length;
|
|
376
427
|
if (count === 0)
|
|
377
|
-
return 0;
|
|
428
|
+
return { ok: true, count: 0, removed: 0 };
|
|
378
429
|
const res = await fetchWithRetry(`${API_BASE}/api/${kind}`, {
|
|
379
430
|
method: 'POST',
|
|
380
431
|
headers: { 'Content-Type': 'application/json', 'x-sync-token': syncToken },
|
|
381
432
|
body: JSON.stringify({ items }),
|
|
382
433
|
});
|
|
383
|
-
return res?.ok ? count :
|
|
434
|
+
return res?.ok ? { ok: true, count, removed: 0 } : { ok: false, reason: 'unreachable' };
|
|
384
435
|
}
|
|
385
436
|
export const pullPersonalities = () => pullBundle('personalities');
|
|
386
437
|
export const getLastPersonalityPullBackupCount = () => lastPersonalityPullBackupCount;
|
|
@@ -699,3 +750,4 @@ export const _globalDirForTest = globalDir;
|
|
|
699
750
|
export const _readFileBundleForTest = readFileBundle;
|
|
700
751
|
export const _writeFileBundleForTest = writeFileBundle;
|
|
701
752
|
export const _writePulledPersonalityBundleForTest = writePulledPersonalityBundle;
|
|
753
|
+
export const _applyPersonalityTombstonesForTest = applyPersonalityTombstones;
|
|
@@ -15,7 +15,6 @@ export function generateDiff(oldContent, newContent, contextLines = 3) {
|
|
|
15
15
|
let oldIdx = 0;
|
|
16
16
|
let newIdx = 0;
|
|
17
17
|
let currentHunk = null;
|
|
18
|
-
let pendingContext = [];
|
|
19
18
|
for (const [oldMatch, newMatch] of lcs) {
|
|
20
19
|
// Handle deletions
|
|
21
20
|
while (oldIdx < oldMatch) {
|
package/dist/utils/git.js
CHANGED
|
@@ -286,7 +286,6 @@ export function generateCommitMessage(prompt, actions) {
|
|
|
286
286
|
const hasWrites = actions.some(a => a.type === 'write');
|
|
287
287
|
const hasEdits = actions.some(a => a.type === 'edit');
|
|
288
288
|
const hasDeletes = actions.some(a => a.type === 'delete');
|
|
289
|
-
const hasCommands = actions.some(a => a.type === 'command');
|
|
290
289
|
// Determine prefix
|
|
291
290
|
let prefix = 'chore';
|
|
292
291
|
// Check prompt for common patterns
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type FixPlan } from './reviewFix.js';
|
|
1
2
|
import { ReviewResult } from './codeReview.js';
|
|
2
3
|
export type FailOn = 'error' | 'warning' | 'info' | 'none';
|
|
3
4
|
export interface ReviewArgs {
|
|
@@ -7,8 +8,12 @@ export interface ReviewArgs {
|
|
|
7
8
|
rules: boolean;
|
|
8
9
|
ai: boolean;
|
|
9
10
|
help: boolean;
|
|
11
|
+
/** Hand the findings to an agent and let it edit the working tree. */
|
|
12
|
+
fix: boolean;
|
|
13
|
+
/** Lowest severity the fix run may act on. Suggestions are never eligible. */
|
|
14
|
+
fixMinSeverity: 'error' | 'warning';
|
|
10
15
|
}
|
|
11
|
-
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\nCustom/disabled rules come from .codeep/review.yml (or .json) in the repo.\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 --rules List the built-in rule ids (for \"disable\" in .codeep/review.*) and exit\n --ai After the offline pass, ask your configured provider for a\n contextual second opinion on the working-tree diff\n (advisory; needs an API key; never affects the exit code)\n -h, --help Show this help\n\nExit code: 0 when nothing at/above --fail-on is found, 1 otherwise.";
|
|
16
|
+
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\nCustom/disabled rules come from .codeep/review.yml (or .json) in the repo.\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 --fix After the review, let an agent fix what it found. Edits the\n working tree and stops there \u2014 it never commits, branches\n or pushes. Runs under a files+tests boundary: no shell, no\n network, no git. Needs an API key.\n --fix-min-severity Lowest severity --fix may act on: error | warning\n (default: warning). Suggestions are never eligible.\n --rules List the built-in rule ids (for \"disable\" in .codeep/review.*) and exit\n --ai After the offline pass, ask your configured provider for a\n contextual second opinion on the working-tree diff\n (advisory; needs an API key; never affects the exit code)\n -h, --help Show this help\n\nExit code: 0 when nothing at/above --fail-on is found, 1 otherwise.";
|
|
12
17
|
/** Parse `codeep review` argv (everything after the subcommand). Pure. */
|
|
13
18
|
export declare function parseReviewArgs(argv: string[]): ReviewArgs;
|
|
14
19
|
/** Exit code for a result under a fail-on threshold. Pure. */
|
|
@@ -29,6 +34,9 @@ export interface ReviewDeps {
|
|
|
29
34
|
provider?: string;
|
|
30
35
|
model?: string;
|
|
31
36
|
};
|
|
37
|
+
/** Run an agent over a fix plan. Returns a human summary, or null when it
|
|
38
|
+
* could not run at all (no API key, provider unreachable). */
|
|
39
|
+
applyFixes: (plan: FixPlan) => Promise<string | null>;
|
|
32
40
|
}
|
|
33
41
|
/**
|
|
34
42
|
* Orchestrate a headless review and return the process exit code. Side effects
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { buildFixPlan, summariseFixPlan } from './reviewFix.js';
|
|
1
2
|
// Headless `codeep review` — a non-interactive entry point around the
|
|
2
3
|
// deterministic reviewer in codeReview.ts. No API key, no TUI: it scans, prints
|
|
3
4
|
// a report (markdown or JSON), and exits non-zero when issues at/above a chosen
|
|
@@ -24,6 +25,12 @@ Options:
|
|
|
24
25
|
--json Print the result as JSON instead of the markdown report
|
|
25
26
|
--fail-on <level> Exit non-zero when an issue at or above <level> is found:
|
|
26
27
|
error | warning | info | none (default: error)
|
|
28
|
+
--fix After the review, let an agent fix what it found. Edits the
|
|
29
|
+
working tree and stops there — it never commits, branches
|
|
30
|
+
or pushes. Runs under a files+tests boundary: no shell, no
|
|
31
|
+
network, no git. Needs an API key.
|
|
32
|
+
--fix-min-severity Lowest severity --fix may act on: error | warning
|
|
33
|
+
(default: warning). Suggestions are never eligible.
|
|
27
34
|
--rules List the built-in rule ids (for "disable" in .codeep/review.*) and exit
|
|
28
35
|
--ai After the offline pass, ask your configured provider for a
|
|
29
36
|
contextual second opinion on the working-tree diff
|
|
@@ -33,7 +40,10 @@ Options:
|
|
|
33
40
|
Exit code: 0 when nothing at/above --fail-on is found, 1 otherwise.`;
|
|
34
41
|
/** Parse `codeep review` argv (everything after the subcommand). Pure. */
|
|
35
42
|
export function parseReviewArgs(argv) {
|
|
36
|
-
const out = {
|
|
43
|
+
const out = {
|
|
44
|
+
files: [], json: false, failOn: 'error', rules: false, ai: false, help: false,
|
|
45
|
+
fix: false, fixMinSeverity: 'warning',
|
|
46
|
+
};
|
|
37
47
|
for (let i = 0; i < argv.length; i++) {
|
|
38
48
|
const arg = argv[i];
|
|
39
49
|
if (arg === '--json') {
|
|
@@ -45,6 +55,19 @@ export function parseReviewArgs(argv) {
|
|
|
45
55
|
else if (arg === '--ai') {
|
|
46
56
|
out.ai = true;
|
|
47
57
|
}
|
|
58
|
+
else if (arg === '--fix') {
|
|
59
|
+
out.fix = true;
|
|
60
|
+
}
|
|
61
|
+
else if (arg === '--fix-min-severity') {
|
|
62
|
+
const v = argv[++i];
|
|
63
|
+
if (v === 'error' || v === 'warning')
|
|
64
|
+
out.fixMinSeverity = v;
|
|
65
|
+
}
|
|
66
|
+
else if (arg.startsWith('--fix-min-severity=')) {
|
|
67
|
+
const v = arg.slice('--fix-min-severity='.length);
|
|
68
|
+
if (v === 'error' || v === 'warning')
|
|
69
|
+
out.fixMinSeverity = v;
|
|
70
|
+
}
|
|
48
71
|
else if (arg === '-h' || arg === '--help') {
|
|
49
72
|
out.help = true;
|
|
50
73
|
}
|
|
@@ -104,12 +127,26 @@ export async function runHeadlessReview(argv, deps = defaultDeps()) {
|
|
|
104
127
|
}
|
|
105
128
|
const result = deps.review(args.files.length ? args.files : undefined);
|
|
106
129
|
const aiText = args.ai ? await deps.aiReview(result) : null;
|
|
130
|
+
// Fixing happens after reporting, and never changes the exit code. CI decides
|
|
131
|
+
// pass or fail from what the reviewer found; whether an agent then managed to
|
|
132
|
+
// repair some of it is a separate question, and letting a successful fix turn
|
|
133
|
+
// a red check green would hide the finding rather than resolve it.
|
|
134
|
+
let fixSummary = null;
|
|
135
|
+
if (args.fix) {
|
|
136
|
+
const plan = buildFixPlan(result.issues, { minSeverity: args.fixMinSeverity });
|
|
137
|
+
fixSummary = plan.skipped ? summariseFixPlan(plan) : await deps.applyFixes(plan);
|
|
138
|
+
}
|
|
107
139
|
if (args.json) {
|
|
108
|
-
deps.write(JSON.stringify(
|
|
140
|
+
deps.write(JSON.stringify({
|
|
141
|
+
...result,
|
|
142
|
+
...(args.ai ? { aiReview: aiText } : {}),
|
|
143
|
+
...(args.fix ? { fix: fixSummary } : {}),
|
|
144
|
+
}, null, 2));
|
|
109
145
|
}
|
|
110
146
|
else {
|
|
111
147
|
const md = formatReviewResult(result);
|
|
112
|
-
|
|
148
|
+
const withAi = args.ai ? appendAiSection(md, aiText, deps.aiMeta()) : md;
|
|
149
|
+
deps.write(fixSummary ? `${withAi}\n\n## Fix run\n\n${fixSummary}\n` : withAi);
|
|
113
150
|
}
|
|
114
151
|
return exitCodeForResult(result, args.failOn);
|
|
115
152
|
}
|
|
@@ -132,6 +169,7 @@ function defaultDeps() {
|
|
|
132
169
|
review: (files) => performCodeReview(minimalContext(cwd), files),
|
|
133
170
|
write: (text) => process.stdout.write(text + '\n'),
|
|
134
171
|
listRules: () => formatBuiltinRules(),
|
|
172
|
+
applyFixes: (plan) => runFixPlan(plan, minimalContext(cwd)),
|
|
135
173
|
aiMeta: () => {
|
|
136
174
|
try {
|
|
137
175
|
return { provider: getCurrentProvider().name, model: String(config.get('model') || '') };
|
|
@@ -165,3 +203,39 @@ function defaultDeps() {
|
|
|
165
203
|
},
|
|
166
204
|
};
|
|
167
205
|
}
|
|
206
|
+
/**
|
|
207
|
+
* Run a fix plan through the agent.
|
|
208
|
+
*
|
|
209
|
+
* The plan's personality is passed as the active one, so the same enforcement
|
|
210
|
+
* any custom bot gets applies here: the model is offered `files` and `tests`
|
|
211
|
+
* and nothing else. It edits the working tree and stops — branching, committing
|
|
212
|
+
* and opening a pull request belong to whatever called this, which in CI is the
|
|
213
|
+
* action that holds the token.
|
|
214
|
+
*/
|
|
215
|
+
async function runFixPlan(plan, context) {
|
|
216
|
+
try {
|
|
217
|
+
const { runAgent } = await import('./agent.js');
|
|
218
|
+
// No cast here. `as never` on this call once hid the fact that
|
|
219
|
+
// personalityOverride did not exist, which would have run the CI fix with
|
|
220
|
+
// no boundary at all while the tests happily asserted otherwise.
|
|
221
|
+
const result = await runAgent(plan.prompt, context, {
|
|
222
|
+
personalityOverride: plan.personality,
|
|
223
|
+
maxIterations: 12,
|
|
224
|
+
});
|
|
225
|
+
const edited = new Set(result.actions
|
|
226
|
+
.filter(a => a.type === 'write' || a.type === 'edit')
|
|
227
|
+
.map(a => a.target));
|
|
228
|
+
if (!result.success) {
|
|
229
|
+
return `${summariseFixPlan(plan)} The run did not finish: ${result.error ?? 'unknown error'}.`;
|
|
230
|
+
}
|
|
231
|
+
if (edited.size === 0) {
|
|
232
|
+
return `${summariseFixPlan(plan)} Nothing was changed — the agent judged the findings not mechanically fixable.`;
|
|
233
|
+
}
|
|
234
|
+
return `${summariseFixPlan(plan)} Edited ${edited.size} file${edited.size === 1 ? '' : 's'}: ${[...edited].join(', ')}.`;
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
// A missing key or an unreachable provider must not fail the review. The
|
|
238
|
+
// findings are already reported and the exit code already decided.
|
|
239
|
+
return `Could not run the fix: ${error.message}`;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
@@ -32,7 +32,6 @@ export declare class StreamableHttpClient {
|
|
|
32
32
|
private notificationAbort;
|
|
33
33
|
private stopped;
|
|
34
34
|
/** True after the server has set a session id (i.e. it tracks state). */
|
|
35
|
-
private get hasServerSession();
|
|
36
35
|
constructor(opts: StreamableHttpOptions);
|
|
37
36
|
/**
|
|
38
37
|
* Issue a JSON-RPC frame as POST. Reply may be a single JSON response
|
|
@@ -28,9 +28,6 @@ export class StreamableHttpClient {
|
|
|
28
28
|
notificationAbort = null;
|
|
29
29
|
stopped = false;
|
|
30
30
|
/** True after the server has set a session id (i.e. it tracks state). */
|
|
31
|
-
get hasServerSession() {
|
|
32
|
-
return this.sessionId !== null;
|
|
33
|
-
}
|
|
34
31
|
constructor(opts) {
|
|
35
32
|
this.opts = opts;
|
|
36
33
|
}
|
|
@@ -648,7 +648,6 @@ export function isPersonalityToolCallAllowed(personality, toolCall, registeredMc
|
|
|
648
648
|
if (tool === 'execute_command') {
|
|
649
649
|
if (personality.tools?.includes('terminal'))
|
|
650
650
|
return true;
|
|
651
|
-
const command = commandName(toolCall);
|
|
652
651
|
if (personality.tools?.includes('git') && isRestrictedGitCommandAllowed(toolCall))
|
|
653
652
|
return true;
|
|
654
653
|
if (personality.tools?.includes('tests') && isTestCommand(toolCall))
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deciding what a CI agent may attempt to fix, and under what boundary.
|
|
3
|
+
*
|
|
4
|
+
* The reviewer already finds problems. This decides which of them are worth
|
|
5
|
+
* handing to an agent, caps how much it may take on, and pins the capabilities
|
|
6
|
+
* it runs with. It deliberately stops there: it produces a plan, never a
|
|
7
|
+
* commit. Staging, branching and opening a pull request belong to the action,
|
|
8
|
+
* which has the token — and keeping git out of the agent's reach is half the
|
|
9
|
+
* reason this is safe to run in CI at all.
|
|
10
|
+
*
|
|
11
|
+
* The boundary is the point. A fix run gets `files` (it must edit) and `tests`
|
|
12
|
+
* (it must check its own work) and nothing else. No shell, no git, no network.
|
|
13
|
+
* Enforced by the same machinery as any other custom bot, so an agent that
|
|
14
|
+
* decides it would like to curl something simply has no tool to do it with.
|
|
15
|
+
*/
|
|
16
|
+
import type { ReviewIssue } from './codeReview.js';
|
|
17
|
+
import type { Personality } from './personalities.js';
|
|
18
|
+
export interface FixPlanOptions {
|
|
19
|
+
/** Lowest severity to attempt. Defaults to `warning`. */
|
|
20
|
+
minSeverity?: 'error' | 'warning';
|
|
21
|
+
/** Most issues to hand over in one run. */
|
|
22
|
+
maxIssues?: number;
|
|
23
|
+
/** Most files to touch. A fix that rewrites half the repo is not a fix. */
|
|
24
|
+
maxFiles?: number;
|
|
25
|
+
}
|
|
26
|
+
export interface FixPlan {
|
|
27
|
+
/** The issues the agent is being asked to address, in file order. */
|
|
28
|
+
issues: ReviewIssue[];
|
|
29
|
+
/** Files it is allowed to be working in. */
|
|
30
|
+
files: string[];
|
|
31
|
+
/** Why nothing is being attempted, when that is the case. */
|
|
32
|
+
skipped?: 'no-issues' | 'nothing-fixable';
|
|
33
|
+
/** The instruction handed to the agent. */
|
|
34
|
+
prompt: string;
|
|
35
|
+
/** The capability boundary the run executes under. */
|
|
36
|
+
personality: Personality;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* `suggestion` and `info` are opinion — style preferences, "consider extracting
|
|
40
|
+
* this". Acting on them unasked produces churn in someone else's pull request
|
|
41
|
+
* and buries the findings that matter. Only what the reviewer states as a
|
|
42
|
+
* defect is eligible.
|
|
43
|
+
*/
|
|
44
|
+
export declare function isFixable(issue: ReviewIssue, minSeverity: 'error' | 'warning'): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* The capability set a CI fix runs under.
|
|
47
|
+
*
|
|
48
|
+
* Not a suggestion in the prompt — a real `custom-bot/v1` personality, enforced
|
|
49
|
+
* by `isPersonalityToolCallAllowed` and by the tool registry filter, exactly as
|
|
50
|
+
* a bot built in Agent Studio would be. An agent running unattended against
|
|
51
|
+
* someone else's repository is precisely where a boundary has to be real.
|
|
52
|
+
*/
|
|
53
|
+
export declare function ciFixPersonality(): Personality;
|
|
54
|
+
/**
|
|
55
|
+
* Turn a review into a bounded instruction, or decline.
|
|
56
|
+
*
|
|
57
|
+
* Caps matter more than they look. An agent handed sixty findings across forty
|
|
58
|
+
* files will produce a pull request nobody reviews, which is the same as no
|
|
59
|
+
* pull request — except it also burned tokens and someone's afternoon.
|
|
60
|
+
*/
|
|
61
|
+
export declare function buildFixPlan(issues: ReviewIssue[], options?: FixPlanOptions): FixPlan;
|
|
62
|
+
/** The instruction the agent receives: the findings, grouped by file. */
|
|
63
|
+
export declare function formatFixPrompt(issues: ReviewIssue[]): string;
|
|
64
|
+
/** A one-line summary for the pull request body the action opens. */
|
|
65
|
+
export declare function summariseFixPlan(plan: FixPlan): string;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deciding what a CI agent may attempt to fix, and under what boundary.
|
|
3
|
+
*
|
|
4
|
+
* The reviewer already finds problems. This decides which of them are worth
|
|
5
|
+
* handing to an agent, caps how much it may take on, and pins the capabilities
|
|
6
|
+
* it runs with. It deliberately stops there: it produces a plan, never a
|
|
7
|
+
* commit. Staging, branching and opening a pull request belong to the action,
|
|
8
|
+
* which has the token — and keeping git out of the agent's reach is half the
|
|
9
|
+
* reason this is safe to run in CI at all.
|
|
10
|
+
*
|
|
11
|
+
* The boundary is the point. A fix run gets `files` (it must edit) and `tests`
|
|
12
|
+
* (it must check its own work) and nothing else. No shell, no git, no network.
|
|
13
|
+
* Enforced by the same machinery as any other custom bot, so an agent that
|
|
14
|
+
* decides it would like to curl something simply has no tool to do it with.
|
|
15
|
+
*/
|
|
16
|
+
/** Severities an agent may act on, in descending confidence. */
|
|
17
|
+
const FIXABLE_SEVERITIES = ['error', 'warning'];
|
|
18
|
+
const DEFAULTS = { minSeverity: 'warning', maxIssues: 20, maxFiles: 10 };
|
|
19
|
+
/**
|
|
20
|
+
* `suggestion` and `info` are opinion — style preferences, "consider extracting
|
|
21
|
+
* this". Acting on them unasked produces churn in someone else's pull request
|
|
22
|
+
* and buries the findings that matter. Only what the reviewer states as a
|
|
23
|
+
* defect is eligible.
|
|
24
|
+
*/
|
|
25
|
+
export function isFixable(issue, minSeverity) {
|
|
26
|
+
if (!FIXABLE_SEVERITIES.includes(issue.severity))
|
|
27
|
+
return false;
|
|
28
|
+
return minSeverity === 'warning' ? true : issue.severity === 'error';
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The capability set a CI fix runs under.
|
|
32
|
+
*
|
|
33
|
+
* Not a suggestion in the prompt — a real `custom-bot/v1` personality, enforced
|
|
34
|
+
* by `isPersonalityToolCallAllowed` and by the tool registry filter, exactly as
|
|
35
|
+
* a bot built in Agent Studio would be. An agent running unattended against
|
|
36
|
+
* someone else's repository is precisely where a boundary has to be real.
|
|
37
|
+
*/
|
|
38
|
+
export function ciFixPersonality() {
|
|
39
|
+
return {
|
|
40
|
+
name: 'ci-fix',
|
|
41
|
+
displayName: 'CI Fix',
|
|
42
|
+
description: 'Applies review findings in CI. Files and tests only.',
|
|
43
|
+
prompt: [
|
|
44
|
+
'You are fixing problems a reviewer already found in a pull request.',
|
|
45
|
+
'',
|
|
46
|
+
'Rules:',
|
|
47
|
+
'- Fix only the issues listed. Do not refactor around them.',
|
|
48
|
+
'- Do not reformat untouched lines; the diff should read as a fix, not a rewrite.',
|
|
49
|
+
'- Run the project tests when you are done and fix what you broke.',
|
|
50
|
+
'- If an issue needs a judgement call you cannot make from the code, leave it and say so.',
|
|
51
|
+
].join('\n'),
|
|
52
|
+
scope: 'project',
|
|
53
|
+
structured: true,
|
|
54
|
+
schemaValid: true,
|
|
55
|
+
modelPreference: 'automatic',
|
|
56
|
+
restrictTools: true,
|
|
57
|
+
tools: ['files', 'tests'],
|
|
58
|
+
declaredTools: ['files', 'tests'],
|
|
59
|
+
projectScope: 'all',
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Turn a review into a bounded instruction, or decline.
|
|
64
|
+
*
|
|
65
|
+
* Caps matter more than they look. An agent handed sixty findings across forty
|
|
66
|
+
* files will produce a pull request nobody reviews, which is the same as no
|
|
67
|
+
* pull request — except it also burned tokens and someone's afternoon.
|
|
68
|
+
*/
|
|
69
|
+
export function buildFixPlan(issues, options = {}) {
|
|
70
|
+
const { minSeverity, maxIssues, maxFiles } = { ...DEFAULTS, ...options };
|
|
71
|
+
const personality = ciFixPersonality();
|
|
72
|
+
if (issues.length === 0) {
|
|
73
|
+
return { issues: [], files: [], skipped: 'no-issues', prompt: '', personality };
|
|
74
|
+
}
|
|
75
|
+
const eligible = issues
|
|
76
|
+
.filter(issue => isFixable(issue, minSeverity))
|
|
77
|
+
// Errors before warnings, then by file so one file's issues arrive together.
|
|
78
|
+
.sort((a, b) => {
|
|
79
|
+
if (a.severity !== b.severity)
|
|
80
|
+
return a.severity === 'error' ? -1 : 1;
|
|
81
|
+
return a.file.localeCompare(b.file) || (a.line ?? 0) - (b.line ?? 0);
|
|
82
|
+
});
|
|
83
|
+
if (eligible.length === 0) {
|
|
84
|
+
return { issues: [], files: [], skipped: 'nothing-fixable', prompt: '', personality };
|
|
85
|
+
}
|
|
86
|
+
// Take whole files rather than cutting a file's issues in half — a partial
|
|
87
|
+
// fix to one file is the worst outcome available, since it looks addressed.
|
|
88
|
+
const files = [];
|
|
89
|
+
const taken = [];
|
|
90
|
+
for (const issue of eligible) {
|
|
91
|
+
const knownFile = files.includes(issue.file);
|
|
92
|
+
if (!knownFile && files.length >= maxFiles)
|
|
93
|
+
continue;
|
|
94
|
+
if (taken.length >= maxIssues && !knownFile)
|
|
95
|
+
continue;
|
|
96
|
+
if (!knownFile)
|
|
97
|
+
files.push(issue.file);
|
|
98
|
+
taken.push(issue);
|
|
99
|
+
}
|
|
100
|
+
return { issues: taken, files, prompt: formatFixPrompt(taken), personality };
|
|
101
|
+
}
|
|
102
|
+
/** The instruction the agent receives: the findings, grouped by file. */
|
|
103
|
+
export function formatFixPrompt(issues) {
|
|
104
|
+
const byFile = new Map();
|
|
105
|
+
for (const issue of issues) {
|
|
106
|
+
const list = byFile.get(issue.file) ?? [];
|
|
107
|
+
list.push(issue);
|
|
108
|
+
byFile.set(issue.file, list);
|
|
109
|
+
}
|
|
110
|
+
const lines = [
|
|
111
|
+
`Fix the following ${issues.length} review finding${issues.length === 1 ? '' : 's'}.`,
|
|
112
|
+
'',
|
|
113
|
+
];
|
|
114
|
+
for (const [file, found] of byFile) {
|
|
115
|
+
lines.push(`## ${file}`);
|
|
116
|
+
for (const issue of found) {
|
|
117
|
+
const where = issue.line ? `line ${issue.line}` : 'file';
|
|
118
|
+
lines.push(`- [${issue.severity}] ${where}: ${issue.message}`);
|
|
119
|
+
if (issue.suggestion)
|
|
120
|
+
lines.push(` suggested: ${issue.suggestion}`);
|
|
121
|
+
}
|
|
122
|
+
lines.push('');
|
|
123
|
+
}
|
|
124
|
+
lines.push('Change nothing outside these files.');
|
|
125
|
+
return lines.join('\n');
|
|
126
|
+
}
|
|
127
|
+
/** A one-line summary for the pull request body the action opens. */
|
|
128
|
+
export function summariseFixPlan(plan) {
|
|
129
|
+
if (plan.skipped === 'no-issues')
|
|
130
|
+
return 'The review found nothing.';
|
|
131
|
+
if (plan.skipped === 'nothing-fixable') {
|
|
132
|
+
return 'The review found only suggestions, which are left for a human to weigh.';
|
|
133
|
+
}
|
|
134
|
+
const errors = plan.issues.filter(i => i.severity === 'error').length;
|
|
135
|
+
const warnings = plan.issues.length - errors;
|
|
136
|
+
const parts = [
|
|
137
|
+
errors ? `${errors} error${errors === 1 ? '' : 's'}` : '',
|
|
138
|
+
warnings ? `${warnings} warning${warnings === 1 ? '' : 's'}` : '',
|
|
139
|
+
].filter(Boolean);
|
|
140
|
+
return `Attempting ${parts.join(' and ')} across ${plan.files.length} file${plan.files.length === 1 ? '' : 's'}.`;
|
|
141
|
+
}
|
|
@@ -52,12 +52,10 @@ export function parseFrontmatter(raw) {
|
|
|
52
52
|
return { meta: {}, body: normalised };
|
|
53
53
|
const meta = {};
|
|
54
54
|
const lines = match[1].split('\n');
|
|
55
|
-
let currentKey = null;
|
|
56
55
|
let currentList = null;
|
|
57
56
|
for (const rawLine of lines) {
|
|
58
57
|
const line = rawLine.replace(/\s+$/, '');
|
|
59
58
|
if (!line.trim()) {
|
|
60
|
-
currentKey = null;
|
|
61
59
|
currentList = null;
|
|
62
60
|
continue;
|
|
63
61
|
}
|
|
@@ -75,7 +73,6 @@ export function parseFrontmatter(raw) {
|
|
|
75
73
|
let value = kv[2];
|
|
76
74
|
if (value === '') {
|
|
77
75
|
// Empty → expecting a block list below
|
|
78
|
-
currentKey = key;
|
|
79
76
|
currentList = [];
|
|
80
77
|
meta[key] = currentList;
|
|
81
78
|
continue;
|
|
@@ -89,7 +86,6 @@ export function parseFrontmatter(raw) {
|
|
|
89
86
|
value = stripQuotes(value);
|
|
90
87
|
}
|
|
91
88
|
meta[key] = value;
|
|
92
|
-
currentKey = null;
|
|
93
89
|
currentList = null;
|
|
94
90
|
}
|
|
95
91
|
return { meta, body: match[2].trimStart() };
|
|
@@ -9,24 +9,6 @@ import { logger } from './logger.js';
|
|
|
9
9
|
const MAX_CONTEXT_SIZE = 50000;
|
|
10
10
|
const MAX_FILES = 15;
|
|
11
11
|
// File extensions we care about
|
|
12
|
-
const CODE_EXTENSIONS = new Set([
|
|
13
|
-
'.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs',
|
|
14
|
-
'.py', '.pyw',
|
|
15
|
-
'.go',
|
|
16
|
-
'.rs',
|
|
17
|
-
'.php', '.phtml',
|
|
18
|
-
'.java', '.kt', '.scala',
|
|
19
|
-
'.cs', '.fs',
|
|
20
|
-
'.rb',
|
|
21
|
-
'.swift',
|
|
22
|
-
'.c', '.cpp', '.h', '.hpp',
|
|
23
|
-
'.vue', '.svelte',
|
|
24
|
-
'.css', '.scss', '.less',
|
|
25
|
-
'.html', '.htm',
|
|
26
|
-
'.json', '.yaml', '.yml', '.toml',
|
|
27
|
-
'.sql',
|
|
28
|
-
'.md',
|
|
29
|
-
]);
|
|
30
12
|
/**
|
|
31
13
|
* Extract imports/requires from file content
|
|
32
14
|
*/
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "2.
|
|
1
|
+
export declare const VERSION = "2.22.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 = '2.
|
|
4
|
+
export const VERSION = '2.22.0';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codeep",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.22.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",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"@napi-rs/keyring": "^1.3.0",
|
|
43
43
|
"clipboardy": "^4.0.0",
|
|
44
44
|
"conf": "^13.1.0",
|
|
45
|
-
"js-yaml": "^4.1
|
|
45
|
+
"js-yaml": "^4.3.1",
|
|
46
46
|
"open": "^10.0.0"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Permission screen for granting folder access
|
|
3
|
-
*/
|
|
4
|
-
import { Screen } from '../Screen';
|
|
5
|
-
export type PermissionLevel = 'none' | 'read' | 'write';
|
|
6
|
-
export interface PermissionOptions {
|
|
7
|
-
projectPath: string;
|
|
8
|
-
isProject: boolean;
|
|
9
|
-
currentPermission: PermissionLevel;
|
|
10
|
-
onSelect: (permission: PermissionLevel) => void;
|
|
11
|
-
onCancel: () => void;
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* Render permission screen
|
|
15
|
-
*/
|
|
16
|
-
export declare function renderPermissionScreen(screen: Screen, options: PermissionOptions, selectedIndex: number): void;
|
|
17
|
-
/**
|
|
18
|
-
* Get permission options array for easy indexing
|
|
19
|
-
*/
|
|
20
|
-
export declare function getPermissionOptions(): PermissionLevel[];
|
|
21
|
-
/**
|
|
22
|
-
* Truncate path for display
|
|
23
|
-
*/
|
|
24
|
-
export declare function truncatePath(path: string, maxLen: number): string;
|