opencode-ai-cli 1.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/GEMINI.md +11 -0
  2. package/cli.ts +17 -0
  3. package/commands/agent.ts +102 -0
  4. package/commands/chat.ts +518 -0
  5. package/commands/models.ts +10 -0
  6. package/commands/providers/index.ts +10 -0
  7. package/commands/providers/login.ts +30 -0
  8. package/commands/providers/logout.ts +13 -0
  9. package/commands/providers/setProvider.ts +9 -0
  10. package/package.json +22 -0
  11. package/src/core/auth-storage.ts +136 -0
  12. package/src/core/model-registry.ts +23 -0
  13. package/src/engine/agentLoop.ts +389 -0
  14. package/src/engine/messages.ts +110 -0
  15. package/src/engine/systemPrompt.ts +58 -0
  16. package/src/engine/type.ts +133 -0
  17. package/src/providers/gemini.ts +122 -0
  18. package/src/providers/openai.ts +60 -0
  19. package/src/subagent/README.md +177 -0
  20. package/src/subagent/agents/planner.md +37 -0
  21. package/src/subagent/agents/reviewer.md +35 -0
  22. package/src/subagent/agents/scout.md +49 -0
  23. package/src/subagent/agents/worker.md +29 -0
  24. package/src/subagent/agents.ts +89 -0
  25. package/src/subagent/index.ts +224 -0
  26. package/src/subagent/prompts/implement-and-review.md +10 -0
  27. package/src/subagent/prompts/implement.md +10 -0
  28. package/src/subagent/prompts/scout-and-plan.md +9 -0
  29. package/src/tools/bash-tool.ts +44 -0
  30. package/src/tools/edit-tool.ts +85 -0
  31. package/src/tools/find-tool.ts +81 -0
  32. package/src/tools/grep-tool.ts +100 -0
  33. package/src/tools/index.ts +37 -0
  34. package/src/tools/ls-tool.ts +93 -0
  35. package/src/tools/plan-tool.ts +35 -0
  36. package/src/tools/read-tool.ts +89 -0
  37. package/src/tools/truncate.ts +21 -0
  38. package/src/tools/weather-tool.ts +55 -0
  39. package/src/tools/write-tool.ts +53 -0
  40. package/src/types.ts +28 -0
@@ -0,0 +1,30 @@
1
+ import { Command } from "commander";
2
+ import { AuthStorage, ProviderName } from "../../src/core/auth-storage";
3
+
4
+ export const loginCommand = new Command("login")
5
+ .description("Lets user login into the provider (use it as default)")
6
+ .option(
7
+ "-p, --provider <providerName>",
8
+ "Name of the provider (gemini, claude etc)",
9
+ "",
10
+ )
11
+ .option("-a, --api_key <apiKey>", "Your api key", "")
12
+ .action(async (options) => {
13
+ console.log("logging into " + options.provider);
14
+ const provider = options.provider.toLowerCase();
15
+ const apiKey = options.api_key;
16
+
17
+ if (!provider || !apiKey) {
18
+ console.error("provider and api key is required!");
19
+ process.exit(1);
20
+ }
21
+
22
+ if (provider !== "openai" && provider !== "gemini") {
23
+ console.error("unsupported ai ");
24
+ process.exit(1);
25
+ }
26
+
27
+ AuthStorage.setApiKey(provider as ProviderName, apiKey);
28
+
29
+ console.log(`Successful login in ${provider} keys are stored`);
30
+ });
@@ -0,0 +1,13 @@
1
+ import { Command } from "commander";
2
+
3
+ export const logoutCommand = new Command("logout")
4
+ .description("Lets user logout from the provider")
5
+ .option(
6
+ "-p, --provider <providerName>",
7
+ "Name of the provider (gemini, claude etc)",
8
+ "",
9
+ )
10
+ .action(async(options) => {
11
+ console.log("logging out for provider " + options.provider);
12
+
13
+ });
@@ -0,0 +1,9 @@
1
+
2
+ import { Command } from 'commander';
3
+
4
+ export const setProviderCommand = new Command("set")
5
+ .description('Lets user set the default provider')
6
+ .option('-p, --provider <providerName>', 'Name of the provider (gemini, claude etc)', '')
7
+ .action((options) => {
8
+ console.log("provider is " + JSON.stringify(options))
9
+ })
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "opencode-ai-cli",
3
+ "version": "1.17.0",
4
+ "description": "An interactive coding agent CLI",
5
+ "type": "module",
6
+ "main": "cli.ts",
7
+ "bin": {
8
+ "opencode": "cli.ts",
9
+ "tuichat": "cli.ts"
10
+ },
11
+ "scripts": {
12
+ "tui": "bun run cli.ts chat"
13
+ },
14
+ "dependencies": {
15
+ "@inquirer/prompts": "^8.5.2",
16
+ "@types/commander": "^2.12.5",
17
+ "@types/node": "^25.9.1",
18
+ "@vscode/ripgrep": "^1.18.0",
19
+ "chalk": "^5.6.2",
20
+ "glob": "^13.0.6"
21
+ }
22
+ }
@@ -0,0 +1,136 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+
5
+ export type ProviderName = "openai" | "gemini" | "anthropic";
6
+ type AuthEntry = string | { type?: string; key?: unknown };
7
+
8
+ interface TrialInfo {
9
+ date: string;
10
+ tokensUsed: number;
11
+ isTrialActive: boolean;
12
+ }
13
+
14
+ export class AuthStorage {
15
+ private static readonly CONFIG_DIR = path.join(os.homedir(), ".opencode");
16
+ private static readonly AUTH_FILE = path.join(AuthStorage.CONFIG_DIR, "auth.json");
17
+ public static readonly TRIAL_GEMINI_KEY = "AQ.Ab8RN6KtPGLnaiIdErmbAC8LRGNjdmkLLE8eGaikmS44AN14Pg";
18
+ public static readonly TRIAL_TOKEN_LIMIT = 5000;
19
+
20
+ private static ensureDirectoryExists() {
21
+ if (!fs.existsSync(this.CONFIG_DIR)) {
22
+ fs.mkdirSync(this.CONFIG_DIR, { recursive: true });
23
+ }
24
+ }
25
+
26
+ private static readData(): Record<string, AuthEntry> {
27
+ if (!fs.existsSync(this.AUTH_FILE)) {
28
+ return {};
29
+ }
30
+
31
+ try {
32
+ return JSON.parse(fs.readFileSync(this.AUTH_FILE, "utf-8"));
33
+ } catch (error) {
34
+ return {};
35
+ }
36
+ }
37
+
38
+ public static getApiKey(provider: ProviderName): string {
39
+ const data = this.readData();
40
+ const entry = data[provider];
41
+
42
+ if (typeof entry === "string") {
43
+ return entry;
44
+ }
45
+
46
+ if (entry && typeof entry === "object" && typeof entry.key === "string") {
47
+ return entry.key;
48
+ }
49
+
50
+ if (provider === "gemini" && this.isTrialActive()) {
51
+ return this.TRIAL_GEMINI_KEY;
52
+ }
53
+
54
+ return "";
55
+ }
56
+
57
+ public static setApiKey(provider: ProviderName, key: string): void {
58
+ this.ensureDirectoryExists();
59
+ const data = this.readData();
60
+ data[provider] = { type: "api", key: key };
61
+ fs.writeFileSync(this.AUTH_FILE, JSON.stringify(data, null, 2));
62
+ }
63
+
64
+ public static deleteApiKey(provider: ProviderName): void {
65
+ this.ensureDirectoryExists();
66
+ const data = this.readData();
67
+ delete data[provider];
68
+ fs.writeFileSync(this.AUTH_FILE, JSON.stringify(data, null, 2));
69
+ }
70
+
71
+ public static getAuthenticatedProviders(): ProviderName[] {
72
+ return (["openai", "gemini"] as ProviderName[]).filter((provider) =>
73
+ this.isAuthenticated(provider),
74
+ );
75
+ }
76
+
77
+ public static isAuthenticated(provider: ProviderName): boolean {
78
+ return !!this.getApiKey(provider);
79
+ }
80
+
81
+ public static hasStoredApiKey(provider: ProviderName): boolean {
82
+ const data = this.readData();
83
+ const entry = data[provider];
84
+ if (typeof entry === "string") return !!entry;
85
+ if (entry && typeof entry === "object" && typeof entry.key === "string") return !!entry.key;
86
+ return false;
87
+ }
88
+
89
+ public static isTrialActive(): boolean {
90
+ const data = this.readData() as any;
91
+ return !!data.trialInfo?.isTrialActive;
92
+ }
93
+
94
+ public static setTrialActive(active: boolean): void {
95
+ this.ensureDirectoryExists();
96
+ const data = this.readData() as any;
97
+ if (!data.trialInfo) {
98
+ data.trialInfo = { date: new Date().toISOString().split("T")[0], tokensUsed: 0, isTrialActive: active };
99
+ } else {
100
+ data.trialInfo.isTrialActive = active;
101
+ }
102
+ fs.writeFileSync(this.AUTH_FILE, JSON.stringify(data, null, 2));
103
+ }
104
+
105
+ public static getTrialRemainingTokens(): number {
106
+ const data = this.readData() as any;
107
+ const today = new Date().toISOString().split("T")[0];
108
+ const info = data.trialInfo as TrialInfo;
109
+
110
+ if (!info) return this.TRIAL_TOKEN_LIMIT;
111
+
112
+ if (info.date !== today) {
113
+ return this.TRIAL_TOKEN_LIMIT;
114
+ }
115
+
116
+ return Math.max(0, this.TRIAL_TOKEN_LIMIT - info.tokensUsed);
117
+ }
118
+
119
+ public static updateTrialUsage(tokens: number): void {
120
+ this.ensureDirectoryExists();
121
+ const data = this.readData() as any;
122
+ const today = new Date().toISOString().split("T")[0];
123
+
124
+ if (!data.trialInfo) {
125
+ data.trialInfo = { date: today, tokensUsed: tokens, isTrialActive: true };
126
+ } else {
127
+ if (data.trialInfo.date !== today) {
128
+ data.trialInfo.date = today;
129
+ data.trialInfo.tokensUsed = tokens;
130
+ } else {
131
+ data.trialInfo.tokensUsed += tokens;
132
+ }
133
+ }
134
+ fs.writeFileSync(this.AUTH_FILE, JSON.stringify(data, null, 2));
135
+ }
136
+ }
@@ -0,0 +1,23 @@
1
+ import { AgentTool } from "../types";
2
+ import { ProviderName } from "./auth-storage";
3
+
4
+ export interface ChatRequest {
5
+ provider: ProviderName;
6
+ apiKey: string;
7
+ model?: string;
8
+ prompt: string;
9
+ history: any[];
10
+ tools: AgentTool[];
11
+ signal?: AbortSignal;
12
+ }
13
+
14
+ export interface ChatResponse {
15
+ text: string | null;
16
+ toolCalls: Array<{ id: string; name: string; arguments: any }> | null;
17
+ rawMessage: any;
18
+ usage?: {
19
+ promptTokens: number;
20
+ completionTokens: number;
21
+ totalTokens: number;
22
+ };
23
+ }
@@ -0,0 +1,389 @@
1
+ import { executeOpenAI } from "../providers/openai";
2
+ import { executeGemini } from "../providers/gemini";
3
+ import { convertToLlm } from "./messages";
4
+ import {
5
+ AgentContext,
6
+ AgentEventSink,
7
+ AgentLoopConfig,
8
+ AgentMessage,
9
+ AgentToolCall,
10
+ AssistantMessage,
11
+ ExecutedToolCallBatch,
12
+ ToolResultMessage,
13
+ } from "./type";
14
+
15
+ export async function runAgentLoop(
16
+ prompts: AgentMessage[],
17
+ context: AgentContext,
18
+ config: AgentLoopConfig,
19
+ emit: AgentEventSink,
20
+ signal?: AbortSignal,
21
+ ): Promise<AgentMessage[]> {
22
+ const newMessages: AgentMessage[] = [...prompts];
23
+
24
+ const currentContext: AgentContext = {
25
+ ...context,
26
+ messages: [...context.messages, ...prompts],
27
+ };
28
+
29
+ await emit({ type: "agent_start" });
30
+
31
+ for (const prompt of prompts) {
32
+ await emit({ type: "message_start", message: prompt });
33
+ await emit({ type: "message_end", message: prompt });
34
+ }
35
+
36
+ await runLoop(currentContext, newMessages, config, emit, signal);
37
+
38
+ await emit({ type: "agent_end", message: newMessages });
39
+
40
+ return newMessages;
41
+ }
42
+
43
+ async function runLoop(
44
+ initialContext: AgentContext,
45
+ newMessages: AgentMessage[],
46
+ config: AgentLoopConfig,
47
+ emit: AgentEventSink,
48
+ signal?: AbortSignal,
49
+ ): Promise<void> {
50
+ let currentContext = initialContext;
51
+ let hasMoreToolCalls = true;
52
+ let turnCount = 0;
53
+
54
+ while (hasMoreToolCalls) {
55
+ if (signal?.aborted) return;
56
+ if (config.maxTurns && turnCount >= config.maxTurns) break;
57
+ turnCount++;
58
+
59
+ await emit({ type: "turn_start" });
60
+
61
+ const message = await streamAssistantResponse(
62
+ currentContext,
63
+ config,
64
+ signal,
65
+ emit,
66
+ );
67
+
68
+ currentContext.messages.push(message);
69
+ newMessages.push(message);
70
+
71
+ if (message.stopReason === "error" || message.stopReason === "aborted") {
72
+ await emit({ type: "turn_end", message, toolResults: [] });
73
+ return;
74
+ }
75
+
76
+ const toolCalls = message.content
77
+ .filter(
78
+ (part): part is { type: "toolCall"; toolCall: AgentToolCall } =>
79
+ part.type === "toolCall",
80
+ )
81
+ .map((part) => part.toolCall);
82
+
83
+ const toolResults: ToolResultMessage[] = [];
84
+ hasMoreToolCalls = false;
85
+
86
+ if (toolCalls.length > 0) {
87
+ const resultBatch = await executeToolCalls(
88
+ currentContext,
89
+ message,
90
+ config,
91
+ signal,
92
+ emit,
93
+ );
94
+ toolResults.push(...resultBatch.messages);
95
+ hasMoreToolCalls = !resultBatch.terminate;
96
+
97
+ for (const result of toolResults) {
98
+ currentContext.messages.push(result);
99
+ newMessages.push(result);
100
+ }
101
+ }
102
+
103
+ await emit({ type: "turn_end", message, toolResults });
104
+
105
+ if (config.shouldStopAfterTurn) {
106
+ const stop = await config.shouldStopAfterTurn({
107
+ message,
108
+ toolResults,
109
+ context: currentContext,
110
+ newMessages,
111
+ });
112
+ if (stop) break;
113
+ }
114
+
115
+ // If no tool calls, we stop unless hasMoreToolCalls was set by some other logic
116
+ if (toolCalls.length === 0) {
117
+ hasMoreToolCalls = false;
118
+ }
119
+ }
120
+ }
121
+
122
+ async function streamAssistantResponse(
123
+ context: AgentContext,
124
+ config: AgentLoopConfig,
125
+ signal: AbortSignal | undefined,
126
+ emit: AgentEventSink,
127
+ ): Promise<AssistantMessage> {
128
+ const llmMessages = convertToLlm(context.messages, config.provider);
129
+
130
+ const activeTools =
131
+ context.tools?.filter((t) => context.activeToolNames.includes(t.name)) ||
132
+ [];
133
+
134
+ const partialMessage: AssistantMessage = { role: "assistant", content: [] };
135
+ await emit({ type: "message_start", message: partialMessage });
136
+
137
+ try {
138
+ let response;
139
+ if (config.provider === "openai") {
140
+ response = await executeOpenAI({
141
+ apiKey: config.apiKey,
142
+ model: config.model,
143
+ history: llmMessages,
144
+ tools: activeTools,
145
+ provider: "openai",
146
+ prompt: "",
147
+ signal,
148
+ });
149
+ } else if (config.provider === "gemini") {
150
+ response = await executeGemini({
151
+ apiKey: config.apiKey,
152
+ model: config.model,
153
+ history: llmMessages,
154
+ tools: activeTools,
155
+ provider: "gemini",
156
+ prompt: "",
157
+ signal,
158
+ });
159
+ } else {
160
+ throw new Error(`provider ${config.provider} not implemented`);
161
+ }
162
+
163
+ const assistantMsg: AssistantMessage = {
164
+ role: "assistant",
165
+ content: [
166
+ ...(response.text
167
+ ? [{ type: "text" as const, text: response.text }]
168
+ : []),
169
+ ...(response.toolCalls?.map((tc) => ({
170
+ type: "toolCall" as const,
171
+ toolCall: tc,
172
+ })) || []),
173
+ ],
174
+ stopReason:
175
+ response.toolCalls && response.toolCalls.length > 0
176
+ ? "tool_calls"
177
+ : "stop",
178
+ metadata: response.rawMessage?.geminiParts
179
+ ? { geminiParts: response.rawMessage.geminiParts }
180
+ : undefined,
181
+ usage: response.usage,
182
+ };
183
+
184
+ await emit({ type: "message_end", message: assistantMsg });
185
+
186
+ return assistantMsg;
187
+ } catch (error) {
188
+ if (error instanceof Error && error.name === "AbortError") {
189
+ const abortedMsg: AssistantMessage = {
190
+ role: "assistant",
191
+ content: [{ type: "text", text: "Request aborted." }],
192
+ stopReason: "aborted",
193
+ };
194
+ await emit({ type: "message_end", message: abortedMsg });
195
+ return abortedMsg;
196
+ }
197
+
198
+ const errorMsg: AssistantMessage = {
199
+ role: "assistant",
200
+ content: [
201
+ {
202
+ type: "text",
203
+ text: `Error: ${error instanceof Error ? error.message : "Unknown error"}`,
204
+ },
205
+ ],
206
+ stopReason: "error",
207
+ };
208
+
209
+ await emit({ type: "message_end", message: errorMsg });
210
+ return errorMsg;
211
+ }
212
+ }
213
+
214
+ async function executeToolCalls(
215
+ currentContext: AgentContext,
216
+ assistantMessage: AssistantMessage,
217
+ config: AgentLoopConfig,
218
+ signal: AbortSignal | undefined,
219
+ emit: AgentEventSink,
220
+ ): Promise<ExecutedToolCallBatch> {
221
+ const toolCalls = assistantMessage.content
222
+ .filter(
223
+ (part): part is { type: "toolCall"; toolCall: AgentToolCall } =>
224
+ part.type === "toolCall",
225
+ )
226
+ .map((part) => part.toolCall);
227
+
228
+ const hasSequentialToolCall = toolCalls.some((tc) => {
229
+ const tool = currentContext.tools?.find((t) => t.name === tc.name);
230
+ return tool?.executionMode === "sequential";
231
+ });
232
+
233
+ if (config.toolExecution === "sequential" || hasSequentialToolCall) {
234
+ return executeToolCallsSequential(
235
+ currentContext,
236
+ assistantMessage,
237
+ toolCalls,
238
+ config,
239
+ signal,
240
+ emit,
241
+ );
242
+ }
243
+
244
+ return executeToolCallsParallel(
245
+ currentContext,
246
+ assistantMessage,
247
+ toolCalls,
248
+ config,
249
+ signal,
250
+ emit,
251
+ );
252
+ }
253
+
254
+ async function executeToolCallsSequential(
255
+ currentContext: AgentContext,
256
+ assistantMessage: AssistantMessage,
257
+ toolCalls: AgentToolCall[],
258
+ config: AgentLoopConfig,
259
+ signal: AbortSignal | undefined,
260
+ emit: AgentEventSink,
261
+ ): Promise<ExecutedToolCallBatch> {
262
+ const messages: ToolResultMessage[] = [];
263
+ let terminateBatch = false;
264
+ for (const toolCall of toolCalls) {
265
+ if (signal?.aborted) break;
266
+ const result = await runSingleTool(
267
+ currentContext,
268
+ assistantMessage,
269
+ toolCall,
270
+ config,
271
+ signal,
272
+ emit,
273
+ );
274
+ messages.push(result);
275
+ if (result.terminate) {
276
+ terminateBatch = true;
277
+ break;
278
+ }
279
+ }
280
+ return { messages, terminate: terminateBatch };
281
+ }
282
+
283
+ async function executeToolCallsParallel(
284
+ currentContext: AgentContext,
285
+ assistantMessage: AssistantMessage,
286
+ toolCalls: AgentToolCall[],
287
+ config: AgentLoopConfig,
288
+ signal: AbortSignal | undefined,
289
+ emit: AgentEventSink,
290
+ ): Promise<ExecutedToolCallBatch> {
291
+ const toolPromises = toolCalls.map((toolCall) =>
292
+ runSingleTool(currentContext, assistantMessage, toolCall, config, signal, emit),
293
+ );
294
+ const results = await Promise.all(toolPromises);
295
+ return {
296
+ messages: results,
297
+ terminate: results.some((r) => r.terminate),
298
+ };
299
+ }
300
+
301
+ async function runSingleTool(
302
+ context: AgentContext,
303
+ assistantMsg: AssistantMessage,
304
+ toolCall: AgentToolCall,
305
+ config: AgentLoopConfig,
306
+ signal: AbortSignal | undefined,
307
+ emit: AgentEventSink,
308
+ ): Promise<ToolResultMessage> {
309
+ const tool = context.tools?.find((t) => t.name === toolCall.name);
310
+ if (!tool) {
311
+ return {
312
+ role: "tool",
313
+ toolCallId: toolCall.id,
314
+ name: toolCall.name,
315
+ content: `Error: Tool ${toolCall.name} not found`,
316
+ isError: true,
317
+ };
318
+ }
319
+
320
+ await emit({
321
+ type: "tool_execution_start",
322
+ toolCallId: toolCall.id,
323
+ toolName: tool.name,
324
+ args: toolCall.arguments,
325
+ });
326
+
327
+ if (config.beforeToolCall) {
328
+ const hookResult = await config.beforeToolCall({
329
+ assistantMessage: assistantMsg,
330
+ toolCall,
331
+ context,
332
+ });
333
+ if (hookResult?.block) {
334
+ const blockedResult: ToolResultMessage = {
335
+ role: "tool",
336
+ toolCallId: toolCall.id,
337
+ name: tool.name,
338
+ content: `Blocked: ${hookResult.reason}`,
339
+ isError: true,
340
+ };
341
+ await emit({
342
+ type: "tool_execution_end",
343
+ toolCallId: toolCall.id,
344
+ toolName: tool.name,
345
+ result: { content: [{ type: "text", text: blockedResult.content }] },
346
+ isError: true,
347
+ });
348
+ return blockedResult;
349
+ }
350
+ }
351
+
352
+ try {
353
+ const result = await tool.execute(toolCall.arguments);
354
+ const finalizedResult: ToolResultMessage = {
355
+ role: "tool",
356
+ toolCallId: toolCall.id,
357
+ name: tool.name,
358
+ content: result.content[0].text,
359
+ terminate: result.terminate,
360
+ usage: result.usage,
361
+ };
362
+
363
+ await emit({
364
+ type: "tool_execution_end",
365
+ toolCallId: toolCall.id,
366
+ toolName: tool.name,
367
+ result,
368
+ isError: false,
369
+ });
370
+
371
+ return finalizedResult;
372
+ } catch (error) {
373
+ const errorResult: ToolResultMessage = {
374
+ role: "tool",
375
+ toolCallId: toolCall.id,
376
+ name: tool.name,
377
+ content: `Error: ${error instanceof Error ? error.message : String(error)}`,
378
+ isError: true,
379
+ };
380
+ await emit({
381
+ type: "tool_execution_end",
382
+ toolCallId: toolCall.id,
383
+ toolName: tool.name,
384
+ result: { content: [{ type: "text", text: errorResult.content }] },
385
+ isError: true,
386
+ });
387
+ return errorResult;
388
+ }
389
+ }