codeep 1.2.35 → 1.2.37

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.
@@ -31,6 +31,15 @@ export declare function initWorkspace(workspaceRoot: string): {
31
31
  history: Message[];
32
32
  welcomeText: string;
33
33
  };
34
+ /**
35
+ * Restore a previously saved ACP session by its Zed sessionId.
36
+ * Falls back to initWorkspace if the session cannot be found on disk.
37
+ */
38
+ export declare function loadWorkspace(workspaceRoot: string, acpSessionId: string): {
39
+ codeepSessionId: string;
40
+ history: Message[];
41
+ welcomeText: string;
42
+ };
34
43
  /**
35
44
  * Try to handle a slash command. Async because skills and diff/review
36
45
  * need to call the AI API or run shell commands.
@@ -74,6 +74,40 @@ export function initWorkspace(workspaceRoot) {
74
74
  }
75
75
  return { codeepSessionId, history, welcomeText: lines.join('\n') };
76
76
  }
77
+ /**
78
+ * Restore a previously saved ACP session by its Zed sessionId.
79
+ * Falls back to initWorkspace if the session cannot be found on disk.
80
+ */
81
+ export function loadWorkspace(workspaceRoot, acpSessionId) {
82
+ // Ensure workspace is set up
83
+ const codeepDir = join(workspaceRoot, '.codeep');
84
+ if (!existsSync(codeepDir)) {
85
+ mkdirSync(codeepDir, { recursive: true });
86
+ }
87
+ if (!isManuallyInitializedProject(workspaceRoot)) {
88
+ initializeAsProject(workspaceRoot);
89
+ }
90
+ if (!hasReadPermission(workspaceRoot)) {
91
+ setProjectPermission(workspaceRoot, true, true);
92
+ }
93
+ // Try to load the session that was saved under this ACP session ID
94
+ const loaded = loadSession(acpSessionId, workspaceRoot);
95
+ if (loaded) {
96
+ const history = loaded;
97
+ const provider = getCurrentProvider();
98
+ const model = config.get('model');
99
+ const lines = [
100
+ `**Codeep** • ${provider.name} • \`${model}\``,
101
+ '',
102
+ `**Session restored:** ${acpSessionId} (${history.length} messages)`,
103
+ '',
104
+ ...formatSessionPreviewLines(history),
105
+ ];
106
+ return { codeepSessionId: acpSessionId, history, welcomeText: lines.join('\n') };
107
+ }
108
+ // Session not found — fall back to initWorkspace behaviour
109
+ return initWorkspace(workspaceRoot);
110
+ }
77
111
  // ─── Command dispatch ─────────────────────────────────────────────────────────
78
112
  /**
79
113
  * Try to handle a slash command. Async because skills and diff/review
@@ -19,15 +19,23 @@ export interface JsonRpcNotification {
19
19
  params?: unknown;
20
20
  }
21
21
  export interface InitializeParams {
22
- capabilities?: Record<string, unknown>;
23
- workspaceFolders?: {
24
- uri: string;
22
+ protocolVersion?: number;
23
+ clientCapabilities?: {
24
+ fs?: {
25
+ readTextFile?: boolean;
26
+ writeTextFile?: boolean;
27
+ };
28
+ terminal?: boolean;
29
+ };
30
+ clientInfo?: {
25
31
  name: string;
26
- }[];
32
+ version: string;
33
+ };
27
34
  }
28
35
  export interface InitializeResult {
29
36
  protocolVersion: number;
30
37
  agentCapabilities: {
38
+ loadSession?: boolean;
31
39
  streaming?: boolean;
32
40
  fileEditing?: boolean;
33
41
  };
@@ -37,21 +45,155 @@ export interface InitializeResult {
37
45
  };
38
46
  authMethods: unknown[];
39
47
  }
40
- export interface SessionNewParams {
41
- cwd: string;
42
- mcpServers?: {
48
+ export interface McpServer {
49
+ name: string;
50
+ command: string;
51
+ args: string[];
52
+ env?: Record<string, string>;
53
+ }
54
+ export interface SessionMode {
55
+ id: string;
56
+ name: string;
57
+ description?: string | null;
58
+ }
59
+ export interface SessionModeState {
60
+ availableModes: SessionMode[];
61
+ currentModeId: string;
62
+ }
63
+ export interface SessionConfigOption {
64
+ id: string;
65
+ name: string;
66
+ description?: string | null;
67
+ category?: 'mode' | 'model' | 'thought_level' | null;
68
+ type: 'select';
69
+ options?: {
70
+ id: string;
43
71
  name: string;
44
- command: string;
45
- args: string[];
46
- env?: Record<string, string>;
47
72
  }[];
73
+ currentValue?: string;
74
+ }
75
+ export interface SessionNewParams {
76
+ cwd: string;
77
+ mcpServers?: McpServer[];
78
+ }
79
+ export interface SessionNewResult {
80
+ sessionId: string;
81
+ modes?: SessionModeState | null;
82
+ configOptions?: SessionConfigOption[] | null;
83
+ }
84
+ export interface SessionLoadParams {
85
+ sessionId: string;
86
+ cwd: string;
87
+ mcpServers?: McpServer[];
88
+ }
89
+ export interface SessionLoadResult {
90
+ modes?: SessionModeState | null;
91
+ configOptions?: SessionConfigOption[] | null;
48
92
  }
49
93
  export interface ContentBlock {
50
- type: 'text';
51
- text: string;
94
+ type: 'text' | 'image' | 'audio' | 'resource_link' | 'resource';
95
+ text?: string;
96
+ data?: string;
97
+ mimeType?: string;
98
+ uri?: string;
99
+ name?: string;
52
100
  }
53
101
  export interface SessionPromptParams {
54
102
  sessionId: string;
55
103
  prompt: ContentBlock[];
56
104
  }
105
+ export interface SessionPromptResult {
106
+ stopReason: 'end_turn' | 'cancelled';
107
+ }
108
+ export interface SessionCancelParams {
109
+ sessionId: string;
110
+ }
111
+ export interface SetSessionModeParams {
112
+ sessionId: string;
113
+ modeId: string;
114
+ }
115
+ export interface SetSessionConfigOptionParams {
116
+ sessionId: string;
117
+ configId: string;
118
+ value: unknown;
119
+ }
120
+ export type ToolCallState = 'running' | 'finished' | 'error';
121
+ export interface SessionUpdateContentChunk {
122
+ type: 'content_chunk';
123
+ sessionId: string;
124
+ content: ContentBlock;
125
+ }
126
+ export interface SessionUpdateToolCall {
127
+ type: 'tool_call';
128
+ sessionId: string;
129
+ toolCallId: string;
130
+ toolName: string;
131
+ toolInput: unknown;
132
+ state: ToolCallState;
133
+ content?: {
134
+ type: 'text';
135
+ text: string;
136
+ }[];
137
+ }
138
+ export interface SessionUpdateThoughtChunk {
139
+ type: 'agent_thought_chunk';
140
+ sessionId: string;
141
+ content: ContentBlock;
142
+ }
143
+ export interface SessionUpdateAvailableCommands {
144
+ type: 'available_commands_update';
145
+ sessionId: string;
146
+ availableCommands: {
147
+ name: string;
148
+ description: string;
149
+ input?: {
150
+ hint: string;
151
+ };
152
+ }[];
153
+ }
154
+ export interface SessionUpdateCurrentMode {
155
+ type: 'current_mode_update';
156
+ sessionId: string;
157
+ currentModeId: string;
158
+ }
159
+ export type SessionUpdateParams = SessionUpdateContentChunk | SessionUpdateToolCall | SessionUpdateThoughtChunk | SessionUpdateAvailableCommands | SessionUpdateCurrentMode;
160
+ export type PermissionOptionKind = 'allow_once' | 'allow_always' | 'reject_once' | 'reject_always';
161
+ export interface PermissionOption {
162
+ optionId: string;
163
+ name: string;
164
+ kind: PermissionOptionKind;
165
+ }
166
+ export interface RequestPermissionParams {
167
+ sessionId: string;
168
+ toolCall: {
169
+ toolCallId: string;
170
+ toolName: string;
171
+ toolInput: unknown;
172
+ state: ToolCallState;
173
+ content: unknown[];
174
+ };
175
+ options: PermissionOption[];
176
+ }
177
+ export interface RequestPermissionResult {
178
+ outcome: {
179
+ type: 'cancelled';
180
+ } | {
181
+ type: 'selected';
182
+ optionId: string;
183
+ };
184
+ }
185
+ export interface FsReadTextFileParams {
186
+ sessionId: string;
187
+ path: string;
188
+ line?: number;
189
+ limit?: number;
190
+ }
191
+ export interface FsReadTextFileResult {
192
+ content: string;
193
+ }
194
+ export interface FsWriteTextFileParams {
195
+ sessionId: string;
196
+ path: string;
197
+ content: string;
198
+ }
57
199
  export type AcpMessage = JsonRpcRequest | JsonRpcResponse | JsonRpcNotification;
@@ -1,3 +1,3 @@
1
1
  // acp/protocol.ts
2
- // ACP JSON-RPC message types for Zed Agent Client Protocol
2
+ // ACP JSON-RPC message types Agent Client Protocol spec
3
3
  export {};
@@ -3,9 +3,10 @@
3
3
  import { randomUUID } from 'crypto';
4
4
  import { StdioTransport } from './transport.js';
5
5
  import { runAgentSession } from './session.js';
6
- import { initWorkspace, handleCommand } from './commands.js';
7
- import { autoSaveSession } from '../config/index.js';
8
- // All advertised slash commands (shown in Zed autocomplete)
6
+ import { initWorkspace, loadWorkspace, handleCommand } from './commands.js';
7
+ import { autoSaveSession, config } from '../config/index.js';
8
+ import { getCurrentVersion } from '../utils/update.js';
9
+ // ─── Slash commands advertised to Zed ────────────────────────────────────────
9
10
  const AVAILABLE_COMMANDS = [
10
11
  // Configuration
11
12
  { name: 'help', description: 'Show available commands' },
@@ -48,51 +49,92 @@ const AVAILABLE_COMMANDS = [
48
49
  { name: 'build', description: 'Build the project' },
49
50
  { name: 'deploy', description: 'Deploy the project' },
50
51
  ];
52
+ // ─── Mode definitions ─────────────────────────────────────────────────────────
53
+ const AGENT_MODES = {
54
+ currentModeId: 'auto',
55
+ availableModes: [
56
+ { id: 'auto', name: 'Auto', description: 'Agent runs automatically without confirmation' },
57
+ { id: 'manual', name: 'Manual', description: 'Confirm dangerous operations before running' },
58
+ ],
59
+ };
60
+ // ─── Config options ───────────────────────────────────────────────────────────
61
+ function buildConfigOptions() {
62
+ return [
63
+ {
64
+ id: 'model',
65
+ name: 'Model',
66
+ description: 'AI model to use',
67
+ category: 'model',
68
+ type: 'select',
69
+ currentValue: config.get('model'),
70
+ },
71
+ ];
72
+ }
73
+ // ─── Server ───────────────────────────────────────────────────────────────────
51
74
  export function startAcpServer() {
52
75
  const transport = new StdioTransport();
53
76
  // ACP sessionId → full AcpSession (includes history + codeep session tracking)
54
77
  const sessions = new Map();
55
78
  transport.start((msg) => {
56
- switch (msg.method) {
79
+ // Notifications have no id — handle separately
80
+ if (!('id' in msg)) {
81
+ handleNotification(msg);
82
+ return;
83
+ }
84
+ const req = msg;
85
+ switch (req.method) {
57
86
  case 'initialize':
58
- handleInitialize(msg);
59
- break;
60
- case 'initialized':
61
- // no-op acknowledgment
87
+ handleInitialize(req);
62
88
  break;
89
+ case 'initialized': /* no-op acknowledgment */ break;
63
90
  case 'session/new':
64
- handleSessionNew(msg);
91
+ handleSessionNew(req);
92
+ break;
93
+ case 'session/load':
94
+ handleSessionLoad(req);
65
95
  break;
66
96
  case 'session/prompt':
67
- handleSessionPrompt(msg);
97
+ handleSessionPrompt(req);
98
+ break;
99
+ case 'session/set_mode':
100
+ handleSetMode(req);
68
101
  break;
69
- case 'session/cancel':
70
- handleSessionCancel(msg);
102
+ case 'session/set_config_option':
103
+ handleSetConfigOption(req);
71
104
  break;
72
105
  default:
73
- transport.error(msg.id, -32601, `Method not found: ${msg.method}`);
106
+ transport.error(req.id, -32601, `Method not found: ${req.method}`);
74
107
  }
75
108
  });
109
+ // ── Notification handler (no id, no response) ──────────────────────────────
110
+ function handleNotification(msg) {
111
+ if (msg.method === 'session/cancel') {
112
+ const { sessionId } = (msg.params ?? {});
113
+ sessions.get(sessionId)?.abortController?.abort();
114
+ }
115
+ }
116
+ // ── initialize ──────────────────────────────────────────────────────────────
76
117
  function handleInitialize(msg) {
77
118
  const _params = msg.params;
78
119
  const result = {
79
120
  protocolVersion: 1,
80
121
  agentCapabilities: {
122
+ loadSession: true,
81
123
  streaming: true,
82
124
  fileEditing: true,
83
125
  },
84
126
  agentInfo: {
85
127
  name: 'codeep',
86
- version: '1.0.0',
128
+ version: getCurrentVersion(),
87
129
  },
88
130
  authMethods: [],
89
131
  };
90
132
  transport.respond(msg.id, result);
91
133
  }
134
+ // ── session/new ─────────────────────────────────────────────────────────────
92
135
  function handleSessionNew(msg) {
93
136
  const params = msg.params;
94
137
  const acpSessionId = randomUUID();
95
- // Initialise workspace: create .codeep folder, load/create codeep session
96
138
  const { codeepSessionId, history, welcomeText } = initWorkspace(params.cwd);
97
139
  sessions.set(acpSessionId, {
98
140
  sessionId: acpSessionId,
@@ -101,25 +143,103 @@ export function startAcpServer() {
101
143
  codeepSessionId,
102
144
  addedFiles: new Map(),
103
145
  abortController: null,
146
+ currentModeId: 'auto',
104
147
  });
105
- transport.respond(msg.id, { sessionId: acpSessionId });
106
- // Advertise all available slash commands to Zed
148
+ const result = {
149
+ sessionId: acpSessionId,
150
+ modes: AGENT_MODES,
151
+ configOptions: buildConfigOptions(),
152
+ };
153
+ transport.respond(msg.id, result);
154
+ // Advertise slash commands
107
155
  transport.notify('session/update', {
156
+ type: 'available_commands_update',
108
157
  sessionId: acpSessionId,
109
- update: {
110
- sessionUpdate: 'available_commands_update',
111
- availableCommands: AVAILABLE_COMMANDS,
112
- },
158
+ availableCommands: AVAILABLE_COMMANDS,
113
159
  });
114
- // Stream welcome message
160
+ // Send welcome message
115
161
  transport.notify('session/update', {
162
+ type: 'content_chunk',
116
163
  sessionId: acpSessionId,
117
- update: {
118
- sessionUpdate: 'agent_message_chunk',
119
- content: { type: 'text', text: welcomeText },
120
- },
164
+ content: { type: 'text', text: welcomeText },
165
+ });
166
+ }
167
+ // ── session/load ────────────────────────────────────────────────────────────
168
+ function handleSessionLoad(msg) {
169
+ const params = msg.params;
170
+ // Try to restore existing Codeep session or fall back to fresh workspace
171
+ const existing = sessions.get(params.sessionId);
172
+ if (existing) {
173
+ // Session already in memory — update cwd if changed
174
+ existing.workspaceRoot = params.cwd;
175
+ const result = {
176
+ modes: AGENT_MODES,
177
+ configOptions: buildConfigOptions(),
178
+ };
179
+ transport.respond(msg.id, result);
180
+ return;
181
+ }
182
+ // Session not in memory — try to load from disk
183
+ const { codeepSessionId, history, welcomeText } = loadWorkspace(params.cwd, params.sessionId);
184
+ sessions.set(params.sessionId, {
185
+ sessionId: params.sessionId,
186
+ workspaceRoot: params.cwd,
187
+ history,
188
+ codeepSessionId,
189
+ addedFiles: new Map(),
190
+ abortController: null,
191
+ currentModeId: 'auto',
121
192
  });
193
+ const result = {
194
+ modes: AGENT_MODES,
195
+ configOptions: buildConfigOptions(),
196
+ };
197
+ transport.respond(msg.id, result);
198
+ // Send restored session welcome
199
+ transport.notify('session/update', {
200
+ type: 'content_chunk',
201
+ sessionId: params.sessionId,
202
+ content: { type: 'text', text: welcomeText },
203
+ });
204
+ }
205
+ // ── session/set_mode ────────────────────────────────────────────────────────
206
+ function handleSetMode(msg) {
207
+ const { sessionId, modeId } = msg.params;
208
+ const session = sessions.get(sessionId);
209
+ if (!session) {
210
+ transport.error(msg.id, -32602, `Unknown sessionId: ${sessionId}`);
211
+ return;
212
+ }
213
+ const validMode = AGENT_MODES.availableModes.find(m => m.id === modeId);
214
+ if (!validMode) {
215
+ transport.error(msg.id, -32602, `Unknown modeId: ${modeId}`);
216
+ return;
217
+ }
218
+ session.currentModeId = modeId;
219
+ // Map ACP mode to Codeep agentConfirmation setting
220
+ config.set('agentConfirmation', modeId === 'manual' ? 'dangerous' : 'never');
221
+ transport.respond(msg.id, {});
222
+ // Notify Zed of the mode change
223
+ transport.notify('session/update', {
224
+ type: 'current_mode_update',
225
+ sessionId,
226
+ currentModeId: modeId,
227
+ });
228
+ }
229
+ // ── session/set_config_option ───────────────────────────────────────────────
230
+ function handleSetConfigOption(msg) {
231
+ const { sessionId, configId, value } = msg.params;
232
+ const session = sessions.get(sessionId);
233
+ if (!session) {
234
+ transport.error(msg.id, -32602, `Unknown sessionId: ${sessionId}`);
235
+ return;
236
+ }
237
+ if (configId === 'model' && typeof value === 'string') {
238
+ config.set('model', value);
239
+ }
240
+ transport.respond(msg.id, {});
122
241
  }
242
+ // ── session/prompt ──────────────────────────────────────────────────────────
123
243
  function handleSessionPrompt(msg) {
124
244
  const params = msg.params;
125
245
  const session = sessions.get(params.sessionId);
@@ -130,32 +250,29 @@ export function startAcpServer() {
130
250
  // Extract text from ContentBlock[]
131
251
  const prompt = params.prompt
132
252
  .filter((b) => b.type === 'text')
133
- .map((b) => b.text)
253
+ .map((b) => b.text ?? '')
134
254
  .join('\n');
135
255
  const abortController = new AbortController();
136
256
  session.abortController = abortController;
257
+ const agentResponseChunks = [];
137
258
  const sendChunk = (text) => {
259
+ agentResponseChunks.push(text);
138
260
  transport.notify('session/update', {
261
+ type: 'content_chunk',
139
262
  sessionId: params.sessionId,
140
- update: {
141
- sessionUpdate: 'agent_message_chunk',
142
- content: { type: 'text', text },
143
- },
263
+ content: { type: 'text', text },
144
264
  });
145
265
  };
146
- // Try slash commands first (async — skills, diff, scan, etc.)
266
+ // Try slash commands first
147
267
  handleCommand(prompt, session, sendChunk, abortController.signal)
148
268
  .then((cmd) => {
149
269
  if (cmd.handled) {
150
- // For streaming commands (skills, diff), chunks were already sent via onChunk.
151
- // For simple commands, send the response now.
152
270
  if (cmd.response)
153
271
  sendChunk(cmd.response);
154
272
  transport.respond(msg.id, { stopReason: 'end_turn' });
155
273
  return;
156
274
  }
157
275
  // Not a command — run agent loop
158
- // Prepend any added-files context to the prompt
159
276
  let enrichedPrompt = prompt;
160
277
  if (session.addedFiles.size > 0) {
161
278
  const parts = ['[Attached files]'];
@@ -172,28 +289,23 @@ export function startAcpServer() {
172
289
  onChunk: sendChunk,
173
290
  onThought: (text) => {
174
291
  transport.notify('session/update', {
292
+ type: 'agent_thought_chunk',
175
293
  sessionId: params.sessionId,
176
- update: {
177
- sessionUpdate: 'agent_thought_chunk',
178
- content: { type: 'text', text },
179
- },
294
+ content: { type: 'text', text },
180
295
  });
181
296
  },
182
- onToolCall: (toolCallId, _toolName, kind, title, status, locations) => {
297
+ onToolCall: (toolCallId, toolName, _kind, _title, status, _locations) => {
183
298
  transport.notify('session/update', {
299
+ type: 'tool_call',
184
300
  sessionId: params.sessionId,
185
- update: {
186
- sessionUpdate: 'tool_call',
187
- toolCallId,
188
- title,
189
- kind,
190
- status,
191
- ...(locations?.length ? { locations: locations.map(uri => ({ uri })) } : {}),
192
- },
301
+ toolCallId,
302
+ toolName,
303
+ toolInput: {},
304
+ state: status === 'running' ? 'running' : status === 'finished' ? 'finished' : 'error',
305
+ content: [],
193
306
  });
194
307
  },
195
308
  onFileEdit: (uri, newText) => {
196
- // ACP structured file/edit notification — lets the editor apply changes
197
309
  transport.notify('file/edit', {
198
310
  uri,
199
311
  textChanges: newText
@@ -203,6 +315,10 @@ export function startAcpServer() {
203
315
  },
204
316
  }).then(() => {
205
317
  session.history.push({ role: 'user', content: prompt });
318
+ const agentResponse = agentResponseChunks.join('');
319
+ if (agentResponse) {
320
+ session.history.push({ role: 'assistant', content: agentResponse });
321
+ }
206
322
  autoSaveSession(session.history, session.workspaceRoot);
207
323
  transport.respond(msg.id, { stopReason: 'end_turn' });
208
324
  }).catch((err) => {
@@ -223,11 +339,6 @@ export function startAcpServer() {
223
339
  session.abortController = null;
224
340
  });
225
341
  }
226
- function handleSessionCancel(msg) {
227
- const { sessionId } = msg.params;
228
- sessions.get(sessionId)?.abortController?.abort();
229
- transport.respond(msg.id, {});
230
- }
231
342
  // Keep process alive until stdin closes (Zed terminates us)
232
343
  return new Promise((resolve) => {
233
344
  process.stdin.on('end', resolve);
@@ -44,6 +44,8 @@ function toolCallMeta(toolName, params) {
44
44
  export async function runAgentSession(opts) {
45
45
  const projectContext = buildProjectContext(opts.workspaceRoot);
46
46
  let toolCallCounter = 0;
47
+ // Maps tool call key → ACP toolCallId so onToolResult can emit finished/error status
48
+ const toolCallIdMap = new Map();
47
49
  const result = await runAgent(opts.prompt, projectContext, {
48
50
  abortSignal: opts.abortSignal,
49
51
  onIteration: (_iteration, _message) => {
@@ -68,6 +70,9 @@ export async function runAgentSession(opts) {
68
70
  : join(opts.workspaceRoot, filePath);
69
71
  locations.push(pathToFileURL(absPath).href);
70
72
  }
73
+ // Track this tool call so onToolResult can emit finished/error
74
+ const mapKey = toolCall.id ?? `${name}_${toolCallCounter}`;
75
+ toolCallIdMap.set(mapKey, { toolCallId, kind, locations: locations.length ? locations : undefined });
71
76
  // Emit tool_call notification (running state)
72
77
  opts.onToolCall?.(toolCallId, name, kind, title, 'running', locations.length ? locations : undefined);
73
78
  // For file edits, also send structured file/edit notification
@@ -81,6 +86,30 @@ export async function runAgentSession(opts) {
81
86
  }
82
87
  }
83
88
  },
89
+ onToolResult: (toolResult, toolCall) => {
90
+ // Find the tracked entry: prefer exact id match, then first FIFO entry for same tool name
91
+ let mapKey;
92
+ if (toolCall.id && toolCallIdMap.has(toolCall.id)) {
93
+ mapKey = toolCall.id;
94
+ }
95
+ else {
96
+ // FIFO: find oldest pending entry for this tool name
97
+ for (const [k, v] of toolCallIdMap) {
98
+ if (k.startsWith(`${toolCall.tool}_`) && v.toolCallId) {
99
+ mapKey = k;
100
+ break;
101
+ }
102
+ }
103
+ }
104
+ if (mapKey !== undefined) {
105
+ const tracked = toolCallIdMap.get(mapKey);
106
+ if (tracked && opts.onToolCall) {
107
+ const status = toolResult.success ? 'finished' : 'error';
108
+ opts.onToolCall(tracked.toolCallId, toolCall.tool, tracked.kind, '', status, tracked.locations);
109
+ }
110
+ toolCallIdMap.delete(mapKey);
111
+ }
112
+ },
84
113
  });
85
114
  // Emit the final response text if present
86
115
  if (result.finalResponse) {
@@ -1,5 +1,5 @@
1
1
  import { JsonRpcRequest, JsonRpcResponse, JsonRpcNotification } from './protocol.js';
2
- type MessageHandler = (msg: JsonRpcRequest) => void;
2
+ type MessageHandler = (msg: JsonRpcRequest | JsonRpcNotification) => void;
3
3
  export declare class StdioTransport {
4
4
  private buffer;
5
5
  private handler;
package/dist/api/index.js CHANGED
@@ -579,6 +579,6 @@ export async function validateApiKey(apiKey, providerId) {
579
579
  }
580
580
  }
581
581
  catch (err) {
582
- return { valid: false, error: err.message };
582
+ return { valid: false, error: err instanceof Error ? err.message : String(err) };
583
583
  }
584
584
  }
@@ -1,6 +1,7 @@
1
1
  import Conf from 'conf';
2
2
  import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, unlinkSync, statSync } from 'fs';
3
3
  import { join, dirname } from 'path';
4
+ import { randomUUID } from 'crypto';
4
5
  import { PROVIDERS, getProvider } from './providers.js';
5
6
  import { logSession } from '../utils/logger.js';
6
7
  // We'll initialize GLOBAL_SESSIONS_DIR after config is created (to use config.path)
@@ -389,8 +390,17 @@ export function isConfigured(providerId) {
389
390
  }
390
391
  // Get current provider info
391
392
  export function getCurrentProvider() {
392
- const providerId = config.get('provider');
393
- const provider = getProvider(providerId);
393
+ let providerId = config.get('provider');
394
+ let provider = getProvider(providerId);
395
+ // If stored provider no longer exists in registry, fall back to first available
396
+ if (!provider) {
397
+ const fallback = Object.keys(PROVIDERS)[0];
398
+ if (fallback) {
399
+ providerId = fallback;
400
+ provider = getProvider(fallback);
401
+ config.set('provider', fallback);
402
+ }
403
+ }
394
404
  return {
395
405
  id: providerId,
396
406
  name: provider?.name || providerId,
@@ -406,7 +416,7 @@ export function setProvider(providerId) {
406
416
  config.set('protocol', provider.defaultProtocol);
407
417
  // Load API key for the new provider into cache
408
418
  // This is async but we fire-and-forget since the key will be loaded before next API call
409
- loadApiKey(providerId);
419
+ loadApiKey(providerId).catch(() => { });
410
420
  return true;
411
421
  }
412
422
  // Get models for current provider
@@ -425,10 +435,8 @@ export function getModelsForCurrentProvider() {
425
435
  export { PROVIDERS } from './providers.js';
426
436
  // Generate unique session ID
427
437
  function generateSessionId() {
428
- const now = new Date();
429
- const date = now.toISOString().split('T')[0];
430
- const time = now.toTimeString().split(' ')[0].replace(/:/g, '-');
431
- return `session-${date}-${time}`;
438
+ const date = new Date().toISOString().split('T')[0];
439
+ return `session-${date}-${randomUUID().slice(0, 8)}`;
432
440
  }
433
441
  // Get or create current session ID
434
442
  export function getCurrentSessionId() {
@@ -636,7 +644,7 @@ export function getProjectPermission(projectPath) {
636
644
  if (configPath && existsSync(configPath)) {
637
645
  try {
638
646
  const data = JSON.parse(readFileSync(configPath, 'utf-8'));
639
- if (data.permission)
647
+ if (data.permission && typeof data.permission === 'object')
640
648
  return data.permission;
641
649
  }
642
650
  catch {
@@ -207,7 +207,7 @@ export function supportsNativeTools(providerId, protocol) {
207
207
  const provider = PROVIDERS[providerId];
208
208
  if (!provider)
209
209
  return false;
210
- return provider.protocols[protocol]?.supportsNativeTools ?? true; // Default to true
210
+ return provider.protocols[protocol]?.supportsNativeTools ?? false; // Default to false (safer)
211
211
  }
212
212
  /**
213
213
  * Returns the effective max output tokens for a provider, capped by the provider's limit.
@@ -129,17 +129,17 @@ async function handleSubmit(message) {
129
129
  executeAgentTask(enhancedTask, dryRun, ctx);
130
130
  return;
131
131
  }
132
+ const rateCheck = checkApiRateLimit();
133
+ if (!rateCheck.allowed) {
134
+ app.notify(rateCheck.message || 'Rate limit exceeded', 5000);
135
+ return;
136
+ }
132
137
  // Auto agent mode
133
138
  const agentMode = config.get('agentMode') || 'off';
134
139
  if (agentMode === 'on' && projectContext && hasWriteAccess && !isAgentRunningFlag) {
135
140
  runAgentTask(message, false, ctx, () => pendingInteractiveContext, (v) => { pendingInteractiveContext = v; });
136
141
  return;
137
142
  }
138
- const rateCheck = checkApiRateLimit();
139
- if (!rateCheck.allowed) {
140
- app.notify(rateCheck.message || 'Rate limit exceeded', 5000);
141
- return;
142
- }
143
143
  try {
144
144
  app.startStreaming();
145
145
  const history = app.getChatHistory();
@@ -124,6 +124,8 @@ export async function runAgent(prompt, projectContext, options = {}) {
124
124
  let finalResponse = '';
125
125
  let result;
126
126
  let consecutiveTimeouts = 0;
127
+ let incompleteWorkRetries = 0;
128
+ const maxIncompleteWorkRetries = 2;
127
129
  const maxTimeoutRetries = 3;
128
130
  const maxConsecutiveTimeouts = 9; // Allow more consecutive timeouts before giving up
129
131
  const baseTimeout = config.get('agentApiTimeout');
@@ -159,7 +161,7 @@ export async function runAgent(prompt, projectContext, options = {}) {
159
161
  const dynamicTimeout = calculateDynamicTimeout(prompt, iteration, baseTimeout);
160
162
  debug(`Using timeout: ${dynamicTimeout}ms (base: ${baseTimeout}ms)`);
161
163
  // Get AI response with retry logic for timeouts
162
- let chatResponse;
164
+ let chatResponse = null;
163
165
  let retryCount = 0;
164
166
  while (true) {
165
167
  try {
@@ -248,9 +250,11 @@ export async function runAgent(prompt, projectContext, options = {}) {
248
250
  const wantsToContinue = continueIndicators.some(indicator => lowerResponse.includes(indicator));
249
251
  // Also check if there were tool call parsing failures in this iteration
250
252
  // by looking for incomplete actions (e.g., write_file without content)
251
- const hasIncompleteWork = iteration < 10 && wantsToContinue && finalResponse.length < 500;
253
+ const hasIncompleteWork = wantsToContinue && finalResponse.length < 500
254
+ && incompleteWorkRetries < maxIncompleteWorkRetries;
252
255
  if (hasIncompleteWork) {
253
256
  debug('Model wants to continue, prompting for next action');
257
+ incompleteWorkRetries++;
254
258
  messages.push({ role: 'assistant', content });
255
259
  messages.push({
256
260
  role: 'user',
@@ -258,6 +262,8 @@ export async function runAgent(prompt, projectContext, options = {}) {
258
262
  });
259
263
  continue;
260
264
  }
265
+ // Reset counter once model produces real output or we give up
266
+ incompleteWorkRetries = 0;
261
267
  // Model is done
262
268
  debug(`Agent finished at iteration ${iteration}`);
263
269
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeep",
3
- "version": "1.2.35",
3
+ "version": "1.2.37",
4
4
  "description": "AI-powered coding assistant built for the terminal. Multiple LLM providers, project-aware context, and a seamless development workflow.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",