codeep 3.3.3 → 3.4.1

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.
Files changed (79) hide show
  1. package/dist/acp/commands.d.ts +50 -1
  2. package/dist/acp/commands.js +545 -109
  3. package/dist/acp/protocol.d.ts +14 -5
  4. package/dist/acp/server.d.ts +36 -1
  5. package/dist/acp/server.js +581 -155
  6. package/dist/acp/serverHandlers.d.ts +2 -1
  7. package/dist/acp/serverHandlers.js +3 -0
  8. package/dist/acp/session.d.ts +28 -2
  9. package/dist/acp/session.js +25 -6
  10. package/dist/acp/transport.d.ts +40 -4
  11. package/dist/acp/transport.js +218 -25
  12. package/dist/acp/turns.d.ts +20 -0
  13. package/dist/acp/turns.js +30 -0
  14. package/dist/api/index.js +2 -0
  15. package/dist/api/ollamaNative.d.ts +3 -0
  16. package/dist/api/ollamaNative.js +35 -3
  17. package/dist/config/index.d.ts +21 -4
  18. package/dist/config/index.js +178 -123
  19. package/dist/renderer/agentExecution.d.ts +30 -2
  20. package/dist/renderer/agentExecution.js +248 -92
  21. package/dist/renderer/commands/helpers.d.ts +18 -2
  22. package/dist/renderer/commands/helpers.js +28 -5
  23. package/dist/renderer/commands.d.ts +2 -0
  24. package/dist/renderer/commands.js +180 -64
  25. package/dist/renderer/main.d.ts +41 -0
  26. package/dist/renderer/main.js +181 -80
  27. package/dist/utils/agent.d.ts +69 -4
  28. package/dist/utils/agent.js +416 -248
  29. package/dist/utils/agentChat.js +82 -10
  30. package/dist/utils/agents.d.ts +2 -1
  31. package/dist/utils/agents.js +100 -29
  32. package/dist/utils/auditLog.d.ts +4 -3
  33. package/dist/utils/auditLog.js +92 -9
  34. package/dist/utils/checkpoints.js +11 -6
  35. package/dist/utils/codeReview.js +28 -23
  36. package/dist/utils/codeepCloud.d.ts +14 -2
  37. package/dist/utils/codeepCloud.js +56 -20
  38. package/dist/utils/customCommands.js +7 -2
  39. package/dist/utils/git.d.ts +262 -4
  40. package/dist/utils/git.js +1928 -61
  41. package/dist/utils/gitHookInstaller.d.ts +32 -1
  42. package/dist/utils/gitHookInstaller.js +76 -8
  43. package/dist/utils/gitignore.d.ts +8 -0
  44. package/dist/utils/gitignore.js +41 -10
  45. package/dist/utils/headlessReview.d.ts +11 -0
  46. package/dist/utils/headlessReview.js +33 -5
  47. package/dist/utils/history.d.ts +22 -6
  48. package/dist/utils/history.js +140 -26
  49. package/dist/utils/logger.js +6 -7
  50. package/dist/utils/mcpConfig.d.ts +24 -0
  51. package/dist/utils/mcpConfig.js +36 -5
  52. package/dist/utils/mentions.d.ts +28 -5
  53. package/dist/utils/mentions.js +253 -45
  54. package/dist/utils/personalities.js +16 -6
  55. package/dist/utils/planMode.d.ts +13 -7
  56. package/dist/utils/planMode.js +32 -12
  57. package/dist/utils/projectIntelligence.d.ts +2 -0
  58. package/dist/utils/projectIntelligence.js +27 -8
  59. package/dist/utils/projectPaths.d.ts +53 -0
  60. package/dist/utils/projectPaths.js +146 -0
  61. package/dist/utils/shell.d.ts +119 -0
  62. package/dist/utils/shell.js +417 -45
  63. package/dist/utils/skillBundles.js +17 -7
  64. package/dist/utils/skillBundlesCloud.js +20 -3
  65. package/dist/utils/skills.d.ts +24 -2
  66. package/dist/utils/skills.js +235 -43
  67. package/dist/utils/smartContext.js +97 -23
  68. package/dist/utils/telegramApproval.d.ts +10 -2
  69. package/dist/utils/telegramApproval.js +22 -4
  70. package/dist/utils/toolExecution.d.ts +50 -2
  71. package/dist/utils/toolExecution.js +418 -16
  72. package/dist/utils/toolParsing.d.ts +7 -1
  73. package/dist/utils/toolParsing.js +12 -3
  74. package/dist/utils/userProfile.js +58 -16
  75. package/dist/utils/verify.d.ts +25 -4
  76. package/dist/utils/verify.js +259 -74
  77. package/dist/version.d.ts +1 -1
  78. package/dist/version.js +1 -1
  79. package/package.json +1 -1
@@ -5,16 +5,16 @@
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, saveSession, startNewSession, loadSession, listSessionsWithInfo, deleteSession, renameSession, sessionNameProblem, setProjectPermission, saveProfile, loadProfile, applyProfile, listProfiles, deleteProfile, initializeAsProject, isManuallyInitializedProject, } from '../config/index.js';
9
9
  import { setTelegramToken, clearTelegramToken, hasTelegramToken } from '../utils/telegramCredentials.js';
10
10
  import { getProjectContext } from '../utils/project.js';
11
11
  import { getCurrentVersion } from '../utils/update.js';
12
12
  import { getProviderList, getProvider, modelSupportsReasoningEffort, reasoningParamsFor, availableReasoningTiers, resolveReasoningTier, REASONING_TIERS } from '../config/providers.js';
13
13
  import { setProjectContext } from '../api/index.js';
14
14
  import { runSkill, runCommandChain } from './agentExecution.js';
15
- import { loadProjectIntelligence, saveProjectIntelligence } from '../utils/projectIntelligence.js';
15
+ import { loadProjectIntelligence, saveProjectIntelligence, INTELLIGENCE_NOT_SAVED } from '../utils/projectIntelligence.js';
16
16
  import { ollamaModelHint } from './ollamaHint.js';
17
- import { buildSearchSnippets, parseKeepRecent, joinSessionName, parseTaskAddArgs, formatTaskList, formatProfileList, formatMemoryList, formatStatsReport, extractCodeBlocks, resolveBlockIndex, extractFileChanges, formatApplyDiffLine, parsePromptArgs, formatMcpReloadReport, formatMcpResourcesList, formatMcpResourceRead, formatMcpPromptsList, formatMcpPromptResult, formatMcpServerList, parseInsightsDays, formatCloudSessionLabel, formatMeSyncReport, formatMeLearnResult, formatMeInitResult, formatSkillsShow, formatSkillsBrowseEmpty, formatSkillsPublishResult } from './commands/helpers.js';
17
+ import { buildSearchSnippets, parseKeepRecent, joinSessionName, parseTaskAddArgs, formatTaskList, formatProfileList, formatMemoryList, formatStatsReport, extractCodeBlocks, resolveBlockIndex, extractFileChanges, formatApplyDiffLine, parsePromptArgs, formatMcpReloadReport, formatMcpResourcesList, formatMcpResourceRead, formatMcpPromptsList, formatMcpPromptResult, formatMcpServerList, parseInsightsDays, formatCloudSessionLabel, formatMeSyncReport, formatUndoAllReport, formatMeLearnResult, formatMeInitResult, formatSkillsShow, formatSkillsBrowseEmpty, formatSkillsPublishResult } from './commands/helpers.js';
18
18
  import { resolveCommand } from './commands/registry.js';
19
19
  import { telemetryCommand } from '../commands/core/telemetry.js';
20
20
  import { keysyncCommand } from '../commands/core/keysync.js';
@@ -523,11 +523,13 @@ export async function handleCommand(command, args, ctx) {
523
523
  ctx.app.notify('Not linked to codeep.dev. Run: codeep account');
524
524
  break;
525
525
  }
526
- const { pushUserProfile, pullUserProfile } = await import('../utils/codeepCloud.js');
526
+ const { pushUserProfileResult, pullUserProfileResult, describeSyncFailure } = await import('../utils/codeepCloud.js');
527
527
  ctx.app.notify('Syncing your profile with codeep.dev…');
528
- const pushed = await pushUserProfile();
529
- const pulled = await pullUserProfile();
530
- ctx.app.addMessage({ role: 'system', content: formatMeSyncReport(pushed, pulled) });
528
+ // Push first: the pull may create the local file, and pushing that
529
+ // back would be pointless.
530
+ const pushed = await pushUserProfileResult();
531
+ const pulled = await pullUserProfileResult();
532
+ ctx.app.addMessage({ role: 'system', content: formatMeSyncReport(pushed, pulled, describeSyncFailure) });
531
533
  break;
532
534
  }
533
535
  if (sub === 'init') {
@@ -601,10 +603,17 @@ export async function handleCommand(command, args, ctx) {
601
603
  return;
602
604
  }
603
605
  const prompt = composeExecutionPrompt(cur);
604
- clearPendingPlan();
605
- ctx.app.notify(`Executing plan for: ${cur.task.slice(0, 80)}${cur.task.length > 80 ? '…' : ''}`);
606
+ ctx.app.notify(`Executing plan for: ${cur.task.slice(0, 80)}${cur.task.length > 80 ? '…' : ''} — it stays pending until it has run successfully`);
606
607
  const { runAgentTask } = await import('./agentExecution.js');
607
- runAgentTask(prompt, false, ctx, () => null, () => { });
608
+ // The plan stays pending until it has run: a failed, stopped or
609
+ // declined run can be started again with /go.
610
+ runAgentTask(prompt, false, ctx, () => null, () => { }, {
611
+ onFinished: (outcome) => {
612
+ // A /plan issued meanwhile replaced it; that one has not run.
613
+ if (outcome === 'success' && getPendingPlan() === cur)
614
+ clearPendingPlan();
615
+ },
616
+ });
608
617
  break;
609
618
  }
610
619
  case 'stop': {
@@ -759,7 +768,6 @@ export async function handleCommand(command, args, ctx) {
759
768
  if (local && local.length > 0) {
760
769
  ctx.app.setMessages(local);
761
770
  ctx.setSessionId(localName);
762
- config.set('currentSessionId', localName);
763
771
  ctx.setSessionDisplayName?.(selected.sessionName ?? null);
764
772
  ctx.app.notify('Local copy is newer than the cloud record — loaded the local session instead.');
765
773
  return;
@@ -770,11 +778,11 @@ export async function handleCommand(command, args, ctx) {
770
778
  saveSession(localName, history, ctx.projectPath);
771
779
  ctx.app.setMessages(history);
772
780
  // Keep ALL session-identity state in step, not just the renderer's
773
- // copy: autosave + agent-mode sync read config.currentSessionId, and
774
- // the next syncSession reads the display name — leaving either stale
775
- // writes/renames the pulled history under the PREVIOUS session.
781
+ // copy: autosave + agent-mode sync read config.currentSessionId (which
782
+ // setSessionId updates), and the next syncSession reads the display
783
+ // name — leaving either stale writes/renames the pulled history under
784
+ // the PREVIOUS session.
776
785
  ctx.setSessionId(localName);
777
- config.set('currentSessionId', localName);
778
786
  ctx.setSessionDisplayName?.(selected.sessionName ?? null);
779
787
  ctx.app.notify(`Resumed from cloud: ${selected.sessionName || localName}`);
780
788
  };
@@ -819,15 +827,21 @@ export async function handleCommand(command, args, ctx) {
819
827
  }
820
828
  case 'undo': {
821
829
  import('../utils/agent.js').then(({ undoLastAction }) => {
822
- const result = undoLastAction();
830
+ // Only a run in this workspace; the agent records runs under this root.
831
+ const result = undoLastAction(ctx.projectContext?.root || ctx.projectPath);
823
832
  ctx.app.notify(result.success ? `Undo: ${result.message}` : `Cannot undo: ${result.message}`);
824
833
  });
825
834
  break;
826
835
  }
827
836
  case 'undo-all': {
828
837
  import('../utils/agent.js').then(({ undoAllActions }) => {
829
- const result = undoAllActions();
830
- ctx.app.notify(result.success ? `Undone ${result.results.length} action(s)` : 'Nothing to undo');
838
+ const result = undoAllActions(ctx.projectContext?.root || ctx.projectPath);
839
+ if (!result.success && result.results.length <= 1) {
840
+ ctx.app.notify(result.results[0] ?? 'Nothing to undo');
841
+ }
842
+ else {
843
+ ctx.app.addMessage({ role: 'system', content: formatUndoAllReport(result) });
844
+ }
831
845
  });
832
846
  break;
833
847
  }
@@ -851,9 +865,12 @@ export async function handleCommand(command, args, ctx) {
851
865
  return;
852
866
  }
853
867
  ctx.app.notify('Scanning project...');
854
- import('../utils/projectIntelligence.js').then(({ scanProject, saveProjectIntelligence, generateContextFromIntelligence }) => {
868
+ import('../utils/projectIntelligence.js').then(({ scanProject, saveProjectIntelligence, generateContextFromIntelligence, INTELLIGENCE_NOT_SAVED }) => {
855
869
  scanProject(ctx.projectContext.root).then(intelligence => {
856
- saveProjectIntelligence(ctx.projectContext.root, intelligence);
870
+ if (!saveProjectIntelligence(ctx.projectContext.root, intelligence)) {
871
+ ctx.app.notify(INTELLIGENCE_NOT_SAVED);
872
+ return;
873
+ }
857
874
  const context = generateContextFromIntelligence(intelligence);
858
875
  ctx.app.addMessage({ role: 'assistant', content: `# Project Scan Complete\n\n${context}` });
859
876
  ctx.app.notify(`Scanned: ${intelligence.structure.totalFiles} files`);
@@ -935,6 +952,11 @@ Format: use headers per category, only include categories where you found issues
935
952
  return;
936
953
  }
937
954
  const newName = joinSessionName(args);
955
+ const nameProblem = sessionNameProblem(newName);
956
+ if (nameProblem) {
957
+ ctx.app.notify(nameProblem);
958
+ return;
959
+ }
938
960
  const messages = ctx.app.getMessages();
939
961
  if (messages.length === 0) {
940
962
  ctx.app.notify('No messages to save. Start a conversation first.');
@@ -959,7 +981,14 @@ Format: use headers per category, only include categories where you found issues
959
981
  }).catch(() => { });
960
982
  }
961
983
  else {
962
- ctx.app.notify('Failed to rename session');
984
+ // renameSession refuses to replace another saved conversation. Looked
985
+ // up only now: a case-only rename finds this same file and succeeds.
986
+ const { existsSync } = await import('fs');
987
+ const { join } = await import('path');
988
+ const { getSessionsDir } = await import('../config/index.js');
989
+ ctx.app.notify(existsSync(join(getSessionsDir(ctx.projectPath), `${newName}.json`))
990
+ ? `A session named "${newName}" already exists — pick another name`
991
+ : 'Failed to rename session');
963
992
  }
964
993
  break;
965
994
  }
@@ -1186,9 +1215,25 @@ Format: use headers per category, only include categories where you found issues
1186
1215
  ctx.app.notify('Usage: /git-commit <message>');
1187
1216
  return;
1188
1217
  }
1189
- // Use execFile to avoid shell injection — pass commit message as a direct argument
1190
- import('child_process').then(({ execFile }) => {
1191
- execFile('git', ['commit', '-m', message], { cwd: ctx.projectPath, encoding: 'utf-8' }, (err) => {
1218
+ // Use execFile to avoid shell injection — pass commit message as a direct argument.
1219
+ // hardenedGitEnv() on top of that: the repo's own .git/config can name programs
1220
+ // git would run for this commit (core.fsmonitor, core.hooksPath, …).
1221
+ Promise.all([import('child_process'), import('../utils/git.js')]).then(([{ execFile }, { hardenedGitEnv }]) => {
1222
+ // The SAME cwd the commit runs in. Called with no argument, the scan
1223
+ // read process.cwd() instead — so a project opened anywhere other
1224
+ // than the directory Codeep was launched from was "hardened" against
1225
+ // a different repository's config entirely.
1226
+ let env;
1227
+ try {
1228
+ env = hardenedGitEnv({ cwd: ctx.projectPath });
1229
+ }
1230
+ catch (error) {
1231
+ // It refuses rather than hand git a half-scanned environment, and
1232
+ // its message names the key and what to do about it.
1233
+ ctx.app.notify(error instanceof Error ? error.message : 'Commit failed');
1234
+ return;
1235
+ }
1236
+ execFile('git', ['commit', '-m', message], { cwd: ctx.projectPath, encoding: 'utf-8', env }, (err) => {
1192
1237
  if (err) {
1193
1238
  ctx.app.notify(`Commit failed: ${err.message}`);
1194
1239
  }
@@ -1510,7 +1555,8 @@ Format: use headers per category, only include categories where you found issues
1510
1555
  }
1511
1556
  case 'changes': {
1512
1557
  import('../utils/agent.js').then(({ getCurrentSessionActions }) => {
1513
- const actions = getCurrentSessionActions();
1558
+ // Changes of the run /undo acts on, in this project.
1559
+ const actions = getCurrentSessionActions(ctx.projectContext?.root || ctx.projectPath);
1514
1560
  if (actions.length === 0) {
1515
1561
  ctx.app.notify('No changes in current session');
1516
1562
  return;
@@ -1572,18 +1618,25 @@ Format: use headers per category, only include categories where you found issues
1572
1618
  }
1573
1619
  const name = args.join(' ').trim() || undefined;
1574
1620
  const provider = getCurrentProvider();
1575
- const filesTouched = Array.from(new Set(getCurrentSessionActions()
1576
- .filter(a => a.target && (a.type === 'write' || a.type === 'edit' || a.type === 'delete' || a.type === 'mkdir'))
1621
+ const filesTouched = Array.from(new Set(getCurrentSessionActions(ctx.projectContext?.root || ctx.projectPath)
1622
+ .filter(a => a.target && a.result !== 'undone' && (a.type === 'write' || a.type === 'edit' || a.type === 'delete' || a.type === 'mkdir'))
1577
1623
  .map(a => a.target)));
1578
- const cp = createCheckpoint({
1579
- workspaceRoot: ctx.projectPath,
1580
- sessionId: ctx.sessionId,
1581
- provider: provider.id,
1582
- model: config.get('model'),
1583
- messages: ctx.app.getMessages(),
1584
- filesTouched,
1585
- name,
1586
- });
1624
+ let cp;
1625
+ try {
1626
+ cp = createCheckpoint({
1627
+ workspaceRoot: ctx.projectPath,
1628
+ sessionId: ctx.sessionId,
1629
+ provider: provider.id,
1630
+ model: config.get('model'),
1631
+ messages: ctx.app.getMessages(),
1632
+ filesTouched,
1633
+ name,
1634
+ });
1635
+ }
1636
+ catch (err) {
1637
+ ctx.app.notify(`Could not save the checkpoint: ${err.message}`);
1638
+ break;
1639
+ }
1587
1640
  ctx.app.addMessage({
1588
1641
  role: 'system',
1589
1642
  content: `# Checkpoint created\n\n\`${cp.id}\`${cp.name ? ` — **${cp.name}**` : ''}\n\nCaptured ${cp.messages.length} message${cp.messages.length === 1 ? '' : 's'}, ${cp.filesTouched.length} file${cp.filesTouched.length === 1 ? '' : 's'} touched${cp.gitHead ? `, git \`${cp.gitHead}\`` : ''}.\n\nUse \`/rewind ${cp.id}\` to restore.`,
@@ -1731,7 +1784,7 @@ Format: use headers per category, only include categories where you found issues
1731
1784
  }
1732
1785
  if (args[0] === 'rule' && args.length > 1) {
1733
1786
  import('../utils/learning.js').then(({ addCustomRule }) => {
1734
- addCustomRule(ctx.projectPath, args.slice(1).join(' '));
1787
+ addCustomRule(args.slice(1).join(' '), ctx.projectPath);
1735
1788
  ctx.app.notify('Custom rule added');
1736
1789
  }).catch(() => ctx.app.notify('Learning module not available'));
1737
1790
  return;
@@ -1962,7 +2015,7 @@ Describe what this skill does. The agent reads this body verbatim when it invoke
1962
2015
  break;
1963
2016
  }
1964
2017
  case 'skill': {
1965
- import('../utils/skills.js').then(({ findSkill, formatSkillHelp, createSkillTemplate, saveCustomSkill, deleteCustomSkill, }) => {
2018
+ import('../utils/skills.js').then(({ findSkill, formatSkillHelp, createSkillTemplate, saveCustomSkill, deleteCustomSkill, customSkillFileExists, getSkippedCustomSkills, formatSkippedCustomSkills, }) => {
1966
2019
  const subCommand = args[0]?.toLowerCase();
1967
2020
  const skillName = args[1];
1968
2021
  if (!subCommand) {
@@ -1992,6 +2045,17 @@ Describe what this skill does. The agent reads this body verbatim when it invoke
1992
2045
  ctx.app.notify(`Skill "${skillName}" already exists`);
1993
2046
  return;
1994
2047
  }
2048
+ // A file that does not load is not found above, and is still the
2049
+ // user's work.
2050
+ if (customSkillFileExists(skillName)) {
2051
+ const skipped = getSkippedCustomSkills().filter(f => f.file === `${skillName}.json`);
2052
+ ctx.app.addMessage({
2053
+ role: 'system',
2054
+ content: `~/.codeep/skills/${skillName}.json already exists — not replaced.`
2055
+ + (skipped.length ? `\n\n${formatSkippedCustomSkills(skipped)}` : ''),
2056
+ });
2057
+ return;
2058
+ }
1995
2059
  const template = createSkillTemplate(skillName);
1996
2060
  saveCustomSkill(template);
1997
2061
  ctx.app.addMessage({
@@ -2300,10 +2364,18 @@ Describe what this skill does. The agent reads this body verbatim when it invoke
2300
2364
  // Sync the hand-written user profile (~/.codeep/profile.md). Push sends
2301
2365
  // the local file; pull is additive (writes only if no local profile).
2302
2366
  if (subCmd === 'all' || subCmd === 'profile') {
2303
- const { pushUserProfile, pullUserProfile } = await import('../utils/codeepCloud.js');
2304
- if (await pushUserProfile())
2367
+ const { pushUserProfileResult, pullUserProfileResult, describeSyncFailure } = await import('../utils/codeepCloud.js');
2368
+ // Without a local profile there is nothing to push, which is not a
2369
+ // failure. Pushed before the pull, which may create the file.
2370
+ const pushedProfile = await pushUserProfileResult();
2371
+ if (!pushedProfile.ok)
2372
+ results.push(`✗ Failed to push your profile (about you) — ${describeSyncFailure(pushedProfile.reason)}`);
2373
+ else if (pushedProfile.count > 0)
2305
2374
  results.push('✓ Your profile (about you) pushed');
2306
- if ((await pullUserProfile()) === 1)
2375
+ const pulledProfile = await pullUserProfileResult();
2376
+ if (!pulledProfile.ok)
2377
+ results.push(`✗ Failed to pull your profile (about you) — ${describeSyncFailure(pulledProfile.reason)}`);
2378
+ else if (pulledProfile.count > 0)
2307
2379
  results.push('✓ Your profile pulled to this machine');
2308
2380
  }
2309
2381
  ctx.app.addMessage({
@@ -2358,7 +2430,10 @@ Describe what this skill does. The agent reads this body verbatim when it invoke
2358
2430
  break;
2359
2431
  }
2360
2432
  const removed = intelligence.notes.splice(idx - 1, 1)[0];
2361
- saveProjectIntelligence(projectRoot, intelligence);
2433
+ if (!saveProjectIntelligence(projectRoot, intelligence)) {
2434
+ ctx.app.notify(INTELLIGENCE_NOT_SAVED);
2435
+ break;
2436
+ }
2362
2437
  import('../utils/codeepCloud.js').then(({ syncMemoryNotes }) => syncMemoryNotes(projectCtx?.name || '', intelligence.notes)).catch(() => { });
2363
2438
  ctx.app.notify(`Removed: "${removed}"`);
2364
2439
  break;
@@ -2366,7 +2441,10 @@ Describe what this skill does. The agent reads this body verbatim when it invoke
2366
2441
  if (sub === 'clear') {
2367
2442
  const count = intelligence.notes.length;
2368
2443
  intelligence.notes = [];
2369
- saveProjectIntelligence(projectRoot, intelligence);
2444
+ if (!saveProjectIntelligence(projectRoot, intelligence)) {
2445
+ ctx.app.notify(INTELLIGENCE_NOT_SAVED);
2446
+ break;
2447
+ }
2370
2448
  import('../utils/codeepCloud.js').then(({ syncMemoryNotes }) => syncMemoryNotes(projectCtx?.name || '', [])).catch(() => { });
2371
2449
  ctx.app.notify(`Cleared ${count} memory note${count !== 1 ? 's' : ''}.`);
2372
2450
  break;
@@ -2378,7 +2456,10 @@ Describe what this skill does. The agent reads this body verbatim when it invoke
2378
2456
  break;
2379
2457
  }
2380
2458
  intelligence.notes.push(note);
2381
- saveProjectIntelligence(projectRoot, intelligence);
2459
+ if (!saveProjectIntelligence(projectRoot, intelligence)) {
2460
+ ctx.app.notify(INTELLIGENCE_NOT_SAVED);
2461
+ break;
2462
+ }
2382
2463
  import('../utils/codeepCloud.js').then(({ syncMemoryNotes }) => syncMemoryNotes(projectCtx?.name || '', intelligence.notes)).catch(() => { });
2383
2464
  ctx.app.notify(`Memory saved (${intelligence.notes.length} total): "${note}"`);
2384
2465
  break;
@@ -2428,8 +2509,32 @@ Describe what this skill does. The agent reads this body verbatim when it invoke
2428
2509
  }
2429
2510
  return true;
2430
2511
  };
2431
- const { addProjectMcpServer, removeProjectMcpServer, loadMcpServerConfig, loadMcpServerConfigSplit, isWorkspaceMcpTrusted, trustWorkspaceMcp, untrustWorkspaceMcp } = await import('../utils/mcpConfig.js');
2512
+ const { addProjectMcpServer, removeProjectMcpServer, loadMcpServerConfigSplit, selectSessionMcpServers, isWorkspaceMcpTrusted, trustWorkspaceMcp, untrustWorkspaceMcp } = await import('../utils/mcpConfig.js');
2432
2513
  const { registerSessionServers } = await import('../utils/mcpRegistry.js');
2514
+ // The servers this session may run, chosen by the same rule startup and
2515
+ // ACP use: workspace entries arrive with the repo and start only once the
2516
+ // workspace is trusted. `addedHere` is an entry the user just typed into
2517
+ // /mcp add or /mcp install; starting that one needs no further consent.
2518
+ // registerSessionServers replaces the whole session, so everything that
2519
+ // should keep running has to be in the list.
2520
+ const serversToStart = (addedHere) => selectSessionMcpServers(projectPath, { userAdded: addedHere ? [addedHere] : [] });
2521
+ const untrustedNote = ({ servers, skipped }) => {
2522
+ if (skipped.length === 0)
2523
+ return '';
2524
+ const n = skipped.length;
2525
+ let note = `\n\n${n} workspace MCP server${n === 1 ? '' : 's'} not started — this workspace isn't trusted. Run \`/mcp trust\` to start ${n === 1 ? 'it' : 'them'}.`;
2526
+ // A repo entry that shares a name with one of the user's own servers
2527
+ // can keep that one from starting too; say so rather than let it look
2528
+ // like the user's server just vanished.
2529
+ const running = new Set(servers.map(s => s.name));
2530
+ const globalNames = new Set(loadMcpServerConfigSplit(undefined).global.map(s => s.name));
2531
+ const shadowed = skipped.map(s => s.name).filter(name => globalNames.has(name) && !running.has(name));
2532
+ if (shadowed.length > 0) {
2533
+ const list = shadowed.map(name => `\`${name}\``).join(', ');
2534
+ note += `\n\nYour own server${shadowed.length === 1 ? '' : 's'} ${list} ${shadowed.length === 1 ? 'is' : 'are'} not running either: this workspace defines a server with the same name, which takes its place once trusted.`;
2535
+ }
2536
+ return note;
2537
+ };
2433
2538
  if (sub === 'trust') {
2434
2539
  if (!requireProject())
2435
2540
  break;
@@ -2443,8 +2548,8 @@ Describe what this skill does. The agent reads this body verbatim when it invoke
2443
2548
  ctx.app.notify('Workspace trusted — no workspace MCP servers defined yet.');
2444
2549
  break;
2445
2550
  }
2446
- ctx.app.notify(`Workspace trusted. Spawning ${workspace.length} MCP server(s)…`);
2447
- const { registered, errors } = await registerSessionServers(TUI_SESSION, workspace, { workspaceRoot: projectPath });
2551
+ ctx.app.notify(`Workspace trusted. Restarting MCP servers with ${workspace.length} from this workspace…`);
2552
+ const { registered, errors } = await registerSessionServers(TUI_SESSION, serversToStart().servers, { workspaceRoot: projectPath });
2448
2553
  if (registered.length > 0)
2449
2554
  ctx.app.notify(`MCP: ${registered.length} tool(s) ready. Type /mcp.`);
2450
2555
  for (const e of errors)
@@ -2455,7 +2560,7 @@ Describe what this skill does. The agent reads this body verbatim when it invoke
2455
2560
  if (!requireProject())
2456
2561
  break;
2457
2562
  untrustWorkspaceMcp(projectPath);
2458
- ctx.app.notify('Workspace MCP trust revoked — workspace servers won\'t spawn on next start. (Running servers stop when you exit.)');
2563
+ ctx.app.notify('Workspace MCP trust revoked — workspace servers won\'t start again. Those running now stop at the next /mcp reload, add or remove, or when you exit.');
2459
2564
  break;
2460
2565
  }
2461
2566
  if (sub === 'add') {
@@ -2470,15 +2575,15 @@ Describe what this skill does. The agent reads this body verbatim when it invoke
2470
2575
  const extraArgs = args.slice(3);
2471
2576
  addProjectMcpServer(projectPath, { name, command, args: extraArgs });
2472
2577
  ctx.app.notify(`Saved MCP server ${name} to .codeep/mcp_servers.json. Spawning…`);
2473
- const merged = loadMcpServerConfig(projectPath);
2474
- const { registered, errors } = await registerSessionServers(TUI_SESSION, merged, { workspaceRoot: projectPath });
2578
+ const selection = serversToStart(name);
2579
+ const { registered, errors } = await registerSessionServers(TUI_SESSION, selection.servers, { workspaceRoot: projectPath });
2475
2580
  const ok = registered.filter(t => t.serverName === name);
2476
2581
  const failed = errors.find(e => e.server === name);
2477
2582
  ctx.app.addMessage({
2478
2583
  role: 'system',
2479
- content: failed
2584
+ content: (failed
2480
2585
  ? `Saved \`${name}\` but spawn failed: \`${failed.error}\``
2481
- : `Added \`${name}\` (${ok.length} tool${ok.length === 1 ? '' : 's'} available).`,
2586
+ : `Added \`${name}\` (${ok.length} tool${ok.length === 1 ? '' : 's'} available).`) + untrustedNote(selection),
2482
2587
  });
2483
2588
  break;
2484
2589
  }
@@ -2495,9 +2600,9 @@ Describe what this skill does. The agent reads this body verbatim when it invoke
2495
2600
  ctx.app.addMessage({ role: 'system', content: `No project-scoped MCP server named \`${name}\`.` });
2496
2601
  break;
2497
2602
  }
2498
- const merged = loadMcpServerConfig(projectPath);
2499
- await registerSessionServers(TUI_SESSION, merged, { workspaceRoot: projectPath });
2500
- ctx.app.addMessage({ role: 'system', content: `Removed \`${name}\` from project config and stopped its process.` });
2603
+ const selection = serversToStart();
2604
+ await registerSessionServers(TUI_SESSION, selection.servers, { workspaceRoot: projectPath });
2605
+ ctx.app.addMessage({ role: 'system', content: `Removed \`${name}\` from project config and stopped its process.` + untrustedNote(selection) });
2501
2606
  break;
2502
2607
  }
2503
2608
  if (sub === 'browse') {
@@ -2542,8 +2647,8 @@ Describe what this skill does. The agent reads this body verbatim when it invoke
2542
2647
  headers: entry.server.headers,
2543
2648
  });
2544
2649
  ctx.app.notify(`Saved ${entry.id} to project config. Spawning…`);
2545
- const merged = loadMcpServerConfig(projectPath);
2546
- const { registered, errors } = await registerSessionServers(TUI_SESSION, merged, { workspaceRoot: projectPath });
2650
+ const selection = serversToStart(entry.id);
2651
+ const { registered, errors } = await registerSessionServers(TUI_SESSION, selection.servers, { workspaceRoot: projectPath });
2547
2652
  const failed = errors.find(e => e.server === entry.id);
2548
2653
  const lines = [];
2549
2654
  if (failed) {
@@ -2560,16 +2665,16 @@ Describe what this skill does. The agent reads this body verbatim when it invoke
2560
2665
  lines.push(`- \`${e.name}\`${req} — ${e.description}`);
2561
2666
  }
2562
2667
  }
2563
- ctx.app.addMessage({ role: 'system', content: lines.join('\n') });
2668
+ ctx.app.addMessage({ role: 'system', content: lines.join('\n') + untrustedNote(selection) });
2564
2669
  break;
2565
2670
  }
2566
2671
  if (sub === 'reload') {
2567
2672
  if (!requireProject())
2568
2673
  break;
2569
2674
  ctx.app.notify('Reloading MCP server config…');
2570
- const merged = loadMcpServerConfig(projectPath);
2571
- const { registered, errors } = await registerSessionServers(TUI_SESSION, merged, { workspaceRoot: projectPath });
2572
- ctx.app.addMessage({ role: 'system', content: formatMcpReloadReport(registered.length, merged.length, errors) });
2675
+ const selection = serversToStart();
2676
+ const { registered, errors } = await registerSessionServers(TUI_SESSION, selection.servers, { workspaceRoot: projectPath });
2677
+ ctx.app.addMessage({ role: 'system', content: formatMcpReloadReport(registered.length, selection.servers.length, errors) + untrustedNote(selection) });
2573
2678
  break;
2574
2679
  }
2575
2680
  if (sub === 'resources') {
@@ -2641,9 +2746,20 @@ Describe what this skill does. The agent reads this body verbatim when it invoke
2641
2746
  break;
2642
2747
  }
2643
2748
  // 2. Fall through to skill registry.
2644
- runSkill(command, args, ctx).then(handled => {
2645
- if (!handled)
2646
- ctx.app.notify(`Unknown command: /${command}`);
2749
+ runSkill(command, args, ctx).then(async (handled) => {
2750
+ if (handled)
2751
+ return;
2752
+ ctx.app.notify(`Unknown command: /${command}`);
2753
+ // The command may be a custom skill whose file does not load. Scanned
2754
+ // afresh here rather than trusting whatever the lookup left behind.
2755
+ const { loadCustomSkills, getSkippedCustomSkills, formatSkippedCustomSkills } = await import('../utils/skills.js');
2756
+ loadCustomSkills();
2757
+ const skipped = getSkippedCustomSkills().filter(f => f.file.toLowerCase() === `${command.toLowerCase()}.json`);
2758
+ if (skipped.length > 0) {
2759
+ ctx.app.addMessage({ role: 'system', content: `Unknown command: /${command}\n\n${formatSkippedCustomSkills(skipped)}` });
2760
+ }
2761
+ }).catch(err => {
2762
+ ctx.app.notify(`Skill error: ${err.message}`);
2647
2763
  });
2648
2764
  }
2649
2765
  }
@@ -5,5 +5,46 @@
5
5
  * This file contains only startup/init logic. Command dispatch lives in
6
6
  * commands.ts and agent execution in agentExecution.ts.
7
7
  */
8
+ import { App } from './App';
9
+ import { type GitStatus } from '../utils/git';
10
+ import type { McpServer } from '../acp/protocol';
11
+ /**
12
+ * What to tell the user when git would not run in this project, or null when
13
+ * there is nothing to tell them.
14
+ *
15
+ * Without it the only symptom is the branch quietly missing from the header,
16
+ * which reads as "not a repository" — so a repository whose own `.git/config`
17
+ * names a program git would run looks like an ordinary folder, and the one
18
+ * thing the user has to do (remove that key) is never said anywhere. The
19
+ * refusal names the key and the `git config --unset` that clears it, so it is
20
+ * passed through verbatim rather than summarised into "git failed".
21
+ *
22
+ * Read off `refusal` and NOT off `error`, which is the field an earlier cut
23
+ * of this used. `error` is every way git can fail in a repository, and the
24
+ * commonest of them is a brand-new `git init` with no commit yet: `git
25
+ * rev-parse --abbrev-ref HEAD` answers `fatal: ambiguous argument 'HEAD'`
26
+ * there (git 2.54), so the first thing a user does in a new project met a
27
+ * warning made of git internals telling them to remove a config key that does
28
+ * not exist. `refusal` is filled on the hardening path and nowhere else, so
29
+ * an ordinary git failure stays as silent as it was before this notice
30
+ * existed — the branch is simply missing from the header, which is what it
31
+ * has always done.
32
+ */
33
+ export declare function gitRefusalNotice(status: GitStatus): string | null;
8
34
  /** Derive a short display name from a user message (first ~5 words, max 48 chars). */
9
35
  export declare function deriveSessionName(message: string): string;
36
+ /**
37
+ * Start the MCP servers this terminal may run, under the fixed session id
38
+ * `codeep-tui`: the global servers (~/.codeep, the user's own) plus the
39
+ * workspace ones once the workspace is trusted. Registering replaces the
40
+ * session's whole set, so they go in one call; starting the global list and
41
+ * then the workspace list stopped the global servers. Returns the workspace
42
+ * servers left out because the workspace is not trusted yet.
43
+ */
44
+ export declare function startTuiMcpServers(root: string, ui: Pick<App, 'notify' | 'notifyWarn'>): Promise<McpServer[]>;
45
+ /** Start the servers of a workspace the user has just trusted. A failure is
46
+ * shown, not swallowed: the user asked for these servers. */
47
+ export declare function startTrustedWorkspaceMcp(root: string, ui: Pick<App, 'notify' | 'notifyWarn'>): Promise<void>;
48
+ /** The CLI entry point. Exported so tests can drive a command without the
49
+ * module starting the app on import. */
50
+ export declare function main(): Promise<void>;