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.
- package/dist/acp/commands.d.ts +50 -1
- package/dist/acp/commands.js +545 -109
- package/dist/acp/protocol.d.ts +14 -5
- package/dist/acp/server.d.ts +36 -1
- package/dist/acp/server.js +581 -155
- package/dist/acp/serverHandlers.d.ts +2 -1
- package/dist/acp/serverHandlers.js +3 -0
- package/dist/acp/session.d.ts +28 -2
- package/dist/acp/session.js +25 -6
- package/dist/acp/transport.d.ts +40 -4
- package/dist/acp/transport.js +218 -25
- package/dist/acp/turns.d.ts +20 -0
- package/dist/acp/turns.js +30 -0
- package/dist/api/index.js +2 -0
- package/dist/api/ollamaNative.d.ts +3 -0
- package/dist/api/ollamaNative.js +35 -3
- package/dist/config/index.d.ts +21 -4
- package/dist/config/index.js +178 -123
- package/dist/renderer/agentExecution.d.ts +30 -2
- package/dist/renderer/agentExecution.js +248 -92
- package/dist/renderer/commands/helpers.d.ts +18 -2
- package/dist/renderer/commands/helpers.js +28 -5
- package/dist/renderer/commands.d.ts +2 -0
- package/dist/renderer/commands.js +180 -64
- package/dist/renderer/main.d.ts +41 -0
- package/dist/renderer/main.js +181 -80
- package/dist/utils/agent.d.ts +69 -4
- package/dist/utils/agent.js +416 -248
- package/dist/utils/agentChat.js +82 -10
- package/dist/utils/agents.d.ts +2 -1
- package/dist/utils/agents.js +100 -29
- package/dist/utils/auditLog.d.ts +4 -3
- package/dist/utils/auditLog.js +92 -9
- package/dist/utils/checkpoints.js +11 -6
- package/dist/utils/codeReview.js +28 -23
- package/dist/utils/codeepCloud.d.ts +14 -2
- package/dist/utils/codeepCloud.js +56 -20
- package/dist/utils/customCommands.js +7 -2
- package/dist/utils/git.d.ts +262 -4
- package/dist/utils/git.js +1928 -61
- package/dist/utils/gitHookInstaller.d.ts +32 -1
- package/dist/utils/gitHookInstaller.js +76 -8
- package/dist/utils/gitignore.d.ts +8 -0
- package/dist/utils/gitignore.js +41 -10
- package/dist/utils/headlessReview.d.ts +11 -0
- package/dist/utils/headlessReview.js +33 -5
- package/dist/utils/history.d.ts +22 -6
- package/dist/utils/history.js +140 -26
- package/dist/utils/logger.js +6 -7
- package/dist/utils/mcpConfig.d.ts +24 -0
- package/dist/utils/mcpConfig.js +36 -5
- package/dist/utils/mentions.d.ts +28 -5
- package/dist/utils/mentions.js +253 -45
- package/dist/utils/personalities.js +16 -6
- package/dist/utils/planMode.d.ts +13 -7
- package/dist/utils/planMode.js +32 -12
- package/dist/utils/projectIntelligence.d.ts +2 -0
- package/dist/utils/projectIntelligence.js +27 -8
- package/dist/utils/projectPaths.d.ts +53 -0
- package/dist/utils/projectPaths.js +146 -0
- package/dist/utils/shell.d.ts +119 -0
- package/dist/utils/shell.js +417 -45
- package/dist/utils/skillBundles.js +17 -7
- package/dist/utils/skillBundlesCloud.js +20 -3
- package/dist/utils/skills.d.ts +24 -2
- package/dist/utils/skills.js +235 -43
- package/dist/utils/smartContext.js +97 -23
- package/dist/utils/telegramApproval.d.ts +10 -2
- package/dist/utils/telegramApproval.js +22 -4
- package/dist/utils/toolExecution.d.ts +50 -2
- package/dist/utils/toolExecution.js +418 -16
- package/dist/utils/toolParsing.d.ts +7 -1
- package/dist/utils/toolParsing.js +12 -3
- package/dist/utils/userProfile.js +58 -16
- package/dist/utils/verify.d.ts +25 -4
- package/dist/utils/verify.js +259 -74
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/renderer/main.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
543
|
-
if (
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
808
|
-
const
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
1021
|
-
//
|
|
1022
|
-
//
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
//
|
|
1026
|
-
//
|
|
1027
|
-
//
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
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
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
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
|
+
}
|
package/dist/utils/agent.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
*
|
|
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:
|
|
220
|
+
result: 'success' | 'undone';
|
|
156
221
|
}>;
|