codeep 2.20.0 → 2.21.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.
@@ -869,13 +869,17 @@ export function startAcpServer() {
869
869
  transport.error(msg.id, -32602, `Unknown sessionId: ${params.sessionId}`);
870
870
  return;
871
871
  }
872
- const updated = await pullPersonalities();
873
- if (updated === null) {
874
- transport.error(msg.id, -32001, 'Personality sync failed or this device is not linked to codeep.dev.');
872
+ const { describeSyncFailure } = await import('../utils/codeepCloud.js');
873
+ const sync = await pullPersonalities();
874
+ if (!sync.ok) {
875
+ // The old contract collapsed every failure into one message that also
876
+ // covered "not linked", so a client could not tell an expired session
877
+ // from an unreachable server. Say which.
878
+ transport.error(msg.id, -32001, `Personality sync failed — ${describeSyncFailure(sync.reason)}.`);
875
879
  return;
876
880
  }
877
881
  const list = personalityListResult(params.sessionId);
878
- const result = { updated, ...list };
882
+ const result = { updated: sync.count, ...list };
879
883
  transport.respond(msg.id, result);
880
884
  }
881
885
  // ── session/prompt ──────────────────────────────────────────────────────────
@@ -1393,7 +1393,6 @@ export class App {
1393
1393
  mentionItemCount: this.mention.items.length,
1394
1394
  });
1395
1395
  const layout = chatLayout(height, panelHeight);
1396
- const mainHeight = layout.mainHeight;
1397
1396
  const headerHeight = width >= 60 && height >= 16 ? 2 : 0;
1398
1397
  const messagesStart = Math.min(layout.messagesEnd, headerHeight);
1399
1398
  const messagesEnd = layout.messagesEnd;
@@ -13,7 +13,6 @@ export interface KeyEvent {
13
13
  export type KeyHandler = (event: KeyEvent) => void;
14
14
  export declare class Input {
15
15
  private handlers;
16
- private rl;
17
16
  private dataHandler;
18
17
  /**
19
18
  * Start listening for input
@@ -4,7 +4,6 @@
4
4
  */
5
5
  export class Input {
6
6
  handlers = [];
7
- rl = null;
8
7
  dataHandler = null;
9
8
  /**
10
9
  * Start listening for input
@@ -2102,7 +2102,6 @@ Describe what this skill does. The agent reads this body verbatim when it invoke
2102
2102
  }
2103
2103
  case 'profile': {
2104
2104
  const subCmd = args[0]?.toLowerCase();
2105
- const profileName = args[1] || args[0]; // /profile save name OR /profile name
2106
2105
  if (!subCmd || subCmd === 'list') {
2107
2106
  const profiles = listProfiles();
2108
2107
  if (profiles.length === 0) {
@@ -2165,9 +2164,9 @@ Describe what this skill does. The agent reads this body verbatim when it invoke
2165
2164
  }
2166
2165
  case 'sync': {
2167
2166
  const subCmd = args[0]?.toLowerCase() || 'all';
2168
- const { pushLearning, pullLearning, pushProfiles, pullProfiles } = await import('../utils/codeepCloud.js');
2167
+ const { pushLearning, pushProfiles, pullProfiles } = await import('../utils/codeepCloud.js');
2169
2168
  const { getSyncToken } = await import('../config/index.js');
2170
- const { loadGlobalPreferences, saveGlobalPreferences } = await import('../utils/learning.js');
2169
+ const { loadGlobalPreferences } = await import('../utils/learning.js');
2171
2170
  if (!getSyncToken()) {
2172
2171
  ctx.app.notify('Not linked to codeep.dev. Run: codeep account');
2173
2172
  break;
@@ -2,8 +2,6 @@
2
2
  * Export panel component
3
3
  */
4
4
  import { fg, style } from '../ansi.js';
5
- // Primary color: #f02a30 (Codeep red)
6
- const PRIMARY_COLOR = fg.rgb(240, 42, 48);
7
5
  const FORMATS = [
8
6
  { id: 'md', name: 'Markdown', desc: 'Formatted with headers and separators' },
9
7
  { id: 'json', name: 'JSON', desc: 'Structured data format' },
@@ -15,7 +15,6 @@ export interface LoginOptions {
15
15
  */
16
16
  export declare class LoginScreen {
17
17
  private screen;
18
- private input;
19
18
  private editor;
20
19
  private options;
21
20
  private showKey;
@@ -14,13 +14,11 @@ const PRIMARY_BRIGHT = fg.rgb(255, 80, 85);
14
14
  */
15
15
  export class LoginScreen {
16
16
  screen;
17
- input;
18
17
  editor;
19
18
  options;
20
19
  showKey = false;
21
20
  constructor(screen, input, options) {
22
21
  this.screen = screen;
23
- this.input = input;
24
22
  this.editor = new LineEditor();
25
23
  this.options = options;
26
24
  }
@@ -2,8 +2,6 @@
2
2
  * Logout panel component
3
3
  */
4
4
  import { fg, style } from '../ansi.js';
5
- // Primary color: #f02a30 (Codeep red)
6
- const PRIMARY_COLOR = fg.rgb(240, 42, 48);
7
5
  /**
8
6
  * Render inline logout picker
9
7
  */
@@ -24,6 +24,9 @@ export interface SettingsState {
24
24
  editing: boolean;
25
25
  editValue: string;
26
26
  }
27
+ /**
28
+ * Format value for display
29
+ */
27
30
  /**
28
31
  * Handle settings key
29
32
  * Returns: { handled: boolean, close: boolean, notify?: string }
@@ -1,12 +1,8 @@
1
1
  /**
2
2
  * Settings screen component
3
3
  */
4
- import { fg } from '../ansi.js';
5
4
  import { config } from '../../config/index.js';
6
5
  import { updateRateLimits } from '../../utils/ratelimit.js';
7
- // Primary color: #f02a30 (Codeep red)
8
- const PRIMARY_COLOR = fg.rgb(240, 42, 48);
9
- const PRIMARY_BRIGHT = fg.rgb(255, 80, 85);
10
6
  /**
11
7
  * Write a value to the config for a given setting.
12
8
  *
@@ -295,14 +291,6 @@ export const SETTINGS = [
295
291
  /**
296
292
  * Format value for display
297
293
  */
298
- function formatValue(setting) {
299
- const value = setting.getValue();
300
- if (setting.type === 'select' && setting.options) {
301
- const option = setting.options.find(o => o.value === value);
302
- return option ? option.label : String(value);
303
- }
304
- return String(value);
305
- }
306
294
  /**
307
295
  * Handle settings key
308
296
  * Returns: { handled: boolean, close: boolean, notify?: string }
@@ -9,7 +9,6 @@ import { App } from './App.js';
9
9
  import { Screen } from './Screen.js';
10
10
  import { Input } from './Input.js';
11
11
  import { LoginScreen, renderProviderSelect } from './components/Login.js';
12
- import { renderPermissionScreen, getPermissionOptions } from './components/Permission.js';
13
12
  import { chat, setProjectContext } from '../api/index.js';
14
13
  import { getZaiVisionConfig, getMinimaxMcpConfig, callZaiVisionApi, callMinimaxApi } from '../utils/mcpIntegration.js';
15
14
  import { config, loadApiKey, loadAllApiKeys, getCurrentProvider, autoSaveSession, startNewSession, getCurrentSessionId, loadSession, listSessionsWithInfo, deleteSession, hasReadPermission, hasWritePermission, setProjectPermission, initializeAsProject, isManuallyInitializedProject, setApiKey, setProvider, getGithubId, } from '../config/index.js';
@@ -384,49 +383,6 @@ async function showLoginFlow() {
384
383
  renderCurrentStep();
385
384
  });
386
385
  }
387
- // ─── Permission flow (full-screen, pre-app) ───────────────────────────────────
388
- async function showPermissionFlow() {
389
- return new Promise((resolve) => {
390
- const screen = new Screen();
391
- const input = new Input();
392
- let selectedIndex = 0;
393
- const options = getPermissionOptions();
394
- const isProject = isProjectDirectory(projectPath);
395
- const currentPermission = hasWritePermission(projectPath)
396
- ? 'write'
397
- : hasReadPermission(projectPath)
398
- ? 'read'
399
- : 'none';
400
- screen.init();
401
- input.start();
402
- const cleanup = () => { input.stop(); screen.cleanup(); };
403
- const render = () => {
404
- renderPermissionScreen(screen, {
405
- projectPath, isProject, currentPermission,
406
- onSelect: () => { }, onCancel: () => { },
407
- }, selectedIndex);
408
- };
409
- input.onKey((event) => {
410
- if (event.key === 'up') {
411
- selectedIndex = Math.max(0, selectedIndex - 1);
412
- render();
413
- }
414
- else if (event.key === 'down') {
415
- selectedIndex = Math.min(options.length - 1, selectedIndex + 1);
416
- render();
417
- }
418
- else if (event.key === 'enter') {
419
- cleanup();
420
- resolve(options[selectedIndex]);
421
- }
422
- else if (event.key === 'escape') {
423
- cleanup();
424
- resolve('none');
425
- }
426
- });
427
- render();
428
- });
429
- }
430
386
  // ─── Session picker ───────────────────────────────────────────────────────────
431
387
  function showSessionPickerInline() {
432
388
  const sessions = listSessionsWithInfo(projectPath);
@@ -544,16 +500,32 @@ Commands (in chat):
544
500
  // the user profile. Web-edited personalities replace their local copy
545
501
  // after a safety backup; commands/profile retain additive merge rules.
546
502
  const { pullPersonalities, pullCommands, pullUserProfile, getLastPersonalityPullBackupCount } = await import('../utils/codeepCloud.js');
547
- const pCount = await pullPersonalities();
548
- if (typeof pCount === 'number' && pCount > 0) {
549
- console.log(` Pulled ${pCount} personalit${pCount === 1 ? 'y' : 'ies'}.`);
503
+ // Report all three outcomes, not just the interesting one. Printing only
504
+ // on count > 0 made a failed sync look identical to a sync with nothing
505
+ // new — silence meant either, and the user could not tell which.
506
+ const { describeSyncFailure } = await import('../utils/codeepCloud.js');
507
+ const personalities = await pullPersonalities();
508
+ if (!personalities.ok) {
509
+ console.log(` Could not pull agents — ${describeSyncFailure(personalities.reason)}.`);
510
+ }
511
+ else if (personalities.count > 0) {
512
+ console.log(` Pulled ${personalities.count} personalit${personalities.count === 1 ? 'y' : 'ies'}.`);
550
513
  const backups = getLastPersonalityPullBackupCount();
551
514
  if (backups > 0)
552
515
  console.log(` Backed up ${backups} replaced local cop${backups === 1 ? 'y' : 'ies'} in ~/.codeep/backups/personalities/.`);
553
516
  }
554
- const cCount = await pullCommands();
555
- if (typeof cCount === 'number' && cCount > 0) {
556
- console.log(` Pulled ${cCount} custom command${cCount === 1 ? '' : 's'}.`);
517
+ else if (personalities.removed === 0) {
518
+ console.log(' Agents already up to date.');
519
+ }
520
+ if (personalities.ok && personalities.removed > 0) {
521
+ console.log(` Removed ${personalities.removed} agent${personalities.removed === 1 ? '' : 's'} deleted on codeep.dev (backed up first).`);
522
+ }
523
+ const commands = await pullCommands();
524
+ if (!commands.ok) {
525
+ console.log(` Could not pull custom commands — ${describeSyncFailure(commands.reason)}.`);
526
+ }
527
+ else if (commands.count > 0) {
528
+ console.log(` Pulled ${commands.count} custom command${commands.count === 1 ? '' : 's'}.`);
557
529
  }
558
530
  const profPulled = await pullUserProfile();
559
531
  if (profPulled === 1) {
@@ -597,13 +569,20 @@ Commands (in chat):
597
569
  }
598
570
  // Also push portable personal config — personalities + commands + profile.
599
571
  const { pushPersonalities, pushCommands, pushUserProfile } = await import('../utils/codeepCloud.js');
600
- const pCount = await pushPersonalities();
601
- if (typeof pCount === 'number' && pCount > 0) {
602
- console.log(` Pushed ${pCount} personalit${pCount === 1 ? 'y' : 'ies'}.`);
572
+ const { describeSyncFailure } = await import('../utils/codeepCloud.js');
573
+ const personalities = await pushPersonalities();
574
+ if (!personalities.ok) {
575
+ console.log(` Could not push agents — ${describeSyncFailure(personalities.reason)}.`);
576
+ }
577
+ else if (personalities.count > 0) {
578
+ console.log(` Pushed ${personalities.count} personalit${personalities.count === 1 ? 'y' : 'ies'}.`);
579
+ }
580
+ const commands = await pushCommands();
581
+ if (!commands.ok) {
582
+ console.log(` Could not push custom commands — ${describeSyncFailure(commands.reason)}.`);
603
583
  }
604
- const cCount = await pushCommands();
605
- if (typeof cCount === 'number' && cCount > 0) {
606
- console.log(` Pushed ${cCount} custom command${cCount === 1 ? '' : 's'}.`);
584
+ else if (commands.count > 0) {
585
+ console.log(` Pushed ${commands.count} custom command${commands.count === 1 ? '' : 's'}.`);
607
586
  }
608
587
  if (await pushUserProfile()) {
609
588
  console.log(' Pushed your profile (about you).');
@@ -205,7 +205,11 @@ export async function runAgent(prompt, projectContext, options = {}) {
205
205
  // Start history session for undo support. Skipped for nested (delegated)
206
206
  // runs so we don't reset the parent's currentSession singleton — the
207
207
  // sub-agent's actions still record into the parent's open session.
208
- const sessionId = opts.nested ? '' : startSession(prompt, projectContext.root || process.cwd());
208
+ // The return value is unused — startSession's point here is the side
209
+ // effect of opening the history session. Binding it hid that from
210
+ // noUnusedLocals, so the dead binding is gone and the call stays.
211
+ if (!opts.nested)
212
+ startSession(prompt, projectContext.root || process.cwd());
209
213
  // Task planning phase (if enabled)
210
214
  // Use planning for complex keywords or multi-word prompts
211
215
  let taskPlan = null;
@@ -84,12 +84,37 @@ declare function readFileBundle(kind: 'personalities' | 'commands'): Record<stri
84
84
  * files. Only writes files that don't already exist (additive merge —
85
85
  * never clobber local edits). Returns the count of newly written files. */
86
86
  declare function writeFileBundle(kind: 'personalities' | 'commands', items: Record<string, string>): number;
87
+ /** Apply the server's explicit deletion list.
88
+ *
89
+ * Only names the server named. Absence from `items` is deliberately NOT a
90
+ * deletion signal: an expired session, the wrong account, or a truncated
91
+ * response all yield an empty `items`, and deleting on absence would wipe
92
+ * every local agent. Project-scoped agents in `.codeep/personalities/` are
93
+ * not cloud-owned and are never touched — only the global directory is.
94
+ * Every removal is backed up first, and a failed backup cancels the delete. */
95
+ declare function applyPersonalityTombstones(deleted: readonly string[]): number;
87
96
  declare function writePulledPersonalityBundle(items: Record<string, string>): number;
88
- export declare const pullPersonalities: () => Promise<number | null>;
97
+ /** Why a sync attempt produced nothing. Reported so a silent failure cannot
98
+ * look like a successful no-op — the two were indistinguishable when every
99
+ * path returned `null`, and a user watching `codeep account sync` print
100
+ * nothing had no way to tell which had happened. */
101
+ export type SyncFailure = 'not-linked' | 'unreachable' | 'rejected' | 'malformed';
102
+ /** Success carries a count (which may legitimately be 0 — nothing new), plus
103
+ * how many local agents the server's tombstone list removed. */
104
+ export type SyncResult = {
105
+ ok: true;
106
+ count: number;
107
+ removed: number;
108
+ } | {
109
+ ok: false;
110
+ reason: SyncFailure;
111
+ };
112
+ export declare function describeSyncFailure(reason: SyncFailure): string;
113
+ export declare const pullPersonalities: () => Promise<SyncResult>;
89
114
  export declare const getLastPersonalityPullBackupCount: () => number;
90
- export declare const pushPersonalities: () => Promise<number | null>;
91
- export declare const pullCommands: () => Promise<number | null>;
92
- export declare const pushCommands: () => Promise<number | null>;
115
+ export declare const pushPersonalities: () => Promise<SyncResult>;
116
+ export declare const pullCommands: () => Promise<SyncResult>;
117
+ export declare const pushCommands: () => Promise<SyncResult>;
93
118
  /**
94
119
  * Sync session conversation history to codeep.dev.
95
120
  * Only user/assistant messages are sent — system messages are filtered out.
@@ -184,4 +209,5 @@ export declare const _globalDirForTest: typeof globalDir;
184
209
  export declare const _readFileBundleForTest: typeof readFileBundle;
185
210
  export declare const _writeFileBundleForTest: typeof writeFileBundle;
186
211
  export declare const _writePulledPersonalityBundleForTest: typeof writePulledPersonalityBundle;
212
+ export declare const _applyPersonalityTombstonesForTest: typeof applyPersonalityTombstones;
187
213
  export {};
@@ -301,6 +301,52 @@ function writeFileBundle(kind, items) {
301
301
  * file so web edits actually take effect, but every divergent local body is
302
302
  * first copied to ~/.codeep/backups/personalities/. */
303
303
  let lastPersonalityPullBackupCount = 0;
304
+ /** Copy an about-to-be-replaced-or-removed personality into the backup dir.
305
+ * Shared so a deletion is backed up by exactly the same rules as an overwrite
306
+ * — nothing local is ever lost without a copy first. */
307
+ function backupLocalPersonality(name, body) {
308
+ const backupDir = join(homedir(), '.codeep', 'backups', 'personalities');
309
+ if (!existsSync(backupDir))
310
+ mkdirSync(backupDir, { recursive: true });
311
+ const suffix = new Date().toISOString().replace(/[:.]/g, '-');
312
+ let backupPath = join(backupDir, `${name}-${suffix}.md`);
313
+ let collision = 1;
314
+ while (existsSync(backupPath)) {
315
+ backupPath = join(backupDir, `${name}-${suffix}-${collision++}.md`);
316
+ }
317
+ writeFileSync(backupPath, body);
318
+ lastPersonalityPullBackupCount++;
319
+ }
320
+ /** Apply the server's explicit deletion list.
321
+ *
322
+ * Only names the server named. Absence from `items` is deliberately NOT a
323
+ * deletion signal: an expired session, the wrong account, or a truncated
324
+ * response all yield an empty `items`, and deleting on absence would wipe
325
+ * every local agent. Project-scoped agents in `.codeep/personalities/` are
326
+ * not cloud-owned and are never touched — only the global directory is.
327
+ * Every removal is backed up first, and a failed backup cancels the delete. */
328
+ function applyPersonalityTombstones(deleted) {
329
+ const dir = globalDir('personalities');
330
+ if (!existsSync(dir))
331
+ return 0;
332
+ let removed = 0;
333
+ for (const name of deleted) {
334
+ if (typeof name !== 'string' || !/^[a-z0-9][a-z0-9-]*$/.test(name) || name.length > 64)
335
+ continue;
336
+ const filePath = join(dir, `${name}.md`);
337
+ if (!existsSync(filePath))
338
+ continue;
339
+ try {
340
+ backupLocalPersonality(name, readFileSync(filePath, 'utf8'));
341
+ unlinkSync(filePath);
342
+ removed++;
343
+ }
344
+ catch {
345
+ // A failed backup must not become a deletion — leave the file alone.
346
+ }
347
+ }
348
+ return removed;
349
+ }
304
350
  function writePulledPersonalityBundle(items) {
305
351
  lastPersonalityPullBackupCount = 0;
306
352
  const dir = globalDir('personalities');
@@ -317,17 +363,7 @@ function writePulledPersonalityBundle(items) {
317
363
  const local = readFileSync(filePath, 'utf8');
318
364
  if (local === body)
319
365
  continue;
320
- const backupDir = join(homedir(), '.codeep', 'backups', 'personalities');
321
- if (!existsSync(backupDir))
322
- mkdirSync(backupDir, { recursive: true });
323
- let suffix = new Date().toISOString().replace(/[:.]/g, '-');
324
- let backupPath = join(backupDir, `${name}-${suffix}.md`);
325
- let collision = 1;
326
- while (existsSync(backupPath)) {
327
- backupPath = join(backupDir, `${name}-${suffix}-${collision++}.md`);
328
- }
329
- writeFileSync(backupPath, local);
330
- lastPersonalityPullBackupCount++;
366
+ backupLocalPersonality(name, local);
331
367
  }
332
368
  // Same-directory rename is atomic on supported local filesystems: a
333
369
  // crash cannot leave a half-written active personality.
@@ -348,39 +384,54 @@ function writePulledPersonalityBundle(items) {
348
384
  }
349
385
  return written;
350
386
  }
387
+ export function describeSyncFailure(reason) {
388
+ switch (reason) {
389
+ case 'not-linked': return 'not linked to codeep.dev — run: codeep account';
390
+ case 'unreachable': return "couldn't reach codeep.dev";
391
+ case 'rejected': return 'codeep.dev refused the request — try signing in again';
392
+ case 'malformed': return 'codeep.dev sent a response this version cannot read';
393
+ }
394
+ }
351
395
  async function pullBundle(kind) {
352
396
  const syncToken = getSyncToken();
353
397
  if (!syncToken)
354
- return null;
398
+ return { ok: false, reason: 'not-linked' };
355
399
  const res = await fetchWithRetry(`${API_BASE}/api/${kind}`, { headers: { 'x-sync-token': syncToken } });
356
400
  if (!res?.ok)
357
- return null;
401
+ return { ok: false, reason: 'unreachable' };
358
402
  try {
359
403
  const data = await res.json();
360
404
  if (!data.ok)
361
- return null;
362
- return kind === 'personalities'
405
+ return { ok: false, reason: 'rejected' };
406
+ const count = kind === 'personalities'
363
407
  ? writePulledPersonalityBundle(data.items ?? {})
364
408
  : writeFileBundle(kind, data.items ?? {});
409
+ // A missing `deleted` field means "no deletions" — never "delete
410
+ // everything". Older servers simply omit it, and a client that treated the
411
+ // omission as a full tombstone list would empty the user's agent folder.
412
+ const removed = kind === 'personalities' && Array.isArray(data.deleted)
413
+ ? applyPersonalityTombstones(data.deleted)
414
+ : 0;
415
+ return { ok: true, count, removed };
365
416
  }
366
417
  catch {
367
- return null;
418
+ return { ok: false, reason: 'malformed' };
368
419
  }
369
420
  }
370
421
  async function pushBundle(kind) {
371
422
  const syncToken = getSyncToken();
372
423
  if (!syncToken)
373
- return null;
424
+ return { ok: false, reason: 'not-linked' };
374
425
  const items = readFileBundle(kind);
375
426
  const count = Object.keys(items).length;
376
427
  if (count === 0)
377
- return 0;
428
+ return { ok: true, count: 0, removed: 0 };
378
429
  const res = await fetchWithRetry(`${API_BASE}/api/${kind}`, {
379
430
  method: 'POST',
380
431
  headers: { 'Content-Type': 'application/json', 'x-sync-token': syncToken },
381
432
  body: JSON.stringify({ items }),
382
433
  });
383
- return res?.ok ? count : null;
434
+ return res?.ok ? { ok: true, count, removed: 0 } : { ok: false, reason: 'unreachable' };
384
435
  }
385
436
  export const pullPersonalities = () => pullBundle('personalities');
386
437
  export const getLastPersonalityPullBackupCount = () => lastPersonalityPullBackupCount;
@@ -699,3 +750,4 @@ export const _globalDirForTest = globalDir;
699
750
  export const _readFileBundleForTest = readFileBundle;
700
751
  export const _writeFileBundleForTest = writeFileBundle;
701
752
  export const _writePulledPersonalityBundleForTest = writePulledPersonalityBundle;
753
+ export const _applyPersonalityTombstonesForTest = applyPersonalityTombstones;
@@ -15,7 +15,6 @@ export function generateDiff(oldContent, newContent, contextLines = 3) {
15
15
  let oldIdx = 0;
16
16
  let newIdx = 0;
17
17
  let currentHunk = null;
18
- let pendingContext = [];
19
18
  for (const [oldMatch, newMatch] of lcs) {
20
19
  // Handle deletions
21
20
  while (oldIdx < oldMatch) {
package/dist/utils/git.js CHANGED
@@ -286,7 +286,6 @@ export function generateCommitMessage(prompt, actions) {
286
286
  const hasWrites = actions.some(a => a.type === 'write');
287
287
  const hasEdits = actions.some(a => a.type === 'edit');
288
288
  const hasDeletes = actions.some(a => a.type === 'delete');
289
- const hasCommands = actions.some(a => a.type === 'command');
290
289
  // Determine prefix
291
290
  let prefix = 'chore';
292
291
  // Check prompt for common patterns
@@ -32,7 +32,6 @@ export declare class StreamableHttpClient {
32
32
  private notificationAbort;
33
33
  private stopped;
34
34
  /** True after the server has set a session id (i.e. it tracks state). */
35
- private get hasServerSession();
36
35
  constructor(opts: StreamableHttpOptions);
37
36
  /**
38
37
  * Issue a JSON-RPC frame as POST. Reply may be a single JSON response
@@ -28,9 +28,6 @@ export class StreamableHttpClient {
28
28
  notificationAbort = null;
29
29
  stopped = false;
30
30
  /** True after the server has set a session id (i.e. it tracks state). */
31
- get hasServerSession() {
32
- return this.sessionId !== null;
33
- }
34
31
  constructor(opts) {
35
32
  this.opts = opts;
36
33
  }
@@ -648,7 +648,6 @@ export function isPersonalityToolCallAllowed(personality, toolCall, registeredMc
648
648
  if (tool === 'execute_command') {
649
649
  if (personality.tools?.includes('terminal'))
650
650
  return true;
651
- const command = commandName(toolCall);
652
651
  if (personality.tools?.includes('git') && isRestrictedGitCommandAllowed(toolCall))
653
652
  return true;
654
653
  if (personality.tools?.includes('tests') && isTestCommand(toolCall))
@@ -52,12 +52,10 @@ export function parseFrontmatter(raw) {
52
52
  return { meta: {}, body: normalised };
53
53
  const meta = {};
54
54
  const lines = match[1].split('\n');
55
- let currentKey = null;
56
55
  let currentList = null;
57
56
  for (const rawLine of lines) {
58
57
  const line = rawLine.replace(/\s+$/, '');
59
58
  if (!line.trim()) {
60
- currentKey = null;
61
59
  currentList = null;
62
60
  continue;
63
61
  }
@@ -75,7 +73,6 @@ export function parseFrontmatter(raw) {
75
73
  let value = kv[2];
76
74
  if (value === '') {
77
75
  // Empty → expecting a block list below
78
- currentKey = key;
79
76
  currentList = [];
80
77
  meta[key] = currentList;
81
78
  continue;
@@ -89,7 +86,6 @@ export function parseFrontmatter(raw) {
89
86
  value = stripQuotes(value);
90
87
  }
91
88
  meta[key] = value;
92
- currentKey = null;
93
89
  currentList = null;
94
90
  }
95
91
  return { meta, body: match[2].trimStart() };
@@ -9,24 +9,6 @@ import { logger } from './logger.js';
9
9
  const MAX_CONTEXT_SIZE = 50000;
10
10
  const MAX_FILES = 15;
11
11
  // File extensions we care about
12
- const CODE_EXTENSIONS = new Set([
13
- '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs',
14
- '.py', '.pyw',
15
- '.go',
16
- '.rs',
17
- '.php', '.phtml',
18
- '.java', '.kt', '.scala',
19
- '.cs', '.fs',
20
- '.rb',
21
- '.swift',
22
- '.c', '.cpp', '.h', '.hpp',
23
- '.vue', '.svelte',
24
- '.css', '.scss', '.less',
25
- '.html', '.htm',
26
- '.json', '.yaml', '.yml', '.toml',
27
- '.sql',
28
- '.md',
29
- ]);
30
12
  /**
31
13
  * Extract imports/requires from file content
32
14
  */
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "2.20.0";
1
+ export declare const VERSION = "2.21.0";
package/dist/version.js CHANGED
@@ -1,4 +1,4 @@
1
1
  // AUTO-GENERATED by scripts/gen-version.js — do not edit by hand.
2
2
  // Baked from package.json at build time so the bun-compiled binary reports
3
3
  // the right version (it has no package.json on disk to read at runtime).
4
- export const VERSION = '2.20.0';
4
+ export const VERSION = '2.21.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeep",
3
- "version": "2.20.0",
3
+ "version": "2.21.0",
4
4
  "description": "AI-powered coding assistant built for the terminal. Multiple LLM providers, project-aware context, and a seamless development workflow.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -42,7 +42,7 @@
42
42
  "@napi-rs/keyring": "^1.3.0",
43
43
  "clipboardy": "^4.0.0",
44
44
  "conf": "^13.1.0",
45
- "js-yaml": "^4.1.0",
45
+ "js-yaml": "^4.3.1",
46
46
  "open": "^10.0.0"
47
47
  },
48
48
  "devDependencies": {
@@ -1,24 +0,0 @@
1
- /**
2
- * Permission screen for granting folder access
3
- */
4
- import { Screen } from '../Screen';
5
- export type PermissionLevel = 'none' | 'read' | 'write';
6
- export interface PermissionOptions {
7
- projectPath: string;
8
- isProject: boolean;
9
- currentPermission: PermissionLevel;
10
- onSelect: (permission: PermissionLevel) => void;
11
- onCancel: () => void;
12
- }
13
- /**
14
- * Render permission screen
15
- */
16
- export declare function renderPermissionScreen(screen: Screen, options: PermissionOptions, selectedIndex: number): void;
17
- /**
18
- * Get permission options array for easy indexing
19
- */
20
- export declare function getPermissionOptions(): PermissionLevel[];
21
- /**
22
- * Truncate path for display
23
- */
24
- export declare function truncatePath(path: string, maxLen: number): string;
@@ -1,113 +0,0 @@
1
- /**
2
- * Permission screen for granting folder access
3
- */
4
- import { fg, style } from '../ansi.js';
5
- import { createBox, centerBox } from './Box.js';
6
- // Primary color: #f02a30 (Codeep red)
7
- const PRIMARY_COLOR = fg.rgb(240, 42, 48);
8
- const PRIMARY_BRIGHT = fg.rgb(255, 80, 85);
9
- /**
10
- * Render permission screen
11
- */
12
- export function renderPermissionScreen(screen, options, selectedIndex) {
13
- const { width, height } = screen.getSize();
14
- screen.clear();
15
- // Title
16
- const title = '═══ Folder Access ═══';
17
- const titleX = Math.floor((width - title.length) / 2);
18
- screen.write(titleX, 1, title, PRIMARY_COLOR + style.bold);
19
- // Box
20
- const boxWidth = Math.min(60, width - 4);
21
- const boxHeight = 14;
22
- const { x: boxX, y: boxY } = centerBox(width, height, boxWidth, boxHeight);
23
- const boxLines = createBox({
24
- x: boxX,
25
- y: boxY,
26
- width: boxWidth,
27
- height: boxHeight,
28
- style: 'rounded',
29
- borderColor: PRIMARY_COLOR,
30
- });
31
- for (const line of boxLines) {
32
- screen.writeLine(line.y, line.text, line.style);
33
- }
34
- // Content
35
- const contentX = boxX + 3;
36
- let contentY = boxY + 2;
37
- // Project path
38
- const displayPath = truncatePath(options.projectPath, boxWidth - 8);
39
- screen.write(contentX, contentY, 'Project:', fg.gray);
40
- screen.write(contentX + 9, contentY, displayPath, fg.white);
41
- contentY += 2;
42
- // Description
43
- if (options.isProject) {
44
- screen.write(contentX, contentY, 'This looks like a project folder.', fg.white);
45
- }
46
- else {
47
- screen.write(contentX, contentY, 'Grant access to enable AI assistance.', fg.white);
48
- }
49
- contentY += 2;
50
- // Options
51
- const permissionOptions = [
52
- {
53
- level: 'read',
54
- label: 'Read Only',
55
- desc: 'AI can read files, no modifications'
56
- },
57
- {
58
- level: 'write',
59
- label: 'Read & Write',
60
- desc: 'AI can read and modify files (Agent mode)'
61
- },
62
- {
63
- level: 'none',
64
- label: 'No Access',
65
- desc: 'Chat without project context'
66
- },
67
- ];
68
- for (let i = 0; i < permissionOptions.length; i++) {
69
- const opt = permissionOptions[i];
70
- const isSelected = i === selectedIndex;
71
- const prefix = isSelected ? '► ' : ' ';
72
- // Label
73
- const labelStyle = isSelected ? PRIMARY_BRIGHT + style.bold : fg.white;
74
- screen.write(contentX, contentY, prefix + opt.label, labelStyle);
75
- // Description on same line
76
- const descX = contentX + 20;
77
- screen.write(descX, contentY, opt.desc, fg.gray);
78
- contentY++;
79
- }
80
- // Current permission indicator
81
- contentY++;
82
- if (options.currentPermission !== 'none') {
83
- screen.write(contentX, contentY, `Current: ${options.currentPermission}`, fg.yellow);
84
- }
85
- // Footer
86
- const footerY = height - 2;
87
- screen.write(2, footerY, '↑↓ Navigate | Enter Select | Esc Skip', fg.gray);
88
- screen.showCursor(false);
89
- screen.fullRender();
90
- }
91
- /**
92
- * Get permission options array for easy indexing
93
- */
94
- export function getPermissionOptions() {
95
- return ['read', 'write', 'none'];
96
- }
97
- /**
98
- * Truncate path for display
99
- */
100
- export function truncatePath(path, maxLen) {
101
- if (path.length <= maxLen)
102
- return path;
103
- const parts = path.split('/');
104
- let result = parts[parts.length - 1];
105
- for (let i = parts.length - 2; i >= 0; i--) {
106
- const newResult = parts[i] + '/' + result;
107
- if (newResult.length + 3 > maxLen) {
108
- return '.../' + result;
109
- }
110
- result = newResult;
111
- }
112
- return result;
113
- }