multiarena 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.
Files changed (59) hide show
  1. package/dist/config/loader.d.ts +6 -0
  2. package/dist/config/loader.js +69 -0
  3. package/dist/config/types.d.ts +15 -0
  4. package/dist/config/types.js +6 -0
  5. package/dist/core/session.d.ts +40 -0
  6. package/dist/core/session.js +155 -0
  7. package/dist/core/turn.d.ts +31 -0
  8. package/dist/core/turn.js +112 -0
  9. package/dist/core/types.d.ts +25 -0
  10. package/dist/core/types.js +1 -0
  11. package/dist/index.d.ts +2 -0
  12. package/dist/index.js +76 -0
  13. package/dist/isolation/worktree.d.ts +11 -0
  14. package/dist/isolation/worktree.js +117 -0
  15. package/dist/persistence/session.d.ts +17 -0
  16. package/dist/persistence/session.js +27 -0
  17. package/dist/provider/adapters/anthropic.d.ts +11 -0
  18. package/dist/provider/adapters/anthropic.js +146 -0
  19. package/dist/provider/adapters/google.d.ts +11 -0
  20. package/dist/provider/adapters/google.js +177 -0
  21. package/dist/provider/adapters/ollama.d.ts +11 -0
  22. package/dist/provider/adapters/ollama.js +147 -0
  23. package/dist/provider/adapters/openai.d.ts +11 -0
  24. package/dist/provider/adapters/openai.js +167 -0
  25. package/dist/provider/provider.d.ts +7 -0
  26. package/dist/provider/provider.js +21 -0
  27. package/dist/provider/types.d.ts +41 -0
  28. package/dist/provider/types.js +1 -0
  29. package/dist/tools/builtin/bash.d.ts +2 -0
  30. package/dist/tools/builtin/bash.js +34 -0
  31. package/dist/tools/builtin/editFile.d.ts +2 -0
  32. package/dist/tools/builtin/editFile.js +40 -0
  33. package/dist/tools/builtin/glob.d.ts +2 -0
  34. package/dist/tools/builtin/glob.js +77 -0
  35. package/dist/tools/builtin/grep.d.ts +2 -0
  36. package/dist/tools/builtin/grep.js +120 -0
  37. package/dist/tools/builtin/readFile.d.ts +2 -0
  38. package/dist/tools/builtin/readFile.js +27 -0
  39. package/dist/tools/builtin/writeFile.d.ts +2 -0
  40. package/dist/tools/builtin/writeFile.js +29 -0
  41. package/dist/tools/permission.d.ts +7 -0
  42. package/dist/tools/permission.js +31 -0
  43. package/dist/tools/registry.d.ts +9 -0
  44. package/dist/tools/registry.js +37 -0
  45. package/dist/tools/types.d.ts +11 -0
  46. package/dist/tools/types.js +1 -0
  47. package/dist/ui/app.d.ts +4 -0
  48. package/dist/ui/app.js +343 -0
  49. package/dist/ui/components/BroadcastSummary.d.ts +7 -0
  50. package/dist/ui/components/BroadcastSummary.js +18 -0
  51. package/dist/ui/components/InputBar.d.ts +9 -0
  52. package/dist/ui/components/InputBar.js +11 -0
  53. package/dist/ui/components/ModelDetail.d.ts +8 -0
  54. package/dist/ui/components/ModelDetail.js +13 -0
  55. package/dist/ui/components/OutputArea.d.ts +15 -0
  56. package/dist/ui/components/OutputArea.js +29 -0
  57. package/dist/ui/components/StatusBar.d.ts +9 -0
  58. package/dist/ui/components/StatusBar.js +51 -0
  59. package/package.json +60 -0
@@ -0,0 +1,167 @@
1
+ import OpenAI from "openai";
2
+ const DEFAULT_MODEL = "gpt-4o";
3
+ const DEFAULT_TIMEOUT_MS = 120_000;
4
+ export class OpenAIProvider {
5
+ client;
6
+ activeController = null;
7
+ constructor(apiKey, baseURL) {
8
+ this.client = new OpenAI({ apiKey, baseURL, timeout: DEFAULT_TIMEOUT_MS, maxRetries: 2 });
9
+ }
10
+ async *chat(request) {
11
+ const abortController = new AbortController();
12
+ this.activeController = abortController;
13
+ let inputTokens = 0;
14
+ let outputTokens = 0;
15
+ try {
16
+ const stream = await this.client.chat.completions.create({
17
+ model: request.model || DEFAULT_MODEL,
18
+ messages: this.convertMessages(request.messages, request.system),
19
+ tools: this.convertTools(request.tools),
20
+ stream: true,
21
+ stream_options: { include_usage: true },
22
+ }, { signal: abortController.signal });
23
+ // Track pending tool calls across stream chunks
24
+ const pendingToolCalls = new Map();
25
+ let doneYielded = false;
26
+ for await (const chunk of stream) {
27
+ // Token usage chunk (when stream_options.include_usage is true)
28
+ if (chunk.usage) {
29
+ inputTokens = chunk.usage.prompt_tokens;
30
+ outputTokens = chunk.usage.completion_tokens;
31
+ continue;
32
+ }
33
+ const choice = chunk.choices?.[0];
34
+ if (!choice)
35
+ continue;
36
+ const delta = choice.delta;
37
+ // Accumulate tool call deltas
38
+ if (delta.tool_calls) {
39
+ for (const tc of delta.tool_calls) {
40
+ const idx = tc.index;
41
+ const existing = pendingToolCalls.get(idx) ?? {
42
+ id: "",
43
+ name: "",
44
+ args: "",
45
+ };
46
+ if (tc.id)
47
+ existing.id = tc.id;
48
+ if (tc.function?.name)
49
+ existing.name += tc.function.name;
50
+ if (tc.function?.arguments)
51
+ existing.args += tc.function.arguments;
52
+ pendingToolCalls.set(idx, existing);
53
+ }
54
+ }
55
+ // Text delta
56
+ if (delta.content) {
57
+ yield { type: "text", content: delta.content };
58
+ }
59
+ // On finish_reason === "tool_calls", emit all pending tool_calls
60
+ if (choice.finish_reason === "tool_calls") {
61
+ for (const [, tc] of pendingToolCalls) {
62
+ yield {
63
+ type: "tool_call",
64
+ id: tc.id,
65
+ name: tc.name,
66
+ args: tc.args,
67
+ };
68
+ }
69
+ pendingToolCalls.clear();
70
+ yield {
71
+ type: "done",
72
+ usage: { input: inputTokens, output: outputTokens },
73
+ };
74
+ doneYielded = true;
75
+ }
76
+ // handle stop
77
+ if (choice.finish_reason === "stop") {
78
+ yield {
79
+ type: "done",
80
+ usage: { input: inputTokens, output: outputTokens },
81
+ };
82
+ doneYielded = true;
83
+ }
84
+ }
85
+ // Ensure done is always yielded (e.g. when stream ends on usage chunk)
86
+ if (!doneYielded) {
87
+ yield {
88
+ type: "done",
89
+ usage: { input: inputTokens, output: outputTokens },
90
+ };
91
+ }
92
+ }
93
+ catch (error) {
94
+ if (error instanceof Error &&
95
+ (error.name === "AbortError" || error.name === "APIUserAbortError")) {
96
+ return; // Aborted silently
97
+ }
98
+ yield {
99
+ type: "error",
100
+ message: error instanceof Error ? error.message : String(error),
101
+ };
102
+ }
103
+ finally {
104
+ this.activeController = null;
105
+ }
106
+ }
107
+ abort() {
108
+ this.activeController?.abort();
109
+ }
110
+ // ── Message conversion ──────────────────────────────────────────────
111
+ convertMessages(messages, system) {
112
+ const result = [];
113
+ // Prepend system message if provided
114
+ if (system) {
115
+ result.push({ role: "system", content: system });
116
+ }
117
+ for (const msg of messages) {
118
+ switch (msg.role) {
119
+ case "user":
120
+ result.push({ role: "user", content: msg.content });
121
+ break;
122
+ case "assistant": {
123
+ const toolCalls = msg.tool_calls?.map((tc) => ({
124
+ id: tc.id,
125
+ type: "function",
126
+ function: {
127
+ name: tc.name,
128
+ arguments: tc.arguments,
129
+ },
130
+ }));
131
+ if (toolCalls && toolCalls.length > 0) {
132
+ result.push({
133
+ role: "assistant",
134
+ content: msg.content || null,
135
+ tool_calls: toolCalls,
136
+ });
137
+ }
138
+ else {
139
+ result.push({ role: "assistant", content: msg.content });
140
+ }
141
+ break;
142
+ }
143
+ case "tool":
144
+ result.push({
145
+ role: "tool",
146
+ tool_call_id: msg.tool_call_id ?? "",
147
+ content: msg.content,
148
+ });
149
+ break;
150
+ }
151
+ }
152
+ return result;
153
+ }
154
+ // ── Tool conversion ─────────────────────────────────────────────────
155
+ convertTools(tools) {
156
+ if (!tools || tools.length === 0)
157
+ return undefined;
158
+ return tools.map((t) => ({
159
+ type: "function",
160
+ function: {
161
+ name: t.name,
162
+ description: t.description,
163
+ parameters: t.parameters,
164
+ },
165
+ }));
166
+ }
167
+ }
@@ -0,0 +1,7 @@
1
+ import { ChatRequest, StreamEvent } from "./types.js";
2
+ import { ModelConfig } from "../config/types.js";
3
+ export interface Provider {
4
+ chat(request: ChatRequest): AsyncGenerator<StreamEvent>;
5
+ abort(): void;
6
+ }
7
+ export declare function createProvider(config: ModelConfig): Provider;
@@ -0,0 +1,21 @@
1
+ import { AnthropicProvider } from "./adapters/anthropic.js";
2
+ import { OpenAIProvider } from "./adapters/openai.js";
3
+ import { GoogleProvider } from "./adapters/google.js";
4
+ import { OllamaProvider } from "./adapters/ollama.js";
5
+ export function createProvider(config) {
6
+ const key = config.api_key ?? "";
7
+ switch (config.provider) {
8
+ case "anthropic":
9
+ return new AnthropicProvider(key);
10
+ case "openai":
11
+ return new OpenAIProvider(key, config.endpoint);
12
+ case "google":
13
+ return new GoogleProvider(key);
14
+ case "ollama": {
15
+ const baseURL = config.endpoint?.replace(/\/v1\/?$/, "") ?? "http://localhost:11434";
16
+ return new OllamaProvider(baseURL);
17
+ }
18
+ default:
19
+ throw new Error(`Unknown provider: ${config.provider}`);
20
+ }
21
+ }
@@ -0,0 +1,41 @@
1
+ export interface Message {
2
+ role: "user" | "assistant" | "tool";
3
+ content: string;
4
+ tool_call_id?: string;
5
+ tool_calls?: ToolCall[];
6
+ }
7
+ export interface ToolCall {
8
+ id: string;
9
+ name: string;
10
+ arguments: string;
11
+ }
12
+ export interface ToolDef {
13
+ name: string;
14
+ description: string;
15
+ parameters: Record<string, unknown>;
16
+ }
17
+ export interface TokenUsage {
18
+ input: number;
19
+ output: number;
20
+ }
21
+ export type StreamEvent = {
22
+ type: "text";
23
+ content: string;
24
+ } | {
25
+ type: "tool_call";
26
+ id: string;
27
+ name: string;
28
+ args: string;
29
+ } | {
30
+ type: "done";
31
+ usage: TokenUsage;
32
+ } | {
33
+ type: "error";
34
+ message: string;
35
+ };
36
+ export interface ChatRequest {
37
+ messages: Message[];
38
+ tools?: ToolDef[];
39
+ system?: string;
40
+ model?: string;
41
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import type { ToolHandler } from "../types.js";
2
+ export declare const bashTool: ToolHandler;
@@ -0,0 +1,34 @@
1
+ import { execSync } from "node:child_process";
2
+ export const bashTool = {
3
+ definition: {
4
+ name: "bash",
5
+ description: "Execute a shell command",
6
+ parameters: {
7
+ type: "object",
8
+ properties: {
9
+ command: { type: "string", description: "Shell command to execute" },
10
+ },
11
+ required: ["command"],
12
+ },
13
+ },
14
+ async execute(args, worktreePath) {
15
+ const command = args.command;
16
+ const dangerous = ["rm -rf /", "sudo ", "mkfs.", "dd if=", "> /dev/sda"];
17
+ for (const d of dangerous) {
18
+ if (command.includes(d))
19
+ return `Blocked: dangerous command pattern "${d}"`;
20
+ }
21
+ try {
22
+ const output = execSync(command, {
23
+ cwd: worktreePath,
24
+ encoding: "utf-8",
25
+ timeout: 30000,
26
+ maxBuffer: 1024 * 1024,
27
+ });
28
+ return output || "(no output)";
29
+ }
30
+ catch (err) {
31
+ return `Command failed (exit ${err.status}): ${err.stderr ?? err.message}`;
32
+ }
33
+ },
34
+ };
@@ -0,0 +1,2 @@
1
+ import type { ToolHandler } from "../types.js";
2
+ export declare const editFileTool: ToolHandler;
@@ -0,0 +1,40 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ export const editFileTool = {
4
+ definition: {
5
+ name: "editFile",
6
+ description: "Replace a string in a file with a new string",
7
+ parameters: {
8
+ type: "object",
9
+ properties: {
10
+ filePath: { type: "string", description: "Path to the file to edit" },
11
+ oldString: { type: "string", description: "Exact text to replace" },
12
+ newString: { type: "string", description: "Replacement text" },
13
+ replaceAll: { type: "boolean", description: "Replace all occurrences (default: false)" },
14
+ },
15
+ required: ["filePath", "oldString", "newString"],
16
+ },
17
+ },
18
+ async execute(args, worktreePath) {
19
+ const filePath = path.resolve(worktreePath, args.filePath);
20
+ if (!fs.existsSync(filePath))
21
+ return `File not found: ${args.filePath}`;
22
+ const content = fs.readFileSync(filePath, "utf-8");
23
+ const oldStr = args.oldString;
24
+ const newStr = args.newString;
25
+ const replaceAll = args.replaceAll;
26
+ if (!content.includes(oldStr)) {
27
+ return `Error: oldString not found in ${args.filePath}`;
28
+ }
29
+ const occurrences = content.split(oldStr).length - 1;
30
+ if (occurrences > 1 && !replaceAll) {
31
+ return `Error: oldString appears ${occurrences} times. Use replaceAll: true or provide more context.`;
32
+ }
33
+ const result = replaceAll
34
+ ? content.replaceAll(oldStr, newStr)
35
+ : content.replace(oldStr, newStr);
36
+ fs.writeFileSync(filePath, result, "utf-8");
37
+ const replaced = replaceAll ? occurrences : 1;
38
+ return `Replaced ${replaced} occurrence(s) in ${args.filePath}`;
39
+ },
40
+ };
@@ -0,0 +1,2 @@
1
+ import type { ToolHandler } from "../types.js";
2
+ export declare const globTool: ToolHandler;
@@ -0,0 +1,77 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ function globToRegex(pattern) {
4
+ let escaped = "";
5
+ let i = 0;
6
+ while (i < pattern.length) {
7
+ const ch = pattern[i];
8
+ if (ch === "*" && pattern[i + 1] === "*" && pattern[i + 2] === "/") {
9
+ escaped += ".*";
10
+ i += 3;
11
+ continue;
12
+ }
13
+ if (ch === "*" && pattern[i + 1] === "*") {
14
+ escaped += ".*";
15
+ i += 2;
16
+ continue;
17
+ }
18
+ if (ch === "*") {
19
+ escaped += "[^/]*";
20
+ i++;
21
+ continue;
22
+ }
23
+ if (ch === "?") {
24
+ escaped += ".";
25
+ i++;
26
+ continue;
27
+ }
28
+ if (".+^$(){}[]|\\".includes(ch)) {
29
+ escaped += "\\" + ch;
30
+ }
31
+ else {
32
+ escaped += ch;
33
+ }
34
+ i++;
35
+ }
36
+ return new RegExp(`^${escaped}$`);
37
+ }
38
+ function collectFiles(dir, base, results) {
39
+ if (!fs.existsSync(dir))
40
+ return;
41
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
42
+ for (const e of entries) {
43
+ const full = path.join(dir, e.name);
44
+ if (e.isDirectory()) {
45
+ collectFiles(full, base, results);
46
+ }
47
+ else if (e.isFile()) {
48
+ results.push(path.relative(base, full));
49
+ }
50
+ }
51
+ }
52
+ export const globTool = {
53
+ definition: {
54
+ name: "glob",
55
+ description: "Find files matching a glob pattern",
56
+ parameters: {
57
+ type: "object",
58
+ properties: {
59
+ pattern: { type: "string", description: "Glob pattern (e.g. **/*.ts, src/**/*.tsx)" },
60
+ path: { type: "string", description: "Base directory to search in" },
61
+ },
62
+ required: ["pattern"],
63
+ },
64
+ },
65
+ async execute(args, worktreePath) {
66
+ const pattern = args.pattern;
67
+ const base = path.resolve(worktreePath, args.path ?? ".");
68
+ const regex = globToRegex(pattern);
69
+ const files = [];
70
+ collectFiles(base, base, files);
71
+ const matches = files
72
+ .filter((f) => regex.test(f))
73
+ .sort()
74
+ .slice(0, 500);
75
+ return matches.length > 0 ? matches.join("\n") : "No files matched";
76
+ },
77
+ };
@@ -0,0 +1,2 @@
1
+ import type { ToolHandler } from "../types.js";
2
+ export declare const grepTool: ToolHandler;
@@ -0,0 +1,120 @@
1
+ import { execSync } from "node:child_process";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ const MAX_RESULTS = 500;
5
+ export const grepTool = {
6
+ definition: {
7
+ name: "grep",
8
+ description: "Search for a pattern in files using regex",
9
+ parameters: {
10
+ type: "object",
11
+ properties: {
12
+ pattern: { type: "string", description: "Regex pattern to search for" },
13
+ path: { type: "string", description: "Directory or file to search in" },
14
+ include: { type: "string", description: "File glob pattern to include" },
15
+ },
16
+ required: ["pattern"],
17
+ },
18
+ },
19
+ async execute(args, worktreePath) {
20
+ const cwd = args.path
21
+ ? path.resolve(worktreePath, args.path)
22
+ : worktreePath;
23
+ const pattern = args.pattern;
24
+ const include = args.include;
25
+ // Check if rg is available; use Node.js fallback if not
26
+ const rgAvailable = (() => {
27
+ try {
28
+ execSync("rg --version", { stdio: "ignore" });
29
+ return true;
30
+ }
31
+ catch {
32
+ return false;
33
+ }
34
+ })();
35
+ if (rgAvailable) {
36
+ try {
37
+ return tryRg(cwd, pattern, include);
38
+ }
39
+ catch (err) {
40
+ // rg found no matches (exit code 1)
41
+ if (err.status === 1)
42
+ return "No matches found";
43
+ throw err;
44
+ }
45
+ }
46
+ return nodeGrep(cwd, pattern, include);
47
+ },
48
+ };
49
+ function tryRg(cwd, pattern, include) {
50
+ const inc = include ? `--glob="${include}"` : "";
51
+ const result = execSync(`rg -n --no-heading ${inc} "${pattern}" .`, {
52
+ cwd,
53
+ encoding: "utf-8",
54
+ maxBuffer: 10 * 1024 * 1024,
55
+ });
56
+ return result || "No matches found";
57
+ }
58
+ function nodeGrep(cwd, pattern, include) {
59
+ let regex;
60
+ try {
61
+ // Don't use 'g' flag — it makes .test() stateful across calls
62
+ regex = new RegExp(pattern);
63
+ }
64
+ catch {
65
+ return `Error: invalid regex pattern "${pattern}"`;
66
+ }
67
+ const includeRegex = include ? globToRegex(include) : null;
68
+ const results = [];
69
+ walkDir(cwd, cwd, (filePath) => {
70
+ if (includeRegex && !includeRegex.test(filePath))
71
+ return;
72
+ let content;
73
+ try {
74
+ content = fs.readFileSync(path.join(cwd, filePath), "utf-8");
75
+ }
76
+ catch {
77
+ return; // skip binary/unreadable
78
+ }
79
+ const lines = content.split("\n");
80
+ for (let i = 0; i < lines.length && results.length < MAX_RESULTS; i++) {
81
+ if (regex.test(lines[i])) {
82
+ // Normalize path separator for output
83
+ const displayPath = filePath.replace(/\\/g, "/");
84
+ results.push(`${displayPath}:${i + 1}:${lines[i].trimEnd()}`);
85
+ }
86
+ }
87
+ });
88
+ return results.length > 0 ? results.join("\n") : "No matches found";
89
+ }
90
+ function walkDir(base, dir, callback) {
91
+ let entries;
92
+ try {
93
+ entries = fs.readdirSync(dir, { withFileTypes: true });
94
+ }
95
+ catch {
96
+ return;
97
+ }
98
+ for (const entry of entries) {
99
+ const fullPath = path.join(dir, entry.name);
100
+ const relPath = path.relative(base, fullPath);
101
+ if (entry.isDirectory()) {
102
+ if (entry.name === "node_modules" || entry.name === ".git")
103
+ continue;
104
+ walkDir(base, fullPath, callback);
105
+ }
106
+ else if (entry.isFile()) {
107
+ // Skip binary-looking files
108
+ if (entry.name.endsWith(".exe") || entry.name.endsWith(".dll"))
109
+ continue;
110
+ callback(relPath);
111
+ }
112
+ }
113
+ }
114
+ function globToRegex(glob) {
115
+ let pattern = glob
116
+ .replace(/[.+^${}()|[\]\\]/g, "\\$&")
117
+ .replace(/\*/g, ".*")
118
+ .replace(/\?/g, ".");
119
+ return new RegExp(`^${pattern}$`, "i");
120
+ }
@@ -0,0 +1,2 @@
1
+ import type { ToolHandler } from "../types.js";
2
+ export declare const readFileTool: ToolHandler;
@@ -0,0 +1,27 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ export const readFileTool = {
4
+ definition: {
5
+ name: "readFile",
6
+ description: "Read the contents of a file",
7
+ parameters: {
8
+ type: "object",
9
+ properties: {
10
+ filePath: { type: "string", description: "Path to the file" },
11
+ offset: { type: "number", description: "Line offset to start reading from" },
12
+ limit: { type: "number", description: "Maximum number of lines to read" },
13
+ },
14
+ required: ["filePath"],
15
+ },
16
+ },
17
+ async execute(args, worktreePath) {
18
+ const filePath = path.resolve(worktreePath, args.filePath);
19
+ if (!fs.existsSync(filePath))
20
+ return `File not found: ${args.filePath}`;
21
+ const content = fs.readFileSync(filePath, "utf-8");
22
+ const lines = content.split("\n");
23
+ const offset = args.offset ?? 0;
24
+ const limit = args.limit ?? lines.length;
25
+ return lines.slice(offset, offset + limit).join("\n");
26
+ },
27
+ };
@@ -0,0 +1,2 @@
1
+ import type { ToolHandler } from "../types.js";
2
+ export declare const writeFileTool: ToolHandler;
@@ -0,0 +1,29 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ export const writeFileTool = {
4
+ definition: {
5
+ name: "writeFile",
6
+ description: "Write content to a file (creates or overwrites)",
7
+ parameters: {
8
+ type: "object",
9
+ properties: {
10
+ filePath: { type: "string", description: "Path to the file to write" },
11
+ content: { type: "string", description: "Content to write to the file" },
12
+ },
13
+ required: ["filePath", "content"],
14
+ },
15
+ },
16
+ async execute(args, worktreePath) {
17
+ const filePath = path.resolve(worktreePath, args.filePath);
18
+ // Prevent writing outside the worktree
19
+ if (!filePath.startsWith(path.resolve(worktreePath))) {
20
+ return "Error: file path escapes the worktree";
21
+ }
22
+ const dir = path.dirname(filePath);
23
+ if (!fs.existsSync(dir)) {
24
+ fs.mkdirSync(dir, { recursive: true });
25
+ }
26
+ fs.writeFileSync(filePath, args.content, "utf-8");
27
+ return `Wrote ${fs.statSync(filePath).size} bytes to ${args.filePath}`;
28
+ },
29
+ };
@@ -0,0 +1,7 @@
1
+ import type { PermissionDecision } from "./types.js";
2
+ export declare class PermissionManager {
3
+ private entries;
4
+ check(toolName: string, args: Record<string, unknown>): PermissionDecision;
5
+ remember(toolName: string, args: Record<string, unknown>, decision: "allow_always" | "deny_always"): void;
6
+ clear(): void;
7
+ }
@@ -0,0 +1,31 @@
1
+ export class PermissionManager {
2
+ entries = [];
3
+ check(toolName, args) {
4
+ // Hard-coded safety rules
5
+ if (toolName === "bash") {
6
+ const cmd = String(args.command ?? "");
7
+ if (cmd.includes("rm -rf /") || cmd.includes("sudo ")) {
8
+ return "deny";
9
+ }
10
+ }
11
+ if (toolName === "readFile" || toolName === "grep") {
12
+ const path = String(args.path ?? args.filePath ?? "");
13
+ if (path.includes(".env") || path.includes(".git-credentials")) {
14
+ return "deny";
15
+ }
16
+ }
17
+ // Session memory
18
+ for (const entry of this.entries) {
19
+ if (entry.toolName === toolName) {
20
+ return entry.decision;
21
+ }
22
+ }
23
+ return "allow";
24
+ }
25
+ remember(toolName, args, decision) {
26
+ this.entries.push({ toolName, args, decision });
27
+ }
28
+ clear() {
29
+ this.entries = [];
30
+ }
31
+ }
@@ -0,0 +1,9 @@
1
+ import type { ToolDef } from "../provider/types.js";
2
+ import type { ToolHandler } from "./types.js";
3
+ export declare class ToolRegistry {
4
+ private tools;
5
+ register(handler: ToolHandler): void;
6
+ getDefinitions(): ToolDef[];
7
+ execute(name: string, args: Record<string, unknown>, worktreePath: string): Promise<string>;
8
+ }
9
+ export declare function createDefaultRegistry(): ToolRegistry;