codeep 1.2.74 → 1.2.76

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
@@ -25,7 +25,8 @@
25
25
  - **Z.AI (ZhipuAI)** — GLM-5, GLM-4.7, GLM-4.7 Flash (international & China endpoints)
26
26
  - **MiniMax** — MiniMax M2.5 (international & China endpoints)
27
27
  - **DeepSeek** — DeepSeek V3, DeepSeek R1 (reasoning)
28
- - **Anthropic** — Claude Sonnet 4.6, Claude Opus 4.6, Claude Haiku 4.5
28
+ - **Anthropic** — Claude Sonnet 4.6, Claude Opus 4.6, Claude Sonnet 4.5, Claude Haiku 4.5
29
+ - **Google AI** — Gemini 3.1 Pro Preview, Gemini 3 Pro Preview, Gemini 3 Flash Preview, Gemini 2.5 Pro, Gemini 2.5 Flash, Gemini 2.5 Flash Lite
29
30
  - Switch between providers with `/provider`
30
31
  - Configure different API keys per provider
31
32
  - Both OpenAI-compatible and Anthropic API protocols supported
@@ -668,6 +669,7 @@ With write access enabled:
668
669
  | `MINIMAX_CN_API_KEY` | MiniMax China API key |
669
670
  | `DEEPSEEK_API_KEY` | DeepSeek API key |
670
671
  | `ANTHROPIC_API_KEY` | Anthropic Claude API key |
672
+ | `GOOGLE_API_KEY` | Google AI (Gemini) API key |
671
673
 
672
674
  ### Settings (`/settings`)
673
675
 
@@ -681,7 +683,7 @@ With write access enabled:
681
683
  | Agent Mode | ON | `ON` = agent runs automatically (requires write permission via `/grant`), `Manual` = use /agent |
682
684
  | Agent API Timeout | 180000ms | Timeout per agent API call (auto-adjusted for complexity) |
683
685
  | Agent Max Duration | 20 min | Maximum time for agent to run (5-60 min) |
684
- | Agent Max Iterations | 100 | Maximum agent iterations (10-200) |
686
+ | Agent Max Iterations | 200 | Maximum agent iterations (10-500) |
685
687
  | Agent Confirmation | Dangerous | `Never`, `Dangerous` (default), or `Always` |
686
688
  | Agent Auto-Commit | Off | Automatically commit after agent completes |
687
689
  | 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,8 +4,10 @@ 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';
10
+ import { ApiError } from '../api/index.js';
9
11
  import { PROVIDERS } from '../config/providers.js';
10
12
  import { getCurrentVersion } from '../utils/update.js';
11
13
  // ─── Slash commands advertised to Zed ────────────────────────────────────────
@@ -148,6 +150,7 @@ export function startAcpServer() {
148
150
  handleSessionDelete(req);
149
151
  break;
150
152
  default:
153
+ process.stderr.write(`[codeep-acp] Unknown method: ${req.method}\n`);
151
154
  transport.error(req.id, -32601, `Method not found: ${req.method}`);
152
155
  }
153
156
  });
@@ -165,6 +168,7 @@ export function startAcpServer() {
165
168
  protocolVersion: 1,
166
169
  agentCapabilities: {
167
170
  loadSession: true,
171
+ terminal: true,
168
172
  sessionCapabilities: { list: {} },
169
173
  },
170
174
  agentInfo: {
@@ -381,6 +385,18 @@ export function startAcpServer() {
381
385
  .join('\n');
382
386
  const abortController = new AbortController();
383
387
  session.abortController = abortController;
388
+ // Plan tracking: build a live plan from tool calls as the agent works
389
+ // ACP spec: send complete list on every update, client replaces current plan
390
+ const planEntries = new Map();
391
+ const sendPlan = () => {
392
+ transport.notify('session/update', {
393
+ sessionId: params.sessionId,
394
+ update: {
395
+ sessionUpdate: 'plan',
396
+ entries: [...planEntries.values()],
397
+ },
398
+ });
399
+ };
384
400
  const agentResponseChunks = [];
385
401
  const sendChunk = (text) => {
386
402
  agentResponseChunks.push(text);
@@ -456,6 +472,16 @@ export function startAcpServer() {
456
472
  : {}),
457
473
  },
458
474
  });
475
+ // Add to plan as in_progress — only meaningful actions (not reads)
476
+ if (kind === 'edit' || kind === 'execute' || kind === 'delete') {
477
+ planEntries.set(toolCallId, {
478
+ id: toolCallId,
479
+ content: title || toolName,
480
+ priority: kind === 'execute' ? 'high' : 'medium',
481
+ status: 'in_progress',
482
+ });
483
+ sendPlan();
484
+ }
459
485
  }
460
486
  else {
461
487
  // tool_call_update: update status to completed/failed, with optional content
@@ -468,6 +494,70 @@ export function startAcpServer() {
468
494
  ...(rawOutput !== undefined ? { rawOutput } : {}),
469
495
  },
470
496
  });
497
+ // Mark plan entry as completed
498
+ const entry = planEntries.get(toolCallId);
499
+ if (entry) {
500
+ entry.status = 'completed';
501
+ sendPlan();
502
+ }
503
+ }
504
+ },
505
+ // Only request permission in Manual mode
506
+ onRequestPermission: session.currentModeId === 'manual'
507
+ ? async (toolCall) => {
508
+ const permToolCallId = `perm_${randomUUID()}`;
509
+ const result = await transport.request('session/request_permission', {
510
+ sessionId: params.sessionId,
511
+ toolCall: {
512
+ toolCallId: permToolCallId,
513
+ toolName: toolCall.tool,
514
+ toolInput: toolCall.parameters,
515
+ status: 'pending',
516
+ content: [],
517
+ },
518
+ options: [
519
+ { optionId: 'allow_once', name: 'Allow once', kind: 'allow_once' },
520
+ { optionId: 'allow_always', name: 'Allow always', kind: 'allow_always' },
521
+ { optionId: 'reject_once', name: 'Reject once', kind: 'reject_once' },
522
+ { optionId: 'reject_always', name: 'Reject always', kind: 'reject_always' },
523
+ ],
524
+ });
525
+ // Map ACP outcome back to PermissionOutcome
526
+ if (!result || result.outcome.type === 'cancelled')
527
+ return 'reject_once';
528
+ return result.outcome.optionId;
529
+ }
530
+ : undefined,
531
+ onExecuteCommand: async (command, args, cwd) => {
532
+ try {
533
+ const createResult = await transport.request('terminal/create', {
534
+ sessionId: params.sessionId,
535
+ command,
536
+ args,
537
+ cwd,
538
+ outputByteLimit: 1_000_000,
539
+ });
540
+ const { terminalId } = createResult;
541
+ const waitResult = await transport.request('terminal/waitForExit', {
542
+ sessionId: params.sessionId,
543
+ terminalId,
544
+ timeoutMs: 120_000,
545
+ });
546
+ const outputResult = await transport.request('terminal/output', {
547
+ sessionId: params.sessionId,
548
+ terminalId,
549
+ });
550
+ await transport.request('terminal/release', {
551
+ sessionId: params.sessionId,
552
+ terminalId,
553
+ });
554
+ const exitCode = waitResult.exitStatus.type === 'exited' ? waitResult.exitStatus.code : 1;
555
+ return { stdout: outputResult.output ?? '', stderr: '', exitCode };
556
+ }
557
+ catch (err) {
558
+ // Zed terminal unavailable — fall back to local execution
559
+ const r = await executeCommandAsync(command, args, { cwd, projectRoot: cwd, timeout: 120000 });
560
+ return { stdout: r.stdout ?? '', stderr: r.stderr ?? '', exitCode: r.exitCode ?? 0 };
471
561
  }
472
562
  },
473
563
  }).then(() => {
@@ -487,6 +577,14 @@ export function startAcpServer() {
487
577
  if (err.name === 'AbortError') {
488
578
  transport.respond(msg.id, { stopReason: 'cancelled' });
489
579
  }
580
+ else if (err.message?.includes('API key not configured') || err.message?.includes('API key') || (err instanceof ApiError && err.status === 401)) {
581
+ sendChunk(`❌ No API key configured. Use /login <provider> <key> or set the environment variable (e.g. ZAI_API_KEY, ANTHROPIC_API_KEY).`);
582
+ transport.respond(msg.id, { stopReason: 'end_turn' });
583
+ }
584
+ else if (err instanceof ApiError && err.status >= 500) {
585
+ sendChunk(`⚠️ API server error (${err.status}). Please try again.`);
586
+ transport.respond(msg.id, { stopReason: 'end_turn' });
587
+ }
490
588
  else {
491
589
  transport.error(msg.id, -32000, err.message);
492
590
  }
@@ -496,7 +594,17 @@ export function startAcpServer() {
496
594
  });
497
595
  })
498
596
  .catch((err) => {
499
- transport.error(msg.id, -32000, err.message);
597
+ if (err.message?.includes('API key not configured') || err.message?.includes('API key') || (err instanceof ApiError && err.status === 401)) {
598
+ sendChunk(`❌ No API key configured. Use /login <provider> <key> or set the environment variable (e.g. ZAI_API_KEY, ANTHROPIC_API_KEY).`);
599
+ transport.respond(msg.id, { stopReason: 'end_turn' });
600
+ }
601
+ else if (err instanceof ApiError && err.status >= 500) {
602
+ sendChunk(`⚠️ API server error (${err.status}). Please try again.`);
603
+ transport.respond(msg.id, { stopReason: 'end_turn' });
604
+ }
605
+ else {
606
+ transport.error(msg.id, -32000, err.message);
607
+ }
500
608
  if (session)
501
609
  session.abortController = null;
502
610
  });
@@ -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;
@@ -11,6 +11,7 @@ const debug = (...args) => {
11
11
  };
12
12
  // Import chat layer (prompt building + API calls)
13
13
  import { agentChat, getAgentSystemPrompt, getFallbackSystemPrompt, loadProjectRules, formatChatHistoryForAgent, } from './agentChat.js';
14
+ import { ApiError } from '../api/index.js';
14
15
  export { loadProjectRules, formatChatHistoryForAgent };
15
16
  /**
16
17
  * Calculate dynamic timeout based on task complexity
@@ -176,6 +177,10 @@ export async function runAgent(prompt, projectContext, options = {}) {
176
177
  let consecutiveTimeouts = 0;
177
178
  let incompleteWorkRetries = 0;
178
179
  const maxIncompleteWorkRetries = 2;
180
+ // Track tools permanently allowed this session via allow_always
181
+ const alwaysAllowedTools = new Set();
182
+ // Tools that require permission when onRequestPermission is set
183
+ const dangerousTools = new Set(['delete_file', 'execute_command']);
179
184
  const maxTimeoutRetries = 3;
180
185
  const maxConsecutiveTimeouts = 9; // Allow more consecutive timeouts before giving up
181
186
  const baseTimeout = config.get('agentApiTimeout');
@@ -283,6 +288,17 @@ export async function runAgent(prompt, projectContext, options = {}) {
283
288
  await new Promise(resolve => setTimeout(resolve, 1000 * retryCount));
284
289
  continue;
285
290
  }
291
+ // Don't retry on 4xx client errors except 429 (rate limit)
292
+ if (err instanceof ApiError && err.status >= 400 && err.status < 500 && err.status !== 429) {
293
+ result = {
294
+ success: false,
295
+ iterations: iteration,
296
+ actions,
297
+ finalResponse: '',
298
+ error: err.message,
299
+ };
300
+ return result;
301
+ }
286
302
  // All non-abort errors are retryable — retry with backoff
287
303
  retryCount++;
288
304
  const isRateLimit = err.message.includes('429');
@@ -376,9 +392,28 @@ export async function runAgent(prompt, projectContext, options = {}) {
376
392
  const toolResults = [];
377
393
  for (const toolCall of toolCalls) {
378
394
  opts.onToolCall?.(toolCall);
395
+ // Permission check for dangerous tools (only when callback is provided, e.g. ACP/Zed)
396
+ if (opts.onRequestPermission && dangerousTools.has(toolCall.tool) && !alwaysAllowedTools.has(toolCall.tool)) {
397
+ const outcome = await opts.onRequestPermission(toolCall);
398
+ if (outcome === 'allow_always') {
399
+ alwaysAllowedTools.add(toolCall.tool);
400
+ }
401
+ else if (outcome === 'reject_once' || outcome === 'reject_always') {
402
+ const toolResult = {
403
+ success: false,
404
+ output: '',
405
+ error: `User rejected permission for ${toolCall.tool}`,
406
+ tool: toolCall.tool,
407
+ parameters: toolCall.parameters,
408
+ };
409
+ opts.onToolResult?.(toolResult, toolCall);
410
+ actions.push(createActionLog(toolCall, toolResult));
411
+ toolResults.push(`Tool ${toolCall.tool} was rejected by user.`);
412
+ continue;
413
+ }
414
+ }
379
415
  let toolResult;
380
416
  if (opts.dryRun) {
381
- // In dry run mode, simulate success
382
417
  toolResult = {
383
418
  success: true,
384
419
  output: `[DRY RUN] Would execute: ${toolCall.tool}`,
@@ -386,8 +421,40 @@ export async function runAgent(prompt, projectContext, options = {}) {
386
421
  parameters: toolCall.parameters,
387
422
  };
388
423
  }
424
+ else if (opts.onExecuteCommand && toolCall.tool === 'execute_command') {
425
+ // Delegate to external terminal (e.g. Zed ACP terminal)
426
+ // Note: onExecuteCommand runs after the permission gate above
427
+ const command = toolCall.parameters.command;
428
+ const args = toolCall.parameters.args || [];
429
+ const cwd = projectContext.root || process.cwd();
430
+ if (!command) {
431
+ toolResult = {
432
+ success: false,
433
+ output: '',
434
+ error: 'execute_command called with missing command field',
435
+ tool: toolCall.tool,
436
+ parameters: toolCall.parameters,
437
+ };
438
+ }
439
+ else {
440
+ try {
441
+ const commandResult = await opts.onExecuteCommand(command, args, cwd);
442
+ toolResult = {
443
+ success: commandResult.exitCode === 0,
444
+ output: commandResult.stdout || '(no output)',
445
+ error: commandResult.exitCode !== 0 ? (commandResult.stderr || `exited with code ${commandResult.exitCode}`) : undefined,
446
+ tool: toolCall.tool,
447
+ parameters: toolCall.parameters,
448
+ };
449
+ }
450
+ catch (err) {
451
+ debug('onExecuteCommand callback threw, falling back to local execution:', err);
452
+ // Fallback to local execution if callback throws
453
+ toolResult = await executeTool(toolCall, cwd);
454
+ }
455
+ }
456
+ }
389
457
  else {
390
- // Actually execute the tool
391
458
  toolResult = await executeTool(toolCall, projectContext.root || process.cwd());
392
459
  }
393
460
  opts.onToolResult?.(toolResult, toolCall);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeep",
3
- "version": "1.2.74",
3
+ "version": "1.2.76",
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",