codeep 2.13.2 → 2.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -24
- package/dist/acp/commands.js +22 -1
- package/dist/acp/server.js +278 -251
- package/dist/config/index.d.ts +10 -0
- package/dist/config/index.js +2 -2
- package/dist/config/providers.js +35 -22
- package/dist/renderer/App.d.ts +0 -30
- package/dist/renderer/App.js +149 -659
- package/dist/renderer/agentExecution.d.ts +2 -1
- package/dist/renderer/agentExecution.js +10 -6
- package/dist/renderer/commands/helpers.d.ts +63 -0
- package/dist/renderer/commands/helpers.js +108 -0
- package/dist/renderer/commands/registry.js +5 -0
- package/dist/renderer/commands.d.ts +4 -0
- package/dist/renderer/commands.js +183 -64
- package/dist/renderer/components/ActionFormatting.d.ts +17 -0
- package/dist/renderer/components/ActionFormatting.js +67 -0
- package/dist/renderer/components/Autocomplete.d.ts +33 -0
- package/dist/renderer/components/Autocomplete.js +40 -0
- package/dist/renderer/components/Intro.d.ts +9 -0
- package/dist/renderer/components/Intro.js +5 -15
- package/dist/renderer/components/MessageFormatter.d.ts +96 -0
- package/dist/renderer/components/MessageFormatter.js +375 -0
- package/dist/renderer/components/Permission.d.ts +4 -0
- package/dist/renderer/components/Permission.js +1 -1
- package/dist/renderer/components/Status.d.ts +4 -0
- package/dist/renderer/components/Status.js +2 -3
- package/dist/renderer/components/WelcomeFormatter.d.ts +19 -0
- package/dist/renderer/components/WelcomeFormatter.js +79 -0
- package/dist/renderer/components/uiConstants.d.ts +8 -0
- package/dist/renderer/components/uiConstants.js +24 -0
- package/dist/renderer/inputParsing.d.ts +22 -0
- package/dist/renderer/inputParsing.js +28 -0
- package/dist/renderer/layout.d.ts +215 -0
- package/dist/renderer/layout.js +326 -0
- package/dist/renderer/main.d.ts +2 -1
- package/dist/renderer/main.js +45 -10
- package/dist/renderer/ollamaHint.d.ts +12 -0
- package/dist/renderer/ollamaHint.js +29 -0
- package/dist/utils/agentChat.js +28 -2
- package/dist/utils/codeepCloud.d.ts +54 -0
- package/dist/utils/codeepCloud.js +95 -0
- package/dist/utils/export.d.ts +12 -0
- package/dist/utils/export.js +3 -3
- package/dist/utils/hooks.d.ts +26 -0
- package/dist/utils/hooks.js +69 -1
- package/dist/utils/keychain.js +45 -29
- package/dist/utils/logger.d.ts +12 -0
- package/dist/utils/logger.js +1 -1
- package/dist/utils/mcpConfig.d.ts +26 -0
- package/dist/utils/mcpConfig.js +109 -4
- package/dist/utils/skillBundles.d.ts +14 -0
- package/dist/utils/skillBundles.js +3 -3
- package/dist/utils/skillBundlesCloud.d.ts +7 -0
- package/dist/utils/skillBundlesCloud.js +1 -1
- package/dist/utils/tokenTracker.d.ts +30 -3
- package/dist/utils/tokenTracker.js +71 -13
- package/dist/utils/toolParsing.d.ts +11 -0
- package/dist/utils/toolParsing.js +6 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
package/dist/acp/server.js
CHANGED
|
@@ -8,7 +8,7 @@ import { StdioTransport } from './transport.js';
|
|
|
8
8
|
import { runAgentSession } from './session.js';
|
|
9
9
|
import { loadCustomCommands } from '../utils/customCommands.js';
|
|
10
10
|
import { registerSessionServers, disposeAllSessions as disposeAllMcpSessions } from '../utils/mcpRegistry.js';
|
|
11
|
-
import {
|
|
11
|
+
import { loadMcpServerConfigSplit, isWorkspaceMcpTrusted, mergeMcpServers } from '../utils/mcpConfig.js';
|
|
12
12
|
import { handleMcpSamplingRequest } from '../utils/mcpSamplingBridge.js';
|
|
13
13
|
import { executeCommandAsync } from '../utils/shell.js';
|
|
14
14
|
import { initWorkspace, loadWorkspace, handleCommand } from './commands.js';
|
|
@@ -18,7 +18,7 @@ import { ApiError } from '../api/index.js';
|
|
|
18
18
|
import { PROVIDERS } from '../config/providers.js';
|
|
19
19
|
import { getCurrentVersion } from '../utils/update.js';
|
|
20
20
|
import { reportStats, syncSession, generateProjectId } from '../utils/codeepCloud.js';
|
|
21
|
-
import { getCostBreakdown,
|
|
21
|
+
import { getCostBreakdown, getRecordCount, createTokenScope, runWithTokenScope } from '../utils/tokenTracker.js';
|
|
22
22
|
import { isGitRepository } from '../utils/git.js';
|
|
23
23
|
import { getProjectContext } from '../utils/project.js';
|
|
24
24
|
// ─── Slash commands advertised to Zed ────────────────────────────────────────
|
|
@@ -141,10 +141,18 @@ export function formatToolInputForPermission(tool, params) {
|
|
|
141
141
|
? lines.slice(0, MAX_CONTENT_LINES).join('\n') + `\n… (${lines.length - MAX_CONTENT_LINES} more lines)`
|
|
142
142
|
: params.content;
|
|
143
143
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
144
|
+
// The built-in edit_file tool schema emits old_text / new_text (see
|
|
145
|
+
// utils/tools.ts); accept the old_string/new_string spelling too for
|
|
146
|
+
// robustness. Without this the permission dialog dropped the diff and
|
|
147
|
+
// showed only { file, path } — the user approved edits blind.
|
|
148
|
+
const oldText = typeof params.old_text === 'string' ? params.old_text
|
|
149
|
+
: (typeof params.old_string === 'string' ? params.old_string : undefined);
|
|
150
|
+
const newText = typeof params.new_text === 'string' ? params.new_text
|
|
151
|
+
: (typeof params.new_string === 'string' ? params.new_string : undefined);
|
|
152
|
+
if (oldText !== undefined && newText !== undefined) {
|
|
153
|
+
out.changes = `replace ${oldText.split('\n').length} line(s)`;
|
|
154
|
+
out.old_string = truncateDiff(oldText);
|
|
155
|
+
out.new_string = truncateDiff(newText);
|
|
148
156
|
}
|
|
149
157
|
return out;
|
|
150
158
|
}
|
|
@@ -531,7 +539,18 @@ export function startAcpServer() {
|
|
|
531
539
|
* in-flight registration so tool calls don't race the startup.
|
|
532
540
|
*/
|
|
533
541
|
function spawnMcpServersForSession(acpSessionId, cwd, acpServers, label) {
|
|
534
|
-
|
|
542
|
+
// Workspace-sourced servers (.codeep/mcp_servers.json, .mcp.json) travel
|
|
543
|
+
// with the repo, so they spawn only for workspaces the user has trusted
|
|
544
|
+
// (same gate the TUI prompts for at startup). ACP-provided servers are
|
|
545
|
+
// the editor's own config and global ~/.codeep entries are the user's —
|
|
546
|
+
// both spawn unconditionally.
|
|
547
|
+
const { global: globalServers, workspace: workspaceServers } = loadMcpServerConfigSplit(cwd);
|
|
548
|
+
const workspaceTrusted = isWorkspaceMcpTrusted(cwd);
|
|
549
|
+
if (workspaceServers.length > 0 && !workspaceTrusted) {
|
|
550
|
+
process.stderr.write(`[codeep-acp] MCP (${label}): skipped ${workspaceServers.length} workspace server(s) — untrusted workspace. ` +
|
|
551
|
+
`Run /mcp trust (or \`codeep\` in the repo once) to enable.\n`);
|
|
552
|
+
}
|
|
553
|
+
const fromConfig = workspaceTrusted ? [...globalServers, ...workspaceServers] : globalServers;
|
|
535
554
|
const merged = mergeMcpServers(fromConfig, acpServers);
|
|
536
555
|
if (merged.length === 0)
|
|
537
556
|
return;
|
|
@@ -860,7 +879,6 @@ export function startAcpServer() {
|
|
|
860
879
|
},
|
|
861
880
|
});
|
|
862
881
|
};
|
|
863
|
-
resetTokenTracking();
|
|
864
882
|
// Manual mode gates write/edit for THIS run via a per-call option passed to
|
|
865
883
|
// runAgentSession (extraDangerousTools, below) — NOT by mutating the global
|
|
866
884
|
// `agentConfirmWriteFile` config, which leaked the session's mode into the
|
|
@@ -876,257 +894,283 @@ export function startAcpServer() {
|
|
|
876
894
|
},
|
|
877
895
|
});
|
|
878
896
|
};
|
|
879
|
-
// Try slash commands first
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
// Update title with first real prompt if session had no history
|
|
896
|
-
if (!session.titleSent && !session.hadHistory) {
|
|
897
|
-
session.titleSent = true;
|
|
898
|
-
sendSessionTitle(params.sessionId, [{ role: 'user', content: prompt }]);
|
|
899
|
-
}
|
|
900
|
-
transport.respond(msg.id, { stopReason: 'end_turn' });
|
|
901
|
-
return;
|
|
902
|
-
}
|
|
903
|
-
// Not a command — run agent loop
|
|
904
|
-
let enrichedPrompt = prompt;
|
|
905
|
-
if (session.addedFiles.size > 0) {
|
|
906
|
-
const parts = ['[Attached files]'];
|
|
907
|
-
for (const [, f] of session.addedFiles) {
|
|
908
|
-
parts.push(`\nFile: ${f.relativePath}\n\`\`\`\n${f.content}\n\`\`\``);
|
|
909
|
-
}
|
|
910
|
-
enrichedPrompt = parts.join('\n') + '\n\n' + prompt;
|
|
911
|
-
}
|
|
912
|
-
runAgentSession({
|
|
913
|
-
prompt: enrichedPrompt,
|
|
914
|
-
workspaceRoot: session.workspaceRoot,
|
|
915
|
-
conversationId: params.sessionId,
|
|
916
|
-
abortSignal: abortController.signal,
|
|
917
|
-
onChunk: sendChunk,
|
|
918
|
-
onThought: (text) => {
|
|
919
|
-
transport.notify('session/update', {
|
|
920
|
-
sessionId: params.sessionId,
|
|
921
|
-
update: {
|
|
922
|
-
sessionUpdate: 'agent_thought_chunk',
|
|
923
|
-
content: { type: 'text', text },
|
|
924
|
-
},
|
|
925
|
-
});
|
|
926
|
-
},
|
|
927
|
-
onToolCall: (toolCallId, toolName, kind, title, status, locations, rawOutput) => {
|
|
928
|
-
if (status === 'running') {
|
|
929
|
-
// Initial tool_call notification: spec ToolCall shape
|
|
897
|
+
// Try slash commands first.
|
|
898
|
+
// Run the whole prompt lifecycle inside THIS ACP session's token scope so
|
|
899
|
+
// (a) concurrent sessions on one process can't mix usage totals, and
|
|
900
|
+
// (b) usage accumulates into the session's own buffer across prompts, so
|
|
901
|
+
// `/cost` stays session-cumulative. `tokenReportStart` marks the pre-prompt
|
|
902
|
+
// count so we report only this prompt's delta to cloud telemetry.
|
|
903
|
+
session.tokenRecords ??= createTokenScope();
|
|
904
|
+
runWithTokenScope(session.tokenRecords, () => {
|
|
905
|
+
const tokenReportStart = getRecordCount();
|
|
906
|
+
return handleCommand(prompt, session, sendChunk, abortController.signal)
|
|
907
|
+
.then((cmd) => {
|
|
908
|
+
if (cmd.handled) {
|
|
909
|
+
if (cmd.response)
|
|
910
|
+
sendChunk(cmd.response);
|
|
911
|
+
// If provider or model changed, push updated config options to Zed
|
|
912
|
+
if (cmd.configOptionsChanged) {
|
|
930
913
|
transport.notify('session/update', {
|
|
931
914
|
sessionId: params.sessionId,
|
|
932
915
|
update: {
|
|
933
|
-
sessionUpdate: '
|
|
934
|
-
|
|
935
|
-
title: title || toolName,
|
|
936
|
-
kind: kind || 'other',
|
|
937
|
-
status: 'in_progress',
|
|
938
|
-
...(locations && locations.length > 0
|
|
939
|
-
? { locations: locations.map(path => ({ path })) }
|
|
940
|
-
: {}),
|
|
916
|
+
sessionUpdate: 'config_option_update',
|
|
917
|
+
configOptions: buildConfigOptions(),
|
|
941
918
|
},
|
|
942
919
|
});
|
|
943
|
-
// Add to plan as in_progress — only meaningful actions (not reads)
|
|
944
|
-
if (kind === 'edit' || kind === 'execute' || kind === 'delete') {
|
|
945
|
-
planEntries.set(toolCallId, {
|
|
946
|
-
id: toolCallId,
|
|
947
|
-
content: title || toolName,
|
|
948
|
-
priority: kind === 'execute' ? 'high' : 'medium',
|
|
949
|
-
status: 'in_progress',
|
|
950
|
-
});
|
|
951
|
-
sendPlan();
|
|
952
|
-
}
|
|
953
920
|
}
|
|
954
|
-
|
|
955
|
-
|
|
921
|
+
// Update title with first real prompt if session had no history
|
|
922
|
+
if (!session.titleSent && !session.hadHistory) {
|
|
923
|
+
session.titleSent = true;
|
|
924
|
+
sendSessionTitle(params.sessionId, [{ role: 'user', content: prompt }]);
|
|
925
|
+
}
|
|
926
|
+
transport.respond(msg.id, { stopReason: 'end_turn' });
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
// Not a command — run agent loop
|
|
930
|
+
let enrichedPrompt = prompt;
|
|
931
|
+
if (session.addedFiles.size > 0) {
|
|
932
|
+
const parts = ['[Attached files]'];
|
|
933
|
+
for (const [, f] of session.addedFiles) {
|
|
934
|
+
parts.push(`\nFile: ${f.relativePath}\n\`\`\`\n${f.content}\n\`\`\``);
|
|
935
|
+
}
|
|
936
|
+
enrichedPrompt = parts.join('\n') + '\n\n' + prompt;
|
|
937
|
+
}
|
|
938
|
+
runAgentSession({
|
|
939
|
+
prompt: enrichedPrompt,
|
|
940
|
+
workspaceRoot: session.workspaceRoot,
|
|
941
|
+
conversationId: params.sessionId,
|
|
942
|
+
abortSignal: abortController.signal,
|
|
943
|
+
onChunk: sendChunk,
|
|
944
|
+
onThought: (text) => {
|
|
956
945
|
transport.notify('session/update', {
|
|
957
946
|
sessionId: params.sessionId,
|
|
958
947
|
update: {
|
|
959
|
-
sessionUpdate: '
|
|
960
|
-
|
|
961
|
-
status: status === 'finished' ? 'completed' : 'failed',
|
|
962
|
-
...(rawOutput !== undefined ? { rawOutput } : {}),
|
|
948
|
+
sessionUpdate: 'agent_thought_chunk',
|
|
949
|
+
content: { type: 'text', text },
|
|
963
950
|
},
|
|
964
951
|
});
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
if (
|
|
968
|
-
|
|
969
|
-
|
|
952
|
+
},
|
|
953
|
+
onToolCall: (toolCallId, toolName, kind, title, status, locations, rawOutput) => {
|
|
954
|
+
if (status === 'running') {
|
|
955
|
+
// Initial tool_call notification: spec ToolCall shape
|
|
956
|
+
transport.notify('session/update', {
|
|
957
|
+
sessionId: params.sessionId,
|
|
958
|
+
update: {
|
|
959
|
+
sessionUpdate: 'tool_call',
|
|
960
|
+
toolCallId,
|
|
961
|
+
title: title || toolName,
|
|
962
|
+
kind: kind || 'other',
|
|
963
|
+
status: 'in_progress',
|
|
964
|
+
...(locations && locations.length > 0
|
|
965
|
+
? { locations: locations.map(path => ({ path })) }
|
|
966
|
+
: {}),
|
|
967
|
+
},
|
|
968
|
+
});
|
|
969
|
+
// Add to plan as in_progress — only meaningful actions (not reads)
|
|
970
|
+
if (kind === 'edit' || kind === 'execute' || kind === 'delete') {
|
|
971
|
+
planEntries.set(toolCallId, {
|
|
972
|
+
id: toolCallId,
|
|
973
|
+
content: title || toolName,
|
|
974
|
+
priority: kind === 'execute' ? 'high' : 'medium',
|
|
975
|
+
status: 'in_progress',
|
|
976
|
+
});
|
|
977
|
+
sendPlan();
|
|
978
|
+
}
|
|
970
979
|
}
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
// no global config mutation).
|
|
975
|
-
extraDangerousTools: session.currentModeId === 'manual' ? ['write_file', 'edit_file'] : undefined,
|
|
976
|
-
// Only request permission in Manual mode
|
|
977
|
-
onRequestPermission: session.currentModeId === 'manual'
|
|
978
|
-
? async (toolCall) => {
|
|
979
|
-
const permToolCallId = `perm_${randomUUID()}`;
|
|
980
|
-
const result = await transport.request('session/request_permission', {
|
|
981
|
-
sessionId: params.sessionId,
|
|
982
|
-
toolCall: {
|
|
983
|
-
toolCallId: permToolCallId,
|
|
984
|
-
toolName: toolCall.tool,
|
|
985
|
-
toolInput: formatToolInputForPermission(toolCall.tool, toolCall.parameters),
|
|
986
|
-
status: 'pending',
|
|
987
|
-
content: [],
|
|
988
|
-
},
|
|
989
|
-
options: [
|
|
990
|
-
{ optionId: 'allow_once', name: 'Allow once', kind: 'allow_once' },
|
|
991
|
-
{ optionId: 'allow_always', name: 'Allow always', kind: 'allow_always' },
|
|
992
|
-
{ optionId: 'reject_once', name: 'Reject once', kind: 'reject_once' },
|
|
993
|
-
{ optionId: 'reject_always', name: 'Reject always', kind: 'reject_always' },
|
|
994
|
-
],
|
|
995
|
-
});
|
|
996
|
-
// Map ACP outcome back to PermissionOutcome
|
|
997
|
-
if (!result || result.outcome.type === 'cancelled')
|
|
998
|
-
return 'reject_once';
|
|
999
|
-
return result.outcome.optionId;
|
|
1000
|
-
}
|
|
1001
|
-
: undefined,
|
|
1002
|
-
// Per ACP spec, `fs/read_text_file` and `fs/write_text_file` are
|
|
1003
|
-
// CLIENT methods — only safe to call when the client advertised
|
|
1004
|
-
// the capability in `initialize`. Routing through the client
|
|
1005
|
-
// means the editor's dirty buffers + undo history stay correct
|
|
1006
|
-
// (otherwise an in-editor unsaved change would be invisible to
|
|
1007
|
-
// the agent, or worse, silently overwritten).
|
|
1008
|
-
fs: {
|
|
1009
|
-
readTextFile: clientSupportsFsRead
|
|
1010
|
-
? async (absolutePath) => {
|
|
1011
|
-
const result = await transport.request('fs/read_text_file', {
|
|
980
|
+
else {
|
|
981
|
+
// tool_call_update: update status to completed/failed, with optional content
|
|
982
|
+
transport.notify('session/update', {
|
|
1012
983
|
sessionId: params.sessionId,
|
|
1013
|
-
|
|
984
|
+
update: {
|
|
985
|
+
sessionUpdate: 'tool_call_update',
|
|
986
|
+
toolCallId,
|
|
987
|
+
status: status === 'finished' ? 'completed' : 'failed',
|
|
988
|
+
...(rawOutput !== undefined ? { rawOutput } : {}),
|
|
989
|
+
},
|
|
1014
990
|
});
|
|
1015
|
-
|
|
1016
|
-
|
|
991
|
+
// Mark plan entry as completed
|
|
992
|
+
const entry = planEntries.get(toolCallId);
|
|
993
|
+
if (entry) {
|
|
994
|
+
entry.status = 'completed';
|
|
995
|
+
sendPlan();
|
|
1017
996
|
}
|
|
1018
|
-
return result.content;
|
|
1019
997
|
}
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
998
|
+
},
|
|
999
|
+
// Manual mode gates write_file/edit_file for this run only (per-call,
|
|
1000
|
+
// no global config mutation).
|
|
1001
|
+
extraDangerousTools: session.currentModeId === 'manual' ? ['write_file', 'edit_file'] : undefined,
|
|
1002
|
+
// Only request permission in Manual mode
|
|
1003
|
+
onRequestPermission: session.currentModeId === 'manual'
|
|
1004
|
+
? async (toolCall) => {
|
|
1005
|
+
const permToolCallId = `perm_${randomUUID()}`;
|
|
1006
|
+
const result = await transport.request('session/request_permission', {
|
|
1024
1007
|
sessionId: params.sessionId,
|
|
1025
|
-
|
|
1026
|
-
|
|
1008
|
+
toolCall: {
|
|
1009
|
+
toolCallId: permToolCallId,
|
|
1010
|
+
toolName: toolCall.tool,
|
|
1011
|
+
toolInput: formatToolInputForPermission(toolCall.tool, toolCall.parameters),
|
|
1012
|
+
status: 'pending',
|
|
1013
|
+
content: [],
|
|
1014
|
+
},
|
|
1015
|
+
options: [
|
|
1016
|
+
{ optionId: 'allow_once', name: 'Allow once', kind: 'allow_once' },
|
|
1017
|
+
{ optionId: 'allow_always', name: 'Allow always', kind: 'allow_always' },
|
|
1018
|
+
{ optionId: 'reject_once', name: 'Reject once', kind: 'reject_once' },
|
|
1019
|
+
{ optionId: 'reject_always', name: 'Reject always', kind: 'reject_always' },
|
|
1020
|
+
],
|
|
1027
1021
|
});
|
|
1022
|
+
// Map ACP outcome back to PermissionOutcome
|
|
1023
|
+
if (!result || result.outcome.type === 'cancelled')
|
|
1024
|
+
return 'reject_once';
|
|
1025
|
+
return result.outcome.optionId;
|
|
1028
1026
|
}
|
|
1029
1027
|
: undefined,
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
//
|
|
1033
|
-
//
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1028
|
+
// Per ACP spec, `fs/read_text_file` and `fs/write_text_file` are
|
|
1029
|
+
// CLIENT methods — only safe to call when the client advertised
|
|
1030
|
+
// the capability in `initialize`. Routing through the client
|
|
1031
|
+
// means the editor's dirty buffers + undo history stay correct
|
|
1032
|
+
// (otherwise an in-editor unsaved change would be invisible to
|
|
1033
|
+
// the agent, or worse, silently overwritten).
|
|
1034
|
+
fs: {
|
|
1035
|
+
readTextFile: clientSupportsFsRead
|
|
1036
|
+
? async (absolutePath) => {
|
|
1037
|
+
const result = await transport.request('fs/read_text_file', {
|
|
1038
|
+
sessionId: params.sessionId,
|
|
1039
|
+
path: absolutePath,
|
|
1040
|
+
});
|
|
1041
|
+
if (!result || typeof result.content !== 'string') {
|
|
1042
|
+
throw new Error('fs/read_text_file returned no content');
|
|
1043
|
+
}
|
|
1044
|
+
return result.content;
|
|
1045
|
+
}
|
|
1046
|
+
: undefined,
|
|
1047
|
+
writeTextFile: clientSupportsFsWrite
|
|
1048
|
+
? async (absolutePath, content) => {
|
|
1049
|
+
await transport.request('fs/write_text_file', {
|
|
1050
|
+
sessionId: params.sessionId,
|
|
1051
|
+
path: absolutePath,
|
|
1052
|
+
content,
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
: undefined,
|
|
1056
|
+
},
|
|
1057
|
+
onExecuteCommand: async (command, args, cwd) => {
|
|
1058
|
+
// Per ACP spec, only call terminal/* if the client advertised the
|
|
1059
|
+
// capability in initialize. Otherwise execute locally.
|
|
1060
|
+
if (!clientSupportsTerminal) {
|
|
1061
|
+
const r = await executeCommandAsync(command, args, { cwd, projectRoot: cwd, timeout: 120000 });
|
|
1062
|
+
return { stdout: r.stdout ?? '', stderr: r.stderr ?? '', exitCode: r.exitCode ?? 0 };
|
|
1063
|
+
}
|
|
1064
|
+
try {
|
|
1065
|
+
const createResult = await transport.request('terminal/create', {
|
|
1066
|
+
sessionId: params.sessionId,
|
|
1067
|
+
command,
|
|
1068
|
+
args,
|
|
1069
|
+
cwd,
|
|
1070
|
+
outputByteLimit: 1_000_000,
|
|
1071
|
+
});
|
|
1072
|
+
const { terminalId } = createResult;
|
|
1073
|
+
// Spec method is snake_case `terminal/wait_for_exit` and takes
|
|
1074
|
+
// only { sessionId, terminalId } — no timeoutMs.
|
|
1075
|
+
const waitResult = await transport.request('terminal/wait_for_exit', {
|
|
1076
|
+
sessionId: params.sessionId,
|
|
1077
|
+
terminalId,
|
|
1078
|
+
});
|
|
1079
|
+
const outputResult = await transport.request('terminal/output', {
|
|
1080
|
+
sessionId: params.sessionId,
|
|
1081
|
+
terminalId,
|
|
1082
|
+
});
|
|
1083
|
+
await transport.request('terminal/release', {
|
|
1084
|
+
sessionId: params.sessionId,
|
|
1085
|
+
terminalId,
|
|
1086
|
+
});
|
|
1087
|
+
const exitCode = waitResult.exitStatus.type === 'exited' ? waitResult.exitStatus.code : 1;
|
|
1088
|
+
return { stdout: outputResult.output ?? '', stderr: '', exitCode };
|
|
1089
|
+
}
|
|
1090
|
+
catch (err) {
|
|
1091
|
+
// Client terminal failed — fall back to local execution
|
|
1092
|
+
const r = await executeCommandAsync(command, args, { cwd, projectRoot: cwd, timeout: 120000 });
|
|
1093
|
+
return { stdout: r.stdout ?? '', stderr: r.stderr ?? '', exitCode: r.exitCode ?? 0 };
|
|
1094
|
+
}
|
|
1095
|
+
},
|
|
1096
|
+
}).then(() => {
|
|
1097
|
+
session.history.push({ role: 'user', content: prompt });
|
|
1098
|
+
const agentResponse = agentResponseChunks.join('');
|
|
1099
|
+
if (agentResponse) {
|
|
1100
|
+
session.history.push({ role: 'assistant', content: agentResponse });
|
|
1037
1101
|
}
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
const
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1102
|
+
autoSaveSession(session.history, session.workspaceRoot);
|
|
1103
|
+
// Report token usage to dashboard
|
|
1104
|
+
const projectCtx = getProjectContext(session.workspaceRoot);
|
|
1105
|
+
const sharedFields = {
|
|
1106
|
+
sessionId: session.codeepSessionId,
|
|
1107
|
+
sessionName: session.codeepSessionId,
|
|
1108
|
+
messageCount: session.history.length,
|
|
1109
|
+
cliVersion: getCurrentVersion(),
|
|
1110
|
+
projectName: projectCtx?.name,
|
|
1111
|
+
projectId: generateProjectId(session.workspaceRoot),
|
|
1112
|
+
language: projectCtx?.type,
|
|
1113
|
+
isGit: isGitRepository(session.workspaceRoot),
|
|
1114
|
+
};
|
|
1115
|
+
const costBreakdown = getCostBreakdown(tokenReportStart);
|
|
1116
|
+
if (costBreakdown.length > 0) {
|
|
1117
|
+
for (const entry of costBreakdown) {
|
|
1118
|
+
reportStats({
|
|
1119
|
+
...sharedFields,
|
|
1120
|
+
model: entry.model,
|
|
1121
|
+
provider: entry.provider,
|
|
1122
|
+
inputTokens: entry.promptTokens || undefined,
|
|
1123
|
+
outputTokens: entry.completionTokens || undefined,
|
|
1124
|
+
cacheCreationTokens: entry.cacheCreationTokens || undefined,
|
|
1125
|
+
cacheReadTokens: entry.cacheReadTokens || undefined,
|
|
1126
|
+
estimatedCost: entry.estimatedCost || undefined,
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
1063
1129
|
}
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
const r = await executeCommandAsync(command, args, { cwd, projectRoot: cwd, timeout: 120000 });
|
|
1067
|
-
return { stdout: r.stdout ?? '', stderr: r.stderr ?? '', exitCode: r.exitCode ?? 0 };
|
|
1130
|
+
else {
|
|
1131
|
+
reportStats({ ...sharedFields, model: config.get('model'), provider: config.get('provider') });
|
|
1068
1132
|
}
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
sessionId: session.codeepSessionId,
|
|
1081
|
-
sessionName: session.codeepSessionId,
|
|
1082
|
-
messageCount: session.history.length,
|
|
1083
|
-
cliVersion: getCurrentVersion(),
|
|
1084
|
-
projectName: projectCtx?.name,
|
|
1085
|
-
projectId: generateProjectId(session.workspaceRoot),
|
|
1086
|
-
language: projectCtx?.type,
|
|
1087
|
-
isGit: isGitRepository(session.workspaceRoot),
|
|
1088
|
-
};
|
|
1089
|
-
const costBreakdown = getCostBreakdown();
|
|
1090
|
-
if (costBreakdown.length > 0) {
|
|
1091
|
-
for (const entry of costBreakdown) {
|
|
1092
|
-
reportStats({
|
|
1093
|
-
...sharedFields,
|
|
1094
|
-
model: entry.model,
|
|
1095
|
-
provider: entry.provider,
|
|
1096
|
-
inputTokens: entry.promptTokens || undefined,
|
|
1097
|
-
outputTokens: entry.completionTokens || undefined,
|
|
1098
|
-
cacheCreationTokens: entry.cacheCreationTokens || undefined,
|
|
1099
|
-
cacheReadTokens: entry.cacheReadTokens || undefined,
|
|
1100
|
-
estimatedCost: entry.estimatedCost || undefined,
|
|
1101
|
-
});
|
|
1133
|
+
// Sync session history to dashboard
|
|
1134
|
+
syncSession({
|
|
1135
|
+
sessionId: session.codeepSessionId,
|
|
1136
|
+
projectName: projectCtx?.name,
|
|
1137
|
+
projectId: generateProjectId(session.workspaceRoot),
|
|
1138
|
+
messages: session.history,
|
|
1139
|
+
});
|
|
1140
|
+
// Update title with first real prompt if session had no history
|
|
1141
|
+
if (!session.titleSent && !session.hadHistory) {
|
|
1142
|
+
session.titleSent = true;
|
|
1143
|
+
sendSessionTitle(params.sessionId, [{ role: 'user', content: prompt }]);
|
|
1102
1144
|
}
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
messages: session.history,
|
|
1113
|
-
});
|
|
1114
|
-
// Update title with first real prompt if session had no history
|
|
1115
|
-
if (!session.titleSent && !session.hadHistory) {
|
|
1116
|
-
session.titleSent = true;
|
|
1117
|
-
sendSessionTitle(params.sessionId, [{ role: 'user', content: prompt }]);
|
|
1118
|
-
}
|
|
1119
|
-
transport.respond(msg.id, { stopReason: 'end_turn' });
|
|
1120
|
-
}).catch((err) => {
|
|
1121
|
-
if (err.name === 'AbortError') {
|
|
1122
|
-
// Clear plan UI on the client side when session is cancelled
|
|
1123
|
-
if (planEntries.size > 0) {
|
|
1124
|
-
planEntries.clear();
|
|
1125
|
-
sendPlan();
|
|
1145
|
+
transport.respond(msg.id, { stopReason: 'end_turn' });
|
|
1146
|
+
}).catch((err) => {
|
|
1147
|
+
if (err.name === 'AbortError') {
|
|
1148
|
+
// Clear plan UI on the client side when session is cancelled
|
|
1149
|
+
if (planEntries.size > 0) {
|
|
1150
|
+
planEntries.clear();
|
|
1151
|
+
sendPlan();
|
|
1152
|
+
}
|
|
1153
|
+
transport.respond(msg.id, { stopReason: 'cancelled' });
|
|
1126
1154
|
}
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1155
|
+
else if (err.message?.includes('API key not configured') || err.message?.includes('API key') || (err instanceof ApiError && err.status === 401)) {
|
|
1156
|
+
sendChunk(`❌ No API key configured. Use /login <provider> <key> or set the environment variable (e.g. ZAI_API_KEY, ANTHROPIC_API_KEY).`);
|
|
1157
|
+
transport.respond(msg.id, { stopReason: 'end_turn' });
|
|
1158
|
+
}
|
|
1159
|
+
else if (err instanceof ApiError && err.status >= 500) {
|
|
1160
|
+
sendChunk(`⚠️ API server error (${err.status}). Please try again.`);
|
|
1161
|
+
transport.respond(msg.id, { stopReason: 'end_turn' });
|
|
1162
|
+
}
|
|
1163
|
+
else {
|
|
1164
|
+
transport.error(msg.id, -32000, err.message);
|
|
1165
|
+
}
|
|
1166
|
+
}).finally(() => {
|
|
1167
|
+
if (session)
|
|
1168
|
+
session.abortController = null;
|
|
1169
|
+
planEntries.clear();
|
|
1170
|
+
});
|
|
1171
|
+
})
|
|
1172
|
+
.catch((err) => {
|
|
1173
|
+
if (err.message?.includes('API key not configured') || err.message?.includes('API key') || (err instanceof ApiError && err.status === 401)) {
|
|
1130
1174
|
sendChunk(`❌ No API key configured. Use /login <provider> <key> or set the environment variable (e.g. ZAI_API_KEY, ANTHROPIC_API_KEY).`);
|
|
1131
1175
|
transport.respond(msg.id, { stopReason: 'end_turn' });
|
|
1132
1176
|
}
|
|
@@ -1137,26 +1181,9 @@ export function startAcpServer() {
|
|
|
1137
1181
|
else {
|
|
1138
1182
|
transport.error(msg.id, -32000, err.message);
|
|
1139
1183
|
}
|
|
1140
|
-
}).finally(() => {
|
|
1141
1184
|
if (session)
|
|
1142
1185
|
session.abortController = null;
|
|
1143
|
-
planEntries.clear();
|
|
1144
1186
|
});
|
|
1145
|
-
})
|
|
1146
|
-
.catch((err) => {
|
|
1147
|
-
if (err.message?.includes('API key not configured') || err.message?.includes('API key') || (err instanceof ApiError && err.status === 401)) {
|
|
1148
|
-
sendChunk(`❌ No API key configured. Use /login <provider> <key> or set the environment variable (e.g. ZAI_API_KEY, ANTHROPIC_API_KEY).`);
|
|
1149
|
-
transport.respond(msg.id, { stopReason: 'end_turn' });
|
|
1150
|
-
}
|
|
1151
|
-
else if (err instanceof ApiError && err.status >= 500) {
|
|
1152
|
-
sendChunk(`⚠️ API server error (${err.status}). Please try again.`);
|
|
1153
|
-
transport.respond(msg.id, { stopReason: 'end_turn' });
|
|
1154
|
-
}
|
|
1155
|
-
else {
|
|
1156
|
-
transport.error(msg.id, -32000, err.message);
|
|
1157
|
-
}
|
|
1158
|
-
if (session)
|
|
1159
|
-
session.abortController = null;
|
|
1160
1187
|
});
|
|
1161
1188
|
}
|
|
1162
1189
|
// Keep process alive until stdin closes (Zed terminates us)
|
package/dist/config/index.d.ts
CHANGED
|
@@ -140,6 +140,16 @@ export interface ConfigSchema {
|
|
|
140
140
|
}
|
|
141
141
|
export type { AgentMode };
|
|
142
142
|
export type { LanguageCode };
|
|
143
|
+
/**
|
|
144
|
+
* Get sessions directory - local .codeep/sessions/ if in project, otherwise global
|
|
145
|
+
*/
|
|
146
|
+
export declare function getSessionsDir(projectPath?: string): string;
|
|
147
|
+
/**
|
|
148
|
+
* Check if directory is a project
|
|
149
|
+
* Looks for common project indicators: package.json, pyproject.toml, Cargo.toml, go.mod, composer.json, etc.
|
|
150
|
+
* Also checks if user has manually initialized this folder as a project (.codeep/project.json)
|
|
151
|
+
*/
|
|
152
|
+
export declare function isProjectDirectory(path: string): boolean;
|
|
143
153
|
/**
|
|
144
154
|
* Check if directory has standard project markers (not manually initialized)
|
|
145
155
|
*/
|