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.
Files changed (62) hide show
  1. package/README.md +35 -24
  2. package/dist/acp/commands.js +22 -1
  3. package/dist/acp/server.js +278 -251
  4. package/dist/config/index.d.ts +10 -0
  5. package/dist/config/index.js +2 -2
  6. package/dist/config/providers.js +35 -22
  7. package/dist/renderer/App.d.ts +0 -30
  8. package/dist/renderer/App.js +149 -659
  9. package/dist/renderer/agentExecution.d.ts +2 -1
  10. package/dist/renderer/agentExecution.js +10 -6
  11. package/dist/renderer/commands/helpers.d.ts +63 -0
  12. package/dist/renderer/commands/helpers.js +108 -0
  13. package/dist/renderer/commands/registry.js +5 -0
  14. package/dist/renderer/commands.d.ts +4 -0
  15. package/dist/renderer/commands.js +183 -64
  16. package/dist/renderer/components/ActionFormatting.d.ts +17 -0
  17. package/dist/renderer/components/ActionFormatting.js +67 -0
  18. package/dist/renderer/components/Autocomplete.d.ts +33 -0
  19. package/dist/renderer/components/Autocomplete.js +40 -0
  20. package/dist/renderer/components/Intro.d.ts +9 -0
  21. package/dist/renderer/components/Intro.js +5 -15
  22. package/dist/renderer/components/MessageFormatter.d.ts +96 -0
  23. package/dist/renderer/components/MessageFormatter.js +375 -0
  24. package/dist/renderer/components/Permission.d.ts +4 -0
  25. package/dist/renderer/components/Permission.js +1 -1
  26. package/dist/renderer/components/Status.d.ts +4 -0
  27. package/dist/renderer/components/Status.js +2 -3
  28. package/dist/renderer/components/WelcomeFormatter.d.ts +19 -0
  29. package/dist/renderer/components/WelcomeFormatter.js +79 -0
  30. package/dist/renderer/components/uiConstants.d.ts +8 -0
  31. package/dist/renderer/components/uiConstants.js +24 -0
  32. package/dist/renderer/inputParsing.d.ts +22 -0
  33. package/dist/renderer/inputParsing.js +28 -0
  34. package/dist/renderer/layout.d.ts +215 -0
  35. package/dist/renderer/layout.js +326 -0
  36. package/dist/renderer/main.d.ts +2 -1
  37. package/dist/renderer/main.js +45 -10
  38. package/dist/renderer/ollamaHint.d.ts +12 -0
  39. package/dist/renderer/ollamaHint.js +29 -0
  40. package/dist/utils/agentChat.js +28 -2
  41. package/dist/utils/codeepCloud.d.ts +54 -0
  42. package/dist/utils/codeepCloud.js +95 -0
  43. package/dist/utils/export.d.ts +12 -0
  44. package/dist/utils/export.js +3 -3
  45. package/dist/utils/hooks.d.ts +26 -0
  46. package/dist/utils/hooks.js +69 -1
  47. package/dist/utils/keychain.js +45 -29
  48. package/dist/utils/logger.d.ts +12 -0
  49. package/dist/utils/logger.js +1 -1
  50. package/dist/utils/mcpConfig.d.ts +26 -0
  51. package/dist/utils/mcpConfig.js +109 -4
  52. package/dist/utils/skillBundles.d.ts +14 -0
  53. package/dist/utils/skillBundles.js +3 -3
  54. package/dist/utils/skillBundlesCloud.d.ts +7 -0
  55. package/dist/utils/skillBundlesCloud.js +1 -1
  56. package/dist/utils/tokenTracker.d.ts +30 -3
  57. package/dist/utils/tokenTracker.js +71 -13
  58. package/dist/utils/toolParsing.d.ts +11 -0
  59. package/dist/utils/toolParsing.js +6 -0
  60. package/dist/version.d.ts +1 -1
  61. package/dist/version.js +1 -1
  62. package/package.json +2 -2
@@ -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 { loadMcpServerConfig, mergeMcpServers } from '../utils/mcpConfig.js';
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, resetTokenTracking } from '../utils/tokenTracker.js';
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
- if (typeof params.old_string === 'string' && typeof params.new_string === 'string') {
145
- out.changes = `replace ${params.old_string.split('\n').length} line(s)`;
146
- out.old_string = truncateDiff(params.old_string);
147
- out.new_string = truncateDiff(params.new_string);
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
- const fromConfig = loadMcpServerConfig(cwd);
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
- handleCommand(prompt, session, sendChunk, abortController.signal)
881
- .then((cmd) => {
882
- if (cmd.handled) {
883
- if (cmd.response)
884
- sendChunk(cmd.response);
885
- // If provider or model changed, push updated config options to Zed
886
- if (cmd.configOptionsChanged) {
887
- transport.notify('session/update', {
888
- sessionId: params.sessionId,
889
- update: {
890
- sessionUpdate: 'config_option_update',
891
- configOptions: buildConfigOptions(),
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: 'tool_call',
934
- toolCallId,
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
- else {
955
- // tool_call_update: update status to completed/failed, with optional content
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: 'tool_call_update',
960
- toolCallId,
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
- // Mark plan entry as completed
966
- const entry = planEntries.get(toolCallId);
967
- if (entry) {
968
- entry.status = 'completed';
969
- sendPlan();
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
- // Manual mode gates write_file/edit_file for this run only (per-call,
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
- path: absolutePath,
984
+ update: {
985
+ sessionUpdate: 'tool_call_update',
986
+ toolCallId,
987
+ status: status === 'finished' ? 'completed' : 'failed',
988
+ ...(rawOutput !== undefined ? { rawOutput } : {}),
989
+ },
1014
990
  });
1015
- if (!result || typeof result.content !== 'string') {
1016
- throw new Error('fs/read_text_file returned no content');
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
- : undefined,
1021
- writeTextFile: clientSupportsFsWrite
1022
- ? async (absolutePath, content) => {
1023
- await transport.request('fs/write_text_file', {
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
- path: absolutePath,
1026
- content,
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
- onExecuteCommand: async (command, args, cwd) => {
1032
- // Per ACP spec, only call terminal/* if the client advertised the
1033
- // capability in initialize. Otherwise execute locally.
1034
- if (!clientSupportsTerminal) {
1035
- const r = await executeCommandAsync(command, args, { cwd, projectRoot: cwd, timeout: 120000 });
1036
- return { stdout: r.stdout ?? '', stderr: r.stderr ?? '', exitCode: r.exitCode ?? 0 };
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
- try {
1039
- const createResult = await transport.request('terminal/create', {
1040
- sessionId: params.sessionId,
1041
- command,
1042
- args,
1043
- cwd,
1044
- outputByteLimit: 1_000_000,
1045
- });
1046
- const { terminalId } = createResult;
1047
- // Spec method is snake_case `terminal/wait_for_exit` and takes
1048
- // only { sessionId, terminalId } — no timeoutMs.
1049
- const waitResult = await transport.request('terminal/wait_for_exit', {
1050
- sessionId: params.sessionId,
1051
- terminalId,
1052
- });
1053
- const outputResult = await transport.request('terminal/output', {
1054
- sessionId: params.sessionId,
1055
- terminalId,
1056
- });
1057
- await transport.request('terminal/release', {
1058
- sessionId: params.sessionId,
1059
- terminalId,
1060
- });
1061
- const exitCode = waitResult.exitStatus.type === 'exited' ? waitResult.exitStatus.code : 1;
1062
- return { stdout: outputResult.output ?? '', stderr: '', exitCode };
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
- catch (err) {
1065
- // Client terminal failed — fall back to local execution
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
- }).then(() => {
1071
- session.history.push({ role: 'user', content: prompt });
1072
- const agentResponse = agentResponseChunks.join('');
1073
- if (agentResponse) {
1074
- session.history.push({ role: 'assistant', content: agentResponse });
1075
- }
1076
- autoSaveSession(session.history, session.workspaceRoot);
1077
- // Report token usage to dashboard
1078
- const projectCtx = getProjectContext(session.workspaceRoot);
1079
- const sharedFields = {
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
- else {
1105
- reportStats({ ...sharedFields, model: config.get('model'), provider: config.get('provider') });
1106
- }
1107
- // Sync session history to dashboard
1108
- syncSession({
1109
- sessionId: session.codeepSessionId,
1110
- projectName: projectCtx?.name,
1111
- projectId: generateProjectId(session.workspaceRoot),
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
- transport.respond(msg.id, { stopReason: 'cancelled' });
1128
- }
1129
- else if (err.message?.includes('API key not configured') || err.message?.includes('API key') || (err instanceof ApiError && err.status === 401)) {
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)
@@ -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
  */