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.
@@ -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
- await setApiKey(result.apiKey);
884
- ctx.app.notify('Logged in successfully');
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) {
@@ -278,7 +278,14 @@ async function showLoginFlow() {
278
278
  renderCurrentStep();
279
279
  return;
280
280
  }
281
- await setApiKey(key);
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
- setApiKey(key, provider);
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 ${count} key${count !== 1 ? 's' : ''}.`);
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).
@@ -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;
@@ -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
- if (outcome === 'allow_always') {
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 (outcome === 'reject_always') {
786
- alwaysRejectedTools.add(toolCall.tool);
787
- rejectResult();
788
- continue;
804
+ else if (decision === 'allow-once') {
805
+ // proceed this once
789
806
  }
790
- else if (outcome === 'reject_once') {
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
  */
@@ -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
- // Common code patterns that indicate issues
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
- for (const pattern of CODE_PATTERNS) {
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(content)) !== null) {
200
+ while ((match = regex.exec(scannable)) !== null) {
172
201
  // Find line number
173
- const beforeMatch = content.slice(0, match.index);
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
- for (let i = 0; i < lines.length; i++) {
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
- const filesToReview = getFilesToReview(projectRoot, specificFiles);
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;
@@ -1,10 +1,23 @@
1
1
  import { logger } from './logger.js';
2
- // keytar is a native addon — load dynamically so compiled binaries fall back gracefully
3
- let keytar = null;
4
- try {
5
- keytar = (await import('keytar')).default;
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
- const password = await keytar.getPassword(SERVICE_NAME, account);
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 keytar.setPassword(SERVICE_NAME, account, apiKey);
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 keytar.deletePassword(SERVICE_NAME, account);
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
- if (!keytar)
113
+ const kt = await loadKeytar();
114
+ if (!kt)
93
115
  throw new Error('keytar unavailable');
94
- await keytar.setPassword(SERVICE_NAME, testKey, 'test');
95
- await keytar.deletePassword(SERVICE_NAME, testKey);
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;