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
@@ -11,13 +11,13 @@ import { Input } from './Input.js';
11
11
  import { LoginScreen, renderProviderSelect } from './components/Login.js';
12
12
  import { chat, setProjectContext } from '../api/index.js';
13
13
  import { getZaiVisionConfig, getMinimaxMcpConfig, callZaiVisionApi, callMinimaxApi } from '../utils/mcpIntegration.js';
14
- import { config, loadApiKey, loadAllApiKeys, getCurrentProvider, autoSaveSession, startNewSession, getCurrentSessionId, loadSession, listSessionsWithInfo, deleteSession, hasReadPermission, hasWritePermission, setProjectPermission, initializeAsProject, isManuallyInitializedProject, setApiKey, setProvider, getGithubId, } from '../config/index.js';
14
+ import { config, loadApiKey, loadAllApiKeys, getCurrentProvider, autoSaveSession, flushAutoSave, startNewSession, getCurrentSessionId, loadSession, listSessionsWithInfo, deleteSession, hasReadPermission, hasWritePermission, setProjectPermission, initializeAsProject, isManuallyInitializedProject, setApiKey, setProvider, getGithubId, } from '../config/index.js';
15
15
  import { isProjectDirectory, getProjectContext, } from '../utils/project.js';
16
16
  import { getCurrentVersion, checkForUpdates, getUpdateInstructions } from '../utils/update.js';
17
17
  import { getProviderList, isNoApiKeyProvider, resolveReasoningTier } from '../config/providers.js';
18
18
  import { getSessionStats, getCostBreakdown, getRecordCount } from '../utils/tokenTracker.js';
19
19
  import { getGitStatus, isGitRepository } from '../utils/git.js';
20
- import { reportStats, syncSession, generateProjectId, ensureDeviceRegistered } from '../utils/codeepCloud.js';
20
+ import { reportStats, syncSession, syncSessionAsync, generateProjectId, ensureDeviceRegistered } from '../utils/codeepCloud.js';
21
21
  import { expandFileAndFolderMentions, expandGitMentions } from '../utils/mentions.js';
22
22
  import { expandWebMentions } from '../utils/webFetch.js';
23
23
  import { handleCommand as dispatchCommand } from './commands.js';
@@ -27,32 +27,123 @@ import { attachTelegramInbox } from '../utils/telegramInbox.js';
27
27
  import { sharedUpdates } from '../utils/telegramUpdates.js';
28
28
  import { sendTelegramNotice } from '../utils/telegramNotify.js';
29
29
  import { executeAgentTask, runAgentTask, } from './agentExecution.js';
30
+ import { symlinkedCodeepNotice } from '../utils/projectPaths.js';
30
31
  // ─── Global state ─────────────────────────────────────────────────────────────
31
32
  let projectPath = process.cwd();
32
33
  /** Cached header branch. Resolved on first use so `--version`/`--help` never
33
- * shell out to git, and cached because getStatus runs on every render frame. */
34
+ * shell out to git, and cached because getStatus runs on every render frame.
35
+ * `refusal` is the message getGitStatus carries back when git would not run
36
+ * here at all — see reportGitRefusal(). */
34
37
  let gitBranchCache = null;
38
+ /** Projects whose refusal has already been put in the transcript, so a cache
39
+ * dropped after every agent run does not repeat it once a minute. */
40
+ const gitRefusalReported = new Set();
35
41
  let projectContext = null;
36
42
  let hasWriteAccess = false;
37
43
  let sessionId = getCurrentSessionId();
38
44
  let app;
39
45
  /** Human-readable session name derived from the first user message */
40
46
  let sessionDisplayName = null;
47
+ /** Switch the conversation this terminal is on. The config copy is kept in
48
+ * step because the next launch resumes from it, and anything still reading
49
+ * the global id would otherwise save into the conversation left behind. */
50
+ function setSessionId(id) {
51
+ sessionId = id;
52
+ config.set('currentSessionId', id);
53
+ }
41
54
  const addedFiles = new Map();
42
55
  /** Branch shown in the persistent header, re-read whenever the project moved
43
56
  * or an agent run finished (an agent can check out a different branch). */
44
57
  function getHeaderBranch() {
45
58
  if (!gitBranchCache || gitBranchCache.path !== projectPath) {
46
- gitBranchCache = { path: projectPath, branch: getGitStatus(projectPath).branch };
59
+ const status = getGitStatus(projectPath);
60
+ const refusal = gitRefusalNotice(status);
61
+ gitBranchCache = { path: projectPath, branch: status.branch, refusal: refusal ?? undefined };
62
+ reportGitRefusal(projectPath, refusal);
47
63
  }
48
64
  return gitBranchCache.branch;
49
65
  }
66
+ /**
67
+ * What to tell the user when git would not run in this project, or null when
68
+ * there is nothing to tell them.
69
+ *
70
+ * Without it the only symptom is the branch quietly missing from the header,
71
+ * which reads as "not a repository" — so a repository whose own `.git/config`
72
+ * names a program git would run looks like an ordinary folder, and the one
73
+ * thing the user has to do (remove that key) is never said anywhere. The
74
+ * refusal names the key and the `git config --unset` that clears it, so it is
75
+ * passed through verbatim rather than summarised into "git failed".
76
+ *
77
+ * Read off `refusal` and NOT off `error`, which is the field an earlier cut
78
+ * of this used. `error` is every way git can fail in a repository, and the
79
+ * commonest of them is a brand-new `git init` with no commit yet: `git
80
+ * rev-parse --abbrev-ref HEAD` answers `fatal: ambiguous argument 'HEAD'`
81
+ * there (git 2.54), so the first thing a user does in a new project met a
82
+ * warning made of git internals telling them to remove a config key that does
83
+ * not exist. `refusal` is filled on the hardening path and nowhere else, so
84
+ * an ordinary git failure stays as silent as it was before this notice
85
+ * existed — the branch is simply missing from the header, which is what it
86
+ * has always done.
87
+ */
88
+ export function gitRefusalNotice(status) {
89
+ const refusal = status.isRepo ? status.refusal : undefined;
90
+ if (!refusal)
91
+ return null;
92
+ return `⚠️ ${refusal}\n\nUntil then the header shows no branch, and everything Codeep does with git here — the status line, \`@git\`, \`/commit\`, the review hook — is off.`;
93
+ }
94
+ /**
95
+ * Put that message in the transcript once per project.
96
+ *
97
+ * A transcript message and not notify(): a toast is gone in three seconds,
98
+ * and this is a thing to act on, not a thing to glance at. Once per project
99
+ * because the cache it rides on is dropped after every agent run, and a
100
+ * warning repeated after each run is one the user learns to scroll past.
101
+ *
102
+ * Queued rather than added inline because the only caller runs inside
103
+ * getStatus(), which the render loop calls — pushing a message onto the list
104
+ * the frame is reading would tear that frame. The microtask lands before the
105
+ * next one.
106
+ */
107
+ function reportGitRefusal(path, notice) {
108
+ if (!notice || gitRefusalReported.has(path))
109
+ return;
110
+ gitRefusalReported.add(path);
111
+ queueMicrotask(() => { app?.addMessage({ role: 'system', content: notice }); });
112
+ }
50
113
  /** Derive a short display name from a user message (first ~5 words, max 48 chars). */
51
114
  export function deriveSessionName(message) {
52
115
  const clean = message.replace(/\s+/g, ' ').trim();
53
116
  const words = clean.split(' ').slice(0, 5).join(' ');
54
117
  return words.length > 48 ? words.slice(0, 45) + '…' : words;
55
118
  }
119
+ /**
120
+ * Start the MCP servers this terminal may run, under the fixed session id
121
+ * `codeep-tui`: the global servers (~/.codeep, the user's own) plus the
122
+ * workspace ones once the workspace is trusted. Registering replaces the
123
+ * session's whole set, so they go in one call; starting the global list and
124
+ * then the workspace list stopped the global servers. Returns the workspace
125
+ * servers left out because the workspace is not trusted yet.
126
+ */
127
+ export async function startTuiMcpServers(root, ui) {
128
+ const { selectSessionMcpServers } = await import('../utils/mcpConfig.js');
129
+ const { registerSessionServers } = await import('../utils/mcpRegistry.js');
130
+ const { servers, skipped } = selectSessionMcpServers(root);
131
+ if (servers.length > 0) {
132
+ const { registered, errors } = await registerSessionServers('codeep-tui', servers, { workspaceRoot: root });
133
+ if (registered.length > 0) {
134
+ ui.notify(`MCP: ${registered.length} tool(s) from ${servers.length} server(s) ready. Type /mcp.`);
135
+ }
136
+ for (const e of errors) {
137
+ ui.notifyWarn(`MCP server "${e.server}" failed: ${e.error}`);
138
+ }
139
+ }
140
+ return skipped;
141
+ }
142
+ /** Start the servers of a workspace the user has just trusted. A failure is
143
+ * shown, not swallowed: the user asked for these servers. */
144
+ export function startTrustedWorkspaceMcp(root, ui) {
145
+ return startTuiMcpServers(root, ui).then(() => { }, (err) => ui.notifyWarn(`MCP: could not start workspace servers: ${err.message}`));
146
+ }
56
147
  let isAgentRunningFlag = false;
57
148
  /** Set once the phone is allowed to send instructions; null when it is not. */
58
149
  let telegramInbox = null;
@@ -83,7 +174,8 @@ function makeCtx() {
83
174
  setAbortController: (ctrl) => { agentAbortController = ctrl; },
84
175
  formatAddedFilesContext,
85
176
  handleCommand: (cmd, args) => dispatchCommand(cmd, args, makeCtx()),
86
- setSessionId: (id) => { sessionId = id; },
177
+ setSessionId,
178
+ getSessionId: () => sessionId,
87
179
  setSessionDisplayName: (name) => { sessionDisplayName = name; },
88
180
  setProjectContext: (ctx) => {
89
181
  projectContext = ctx;
@@ -270,7 +362,7 @@ async function handleSubmit(message) {
270
362
  const enrichedMessage = fileContext ? fileContext + webExpanded : webExpanded;
271
363
  await chat(enrichedMessage, history, (chunk) => app.addStreamChunk(chunk), undefined, projectContext, undefined);
272
364
  app.endStreaming();
273
- autoSaveSession(app.getMessages(), projectPath);
365
+ autoSaveSession(app.getMessages(), projectPath, sessionId);
274
366
  // Sync to dashboard after every successful manual chat (not just on shutdown).
275
367
  // Mirrors the agent-mode behaviour in agentExecution.ts so dashboard stays
276
368
  // in sync regardless of Agent Mode setting.
@@ -410,7 +502,7 @@ function showSessionPickerInline() {
410
502
  else {
411
503
  const messages = loadSession(selectedName, projectPath);
412
504
  if (messages) {
413
- sessionId = selectedName;
505
+ setSessionId(selectedName);
414
506
  app.setMessages(messages);
415
507
  app.notify(`Loaded: ${selectedName}`);
416
508
  }
@@ -422,7 +514,9 @@ function showSessionPickerInline() {
422
514
  }, (sessionName) => { deleteSession(sessionName, projectPath); });
423
515
  }
424
516
  // ─── Main ─────────────────────────────────────────────────────────────────────
425
- async function main() {
517
+ /** The CLI entry point. Exported so tests can drive a command without the
518
+ * module starting the app on import. */
519
+ export async function main() {
426
520
  const args = process.argv.slice(2);
427
521
  // Headless, deterministic code review for CI (no API key, no TUI). Handled
428
522
  // before the global --help/--version checks so `codeep review --help` shows
@@ -511,7 +605,7 @@ Commands (in chat):
511
605
  // Also pull portable personal config — personalities + custom commands +
512
606
  // the user profile. Web-edited personalities replace their local copy
513
607
  // after a safety backup; commands/profile retain additive merge rules.
514
- const { pullPersonalities, pullCommands, pullUserProfile, getLastPersonalityPullBackupCount } = await import('../utils/codeepCloud.js');
608
+ const { pullPersonalities, pullCommands, pullUserProfileResult, getLastPersonalityPullBackupCount } = await import('../utils/codeepCloud.js');
515
609
  // Report all three outcomes, not just the interesting one. Printing only
516
610
  // on count > 0 made a failed sync look identical to a sync with nothing
517
611
  // new — silence meant either, and the user could not tell which.
@@ -539,8 +633,11 @@ Commands (in chat):
539
633
  else if (commands.count > 0) {
540
634
  console.log(` Pulled ${commands.count} custom command${commands.count === 1 ? '' : 's'}.`);
541
635
  }
542
- const profPulled = await pullUserProfile();
543
- if (profPulled === 1) {
636
+ const profile = await pullUserProfileResult();
637
+ if (!profile.ok) {
638
+ console.log(` Could not pull your profile (about you) — ${describeSyncFailure(profile.reason)}.`);
639
+ }
640
+ else if (profile.count > 0) {
544
641
  console.log(' Pulled your profile (about you).');
545
642
  }
546
643
  console.log('');
@@ -580,7 +677,7 @@ Commands (in chat):
580
677
  console.log(' Cloud key sync is off — skipping API keys. Enable with: /keysync on');
581
678
  }
582
679
  // Also push portable personal config — personalities + commands + profile.
583
- const { pushPersonalities, pushCommands, pushUserProfile } = await import('../utils/codeepCloud.js');
680
+ const { pushPersonalities, pushCommands, pushUserProfileResult } = await import('../utils/codeepCloud.js');
584
681
  const { describeSyncFailure } = await import('../utils/codeepCloud.js');
585
682
  const personalities = await pushPersonalities();
586
683
  if (!personalities.ok) {
@@ -596,11 +693,20 @@ Commands (in chat):
596
693
  else if (commands.count > 0) {
597
694
  console.log(` Pushed ${commands.count} custom command${commands.count === 1 ? '' : 's'}.`);
598
695
  }
599
- if (await pushUserProfile()) {
696
+ // No local profile is nothing to push, not a failure.
697
+ const profile = await pushUserProfileResult();
698
+ const profilePushFailed = !profile.ok;
699
+ if (!profile.ok) {
700
+ console.log(` Could not push your profile (about you) — ${describeSyncFailure(profile.reason)}.`);
701
+ }
702
+ else if (profile.count > 0) {
600
703
  console.log(' Pushed your profile (about you).');
601
704
  }
602
705
  console.log('');
603
- process.exit(keyPushFailed ? 1 : 0);
706
+ // Any push that was reported as failed fails the command, so a script
707
+ // running it can tell.
708
+ const anyFailed = keyPushFailed || !personalities.ok || !commands.ok || profilePushFailed;
709
+ process.exit(anyFailed ? 1 : 0);
604
710
  }
605
711
  if (sub === 'purge-keys') {
606
712
  const { getSyncToken } = await import('../config/index.js');
@@ -765,6 +871,9 @@ Commands (in chat):
765
871
  welcomeLines.push(' /help · Ctrl+L clear · Esc cancel');
766
872
  app.addMessage({ role: 'welcome', content: welcomeLines.join('\n') });
767
873
  app.start();
874
+ const codeepNotice = symlinkedCodeepNotice(projectPath);
875
+ if (codeepNotice)
876
+ app.notifyWarn(codeepNotice);
768
877
  // Let the phone send instructions, if it has been switched on. Started after
769
878
  // app.start() so a prompt that arrives immediately has somewhere to land.
770
879
  void (async () => {
@@ -804,32 +913,14 @@ Commands (in chat):
804
913
  if (projectPath) {
805
914
  (async () => {
806
915
  try {
807
- const { loadMcpServerConfigSplit, isWorkspaceMcpTrusted, trustWorkspaceMcp } = await import('../utils/mcpConfig.js');
808
- const { registerSessionServers } = await import('../utils/mcpRegistry.js');
809
- const { global: globalServers, workspace: workspaceServers } = loadMcpServerConfigSplit(projectPath);
810
- const spawnServers = async (servers) => {
811
- if (servers.length === 0)
812
- return;
813
- const { registered, errors } = await registerSessionServers('codeep-tui', servers, { workspaceRoot: projectPath });
814
- if (registered.length > 0) {
815
- app.notify(`MCP: ${registered.length} tool(s) from ${servers.length} server(s) ready. Type /mcp.`);
816
- }
817
- for (const e of errors) {
818
- app.notifyWarn(`MCP server "${e.server}" failed: ${e.error}`);
819
- }
820
- };
821
- // ~/.codeep servers are the user's own machine-wide config — spawn.
822
- await spawnServers(globalServers);
916
+ const { trustWorkspaceMcp } = await import('../utils/mcpConfig.js');
917
+ const workspaceServers = await startTuiMcpServers(projectPath, app);
823
918
  // Workspace files (.codeep/mcp_servers.json, .mcp.json) travel WITH
824
919
  // the repo — a cloned project could otherwise execute arbitrary
825
920
  // commands at startup. One-time per-workspace approval, mirroring
826
921
  // the trustedHookProjects gate for hooks.
827
922
  if (workspaceServers.length === 0)
828
923
  return;
829
- if (isWorkspaceMcpTrusted(projectPath)) {
830
- await spawnServers(workspaceServers);
831
- return;
832
- }
833
924
  const preview = workspaceServers.slice(0, 5).map(s => ` ${s.name}: ${s.command ? [s.command, ...(s.args ?? [])].join(' ') : s.url ?? ''}`);
834
925
  if (workspaceServers.length > 5)
835
926
  preview.push(` …and ${workspaceServers.length - 5} more`);
@@ -845,7 +936,7 @@ Commands (in chat):
845
936
  cancelLabel: 'Not now',
846
937
  onConfirm: () => {
847
938
  trustWorkspaceMcp(projectPath);
848
- void spawnServers(workspaceServers);
939
+ void startTrustedWorkspaceMcp(projectPath, app);
849
940
  },
850
941
  onCancel: () => {
851
942
  app.notify('Workspace MCP servers skipped. Run /mcp trust to enable them.');
@@ -1001,9 +1092,12 @@ async function gracefulShutdown() {
1001
1092
  console.log('Goodbye!');
1002
1093
  if (!app)
1003
1094
  return;
1095
+ // Write the conversation before anything that can fail or take time: the
1096
+ // process exits as soon as this returns or throws, and a debounced save
1097
+ // left on its timer never happens.
1004
1098
  const messages = app.getMessages();
1005
- autoSaveSession(messages, projectPath);
1006
- const { syncSessionAsync, generateProjectId } = require('../utils/codeepCloud.js');
1099
+ autoSaveSession(messages, projectPath, sessionId);
1100
+ flushAutoSave();
1007
1101
  const projectId = projectPath ? generateProjectId(projectPath) : undefined;
1008
1102
  // Successful manual and agent turns report their token deltas immediately.
1009
1103
  // Re-sending the cumulative session total here would count every token a
@@ -1017,47 +1111,54 @@ async function gracefulShutdown() {
1017
1111
  });
1018
1112
  }
1019
1113
  // ─── Last-resort crash handlers ───────────────────────────────────────────────
1020
- // Without these, a stray throw or rejected promise (deep in the agent loop or a
1021
- // background cloud sync) crashes Node with the terminal still in raw mode +
1022
- // alternate screen — leaving the user's shell garbled — or vanishes silently.
1023
- process.on('uncaughtException', (error) => {
1024
- logAppError(error instanceof Error ? error : new Error(String(error)), 'uncaughtException');
1025
- // After an uncaught exception the process state is undefined; Node's guidance
1026
- // is to clean up synchronously and exit rather than limp on. Restore the
1027
- // terminal and best-effort save the conversation so the crash doesn't lose it.
1028
- try {
1029
- if (app)
1030
- app.stop();
1031
- }
1032
- catch { /* ignore */ }
1033
- try {
1034
- process.stdout.write('\x1b[2J\x1b[3J\x1b[H');
1035
- }
1036
- catch { /* ignore */ }
1037
- console.error('Fatal error:', error);
1038
- try {
1114
+ // A test that imports this module for its helpers must not start the app:
1115
+ // main() reads the OS keychain, draws the TUI and goes to the network, and
1116
+ // these handlers would exit the test worker. Every real launch — node, the
1117
+ // npm bin, the bun-compiled binaries — runs this module as the program.
1118
+ if (!process.env.VITEST) {
1119
+ // Without these, a stray throw or rejected promise (deep in the agent loop or a
1120
+ // background cloud sync) crashes Node with the terminal still in raw mode +
1121
+ // alternate screen — leaving the user's shell garbled — or vanishes silently.
1122
+ process.on('uncaughtException', (error) => {
1123
+ logAppError(error instanceof Error ? error : new Error(String(error)), 'uncaughtException');
1124
+ // After an uncaught exception the process state is undefined; Node's guidance
1125
+ // is to clean up synchronously and exit rather than limp on. Restore the
1126
+ // terminal and best-effort save the conversation so the crash doesn't lose it.
1127
+ try {
1128
+ if (app)
1129
+ app.stop();
1130
+ }
1131
+ catch { /* ignore */ }
1132
+ try {
1133
+ process.stdout.write('\x1b[2J\x1b[3J\x1b[H');
1134
+ }
1135
+ catch { /* ignore */ }
1136
+ console.error('Fatal error:', error);
1137
+ try {
1138
+ if (app)
1139
+ autoSaveSession(app.getMessages(), projectPath, sessionId);
1140
+ flushAutoSave();
1141
+ }
1142
+ catch { /* ignore */ }
1143
+ process.exit(1);
1144
+ });
1145
+ process.on('unhandledRejection', (reason) => {
1146
+ logAppError(reason instanceof Error ? reason : new Error(String(reason)), 'unhandledRejection');
1147
+ // A rejected promise is usually recoverable (failed background sync, network
1148
+ // blip), so surface it and keep the TUI alive instead of tearing it down.
1149
+ // Fall back to stderr if the app isn't up yet.
1150
+ const message = reason instanceof Error ? reason.message : String(reason);
1039
1151
  if (app)
1040
- autoSaveSession(app.getMessages(), projectPath);
1041
- }
1042
- catch { /* ignore */ }
1043
- process.exit(1);
1044
- });
1045
- process.on('unhandledRejection', (reason) => {
1046
- logAppError(reason instanceof Error ? reason : new Error(String(reason)), 'unhandledRejection');
1047
- // A rejected promise is usually recoverable (failed background sync, network
1048
- // blip), so surface it and keep the TUI alive instead of tearing it down.
1049
- // Fall back to stderr if the app isn't up yet.
1050
- const message = reason instanceof Error ? reason.message : String(reason);
1051
- if (app)
1052
- app.notifyWarn(`Background error: ${message}`);
1053
- else
1054
- console.error('Unhandled rejection:', reason);
1055
- });
1056
- process.on('SIGINT', () => {
1057
- gracefulShutdown().finally(() => process.exit(0));
1058
- });
1059
- main().catch((error) => {
1060
- logAppError(error instanceof Error ? error : new Error(String(error)), 'main');
1061
- console.error('Fatal error:', error);
1062
- process.exit(1);
1063
- });
1152
+ app.notifyWarn(`Background error: ${message}`);
1153
+ else
1154
+ console.error('Unhandled rejection:', reason);
1155
+ });
1156
+ process.on('SIGINT', () => {
1157
+ gracefulShutdown().finally(() => process.exit(0));
1158
+ });
1159
+ main().catch((error) => {
1160
+ logAppError(error instanceof Error ? error : new Error(String(error)), 'main');
1161
+ console.error('Fatal error:', error);
1162
+ process.exit(1);
1163
+ });
1164
+ }
@@ -10,6 +10,7 @@ import { type Personality } from './personalities';
10
10
  export { loadProjectRules, loadProgressLog, writeProgressLog, formatChatHistoryForAgent };
11
11
  export type { AgentChatResponse };
12
12
  import { ToolCall, ToolResult, ActionLog } from './tools';
13
+ import { type TrustBearingWrite } from './toolExecution';
13
14
  import { undoLastAction, undoAllActions, getCurrentSession, getRecentSessions, formatSession, ActionSession } from './history';
14
15
  import { VerifyResult } from './verify';
15
16
  import { TaskPlan, SubTask } from './taskPlanner';
@@ -31,6 +32,29 @@ export declare function classifyPermissionOutcome(outcome: string | undefined |
31
32
  * session's mode into the TUI and race on restore). Exported for unit testing.
32
33
  */
33
34
  export declare function buildDangerousTools(extra?: string[]): Set<string>;
35
+ /**
36
+ * Whether a tool call must go through the permission prompt this run.
37
+ *
38
+ * MCP tools (`<server>__<tool>`) can do anything their server can — write
39
+ * files, run SQL, drive a browser — and their names never appear in the
40
+ * built-in set, so a mode that confirms dangerous operations confirms these
41
+ * too. The resource/prompt wrappers only read, and stay unprompted.
42
+ */
43
+ export declare function requiresPermission(tool: string, dangerousTools: ReadonlySet<string>): boolean;
44
+ /** Provider, model and protocol a run talks to. */
45
+ export interface AgentModelRuntime {
46
+ providerId: string;
47
+ model: string;
48
+ protocol: 'openai' | 'anthropic';
49
+ }
50
+ /**
51
+ * Read a sub-agent's `model:` setting ("provider/model" or a bare model) as
52
+ * the runtime for its nested run. A known provider prefix switches provider;
53
+ * anything else is a model on the current provider. The protocol is kept when
54
+ * the provider stays the same and supports it, and is otherwise that
55
+ * provider's default.
56
+ */
57
+ export declare function resolveDelegateModel(spec: string, current: AgentModelRuntime): Promise<AgentModelRuntime>;
34
58
  export interface AgentOptions {
35
59
  maxIterations: number;
36
60
  maxDuration: number;
@@ -42,7 +66,18 @@ export interface AgentOptions {
42
66
  onVerification?: (results: VerifyResult[]) => void;
43
67
  onTaskPlan?: (plan: TaskPlan) => void;
44
68
  onTaskUpdate?: (task: SubTask) => void;
45
- onRequestPermission?: (toolCall: ToolCall) => Promise<PermissionOutcome>;
69
+ /**
70
+ * Ask the user about one tool call.
71
+ *
72
+ * `trustBearing` is what this run already worked out about the call — the
73
+ * file it would write that decides what runs later, or null when it writes
74
+ * no such file. It is passed so the side putting up the dialog can word it
75
+ * without calling trustBearingWrite() a second time: that call stats the
76
+ * path, follows a symlink and may ask git where the repo's hooks live.
77
+ * Optional, so a caller that would rather work it out itself (or was not
78
+ * called from the gate below) still type-checks.
79
+ */
80
+ onRequestPermission?: (toolCall: ToolCall, trustBearing?: TrustBearingWrite | null) => Promise<PermissionOutcome>;
46
81
  /** Tool names to force into the per-run dangerous set, on top of the global
47
82
  * agentConfirm* settings. ACP manual mode passes ['write_file','edit_file']
48
83
  * here to gate them for THIS run only, instead of mutating global config. */
@@ -98,6 +133,21 @@ export interface AgentOptions {
98
133
  allowedTools?: string[];
99
134
  /** Role system-prompt addendum injected for a delegated sub-agent. */
100
135
  roleAddendum?: string;
136
+ /** "Always allow" / "always deny" answers shared with a delegating parent,
137
+ * so a sub-agent neither asks again about a tool the user already decided
138
+ * on nor runs one the user refused. `alwaysRejectedPaths` holds the same
139
+ * for a single file that decides what runs later, which is refused by name
140
+ * rather than by tool. */
141
+ permissionMemory?: {
142
+ alwaysAllowed: Set<string>;
143
+ alwaysRejected: Set<string>;
144
+ alwaysRejectedPaths?: Set<string>;
145
+ };
146
+ /** Provider/model for this run only, used in place of the global selection.
147
+ * A sub-agent with its own `model:` runs on it this way; the global config
148
+ * is saved to disk and read by every session in the process, so it is
149
+ * never changed for a single run. */
150
+ modelOverride?: AgentModelRuntime;
101
151
  }
102
152
  /** Why a run stopped early at a safety limit — both are resumable, not errors. */
103
153
  export type InterruptKind = 'iteration_limit' | 'time_limit';
@@ -111,6 +161,19 @@ export interface AgentResult {
111
161
  /** Set when the run paused at a step/time safety limit. The caller can offer
112
162
  * a "continue" affordance instead of treating it as a failure. */
113
163
  interrupted?: InterruptKind;
164
+ /** Commands of the verification checks that still failed when the run
165
+ * ended. Set only when that is why the run did not succeed. */
166
+ failedChecks?: string[];
167
+ /**
168
+ * The end of `finalResponse` that runAgent wrote itself instead of taking
169
+ * it from the model's last reply: the verification passed / failed / could
170
+ * not run blocks and the auto-review section, or the whole notice when one
171
+ * replaced the reply (stopped, paused, API errors). None of it went through
172
+ * `onChunk`, so a client that streamed the reply sends exactly this after
173
+ * it, separated by a blank line. Trimmed; '' when `finalResponse` is only
174
+ * the model's reply. Set on every result runAgent returns.
175
+ */
176
+ unstreamedText?: string;
114
177
  }
115
178
  /**
116
179
  * Build the result for a run that paused at a safety limit. Pausing is a normal,
@@ -147,10 +210,12 @@ export declare function getAgentHistory(): Array<{
147
210
  success: boolean;
148
211
  }>;
149
212
  /**
150
- * Get current session actions
213
+ * Actions of the run undo acts on (see getCurrentSession). Pass the workspace
214
+ * to leave out a run in another one. `result` is 'undone' for an action that
215
+ * has since been undone, 'success' otherwise.
151
216
  */
152
- export declare function getCurrentSessionActions(): Array<{
217
+ export declare function getCurrentSessionActions(projectRoot?: string): Array<{
153
218
  type: string;
154
219
  target: string;
155
- result: string;
220
+ result: 'success' | 'undone';
156
221
  }>;