codeep 1.2.36 → 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,10 +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';
6
+ import { initWorkspace, loadWorkspace, handleCommand } from './commands.js';
7
+ import { autoSaveSession, config } from '../config/index.js';
8
8
  import { getCurrentVersion } from '../utils/update.js';
9
- // All advertised slash commands (shown in Zed autocomplete)
9
+ // ─── Slash commands advertised to Zed ────────────────────────────────────────
10
10
  const AVAILABLE_COMMANDS = [
11
11
  // Configuration
12
12
  { name: 'help', description: 'Show available commands' },
@@ -49,36 +49,77 @@ const AVAILABLE_COMMANDS = [
49
49
  { name: 'build', description: 'Build the project' },
50
50
  { name: 'deploy', description: 'Deploy the project' },
51
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 ───────────────────────────────────────────────────────────────────
52
74
  export function startAcpServer() {
53
75
  const transport = new StdioTransport();
54
76
  // ACP sessionId → full AcpSession (includes history + codeep session tracking)
55
77
  const sessions = new Map();
56
78
  transport.start((msg) => {
57
- 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) {
58
86
  case 'initialize':
59
- handleInitialize(msg);
60
- break;
61
- case 'initialized':
62
- // no-op acknowledgment
87
+ handleInitialize(req);
63
88
  break;
89
+ case 'initialized': /* no-op acknowledgment */ break;
64
90
  case 'session/new':
65
- handleSessionNew(msg);
91
+ handleSessionNew(req);
92
+ break;
93
+ case 'session/load':
94
+ handleSessionLoad(req);
66
95
  break;
67
96
  case 'session/prompt':
68
- handleSessionPrompt(msg);
97
+ handleSessionPrompt(req);
98
+ break;
99
+ case 'session/set_mode':
100
+ handleSetMode(req);
69
101
  break;
70
- case 'session/cancel':
71
- handleSessionCancel(msg);
102
+ case 'session/set_config_option':
103
+ handleSetConfigOption(req);
72
104
  break;
73
105
  default:
74
- transport.error(msg.id, -32601, `Method not found: ${msg.method}`);
106
+ transport.error(req.id, -32601, `Method not found: ${req.method}`);
75
107
  }
76
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 ──────────────────────────────────────────────────────────────
77
117
  function handleInitialize(msg) {
78
118
  const _params = msg.params;
79
119
  const result = {
80
120
  protocolVersion: 1,
81
121
  agentCapabilities: {
122
+ loadSession: true,
82
123
  streaming: true,
83
124
  fileEditing: true,
84
125
  },
@@ -90,10 +131,10 @@ export function startAcpServer() {
90
131
  };
91
132
  transport.respond(msg.id, result);
92
133
  }
134
+ // ── session/new ─────────────────────────────────────────────────────────────
93
135
  function handleSessionNew(msg) {
94
136
  const params = msg.params;
95
137
  const acpSessionId = randomUUID();
96
- // Initialise workspace: create .codeep folder, load/create codeep session
97
138
  const { codeepSessionId, history, welcomeText } = initWorkspace(params.cwd);
98
139
  sessions.set(acpSessionId, {
99
140
  sessionId: acpSessionId,
@@ -102,25 +143,103 @@ export function startAcpServer() {
102
143
  codeepSessionId,
103
144
  addedFiles: new Map(),
104
145
  abortController: null,
146
+ currentModeId: 'auto',
105
147
  });
106
- transport.respond(msg.id, { sessionId: acpSessionId });
107
- // 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
108
155
  transport.notify('session/update', {
156
+ type: 'available_commands_update',
109
157
  sessionId: acpSessionId,
110
- update: {
111
- sessionUpdate: 'available_commands_update',
112
- availableCommands: AVAILABLE_COMMANDS,
113
- },
158
+ availableCommands: AVAILABLE_COMMANDS,
114
159
  });
115
- // Stream welcome message
160
+ // Send welcome message
116
161
  transport.notify('session/update', {
162
+ type: 'content_chunk',
117
163
  sessionId: acpSessionId,
118
- update: {
119
- sessionUpdate: 'agent_message_chunk',
120
- content: { type: 'text', text: welcomeText },
121
- },
164
+ content: { type: 'text', text: welcomeText },
122
165
  });
123
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',
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, {});
241
+ }
242
+ // ── session/prompt ──────────────────────────────────────────────────────────
124
243
  function handleSessionPrompt(msg) {
125
244
  const params = msg.params;
126
245
  const session = sessions.get(params.sessionId);
@@ -131,7 +250,7 @@ export function startAcpServer() {
131
250
  // Extract text from ContentBlock[]
132
251
  const prompt = params.prompt
133
252
  .filter((b) => b.type === 'text')
134
- .map((b) => b.text)
253
+ .map((b) => b.text ?? '')
135
254
  .join('\n');
136
255
  const abortController = new AbortController();
137
256
  session.abortController = abortController;
@@ -139,26 +258,21 @@ export function startAcpServer() {
139
258
  const sendChunk = (text) => {
140
259
  agentResponseChunks.push(text);
141
260
  transport.notify('session/update', {
261
+ type: 'content_chunk',
142
262
  sessionId: params.sessionId,
143
- update: {
144
- sessionUpdate: 'agent_message_chunk',
145
- content: { type: 'text', text },
146
- },
263
+ content: { type: 'text', text },
147
264
  });
148
265
  };
149
- // Try slash commands first (async — skills, diff, scan, etc.)
266
+ // Try slash commands first
150
267
  handleCommand(prompt, session, sendChunk, abortController.signal)
151
268
  .then((cmd) => {
152
269
  if (cmd.handled) {
153
- // For streaming commands (skills, diff), chunks were already sent via onChunk.
154
- // For simple commands, send the response now.
155
270
  if (cmd.response)
156
271
  sendChunk(cmd.response);
157
272
  transport.respond(msg.id, { stopReason: 'end_turn' });
158
273
  return;
159
274
  }
160
275
  // Not a command — run agent loop
161
- // Prepend any added-files context to the prompt
162
276
  let enrichedPrompt = prompt;
163
277
  if (session.addedFiles.size > 0) {
164
278
  const parts = ['[Attached files]'];
@@ -175,28 +289,23 @@ export function startAcpServer() {
175
289
  onChunk: sendChunk,
176
290
  onThought: (text) => {
177
291
  transport.notify('session/update', {
292
+ type: 'agent_thought_chunk',
178
293
  sessionId: params.sessionId,
179
- update: {
180
- sessionUpdate: 'agent_thought_chunk',
181
- content: { type: 'text', text },
182
- },
294
+ content: { type: 'text', text },
183
295
  });
184
296
  },
185
- onToolCall: (toolCallId, _toolName, kind, title, status, locations) => {
297
+ onToolCall: (toolCallId, toolName, _kind, _title, status, _locations) => {
186
298
  transport.notify('session/update', {
299
+ type: 'tool_call',
187
300
  sessionId: params.sessionId,
188
- update: {
189
- sessionUpdate: 'tool_call',
190
- toolCallId,
191
- title,
192
- kind,
193
- status,
194
- ...(locations?.length ? { locations: locations.map(uri => ({ uri })) } : {}),
195
- },
301
+ toolCallId,
302
+ toolName,
303
+ toolInput: {},
304
+ state: status === 'running' ? 'running' : status === 'finished' ? 'finished' : 'error',
305
+ content: [],
196
306
  });
197
307
  },
198
308
  onFileEdit: (uri, newText) => {
199
- // ACP structured file/edit notification — lets the editor apply changes
200
309
  transport.notify('file/edit', {
201
310
  uri,
202
311
  textChanges: newText
@@ -230,11 +339,6 @@ export function startAcpServer() {
230
339
  session.abortController = null;
231
340
  });
232
341
  }
233
- function handleSessionCancel(msg) {
234
- const { sessionId } = msg.params;
235
- sessions.get(sessionId)?.abortController?.abort();
236
- transport.respond(msg.id, {});
237
- }
238
342
  // Keep process alive until stdin closes (Zed terminates us)
239
343
  return new Promise((resolve) => {
240
344
  process.stdin.on('end', resolve);
@@ -87,13 +87,23 @@ export async function runAgentSession(opts) {
87
87
  }
88
88
  },
89
89
  onToolResult: (toolResult, toolCall) => {
90
- // Find the tracked entry by tool call id or by matching tool name in insertion order
91
- const mapKey = toolCall.id
92
- ? [...toolCallIdMap.keys()].find(k => k === toolCall.id)
93
- : [...toolCallIdMap.keys()].find(k => k.startsWith(`${toolCall.tool}_`));
94
- if (mapKey) {
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) {
95
105
  const tracked = toolCallIdMap.get(mapKey);
96
- if (opts.onToolCall) {
106
+ if (tracked && opts.onToolCall) {
97
107
  const status = toolResult.success ? 'finished' : 'error';
98
108
  opts.onToolCall(tracked.toolCallId, toolCall.tool, tracked.kind, '', status, tracked.locations);
99
109
  }
@@ -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;
@@ -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,
@@ -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 {
@@ -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');
@@ -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.36",
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",