codeep 2.6.0 → 2.7.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 +41 -7
- package/dist/acp/server.js +7 -7
- package/dist/acp/session.d.ts +2 -0
- package/dist/acp/session.js +1 -0
- package/dist/config/index.d.ts +4 -0
- package/dist/config/index.js +43 -2
- package/dist/renderer/main.js +8 -2
- package/dist/utils/agent.d.ts +12 -0
- package/dist/utils/agent.js +18 -5
- package/dist/utils/codeReview.d.ts +21 -0
- package/dist/utils/codeReview.js +31 -0
- package/dist/utils/gitHookInstaller.d.ts +26 -0
- package/dist/utils/gitHookInstaller.js +168 -0
- package/dist/utils/headlessReview.d.ts +17 -3
- package/dist/utils/headlessReview.js +82 -6
- package/dist/utils/keychain.d.ts +3 -0
- package/dist/utils/keychain.js +4 -0
- package/dist/utils/reviewConfig.js +29 -10
- package/dist/utils/update.d.ts +3 -1
- package/dist/utils/update.js +6 -2
- package/dist/version.d.ts +1 -0
- package/dist/version.js +4 -0
- package/package.json +6 -4
package/README.md
CHANGED
|
@@ -217,12 +217,32 @@ AI-powered review of your git diff with `/review`:
|
|
|
217
217
|
|
|
218
218
|
If there are no git changes, falls back to static analysis automatically.
|
|
219
219
|
|
|
220
|
-
#### Custom rules (`.codeep/review.json`)
|
|
220
|
+
#### Custom rules (`.codeep/review.yml` or `.codeep/review.json`)
|
|
221
221
|
|
|
222
222
|
The static reviewer (`codeep review` / `/review --static`) ships a set of
|
|
223
|
-
built-in rules, but a project can tailor them — check a `.codeep/review.
|
|
224
|
-
into the repo and the CLI **and** the
|
|
225
|
-
|
|
223
|
+
built-in rules, but a project can tailor them — check a `.codeep/review.yml`
|
|
224
|
+
(or `.codeep/review.json`) into the repo and the CLI **and** the
|
|
225
|
+
[Codeep GitHub Action](https://github.com/VladoIvankovic/codeep-action) both
|
|
226
|
+
pick it up automatically (zero LLM cost). YAML is preferred when present and is
|
|
227
|
+
nicer for regexes — single-quoted YAML keeps backslashes literal
|
|
228
|
+
(`pattern: '\bfoo\('`) so you avoid JSON's double-escaping:
|
|
229
|
+
|
|
230
|
+
```yaml
|
|
231
|
+
# .codeep/review.yml
|
|
232
|
+
rules:
|
|
233
|
+
- id: no-internal-import
|
|
234
|
+
pattern: "from ['\"]@acme/internal"
|
|
235
|
+
category: best-practice
|
|
236
|
+
severity: error
|
|
237
|
+
message: Don't import from @acme/internal outside the platform team
|
|
238
|
+
suggestion: Use the public @acme/sdk package
|
|
239
|
+
extensions: [.ts, .tsx]
|
|
240
|
+
disable: [todo-comment, anonymous-function]
|
|
241
|
+
include: ['src/**']
|
|
242
|
+
exclude: ['**/*.test.ts', 'vendor/**']
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
The same config as JSON:
|
|
226
246
|
|
|
227
247
|
```json
|
|
228
248
|
{
|
|
@@ -246,14 +266,28 @@ both pick it up automatically (zero LLM cost):
|
|
|
246
266
|
- **`rules`** — your own checks. `id`, `pattern` (a regex string), and `message`
|
|
247
267
|
are required; `flags` (default `g`), `category`, `severity`
|
|
248
268
|
(`error|warning|info|suggestion`), `suggestion`, and `extensions` are optional.
|
|
249
|
-
- **`disable`** — turn off built-in rules by id
|
|
250
|
-
|
|
251
|
-
`
|
|
269
|
+
- **`disable`** — turn off built-in rules by id. Run `codeep review --rules` for
|
|
270
|
+
the live list; the built-in ids are: `eval-usage`, `inner-html`,
|
|
271
|
+
`dangerously-set-inner-html`, `hardcoded-password`, `hardcoded-api-key`,
|
|
272
|
+
`foreach-await`, `await-in-loop`, `select-star`, `loose-null-check`,
|
|
273
|
+
`empty-catch`, `console-statement`, `todo-comment`, `any-type`, `ts-ignore`,
|
|
274
|
+
`as-any`, `var-usage`, `anonymous-function`, `missing-jsdoc`, `long-file`,
|
|
275
|
+
`long-function`.
|
|
252
276
|
- **`include` / `exclude`** — glob scoping (`**`, `*`, `?`); `include` empty = all files.
|
|
253
277
|
|
|
254
278
|
A missing, malformed, or partially-invalid config never breaks a review — bad
|
|
255
279
|
entries are skipped and the run proceeds with whatever is valid.
|
|
256
280
|
|
|
281
|
+
**More review commands:**
|
|
282
|
+
- `codeep review --rules` — list the built-in rule ids (above) and exit.
|
|
283
|
+
- `codeep review --ai` — after the offline pass, get an advisory AI second
|
|
284
|
+
opinion on the working-tree diff from your configured provider (needs an API
|
|
285
|
+
key; never changes the exit code, so CI stays deterministic).
|
|
286
|
+
- `codeep hook install` — install a git pre-commit hook that reviews the
|
|
287
|
+
working-tree content of your staged files and blocks the commit on failures
|
|
288
|
+
(`--pre-push` for pre-push, `--fail-on <level>` to set the threshold,
|
|
289
|
+
`codeep hook uninstall` to remove). Stage changes fully before committing.
|
|
290
|
+
|
|
257
291
|
### Interactive Mode
|
|
258
292
|
Agent asks clarifying questions when tasks are ambiguous:
|
|
259
293
|
```
|
package/dist/acp/server.js
CHANGED
|
@@ -973,11 +973,10 @@ export function startAcpServer() {
|
|
|
973
973
|
});
|
|
974
974
|
};
|
|
975
975
|
resetTokenTracking();
|
|
976
|
-
//
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
}
|
|
976
|
+
// Manual mode gates write/edit for THIS run via a per-call option passed to
|
|
977
|
+
// runAgentSession (extraDangerousTools, below) — NOT by mutating the global
|
|
978
|
+
// `agentConfirmWriteFile` config, which leaked the session's mode into the
|
|
979
|
+
// TUI/other processes and raced on a non-atomic restore.
|
|
981
980
|
const agentResponseChunks = [];
|
|
982
981
|
const sendChunk = (text) => {
|
|
983
982
|
agentResponseChunks.push(text);
|
|
@@ -1083,6 +1082,9 @@ export function startAcpServer() {
|
|
|
1083
1082
|
}
|
|
1084
1083
|
}
|
|
1085
1084
|
},
|
|
1085
|
+
// Manual mode gates write_file/edit_file for this run only (per-call,
|
|
1086
|
+
// no global config mutation).
|
|
1087
|
+
extraDangerousTools: session.currentModeId === 'manual' ? ['write_file', 'edit_file'] : undefined,
|
|
1086
1088
|
// Only request permission in Manual mode
|
|
1087
1089
|
onRequestPermission: session.currentModeId === 'manual'
|
|
1088
1090
|
? async (toolCall) => {
|
|
@@ -1183,7 +1185,6 @@ export function startAcpServer() {
|
|
|
1183
1185
|
if (agentResponse) {
|
|
1184
1186
|
session.history.push({ role: 'assistant', content: agentResponse });
|
|
1185
1187
|
}
|
|
1186
|
-
config.set('agentConfirmWriteFile', prevConfirmWrite);
|
|
1187
1188
|
autoSaveSession(session.history, session.workspaceRoot);
|
|
1188
1189
|
// Report token usage to dashboard
|
|
1189
1190
|
const projectCtx = getProjectContext(session.workspaceRoot);
|
|
@@ -1247,7 +1248,6 @@ export function startAcpServer() {
|
|
|
1247
1248
|
transport.error(msg.id, -32000, err.message);
|
|
1248
1249
|
}
|
|
1249
1250
|
}).finally(() => {
|
|
1250
|
-
config.set('agentConfirmWriteFile', prevConfirmWrite);
|
|
1251
1251
|
if (session)
|
|
1252
1252
|
session.abortController = null;
|
|
1253
1253
|
planEntries.clear();
|
package/dist/acp/session.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ export interface AgentSessionOptions {
|
|
|
11
11
|
onThought?: (text: string) => void;
|
|
12
12
|
onToolCall?: (toolCallId: string, toolName: string, kind: string, title: string, status: 'pending' | 'running' | 'finished' | 'error', locations?: string[], rawOutput?: string) => void;
|
|
13
13
|
onRequestPermission?: (toolCall: ToolCall) => Promise<PermissionOutcome>;
|
|
14
|
+
/** Tools to force into the per-run dangerous set (ACP manual mode). */
|
|
15
|
+
extraDangerousTools?: string[];
|
|
14
16
|
onExecuteCommand?: (command: string, args: string[], cwd: string) => Promise<{
|
|
15
17
|
stdout: string;
|
|
16
18
|
stderr: string;
|
package/dist/acp/session.js
CHANGED
|
@@ -145,6 +145,7 @@ export async function runAgentSession(opts) {
|
|
|
145
145
|
}
|
|
146
146
|
},
|
|
147
147
|
onRequestPermission: opts.onRequestPermission,
|
|
148
|
+
extraDangerousTools: opts.extraDangerousTools,
|
|
148
149
|
onExecuteCommand: opts.onExecuteCommand,
|
|
149
150
|
fs: opts.fs,
|
|
150
151
|
// Route MCP-prefixed tool calls through the per-session registry.
|
package/dist/config/index.d.ts
CHANGED
|
@@ -96,6 +96,10 @@ interface ConfigSchema {
|
|
|
96
96
|
/** True once legacy plaintext keys (providerApiKeys / apiKey) have been
|
|
97
97
|
* migrated into secure storage and wiped from the config file. */
|
|
98
98
|
keysSecured: boolean;
|
|
99
|
+
/** Plaintext fallback key map used by utils/keychain.ts ONLY when the OS
|
|
100
|
+
* keychain is unavailable. Swept into the keychain once it becomes available
|
|
101
|
+
* (see sweepFallbackKeysToKeychain). Empty {} on keychain-capable systems. */
|
|
102
|
+
apiKeys?: Record<string, string>;
|
|
99
103
|
/** Master switch for automatic cloud uploads (usage stats, session
|
|
100
104
|
* transcripts, progress, memory notes). Default true; set false to opt out.
|
|
101
105
|
* The CODEEP_NO_TELEMETRY / DO_NOT_TRACK env vars also force it off. */
|
package/dist/config/index.js
CHANGED
|
@@ -186,6 +186,7 @@ function createConfig() {
|
|
|
186
186
|
providerApiKeys: [],
|
|
187
187
|
configuredProviderIds: [],
|
|
188
188
|
keysSecured: false,
|
|
189
|
+
apiKeys: {},
|
|
189
190
|
telemetry: true,
|
|
190
191
|
githubId: '',
|
|
191
192
|
githubUsername: '',
|
|
@@ -359,6 +360,44 @@ async function migrateKeysToSecureStorage() {
|
|
|
359
360
|
_migrationPromise = null;
|
|
360
361
|
}
|
|
361
362
|
}
|
|
363
|
+
let _sweepPromise = null;
|
|
364
|
+
/**
|
|
365
|
+
* If the OS keychain is now available but API keys are still sitting in the
|
|
366
|
+
* plaintext `apiKeys` fallback map (written by utils/keychain.ts while the
|
|
367
|
+
* keychain was unavailable on a prior run), move each into the keychain. The
|
|
368
|
+
* keychain write also deletes the entry from the plaintext fallback map, so a
|
|
369
|
+
* successful sweep leaves no plaintext behind. Cheap no-op when the map is
|
|
370
|
+
* empty or the keychain is unavailable; deduped for concurrent callers. Runs
|
|
371
|
+
* independently of `keysSecured` so a key written during a keychain outage is
|
|
372
|
+
* still swept up later.
|
|
373
|
+
*/
|
|
374
|
+
async function sweepFallbackKeysToKeychain() {
|
|
375
|
+
if (!_sweepPromise) {
|
|
376
|
+
_sweepPromise = (async () => {
|
|
377
|
+
const store = secureKeyStore();
|
|
378
|
+
// Only sweep when the keychain is actually usable; otherwise leave the
|
|
379
|
+
// plaintext fallback in place (there's nowhere safer to move it).
|
|
380
|
+
if (!store.isKeychainAvailable || !(await store.isKeychainAvailable()))
|
|
381
|
+
return;
|
|
382
|
+
const plaintext = config.get('apiKeys') || {};
|
|
383
|
+
for (const [providerId, apiKey] of Object.entries(plaintext)) {
|
|
384
|
+
if (typeof apiKey !== 'string' || !apiKey)
|
|
385
|
+
continue;
|
|
386
|
+
try {
|
|
387
|
+
await store.setApiKey(providerId, apiKey); // writes keychain + deletes from fallback map
|
|
388
|
+
addConfiguredProviderId(providerId);
|
|
389
|
+
}
|
|
390
|
+
catch { /* keep the plaintext entry on failure — never lose a key */ }
|
|
391
|
+
}
|
|
392
|
+
})();
|
|
393
|
+
}
|
|
394
|
+
try {
|
|
395
|
+
await _sweepPromise;
|
|
396
|
+
}
|
|
397
|
+
finally {
|
|
398
|
+
_sweepPromise = null;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
362
401
|
export const LANGUAGES = {
|
|
363
402
|
'auto': 'Auto-detect',
|
|
364
403
|
'en': 'English',
|
|
@@ -417,9 +456,11 @@ export async function loadApiKey(providerId) {
|
|
|
417
456
|
* Should be called at app startup
|
|
418
457
|
*/
|
|
419
458
|
export async function loadAllApiKeys() {
|
|
420
|
-
// Migrate any legacy plaintext keys, then
|
|
421
|
-
//
|
|
459
|
+
// Migrate any legacy plaintext keys, then sweep any keychain-fallback plaintext
|
|
460
|
+
// up into the keychain (no-op when none / keychain unavailable), then load all
|
|
461
|
+
// from secure storage using the non-secret configuredProviderIds index.
|
|
422
462
|
await migrateKeysToSecureStorage();
|
|
463
|
+
await sweepFallbackKeysToKeychain();
|
|
423
464
|
const store = secureKeyStore();
|
|
424
465
|
for (const providerId of (config.get('configuredProviderIds') || [])) {
|
|
425
466
|
const key = await store.getApiKey(providerId);
|
package/dist/renderer/main.js
CHANGED
|
@@ -387,7 +387,11 @@ async function main() {
|
|
|
387
387
|
// the review usage rather than the top-level help.
|
|
388
388
|
if (args[0] === 'review') {
|
|
389
389
|
const { runHeadlessReview } = await import('../utils/headlessReview.js');
|
|
390
|
-
process.exit(runHeadlessReview(args.slice(1)));
|
|
390
|
+
process.exit(await runHeadlessReview(args.slice(1)));
|
|
391
|
+
}
|
|
392
|
+
if (args[0] === 'hook') {
|
|
393
|
+
const { runHookCommand } = await import('../utils/gitHookInstaller.js');
|
|
394
|
+
process.exit(runHookCommand(args.slice(1)));
|
|
391
395
|
}
|
|
392
396
|
if (args.includes('--version') || args.includes('-v')) {
|
|
393
397
|
console.log(`Codeep v${getCurrentVersion()}`);
|
|
@@ -403,7 +407,9 @@ Usage:
|
|
|
403
407
|
codeep account sync Pull keys + personalities + commands + profile from codeep.dev
|
|
404
408
|
codeep account push Push local keys + personalities + commands + profile to codeep.dev
|
|
405
409
|
codeep acp Start ACP server (for Zed editor integration)
|
|
406
|
-
codeep review Offline code review for CI (--json, --fail-on
|
|
410
|
+
codeep review Offline code review for CI (--json, --fail-on, --rules, --ai)
|
|
411
|
+
codeep hook install Install a git pre-commit hook running \`codeep review\`
|
|
412
|
+
codeep hook uninstall Remove the Codeep git hook
|
|
407
413
|
codeep --version Show version
|
|
408
414
|
codeep --help Show this help
|
|
409
415
|
|
package/dist/utils/agent.d.ts
CHANGED
|
@@ -22,6 +22,14 @@ export type PermissionDecision = 'allow-once' | 'allow-always' | 'deny-once' | '
|
|
|
22
22
|
* invariant is unit-tested independently of the agent loop.
|
|
23
23
|
*/
|
|
24
24
|
export declare function classifyPermissionOutcome(outcome: string | undefined | null): PermissionDecision;
|
|
25
|
+
/**
|
|
26
|
+
* Build the set of tools that require a permission prompt this run. Derived from
|
|
27
|
+
* the global agentConfirm* settings, plus any `extra` tools forced in for this
|
|
28
|
+
* run only (ACP manual mode passes ['write_file','edit_file'] this way instead
|
|
29
|
+
* of mutating the global `agentConfirmWriteFile` config — which would leak the
|
|
30
|
+
* session's mode into the TUI and race on restore). Exported for unit testing.
|
|
31
|
+
*/
|
|
32
|
+
export declare function buildDangerousTools(extra?: string[]): Set<string>;
|
|
25
33
|
export interface AgentOptions {
|
|
26
34
|
maxIterations: number;
|
|
27
35
|
maxDuration: number;
|
|
@@ -34,6 +42,10 @@ export interface AgentOptions {
|
|
|
34
42
|
onTaskPlan?: (plan: TaskPlan) => void;
|
|
35
43
|
onTaskUpdate?: (task: SubTask) => void;
|
|
36
44
|
onRequestPermission?: (toolCall: ToolCall) => Promise<PermissionOutcome>;
|
|
45
|
+
/** Tool names to force into the per-run dangerous set, on top of the global
|
|
46
|
+
* agentConfirm* settings. ACP manual mode passes ['write_file','edit_file']
|
|
47
|
+
* here to gate them for THIS run only, instead of mutating global config. */
|
|
48
|
+
extraDangerousTools?: string[];
|
|
37
49
|
onExecuteCommand?: (command: string, args: string[], cwd: string) => Promise<{
|
|
38
50
|
stdout: string;
|
|
39
51
|
stderr: string;
|
package/dist/utils/agent.js
CHANGED
|
@@ -119,6 +119,23 @@ export function classifyPermissionOutcome(outcome) {
|
|
|
119
119
|
return 'deny-always';
|
|
120
120
|
return 'deny-once'; // 'reject_once' OR anything unexpected → fail closed
|
|
121
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* Build the set of tools that require a permission prompt this run. Derived from
|
|
124
|
+
* the global agentConfirm* settings, plus any `extra` tools forced in for this
|
|
125
|
+
* run only (ACP manual mode passes ['write_file','edit_file'] this way instead
|
|
126
|
+
* of mutating the global `agentConfirmWriteFile` config — which would leak the
|
|
127
|
+
* session's mode into the TUI and race on restore). Exported for unit testing.
|
|
128
|
+
*/
|
|
129
|
+
export function buildDangerousTools(extra = []) {
|
|
130
|
+
const tools = new Set([
|
|
131
|
+
...(config.get('agentConfirmDeleteFile') !== false ? ['delete_file'] : []),
|
|
132
|
+
...(config.get('agentConfirmExecuteCommand') !== false ? ['execute_command'] : []),
|
|
133
|
+
...(config.get('agentConfirmWriteFile') === true ? ['write_file', 'edit_file'] : []),
|
|
134
|
+
]);
|
|
135
|
+
for (const t of extra)
|
|
136
|
+
tools.add(t);
|
|
137
|
+
return tools;
|
|
138
|
+
}
|
|
122
139
|
/**
|
|
123
140
|
* Build the result for a run that paused at a safety limit. Pausing is a normal,
|
|
124
141
|
* resumable state — not an error — so the summary tells the user how to resume.
|
|
@@ -384,11 +401,7 @@ export async function runAgent(prompt, projectContext, options = {}) {
|
|
|
384
401
|
// Track tools permanently rejected this session via reject_always
|
|
385
402
|
const alwaysRejectedTools = new Set();
|
|
386
403
|
// Tools that require permission when onRequestPermission is set (configurable)
|
|
387
|
-
const dangerousTools =
|
|
388
|
-
...(config.get('agentConfirmDeleteFile') !== false ? ['delete_file'] : []),
|
|
389
|
-
...(config.get('agentConfirmExecuteCommand') !== false ? ['execute_command'] : []),
|
|
390
|
-
...(config.get('agentConfirmWriteFile') === true ? ['write_file', 'edit_file'] : []),
|
|
391
|
-
]);
|
|
404
|
+
const dangerousTools = buildDangerousTools(opts.extraDangerousTools);
|
|
392
405
|
// Delegation handler: run a named (or generic) sub-agent in its own fresh
|
|
393
406
|
// context and return its summary as the tool result. Reachable only when the
|
|
394
407
|
// `delegate` tool was advertised (depth 0). The sub-agent runs nested (no own
|
|
@@ -54,3 +54,24 @@ export declare function formatReviewResult(result: ReviewResult): string;
|
|
|
54
54
|
* Get review prompt for AI-enhanced review
|
|
55
55
|
*/
|
|
56
56
|
export declare function getReviewSystemPrompt(result: ReviewResult): string;
|
|
57
|
+
export interface BuiltinRuleInfo {
|
|
58
|
+
id: string;
|
|
59
|
+
category: ReviewCategory;
|
|
60
|
+
severity: ReviewIssue['severity'];
|
|
61
|
+
description: string;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Built-in rule metadata (id + what each flags) — the ids that
|
|
65
|
+
* `.codeep/review.{json,yml}` "disable" accepts. Includes the two line-count
|
|
66
|
+
* heuristics (long-file / long-function) that live outside CODE_PATTERNS.
|
|
67
|
+
*/
|
|
68
|
+
export declare function listBuiltinRules(): BuiltinRuleInfo[];
|
|
69
|
+
/**
|
|
70
|
+
* Append an advisory "AI second opinion" section to a markdown review report.
|
|
71
|
+
* Advisory only — it never changes the score or the exit code; the deterministic
|
|
72
|
+
* review remains authoritative.
|
|
73
|
+
*/
|
|
74
|
+
export declare function appendAiSection(markdown: string, aiText: string | null, meta?: {
|
|
75
|
+
provider?: string;
|
|
76
|
+
model?: string;
|
|
77
|
+
}): string;
|
package/dist/utils/codeReview.js
CHANGED
|
@@ -462,3 +462,34 @@ ${formatReviewResult(result)}
|
|
|
462
462
|
|
|
463
463
|
Be concise and practical. Focus on issues that matter most for code quality and maintainability.`;
|
|
464
464
|
}
|
|
465
|
+
/**
|
|
466
|
+
* Built-in rule metadata (id + what each flags) — the ids that
|
|
467
|
+
* `.codeep/review.{json,yml}` "disable" accepts. Includes the two line-count
|
|
468
|
+
* heuristics (long-file / long-function) that live outside CODE_PATTERNS.
|
|
469
|
+
*/
|
|
470
|
+
export function listBuiltinRules() {
|
|
471
|
+
return [
|
|
472
|
+
...CODE_PATTERNS.map((p) => ({
|
|
473
|
+
id: p.id,
|
|
474
|
+
category: p.category,
|
|
475
|
+
severity: p.severity,
|
|
476
|
+
description: p.message,
|
|
477
|
+
})),
|
|
478
|
+
{ id: 'long-file', category: 'maintainability', severity: 'info', description: 'File exceeds 500 lines — consider splitting into smaller modules' },
|
|
479
|
+
{ id: 'long-function', category: 'maintainability', severity: 'info', description: 'Function exceeds 50 lines — consider breaking it down' },
|
|
480
|
+
];
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Append an advisory "AI second opinion" section to a markdown review report.
|
|
484
|
+
* Advisory only — it never changes the score or the exit code; the deterministic
|
|
485
|
+
* review remains authoritative.
|
|
486
|
+
*/
|
|
487
|
+
export function appendAiSection(markdown, aiText, meta = {}) {
|
|
488
|
+
const label = meta.model
|
|
489
|
+
? `${meta.model}${meta.provider ? ` via ${meta.provider}` : ''}`
|
|
490
|
+
: (meta.provider || 'your provider');
|
|
491
|
+
if (!aiText || !aiText.trim()) {
|
|
492
|
+
return `${markdown}\n\n## AI Second Opinion\n_Skipped — no response from ${label}._`;
|
|
493
|
+
}
|
|
494
|
+
return `${markdown}\n\n## AI Second Opinion (${label})\n_Advisory — does not affect the score or exit code; the deterministic review above is authoritative._\n\n${aiText.trim()}`;
|
|
495
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { FailOn } from './headlessReview.js';
|
|
2
|
+
export type HookType = 'pre-commit' | 'pre-push';
|
|
3
|
+
export type HookAction = 'install' | 'uninstall' | 'help';
|
|
4
|
+
export interface HookArgs {
|
|
5
|
+
action: HookAction;
|
|
6
|
+
hookType: HookType;
|
|
7
|
+
failOn: FailOn;
|
|
8
|
+
help: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare const HOOK_HELP = "Usage: codeep hook <install|uninstall> [options]\n\nInstall a git hook that runs `codeep review` on your changes, blocking the\ncommit/push when issues at/above the threshold are found. Honors a project's\n.codeep/review.yml (or .json).\n\nActions:\n install Install the hook (pre-commit by default)\n uninstall Remove the Codeep-managed hook\n\nOptions:\n --pre-push Manage the pre-push hook instead of pre-commit\n --fail-on <level> Severity that blocks: error | warning | info | none (default: error)\n -h, --help Show this help\n\nThe pre-commit hook reviews the working-tree content of staged files, so stage\nyour changes fully before committing for the most accurate result.\nCodeep never overwrites a pre-existing hook it didn't create.";
|
|
11
|
+
/** Parse argv after `hook`. Pure. */
|
|
12
|
+
export declare function parseHookArgs(argv: string[]): HookArgs;
|
|
13
|
+
/** The hook script body. Pure. pre-commit scopes to staged files; pre-push reviews changes. */
|
|
14
|
+
export declare function buildHookScript(hookType: HookType, failOn: FailOn): string;
|
|
15
|
+
/** True when a hook file was created by Codeep (safe to overwrite/remove). */
|
|
16
|
+
export declare function isCodeepHook(content: string): boolean;
|
|
17
|
+
/** Resolve the git hooks directory (honors worktrees + core.hooksPath). Null if not a repo. */
|
|
18
|
+
export declare function resolveHooksDir(cwd: string): string | null;
|
|
19
|
+
export interface HookDeps {
|
|
20
|
+
resolveHooksDir: (cwd: string) => string | null;
|
|
21
|
+
readHook: (path: string) => string | null;
|
|
22
|
+
writeHook: (path: string, content: string) => void;
|
|
23
|
+
removeHook: (path: string) => void;
|
|
24
|
+
write: (text: string) => void;
|
|
25
|
+
}
|
|
26
|
+
export declare function runHookCommand(argv: string[], deps?: HookDeps): number;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// `codeep hook install|uninstall` — installs a GIT hook (pre-commit / pre-push)
|
|
2
|
+
// that runs the offline reviewer on your changes and blocks the commit/push when
|
|
3
|
+
// issues at/above the threshold are found. Honors .codeep/review.{yml,json}.
|
|
4
|
+
//
|
|
5
|
+
// This is a GIT-hook installer — distinct from the lifecycle SHELL hooks in
|
|
6
|
+
// utils/hooks.ts (.codeep/hooks/<event>.sh) and the React hook in src/hooks/.
|
|
7
|
+
import { readFileSync, writeFileSync, mkdirSync, chmodSync, rmSync } from 'fs';
|
|
8
|
+
import { join, dirname, isAbsolute } from 'path';
|
|
9
|
+
import { execSync } from 'child_process';
|
|
10
|
+
const FAIL_ON_VALUES = ['error', 'warning', 'info', 'none'];
|
|
11
|
+
const MARKER_START = '# >>> codeep hook >>>';
|
|
12
|
+
const MARKER_END = '# <<< codeep hook <<<';
|
|
13
|
+
export const HOOK_HELP = `Usage: codeep hook <install|uninstall> [options]
|
|
14
|
+
|
|
15
|
+
Install a git hook that runs \`codeep review\` on your changes, blocking the
|
|
16
|
+
commit/push when issues at/above the threshold are found. Honors a project's
|
|
17
|
+
.codeep/review.yml (or .json).
|
|
18
|
+
|
|
19
|
+
Actions:
|
|
20
|
+
install Install the hook (pre-commit by default)
|
|
21
|
+
uninstall Remove the Codeep-managed hook
|
|
22
|
+
|
|
23
|
+
Options:
|
|
24
|
+
--pre-push Manage the pre-push hook instead of pre-commit
|
|
25
|
+
--fail-on <level> Severity that blocks: error | warning | info | none (default: error)
|
|
26
|
+
-h, --help Show this help
|
|
27
|
+
|
|
28
|
+
The pre-commit hook reviews the working-tree content of staged files, so stage
|
|
29
|
+
your changes fully before committing for the most accurate result.
|
|
30
|
+
Codeep never overwrites a pre-existing hook it didn't create.`;
|
|
31
|
+
/** Parse argv after `hook`. Pure. */
|
|
32
|
+
export function parseHookArgs(argv) {
|
|
33
|
+
const out = { action: 'help', hookType: 'pre-commit', failOn: 'error', help: false };
|
|
34
|
+
let i = 0;
|
|
35
|
+
if (argv[0] === 'install' || argv[0] === 'uninstall') {
|
|
36
|
+
out.action = argv[0];
|
|
37
|
+
i = 1;
|
|
38
|
+
}
|
|
39
|
+
for (; i < argv.length; i++) {
|
|
40
|
+
const a = argv[i];
|
|
41
|
+
if (a === '--pre-push')
|
|
42
|
+
out.hookType = 'pre-push';
|
|
43
|
+
else if (a === '--pre-commit')
|
|
44
|
+
out.hookType = 'pre-commit';
|
|
45
|
+
else if (a === '-h' || a === '--help')
|
|
46
|
+
out.help = true;
|
|
47
|
+
else if (a === '--fail-on') {
|
|
48
|
+
const v = argv[++i];
|
|
49
|
+
if (FAIL_ON_VALUES.includes(v))
|
|
50
|
+
out.failOn = v;
|
|
51
|
+
}
|
|
52
|
+
else if (a.startsWith('--fail-on=')) {
|
|
53
|
+
const v = a.slice('--fail-on='.length);
|
|
54
|
+
if (FAIL_ON_VALUES.includes(v))
|
|
55
|
+
out.failOn = v;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
/** The hook script body. Pure. pre-commit scopes to staged files; pre-push reviews changes. */
|
|
61
|
+
export function buildHookScript(hookType, failOn) {
|
|
62
|
+
const lines = [
|
|
63
|
+
'#!/bin/sh',
|
|
64
|
+
MARKER_START,
|
|
65
|
+
'# Managed by Codeep: `codeep hook install` to update, `codeep hook uninstall` to remove.',
|
|
66
|
+
'command -v codeep >/dev/null 2>&1 || exit 0', // no codeep on PATH → skip, don't block
|
|
67
|
+
];
|
|
68
|
+
if (hookType === 'pre-commit') {
|
|
69
|
+
// NUL-delimited + xargs -0 so staged paths with spaces/metachars survive as
|
|
70
|
+
// single arguments (an unquoted $files would word-split and silently skip
|
|
71
|
+
// them — a fail-open gate). Temp file keeps it portable: BSD/macOS xargs has
|
|
72
|
+
// no `-r`, so we guard emptiness with `[ -s ]` instead.
|
|
73
|
+
lines.push('tmp=$(mktemp)');
|
|
74
|
+
lines.push('git diff --cached --name-only -z --diff-filter=ACMR > "$tmp"');
|
|
75
|
+
lines.push('if [ -s "$tmp" ]; then');
|
|
76
|
+
lines.push(` xargs -0 codeep review --fail-on ${failOn} < "$tmp"`);
|
|
77
|
+
lines.push(' status=$?');
|
|
78
|
+
lines.push('else');
|
|
79
|
+
lines.push(' status=0');
|
|
80
|
+
lines.push('fi');
|
|
81
|
+
lines.push('rm -f "$tmp"');
|
|
82
|
+
lines.push('exit $status');
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
lines.push(`codeep review --fail-on ${failOn}`);
|
|
86
|
+
}
|
|
87
|
+
lines.push(MARKER_END, '');
|
|
88
|
+
return lines.join('\n');
|
|
89
|
+
}
|
|
90
|
+
/** True when a hook file was created by Codeep (safe to overwrite/remove). */
|
|
91
|
+
export function isCodeepHook(content) {
|
|
92
|
+
return content.includes(MARKER_START);
|
|
93
|
+
}
|
|
94
|
+
/** Resolve the git hooks directory (honors worktrees + core.hooksPath). Null if not a repo. */
|
|
95
|
+
export function resolveHooksDir(cwd) {
|
|
96
|
+
try {
|
|
97
|
+
execSync('git rev-parse --is-inside-work-tree', { cwd, stdio: 'ignore' });
|
|
98
|
+
const hooks = execSync('git rev-parse --git-path hooks', { cwd, encoding: 'utf8' }).trim();
|
|
99
|
+
return isAbsolute(hooks) ? hooks : join(cwd, hooks);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
export function runHookCommand(argv, deps = defaultHookDeps()) {
|
|
106
|
+
const args = parseHookArgs(argv);
|
|
107
|
+
if (args.help || args.action === 'help') {
|
|
108
|
+
deps.write(HOOK_HELP);
|
|
109
|
+
return 0;
|
|
110
|
+
}
|
|
111
|
+
const hooksDir = deps.resolveHooksDir(process.cwd());
|
|
112
|
+
if (!hooksDir) {
|
|
113
|
+
deps.write('Not a git repository — run `codeep hook` inside a repo.');
|
|
114
|
+
return 1;
|
|
115
|
+
}
|
|
116
|
+
const target = join(hooksDir, args.hookType);
|
|
117
|
+
const existing = deps.readHook(target);
|
|
118
|
+
if (args.action === 'uninstall') {
|
|
119
|
+
if (existing === null) {
|
|
120
|
+
deps.write(`No ${args.hookType} hook to remove.`);
|
|
121
|
+
return 0;
|
|
122
|
+
}
|
|
123
|
+
if (!isCodeepHook(existing)) {
|
|
124
|
+
deps.write(`Refusing to remove ${args.hookType}: it was not created by Codeep.`);
|
|
125
|
+
return 1;
|
|
126
|
+
}
|
|
127
|
+
deps.removeHook(target);
|
|
128
|
+
deps.write(`Removed the Codeep ${args.hookType} hook.`);
|
|
129
|
+
return 0;
|
|
130
|
+
}
|
|
131
|
+
// install
|
|
132
|
+
if (existing !== null && !isCodeepHook(existing)) {
|
|
133
|
+
deps.write(`A ${args.hookType} hook already exists and was not created by Codeep — refusing to overwrite it. Remove it first, or add a \`codeep review\` call manually.`);
|
|
134
|
+
return 1;
|
|
135
|
+
}
|
|
136
|
+
deps.writeHook(target, buildHookScript(args.hookType, args.failOn));
|
|
137
|
+
const when = args.hookType === 'pre-commit' ? 'commit' : 'push';
|
|
138
|
+
deps.write(`Installed the Codeep ${args.hookType} hook → runs \`codeep review --fail-on ${args.failOn}\` on each ${when}.`);
|
|
139
|
+
return 0;
|
|
140
|
+
}
|
|
141
|
+
function defaultHookDeps() {
|
|
142
|
+
return {
|
|
143
|
+
resolveHooksDir,
|
|
144
|
+
readHook: (p) => {
|
|
145
|
+
try {
|
|
146
|
+
return readFileSync(p, 'utf8');
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
writeHook: (p, content) => {
|
|
153
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
154
|
+
writeFileSync(p, content, { mode: 0o755 });
|
|
155
|
+
try {
|
|
156
|
+
chmodSync(p, 0o755);
|
|
157
|
+
}
|
|
158
|
+
catch { /* best effort (e.g. Windows) */ }
|
|
159
|
+
},
|
|
160
|
+
removeHook: (p) => {
|
|
161
|
+
try {
|
|
162
|
+
rmSync(p);
|
|
163
|
+
}
|
|
164
|
+
catch { /* ignore */ }
|
|
165
|
+
},
|
|
166
|
+
write: (t) => process.stdout.write(t + '\n'),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
@@ -4,21 +4,35 @@ export interface ReviewArgs {
|
|
|
4
4
|
files: string[];
|
|
5
5
|
json: boolean;
|
|
6
6
|
failOn: FailOn;
|
|
7
|
+
rules: boolean;
|
|
8
|
+
ai: boolean;
|
|
7
9
|
help: boolean;
|
|
8
10
|
}
|
|
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.";
|
|
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.";
|
|
10
12
|
/** Parse `codeep review` argv (everything after the subcommand). Pure. */
|
|
11
13
|
export declare function parseReviewArgs(argv: string[]): ReviewArgs;
|
|
12
14
|
/** Exit code for a result under a fail-on threshold. Pure. */
|
|
13
15
|
export declare function exitCodeForResult(result: ReviewResult, failOn: FailOn): number;
|
|
16
|
+
/** Render the built-in rule ids for `--rules`. Pure. */
|
|
17
|
+
export declare function formatBuiltinRules(): string;
|
|
14
18
|
export interface ReviewDeps {
|
|
15
19
|
/** Run the review over optional specific files. */
|
|
16
20
|
review: (files?: string[]) => ReviewResult;
|
|
17
21
|
/** Sink for the report (one call). */
|
|
18
22
|
write: (text: string) => void;
|
|
23
|
+
/** List of built-in rule ids (for --rules). */
|
|
24
|
+
listRules: () => string;
|
|
25
|
+
/** Optional AI second opinion; returns null when unavailable (no key / error). */
|
|
26
|
+
aiReview: (result: ReviewResult) => Promise<string | null>;
|
|
27
|
+
/** Provider/model label for the AI section header. */
|
|
28
|
+
aiMeta: () => {
|
|
29
|
+
provider?: string;
|
|
30
|
+
model?: string;
|
|
31
|
+
};
|
|
19
32
|
}
|
|
20
33
|
/**
|
|
21
34
|
* Orchestrate a headless review and return the process exit code. Side effects
|
|
22
|
-
* (filesystem, stdout) live behind `deps` so the flow is
|
|
35
|
+
* (filesystem, stdout, provider call) live behind `deps` so the flow is
|
|
36
|
+
* unit-testable. The exit code is ALWAYS deterministic — `--ai` is advisory.
|
|
23
37
|
*/
|
|
24
|
-
export declare function runHeadlessReview(argv: string[], deps?: ReviewDeps): number
|
|
38
|
+
export declare function runHeadlessReview(argv: string[], deps?: ReviewDeps): Promise<number>;
|
|
@@ -2,7 +2,12 @@
|
|
|
2
2
|
// deterministic reviewer in codeReview.ts. No API key, no TUI: it scans, prints
|
|
3
3
|
// a report (markdown or JSON), and exits non-zero when issues at/above a chosen
|
|
4
4
|
// severity are found, so it drops cleanly into CI (e.g. a GitHub Action).
|
|
5
|
-
|
|
5
|
+
//
|
|
6
|
+
// `--ai` is the one opt-in online mode: after the offline pass it asks the
|
|
7
|
+
// configured provider for a contextual second opinion (advisory only — it never
|
|
8
|
+
// affects the exit code), and degrades to deterministic-only when no key is set.
|
|
9
|
+
import { performCodeReview, formatReviewResult, getReviewSystemPrompt, listBuiltinRules, appendAiSection, } from './codeReview.js';
|
|
10
|
+
import { config, getCurrentProvider } from '../config/index.js';
|
|
6
11
|
const FAIL_ON_VALUES = ['error', 'warning', 'info', 'none'];
|
|
7
12
|
// Higher = more severe. `suggestion` sits below `info` so `--fail-on info`
|
|
8
13
|
// never trips on a mere suggestion.
|
|
@@ -13,21 +18,33 @@ Run a deterministic, offline code review (no API key required). With no files,
|
|
|
13
18
|
reviews your unstaged git changes, falling back to a src/ scan when the tree is
|
|
14
19
|
clean. Pass files (or let your CI pass the PR's changed files) to scope it.
|
|
15
20
|
|
|
21
|
+
Custom/disabled rules come from .codeep/review.yml (or .json) in the repo.
|
|
22
|
+
|
|
16
23
|
Options:
|
|
17
24
|
--json Print the result as JSON instead of the markdown report
|
|
18
25
|
--fail-on <level> Exit non-zero when an issue at or above <level> is found:
|
|
19
26
|
error | warning | info | none (default: error)
|
|
27
|
+
--rules List the built-in rule ids (for "disable" in .codeep/review.*) and exit
|
|
28
|
+
--ai After the offline pass, ask your configured provider for a
|
|
29
|
+
contextual second opinion on the working-tree diff
|
|
30
|
+
(advisory; needs an API key; never affects the exit code)
|
|
20
31
|
-h, --help Show this help
|
|
21
32
|
|
|
22
33
|
Exit code: 0 when nothing at/above --fail-on is found, 1 otherwise.`;
|
|
23
34
|
/** Parse `codeep review` argv (everything after the subcommand). Pure. */
|
|
24
35
|
export function parseReviewArgs(argv) {
|
|
25
|
-
const out = { files: [], json: false, failOn: 'error', help: false };
|
|
36
|
+
const out = { files: [], json: false, failOn: 'error', rules: false, ai: false, help: false };
|
|
26
37
|
for (let i = 0; i < argv.length; i++) {
|
|
27
38
|
const arg = argv[i];
|
|
28
39
|
if (arg === '--json') {
|
|
29
40
|
out.json = true;
|
|
30
41
|
}
|
|
42
|
+
else if (arg === '--rules') {
|
|
43
|
+
out.rules = true;
|
|
44
|
+
}
|
|
45
|
+
else if (arg === '--ai') {
|
|
46
|
+
out.ai = true;
|
|
47
|
+
}
|
|
31
48
|
else if (arg === '-h' || arg === '--help') {
|
|
32
49
|
out.help = true;
|
|
33
50
|
}
|
|
@@ -56,18 +73,44 @@ export function exitCodeForResult(result, failOn) {
|
|
|
56
73
|
const tripped = result.issues.some((i) => (SEVERITY_RANK[i.severity] ?? 0) >= threshold);
|
|
57
74
|
return tripped ? 1 : 0;
|
|
58
75
|
}
|
|
76
|
+
/** Render the built-in rule ids for `--rules`. Pure. */
|
|
77
|
+
export function formatBuiltinRules() {
|
|
78
|
+
const rules = listBuiltinRules();
|
|
79
|
+
const idW = Math.max(...rules.map((r) => r.id.length));
|
|
80
|
+
const sevW = Math.max(...rules.map((r) => r.severity.length));
|
|
81
|
+
const out = [
|
|
82
|
+
'Built-in review rules — put any id in "disable" in .codeep/review.yml|json:',
|
|
83
|
+
'',
|
|
84
|
+
];
|
|
85
|
+
for (const r of rules) {
|
|
86
|
+
out.push(` ${r.id.padEnd(idW)} ${r.severity.padEnd(sevW)} ${r.description}`);
|
|
87
|
+
}
|
|
88
|
+
return out.join('\n');
|
|
89
|
+
}
|
|
59
90
|
/**
|
|
60
91
|
* Orchestrate a headless review and return the process exit code. Side effects
|
|
61
|
-
* (filesystem, stdout) live behind `deps` so the flow is
|
|
92
|
+
* (filesystem, stdout, provider call) live behind `deps` so the flow is
|
|
93
|
+
* unit-testable. The exit code is ALWAYS deterministic — `--ai` is advisory.
|
|
62
94
|
*/
|
|
63
|
-
export function runHeadlessReview(argv, deps = defaultDeps()) {
|
|
95
|
+
export async function runHeadlessReview(argv, deps = defaultDeps()) {
|
|
64
96
|
const args = parseReviewArgs(argv);
|
|
65
97
|
if (args.help) {
|
|
66
98
|
deps.write(REVIEW_HELP);
|
|
67
99
|
return 0;
|
|
68
100
|
}
|
|
101
|
+
if (args.rules) {
|
|
102
|
+
deps.write(deps.listRules());
|
|
103
|
+
return 0;
|
|
104
|
+
}
|
|
69
105
|
const result = deps.review(args.files.length ? args.files : undefined);
|
|
70
|
-
|
|
106
|
+
const aiText = args.ai ? await deps.aiReview(result) : null;
|
|
107
|
+
if (args.json) {
|
|
108
|
+
deps.write(JSON.stringify(args.ai ? { ...result, aiReview: aiText } : result, null, 2));
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
const md = formatReviewResult(result);
|
|
112
|
+
deps.write(args.ai ? appendAiSection(md, aiText, deps.aiMeta()) : md);
|
|
113
|
+
}
|
|
71
114
|
return exitCodeForResult(result, args.failOn);
|
|
72
115
|
}
|
|
73
116
|
// Only the reviewer's `.root` is read, so a minimal context rooted at cwd is
|
|
@@ -84,8 +127,41 @@ function minimalContext(root) {
|
|
|
84
127
|
};
|
|
85
128
|
}
|
|
86
129
|
function defaultDeps() {
|
|
130
|
+
const cwd = process.cwd();
|
|
87
131
|
return {
|
|
88
|
-
review: (files) => performCodeReview(minimalContext(
|
|
132
|
+
review: (files) => performCodeReview(minimalContext(cwd), files),
|
|
89
133
|
write: (text) => process.stdout.write(text + '\n'),
|
|
134
|
+
listRules: () => formatBuiltinRules(),
|
|
135
|
+
aiMeta: () => {
|
|
136
|
+
try {
|
|
137
|
+
return { provider: getCurrentProvider().name, model: String(config.get('model') || '') };
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return {};
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
aiReview: async (result) => {
|
|
144
|
+
try {
|
|
145
|
+
const { loadAllApiKeys, isConfigured } = await import('../config/index.js');
|
|
146
|
+
await loadAllApiKeys();
|
|
147
|
+
if (!isConfigured()) {
|
|
148
|
+
process.stderr.write('codeep review --ai: no API key configured for the current provider; skipping the AI pass (deterministic results below).\n');
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
const { getGitDiff } = await import('./git.js');
|
|
152
|
+
const { chat } = await import('../api/index.js');
|
|
153
|
+
const d = getGitDiff(false, cwd);
|
|
154
|
+
const diff = d.success && d.diff ? d.diff.slice(0, 50000) : '(no textual diff available — reviewing the deterministic findings only)';
|
|
155
|
+
// Fold the prompt + findings into the user message (not a system-role
|
|
156
|
+
// history entry): chat() does not hoist a history `system` entry into
|
|
157
|
+
// the Anthropic top-level `system` field, so it would be dropped on
|
|
158
|
+
// Anthropic-protocol providers. As the user message it reaches every provider.
|
|
159
|
+
const message = `${getReviewSystemPrompt(result)}\n\n## Diff under review\n\`\`\`diff\n${diff}\n\`\`\``;
|
|
160
|
+
return await chat(message, []);
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return null; // never hard-fail the review on an AI error
|
|
164
|
+
}
|
|
165
|
+
},
|
|
90
166
|
};
|
|
91
167
|
}
|
package/dist/utils/keychain.d.ts
CHANGED
|
@@ -9,6 +9,9 @@ export interface SecureStorage {
|
|
|
9
9
|
setApiKey(providerId: string, apiKey: string): Promise<void>;
|
|
10
10
|
deleteApiKey(providerId: string): Promise<void>;
|
|
11
11
|
hasApiKey(providerId: string): Promise<boolean>;
|
|
12
|
+
/** Whether the OS keychain is usable (vs the plaintext-config fallback).
|
|
13
|
+
* Optional — only SmartStorage (what createSecureStorage returns) implements it. */
|
|
14
|
+
isKeychainAvailable?(): Promise<boolean>;
|
|
12
15
|
}
|
|
13
16
|
/**
|
|
14
17
|
* Migrate existing plain-text API keys to keychain
|
package/dist/utils/keychain.js
CHANGED
|
@@ -124,6 +124,10 @@ class SmartStorage {
|
|
|
124
124
|
}
|
|
125
125
|
this.keychainTested = true;
|
|
126
126
|
}
|
|
127
|
+
async isKeychainAvailable() {
|
|
128
|
+
await this.ensureKeychainTested();
|
|
129
|
+
return this.useKeychain;
|
|
130
|
+
}
|
|
127
131
|
async getApiKey(providerId) {
|
|
128
132
|
await this.ensureKeychainTested();
|
|
129
133
|
if (this.useKeychain) {
|
|
@@ -23,7 +23,10 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import { existsSync, readFileSync } from 'fs';
|
|
25
25
|
import { join } from 'path';
|
|
26
|
-
|
|
26
|
+
import yaml from 'js-yaml';
|
|
27
|
+
// Candidate config files, in precedence order: YAML preferred (nicer for a
|
|
28
|
+
// human-authored rules file — comments, single-quoted regex), JSON as fallback.
|
|
29
|
+
const CONFIG_PATHS = ['.codeep/review.yml', '.codeep/review.yaml', '.codeep/review.json'];
|
|
27
30
|
const MAX_RULES = 200;
|
|
28
31
|
const VALID_CATEGORIES = [
|
|
29
32
|
'security', 'performance', 'maintainability', 'bug', 'style', 'types', 'best-practice', 'documentation',
|
|
@@ -53,19 +56,35 @@ function asStringArray(v) {
|
|
|
53
56
|
: [];
|
|
54
57
|
}
|
|
55
58
|
export function loadReviewConfig(projectRoot) {
|
|
56
|
-
|
|
57
|
-
|
|
59
|
+
// First existing candidate wins (.yml > .yaml > .json).
|
|
60
|
+
let chosen = null;
|
|
61
|
+
let text = '';
|
|
62
|
+
for (const candidate of CONFIG_PATHS) {
|
|
63
|
+
const fp = join(projectRoot, candidate);
|
|
64
|
+
if (existsSync(fp)) {
|
|
65
|
+
chosen = candidate;
|
|
66
|
+
try {
|
|
67
|
+
text = readFileSync(fp, 'utf-8');
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (!chosen)
|
|
58
76
|
return null;
|
|
77
|
+
const isJson = chosen.endsWith('.json');
|
|
59
78
|
let data;
|
|
60
79
|
try {
|
|
61
|
-
data = JSON.parse(
|
|
80
|
+
data = isJson ? JSON.parse(text) : yaml.load(text);
|
|
62
81
|
}
|
|
63
82
|
catch {
|
|
64
|
-
console.warn(`[codeep] Ignoring ${
|
|
83
|
+
console.warn(`[codeep] Ignoring ${chosen}: not valid ${isJson ? 'JSON' : 'YAML'}.`);
|
|
65
84
|
return null;
|
|
66
85
|
}
|
|
67
86
|
if (!data || typeof data !== 'object' || Array.isArray(data)) {
|
|
68
|
-
console.warn(`[codeep] Ignoring ${
|
|
87
|
+
console.warn(`[codeep] Ignoring ${chosen}: expected a top-level object.`);
|
|
69
88
|
return null;
|
|
70
89
|
}
|
|
71
90
|
const cfg = data;
|
|
@@ -82,12 +101,12 @@ export function loadReviewConfig(projectRoot) {
|
|
|
82
101
|
const message = typeof r.message === 'string' && r.message.trim() ? r.message.trim() : null;
|
|
83
102
|
const patternSrc = typeof r.pattern === 'string' && r.pattern ? r.pattern : null;
|
|
84
103
|
if (!id || !message || !patternSrc) {
|
|
85
|
-
console.warn(`[codeep] Skipping a rule in ${
|
|
104
|
+
console.warn(`[codeep] Skipping a rule in ${chosen}: each rule needs id, pattern and message.`);
|
|
86
105
|
continue;
|
|
87
106
|
}
|
|
88
107
|
// Reject oversized patterns outright — keeps regex compilation/run bounded.
|
|
89
108
|
if (patternSrc.length > 1000) {
|
|
90
|
-
console.warn(`[codeep] Skipping rule "${id}" in ${
|
|
109
|
+
console.warn(`[codeep] Skipping rule "${id}" in ${chosen}: pattern is too long (>1000 chars).`);
|
|
91
110
|
continue;
|
|
92
111
|
}
|
|
93
112
|
// Conservative ReDoS screen: reject the classic catastrophic shape — a group
|
|
@@ -95,7 +114,7 @@ export function loadReviewConfig(projectRoot) {
|
|
|
95
114
|
// (\d*)*, (.*)+, (x+){2,}. Not exhaustive (the GitHub Action also bounds
|
|
96
115
|
// wall-clock), but it blocks the common foot-guns in an untrusted review.json.
|
|
97
116
|
if (/\([^)]*[+*]\)\s*[+*{]/.test(patternSrc)) {
|
|
98
|
-
console.warn(`[codeep] Skipping rule "${id}" in ${
|
|
117
|
+
console.warn(`[codeep] Skipping rule "${id}" in ${chosen}: nested quantifiers risk catastrophic backtracking (ReDoS).`);
|
|
99
118
|
continue;
|
|
100
119
|
}
|
|
101
120
|
// Always include the global flag so every match in a file is found.
|
|
@@ -107,7 +126,7 @@ export function loadReviewConfig(projectRoot) {
|
|
|
107
126
|
pattern = new RegExp(patternSrc, flags);
|
|
108
127
|
}
|
|
109
128
|
catch {
|
|
110
|
-
console.warn(`[codeep] Skipping rule "${id}" in ${
|
|
129
|
+
console.warn(`[codeep] Skipping rule "${id}" in ${chosen}: invalid regex.`);
|
|
111
130
|
continue;
|
|
112
131
|
}
|
|
113
132
|
const category = VALID_CATEGORIES.includes(r.category)
|
package/dist/utils/update.d.ts
CHANGED
|
@@ -5,7 +5,9 @@ export interface VersionInfo {
|
|
|
5
5
|
error?: string;
|
|
6
6
|
}
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
8
|
+
* Current Codeep version. Uses the build-time-baked VERSION constant (works in
|
|
9
|
+
* the bun-compiled binary, which has no package.json on disk), falling back to
|
|
10
|
+
* reading package.json for an unbuilt dev tree.
|
|
9
11
|
*/
|
|
10
12
|
export declare function getCurrentVersion(): string;
|
|
11
13
|
/**
|
package/dist/utils/update.js
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import { readFileSync } from 'fs';
|
|
2
2
|
import { join, dirname } from 'path';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
|
+
import { VERSION } from '../version.js';
|
|
4
5
|
const __filename = fileURLToPath(import.meta.url);
|
|
5
6
|
const __dirname = dirname(__filename);
|
|
6
7
|
/**
|
|
7
|
-
*
|
|
8
|
+
* Current Codeep version. Uses the build-time-baked VERSION constant (works in
|
|
9
|
+
* the bun-compiled binary, which has no package.json on disk), falling back to
|
|
10
|
+
* reading package.json for an unbuilt dev tree.
|
|
8
11
|
*/
|
|
9
12
|
export function getCurrentVersion() {
|
|
13
|
+
if (VERSION)
|
|
14
|
+
return VERSION;
|
|
10
15
|
try {
|
|
11
|
-
// In built version, package.json is in parent directory
|
|
12
16
|
const packagePath = join(__dirname, '../../package.json');
|
|
13
17
|
const packageJson = JSON.parse(readFileSync(packagePath, 'utf-8'));
|
|
14
18
|
return packageJson.version;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const VERSION = "2.7.0";
|
package/dist/version.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codeep",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.7.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",
|
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
"codeep": "bin/codeep.js"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
|
-
"dev": "node --import tsx src/renderer/main.ts",
|
|
12
|
-
"prepack": "tsc; node scripts/fix-imports.js",
|
|
13
|
-
"build": "tsc && node scripts/fix-imports.js",
|
|
11
|
+
"dev": "node scripts/gen-version.js && node --import tsx src/renderer/main.ts",
|
|
12
|
+
"prepack": "node scripts/gen-version.js && tsc; node scripts/fix-imports.js",
|
|
13
|
+
"build": "node scripts/gen-version.js && tsc && node scripts/fix-imports.js",
|
|
14
14
|
"start": "node dist/renderer/main.js",
|
|
15
15
|
"demo:renderer": "node --import tsx src/renderer/demo.ts",
|
|
16
16
|
"demo:app": "node --import tsx src/renderer/demo-app.ts",
|
|
@@ -42,10 +42,12 @@
|
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"clipboardy": "^4.0.0",
|
|
44
44
|
"conf": "^12.0.0",
|
|
45
|
+
"js-yaml": "^4.1.0",
|
|
45
46
|
"keytar": "^7.9.0",
|
|
46
47
|
"open": "^10.0.0"
|
|
47
48
|
},
|
|
48
49
|
"devDependencies": {
|
|
50
|
+
"@types/js-yaml": "^4.0.9",
|
|
49
51
|
"@types/node": "^20.10.0",
|
|
50
52
|
"@vitest/coverage-v8": "^4.0.18",
|
|
51
53
|
"pkg": "^5.8.1",
|