codeep 1.2.73 → 1.2.75

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 CHANGED
@@ -681,7 +681,7 @@ With write access enabled:
681
681
  | Agent Mode | ON | `ON` = agent runs automatically (requires write permission via `/grant`), `Manual` = use /agent |
682
682
  | Agent API Timeout | 180000ms | Timeout per agent API call (auto-adjusted for complexity) |
683
683
  | Agent Max Duration | 20 min | Maximum time for agent to run (5-60 min) |
684
- | Agent Max Iterations | 100 | Maximum agent iterations (10-200) |
684
+ | Agent Max Iterations | 200 | Maximum agent iterations (10-500) |
685
685
  | Agent Confirmation | Dangerous | `Never`, `Dangerous` (default), or `Always` |
686
686
  | Agent Auto-Commit | Off | Automatically commit after agent completes |
687
687
  | Agent Branch | Off | Create new branch for agent commits |
@@ -34,6 +34,7 @@ export interface InitializeParams {
34
34
  }
35
35
  export interface AgentCapabilities {
36
36
  loadSession?: boolean;
37
+ terminal?: boolean;
37
38
  promptCapabilities?: {
38
39
  image?: boolean;
39
40
  audio?: boolean;
@@ -174,7 +175,17 @@ export interface SessionUpdateSessionInfo {
174
175
  title: string;
175
176
  updatedAt?: string;
176
177
  }
177
- export type SessionUpdateInner = SessionUpdateAgentMessageChunk | SessionUpdateAgentThoughtChunk | SessionUpdateToolCall | SessionUpdateToolCallUpdate | SessionUpdateAvailableCommands | SessionUpdateCurrentMode | SessionUpdateConfigOption | SessionUpdateSessionInfo;
178
+ export interface PlanEntry {
179
+ id: string;
180
+ content: string;
181
+ priority: 'high' | 'medium' | 'low';
182
+ status: 'pending' | 'in_progress' | 'completed';
183
+ }
184
+ export interface SessionUpdatePlan {
185
+ sessionUpdate: 'plan';
186
+ entries: PlanEntry[];
187
+ }
188
+ export type SessionUpdateInner = SessionUpdateAgentMessageChunk | SessionUpdateAgentThoughtChunk | SessionUpdateToolCall | SessionUpdateToolCallUpdate | SessionUpdatePlan | SessionUpdateAvailableCommands | SessionUpdateCurrentMode | SessionUpdateConfigOption | SessionUpdateSessionInfo;
178
189
  export interface SessionUpdateParams {
179
190
  sessionId: string;
180
191
  update: SessionUpdateInner;
@@ -237,4 +248,46 @@ export interface FsWriteTextFileParams {
237
248
  path: string;
238
249
  content: string;
239
250
  }
251
+ export interface TerminalCreateParams {
252
+ sessionId: string;
253
+ command: string;
254
+ args: string[];
255
+ cwd: string;
256
+ env?: Record<string, string>;
257
+ outputByteLimit?: number;
258
+ }
259
+ export interface TerminalCreateResult {
260
+ terminalId: string;
261
+ }
262
+ export interface TerminalWaitForExitParams {
263
+ sessionId: string;
264
+ terminalId: string;
265
+ timeoutMs?: number;
266
+ }
267
+ export type TerminalExitStatus = {
268
+ type: 'exited';
269
+ code: number;
270
+ } | {
271
+ type: 'killed';
272
+ signal?: string;
273
+ };
274
+ export interface TerminalWaitForExitResult {
275
+ exitStatus: TerminalExitStatus;
276
+ }
277
+ export interface TerminalOutputParams {
278
+ sessionId: string;
279
+ terminalId: string;
280
+ offset?: number;
281
+ }
282
+ export interface TerminalOutputResult {
283
+ output: string;
284
+ /** Absent while the process is still running; present once the process has exited. */
285
+ exitStatus?: TerminalExitStatus;
286
+ }
287
+ export interface TerminalReleaseParams {
288
+ sessionId: string;
289
+ terminalId: string;
290
+ }
291
+ export interface TerminalReleaseResult {
292
+ }
240
293
  export type AcpMessage = JsonRpcRequest | JsonRpcResponse | JsonRpcNotification;
@@ -4,6 +4,7 @@ import { randomUUID } from 'crypto';
4
4
  import { basename as pathBasename } from 'path';
5
5
  import { StdioTransport } from './transport.js';
6
6
  import { runAgentSession } from './session.js';
7
+ import { executeCommandAsync } from '../utils/shell.js';
7
8
  import { initWorkspace, loadWorkspace, handleCommand } from './commands.js';
8
9
  import { autoSaveSession, config, setProvider, listSessionsWithInfo, deleteSession as deleteSessionFile } from '../config/index.js';
9
10
  import { PROVIDERS } from '../config/providers.js';
@@ -165,6 +166,7 @@ export function startAcpServer() {
165
166
  protocolVersion: 1,
166
167
  agentCapabilities: {
167
168
  loadSession: true,
169
+ terminal: true,
168
170
  sessionCapabilities: { list: {} },
169
171
  },
170
172
  agentInfo: {
@@ -381,6 +383,18 @@ export function startAcpServer() {
381
383
  .join('\n');
382
384
  const abortController = new AbortController();
383
385
  session.abortController = abortController;
386
+ // Plan tracking: build a live plan from tool calls as the agent works
387
+ // ACP spec: send complete list on every update, client replaces current plan
388
+ const planEntries = new Map();
389
+ const sendPlan = () => {
390
+ transport.notify('session/update', {
391
+ sessionId: params.sessionId,
392
+ update: {
393
+ sessionUpdate: 'plan',
394
+ entries: [...planEntries.values()],
395
+ },
396
+ });
397
+ };
384
398
  const agentResponseChunks = [];
385
399
  const sendChunk = (text) => {
386
400
  agentResponseChunks.push(text);
@@ -456,6 +470,16 @@ export function startAcpServer() {
456
470
  : {}),
457
471
  },
458
472
  });
473
+ // Add to plan as in_progress — only meaningful actions (not reads)
474
+ if (kind === 'edit' || kind === 'execute' || kind === 'delete') {
475
+ planEntries.set(toolCallId, {
476
+ id: toolCallId,
477
+ content: title || toolName,
478
+ priority: kind === 'execute' ? 'high' : 'medium',
479
+ status: 'in_progress',
480
+ });
481
+ sendPlan();
482
+ }
459
483
  }
460
484
  else {
461
485
  // tool_call_update: update status to completed/failed, with optional content
@@ -468,6 +492,70 @@ export function startAcpServer() {
468
492
  ...(rawOutput !== undefined ? { rawOutput } : {}),
469
493
  },
470
494
  });
495
+ // Mark plan entry as completed
496
+ const entry = planEntries.get(toolCallId);
497
+ if (entry) {
498
+ entry.status = 'completed';
499
+ sendPlan();
500
+ }
501
+ }
502
+ },
503
+ // Only request permission in Manual mode
504
+ onRequestPermission: session.currentModeId === 'manual'
505
+ ? async (toolCall) => {
506
+ const permToolCallId = `perm_${randomUUID()}`;
507
+ const result = await transport.request('session/request_permission', {
508
+ sessionId: params.sessionId,
509
+ toolCall: {
510
+ toolCallId: permToolCallId,
511
+ toolName: toolCall.tool,
512
+ toolInput: toolCall.parameters,
513
+ status: 'pending',
514
+ content: [],
515
+ },
516
+ options: [
517
+ { optionId: 'allow_once', name: 'Allow once', kind: 'allow_once' },
518
+ { optionId: 'allow_always', name: 'Allow always', kind: 'allow_always' },
519
+ { optionId: 'reject_once', name: 'Reject once', kind: 'reject_once' },
520
+ { optionId: 'reject_always', name: 'Reject always', kind: 'reject_always' },
521
+ ],
522
+ });
523
+ // Map ACP outcome back to PermissionOutcome
524
+ if (!result || result.outcome.type === 'cancelled')
525
+ return 'reject_once';
526
+ return result.outcome.optionId;
527
+ }
528
+ : undefined,
529
+ onExecuteCommand: async (command, args, cwd) => {
530
+ try {
531
+ const createResult = await transport.request('terminal/create', {
532
+ sessionId: params.sessionId,
533
+ command,
534
+ args,
535
+ cwd,
536
+ outputByteLimit: 1_000_000,
537
+ });
538
+ const { terminalId } = createResult;
539
+ const waitResult = await transport.request('terminal/waitForExit', {
540
+ sessionId: params.sessionId,
541
+ terminalId,
542
+ timeoutMs: 120_000,
543
+ });
544
+ const outputResult = await transport.request('terminal/output', {
545
+ sessionId: params.sessionId,
546
+ terminalId,
547
+ });
548
+ await transport.request('terminal/release', {
549
+ sessionId: params.sessionId,
550
+ terminalId,
551
+ });
552
+ const exitCode = waitResult.exitStatus.type === 'exited' ? waitResult.exitStatus.code : 1;
553
+ return { stdout: outputResult.output ?? '', stderr: '', exitCode };
554
+ }
555
+ catch (err) {
556
+ // Zed terminal unavailable — fall back to local execution
557
+ const r = await executeCommandAsync(command, args, { cwd, projectRoot: cwd, timeout: 120000 });
558
+ return { stdout: r.stdout ?? '', stderr: r.stderr ?? '', exitCode: r.exitCode ?? 0 };
471
559
  }
472
560
  },
473
561
  }).then(() => {
@@ -1,4 +1,6 @@
1
+ import { PermissionOutcome } from '../utils/agent.js';
1
2
  import { ProjectContext } from '../utils/project.js';
3
+ import { ToolCall } from '../utils/tools.js';
2
4
  export interface AgentSessionOptions {
3
5
  prompt: string;
4
6
  workspaceRoot: string;
@@ -7,6 +9,12 @@ export interface AgentSessionOptions {
7
9
  onChunk: (text: string) => void;
8
10
  onThought?: (text: string) => void;
9
11
  onToolCall?: (toolCallId: string, toolName: string, kind: string, title: string, status: 'pending' | 'running' | 'finished' | 'error', locations?: string[], rawOutput?: string) => void;
12
+ onRequestPermission?: (toolCall: ToolCall) => Promise<PermissionOutcome>;
13
+ onExecuteCommand?: (command: string, args: string[], cwd: string) => Promise<{
14
+ stdout: string;
15
+ stderr: string;
16
+ exitCode: number;
17
+ }>;
10
18
  }
11
19
  /**
12
20
  * Build a ProjectContext from a workspace root directory.
@@ -140,6 +140,8 @@ export async function runAgentSession(opts) {
140
140
  toolCallIdMap.delete(mapKey);
141
141
  }
142
142
  },
143
+ onRequestPermission: opts.onRequestPermission,
144
+ onExecuteCommand: opts.onExecuteCommand,
143
145
  });
144
146
  // result.finalResponse is already emitted via onChunk streaming above;
145
147
  // only emit it here if nothing was streamed (e.g. non-streaming fallback path)
@@ -3,11 +3,18 @@ type MessageHandler = (msg: JsonRpcRequest | JsonRpcNotification) => void;
3
3
  export declare class StdioTransport {
4
4
  private buffer;
5
5
  private handler;
6
+ private pendingRequests;
7
+ private requestIdCounter;
6
8
  start(handler: MessageHandler): void;
7
9
  private onData;
8
10
  send(msg: JsonRpcResponse | JsonRpcNotification): void;
9
11
  respond(id: number | string, result: unknown): void;
10
12
  error(id: number | string, code: number, message: string): void;
11
13
  notify(method: string, params: unknown): void;
14
+ /**
15
+ * Send a JSON-RPC request to the client and wait for the response.
16
+ * Used for agent-initiated requests like session/request_permission.
17
+ */
18
+ request(method: string, params: unknown): Promise<unknown>;
12
19
  }
13
20
  export {};
@@ -3,6 +3,8 @@
3
3
  export class StdioTransport {
4
4
  buffer = '';
5
5
  handler = null;
6
+ pendingRequests = new Map();
7
+ requestIdCounter = 1000;
6
8
  start(handler) {
7
9
  this.handler = handler;
8
10
  process.stdin.setEncoding('utf8');
@@ -19,6 +21,16 @@ export class StdioTransport {
19
21
  continue;
20
22
  try {
21
23
  const msg = JSON.parse(trimmed);
24
+ // Check if this is a response to one of our outbound requests
25
+ if ('result' in msg || 'error' in msg) {
26
+ const response = msg;
27
+ const resolve = this.pendingRequests.get(response.id);
28
+ if (resolve) {
29
+ this.pendingRequests.delete(response.id);
30
+ resolve(response.result ?? null);
31
+ continue;
32
+ }
33
+ }
22
34
  this.handler?.(msg);
23
35
  }
24
36
  catch {
@@ -38,4 +50,15 @@ export class StdioTransport {
38
50
  notify(method, params) {
39
51
  this.send({ jsonrpc: '2.0', method, params });
40
52
  }
53
+ /**
54
+ * Send a JSON-RPC request to the client and wait for the response.
55
+ * Used for agent-initiated requests like session/request_permission.
56
+ */
57
+ request(method, params) {
58
+ const id = ++this.requestIdCounter;
59
+ return new Promise((resolve) => {
60
+ this.pendingRequests.set(id, resolve);
61
+ process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n');
62
+ });
63
+ }
41
64
  }
@@ -148,7 +148,7 @@ function createConfig() {
148
148
  agentAutoCommitBranch: false,
149
149
  agentAutoVerify: 'off',
150
150
  agentMaxFixAttempts: 3,
151
- agentMaxIterations: 100,
151
+ agentMaxIterations: 200,
152
152
  agentMaxDuration: 20,
153
153
  agentApiTimeout: 180000,
154
154
  protocol: 'openai',
@@ -109,7 +109,7 @@ export const SETTINGS = [
109
109
  getValue: () => config.get('agentMaxIterations'),
110
110
  type: 'number',
111
111
  min: 10,
112
- max: 200,
112
+ max: 500,
113
113
  step: 10,
114
114
  },
115
115
  {
@@ -12,6 +12,7 @@ import { ToolCall, ToolResult, ActionLog } from './tools';
12
12
  import { undoLastAction, undoAllActions, getCurrentSession, getRecentSessions, formatSession, ActionSession } from './history';
13
13
  import { VerifyResult } from './verify';
14
14
  import { TaskPlan, SubTask } from './taskPlanner';
15
+ export type PermissionOutcome = 'allow_once' | 'allow_always' | 'reject_once' | 'reject_always';
15
16
  export interface AgentOptions {
16
17
  maxIterations: number;
17
18
  maxDuration: number;
@@ -23,6 +24,12 @@ export interface AgentOptions {
23
24
  onVerification?: (results: VerifyResult[]) => void;
24
25
  onTaskPlan?: (plan: TaskPlan) => void;
25
26
  onTaskUpdate?: (task: SubTask) => void;
27
+ onRequestPermission?: (toolCall: ToolCall) => Promise<PermissionOutcome>;
28
+ onExecuteCommand?: (command: string, args: string[], cwd: string) => Promise<{
29
+ stdout: string;
30
+ stderr: string;
31
+ exitCode: number;
32
+ }>;
26
33
  abortSignal?: AbortSignal;
27
34
  dryRun?: boolean;
28
35
  autoVerify?: 'off' | 'build' | 'typecheck' | 'test' | 'all' | boolean;
@@ -176,6 +176,10 @@ export async function runAgent(prompt, projectContext, options = {}) {
176
176
  let consecutiveTimeouts = 0;
177
177
  let incompleteWorkRetries = 0;
178
178
  const maxIncompleteWorkRetries = 2;
179
+ // Track tools permanently allowed this session via allow_always
180
+ const alwaysAllowedTools = new Set();
181
+ // Tools that require permission when onRequestPermission is set
182
+ const dangerousTools = new Set(['delete_file', 'execute_command']);
179
183
  const maxTimeoutRetries = 3;
180
184
  const maxConsecutiveTimeouts = 9; // Allow more consecutive timeouts before giving up
181
185
  const baseTimeout = config.get('agentApiTimeout');
@@ -376,9 +380,28 @@ export async function runAgent(prompt, projectContext, options = {}) {
376
380
  const toolResults = [];
377
381
  for (const toolCall of toolCalls) {
378
382
  opts.onToolCall?.(toolCall);
383
+ // Permission check for dangerous tools (only when callback is provided, e.g. ACP/Zed)
384
+ if (opts.onRequestPermission && dangerousTools.has(toolCall.tool) && !alwaysAllowedTools.has(toolCall.tool)) {
385
+ const outcome = await opts.onRequestPermission(toolCall);
386
+ if (outcome === 'allow_always') {
387
+ alwaysAllowedTools.add(toolCall.tool);
388
+ }
389
+ else if (outcome === 'reject_once' || outcome === 'reject_always') {
390
+ const toolResult = {
391
+ success: false,
392
+ output: '',
393
+ error: `User rejected permission for ${toolCall.tool}`,
394
+ tool: toolCall.tool,
395
+ parameters: toolCall.parameters,
396
+ };
397
+ opts.onToolResult?.(toolResult, toolCall);
398
+ actions.push(createActionLog(toolCall, toolResult));
399
+ toolResults.push(`Tool ${toolCall.tool} was rejected by user.`);
400
+ continue;
401
+ }
402
+ }
379
403
  let toolResult;
380
404
  if (opts.dryRun) {
381
- // In dry run mode, simulate success
382
405
  toolResult = {
383
406
  success: true,
384
407
  output: `[DRY RUN] Would execute: ${toolCall.tool}`,
@@ -386,8 +409,40 @@ export async function runAgent(prompt, projectContext, options = {}) {
386
409
  parameters: toolCall.parameters,
387
410
  };
388
411
  }
412
+ else if (opts.onExecuteCommand && toolCall.tool === 'execute_command') {
413
+ // Delegate to external terminal (e.g. Zed ACP terminal)
414
+ // Note: onExecuteCommand runs after the permission gate above
415
+ const command = toolCall.parameters.command;
416
+ const args = toolCall.parameters.args || [];
417
+ const cwd = projectContext.root || process.cwd();
418
+ if (!command) {
419
+ toolResult = {
420
+ success: false,
421
+ output: '',
422
+ error: 'execute_command called with missing command field',
423
+ tool: toolCall.tool,
424
+ parameters: toolCall.parameters,
425
+ };
426
+ }
427
+ else {
428
+ try {
429
+ const commandResult = await opts.onExecuteCommand(command, args, cwd);
430
+ toolResult = {
431
+ success: commandResult.exitCode === 0,
432
+ output: commandResult.stdout || '(no output)',
433
+ error: commandResult.exitCode !== 0 ? (commandResult.stderr || `exited with code ${commandResult.exitCode}`) : undefined,
434
+ tool: toolCall.tool,
435
+ parameters: toolCall.parameters,
436
+ };
437
+ }
438
+ catch (err) {
439
+ debug('onExecuteCommand callback threw, falling back to local execution:', err);
440
+ // Fallback to local execution if callback throws
441
+ toolResult = await executeTool(toolCall, cwd);
442
+ }
443
+ }
444
+ }
389
445
  else {
390
- // Actually execute the tool
391
446
  toolResult = await executeTool(toolCall, projectContext.root || process.cwd());
392
447
  }
393
448
  opts.onToolResult?.(toolResult, toolCall);
@@ -123,6 +123,7 @@ export function getAgentSystemPrompt(projectContext) {
123
123
  7. NEVER use execute_command for: ls, find, cat, grep, mkdir, rm, cp, mv, touch
124
124
  8. Use execute_command ONLY for: npm, git, composer, pip, cargo (build/package managers)
125
125
  9. When the task is complete, respond with a summary WITHOUT any tool calls
126
+ 10. CRITICAL: If the task is NOT complete, you MUST call a tool — never respond with only text mid-task. Do not "think out loud" or describe what you are about to do without calling a tool. Act immediately.
126
127
 
127
128
  ## Project Information
128
129
  Name: ${projectContext.name || 'Unknown'}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeep",
3
- "version": "1.2.73",
3
+ "version": "1.2.75",
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",