coder-agent 2.0.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.
- package/LICENSE +21 -0
- package/README.md +104 -0
- package/dist/agent.js +213 -0
- package/dist/config.js +82 -0
- package/dist/index.js +246 -0
- package/dist/memory.js +257 -0
- package/dist/tools.js +342 -0
- package/package.json +40 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# š¤ Coder
|
|
2
|
+
|
|
3
|
+
A powerful, zero-dependency, cross-platform CLI coding agent powered by **Google Gemini** (gemini-2.5-flash / gemini-2.5-pro) with native tool execution and a clean, minimal Apple-inspired design.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Apple-Inspired Design**: A clean, high-contrast, minimalist terminal UI styled in elegant monochrome greys and whites.
|
|
8
|
+
- **Global CLI Execution**: Install once and run anywhere via `coder` (or the `gemini-agent` / `groq-agent` compatibility aliases).
|
|
9
|
+
- **System Automation Tools**:
|
|
10
|
+
- `read_file` / `write_file` / `patch_file` ā Segmented reading and patch-based editing.
|
|
11
|
+
- `list_directory` / `find_files` ā Recursive structure traversal.
|
|
12
|
+
- `run_shell` ā Direct command execution in Windows, macOS, or Linux terminals.
|
|
13
|
+
- `web_search` ā DuckDuckGo instant answers for documentation retrieval.
|
|
14
|
+
- **Context Persistence**: Local JSON-based persistent configuration stored in `~/.coder-config.json` for key management and default model settings.
|
|
15
|
+
- **Interactive REPL & Single-Shot Modes**:
|
|
16
|
+
- Start the interactive shell with `coder`.
|
|
17
|
+
- Execute a direct command: `coder "build a quicksort function in python"` (exits upon completion).
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
Install globally via npm:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install -g coder-agent
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Or execute directly without installation:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npx coder-agent
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Setup & Configuration
|
|
38
|
+
|
|
39
|
+
The agent requires a **Gemini API Key**.
|
|
40
|
+
|
|
41
|
+
### 1. Interactive Bootstrapping
|
|
42
|
+
Simply run `coder`. If no key is configured, the CLI will interactively prompt you for the key and offer to save it globally.
|
|
43
|
+
|
|
44
|
+
### 2. Command Line Flags
|
|
45
|
+
You can save your key globally at any time using flags:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
coder --set-key YOUR_GEMINI_API_KEY
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### 3. Environment Variables
|
|
52
|
+
Alternatively, you can export it as an environment variable:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
export GEMINI_API_KEY=AIzaSy...
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Usage & Arguments
|
|
61
|
+
|
|
62
|
+
### REPL Mode
|
|
63
|
+
Start an interactive pair programming session:
|
|
64
|
+
```bash
|
|
65
|
+
coder
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Single-Shot Prompt Mode
|
|
69
|
+
Run a prompt directly from the shell:
|
|
70
|
+
```bash
|
|
71
|
+
coder "write a binary search script in Python"
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### CLI Command Options
|
|
75
|
+
- `-h, --help` ā Show the help screen.
|
|
76
|
+
- `-v, --version` ā Show version information.
|
|
77
|
+
- `--model <name>` ā Set the default Gemini model globally.
|
|
78
|
+
- `--set-key <key>` ā Persist your Gemini API Key globally.
|
|
79
|
+
- `--memory <scope>` ā Define memory scope (`project`, `user`, or `local`).
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Built-in REPL Commands
|
|
84
|
+
|
|
85
|
+
Type these commands directly inside the interactive session:
|
|
86
|
+
|
|
87
|
+
| Command | Description |
|
|
88
|
+
|---------|-------------|
|
|
89
|
+
| `/model [name]` | View active model or switch active model dynamically (e.g. `/model gemini-2.5-pro`) |
|
|
90
|
+
| `/clear` | Wipe conversation memory |
|
|
91
|
+
| `/status` | Show active model and memory usage details |
|
|
92
|
+
| `/help` | Show command summary |
|
|
93
|
+
| `/exit` | Quit Coder |
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Popular Models Supported
|
|
98
|
+
- `gemini-2.5-flash` (Default, highly capable, extremely fast, massive 1M+ token context)
|
|
99
|
+
- `gemini-2.5-pro` (Reasoning model, superb for complex coding tasks)
|
|
100
|
+
- `gemini-2.0-flash` (Ultra-fast, lightweight)
|
|
101
|
+
- `gemini-2.0-pro-exp` (Experimental reasoning model)
|
|
102
|
+
|
|
103
|
+
## License
|
|
104
|
+
MIT
|
package/dist/agent.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { TOOL_DEFINITIONS, dispatchTool } from "./tools.js";
|
|
4
|
+
import { Memory } from "./memory.js";
|
|
5
|
+
function extractTextToolCalls(content) {
|
|
6
|
+
const calls = [];
|
|
7
|
+
// Pattern 1: <function(name)> {args} </function>
|
|
8
|
+
const pattern1 = /<function\((\w+)\)>([\s\S]*?)(?:<\/function>)?/g;
|
|
9
|
+
let match;
|
|
10
|
+
while ((match = pattern1.exec(content)) !== null) {
|
|
11
|
+
try {
|
|
12
|
+
const name = match[1];
|
|
13
|
+
const argsText = match[2].trim();
|
|
14
|
+
const args = JSON.parse(argsText);
|
|
15
|
+
calls.push({
|
|
16
|
+
id: `text_call_${Math.random().toString(36).substring(2, 9)}`,
|
|
17
|
+
name,
|
|
18
|
+
args,
|
|
19
|
+
rawText: match[0]
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
catch { }
|
|
23
|
+
}
|
|
24
|
+
// Pattern 1b: <function(name){args}></function>
|
|
25
|
+
const pattern1b = /<function\((\w+)\)({[\s\S]*?})>(?:<\/function>)?/g;
|
|
26
|
+
pattern1b.lastIndex = 0;
|
|
27
|
+
while ((match = pattern1b.exec(content)) !== null) {
|
|
28
|
+
try {
|
|
29
|
+
const name = match[1];
|
|
30
|
+
const argsText = match[2].trim();
|
|
31
|
+
if (calls.some(c => c.rawText === match[0]))
|
|
32
|
+
continue;
|
|
33
|
+
const args = JSON.parse(argsText);
|
|
34
|
+
calls.push({
|
|
35
|
+
id: `text_call_${Math.random().toString(36).substring(2, 9)}`,
|
|
36
|
+
name,
|
|
37
|
+
args,
|
|
38
|
+
rawText: match[0]
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
catch { }
|
|
42
|
+
}
|
|
43
|
+
// Pattern 2: <function=name>{args}</function>
|
|
44
|
+
const pattern2 = /<function=(\w+)>({[\s\S]*?})<\/function>/g;
|
|
45
|
+
pattern2.lastIndex = 0;
|
|
46
|
+
while ((match = pattern2.exec(content)) !== null) {
|
|
47
|
+
try {
|
|
48
|
+
const name = match[1];
|
|
49
|
+
const args = JSON.parse(match[2]);
|
|
50
|
+
if (calls.some(c => c.rawText === match[0]))
|
|
51
|
+
continue;
|
|
52
|
+
calls.push({
|
|
53
|
+
id: `text_call_${Math.random().toString(36).substring(2, 9)}`,
|
|
54
|
+
name,
|
|
55
|
+
args,
|
|
56
|
+
rawText: match[0]
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
catch { }
|
|
60
|
+
}
|
|
61
|
+
return calls;
|
|
62
|
+
}
|
|
63
|
+
async function callGeminiAPI(apiKey, params, maxRetries = 3, initialDelayMs = 1500) {
|
|
64
|
+
let attempts = 0;
|
|
65
|
+
while (attempts < maxRetries) {
|
|
66
|
+
try {
|
|
67
|
+
const res = await fetch("https://generativelanguage.googleapis.com/v1beta/openai/chat/completions", {
|
|
68
|
+
method: "POST",
|
|
69
|
+
headers: {
|
|
70
|
+
"Content-Type": "application/json",
|
|
71
|
+
"Authorization": `Bearer ${apiKey}`
|
|
72
|
+
},
|
|
73
|
+
body: JSON.stringify(params)
|
|
74
|
+
});
|
|
75
|
+
if (!res.ok) {
|
|
76
|
+
const errText = await res.text();
|
|
77
|
+
const err = new Error(`Gemini API error (${res.status}): ${errText}`);
|
|
78
|
+
err.status = res.status;
|
|
79
|
+
throw err;
|
|
80
|
+
}
|
|
81
|
+
return await res.json();
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
attempts++;
|
|
85
|
+
const status = err?.status;
|
|
86
|
+
const isRetryableError = status === 429 || status === 503 || (status >= 500 && status < 600) || !status;
|
|
87
|
+
if (isRetryableError && attempts < maxRetries) {
|
|
88
|
+
const delay = initialDelayMs * Math.pow(2, attempts - 1);
|
|
89
|
+
console.log(chalk.gray(` ā ļø Gemini API busy (Status ${status || "network"}). Retrying in ${(delay / 1000).toFixed(1)}s (attempt ${attempts}/${maxRetries})...`));
|
|
90
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
throw err;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
export class Agent {
|
|
98
|
+
apiKey;
|
|
99
|
+
memory;
|
|
100
|
+
model;
|
|
101
|
+
memoryScope;
|
|
102
|
+
constructor(apiKey, model = "gemini-2.5-flash", memoryScope = "project") {
|
|
103
|
+
this.apiKey = apiKey;
|
|
104
|
+
this.memory = new Memory();
|
|
105
|
+
this.model = model;
|
|
106
|
+
this.memoryScope = memoryScope;
|
|
107
|
+
}
|
|
108
|
+
clearMemory() {
|
|
109
|
+
this.memory.clear();
|
|
110
|
+
}
|
|
111
|
+
memoryStatus() {
|
|
112
|
+
return this.memory.summary();
|
|
113
|
+
}
|
|
114
|
+
getModel() {
|
|
115
|
+
return this.model;
|
|
116
|
+
}
|
|
117
|
+
setModel(model) {
|
|
118
|
+
this.model = model;
|
|
119
|
+
}
|
|
120
|
+
async chat(userMessage) {
|
|
121
|
+
await this.memory.init(this.memoryScope, "coder");
|
|
122
|
+
this.memory.add({ role: "user", content: userMessage });
|
|
123
|
+
let iterations = 0;
|
|
124
|
+
const MAX_ITERATIONS = 12; // prevent infinite tool loops
|
|
125
|
+
const modifiedFiles = new Set();
|
|
126
|
+
while (iterations < MAX_ITERATIONS) {
|
|
127
|
+
iterations++;
|
|
128
|
+
const response = await callGeminiAPI(this.apiKey, {
|
|
129
|
+
model: this.model,
|
|
130
|
+
messages: this.memory.getAll(),
|
|
131
|
+
tools: TOOL_DEFINITIONS,
|
|
132
|
+
tool_choice: "auto",
|
|
133
|
+
temperature: 0.2,
|
|
134
|
+
});
|
|
135
|
+
const choice = response.choices[0];
|
|
136
|
+
const msg = choice.message;
|
|
137
|
+
// Extract text-based tool calls if native tool_calls is empty
|
|
138
|
+
let toolCalls = msg.tool_calls || [];
|
|
139
|
+
const extracted = extractTextToolCalls(msg.content ?? "");
|
|
140
|
+
if (toolCalls.length === 0 && extracted.length > 0) {
|
|
141
|
+
toolCalls = extracted.map(e => ({
|
|
142
|
+
id: e.id,
|
|
143
|
+
type: "function",
|
|
144
|
+
function: {
|
|
145
|
+
name: e.name,
|
|
146
|
+
arguments: JSON.stringify(e.args)
|
|
147
|
+
}
|
|
148
|
+
}));
|
|
149
|
+
}
|
|
150
|
+
// Save assistant message to memory (with standard/parsed tool calls)
|
|
151
|
+
const assistantMsg = {
|
|
152
|
+
role: "assistant",
|
|
153
|
+
content: msg.content ?? "",
|
|
154
|
+
tool_calls: toolCalls.length > 0 ? toolCalls : undefined,
|
|
155
|
+
};
|
|
156
|
+
this.memory.add(assistantMsg);
|
|
157
|
+
// Clean up text tool calls from the conversational output content
|
|
158
|
+
let cleanContent = msg.content ?? "";
|
|
159
|
+
for (const call of extracted) {
|
|
160
|
+
cleanContent = cleanContent.replace(call.rawText, "");
|
|
161
|
+
}
|
|
162
|
+
cleanContent = cleanContent.trim();
|
|
163
|
+
// āā No tool calls ā final answer āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
164
|
+
if (toolCalls.length === 0) {
|
|
165
|
+
if (cleanContent) {
|
|
166
|
+
console.log("\n" + chalk.bold.white("coder ⯠") + cleanContent);
|
|
167
|
+
}
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
// If the model returned conversational text along with tool calls, print it cleanly
|
|
171
|
+
if (cleanContent) {
|
|
172
|
+
console.log("\n" + chalk.bold.white("coder ⯠") + cleanContent);
|
|
173
|
+
}
|
|
174
|
+
// āā Execute tool calls āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
175
|
+
for (const toolCall of toolCalls) {
|
|
176
|
+
const name = toolCall.function.name;
|
|
177
|
+
let args = {};
|
|
178
|
+
try {
|
|
179
|
+
args = JSON.parse(toolCall.function.arguments);
|
|
180
|
+
}
|
|
181
|
+
catch { }
|
|
182
|
+
console.log(chalk.gray(` ā tool: ${name}`) + chalk.gray(` (${JSON.stringify(args).slice(0, 100)}...)`));
|
|
183
|
+
// Track created or modified files
|
|
184
|
+
if (name === "write_file" || name === "patch_file") {
|
|
185
|
+
if (args.file_path) {
|
|
186
|
+
modifiedFiles.add(path.normalize(args.file_path));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
const result = await dispatchTool(name, args);
|
|
190
|
+
const preview = result.length > 300 ? result.slice(0, 300) + "\n ⦠(truncated)" : result;
|
|
191
|
+
// Format preview text elegantly with indentation
|
|
192
|
+
const indentedPreview = preview.split("\n").map(l => " " + l).join("\n");
|
|
193
|
+
console.log(chalk.gray(indentedPreview));
|
|
194
|
+
this.memory.add({
|
|
195
|
+
role: "tool",
|
|
196
|
+
content: result,
|
|
197
|
+
tool_call_id: toolCall.id,
|
|
198
|
+
name,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// Print a clean summary of modified files at the end of the conversation turn
|
|
203
|
+
if (modifiedFiles.size > 0) {
|
|
204
|
+
console.log(chalk.gray("\n Modified files:"));
|
|
205
|
+
for (const file of modifiedFiles) {
|
|
206
|
+
console.log(chalk.white(` ⢠${file}`));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (iterations >= MAX_ITERATIONS) {
|
|
210
|
+
console.log(chalk.red(" ā Max tool iterations reached."));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import * as fs from "fs/promises";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import * as os from "os";
|
|
4
|
+
const CONFIG_FILE = path.join(os.homedir(), ".coder-config.json");
|
|
5
|
+
const OLD_GEMINI_CONFIG_FILE = path.join(os.homedir(), ".gemini-agent-config.json");
|
|
6
|
+
const OLD_GROQ_CONFIG_FILE = path.join(os.homedir(), ".groq-agent-config.json");
|
|
7
|
+
async function readConfig() {
|
|
8
|
+
// Try reading the primary Coder config file
|
|
9
|
+
try {
|
|
10
|
+
const data = await fs.readFile(CONFIG_FILE, "utf-8");
|
|
11
|
+
return JSON.parse(data);
|
|
12
|
+
}
|
|
13
|
+
catch (err) {
|
|
14
|
+
if (err.code === "ENOENT") {
|
|
15
|
+
// Try migrating from the gemini config file
|
|
16
|
+
try {
|
|
17
|
+
const data = await fs.readFile(OLD_GEMINI_CONFIG_FILE, "utf-8");
|
|
18
|
+
const oldConfig = JSON.parse(data);
|
|
19
|
+
const migrated = {
|
|
20
|
+
geminiApiKey: oldConfig.geminiApiKey,
|
|
21
|
+
defaultModel: oldConfig.defaultModel,
|
|
22
|
+
};
|
|
23
|
+
// Save the migrated config immediately
|
|
24
|
+
await writeConfig(migrated);
|
|
25
|
+
return migrated;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// Try migrating from the older groq config file
|
|
29
|
+
try {
|
|
30
|
+
const data = await fs.readFile(OLD_GROQ_CONFIG_FILE, "utf-8");
|
|
31
|
+
const oldConfig = JSON.parse(data);
|
|
32
|
+
const migrated = {
|
|
33
|
+
geminiApiKey: oldConfig.geminiApiKey || oldConfig.apiKey,
|
|
34
|
+
defaultModel: oldConfig.defaultModel,
|
|
35
|
+
};
|
|
36
|
+
// Save the migrated config immediately
|
|
37
|
+
await writeConfig(migrated);
|
|
38
|
+
return migrated;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return {};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return {};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async function writeConfig(config) {
|
|
49
|
+
try {
|
|
50
|
+
await fs.writeFile(CONFIG_FILE, JSON.stringify(config, null, 2), "utf-8");
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
console.error(`\n ā ļø Failed to save config to ${CONFIG_FILE}: ${err.message}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export async function getStoredApiKey() {
|
|
57
|
+
const config = await readConfig();
|
|
58
|
+
return config.geminiApiKey;
|
|
59
|
+
}
|
|
60
|
+
export async function saveApiKey(apiKey) {
|
|
61
|
+
const config = await readConfig();
|
|
62
|
+
config.geminiApiKey = apiKey;
|
|
63
|
+
await writeConfig(config);
|
|
64
|
+
}
|
|
65
|
+
export async function getStoredModel() {
|
|
66
|
+
const config = await readConfig();
|
|
67
|
+
return config.defaultModel;
|
|
68
|
+
}
|
|
69
|
+
export async function saveModel(model) {
|
|
70
|
+
const config = await readConfig();
|
|
71
|
+
config.defaultModel = model;
|
|
72
|
+
await writeConfig(config);
|
|
73
|
+
}
|
|
74
|
+
export async function getStoredGeminiApiKey() {
|
|
75
|
+
const config = await readConfig();
|
|
76
|
+
return config.geminiApiKey;
|
|
77
|
+
}
|
|
78
|
+
export async function saveGeminiApiKey(geminiApiKey) {
|
|
79
|
+
const config = await readConfig();
|
|
80
|
+
config.geminiApiKey = geminiApiKey;
|
|
81
|
+
await writeConfig(config);
|
|
82
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import * as readline from "readline";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import { Agent } from "./agent.js";
|
|
5
|
+
import { getStoredApiKey, saveApiKey, getStoredModel, saveModel } from "./config.js";
|
|
6
|
+
const VALID_MODELS = [
|
|
7
|
+
"gemini-2.5-flash",
|
|
8
|
+
"gemini-2.5-pro",
|
|
9
|
+
"gemini-2.0-flash",
|
|
10
|
+
"gemini-2.0-pro-exp"
|
|
11
|
+
];
|
|
12
|
+
// āāā Banner āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
13
|
+
function printBanner(modelName) {
|
|
14
|
+
console.log("");
|
|
15
|
+
console.log(chalk.bold.white(" Coder"));
|
|
16
|
+
console.log(chalk.gray(` Model: ${modelName}`));
|
|
17
|
+
console.log(chalk.gray(" Status: Ready"));
|
|
18
|
+
console.log(chalk.gray(" āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā"));
|
|
19
|
+
console.log(chalk.gray(" Tools: ") + chalk.white("read_file Ā· write_file Ā· patch_file Ā· search_grep Ā· run_shell Ā· web_search"));
|
|
20
|
+
console.log(chalk.gray(" Commands: ") + chalk.white("/model Ā· /clear Ā· /status Ā· /help Ā· /exit"));
|
|
21
|
+
console.log(chalk.gray(" āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā"));
|
|
22
|
+
console.log("");
|
|
23
|
+
}
|
|
24
|
+
function printHelp() {
|
|
25
|
+
console.log(chalk.bold.white("\n Coder CLI v1.1\n"));
|
|
26
|
+
console.log(chalk.gray(" Usage:"));
|
|
27
|
+
console.log(chalk.white(" coder ā Start the interactive REPL"));
|
|
28
|
+
console.log(chalk.white(" coder [options] ā Run with config options"));
|
|
29
|
+
console.log(chalk.white(" coder \"your query here\" ā Run in single-shot mode (exits after completion)"));
|
|
30
|
+
console.log("");
|
|
31
|
+
console.log(chalk.gray(" Options:"));
|
|
32
|
+
console.log(chalk.white(" -h, --help ā Show this help screen"));
|
|
33
|
+
console.log(chalk.white(" -v, --version ā Show version information"));
|
|
34
|
+
console.log(chalk.white(" --model <model_name> ā Set default model globally"));
|
|
35
|
+
console.log(chalk.white(" --memory <scope> ā Set memory scope: 'user', 'project', or 'local' (default: 'project')"));
|
|
36
|
+
console.log(chalk.white(" --set-key <api_key> ā Save your Gemini API Key globally"));
|
|
37
|
+
console.log(chalk.white(" --set-gemini-key <api_key> ā Save your Gemini API Key globally (alias)"));
|
|
38
|
+
console.log("");
|
|
39
|
+
console.log(chalk.gray(" Popular Gemini Models:"));
|
|
40
|
+
console.log(chalk.white(" gemini-2.5-flash ā Default, highly capable & fast"));
|
|
41
|
+
console.log(chalk.white(" gemini-2.5-pro ā Reasoning model, excellent coding"));
|
|
42
|
+
console.log(chalk.white(" gemini-2.0-flash ā Ultra-fast, lightweight"));
|
|
43
|
+
console.log(chalk.white(" gemini-2.0-pro-exp ā Experimental reasoning model"));
|
|
44
|
+
console.log("");
|
|
45
|
+
}
|
|
46
|
+
// āāā API Key Bootstrap āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
47
|
+
async function promptApiKey() {
|
|
48
|
+
const rl = readline.createInterface({
|
|
49
|
+
input: process.stdin,
|
|
50
|
+
output: process.stdout,
|
|
51
|
+
});
|
|
52
|
+
return new Promise((resolve) => {
|
|
53
|
+
console.log(chalk.gray("\n š Gemini API Key is required."));
|
|
54
|
+
console.log(chalk.gray(" Get a free key at https://aistudio.google.com"));
|
|
55
|
+
rl.question(chalk.gray("\n Enter Gemini API Key: "), async (key) => {
|
|
56
|
+
const trimmedKey = key.trim();
|
|
57
|
+
if (!trimmedKey) {
|
|
58
|
+
console.log(chalk.red(" ā API Key cannot be empty."));
|
|
59
|
+
rl.close();
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
rl.question(chalk.gray(" Save this key globally? (y/N): "), async (answer) => {
|
|
63
|
+
const save = answer.toLowerCase().startsWith("y");
|
|
64
|
+
if (save) {
|
|
65
|
+
await saveApiKey(trimmedKey);
|
|
66
|
+
console.log(chalk.gray(` ā Key saved to ~/.coder-config.json`));
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
console.log(chalk.gray(` ā Using key for this session only (not saved).`));
|
|
70
|
+
}
|
|
71
|
+
rl.close();
|
|
72
|
+
resolve(trimmedKey);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
// āāā Main Execution āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
78
|
+
async function main() {
|
|
79
|
+
const args = process.argv.slice(2);
|
|
80
|
+
let tempModel;
|
|
81
|
+
let tempMemoryScope;
|
|
82
|
+
const queryArgs = [];
|
|
83
|
+
// Parse arguments manually
|
|
84
|
+
for (let i = 0; i < args.length; i++) {
|
|
85
|
+
if (args[i] === "--model") {
|
|
86
|
+
tempModel = args[i + 1];
|
|
87
|
+
i++; // skip next arg
|
|
88
|
+
}
|
|
89
|
+
else if (args[i] === "--memory") {
|
|
90
|
+
const scope = args[i + 1];
|
|
91
|
+
if (scope !== "user" && scope !== "project" && scope !== "local") {
|
|
92
|
+
console.log(chalk.red("ā Invalid memory scope. Choose 'user', 'project', or 'local'."));
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
tempMemoryScope = scope;
|
|
96
|
+
i++; // skip next arg
|
|
97
|
+
}
|
|
98
|
+
else if (args[i] === "--set-key" || args[i] === "--set-gemini-key") {
|
|
99
|
+
const key = args[i + 1];
|
|
100
|
+
if (!key) {
|
|
101
|
+
console.log(chalk.red("ā Please specify an API key."));
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
await saveApiKey(key);
|
|
105
|
+
console.log(chalk.gray("ā Gemini API Key saved successfully to global config."));
|
|
106
|
+
process.exit(0);
|
|
107
|
+
}
|
|
108
|
+
else if (args[i] === "-h" || args[i] === "--help") {
|
|
109
|
+
printHelp();
|
|
110
|
+
process.exit(0);
|
|
111
|
+
}
|
|
112
|
+
else if (args[i] === "-v" || args[i] === "--version") {
|
|
113
|
+
console.log("coder v1.1.0");
|
|
114
|
+
process.exit(0);
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
queryArgs.push(args[i]);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// Load stored settings
|
|
121
|
+
const storedKey = await getStoredApiKey();
|
|
122
|
+
const envKey = process.env.GEMINI_API_KEY || process.env.GROQ_API_KEY;
|
|
123
|
+
let apiKey = envKey || storedKey;
|
|
124
|
+
const storedModel = await getStoredModel();
|
|
125
|
+
const defaultModel = storedModel || "gemini-2.5-flash";
|
|
126
|
+
const modelToUse = tempModel || defaultModel;
|
|
127
|
+
// Save model if specified without query
|
|
128
|
+
if (tempModel && queryArgs.length === 0) {
|
|
129
|
+
await saveModel(tempModel);
|
|
130
|
+
console.log(chalk.gray(`ā Default model set to: ${tempModel}`));
|
|
131
|
+
process.exit(0);
|
|
132
|
+
}
|
|
133
|
+
// Bootstrap API Key if missing
|
|
134
|
+
if (!apiKey) {
|
|
135
|
+
apiKey = await promptApiKey();
|
|
136
|
+
}
|
|
137
|
+
const agent = new Agent(apiKey, modelToUse, tempMemoryScope);
|
|
138
|
+
// Single-Shot Mode
|
|
139
|
+
if (queryArgs.length > 0) {
|
|
140
|
+
const singleShotPrompt = queryArgs.join(" ").trim();
|
|
141
|
+
console.log(chalk.gray(`š Running single-shot query using model '${modelToUse}'...`));
|
|
142
|
+
console.log(chalk.gray(`⯠Query: ${singleShotPrompt}`));
|
|
143
|
+
try {
|
|
144
|
+
await agent.chat(singleShotPrompt);
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
if (err?.status === 401) {
|
|
148
|
+
console.log(chalk.red("ā Invalid API key. Check your configuration."));
|
|
149
|
+
}
|
|
150
|
+
else if (err?.status === 429) {
|
|
151
|
+
console.log(chalk.red("\nā Rate limit exceeded on Gemini API."));
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
console.log(chalk.red(`ā Error: ${err.message}`));
|
|
155
|
+
}
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
process.exit(0);
|
|
159
|
+
}
|
|
160
|
+
// Interactive REPL Mode
|
|
161
|
+
printBanner(modelToUse);
|
|
162
|
+
const rl = readline.createInterface({
|
|
163
|
+
input: process.stdin,
|
|
164
|
+
output: process.stdout,
|
|
165
|
+
terminal: true,
|
|
166
|
+
});
|
|
167
|
+
const prompt = () => {
|
|
168
|
+
rl.question(chalk.gray("\ncoder ⯠"), async (input) => {
|
|
169
|
+
const trimmed = input.trim();
|
|
170
|
+
if (!trimmed) {
|
|
171
|
+
prompt();
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
// Built-in slash commands
|
|
175
|
+
if (trimmed === "/exit" || trimmed === "/quit") {
|
|
176
|
+
console.log(chalk.gray("\nGoodbye."));
|
|
177
|
+
rl.close();
|
|
178
|
+
process.exit(0);
|
|
179
|
+
}
|
|
180
|
+
if (trimmed === "/clear") {
|
|
181
|
+
agent.clearMemory();
|
|
182
|
+
prompt();
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (trimmed === "/status") {
|
|
186
|
+
console.log(chalk.gray(` Model: `) + chalk.white(agent.getModel()));
|
|
187
|
+
console.log(chalk.gray(` Memory: `) + chalk.white(agent.memoryStatus()));
|
|
188
|
+
prompt();
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (trimmed.startsWith("/model")) {
|
|
192
|
+
const parts = trimmed.split(/\s+/);
|
|
193
|
+
if (parts.length === 1) {
|
|
194
|
+
console.log(chalk.gray(`\n Active model: ${agent.getModel()}`));
|
|
195
|
+
console.log(chalk.gray(` Available models:\n` + VALID_MODELS.map(m => ` ⢠${m}`).join("\n")));
|
|
196
|
+
console.log(chalk.gray(`\n To switch model, run: /model <model-name>`));
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
const newModel = parts[1];
|
|
200
|
+
agent.setModel(newModel);
|
|
201
|
+
console.log(chalk.gray(`\nā Switched active model to: ${newModel}`));
|
|
202
|
+
}
|
|
203
|
+
prompt();
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (trimmed === "/help") {
|
|
207
|
+
console.log(chalk.bold.white("\n Interactive Commands:"));
|
|
208
|
+
console.log(chalk.gray(` /model [name] ā View active model or switch to [name]
|
|
209
|
+
/clear ā Wipe conversation memory
|
|
210
|
+
/status ā Show active model and memory usage
|
|
211
|
+
/exit ā Exit Coder`));
|
|
212
|
+
console.log(chalk.bold.white("\n API Keys & Configuration:"));
|
|
213
|
+
console.log(chalk.gray(` ⢠Stored at: ~/.coder-config.json
|
|
214
|
+
⢠To change key: Exit and run 'coder --set-key <key>'
|
|
215
|
+
⢠Env variable option: GEMINI_API_KEY`));
|
|
216
|
+
prompt();
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
try {
|
|
220
|
+
await agent.chat(trimmed);
|
|
221
|
+
}
|
|
222
|
+
catch (err) {
|
|
223
|
+
if (err?.status === 401) {
|
|
224
|
+
console.log(chalk.red("ā Invalid API key. Check your configuration."));
|
|
225
|
+
}
|
|
226
|
+
else if (err?.status === 429) {
|
|
227
|
+
console.log(chalk.red("\nā Rate limit exceeded on Gemini API."));
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
console.log(chalk.red(`ā Error: ${err.message}`));
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
prompt();
|
|
234
|
+
});
|
|
235
|
+
};
|
|
236
|
+
// Handle Ctrl+C gracefully
|
|
237
|
+
rl.on("SIGINT", () => {
|
|
238
|
+
console.log(chalk.gray("\n\nGoodbye."));
|
|
239
|
+
process.exit(0);
|
|
240
|
+
});
|
|
241
|
+
prompt();
|
|
242
|
+
}
|
|
243
|
+
main().catch((err) => {
|
|
244
|
+
console.error(chalk.red(`Fatal Error: ${err.message}`));
|
|
245
|
+
process.exit(1);
|
|
246
|
+
});
|
package/dist/memory.js
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import * as fs from "fs/promises";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import * as os from "os";
|
|
4
|
+
const SYSTEM_PROMPT = `You are a powerful, intelligent CLI coding agent named Coder. You help users write code, debug, manage files, run commands, and search the web.
|
|
5
|
+
|
|
6
|
+
The tools available to you are provided automatically by the API schema. Do NOT describe the tools in your text or invent custom tags.
|
|
7
|
+
|
|
8
|
+
PRINCIPLES & SYSTEM PROTOCOLS FOR ERROR-FREE EXECUTION:
|
|
9
|
+
1. Ground Truth Workspace Context: Use the provided environment platform info, workspace file structure snapshot, and package configurations as your primary source of requirements. Do not guess what files exist or how the project compiles.
|
|
10
|
+
2. Read before Writing/Editing: Always look at the files you want to change first. Read the relevant lines (using read_file_lines or read_file) to understand import requirements, types, and architecture.
|
|
11
|
+
3. Precise Target Editing: Prefer patching files (using patch_file) over complete overwriting. Ensure targeted matches are unique and match exactly, leaving existing unrelated functions/comments intact.
|
|
12
|
+
4. Auto-Verification Loop: After any code or file edit, you MUST run the appropriate compiler, type-check, build script, or test tool (e.g. npm run build, npx tsc, pytest, cargo build, etc.) to verify your changes are syntactically and logically correct. If compilation fails, diagnose the error and patch it immediately.
|
|
13
|
+
5. Autonomous Troubleshooting: If a command fails or times out, inspect the codebase or script to see why it hangs or fails. Do not blindly edit package scripts or configs.
|
|
14
|
+
|
|
15
|
+
Guidelines:
|
|
16
|
+
- Be concise in your explanations; let code and command output speak for itself.
|
|
17
|
+
- When writing code, always use write_file then run_shell to verify it works.
|
|
18
|
+
- Prefer using search_grep to locate code, read_file_lines to read relevant parts, and patch_file to make targeted edits, especially in large codebases. This prevents token/context overflow.
|
|
19
|
+
- When you encounter errors, diagnose and fix them autonomously.
|
|
20
|
+
- Prefer running commands to verify assumptions rather than guessing.
|
|
21
|
+
- Before answering questions or checking for errors in the codebase, always inspect the workspace (e.g., list directories, read files) to identify the files and languages present. Do not guess file names or run commands on files without checking if they exist first.
|
|
22
|
+
- Focus on the actual files and programming language of the codebase you are currently running in (the user's repository). Do not assume the project is in a different language. Identify the project type and use appropriate commands (e.g., check package.json/tsconfig.json and run 'npm run build' or 'npx tsc' for TypeScript, or use python commands only if the workspace is a Python project).
|
|
23
|
+
- NEVER create dummy or placeholder files (such as write_file of a hello-world 'test.py' or 'filename.py') to simulate compilation or execution. Work only with the actual code of the project.
|
|
24
|
+
- When asked to "check for syntax errors" or "compile", do NOT search the files for the literal string "syntax error". Instead, run the project's compiler, build script, type-checker, or linter (such as 'npm run build', 'npx tsc --noEmit', or standard language compilers) to find actual code syntax issues.
|
|
25
|
+
- If a shell command fails, hangs, or times out with minimal output (e.g. "Command failed"), do not make blind edits to configuration files (like package.json). Inspect the source code of the entry point or script to see if it requires environment variables (e.g., API keys), blocks on interactive terminal input (e.g., readline prompts), or has other expectations. You can test commands by passing dummy environment variables if needed.
|
|
26
|
+
- If a task is ambiguous or you cannot find the code the user is referring to, ask ONE clarifying question before proceeding.
|
|
27
|
+
- Always show the user what files you've created/modified.
|
|
28
|
+
- CRITICAL (Tool Calling): Use the native API tool calling mechanism to execute tools. Never output raw XML tags, HTML tags, or mock function call strings (like '<function=...>') in your conversational chat response.
|
|
29
|
+
- CRITICAL (Response Limitation): When calling a tool, do not output any conversational explanations, thoughts, or markdown before or after the tool call in the same response. Only output conversational text when you are providing the final answer.`;
|
|
30
|
+
function sanitizeAgentTypeForPath(agentType) {
|
|
31
|
+
return agentType.replace(/:/g, "-");
|
|
32
|
+
}
|
|
33
|
+
function getLocalAgentMemoryDir(dirName) {
|
|
34
|
+
return path.join(process.cwd(), ".coder", "agent-memory-local", dirName);
|
|
35
|
+
}
|
|
36
|
+
export function getAgentMemoryDir(agentType, scope) {
|
|
37
|
+
const dirName = sanitizeAgentTypeForPath(agentType);
|
|
38
|
+
switch (scope) {
|
|
39
|
+
case "project":
|
|
40
|
+
return path.join(process.cwd(), ".coder", "agent-memory", dirName);
|
|
41
|
+
case "local":
|
|
42
|
+
return getLocalAgentMemoryDir(dirName);
|
|
43
|
+
case "user":
|
|
44
|
+
return path.join(os.homedir(), ".coder", "agent-memory", dirName);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export function getAgentMemoryEntrypoint(agentType, scope) {
|
|
48
|
+
return path.join(getAgentMemoryDir(agentType, scope), "MEMORY.md");
|
|
49
|
+
}
|
|
50
|
+
export function getMemoryScopeDisplay(memory) {
|
|
51
|
+
switch (memory) {
|
|
52
|
+
case "user":
|
|
53
|
+
return `User (${path.join(os.homedir(), ".coder", "agent-memory")})`;
|
|
54
|
+
case "project":
|
|
55
|
+
return "Project (.coder/agent-memory/)";
|
|
56
|
+
case "local":
|
|
57
|
+
return "Local (.coder/agent-memory-local/)";
|
|
58
|
+
default:
|
|
59
|
+
return "None";
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export async function loadAgentMemoryPrompt(agentType, scope) {
|
|
63
|
+
const memoryDir = getAgentMemoryDir(agentType, scope);
|
|
64
|
+
const memoryFile = getAgentMemoryEntrypoint(agentType, scope);
|
|
65
|
+
// Ensure memory directory exists
|
|
66
|
+
try {
|
|
67
|
+
await fs.mkdir(memoryDir, { recursive: true });
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
console.error(`Failed to create memory directory: ${err.message}`);
|
|
71
|
+
}
|
|
72
|
+
// Ensure MEMORY.md exists
|
|
73
|
+
let memoryContent = "";
|
|
74
|
+
try {
|
|
75
|
+
memoryContent = await fs.readFile(memoryFile, "utf-8");
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
if (err.code === "ENOENT") {
|
|
79
|
+
// Try to migrate from gemini memory file first
|
|
80
|
+
const geminiMemoryFile = path.join(scope === "user" ? os.homedir() : process.cwd(), ".gemini-agent", scope === "local" ? "agent-memory-local" : "agent-memory", sanitizeAgentTypeForPath("gemini-agent"), "MEMORY.md");
|
|
81
|
+
// Try to migrate from groq memory file second
|
|
82
|
+
const groqMemoryFile = path.join(scope === "user" ? os.homedir() : process.cwd(), ".groq-agent", scope === "local" ? "agent-memory-local" : "agent-memory", sanitizeAgentTypeForPath("groq-agent"), "MEMORY.md");
|
|
83
|
+
try {
|
|
84
|
+
memoryContent = await fs.readFile(geminiMemoryFile, "utf-8");
|
|
85
|
+
// Save to new path
|
|
86
|
+
await fs.writeFile(memoryFile, memoryContent, "utf-8");
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
try {
|
|
90
|
+
memoryContent = await fs.readFile(groqMemoryFile, "utf-8");
|
|
91
|
+
// Save to new path
|
|
92
|
+
await fs.writeFile(memoryFile, memoryContent, "utf-8");
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
// Create default template
|
|
96
|
+
const defaultTemplate = `# Persistent Agent Memory\n\nUse this section to write general rules, project setup details, style preferences, or learnings. Update this file using your write_file/patch_file tools to persist memories.\n\n## Learnings & Guidelines\n- (No memories stored yet. Add your learnings here.)\n`;
|
|
97
|
+
try {
|
|
98
|
+
await fs.writeFile(memoryFile, defaultTemplate, "utf-8");
|
|
99
|
+
memoryContent = defaultTemplate;
|
|
100
|
+
}
|
|
101
|
+
catch (writeErr) {
|
|
102
|
+
console.error(`Failed to create default memory file: ${writeErr.message}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
console.error(`Failed to read memory file: ${err.message}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
let scopeNote = "";
|
|
112
|
+
switch (scope) {
|
|
113
|
+
case "user":
|
|
114
|
+
scopeNote = "- Since this memory has 'user' scope, keep learnings general since they apply across all of the user's projects.";
|
|
115
|
+
break;
|
|
116
|
+
case "project":
|
|
117
|
+
scopeNote = "- Since this memory has 'project' scope and is shared with the team via version control, tailor memories specifically to this project.";
|
|
118
|
+
break;
|
|
119
|
+
case "local":
|
|
120
|
+
scopeNote = "- Since this memory has 'local' scope (not checked into version control), tailor memories specifically to this machine and project.";
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
return `
|
|
124
|
+
## Persistent Agent Memory
|
|
125
|
+
You have access to a persistent memory file stored at: \`${memoryFile}\`.
|
|
126
|
+
This memory is loaded at the start of every session and injected into your system prompt.
|
|
127
|
+
|
|
128
|
+
### Guidelines for Memory Usage:
|
|
129
|
+
1. Read the memory contents below to see existing learnings, project instructions, or user preferences.
|
|
130
|
+
2. If you learn something new that would be useful for future sessions (e.g., build commands, package quirks, style guidelines, API details, tool workarounds), update this file directly using your \`write_file\` or \`patch_file\` tools targeting the file path: \`${memoryFile}\`.
|
|
131
|
+
3. Keep the content concise, clean, and organized.
|
|
132
|
+
${scopeNote}
|
|
133
|
+
|
|
134
|
+
### Current Memory Contents:
|
|
135
|
+
\`\`\`markdown
|
|
136
|
+
${memoryContent}
|
|
137
|
+
\`\`\`
|
|
138
|
+
`;
|
|
139
|
+
}
|
|
140
|
+
async function getWorkspaceContext() {
|
|
141
|
+
const osPlatform = os.platform();
|
|
142
|
+
const cwd = process.cwd();
|
|
143
|
+
// Get top level files & folders
|
|
144
|
+
let topLevelStructure = "";
|
|
145
|
+
try {
|
|
146
|
+
const entries = await fs.readdir(cwd, { withFileTypes: true });
|
|
147
|
+
const folders = [];
|
|
148
|
+
const files = [];
|
|
149
|
+
for (const entry of entries) {
|
|
150
|
+
if (["node_modules", ".git", "dist", "build", ".coder", ".gemini-agent", ".groq-agent"].includes(entry.name)) {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (entry.isDirectory()) {
|
|
154
|
+
folders.push(`š ${entry.name}/`);
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
files.push(`š ${entry.name}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
topLevelStructure = [...folders, ...files].join("\n");
|
|
161
|
+
}
|
|
162
|
+
catch (err) {
|
|
163
|
+
topLevelStructure = `Error reading workspace structure: ${err.message}`;
|
|
164
|
+
}
|
|
165
|
+
// Check for package.json or key config files
|
|
166
|
+
let projectMeta = "";
|
|
167
|
+
try {
|
|
168
|
+
const packageJsonPath = path.join(cwd, "package.json");
|
|
169
|
+
const packageJsonData = await fs.readFile(packageJsonPath, "utf-8");
|
|
170
|
+
const pkg = JSON.parse(packageJsonData);
|
|
171
|
+
projectMeta += `\n- **Project Type**: Node.js / npm\n`;
|
|
172
|
+
if (pkg.name)
|
|
173
|
+
projectMeta += ` - **Name**: ${pkg.name}\n`;
|
|
174
|
+
if (pkg.version)
|
|
175
|
+
projectMeta += ` - **Version**: ${pkg.version}\n`;
|
|
176
|
+
if (pkg.scripts) {
|
|
177
|
+
projectMeta += ` - **Available Scripts**:\n` + Object.keys(pkg.scripts).map(s => ` - npm run ${s} (\`${pkg.scripts[s]}\`)`).join("\n") + "\n";
|
|
178
|
+
}
|
|
179
|
+
if (pkg.dependencies) {
|
|
180
|
+
projectMeta += ` - **Dependencies**:\n` + Object.keys(pkg.dependencies).map(d => ` - \`${d}\`: ${pkg.dependencies[d]}`).join("\n") + "\n";
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
catch (err) {
|
|
184
|
+
// Not a Node.js project or no package.json
|
|
185
|
+
}
|
|
186
|
+
// Check for python project config
|
|
187
|
+
try {
|
|
188
|
+
const reqsTxtPath = path.join(cwd, "requirements.txt");
|
|
189
|
+
await fs.stat(reqsTxtPath);
|
|
190
|
+
projectMeta += `\n- **Project Type**: Python (requirements.txt present)\n`;
|
|
191
|
+
}
|
|
192
|
+
catch { }
|
|
193
|
+
// Check for cargo
|
|
194
|
+
try {
|
|
195
|
+
const cargoTomlPath = path.join(cwd, "Cargo.toml");
|
|
196
|
+
await fs.stat(cargoTomlPath);
|
|
197
|
+
projectMeta += `\n- **Project Type**: Rust / Cargo (Cargo.toml present)\n`;
|
|
198
|
+
}
|
|
199
|
+
catch { }
|
|
200
|
+
// Check for tsconfig.json
|
|
201
|
+
let hasTsConfig = false;
|
|
202
|
+
try {
|
|
203
|
+
await fs.stat(path.join(cwd, "tsconfig.json"));
|
|
204
|
+
hasTsConfig = true;
|
|
205
|
+
}
|
|
206
|
+
catch { }
|
|
207
|
+
return `
|
|
208
|
+
## Current Workspace & System Environment Context:
|
|
209
|
+
- **Operating System Platform**: ${osPlatform}
|
|
210
|
+
- **Current Working Directory**: \`${cwd}\`
|
|
211
|
+
- **TypeScript Configured**: ${hasTsConfig ? "Yes (tsconfig.json present)" : "No"}
|
|
212
|
+
${projectMeta}
|
|
213
|
+
- **Workspace File Structure (excluding node_modules/dist/.git):**
|
|
214
|
+
\`\`\`text
|
|
215
|
+
${topLevelStructure || "(empty)"}
|
|
216
|
+
\`\`\`
|
|
217
|
+
`;
|
|
218
|
+
}
|
|
219
|
+
export class Memory {
|
|
220
|
+
messages = [];
|
|
221
|
+
maxMessages;
|
|
222
|
+
initialized = false;
|
|
223
|
+
scope = "project";
|
|
224
|
+
agentType = "coder";
|
|
225
|
+
constructor(maxMessages = 50) {
|
|
226
|
+
this.maxMessages = maxMessages;
|
|
227
|
+
this.messages.push({ role: "system", content: SYSTEM_PROMPT });
|
|
228
|
+
}
|
|
229
|
+
async init(scope = "project", agentType = "coder") {
|
|
230
|
+
if (this.initialized)
|
|
231
|
+
return;
|
|
232
|
+
this.scope = scope;
|
|
233
|
+
this.agentType = agentType;
|
|
234
|
+
const memoryPrompt = await loadAgentMemoryPrompt(agentType, scope);
|
|
235
|
+
const workspaceContext = await getWorkspaceContext();
|
|
236
|
+
this.messages[0].content = `${SYSTEM_PROMPT}\n\n${workspaceContext}\n\n${memoryPrompt}`;
|
|
237
|
+
this.initialized = true;
|
|
238
|
+
}
|
|
239
|
+
add(msg) {
|
|
240
|
+
this.messages.push(msg);
|
|
241
|
+
// Keep within token budget: always keep system prompt + last N messages
|
|
242
|
+
if (this.messages.length > this.maxMessages + 1) {
|
|
243
|
+
this.messages = [this.messages[0], ...this.messages.slice(-(this.maxMessages))];
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
getAll() {
|
|
247
|
+
return this.messages;
|
|
248
|
+
}
|
|
249
|
+
clear() {
|
|
250
|
+
this.messages = [this.messages[0]]; // keep system prompt
|
|
251
|
+
console.log(" Memory cleared.");
|
|
252
|
+
}
|
|
253
|
+
summary() {
|
|
254
|
+
const turns = this.messages.filter(m => m.role === "user").length;
|
|
255
|
+
return `${turns} turn(s) in memory (${getMemoryScopeDisplay(this.scope)} scope)`;
|
|
256
|
+
}
|
|
257
|
+
}
|
package/dist/tools.js
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import { exec } from "child_process";
|
|
2
|
+
import { promisify } from "util";
|
|
3
|
+
import * as fs from "fs/promises";
|
|
4
|
+
import * as path from "path";
|
|
5
|
+
const execAsync = promisify(exec);
|
|
6
|
+
// āāā Tool Definitions (sent to Groq) āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
7
|
+
export const TOOL_DEFINITIONS = [
|
|
8
|
+
{
|
|
9
|
+
type: "function",
|
|
10
|
+
function: {
|
|
11
|
+
name: "read_file",
|
|
12
|
+
description: "Read the contents of a file from disk",
|
|
13
|
+
parameters: {
|
|
14
|
+
type: "object",
|
|
15
|
+
properties: {
|
|
16
|
+
file_path: { type: "string", description: "Absolute or relative path to the file" },
|
|
17
|
+
},
|
|
18
|
+
required: ["file_path"],
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
type: "function",
|
|
24
|
+
function: {
|
|
25
|
+
name: "write_file",
|
|
26
|
+
description: "Write or overwrite a file on disk",
|
|
27
|
+
parameters: {
|
|
28
|
+
type: "object",
|
|
29
|
+
properties: {
|
|
30
|
+
file_path: { type: "string", description: "Path to write to" },
|
|
31
|
+
content: { type: "string", description: "Content to write" },
|
|
32
|
+
},
|
|
33
|
+
required: ["file_path", "content"],
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
type: "function",
|
|
39
|
+
function: {
|
|
40
|
+
name: "list_directory",
|
|
41
|
+
description: "List files and folders in a directory",
|
|
42
|
+
parameters: {
|
|
43
|
+
type: "object",
|
|
44
|
+
properties: {
|
|
45
|
+
dir_path: { type: "string", description: "Directory path (default: current dir)" },
|
|
46
|
+
},
|
|
47
|
+
required: [],
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
type: "function",
|
|
53
|
+
function: {
|
|
54
|
+
name: "run_shell",
|
|
55
|
+
description: "Execute a shell command and return stdout/stderr. Use for running code, installing packages, git, etc.",
|
|
56
|
+
parameters: {
|
|
57
|
+
type: "object",
|
|
58
|
+
properties: {
|
|
59
|
+
command: { type: "string", description: "Shell command to run" },
|
|
60
|
+
cwd: { type: "string", description: "Working directory (optional). Must be a valid, existing directory. Do not use placeholders like 'current directory'." },
|
|
61
|
+
},
|
|
62
|
+
required: ["command"],
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
type: "function",
|
|
68
|
+
function: {
|
|
69
|
+
name: "web_search",
|
|
70
|
+
description: "Search the web using DuckDuckGo instant answers API. Good for docs, errors, packages.",
|
|
71
|
+
parameters: {
|
|
72
|
+
type: "object",
|
|
73
|
+
properties: {
|
|
74
|
+
query: { type: "string", description: "Search query" },
|
|
75
|
+
},
|
|
76
|
+
required: ["query"],
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
type: "function",
|
|
82
|
+
function: {
|
|
83
|
+
name: "find_files",
|
|
84
|
+
description: "Recursively search for files in the project directory matching a search term or pattern. Excludes standard build/git/dependency directories.",
|
|
85
|
+
parameters: {
|
|
86
|
+
type: "object",
|
|
87
|
+
properties: {
|
|
88
|
+
query: { type: "string", description: "Search query or pattern to match filenames (e.g. 'index' or 'agent.ts')" },
|
|
89
|
+
dir_path: { type: "string", description: "Directory to start searching from (default: current dir)" }
|
|
90
|
+
},
|
|
91
|
+
required: ["query"]
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
type: "function",
|
|
97
|
+
function: {
|
|
98
|
+
name: "read_file_lines",
|
|
99
|
+
description: "Read a specific range of lines from a file on disk. Use this instead of read_file for large files to save token context.",
|
|
100
|
+
parameters: {
|
|
101
|
+
type: "object",
|
|
102
|
+
properties: {
|
|
103
|
+
file_path: { type: "string", description: "Absolute or relative path to the file" },
|
|
104
|
+
start_line: { type: "number", description: "First line to read (1-indexed, inclusive)" },
|
|
105
|
+
end_line: { type: "number", description: "Last line to read (1-indexed, inclusive)" }
|
|
106
|
+
},
|
|
107
|
+
required: ["file_path", "start_line", "end_line"]
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
type: "function",
|
|
113
|
+
function: {
|
|
114
|
+
name: "search_grep",
|
|
115
|
+
description: "Search for a string pattern or regex across all project files recursively. Excludes dependencies and build directories.",
|
|
116
|
+
parameters: {
|
|
117
|
+
type: "object",
|
|
118
|
+
properties: {
|
|
119
|
+
query: { type: "string", description: "Search query string or regex pattern to search for" },
|
|
120
|
+
is_regex: { type: "boolean", description: "Whether to treat query as a regular expression (default: false)" }
|
|
121
|
+
},
|
|
122
|
+
required: ["query"]
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
type: "function",
|
|
128
|
+
function: {
|
|
129
|
+
name: "patch_file",
|
|
130
|
+
description: "Replace a specific contiguous block of text in a file with new code. This is much faster and safer than write_file for editing existing files.",
|
|
131
|
+
parameters: {
|
|
132
|
+
type: "object",
|
|
133
|
+
properties: {
|
|
134
|
+
file_path: { type: "string", description: "Path to the file to modify" },
|
|
135
|
+
target_code: { type: "string", description: "The exact block of code to find and replace. Must match exactly (including spacing)." },
|
|
136
|
+
replacement_code: { type: "string", description: "The replacement block of code" }
|
|
137
|
+
},
|
|
138
|
+
required: ["file_path", "target_code", "replacement_code"]
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
];
|
|
143
|
+
// āāā Tool Implementations āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
144
|
+
export async function read_file({ file_path }) {
|
|
145
|
+
try {
|
|
146
|
+
const content = await fs.readFile(file_path, "utf-8");
|
|
147
|
+
return content;
|
|
148
|
+
}
|
|
149
|
+
catch (e) {
|
|
150
|
+
return `ERROR: ${e.message}`;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
export async function write_file({ file_path, content }) {
|
|
154
|
+
try {
|
|
155
|
+
await fs.mkdir(path.dirname(file_path), { recursive: true });
|
|
156
|
+
await fs.writeFile(file_path, content, "utf-8");
|
|
157
|
+
return `ā Written ${content.length} chars to ${file_path}`;
|
|
158
|
+
}
|
|
159
|
+
catch (e) {
|
|
160
|
+
return `ERROR: ${e.message}`;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
export async function list_directory({ dir_path = "." }) {
|
|
164
|
+
try {
|
|
165
|
+
const entries = await fs.readdir(dir_path, { withFileTypes: true });
|
|
166
|
+
const lines = entries.map((e) => `${e.isDirectory() ? "š" : "š"} ${e.name}`);
|
|
167
|
+
return lines.join("\n") || "(empty)";
|
|
168
|
+
}
|
|
169
|
+
catch (e) {
|
|
170
|
+
return `ERROR: ${e.message}`;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
export async function run_shell({ command, cwd }) {
|
|
174
|
+
try {
|
|
175
|
+
let targetCwd = process.cwd();
|
|
176
|
+
if (cwd) {
|
|
177
|
+
try {
|
|
178
|
+
const stats = await fs.stat(cwd);
|
|
179
|
+
if (!stats.isDirectory()) {
|
|
180
|
+
return `ERROR: The specified working directory (cwd) "${cwd}" is a file, not a directory.`;
|
|
181
|
+
}
|
|
182
|
+
targetCwd = path.resolve(cwd);
|
|
183
|
+
}
|
|
184
|
+
catch (err) {
|
|
185
|
+
return `ERROR: The specified working directory (cwd) "${cwd}" does not exist. Please specify a valid, existing directory path or omit 'cwd'.`;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const { stdout, stderr } = await execAsync(command, {
|
|
189
|
+
cwd: targetCwd,
|
|
190
|
+
timeout: 30_000,
|
|
191
|
+
});
|
|
192
|
+
const out = [stdout.trim(), stderr.trim()].filter(Boolean).join("\n--- stderr ---\n");
|
|
193
|
+
return out || "(no output)";
|
|
194
|
+
}
|
|
195
|
+
catch (e) {
|
|
196
|
+
return `EXIT ${e.code ?? "?"}: ${e.stderr || e.message}`;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
export async function web_search({ query }) {
|
|
200
|
+
try {
|
|
201
|
+
const url = `https://api.duckduckgo.com/?q=${encodeURIComponent(query)}&format=json&no_html=1&skip_disambig=1`;
|
|
202
|
+
const res = await fetch(url);
|
|
203
|
+
const data = (await res.json());
|
|
204
|
+
const parts = [];
|
|
205
|
+
if (data.AbstractText)
|
|
206
|
+
parts.push(`š ${data.AbstractText}`);
|
|
207
|
+
if (data.Answer)
|
|
208
|
+
parts.push(`ā” ${data.Answer}`);
|
|
209
|
+
if (data.RelatedTopics?.length) {
|
|
210
|
+
const topics = data.RelatedTopics
|
|
211
|
+
.slice(0, 4)
|
|
212
|
+
.filter((t) => t.Text)
|
|
213
|
+
.map((t) => `⢠${t.Text}`);
|
|
214
|
+
if (topics.length)
|
|
215
|
+
parts.push("Related:\n" + topics.join("\n"));
|
|
216
|
+
}
|
|
217
|
+
return parts.length ? parts.join("\n\n") : `No instant answer found for: "${query}". Try rephrasing.`;
|
|
218
|
+
}
|
|
219
|
+
catch (e) {
|
|
220
|
+
return `Search error: ${e.message}`;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
async function walkDir(dir, fileList = []) {
|
|
224
|
+
try {
|
|
225
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
226
|
+
for (const entry of entries) {
|
|
227
|
+
const fullPath = path.join(dir, entry.name);
|
|
228
|
+
if (["node_modules", ".git", "dist", "build", "__pycache__", ".next", "out"].includes(entry.name)) {
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
if (entry.isDirectory()) {
|
|
232
|
+
await walkDir(fullPath, fileList);
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
fileList.push(fullPath);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
catch { }
|
|
240
|
+
return fileList;
|
|
241
|
+
}
|
|
242
|
+
export async function find_files({ query, dir_path = "." }) {
|
|
243
|
+
try {
|
|
244
|
+
const allFiles = await walkDir(dir_path);
|
|
245
|
+
const lowercaseQuery = query.toLowerCase();
|
|
246
|
+
const matches = allFiles
|
|
247
|
+
.map(f => path.relative(dir_path, f))
|
|
248
|
+
.filter(f => f.toLowerCase().includes(lowercaseQuery));
|
|
249
|
+
if (matches.length === 0) {
|
|
250
|
+
return `No files found matching: "${query}"`;
|
|
251
|
+
}
|
|
252
|
+
return matches.slice(0, 50).join("\n") + (matches.length > 50 ? `\n⦠(and ${matches.length - 50} more files)` : "");
|
|
253
|
+
}
|
|
254
|
+
catch (e) {
|
|
255
|
+
return `ERROR: ${e.message}`;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
export async function read_file_lines({ file_path, start_line, end_line }) {
|
|
259
|
+
try {
|
|
260
|
+
const content = await fs.readFile(file_path, "utf-8");
|
|
261
|
+
const lines = content.split(/\r?\n/);
|
|
262
|
+
const totalLines = lines.length;
|
|
263
|
+
const start = Math.max(1, start_line);
|
|
264
|
+
const end = Math.min(totalLines, end_line);
|
|
265
|
+
if (start > totalLines || start > end) {
|
|
266
|
+
return `ERROR: Invalid line range ${start_line}-${end_line}. Total lines in file: ${totalLines}.`;
|
|
267
|
+
}
|
|
268
|
+
const slice = lines.slice(start - 1, end);
|
|
269
|
+
const numberedLines = slice.map((line, idx) => `${start + idx}: ${line}`);
|
|
270
|
+
return `[Showing lines ${start}-${end} of ${totalLines} in ${file_path}]\n` + numberedLines.join("\n");
|
|
271
|
+
}
|
|
272
|
+
catch (e) {
|
|
273
|
+
return `ERROR: ${e.message}`;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
export async function search_grep({ query, is_regex = false }) {
|
|
277
|
+
try {
|
|
278
|
+
const allFiles = await walkDir(".");
|
|
279
|
+
const matches = [];
|
|
280
|
+
let regex;
|
|
281
|
+
if (is_regex) {
|
|
282
|
+
regex = new RegExp(query, "i");
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
const escaped = query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
286
|
+
regex = new RegExp(escaped, "i");
|
|
287
|
+
}
|
|
288
|
+
for (const filePath of allFiles) {
|
|
289
|
+
try {
|
|
290
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
291
|
+
const lines = content.split(/\r?\n/);
|
|
292
|
+
for (let i = 0; i < lines.length; i++) {
|
|
293
|
+
if (regex.test(lines[i])) {
|
|
294
|
+
const relPath = path.relative(".", filePath);
|
|
295
|
+
matches.push(`${relPath}:${i + 1}: ${lines[i].trim()}`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
catch { }
|
|
300
|
+
}
|
|
301
|
+
if (matches.length === 0) {
|
|
302
|
+
return `No matches found for query: "${query}"`;
|
|
303
|
+
}
|
|
304
|
+
return matches.slice(0, 50).join("\n") + (matches.length > 50 ? `\n⦠(and ${matches.length - 50} more occurrences)` : "");
|
|
305
|
+
}
|
|
306
|
+
catch (e) {
|
|
307
|
+
return `ERROR: ${e.message}`;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
export async function patch_file({ file_path, target_code, replacement_code }) {
|
|
311
|
+
try {
|
|
312
|
+
const content = await fs.readFile(file_path, "utf-8");
|
|
313
|
+
if (!content.includes(target_code)) {
|
|
314
|
+
return `ERROR: Target code not found in file ${file_path}. Please verify target content.`;
|
|
315
|
+
}
|
|
316
|
+
const occurrences = content.split(target_code).length - 1;
|
|
317
|
+
if (occurrences > 1) {
|
|
318
|
+
return `ERROR: Target code matches ${occurrences} times in file ${file_path}. Please provide more unique context to target the exact edit.`;
|
|
319
|
+
}
|
|
320
|
+
const newContent = content.replace(target_code, replacement_code);
|
|
321
|
+
await fs.writeFile(file_path, newContent, "utf-8");
|
|
322
|
+
return `ā Patched file ${file_path} successfully.`;
|
|
323
|
+
}
|
|
324
|
+
catch (e) {
|
|
325
|
+
return `ERROR: ${e.message}`;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
// āāā Dispatcher āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
329
|
+
export async function dispatchTool(name, args) {
|
|
330
|
+
switch (name) {
|
|
331
|
+
case "read_file": return read_file(args);
|
|
332
|
+
case "write_file": return write_file(args);
|
|
333
|
+
case "list_directory": return list_directory(args);
|
|
334
|
+
case "run_shell": return run_shell(args);
|
|
335
|
+
case "web_search": return web_search(args);
|
|
336
|
+
case "find_files": return find_files(args);
|
|
337
|
+
case "read_file_lines": return read_file_lines(args);
|
|
338
|
+
case "search_grep": return search_grep(args);
|
|
339
|
+
case "patch_file": return patch_file(args);
|
|
340
|
+
default: return `Unknown tool: ${name}`;
|
|
341
|
+
}
|
|
342
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "coder-agent",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "CLI coding agent powered by Google Gemini",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"coder": "dist/index.js",
|
|
9
|
+
"gemini-agent": "dist/index.js",
|
|
10
|
+
"groq-agent": "dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist"
|
|
14
|
+
],
|
|
15
|
+
"preferGlobal": true,
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"keywords": [
|
|
18
|
+
"gemini",
|
|
19
|
+
"cli",
|
|
20
|
+
"agent",
|
|
21
|
+
"coding-agent",
|
|
22
|
+
"ai",
|
|
23
|
+
"coder"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"start": "tsx src/index.ts",
|
|
27
|
+
"dev": "tsx watch src/index.ts",
|
|
28
|
+
"build": "tsc && node -e \"const fs = require('fs'); const f = 'dist/index.js'; let c = fs.readFileSync(f, 'utf8'); if (!c.startsWith('#!/usr/bin/env node')) c = '#!/usr/bin/env node\\n' + c; fs.writeFileSync(f, c.replace(/\\r\\n/g, '\\n'), 'utf8')\"",
|
|
29
|
+
"prepublishOnly": "npm run build",
|
|
30
|
+
"start:build": "node dist/index.js"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"chalk": "^5.3.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/node": "^20.0.0",
|
|
37
|
+
"tsx": "^4.7.0",
|
|
38
|
+
"typescript": "^5.4.0"
|
|
39
|
+
}
|
|
40
|
+
}
|