codeep 2.5.1 → 2.6.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 +59 -0
- package/dist/acp/commands.js +48 -8
- package/dist/acp/server.js +20 -8
- package/dist/config/index.d.ts +26 -3
- package/dist/config/index.js +148 -65
- package/dist/renderer/App.js +2 -1
- package/dist/renderer/commands.js +39 -5
- package/dist/renderer/main.js +22 -4
- package/dist/utils/agent.d.ts +9 -0
- package/dist/utils/agent.js +25 -6
- package/dist/utils/codeReview.d.ts +14 -0
- package/dist/utils/codeReview.js +67 -9
- package/dist/utils/codeepCloud.js +13 -1
- package/dist/utils/keychain.js +34 -12
- package/dist/utils/reviewConfig.d.ts +10 -0
- package/dist/utils/reviewConfig.js +129 -0
- package/package.json +1 -1
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* decoupled from global state. Import-heavy commands use dynamic imports
|
|
6
6
|
* to keep startup time low.
|
|
7
7
|
*/
|
|
8
|
-
import { config, getCurrentProvider, getModelsForCurrentProvider, PROTOCOLS, LANGUAGES, setProvider, setApiKey, clearApiKey, getApiKey, saveSession, startNewSession, loadSession, listSessionsWithInfo, deleteSession, renameSession, setProjectPermission, saveProfile, loadProfile, applyProfile, listProfiles, deleteProfile, initializeAsProject, isManuallyInitializedProject, } from '../config/index.js';
|
|
8
|
+
import { config, getCurrentProvider, getModelsForCurrentProvider, PROTOCOLS, LANGUAGES, setProvider, setApiKey, clearApiKey, getApiKey, isTelemetryEnabled, telemetryForcedOffByEnv, saveSession, startNewSession, loadSession, listSessionsWithInfo, deleteSession, renameSession, setProjectPermission, saveProfile, loadProfile, applyProfile, listProfiles, deleteProfile, initializeAsProject, isManuallyInitializedProject, } from '../config/index.js';
|
|
9
9
|
import { getProjectContext } from '../utils/project.js';
|
|
10
10
|
import { getCurrentVersion } from '../utils/update.js';
|
|
11
11
|
import { getProviderList, getProvider } from '../config/providers.js';
|
|
@@ -245,6 +245,35 @@ export async function handleCommand(command, args, ctx) {
|
|
|
245
245
|
ctx.app.addMessage({ role: 'system', content: lines.join('\n') });
|
|
246
246
|
break;
|
|
247
247
|
}
|
|
248
|
+
case 'telemetry': {
|
|
249
|
+
const sub = args[0]?.toLowerCase();
|
|
250
|
+
const envOff = telemetryForcedOffByEnv();
|
|
251
|
+
if (sub === 'on' || sub === 'off') {
|
|
252
|
+
if (envOff) {
|
|
253
|
+
ctx.app.notify('Telemetry is forced OFF by CODEEP_NO_TELEMETRY / DO_NOT_TRACK — unset that env var to change it.');
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
config.set('telemetry', sub === 'on');
|
|
257
|
+
ctx.app.notify(sub === 'on'
|
|
258
|
+
? 'Telemetry on — usage stats, transcripts, progress & notes sync to codeep.dev.'
|
|
259
|
+
: 'Telemetry off — no automatic cloud uploads.');
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
if (sub && sub !== 'status') {
|
|
263
|
+
ctx.app.notify('Usage: /telemetry · /telemetry on · /telemetry off');
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
const flag = config.get('telemetry') !== false;
|
|
267
|
+
const tLines = ['## Telemetry', ''];
|
|
268
|
+
tLines.push(`**State** ${isTelemetryEnabled() ? 'on' : 'off'}`);
|
|
269
|
+
tLines.push(`**Flag** telemetry = ${flag}`);
|
|
270
|
+
if (envOff)
|
|
271
|
+
tLines.push('**Env** forced off by CODEEP_NO_TELEMETRY / DO_NOT_TRACK (overrides the flag)');
|
|
272
|
+
tLines.push('');
|
|
273
|
+
tLines.push('Toggle with `/telemetry on` or `/telemetry off`. Controls automatic uploads of usage stats, session transcripts, progress, and memory notes.');
|
|
274
|
+
ctx.app.addMessage({ role: 'system', content: tLines.join('\n') });
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
248
277
|
case 'grant': {
|
|
249
278
|
setProjectPermission(ctx.projectPath, true, true);
|
|
250
279
|
ctx.setHasWriteAccess(true);
|
|
@@ -880,8 +909,13 @@ Format: use headers per category, only include categories where you found issues
|
|
|
880
909
|
ctx.app.showLogin(providers.map(p => ({ id: p.id, name: p.name, description: p.description, subscribeUrl: p.subscribeUrl, noApiKey: p.noApiKey })), async (result) => {
|
|
881
910
|
if (result) {
|
|
882
911
|
setProvider(result.providerId);
|
|
883
|
-
|
|
884
|
-
|
|
912
|
+
try {
|
|
913
|
+
await setApiKey(result.apiKey);
|
|
914
|
+
ctx.app.notify('Logged in successfully');
|
|
915
|
+
}
|
|
916
|
+
catch {
|
|
917
|
+
ctx.app.notify('Could not save the API key (secure storage unavailable).');
|
|
918
|
+
}
|
|
885
919
|
}
|
|
886
920
|
});
|
|
887
921
|
break;
|
|
@@ -901,11 +935,11 @@ Format: use headers per category, only include categories where you found issues
|
|
|
901
935
|
return;
|
|
902
936
|
if (result === 'all') {
|
|
903
937
|
for (const p of configuredProviders)
|
|
904
|
-
clearApiKey(p.id);
|
|
938
|
+
void clearApiKey(p.id);
|
|
905
939
|
ctx.app.notify('Logged out from all providers. Use /login to sign in.');
|
|
906
940
|
}
|
|
907
941
|
else {
|
|
908
|
-
clearApiKey(result);
|
|
942
|
+
void clearApiKey(result);
|
|
909
943
|
const provider = configuredProviders.find(p => p.id === result);
|
|
910
944
|
ctx.app.notify(`Logged out from ${provider?.name || result}`);
|
|
911
945
|
if (result === currentProvider.id) {
|
package/dist/renderer/main.js
CHANGED
|
@@ -278,7 +278,14 @@ async function showLoginFlow() {
|
|
|
278
278
|
renderCurrentStep();
|
|
279
279
|
return;
|
|
280
280
|
}
|
|
281
|
-
|
|
281
|
+
try {
|
|
282
|
+
await setApiKey(key);
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
loginError = 'Could not save the API key (secure storage unavailable). Please try again.';
|
|
286
|
+
renderCurrentStep();
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
282
289
|
cleanup();
|
|
283
290
|
resolve(key);
|
|
284
291
|
},
|
|
@@ -414,11 +421,15 @@ Commands (in chat):
|
|
|
414
421
|
if (sub === 'sync' || sub === 'pull') {
|
|
415
422
|
// Pull API keys from codeep.dev and save to local config
|
|
416
423
|
const { pullKeys } = await import('../utils/codeepCloud.js');
|
|
417
|
-
const { getSyncToken, setApiKey } = await import('../config/index.js');
|
|
424
|
+
const { getSyncToken, setApiKey, loadAllApiKeys: loadKeys } = await import('../config/index.js');
|
|
418
425
|
if (!getSyncToken()) {
|
|
419
426
|
console.log('\n Not linked to codeep.dev. Run: codeep account\n');
|
|
420
427
|
process.exit(1);
|
|
421
428
|
}
|
|
429
|
+
// Run the one-time plaintext->keychain migration BEFORE storing any pulled
|
|
430
|
+
// key. Otherwise the first setApiKey flips keysSecured=true and any local
|
|
431
|
+
// legacy plaintext keys would never migrate (orphaned, invisible).
|
|
432
|
+
await loadKeys();
|
|
422
433
|
process.stdout.write(' Pulling keys from codeep.dev...');
|
|
423
434
|
const keys = await pullKeys();
|
|
424
435
|
if (!keys) {
|
|
@@ -430,10 +441,17 @@ Commands (in chat):
|
|
|
430
441
|
console.log(' no keys found.\n Add keys at codeep.dev/dashboard');
|
|
431
442
|
}
|
|
432
443
|
else {
|
|
444
|
+
let synced = 0;
|
|
433
445
|
for (const [provider, key] of Object.entries(keys)) {
|
|
434
|
-
|
|
446
|
+
try {
|
|
447
|
+
await setApiKey(key, provider);
|
|
448
|
+
synced++;
|
|
449
|
+
}
|
|
450
|
+
catch {
|
|
451
|
+
console.log(`\n Warning: could not securely store the key for ${provider}.`);
|
|
452
|
+
}
|
|
435
453
|
}
|
|
436
|
-
console.log(` synced ${
|
|
454
|
+
console.log(` synced ${synced} key${synced !== 1 ? 's' : ''}.`);
|
|
437
455
|
}
|
|
438
456
|
// Also pull portable personal config — personalities + custom commands +
|
|
439
457
|
// the user profile. Additive merge (never clobbers local files).
|
package/dist/utils/agent.d.ts
CHANGED
|
@@ -13,6 +13,15 @@ import { undoLastAction, undoAllActions, getCurrentSession, getRecentSessions, f
|
|
|
13
13
|
import { VerifyResult } from './verify';
|
|
14
14
|
import { TaskPlan, SubTask } from './taskPlanner';
|
|
15
15
|
export type PermissionOutcome = 'allow_once' | 'allow_always' | 'reject_once' | 'reject_always';
|
|
16
|
+
export type PermissionDecision = 'allow-once' | 'allow-always' | 'deny-once' | 'deny-always';
|
|
17
|
+
/**
|
|
18
|
+
* Map a permission outcome to a decision, FAILING CLOSED: a dangerous tool is
|
|
19
|
+
* allowed only on an explicit allow outcome. `reject_*` deny, and — critically —
|
|
20
|
+
* any unknown/malformed outcome from a buggy or hostile client also denies
|
|
21
|
+
* (deny-once) rather than slipping through to execution. Pure + exported so the
|
|
22
|
+
* invariant is unit-tested independently of the agent loop.
|
|
23
|
+
*/
|
|
24
|
+
export declare function classifyPermissionOutcome(outcome: string | undefined | null): PermissionDecision;
|
|
16
25
|
export interface AgentOptions {
|
|
17
26
|
maxIterations: number;
|
|
18
27
|
maxDuration: number;
|
package/dist/utils/agent.js
CHANGED
|
@@ -103,6 +103,22 @@ 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
|
+
* Map a permission outcome to a decision, FAILING CLOSED: a dangerous tool is
|
|
108
|
+
* allowed only on an explicit allow outcome. `reject_*` deny, and — critically —
|
|
109
|
+
* any unknown/malformed outcome from a buggy or hostile client also denies
|
|
110
|
+
* (deny-once) rather than slipping through to execution. Pure + exported so the
|
|
111
|
+
* invariant is unit-tested independently of the agent loop.
|
|
112
|
+
*/
|
|
113
|
+
export function classifyPermissionOutcome(outcome) {
|
|
114
|
+
if (outcome === 'allow_always')
|
|
115
|
+
return 'allow-always';
|
|
116
|
+
if (outcome === 'allow_once')
|
|
117
|
+
return 'allow-once';
|
|
118
|
+
if (outcome === 'reject_always')
|
|
119
|
+
return 'deny-always';
|
|
120
|
+
return 'deny-once'; // 'reject_once' OR anything unexpected → fail closed
|
|
121
|
+
}
|
|
106
122
|
/**
|
|
107
123
|
* Build the result for a run that paused at a safety limit. Pausing is a normal,
|
|
108
124
|
* resumable state — not an error — so the summary tells the user how to resume.
|
|
@@ -779,15 +795,18 @@ export async function runAgent(prompt, projectContext, options = {}) {
|
|
|
779
795
|
continue;
|
|
780
796
|
}
|
|
781
797
|
const outcome = await opts.onRequestPermission(toolCall);
|
|
782
|
-
|
|
798
|
+
// Fail CLOSED: allow ONLY on an explicit allow outcome; reject_* and
|
|
799
|
+
// any malformed/unknown outcome deny (see classifyPermissionOutcome).
|
|
800
|
+
const decision = classifyPermissionOutcome(outcome);
|
|
801
|
+
if (decision === 'allow-always') {
|
|
783
802
|
alwaysAllowedTools.add(toolCall.tool);
|
|
784
803
|
}
|
|
785
|
-
else if (
|
|
786
|
-
|
|
787
|
-
rejectResult();
|
|
788
|
-
continue;
|
|
804
|
+
else if (decision === 'allow-once') {
|
|
805
|
+
// proceed this once
|
|
789
806
|
}
|
|
790
|
-
else
|
|
807
|
+
else {
|
|
808
|
+
if (decision === 'deny-always')
|
|
809
|
+
alwaysRejectedTools.add(toolCall.tool);
|
|
791
810
|
rejectResult();
|
|
792
811
|
continue;
|
|
793
812
|
}
|
|
@@ -28,6 +28,20 @@ export interface ReviewSummary {
|
|
|
28
28
|
byCategory: Record<ReviewCategory, number>;
|
|
29
29
|
bySeverity: Record<string, number>;
|
|
30
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* A single deterministic review rule. Built-in rules and user rules from
|
|
33
|
+
* `.codeep/review.json` share this shape. `id` is stable so a project can
|
|
34
|
+
* disable a built-in rule by id (see utils/reviewConfig.ts).
|
|
35
|
+
*/
|
|
36
|
+
export interface RuleDef {
|
|
37
|
+
id: string;
|
|
38
|
+
pattern: RegExp;
|
|
39
|
+
category: ReviewCategory;
|
|
40
|
+
severity: ReviewIssue['severity'];
|
|
41
|
+
message: string;
|
|
42
|
+
suggestion?: string;
|
|
43
|
+
extensions?: string[];
|
|
44
|
+
}
|
|
31
45
|
/**
|
|
32
46
|
* Perform code review
|
|
33
47
|
*/
|
package/dist/utils/codeReview.js
CHANGED
|
@@ -4,10 +4,13 @@
|
|
|
4
4
|
import { existsSync, readFileSync, readdirSync } from 'fs';
|
|
5
5
|
import { join, extname, relative } from 'path';
|
|
6
6
|
import { getChangedFiles } from './git.js';
|
|
7
|
-
|
|
7
|
+
import { loadReviewConfig, globToRegExp } from './reviewConfig.js';
|
|
8
|
+
// Built-in code patterns that indicate issues. Each has a stable `id` so it can
|
|
9
|
+
// be turned off per-project via `.codeep/review.json` { "disable": ["..."] }.
|
|
8
10
|
const CODE_PATTERNS = [
|
|
9
11
|
// Security issues
|
|
10
12
|
{
|
|
13
|
+
id: 'eval-usage',
|
|
11
14
|
pattern: /eval\s*\(/g,
|
|
12
15
|
category: 'security',
|
|
13
16
|
severity: 'error',
|
|
@@ -16,6 +19,7 @@ const CODE_PATTERNS = [
|
|
|
16
19
|
extensions: ['.js', '.ts', '.jsx', '.tsx'],
|
|
17
20
|
},
|
|
18
21
|
{
|
|
22
|
+
id: 'inner-html',
|
|
19
23
|
pattern: /innerHTML\s*=/g,
|
|
20
24
|
category: 'security',
|
|
21
25
|
severity: 'warning',
|
|
@@ -24,6 +28,7 @@ const CODE_PATTERNS = [
|
|
|
24
28
|
extensions: ['.js', '.ts', '.jsx', '.tsx'],
|
|
25
29
|
},
|
|
26
30
|
{
|
|
31
|
+
id: 'dangerously-set-inner-html',
|
|
27
32
|
pattern: /dangerouslySetInnerHTML/g,
|
|
28
33
|
category: 'security',
|
|
29
34
|
severity: 'warning',
|
|
@@ -32,6 +37,7 @@ const CODE_PATTERNS = [
|
|
|
32
37
|
extensions: ['.jsx', '.tsx'],
|
|
33
38
|
},
|
|
34
39
|
{
|
|
40
|
+
id: 'hardcoded-password',
|
|
35
41
|
pattern: /password\s*=\s*['"][^'"]+['"]/gi,
|
|
36
42
|
category: 'security',
|
|
37
43
|
severity: 'error',
|
|
@@ -39,6 +45,7 @@ const CODE_PATTERNS = [
|
|
|
39
45
|
suggestion: 'Use environment variables for sensitive data',
|
|
40
46
|
},
|
|
41
47
|
{
|
|
48
|
+
id: 'hardcoded-api-key',
|
|
42
49
|
pattern: /api[_-]?key\s*=\s*['"][^'"]+['"]/gi,
|
|
43
50
|
category: 'security',
|
|
44
51
|
severity: 'error',
|
|
@@ -47,6 +54,7 @@ const CODE_PATTERNS = [
|
|
|
47
54
|
},
|
|
48
55
|
// Performance issues
|
|
49
56
|
{
|
|
57
|
+
id: 'foreach-await',
|
|
50
58
|
pattern: /\.forEach\s*\([^)]*\)\s*{\s*await/g,
|
|
51
59
|
category: 'performance',
|
|
52
60
|
severity: 'warning',
|
|
@@ -55,6 +63,7 @@ const CODE_PATTERNS = [
|
|
|
55
63
|
extensions: ['.js', '.ts', '.jsx', '.tsx'],
|
|
56
64
|
},
|
|
57
65
|
{
|
|
66
|
+
id: 'await-in-loop',
|
|
58
67
|
pattern: /for\s*\([^)]+\)\s*{\s*await/g,
|
|
59
68
|
category: 'performance',
|
|
60
69
|
severity: 'info',
|
|
@@ -63,6 +72,7 @@ const CODE_PATTERNS = [
|
|
|
63
72
|
extensions: ['.js', '.ts', '.jsx', '.tsx'],
|
|
64
73
|
},
|
|
65
74
|
{
|
|
75
|
+
id: 'select-star',
|
|
66
76
|
pattern: /SELECT\s+\*/gi,
|
|
67
77
|
category: 'performance',
|
|
68
78
|
severity: 'warning',
|
|
@@ -71,6 +81,7 @@ const CODE_PATTERNS = [
|
|
|
71
81
|
},
|
|
72
82
|
// Bug-prone patterns
|
|
73
83
|
{
|
|
84
|
+
id: 'loose-null-check',
|
|
74
85
|
pattern: /==\s*null|null\s*==/g,
|
|
75
86
|
category: 'bug',
|
|
76
87
|
severity: 'info',
|
|
@@ -79,6 +90,7 @@ const CODE_PATTERNS = [
|
|
|
79
90
|
extensions: ['.js', '.ts', '.jsx', '.tsx'],
|
|
80
91
|
},
|
|
81
92
|
{
|
|
93
|
+
id: 'empty-catch',
|
|
82
94
|
pattern: /catch\s*\(\s*\w*\s*\)\s*{\s*}/g,
|
|
83
95
|
category: 'bug',
|
|
84
96
|
severity: 'warning',
|
|
@@ -86,6 +98,7 @@ const CODE_PATTERNS = [
|
|
|
86
98
|
suggestion: 'Log the error or handle it appropriately',
|
|
87
99
|
},
|
|
88
100
|
{
|
|
101
|
+
id: 'console-statement',
|
|
89
102
|
pattern: /console\.(log|debug|info|warn|error)\s*\(/g,
|
|
90
103
|
category: 'maintainability',
|
|
91
104
|
severity: 'info',
|
|
@@ -94,6 +107,7 @@ const CODE_PATTERNS = [
|
|
|
94
107
|
extensions: ['.js', '.ts', '.jsx', '.tsx'],
|
|
95
108
|
},
|
|
96
109
|
{
|
|
110
|
+
id: 'todo-comment',
|
|
97
111
|
pattern: /TODO|FIXME|HACK|XXX/g,
|
|
98
112
|
category: 'maintainability',
|
|
99
113
|
severity: 'info',
|
|
@@ -102,6 +116,7 @@ const CODE_PATTERNS = [
|
|
|
102
116
|
},
|
|
103
117
|
// Type safety
|
|
104
118
|
{
|
|
119
|
+
id: 'any-type',
|
|
105
120
|
pattern: /:\s*any\b/g,
|
|
106
121
|
category: 'types',
|
|
107
122
|
severity: 'warning',
|
|
@@ -110,6 +125,7 @@ const CODE_PATTERNS = [
|
|
|
110
125
|
extensions: ['.ts', '.tsx'],
|
|
111
126
|
},
|
|
112
127
|
{
|
|
128
|
+
id: 'ts-ignore',
|
|
113
129
|
pattern: /@ts-ignore/g,
|
|
114
130
|
category: 'types',
|
|
115
131
|
severity: 'warning',
|
|
@@ -118,6 +134,7 @@ const CODE_PATTERNS = [
|
|
|
118
134
|
extensions: ['.ts', '.tsx'],
|
|
119
135
|
},
|
|
120
136
|
{
|
|
137
|
+
id: 'as-any',
|
|
121
138
|
pattern: /as\s+any\b/g,
|
|
122
139
|
category: 'types',
|
|
123
140
|
severity: 'warning',
|
|
@@ -127,6 +144,7 @@ const CODE_PATTERNS = [
|
|
|
127
144
|
},
|
|
128
145
|
// Best practices
|
|
129
146
|
{
|
|
147
|
+
id: 'var-usage',
|
|
130
148
|
pattern: /var\s+\w+/g,
|
|
131
149
|
category: 'best-practice',
|
|
132
150
|
severity: 'info',
|
|
@@ -135,6 +153,7 @@ const CODE_PATTERNS = [
|
|
|
135
153
|
extensions: ['.js', '.jsx'],
|
|
136
154
|
},
|
|
137
155
|
{
|
|
156
|
+
id: 'anonymous-function',
|
|
138
157
|
pattern: /function\s*\(/g,
|
|
139
158
|
category: 'style',
|
|
140
159
|
severity: 'info',
|
|
@@ -144,6 +163,7 @@ const CODE_PATTERNS = [
|
|
|
144
163
|
},
|
|
145
164
|
// Documentation
|
|
146
165
|
{
|
|
166
|
+
id: 'missing-jsdoc',
|
|
147
167
|
pattern: /export\s+(default\s+)?(?:function|class|const)\s+\w+/g,
|
|
148
168
|
category: 'documentation',
|
|
149
169
|
severity: 'suggestion',
|
|
@@ -155,22 +175,31 @@ const CODE_PATTERNS = [
|
|
|
155
175
|
/**
|
|
156
176
|
* Analyze a single file for issues
|
|
157
177
|
*/
|
|
158
|
-
function analyzeFile(filePath, content, projectRoot) {
|
|
178
|
+
function analyzeFile(filePath, content, projectRoot, rules, disabled) {
|
|
159
179
|
const issues = [];
|
|
160
180
|
const ext = extname(filePath);
|
|
161
181
|
const relativePath = relative(projectRoot, filePath);
|
|
162
182
|
const lines = content.split('\n');
|
|
163
|
-
|
|
183
|
+
// Skip the regex pass on very large files (the cheap line-count heuristics
|
|
184
|
+
// below still run) so an oversized file can't stall the reviewer. NOTE: this
|
|
185
|
+
// bounds input SIZE only, not regex run-time — catastrophic backtracking is a
|
|
186
|
+
// function of pattern shape. Untrusted custom rules from .codeep/review.json
|
|
187
|
+
// are additionally screened at load (utils/reviewConfig.ts) and the GitHub
|
|
188
|
+
// Action caps wall-clock, but a zero-width match is guarded right here.
|
|
189
|
+
const scannable = content.length <= 2_000_000 ? content : '';
|
|
190
|
+
const MAX_MATCHES_PER_RULE = 1000;
|
|
191
|
+
for (const pattern of rules) {
|
|
164
192
|
// Skip if pattern doesn't apply to this file type
|
|
165
193
|
if (pattern.extensions && !pattern.extensions.includes(ext)) {
|
|
166
194
|
continue;
|
|
167
195
|
}
|
|
168
196
|
// Find all matches
|
|
169
197
|
let match;
|
|
198
|
+
let count = 0;
|
|
170
199
|
const regex = new RegExp(pattern.pattern.source, pattern.pattern.flags);
|
|
171
|
-
while ((match = regex.exec(
|
|
200
|
+
while ((match = regex.exec(scannable)) !== null) {
|
|
172
201
|
// Find line number
|
|
173
|
-
const beforeMatch =
|
|
202
|
+
const beforeMatch = scannable.slice(0, match.index);
|
|
174
203
|
const lineNumber = beforeMatch.split('\n').length;
|
|
175
204
|
issues.push({
|
|
176
205
|
file: relativePath,
|
|
@@ -180,10 +209,17 @@ function analyzeFile(filePath, content, projectRoot) {
|
|
|
180
209
|
message: pattern.message,
|
|
181
210
|
suggestion: pattern.suggestion,
|
|
182
211
|
});
|
|
212
|
+
// A zero-width match (e.g. a custom rule like `a?` or `(?:)`) leaves
|
|
213
|
+
// lastIndex unchanged, so exec() would return it forever — advance past it.
|
|
214
|
+
if (match.index === regex.lastIndex)
|
|
215
|
+
regex.lastIndex++;
|
|
216
|
+
// Bound pathological match floods (also caps the per-match work above).
|
|
217
|
+
if (++count >= MAX_MATCHES_PER_RULE)
|
|
218
|
+
break;
|
|
183
219
|
}
|
|
184
220
|
}
|
|
185
221
|
// Check for long files
|
|
186
|
-
if (lines.length > 500) {
|
|
222
|
+
if (!disabled.has('long-file') && lines.length > 500) {
|
|
187
223
|
issues.push({
|
|
188
224
|
file: relativePath,
|
|
189
225
|
severity: 'info',
|
|
@@ -194,7 +230,8 @@ function analyzeFile(filePath, content, projectRoot) {
|
|
|
194
230
|
// Check for long functions (basic heuristic)
|
|
195
231
|
let braceDepth = 0;
|
|
196
232
|
let functionStart = -1;
|
|
197
|
-
|
|
233
|
+
const checkLongFunctions = !disabled.has('long-function');
|
|
234
|
+
for (let i = 0; checkLongFunctions && i < lines.length; i++) {
|
|
198
235
|
const line = lines[i];
|
|
199
236
|
if (/function\s+\w+|=>\s*{|\)\s*{/.test(line)) {
|
|
200
237
|
if (braceDepth === 0) {
|
|
@@ -278,7 +315,28 @@ function getAllSourceFiles(dir, maxFiles = 50) {
|
|
|
278
315
|
*/
|
|
279
316
|
export function performCodeReview(projectContext, specificFiles) {
|
|
280
317
|
const projectRoot = projectContext.root || process.cwd();
|
|
281
|
-
|
|
318
|
+
// Project-level config (.codeep/review.json): custom rules, disabled built-in
|
|
319
|
+
// ids, and include/exclude globs. Absent/invalid → defaults (built-ins only).
|
|
320
|
+
const config = loadReviewConfig(projectRoot);
|
|
321
|
+
const disabled = config?.disabled ?? new Set();
|
|
322
|
+
const effectiveRules = [
|
|
323
|
+
...CODE_PATTERNS.filter((p) => !disabled.has(p.id)),
|
|
324
|
+
...(config?.rules ?? []),
|
|
325
|
+
];
|
|
326
|
+
let filesToReview = getFilesToReview(projectRoot, specificFiles);
|
|
327
|
+
// Apply include/exclude globs (posix-relative paths). Empty include = all.
|
|
328
|
+
if (config && (config.include.length > 0 || config.exclude.length > 0)) {
|
|
329
|
+
const inc = config.include.map(globToRegExp);
|
|
330
|
+
const exc = config.exclude.map(globToRegExp);
|
|
331
|
+
filesToReview = filesToReview.filter((f) => {
|
|
332
|
+
const rel = relative(projectRoot, f).split('\\').join('/');
|
|
333
|
+
if (inc.length > 0 && !inc.some((re) => re.test(rel)))
|
|
334
|
+
return false;
|
|
335
|
+
if (exc.some((re) => re.test(rel)))
|
|
336
|
+
return false;
|
|
337
|
+
return true;
|
|
338
|
+
});
|
|
339
|
+
}
|
|
282
340
|
const allIssues = [];
|
|
283
341
|
// Determine scope — mirrors the branching in getFilesToReview so the user
|
|
284
342
|
// sees exactly which branch ran.
|
|
@@ -295,7 +353,7 @@ export function performCodeReview(projectContext, specificFiles) {
|
|
|
295
353
|
for (const filePath of filesToReview) {
|
|
296
354
|
try {
|
|
297
355
|
const content = readFileSync(filePath, 'utf-8');
|
|
298
|
-
const issues = analyzeFile(filePath, content, projectRoot);
|
|
356
|
+
const issues = analyzeFile(filePath, content, projectRoot, effectiveRules, disabled);
|
|
299
357
|
allIssues.push(...issues);
|
|
300
358
|
}
|
|
301
359
|
catch { }
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { randomBytes, createHash } from 'crypto';
|
|
9
9
|
import { spawn } from 'child_process';
|
|
10
|
-
import { getGithubId, getSyncToken, setGithubAccount, setSyncToken, getDeviceId } from '../config/index.js';
|
|
10
|
+
import { getGithubId, getSyncToken, setGithubAccount, setSyncToken, getDeviceId, isTelemetryEnabled } from '../config/index.js';
|
|
11
11
|
import { hostname } from 'os';
|
|
12
12
|
const API_BASE = 'https://codeep.dev';
|
|
13
13
|
const POLL_INTERVAL_MS = 2000;
|
|
@@ -110,6 +110,8 @@ export function generateProjectId(projectRoot) {
|
|
|
110
110
|
* Retries up to 2 times on network errors or 5xx responses.
|
|
111
111
|
*/
|
|
112
112
|
export function reportStats(payload) {
|
|
113
|
+
if (!isTelemetryEnabled())
|
|
114
|
+
return; // user opted out of automatic uploads
|
|
113
115
|
const githubId = getGithubId();
|
|
114
116
|
if (!githubId)
|
|
115
117
|
return; // not linked, skip silently
|
|
@@ -124,6 +126,8 @@ export function reportStats(payload) {
|
|
|
124
126
|
}).catch(() => { });
|
|
125
127
|
}
|
|
126
128
|
export async function reportStatsAsync(payload) {
|
|
129
|
+
if (!isTelemetryEnabled())
|
|
130
|
+
return; // user opted out of automatic uploads
|
|
127
131
|
const githubId = getGithubId();
|
|
128
132
|
if (!githubId)
|
|
129
133
|
return;
|
|
@@ -320,6 +324,8 @@ export const pushCommands = () => pushBundle('commands');
|
|
|
320
324
|
* Fire-and-forget. Only sends if linked and sync_token is available.
|
|
321
325
|
*/
|
|
322
326
|
export function syncSession(payload) {
|
|
327
|
+
if (!isTelemetryEnabled())
|
|
328
|
+
return; // user opted out of conversation/session upload
|
|
323
329
|
const githubId = getGithubId();
|
|
324
330
|
const syncToken = getSyncToken();
|
|
325
331
|
if (!githubId || !syncToken)
|
|
@@ -335,6 +341,8 @@ export function syncSession(payload) {
|
|
|
335
341
|
}).catch(() => { });
|
|
336
342
|
}
|
|
337
343
|
export async function syncSessionAsync(payload) {
|
|
344
|
+
if (!isTelemetryEnabled())
|
|
345
|
+
return; // user opted out of conversation/session upload
|
|
338
346
|
const githubId = getGithubId();
|
|
339
347
|
const syncToken = getSyncToken();
|
|
340
348
|
if (!githubId || !syncToken)
|
|
@@ -354,6 +362,8 @@ export async function syncSessionAsync(payload) {
|
|
|
354
362
|
* Fire-and-forget. Only sends if linked (githubId + syncToken).
|
|
355
363
|
*/
|
|
356
364
|
export function syncProgress(payload) {
|
|
365
|
+
if (!isTelemetryEnabled())
|
|
366
|
+
return; // user opted out of automatic uploads
|
|
357
367
|
const githubId = getGithubId();
|
|
358
368
|
const syncToken = getSyncToken();
|
|
359
369
|
if (!githubId || !syncToken)
|
|
@@ -483,6 +493,8 @@ export async function pullUserProfile() {
|
|
|
483
493
|
}
|
|
484
494
|
}
|
|
485
495
|
export async function syncMemoryNotes(projectName, notes) {
|
|
496
|
+
if (!isTelemetryEnabled())
|
|
497
|
+
return; // user opted out of automatic uploads
|
|
486
498
|
const syncToken = getSyncToken();
|
|
487
499
|
if (!syncToken)
|
|
488
500
|
return;
|
package/dist/utils/keychain.js
CHANGED
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
import { logger } from './logger.js';
|
|
2
|
-
// keytar is a native addon — load
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
// keytar is a native addon — load it LAZILY (on first use), never at module
|
|
3
|
+
// top level. A top-level `await import()` here gives the module a top-level
|
|
4
|
+
// await, which makes `bun build --compile` reject any CommonJS require() that
|
|
5
|
+
// transitively depends on this file (renderer/main.js → codeepCloud → config →
|
|
6
|
+
// keychain). Lazy loading keeps the module side-effect-free at import time.
|
|
7
|
+
let _keytar = null;
|
|
8
|
+
let _keytarTried = false;
|
|
9
|
+
async function loadKeytar() {
|
|
10
|
+
if (!_keytarTried) {
|
|
11
|
+
_keytarTried = true;
|
|
12
|
+
try {
|
|
13
|
+
_keytar = (await import('keytar')).default;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
_keytar = null; /* native addon unavailable */
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return _keytar;
|
|
6
20
|
}
|
|
7
|
-
catch { /* native addon unavailable */ }
|
|
8
21
|
const SERVICE_NAME = 'codeep';
|
|
9
22
|
class KeychainStorage {
|
|
10
23
|
getAccountName(providerId) {
|
|
@@ -12,9 +25,11 @@ class KeychainStorage {
|
|
|
12
25
|
}
|
|
13
26
|
async getApiKey(providerId) {
|
|
14
27
|
try {
|
|
28
|
+
const kt = await loadKeytar();
|
|
29
|
+
if (!kt)
|
|
30
|
+
return null;
|
|
15
31
|
const account = this.getAccountName(providerId);
|
|
16
|
-
|
|
17
|
-
return password;
|
|
32
|
+
return await kt.getPassword(SERVICE_NAME, account);
|
|
18
33
|
}
|
|
19
34
|
catch (error) {
|
|
20
35
|
logger.debug(`Failed to get API key from keychain: ${error}`);
|
|
@@ -23,8 +38,11 @@ class KeychainStorage {
|
|
|
23
38
|
}
|
|
24
39
|
async setApiKey(providerId, apiKey) {
|
|
25
40
|
try {
|
|
41
|
+
const kt = await loadKeytar();
|
|
42
|
+
if (!kt)
|
|
43
|
+
throw new Error('keytar unavailable');
|
|
26
44
|
const account = this.getAccountName(providerId);
|
|
27
|
-
await
|
|
45
|
+
await kt.setPassword(SERVICE_NAME, account, apiKey);
|
|
28
46
|
}
|
|
29
47
|
catch (error) {
|
|
30
48
|
throw new Error(`Failed to store API key in keychain: ${error}`);
|
|
@@ -32,8 +50,11 @@ class KeychainStorage {
|
|
|
32
50
|
}
|
|
33
51
|
async deleteApiKey(providerId) {
|
|
34
52
|
try {
|
|
53
|
+
const kt = await loadKeytar();
|
|
54
|
+
if (!kt)
|
|
55
|
+
return;
|
|
35
56
|
const account = this.getAccountName(providerId);
|
|
36
|
-
await
|
|
57
|
+
await kt.deletePassword(SERVICE_NAME, account);
|
|
37
58
|
}
|
|
38
59
|
catch (error) {
|
|
39
60
|
logger.debug(`Failed to delete API key from keychain: ${error}`);
|
|
@@ -89,10 +110,11 @@ class SmartStorage {
|
|
|
89
110
|
return;
|
|
90
111
|
try {
|
|
91
112
|
const testKey = '__codeep_test__';
|
|
92
|
-
|
|
113
|
+
const kt = await loadKeytar();
|
|
114
|
+
if (!kt)
|
|
93
115
|
throw new Error('keytar unavailable');
|
|
94
|
-
await
|
|
95
|
-
await
|
|
116
|
+
await kt.setPassword(SERVICE_NAME, testKey, 'test');
|
|
117
|
+
await kt.deletePassword(SERVICE_NAME, testKey);
|
|
96
118
|
this.useKeychain = true;
|
|
97
119
|
}
|
|
98
120
|
catch {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { RuleDef } from './codeReview';
|
|
2
|
+
export interface ReviewConfig {
|
|
3
|
+
rules: RuleDef[];
|
|
4
|
+
disabled: Set<string>;
|
|
5
|
+
include: string[];
|
|
6
|
+
exclude: string[];
|
|
7
|
+
}
|
|
8
|
+
/** Convert a simple glob (`**`, `*`, `?`) into an anchored RegExp over posix paths. */
|
|
9
|
+
export declare function globToRegExp(glob: string): RegExp;
|
|
10
|
+
export declare function loadReviewConfig(projectRoot: string): ReviewConfig | null;
|