@devang0907/agent-dev 0.1.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/README.md +58 -0
- package/dist/agent/loop.d.ts +32 -0
- package/dist/agent/loop.js +83 -0
- package/dist/agent/session.d.ts +33 -0
- package/dist/agent/session.js +107 -0
- package/dist/agent/tools/index.d.ts +8 -0
- package/dist/agent/tools/index.js +21 -0
- package/dist/agent/tools/read.d.ts +20 -0
- package/dist/agent/tools/read.js +108 -0
- package/dist/cli/args.d.ts +9 -0
- package/dist/cli/args.js +60 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +6 -0
- package/dist/config/models.d.ts +6 -0
- package/dist/config/models.js +43 -0
- package/dist/config/paths.d.ts +4 -0
- package/dist/config/paths.js +6 -0
- package/dist/config/settings.d.ts +12 -0
- package/dist/config/settings.js +29 -0
- package/dist/main.d.ts +1 -0
- package/dist/main.js +45 -0
- package/dist/modes/print-mode.d.ts +2 -0
- package/dist/modes/print-mode.js +32 -0
- package/dist/providers/gemini.d.ts +10 -0
- package/dist/providers/gemini.js +116 -0
- package/dist/providers/groq.d.ts +10 -0
- package/dist/providers/groq.js +113 -0
- package/dist/providers/openai.d.ts +10 -0
- package/dist/providers/openai.js +121 -0
- package/dist/providers/openrouter-free.d.ts +10 -0
- package/dist/providers/openrouter-free.js +133 -0
- package/dist/providers/registry.d.ts +7 -0
- package/dist/providers/registry.js +39 -0
- package/dist/providers/types.d.ts +60 -0
- package/dist/providers/types.js +1 -0
- package/dist/session/manager.d.ts +25 -0
- package/dist/session/manager.js +85 -0
- package/dist/ui/App.d.ts +14 -0
- package/dist/ui/App.js +131 -0
- package/dist/ui/ChatView.d.ts +10 -0
- package/dist/ui/ChatView.js +5 -0
- package/dist/ui/Editor.d.ts +10 -0
- package/dist/ui/Editor.js +35 -0
- package/dist/ui/Footer.d.ts +11 -0
- package/dist/ui/Footer.js +6 -0
- package/dist/ui/ModelSelector.d.ts +13 -0
- package/dist/ui/ModelSelector.js +42 -0
- package/dist/ui/SettingsView.d.ts +11 -0
- package/dist/ui/SettingsView.js +37 -0
- package/dist/ui/theme.d.ts +13 -0
- package/dist/ui/theme.js +25 -0
- package/package.json +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# agent-dev
|
|
2
|
+
|
|
3
|
+
A minimal pi-like terminal coding agent with an Ink UI. Chat with an AI that can read, write, edit files, and run bash commands.
|
|
4
|
+
|
|
5
|
+
## Quick start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install
|
|
9
|
+
npm run dev
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Set at least one API key:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
export OPENROUTER_API_KEY=sk-or-... # Free models (default)
|
|
16
|
+
export OPENAI_API_KEY=sk-... # ChatGPT
|
|
17
|
+
export GROQ_API_KEY=gsk_... # Groq
|
|
18
|
+
export GEMINI_API_KEY=... # Google Gemini
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Providers
|
|
22
|
+
|
|
23
|
+
| Provider | Env var | Example models |
|
|
24
|
+
|----------|---------|----------------|
|
|
25
|
+
| OpenAI (ChatGPT) | `OPENAI_API_KEY` | `gpt-4o`, `gpt-4o-mini` |
|
|
26
|
+
| Groq | `GROQ_API_KEY` | `llama-3.3-70b-versatile` |
|
|
27
|
+
| Google Gemini | `GEMINI_API_KEY` or `GOOGLE_API_KEY` | `gemini-2.0-flash` |
|
|
28
|
+
| Free (OpenRouter) | `OPENROUTER_API_KEY` | `meta-llama/llama-3.3-70b-instruct:free` |
|
|
29
|
+
|
|
30
|
+
## Interactive commands
|
|
31
|
+
|
|
32
|
+
| Command | Description |
|
|
33
|
+
|---------|-------------|
|
|
34
|
+
| `/model` | Open model selector (grouped by provider) |
|
|
35
|
+
| `/model groq` | Open selector filtered by search |
|
|
36
|
+
| `/settings` | Thinking level, theme, API key status |
|
|
37
|
+
| `/new` | Clear session |
|
|
38
|
+
| `/quit` | Exit |
|
|
39
|
+
|
|
40
|
+
## CLI
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npm run dev # Interactive
|
|
44
|
+
npm run dev -- -p "List files" # Print mode
|
|
45
|
+
npm run dev -- -c # Continue last session
|
|
46
|
+
npm run dev -- --model groq/llama-3.3-70b-versatile "hello"
|
|
47
|
+
npm run build && npm start
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Config and sessions are stored in `~/.agent-dev/`.
|
|
51
|
+
|
|
52
|
+
## Tools
|
|
53
|
+
|
|
54
|
+
The agent has four built-in tools: `read`, `write`, `edit`, `bash`. File operations are restricted to the current working directory.
|
|
55
|
+
|
|
56
|
+
## License
|
|
57
|
+
|
|
58
|
+
MIT
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ChatMessage, Model, ToolCall } from "../providers/types.js";
|
|
2
|
+
import type { Settings } from "../config/settings.js";
|
|
3
|
+
export type AgentEvent = {
|
|
4
|
+
type: "message_start";
|
|
5
|
+
role: "assistant";
|
|
6
|
+
} | {
|
|
7
|
+
type: "text_delta";
|
|
8
|
+
delta: string;
|
|
9
|
+
} | {
|
|
10
|
+
type: "tool_call";
|
|
11
|
+
toolCall: ToolCall;
|
|
12
|
+
} | {
|
|
13
|
+
type: "tool_result";
|
|
14
|
+
toolCallId: string;
|
|
15
|
+
name: string;
|
|
16
|
+
result: string;
|
|
17
|
+
} | {
|
|
18
|
+
type: "turn_end";
|
|
19
|
+
} | {
|
|
20
|
+
type: "error";
|
|
21
|
+
message: string;
|
|
22
|
+
};
|
|
23
|
+
export interface AgentLoopOptions {
|
|
24
|
+
model: Model;
|
|
25
|
+
messages: ChatMessage[];
|
|
26
|
+
settings: Settings;
|
|
27
|
+
workdir: string;
|
|
28
|
+
systemPrompt?: string;
|
|
29
|
+
signal?: AbortSignal;
|
|
30
|
+
onEvent: (event: AgentEvent) => void;
|
|
31
|
+
}
|
|
32
|
+
export declare function runAgentLoop(options: AgentLoopOptions): Promise<ChatMessage[]>;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { streamChat } from "../providers/registry.js";
|
|
2
|
+
import { getToolDefinitions, executeTool } from "./tools/index.js";
|
|
3
|
+
const DEFAULT_SYSTEM_PROMPT = `You are a helpful coding assistant with access to tools: read, write, edit, and bash.
|
|
4
|
+
Use tools to inspect and modify the codebase. Be concise and accurate.
|
|
5
|
+
Working directory: ${process.cwd()}`;
|
|
6
|
+
async function collectStream(model, messages, settings, systemPrompt, signal, onEvent) {
|
|
7
|
+
const tools = getToolDefinitions();
|
|
8
|
+
let content = "";
|
|
9
|
+
const toolCallMap = new Map();
|
|
10
|
+
const stream = streamChat(model, {
|
|
11
|
+
messages,
|
|
12
|
+
tools,
|
|
13
|
+
systemPrompt,
|
|
14
|
+
thinkingLevel: settings.thinkingLevel,
|
|
15
|
+
signal,
|
|
16
|
+
});
|
|
17
|
+
for await (const event of stream) {
|
|
18
|
+
if (event.type === "text_delta") {
|
|
19
|
+
content += event.delta;
|
|
20
|
+
onEvent?.({ type: "text_delta", delta: event.delta });
|
|
21
|
+
}
|
|
22
|
+
else if (event.type === "tool_call_delta") {
|
|
23
|
+
if (!toolCallMap.has(event.index)) {
|
|
24
|
+
toolCallMap.set(event.index, { id: event.id ?? "", name: event.name ?? "", arguments: "" });
|
|
25
|
+
}
|
|
26
|
+
const tc = toolCallMap.get(event.index);
|
|
27
|
+
if (event.id)
|
|
28
|
+
tc.id = event.id;
|
|
29
|
+
if (event.name)
|
|
30
|
+
tc.name = event.name;
|
|
31
|
+
if (event.argumentsDelta)
|
|
32
|
+
tc.arguments += event.argumentsDelta;
|
|
33
|
+
}
|
|
34
|
+
else if (event.type === "error") {
|
|
35
|
+
return { content, toolCalls: [], error: event.message };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const toolCalls = Array.from(toolCallMap.values()).filter((tc) => tc.name);
|
|
39
|
+
return { content, toolCalls };
|
|
40
|
+
}
|
|
41
|
+
export async function runAgentLoop(options) {
|
|
42
|
+
const { model, messages, settings, workdir, systemPrompt = DEFAULT_SYSTEM_PROMPT, signal, onEvent, } = options;
|
|
43
|
+
const context = [...messages];
|
|
44
|
+
while (true) {
|
|
45
|
+
if (signal?.aborted)
|
|
46
|
+
break;
|
|
47
|
+
onEvent({ type: "message_start", role: "assistant" });
|
|
48
|
+
const { content, toolCalls, error } = await collectStream(model, context, settings, systemPrompt, signal, onEvent);
|
|
49
|
+
if (error) {
|
|
50
|
+
onEvent({ type: "error", message: error });
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
const assistantMsg = {
|
|
54
|
+
role: "assistant",
|
|
55
|
+
content,
|
|
56
|
+
toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
|
|
57
|
+
};
|
|
58
|
+
context.push(assistantMsg);
|
|
59
|
+
if (toolCalls.length === 0) {
|
|
60
|
+
onEvent({ type: "turn_end" });
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
for (const tc of toolCalls) {
|
|
64
|
+
onEvent({ type: "tool_call", toolCall: tc });
|
|
65
|
+
let args = {};
|
|
66
|
+
try {
|
|
67
|
+
args = JSON.parse(tc.arguments || "{}");
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
args = {};
|
|
71
|
+
}
|
|
72
|
+
const result = await executeTool(tc.name, args, workdir);
|
|
73
|
+
onEvent({ type: "tool_result", toolCallId: tc.id, name: tc.name, result });
|
|
74
|
+
context.push({
|
|
75
|
+
role: "tool",
|
|
76
|
+
content: result,
|
|
77
|
+
toolCallId: tc.id,
|
|
78
|
+
name: tc.name,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return context.slice(messages.length);
|
|
83
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import type { ChatMessage, Model } from "../providers/types.js";
|
|
3
|
+
import type { Settings } from "../config/settings.js";
|
|
4
|
+
import { type AgentEvent } from "./loop.js";
|
|
5
|
+
import { SessionManager } from "../session/manager.js";
|
|
6
|
+
export type SessionEvent = AgentEvent | {
|
|
7
|
+
type: "user_message";
|
|
8
|
+
content: string;
|
|
9
|
+
} | {
|
|
10
|
+
type: "model_changed";
|
|
11
|
+
model: Model;
|
|
12
|
+
};
|
|
13
|
+
export declare class AgentSession extends EventEmitter {
|
|
14
|
+
private messages;
|
|
15
|
+
private model;
|
|
16
|
+
private settings;
|
|
17
|
+
private workdir;
|
|
18
|
+
private sessionManager;
|
|
19
|
+
private abortController?;
|
|
20
|
+
private running;
|
|
21
|
+
constructor(settings: Settings, sessionManager: SessionManager, workdir: string, initialModel?: Model);
|
|
22
|
+
getModel(): Model;
|
|
23
|
+
getSettings(): Settings;
|
|
24
|
+
getMessages(): ChatMessage[];
|
|
25
|
+
isRunning(): boolean;
|
|
26
|
+
setModel(model: Model): void;
|
|
27
|
+
updateSettings(settings: Settings): void;
|
|
28
|
+
abort(): void;
|
|
29
|
+
prompt(content: string): Promise<void>;
|
|
30
|
+
newSession(): void;
|
|
31
|
+
getAvailableModels(): Model[];
|
|
32
|
+
static resolveInitialModel(settings: Settings, modelRef?: string): Model | undefined;
|
|
33
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import { findModel } from "../config/models.js";
|
|
3
|
+
import { setDefaultModel, saveSettings } from "../config/settings.js";
|
|
4
|
+
import { getAvailableModels, getDefaultModelForProvider } from "../providers/registry.js";
|
|
5
|
+
import { runAgentLoop } from "./loop.js";
|
|
6
|
+
export class AgentSession extends EventEmitter {
|
|
7
|
+
messages = [];
|
|
8
|
+
model;
|
|
9
|
+
settings;
|
|
10
|
+
workdir;
|
|
11
|
+
sessionManager;
|
|
12
|
+
abortController;
|
|
13
|
+
running = false;
|
|
14
|
+
constructor(settings, sessionManager, workdir, initialModel) {
|
|
15
|
+
super();
|
|
16
|
+
this.settings = settings;
|
|
17
|
+
this.sessionManager = sessionManager;
|
|
18
|
+
this.workdir = workdir;
|
|
19
|
+
this.messages = sessionManager.getMessages();
|
|
20
|
+
const available = getAvailableModels(settings);
|
|
21
|
+
const fromSettings = findModel(settings.defaultProvider, settings.defaultModel);
|
|
22
|
+
this.model =
|
|
23
|
+
initialModel ??
|
|
24
|
+
(fromSettings && available.some((m) => m.provider === fromSettings.provider && m.id === fromSettings.id)
|
|
25
|
+
? fromSettings
|
|
26
|
+
: available[0] ?? findModel("free", "meta-llama/llama-3.3-70b-instruct:free"));
|
|
27
|
+
}
|
|
28
|
+
getModel() {
|
|
29
|
+
return this.model;
|
|
30
|
+
}
|
|
31
|
+
getSettings() {
|
|
32
|
+
return this.settings;
|
|
33
|
+
}
|
|
34
|
+
getMessages() {
|
|
35
|
+
return [...this.messages];
|
|
36
|
+
}
|
|
37
|
+
isRunning() {
|
|
38
|
+
return this.running;
|
|
39
|
+
}
|
|
40
|
+
setModel(model) {
|
|
41
|
+
this.model = model;
|
|
42
|
+
this.settings = setDefaultModel(this.settings, model.provider, model.id);
|
|
43
|
+
this.sessionManager.appendModelChange(model);
|
|
44
|
+
this.emit("event", { type: "model_changed", model });
|
|
45
|
+
}
|
|
46
|
+
updateSettings(settings) {
|
|
47
|
+
this.settings = settings;
|
|
48
|
+
saveSettings(settings);
|
|
49
|
+
}
|
|
50
|
+
abort() {
|
|
51
|
+
this.abortController?.abort();
|
|
52
|
+
}
|
|
53
|
+
async prompt(content) {
|
|
54
|
+
if (this.running)
|
|
55
|
+
return;
|
|
56
|
+
this.running = true;
|
|
57
|
+
const userMsg = { role: "user", content };
|
|
58
|
+
this.messages.push(userMsg);
|
|
59
|
+
this.sessionManager.appendMessage(userMsg);
|
|
60
|
+
this.emit("event", { type: "user_message", content });
|
|
61
|
+
this.abortController = new AbortController();
|
|
62
|
+
try {
|
|
63
|
+
const newMessages = await runAgentLoop({
|
|
64
|
+
model: this.model,
|
|
65
|
+
messages: [...this.messages],
|
|
66
|
+
settings: this.settings,
|
|
67
|
+
workdir: this.workdir,
|
|
68
|
+
signal: this.abortController.signal,
|
|
69
|
+
onEvent: (event) => this.emit("event", event),
|
|
70
|
+
});
|
|
71
|
+
for (const msg of newMessages) {
|
|
72
|
+
if (msg.role !== "user") {
|
|
73
|
+
this.messages.push(msg);
|
|
74
|
+
this.sessionManager.appendMessage(msg);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
this.running = false;
|
|
80
|
+
this.abortController = undefined;
|
|
81
|
+
this.sessionManager.saveAsLast();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
newSession() {
|
|
85
|
+
this.messages = [];
|
|
86
|
+
this.sessionManager.clear();
|
|
87
|
+
}
|
|
88
|
+
getAvailableModels() {
|
|
89
|
+
return getAvailableModels(this.settings);
|
|
90
|
+
}
|
|
91
|
+
static resolveInitialModel(settings, modelRef) {
|
|
92
|
+
if (modelRef) {
|
|
93
|
+
const slash = modelRef.indexOf("/");
|
|
94
|
+
if (slash > 0) {
|
|
95
|
+
const provider = modelRef.slice(0, slash);
|
|
96
|
+
const id = modelRef.slice(slash + 1);
|
|
97
|
+
return findModel(provider, id);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const available = getAvailableModels(settings);
|
|
101
|
+
const fromSettings = findModel(settings.defaultProvider, settings.defaultModel);
|
|
102
|
+
if (fromSettings && available.some((m) => m.provider === fromSettings.provider && m.id === fromSettings.id)) {
|
|
103
|
+
return fromSettings;
|
|
104
|
+
}
|
|
105
|
+
return available[0] ?? getDefaultModelForProvider(settings.defaultProvider);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ToolDefinition } from "../../providers/types.js";
|
|
2
|
+
export interface AgentTool {
|
|
3
|
+
definition: ToolDefinition;
|
|
4
|
+
execute: (args: Record<string, unknown>, workdir: string) => Promise<string>;
|
|
5
|
+
}
|
|
6
|
+
export declare const BUILTIN_TOOLS: AgentTool[];
|
|
7
|
+
export declare function getToolDefinitions(): ToolDefinition[];
|
|
8
|
+
export declare function executeTool(name: string, args: Record<string, unknown>, workdir: string): Promise<string>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { readTool, writeTool, editTool, bashTool, executeRead, executeWrite, executeEdit, executeBash, } from "./read.js";
|
|
2
|
+
export const BUILTIN_TOOLS = [
|
|
3
|
+
{ definition: readTool, execute: (args, wd) => executeRead(args, wd) },
|
|
4
|
+
{ definition: writeTool, execute: (args, wd) => executeWrite(args, wd) },
|
|
5
|
+
{ definition: editTool, execute: (args, wd) => executeEdit(args, wd) },
|
|
6
|
+
{ definition: bashTool, execute: (args, wd) => executeBash(args, wd) },
|
|
7
|
+
];
|
|
8
|
+
export function getToolDefinitions() {
|
|
9
|
+
return BUILTIN_TOOLS.map((t) => t.definition);
|
|
10
|
+
}
|
|
11
|
+
export async function executeTool(name, args, workdir) {
|
|
12
|
+
const tool = BUILTIN_TOOLS.find((t) => t.definition.name === name);
|
|
13
|
+
if (!tool)
|
|
14
|
+
return `Error: unknown tool ${name}`;
|
|
15
|
+
try {
|
|
16
|
+
return await tool.execute(args, workdir);
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
return `Error: ${err instanceof Error ? err.message : String(err)}`;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ToolDefinition } from "../../providers/types.js";
|
|
2
|
+
export declare const readTool: ToolDefinition;
|
|
3
|
+
export declare function executeRead(args: {
|
|
4
|
+
path: string;
|
|
5
|
+
}, workdir?: string): Promise<string>;
|
|
6
|
+
export declare const writeTool: ToolDefinition;
|
|
7
|
+
export declare function executeWrite(args: {
|
|
8
|
+
path: string;
|
|
9
|
+
content: string;
|
|
10
|
+
}, workdir?: string): Promise<string>;
|
|
11
|
+
export declare const editTool: ToolDefinition;
|
|
12
|
+
export declare function executeEdit(args: {
|
|
13
|
+
path: string;
|
|
14
|
+
old_string: string;
|
|
15
|
+
new_string: string;
|
|
16
|
+
}, workdir?: string): Promise<string>;
|
|
17
|
+
export declare const bashTool: ToolDefinition;
|
|
18
|
+
export declare function executeBash(args: {
|
|
19
|
+
command: string;
|
|
20
|
+
}, workdir?: string): Promise<string>;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
2
|
+
import { resolve, isAbsolute } from "node:path";
|
|
3
|
+
const DEFAULT_WORKDIR = process.cwd();
|
|
4
|
+
function resolvePath(path, workdir = DEFAULT_WORKDIR) {
|
|
5
|
+
return isAbsolute(path) ? path : resolve(workdir, path);
|
|
6
|
+
}
|
|
7
|
+
function assertWithinWorkdir(path, workdir = DEFAULT_WORKDIR) {
|
|
8
|
+
const resolved = resolve(path);
|
|
9
|
+
const root = resolve(workdir);
|
|
10
|
+
if (!resolved.startsWith(root)) {
|
|
11
|
+
throw new Error(`Path outside working directory: ${path}`);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export const readTool = {
|
|
15
|
+
name: "read",
|
|
16
|
+
description: "Read the contents of a file",
|
|
17
|
+
parameters: {
|
|
18
|
+
type: "object",
|
|
19
|
+
properties: {
|
|
20
|
+
path: { type: "string", description: "File path relative to project root" },
|
|
21
|
+
},
|
|
22
|
+
required: ["path"],
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
export async function executeRead(args, workdir = DEFAULT_WORKDIR) {
|
|
26
|
+
const filePath = resolvePath(args.path, workdir);
|
|
27
|
+
assertWithinWorkdir(filePath, workdir);
|
|
28
|
+
if (!existsSync(filePath)) {
|
|
29
|
+
return `Error: file not found: ${args.path}`;
|
|
30
|
+
}
|
|
31
|
+
const content = readFileSync(filePath, "utf-8");
|
|
32
|
+
return content.length > 50000 ? content.slice(0, 50000) + "\n... (truncated)" : content;
|
|
33
|
+
}
|
|
34
|
+
export const writeTool = {
|
|
35
|
+
name: "write",
|
|
36
|
+
description: "Write content to a file (creates or overwrites)",
|
|
37
|
+
parameters: {
|
|
38
|
+
type: "object",
|
|
39
|
+
properties: {
|
|
40
|
+
path: { type: "string", description: "File path relative to project root" },
|
|
41
|
+
content: { type: "string", description: "Content to write" },
|
|
42
|
+
},
|
|
43
|
+
required: ["path", "content"],
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
export async function executeWrite(args, workdir = DEFAULT_WORKDIR) {
|
|
47
|
+
const filePath = resolvePath(args.path, workdir);
|
|
48
|
+
assertWithinWorkdir(filePath, workdir);
|
|
49
|
+
writeFileSync(filePath, args.content, "utf-8");
|
|
50
|
+
return `Written ${args.content.length} bytes to ${args.path}`;
|
|
51
|
+
}
|
|
52
|
+
export const editTool = {
|
|
53
|
+
name: "edit",
|
|
54
|
+
description: "Replace old_string with new_string in a file",
|
|
55
|
+
parameters: {
|
|
56
|
+
type: "object",
|
|
57
|
+
properties: {
|
|
58
|
+
path: { type: "string", description: "File path relative to project root" },
|
|
59
|
+
old_string: { type: "string", description: "String to find" },
|
|
60
|
+
new_string: { type: "string", description: "Replacement string" },
|
|
61
|
+
},
|
|
62
|
+
required: ["path", "old_string", "new_string"],
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
export async function executeEdit(args, workdir = DEFAULT_WORKDIR) {
|
|
66
|
+
const filePath = resolvePath(args.path, workdir);
|
|
67
|
+
assertWithinWorkdir(filePath, workdir);
|
|
68
|
+
if (!existsSync(filePath)) {
|
|
69
|
+
return `Error: file not found: ${args.path}`;
|
|
70
|
+
}
|
|
71
|
+
const content = readFileSync(filePath, "utf-8");
|
|
72
|
+
if (!content.includes(args.old_string)) {
|
|
73
|
+
return `Error: old_string not found in ${args.path}`;
|
|
74
|
+
}
|
|
75
|
+
const newContent = content.replace(args.old_string, args.new_string);
|
|
76
|
+
writeFileSync(filePath, newContent, "utf-8");
|
|
77
|
+
return `Edited ${args.path}`;
|
|
78
|
+
}
|
|
79
|
+
export const bashTool = {
|
|
80
|
+
name: "bash",
|
|
81
|
+
description: "Run a shell command in the project directory",
|
|
82
|
+
parameters: {
|
|
83
|
+
type: "object",
|
|
84
|
+
properties: {
|
|
85
|
+
command: { type: "string", description: "Shell command to execute" },
|
|
86
|
+
},
|
|
87
|
+
required: ["command"],
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
export async function executeBash(args, workdir = DEFAULT_WORKDIR) {
|
|
91
|
+
const { exec } = await import("node:child_process");
|
|
92
|
+
const { promisify } = await import("node:util");
|
|
93
|
+
const execAsync = promisify(exec);
|
|
94
|
+
try {
|
|
95
|
+
const { stdout, stderr } = await execAsync(args.command, {
|
|
96
|
+
cwd: workdir,
|
|
97
|
+
timeout: 120000,
|
|
98
|
+
maxBuffer: 1024 * 1024,
|
|
99
|
+
});
|
|
100
|
+
const out = stdout + (stderr ? `\n${stderr}` : "");
|
|
101
|
+
return out.trim() || "(no output)";
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
const e = err;
|
|
105
|
+
const out = (e.stdout ?? "") + (e.stderr ? `\n${e.stderr}` : "");
|
|
106
|
+
return out.trim() || (e.message ?? "Command failed");
|
|
107
|
+
}
|
|
108
|
+
}
|
package/dist/cli/args.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export function parseArgs(argv) {
|
|
2
|
+
const args = argv.slice(2);
|
|
3
|
+
const result = {
|
|
4
|
+
print: false,
|
|
5
|
+
continueSession: false,
|
|
6
|
+
help: false,
|
|
7
|
+
};
|
|
8
|
+
const positional = [];
|
|
9
|
+
for (let i = 0; i < args.length; i++) {
|
|
10
|
+
const arg = args[i];
|
|
11
|
+
if (arg === "-h" || arg === "--help") {
|
|
12
|
+
result.help = true;
|
|
13
|
+
}
|
|
14
|
+
else if (arg === "-p" || arg === "--print") {
|
|
15
|
+
result.print = true;
|
|
16
|
+
}
|
|
17
|
+
else if (arg === "-c" || arg === "--continue") {
|
|
18
|
+
result.continueSession = true;
|
|
19
|
+
}
|
|
20
|
+
else if (arg === "--model" && args[i + 1]) {
|
|
21
|
+
result.model = args[++i];
|
|
22
|
+
}
|
|
23
|
+
else if (!arg.startsWith("-")) {
|
|
24
|
+
positional.push(arg);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (positional.length > 0) {
|
|
28
|
+
result.prompt = positional.join(" ");
|
|
29
|
+
}
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
export function printHelp() {
|
|
33
|
+
console.log(`
|
|
34
|
+
agent-dev — minimal pi-like coding agent
|
|
35
|
+
|
|
36
|
+
Usage:
|
|
37
|
+
agent Interactive mode
|
|
38
|
+
agent -p "prompt" Print mode (no TUI)
|
|
39
|
+
agent -c Continue last session
|
|
40
|
+
agent --model groq/llama-3.3-70b-versatile "prompt"
|
|
41
|
+
|
|
42
|
+
Options:
|
|
43
|
+
-p, --print Print response and exit
|
|
44
|
+
-c, --continue Continue most recent session
|
|
45
|
+
--model <ref> Provider/model (e.g. openai/gpt-4o)
|
|
46
|
+
-h, --help Show help
|
|
47
|
+
|
|
48
|
+
Commands (interactive):
|
|
49
|
+
/model [search] Select model
|
|
50
|
+
/settings Settings menu
|
|
51
|
+
/new New session
|
|
52
|
+
/quit Quit
|
|
53
|
+
|
|
54
|
+
Environment:
|
|
55
|
+
OPENAI_API_KEY OpenAI (ChatGPT)
|
|
56
|
+
GROQ_API_KEY Groq
|
|
57
|
+
GEMINI_API_KEY Google Gemini
|
|
58
|
+
OPENROUTER_API_KEY Free models via OpenRouter
|
|
59
|
+
`);
|
|
60
|
+
}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Model, ProviderId } from "../providers/types.js";
|
|
2
|
+
export declare const PROVIDER_LABELS: Record<ProviderId, string>;
|
|
3
|
+
export declare const ALL_MODELS: Model[];
|
|
4
|
+
export declare function findModel(provider: ProviderId, id: string): Model | undefined;
|
|
5
|
+
export declare function parseModelRef(ref: string): Model | undefined;
|
|
6
|
+
export declare function modelRef(model: Model): string;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export const PROVIDER_LABELS = {
|
|
2
|
+
openai: "OpenAI (ChatGPT)",
|
|
3
|
+
groq: "Groq",
|
|
4
|
+
gemini: "Google Gemini",
|
|
5
|
+
free: "Free (OpenRouter)",
|
|
6
|
+
};
|
|
7
|
+
export const ALL_MODELS = [
|
|
8
|
+
{ provider: "openai", id: "gpt-4o", name: "GPT-4o" },
|
|
9
|
+
{ provider: "openai", id: "gpt-4o-mini", name: "GPT-4o Mini" },
|
|
10
|
+
{ provider: "groq", id: "llama-3.3-70b-versatile", name: "Llama 3.3 70B" },
|
|
11
|
+
{ provider: "groq", id: "openai/gpt-oss-120b", name: "GPT-OSS 120B" },
|
|
12
|
+
{ provider: "gemini", id: "gemini-2.0-flash", name: "Gemini 2.0 Flash" },
|
|
13
|
+
{ provider: "gemini", id: "gemini-2.5-flash", name: "Gemini 2.5 Flash" },
|
|
14
|
+
{
|
|
15
|
+
provider: "free",
|
|
16
|
+
id: "meta-llama/llama-3.3-70b-instruct:free",
|
|
17
|
+
name: "Llama 3.3 70B (free)",
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
provider: "free",
|
|
21
|
+
id: "google/gemini-2.0-flash-exp:free",
|
|
22
|
+
name: "Gemini 2.0 Flash (free)",
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
provider: "free",
|
|
26
|
+
id: "qwen/qwen-2.5-72b-instruct:free",
|
|
27
|
+
name: "Qwen 2.5 72B (free)",
|
|
28
|
+
},
|
|
29
|
+
];
|
|
30
|
+
export function findModel(provider, id) {
|
|
31
|
+
return ALL_MODELS.find((m) => m.provider === provider && m.id === id);
|
|
32
|
+
}
|
|
33
|
+
export function parseModelRef(ref) {
|
|
34
|
+
const slash = ref.indexOf("/");
|
|
35
|
+
if (slash === -1)
|
|
36
|
+
return undefined;
|
|
37
|
+
const provider = ref.slice(0, slash);
|
|
38
|
+
const id = ref.slice(slash + 1);
|
|
39
|
+
return findModel(provider, id);
|
|
40
|
+
}
|
|
41
|
+
export function modelRef(model) {
|
|
42
|
+
return `${model.provider}/${model.id}`;
|
|
43
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
export const CONFIG_DIR = process.env.AGENT_DEV_DIR ?? join(homedir(), ".agent-dev");
|
|
4
|
+
export const SETTINGS_PATH = join(CONFIG_DIR, "settings.json");
|
|
5
|
+
export const SESSIONS_DIR = join(CONFIG_DIR, "sessions");
|
|
6
|
+
export const LAST_SESSION_PATH = join(CONFIG_DIR, "last-session.json");
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ProviderId } from "../providers/types.js";
|
|
2
|
+
import type { ThinkingLevel, Theme } from "../providers/types.js";
|
|
3
|
+
export interface Settings {
|
|
4
|
+
defaultProvider: ProviderId;
|
|
5
|
+
defaultModel: string;
|
|
6
|
+
thinkingLevel: ThinkingLevel;
|
|
7
|
+
theme: Theme;
|
|
8
|
+
apiKeys?: Partial<Record<ProviderId, string>>;
|
|
9
|
+
}
|
|
10
|
+
export declare function loadSettings(): Settings;
|
|
11
|
+
export declare function saveSettings(settings: Settings): void;
|
|
12
|
+
export declare function setDefaultModel(settings: Settings, provider: ProviderId, modelId: string): Settings;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
2
|
+
import { CONFIG_DIR, SETTINGS_PATH } from "./paths.js";
|
|
3
|
+
const DEFAULT_SETTINGS = {
|
|
4
|
+
defaultProvider: "free",
|
|
5
|
+
defaultModel: "meta-llama/llama-3.3-70b-instruct:free",
|
|
6
|
+
thinkingLevel: "off",
|
|
7
|
+
theme: "dark",
|
|
8
|
+
};
|
|
9
|
+
export function loadSettings() {
|
|
10
|
+
if (!existsSync(SETTINGS_PATH)) {
|
|
11
|
+
return { ...DEFAULT_SETTINGS };
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
const raw = readFileSync(SETTINGS_PATH, "utf-8");
|
|
15
|
+
return { ...DEFAULT_SETTINGS, ...JSON.parse(raw) };
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return { ...DEFAULT_SETTINGS };
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function saveSettings(settings) {
|
|
22
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
23
|
+
writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2), "utf-8");
|
|
24
|
+
}
|
|
25
|
+
export function setDefaultModel(settings, provider, modelId) {
|
|
26
|
+
const updated = { ...settings, defaultProvider: provider, defaultModel: modelId };
|
|
27
|
+
saveSettings(updated);
|
|
28
|
+
return updated;
|
|
29
|
+
}
|
package/dist/main.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function main(): Promise<void>;
|