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,100 @@
1
+ import { rgPath } from "@vscode/ripgrep";
2
+
3
+ import { promisify } from "node:util";
4
+ import { execFile } from "node:child_process";
5
+ import { AgentTool } from "../types";
6
+
7
+ const execFileAsync = promisify(execFile);
8
+
9
+ export const grepTool: AgentTool = {
10
+ name: "grep",
11
+ label: "find text",
12
+ description: "Search for specific text inside files using ripgrep.",
13
+ schema: {
14
+ type: "function",
15
+ function: {
16
+ name: "grep",
17
+ description:
18
+ "Search for a specific string or regex pattern inside files in a directory. Uses ripgrep so it respects .gitignore.",
19
+ parameters: {
20
+ type: "object",
21
+ properties: {
22
+ pattern: {
23
+ type: "string",
24
+ description: "The text or regex pattern to search for.",
25
+ },
26
+ dirPath: {
27
+ type: "string",
28
+ description:
29
+ "The directory to search in. Default is current directory '.'.",
30
+ },
31
+ },
32
+ required: ["pattern"],
33
+ },
34
+ },
35
+ },
36
+ execute: async (agrs: any) => {
37
+ try {
38
+ const pattern = agrs.pattern;
39
+ const targetPath = agrs.dirPath || ".";
40
+
41
+ console.log(
42
+ `\n system runing 'grep' for pattern ${pattern} in ${targetPath}`,
43
+ );
44
+
45
+ let stdout = "";
46
+
47
+ try {
48
+ // execFile bypasses the shell entirely, so we don't have to worry about
49
+ // escaping quotes or bash syntax errors!
50
+ const result = await execFileAsync(rgPath, [
51
+ "-n",
52
+ "-i",
53
+ pattern,
54
+ targetPath,
55
+ ]);
56
+ stdout = result.stdout;
57
+ } catch (execError: any) {
58
+ if (execError.code === 1) {
59
+ return {
60
+ content: [
61
+ { type: "text", text: `No matches found for text: ${pattern}` },
62
+ ],
63
+ details: {},
64
+ };
65
+ }
66
+ throw execError;
67
+ }
68
+
69
+ if (!stdout.trim()) {
70
+ return {
71
+ content: [
72
+ { type: "text", text: `No matches found for text: ${pattern}` },
73
+ ],
74
+ details: {},
75
+ };
76
+ }
77
+
78
+ const lines = stdout.split("\n");
79
+ const limit = 50;
80
+ let output = lines.slice(0, limit).join("\n");
81
+
82
+ if (lines.length > limit) {
83
+ output += `\n\n[Warning: Output truncated. ${lines.length} lines found, showing first ${limit}.]`;
84
+ }
85
+
86
+ return {
87
+ content: [{ type: "text", text: output }],
88
+ details: {},
89
+ };
90
+ } catch (error: any) {
91
+ console.error(`grep tool error ${error.message}`);
92
+ return {
93
+ content: [
94
+ { type: "text", text: `Failed to excuate grep: ${error.message}` },
95
+ ],
96
+ details: {},
97
+ };
98
+ }
99
+ },
100
+ };
@@ -0,0 +1,37 @@
1
+
2
+ import { weatherTool } from "./weather-tool";
3
+ import { lsTool } from "./ls-tool";
4
+ import { findTool } from "./find-tool";
5
+ import { grepTool } from "./grep-tool";
6
+ import { readTool } from "./read-tool";
7
+ import { planTool } from "./plan-tool";
8
+ import { nativeSubagentTool } from "../subagent/index";
9
+ import { bashTool } from "./bash-tool";
10
+ import { writeTool } from "./write-tool";
11
+ import { editTool } from "./edit-tool";
12
+ import { AgentTool } from "../types";
13
+
14
+ export { weatherTool, lsTool };
15
+
16
+ export function getAllTools(): AgentTool[] {
17
+ return [
18
+ weatherTool,
19
+ lsTool,
20
+ findTool,
21
+ grepTool,
22
+ readTool,
23
+ planTool,
24
+ nativeSubagentTool,
25
+ bashTool,
26
+ writeTool,
27
+ editTool
28
+ ];
29
+ }
30
+
31
+ export function getOrchestratorTools(): AgentTool[] {
32
+ return getAllTools();
33
+ }
34
+
35
+ export function getReadOnlyTools(): AgentTool[] {
36
+ return [lsTool, findTool, grepTool, readTool];
37
+ }
@@ -0,0 +1,93 @@
1
+ import fs from "node:fs/promises";
2
+ import { AgentTool } from "../types";
3
+ import path from "node:path";
4
+
5
+ export const lsTool: AgentTool = {
6
+ name: "ls",
7
+ label: "List Directory",
8
+ description:
9
+ "List directory contents for a SINGLE folder. DO NOT use this for deep recursive searching or finding specificfile types. Use the 'find' tool instead.",
10
+ schema: {
11
+ type: "function",
12
+ function: {
13
+ name: "ls",
14
+ description:
15
+ "List directory contents for a SINGLE folder. DO NOT use this for deep recursive searching or finding specific file types. Use the 'find' tool instead.",
16
+ parameters: {
17
+ type: "object",
18
+ properties: {
19
+ dirPath: {
20
+ type: "string",
21
+ description:
22
+ "The directory path to list (e.g., '.', './src', '/Users/vibhu/'). Default is '.'.",
23
+ },
24
+ },
25
+ required: [],
26
+ },
27
+ },
28
+ },
29
+
30
+ execute: async (args: any) => {
31
+ try {
32
+ const targetPath = args.dirPath || ".";
33
+
34
+ console.log(`\n system runing ls for : ${targetPath}...`);
35
+
36
+ try {
37
+ await fs.access(targetPath);
38
+ } catch (error) {
39
+ throw new Error(`Path not found : ${targetPath}`);
40
+ }
41
+
42
+ const start = await fs.stat(targetPath);
43
+ if (!start.isDirectory()) {
44
+ throw new Error(`it is not a directory : ${targetPath}`);
45
+ }
46
+
47
+ const entires = await fs.readdir(targetPath);
48
+
49
+ // Sort alphabetically
50
+ entires.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
51
+
52
+ const result: string[] = [];
53
+
54
+ for (const entry of entires) {
55
+ const fullPath = path.join(targetPath, entry);
56
+
57
+ try {
58
+ const entryStart = await fs.stat(fullPath);
59
+
60
+ if (entryStart.isDirectory()) {
61
+ result.push(entry + "/");
62
+ } else {
63
+ result.push(entry);
64
+ }
65
+ } catch (error) {
66
+ // skip entries we cant read it
67
+ result.push(entry);
68
+ }
69
+ }
70
+
71
+ if (result.length === 0) {
72
+ return {
73
+ content: [{ type: "text", text: "(empty directory" }],
74
+ details: {},
75
+ };
76
+ }
77
+
78
+ const outputString = result.join("\n");
79
+ return {
80
+ content: [{ type: "text", text: outputString }],
81
+ details: {},
82
+ };
83
+ } catch (error: any) {
84
+ console.error(`Ls tool error ${error.message}`);
85
+ return {
86
+ content: [
87
+ { type: "text", text: `Failed to list directory: ${error.message}` },
88
+ ],
89
+ details: {},
90
+ };
91
+ }
92
+ },
93
+ };
@@ -0,0 +1,35 @@
1
+ import { AgentTool, AgentToolResult } from "../types";
2
+
3
+ export const planTool: AgentTool = {
4
+ name: "propose_plan",
5
+ label: "Propose Plan",
6
+ description:
7
+ "CRITICAL: Orchestrator only. Do NOT call this tool until a scout subagent has thoroughly explored the codebase and returned evidence. Use this tool to propose a step-by-step plan for the user to review BEFORE executing mutating work.",
8
+ schema: {
9
+ type: "function",
10
+ function: {
11
+ name: "propose_plan",
12
+ description: "CRITICAL: Must ONLY be called AFTER the scout subagent has researched the codebase. Propose a step-by-step plan.",
13
+ parameters: {
14
+ type: "object",
15
+ properties: {
16
+ steps: {
17
+ type: "array",
18
+ items: {
19
+ type: "string",
20
+ },
21
+ description:
22
+ "An array of detailed steps explaining what you plan to do. Mention which steps are read-only and which steps require user approval because they edit, write, delete, or run mutating commands.",
23
+ },
24
+ },
25
+ required: ["steps"],
26
+ },
27
+ },
28
+ },
29
+ execute: async (args: any): Promise<AgentToolResult> => {
30
+ return {
31
+ content: [{ type: "text", text: JSON.stringify(args) }],
32
+ terminate: true,
33
+ };
34
+ },
35
+ };
@@ -0,0 +1,89 @@
1
+ import { AgentTool } from "../types";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { truncateByBytes } from "./truncate";
5
+
6
+ export const readTool: AgentTool = {
7
+ name: "read_file",
8
+ label: "Read File",
9
+ description: "Read the text contents of a file.",
10
+ schema: {
11
+ type: "function",
12
+ function: {
13
+ name: "read_file",
14
+ description:
15
+ "Read the contents of a file. Use offset and limit for large files.",
16
+ parameters: {
17
+ type: "object",
18
+ properties: {
19
+ path: { type: "string" },
20
+ offset: { type: "number" },
21
+ limit: { type: "number" },
22
+ },
23
+
24
+ required: ["path"],
25
+ },
26
+ },
27
+ },
28
+
29
+ execute: async (args: any) => {
30
+ try {
31
+ const rawPath = args.path;
32
+ const offset = args.offset || 1;
33
+ const limit = args.limit || 500;
34
+
35
+ const filePath = path.resolve(process.cwd(), rawPath);
36
+ if (!filePath.startsWith(process.cwd())) {
37
+ throw new Error(`Permission denied: Cannot read file outside of project directory: ${rawPath}`);
38
+ }
39
+
40
+ console.log(`\n system runing 'read' for pattern ${rawPath}`);
41
+
42
+ await fs.access(filePath);
43
+
44
+ // read the file contents
45
+ const content = await fs.readFile(filePath, "utf-8");
46
+
47
+ const allLines = content.split("\n");
48
+ const totalLine = allLines.length;
49
+
50
+ const startIndex = Math.max(0, offset - 1);
51
+
52
+ if (startIndex >= totalLine) {
53
+ throw new Error(
54
+ `offset ${offset} is beyond the file lines ${totalLine} line total`,
55
+ );
56
+ }
57
+
58
+ // apply the limit
59
+ const endIndex = Math.min(startIndex + limit, totalLine);
60
+
61
+ const selectedContent = allLines.slice(startIndex, endIndex).join("\n");
62
+
63
+ let outputText = selectedContent;
64
+
65
+ if (endIndex < totalLine) {
66
+ const nextOffset = endIndex + 1;
67
+
68
+ outputText += `\n\n [Warning:file truncated. more line in the file, use offset=${nextOffset} to read next chunk ]`;
69
+ }
70
+ const safeOutput = truncateByBytes(outputText);
71
+
72
+ return {
73
+ content: [{ type: "text", text: safeOutput }],
74
+ details: {},
75
+ };
76
+ } catch (error: any) {
77
+ console.error(`read_file tool error ${error.message}`);
78
+ return {
79
+ content: [
80
+ {
81
+ type: "text",
82
+ text: `Failed to excuate read_file: ${error.message}`,
83
+ },
84
+ ],
85
+ details: {},
86
+ };
87
+ }
88
+ },
89
+ };
@@ -0,0 +1,21 @@
1
+
2
+ const default_max_byte = 50 * 1024;
3
+
4
+
5
+ export function truncateByBytes(text: string, maxBytes: number = default_max_byte): string {
6
+
7
+ const buffer = Buffer.from(text, "utf-8");
8
+
9
+ if (buffer.length <= maxBytes) {
10
+ return text;
11
+
12
+ }
13
+
14
+
15
+ const truncatedBuffer = buffer.subarray(0, maxBytes);
16
+
17
+ const safeString = truncatedBuffer.toString('utf-8');
18
+
19
+ return safeString + `\n\n [System Warning: File was too massive. It was brutally truncated at ${maxBytes / 1024}KB to prevent LLM
20
+ memory overload.]`
21
+ }
@@ -0,0 +1,55 @@
1
+ import type { AgentTool, AgentToolResult } from "../types";
2
+
3
+ export const weatherTool: AgentTool = {
4
+ name: "get_current_weather",
5
+ label: "Weather",
6
+ description: "Gets the current weather for a specific city.",
7
+
8
+ schema: {
9
+ type: "function",
10
+ function: {
11
+ name: "get_current_weather",
12
+ description:
13
+ "Gets the current weather for a specific city. Use this when the user asks for weather conditions.",
14
+ parameters: {
15
+ type: "object",
16
+ properties: {
17
+ location: {
18
+ type: "string",
19
+ description: "The city name, e.g., Delhi, London",
20
+ },
21
+ },
22
+ required: ["location"],
23
+ },
24
+ },
25
+ },
26
+
27
+ execute: async (args: any): Promise<AgentToolResult> => {
28
+ try {
29
+ console.log(" args from Openaai:", JSON.stringify(args));
30
+ console.log(`\n[system] running whether tool for : ${args.location}...`);
31
+ const response = await fetch(
32
+ `https://api.openweathermap.org/data/2.5/weather?q=${args.location}&appid=bd5e378503939ddaee76f12ad7a97608&units=metric`,
33
+ {
34
+ method: "GET",
35
+ headers: {
36
+ "Content-Type": "application/json",
37
+ },
38
+ },
39
+ );
40
+
41
+ const result = await response.json();
42
+
43
+ const wheaterString = `The current wheater in ${args.location} is ${result.main.temp} C and ${result.weather[0].description}.`;
44
+ return {
45
+ content: [{ type: "text", text: wheaterString }],
46
+ details: {},
47
+ };
48
+ } catch (error) {
49
+ return {
50
+ content: [{ type: "text", text: "Failed to get weather from API." }],
51
+ details: {},
52
+ };
53
+ }
54
+ },
55
+ };
@@ -0,0 +1,53 @@
1
+ import * as fs from "fs/promises";
2
+ import * as path from "path";
3
+ import { AgentTool, AgentToolResult } from "../types";
4
+
5
+ export const writeTool: AgentTool = {
6
+ name: "write_file",
7
+ label: "Write File",
8
+ description:
9
+ "Write complete content to a new or existing file. This OVERWRITES the entire file. Use 'edit_file' for surgical changes.",
10
+ schema: {
11
+ type: "function",
12
+ function: {
13
+ name: "write_file",
14
+ description: "Write text content to a file.",
15
+ parameters: {
16
+ type: "object",
17
+ properties: {
18
+ file_path: {
19
+ type: "string",
20
+ description: "The path to the file to write.",
21
+ },
22
+ content: {
23
+ type: "string",
24
+ description: "The full content to write to the file.",
25
+ },
26
+ },
27
+ required: ["file_path", "content"],
28
+ },
29
+ },
30
+ },
31
+ execute: async (args: any): Promise<AgentToolResult> => {
32
+ try {
33
+ const fullPath = path.resolve(process.cwd(), args.file_path);
34
+ // Ensure the directory exists
35
+ await fs.mkdir(path.dirname(fullPath), { recursive: true });
36
+ await fs.writeFile(fullPath, args.content, "utf-8");
37
+
38
+ return {
39
+ content: [
40
+ { type: "text", text: `Successfully wrote to ${args.file_path}` },
41
+ ],
42
+ terminate: false,
43
+ };
44
+ } catch (error: any) {
45
+ return {
46
+ content: [
47
+ { type: "text", text: `Failed to write file: ${error.message}` },
48
+ ],
49
+ terminate: false,
50
+ };
51
+ }
52
+ },
53
+ };
package/src/types.ts ADDED
@@ -0,0 +1,28 @@
1
+ export interface AgentToolResult<T = any> {
2
+ content: { type: "text"; text: string }[];
3
+ details?: T;
4
+ terminate?: boolean;
5
+ usage?: {
6
+ promptTokens: number;
7
+ completionTokens: number;
8
+ totalTokens: number;
9
+ };
10
+ }
11
+
12
+ export interface ToolSchema {
13
+ type: "function";
14
+ function: {
15
+ name: string;
16
+ description: string;
17
+ parameters: any;
18
+ };
19
+ }
20
+
21
+ export interface AgentTool {
22
+ name: string;
23
+ label: string;
24
+ description: string;
25
+ schema: ToolSchema;
26
+ execute: (args: any) => Promise<AgentToolResult>;
27
+ executionMode?: "sequential" | "parallel";
28
+ }