woopcode 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/CONTRIBUTING.md +329 -0
- package/LICENSE +21 -0
- package/README.md +582 -0
- package/cli.ts +17 -0
- package/commands/agent.tsx +139 -0
- package/commands/agentController.ts +138 -0
- package/commands/models.ts +21 -0
- package/commands/providers/index.ts +12 -0
- package/commands/providers/listProviders.ts +28 -0
- package/commands/providers/login.ts +28 -0
- package/commands/providers/logout.ts +33 -0
- package/commands/providers/setProvider.ts +34 -0
- package/config/authProvider.ts +54 -0
- package/config/client.ts +163 -0
- package/config/config.ts +118 -0
- package/config/conversation.json +1 -0
- package/config/models.json +50 -0
- package/config/paths.ts +96 -0
- package/config/providers.json +21 -0
- package/config/runtime.ts +130 -0
- package/config/systemPrompt.ts +54 -0
- package/config/types.ts +88 -0
- package/onboarding/FLOW.md +291 -0
- package/onboarding/index.ts +56 -0
- package/onboarding/providers.ts +47 -0
- package/onboarding/setupWizard.tsx +193 -0
- package/onboarding/test-reset.ts +55 -0
- package/package.json +86 -0
- package/tools/createFile.ts +24 -0
- package/tools/editFile.ts +85 -0
- package/tools/findFiles.ts +94 -0
- package/tools/grep.ts +68 -0
- package/tools/index.ts +26 -0
- package/tools/listFiles.ts +49 -0
- package/tools/readFile.ts +53 -0
- package/tools/runTests.ts +29 -0
- package/tools/terminal.ts +37 -0
- package/tools/writeFile.ts +75 -0
- package/tui/package.json +0 -0
- package/tui/src/app.tsx +60 -0
- package/tui/src/components/ApprovalFooter.tsx +29 -0
- package/tui/src/components/AsciiLogo.tsx +78 -0
- package/tui/src/components/BootScreen.tsx +76 -0
- package/tui/src/components/CapabilityRow.tsx +17 -0
- package/tui/src/components/CodeBlock.tsx +70 -0
- package/tui/src/components/DiffPreview.tsx +46 -0
- package/tui/src/components/DiffViewer.tsx +36 -0
- package/tui/src/components/HomeFooter.tsx +11 -0
- package/tui/src/components/HomeScreen.tsx +71 -0
- package/tui/src/components/InlineCode.tsx +13 -0
- package/tui/src/components/LogoReveal.tsx +158 -0
- package/tui/src/components/Markdown.tsx +280 -0
- package/tui/src/components/MessageRenderer.tsx +84 -0
- package/tui/src/components/PromptCard.tsx +52 -0
- package/tui/src/components/StreamingCursor.tsx +13 -0
- package/tui/src/components/ThinkingIndicator.tsx +14 -0
- package/tui/src/components/ToolStatus.tsx +26 -0
- package/tui/src/header.tsx +25 -0
- package/tui/src/hooks/useBootAnimation.ts +67 -0
- package/tui/src/hooks/useLogoAnimation.ts +114 -0
- package/tui/src/index.ts +5 -0
- package/tui/src/prompt.tsx +66 -0
- package/tui/src/statusBar.tsx +83 -0
- package/tui/src/store/ui-store.ts +234 -0
- package/tui/src/store/useUIStore.ts +9 -0
- package/tui/src/timeline.tsx +96 -0
- package/tui/src/types.ts +40 -0
package/package.json
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "woopcode",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "An autonomous AI coding assistant built for the terminal",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "Manas Raghuwanshi",
|
|
8
|
+
"url": "https://github.com/mangit955"
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/mangit955/woop-code.git"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://github.com/mangit955/woop-code#readme",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/mangit955/woop-code/issues"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"ai",
|
|
20
|
+
"coding-assistant",
|
|
21
|
+
"cli",
|
|
22
|
+
"terminal",
|
|
23
|
+
"autonomous-agent",
|
|
24
|
+
"llm",
|
|
25
|
+
"gemini",
|
|
26
|
+
"bun",
|
|
27
|
+
"typescript",
|
|
28
|
+
"developer-tools",
|
|
29
|
+
"code-generation",
|
|
30
|
+
"ai-agent"
|
|
31
|
+
],
|
|
32
|
+
"bin": {
|
|
33
|
+
"woopcode": "./cli.ts"
|
|
34
|
+
},
|
|
35
|
+
"main": "./cli.ts",
|
|
36
|
+
"type": "module",
|
|
37
|
+
"files": [
|
|
38
|
+
"cli.ts",
|
|
39
|
+
"commands/",
|
|
40
|
+
"config/",
|
|
41
|
+
"tools/",
|
|
42
|
+
"tui/",
|
|
43
|
+
"onboarding/",
|
|
44
|
+
"README.md",
|
|
45
|
+
"LICENSE",
|
|
46
|
+
"CONTRIBUTING.md"
|
|
47
|
+
],
|
|
48
|
+
"scripts": {
|
|
49
|
+
"start": "bun ./cli.ts",
|
|
50
|
+
"test": "bun test",
|
|
51
|
+
"test:watch": "bun test --watch",
|
|
52
|
+
"prepublishOnly": "bun test"
|
|
53
|
+
},
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"@google/genai": "^2.12.0",
|
|
56
|
+
"@inkjs/ui": "^2.0.0",
|
|
57
|
+
"chalk": "^5.6.2",
|
|
58
|
+
"cli-highlight": "^2.1.11",
|
|
59
|
+
"commander": "^12.0.0",
|
|
60
|
+
"diff": "^9.0.0",
|
|
61
|
+
"ink": "^7.1.1",
|
|
62
|
+
"ink-spinner": "^5.0.0",
|
|
63
|
+
"ink-text-input": "^6.0.0",
|
|
64
|
+
"marked": "^18.0.7",
|
|
65
|
+
"react": "^19.2.7",
|
|
66
|
+
"woopcode": "file:woopcode-0.1.0.tgz"
|
|
67
|
+
},
|
|
68
|
+
"devDependencies": {
|
|
69
|
+
"@stryker-mutator/core": "^9.6.1",
|
|
70
|
+
"@stryker-mutator/typescript-checker": "^9.6.1",
|
|
71
|
+
"@types/bun": "latest",
|
|
72
|
+
"@types/commander": "^2.12.5",
|
|
73
|
+
"@types/react": "^19.2.17",
|
|
74
|
+
"fast-check": "^4.9.0"
|
|
75
|
+
},
|
|
76
|
+
"peerDependencies": {
|
|
77
|
+
"typescript": "^5.0.0"
|
|
78
|
+
},
|
|
79
|
+
"engines": {
|
|
80
|
+
"bun": ">=1.0.0"
|
|
81
|
+
},
|
|
82
|
+
"publishConfig": {
|
|
83
|
+
"access": "public",
|
|
84
|
+
"registry": "https://registry.npmjs.org/"
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Tool } from "../config/types";
|
|
2
|
+
import { existsSync } from "fs";
|
|
3
|
+
|
|
4
|
+
export const createFileTool: Tool = {
|
|
5
|
+
name: "create_file",
|
|
6
|
+
description: "Creates a new file with the provided content.",
|
|
7
|
+
parameters: [
|
|
8
|
+
{ name: "path", description: "File path", required: true },
|
|
9
|
+
{ name: "content", description: "File content", required: true },
|
|
10
|
+
],
|
|
11
|
+
async execute(args) {
|
|
12
|
+
const path = args.path as string;
|
|
13
|
+
const content = args.content as string;
|
|
14
|
+
|
|
15
|
+
if (!path) throw new Error("Missing required argument: path");
|
|
16
|
+
if (!content) throw new Error("Missing required argument: content");
|
|
17
|
+
|
|
18
|
+
if (existsSync(path)) {
|
|
19
|
+
throw new Error(`File already exists: ${path}`);
|
|
20
|
+
}
|
|
21
|
+
await Bun.write(path, content);
|
|
22
|
+
return `Created file: ${path}`;
|
|
23
|
+
},
|
|
24
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { Tool } from "../config/types";
|
|
2
|
+
import { createTwoFilesPatch } from "diff";
|
|
3
|
+
import { store } from "../tui/src/store/ui-store";
|
|
4
|
+
import type { PendingEdit } from "../tui/src/types";
|
|
5
|
+
|
|
6
|
+
export const editFileTool: Tool = {
|
|
7
|
+
name: "edit_file",
|
|
8
|
+
description: "Replace text inside an existing file.",
|
|
9
|
+
|
|
10
|
+
parameters: [
|
|
11
|
+
{
|
|
12
|
+
name: "path",
|
|
13
|
+
description: "File path",
|
|
14
|
+
required: true,
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
name: "oldText",
|
|
18
|
+
description: "Text to replace",
|
|
19
|
+
required: true,
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
name: "newText",
|
|
23
|
+
description: "Replacement text",
|
|
24
|
+
required: true,
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
|
|
28
|
+
async execute(args) {
|
|
29
|
+
const path = args.path as string;
|
|
30
|
+
const oldText = args.oldText as string;
|
|
31
|
+
const newText = args.newText as string;
|
|
32
|
+
|
|
33
|
+
const file = Bun.file(path);
|
|
34
|
+
|
|
35
|
+
if (!(await file.exists())) {
|
|
36
|
+
throw new Error(`File not found: ${path}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const content = await file.text();
|
|
40
|
+
|
|
41
|
+
if (!content.includes(oldText)) {
|
|
42
|
+
throw new Error("Text to replace not found.");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const updated = content.replace(oldText, newText);
|
|
46
|
+
|
|
47
|
+
// If content is identical, skip diff preview
|
|
48
|
+
if (content === updated) {
|
|
49
|
+
return `No changes needed for ${path}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Generate unified diff
|
|
53
|
+
const diff = createTwoFilesPatch(path, path, content, updated, "", "", {
|
|
54
|
+
context: 3,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Create pending edit
|
|
58
|
+
const pendingEdit: PendingEdit = {
|
|
59
|
+
id: crypto.randomUUID(),
|
|
60
|
+
filePath: path,
|
|
61
|
+
oldContent: content,
|
|
62
|
+
newContent: updated,
|
|
63
|
+
diff,
|
|
64
|
+
toolCallId: crypto.randomUUID(),
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// Request approval from UI
|
|
68
|
+
let approved: boolean;
|
|
69
|
+
try {
|
|
70
|
+
approved = await store.setPendingEdit(pendingEdit);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
// User cancelled with Esc
|
|
73
|
+
return `Edit cancelled for ${path}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!approved) {
|
|
77
|
+
return `Edit rejected for ${path}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Write file after approval
|
|
81
|
+
await Bun.write(path, updated);
|
|
82
|
+
|
|
83
|
+
return `Edited ${path}`;
|
|
84
|
+
},
|
|
85
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { Tool } from "../config/types";
|
|
2
|
+
|
|
3
|
+
export const findFilesTool: Tool = {
|
|
4
|
+
name: "find_files",
|
|
5
|
+
description: "Finds files by name or partial filename.",
|
|
6
|
+
parameters: [
|
|
7
|
+
{
|
|
8
|
+
name: "query",
|
|
9
|
+
required: true,
|
|
10
|
+
description: "filename or partial filename",
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
name: "path",
|
|
14
|
+
required: false,
|
|
15
|
+
description: "root directory, default process.cwd()",
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
async execute(args) {
|
|
19
|
+
const query = args.query;
|
|
20
|
+
if (!query || typeof query !== "string") {
|
|
21
|
+
throw new Error("Parameter 'query' is required and must be a string.");
|
|
22
|
+
}
|
|
23
|
+
const rootPath =
|
|
24
|
+
args.path && typeof args.path === "string" ? args.path : process.cwd();
|
|
25
|
+
|
|
26
|
+
const matches: string[] = [];
|
|
27
|
+
const lowerQuery = query.toLowerCase();
|
|
28
|
+
|
|
29
|
+
const glob = new Bun.Glob("**/*");
|
|
30
|
+
for await (const entry of glob.scan(rootPath)) {
|
|
31
|
+
const parts = entry.split("/");
|
|
32
|
+
if (
|
|
33
|
+
parts.some((part) =>
|
|
34
|
+
[
|
|
35
|
+
".git",
|
|
36
|
+
"node_modules",
|
|
37
|
+
"dist",
|
|
38
|
+
"build",
|
|
39
|
+
".next",
|
|
40
|
+
"coverage",
|
|
41
|
+
].includes(part),
|
|
42
|
+
)
|
|
43
|
+
) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const basename = parts.at(-1);
|
|
47
|
+
if (!basename) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (
|
|
51
|
+
[
|
|
52
|
+
"bun.lock",
|
|
53
|
+
"package-lock.json",
|
|
54
|
+
"pnpm-lock.yaml",
|
|
55
|
+
"yarn.lock",
|
|
56
|
+
].includes(basename)
|
|
57
|
+
) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (
|
|
61
|
+
basename.toLowerCase().includes(lowerQuery) ||
|
|
62
|
+
entry.toLowerCase().includes(lowerQuery)
|
|
63
|
+
) {
|
|
64
|
+
// Only include files, not directories
|
|
65
|
+
const fullPath = `${rootPath}/${entry}`;
|
|
66
|
+
try {
|
|
67
|
+
const stat = await Bun.file(fullPath).stat();
|
|
68
|
+
if (!stat.isDirectory) {
|
|
69
|
+
matches.push(entry);
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
// If we can't stat it, skip it
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (matches.length === 0) {
|
|
79
|
+
return "No matching files found.";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const output = matches.join("\n");
|
|
83
|
+
const MAX_OUTPUT_SIZE = 8 * 1024; // 8 KB
|
|
84
|
+
|
|
85
|
+
if (output.length <= MAX_OUTPUT_SIZE) {
|
|
86
|
+
return output;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
output.slice(0, MAX_OUTPUT_SIZE) +
|
|
91
|
+
`\n\n... output truncated (${matches.length} matches found)`
|
|
92
|
+
);
|
|
93
|
+
},
|
|
94
|
+
};
|
package/tools/grep.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { Tool } from "../config/types";
|
|
2
|
+
|
|
3
|
+
export const grepTool: Tool = {
|
|
4
|
+
name: "grep",
|
|
5
|
+
description: "Searches recursively for text in project files.",
|
|
6
|
+
parameters: [
|
|
7
|
+
{ name: "pattern", required: true, description: "text to search for" },
|
|
8
|
+
{
|
|
9
|
+
name: "path",
|
|
10
|
+
required: false,
|
|
11
|
+
description: "directory to search, default process.cwd()",
|
|
12
|
+
},
|
|
13
|
+
],
|
|
14
|
+
async execute(args) {
|
|
15
|
+
const pattern = args.pattern;
|
|
16
|
+
if (!pattern || typeof pattern !== "string") {
|
|
17
|
+
throw new Error("Parameter 'pattern' is required and must be a string.");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const path =
|
|
21
|
+
args.path && typeof args.path === "string" ? args.path : process.cwd();
|
|
22
|
+
|
|
23
|
+
const proc = Bun.spawn({
|
|
24
|
+
cmd: [
|
|
25
|
+
"grep",
|
|
26
|
+
"-RIn",
|
|
27
|
+
"--exclude-dir=node_modules",
|
|
28
|
+
"--exclude-dir=.git",
|
|
29
|
+
"--exclude-dir=dist",
|
|
30
|
+
"--exclude-dir=build",
|
|
31
|
+
"--exclude-dir=.next",
|
|
32
|
+
"--exclude-dir=coverage",
|
|
33
|
+
"--exclude=bun.lock",
|
|
34
|
+
"--exclude=package-lock.json",
|
|
35
|
+
"--exclude=pnpm-lock.yaml",
|
|
36
|
+
"--exclude=yarn.lock",
|
|
37
|
+
pattern,
|
|
38
|
+
path,
|
|
39
|
+
],
|
|
40
|
+
stdout: "pipe",
|
|
41
|
+
stderr: "pipe",
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
45
|
+
proc.stdout.text(),
|
|
46
|
+
proc.stderr.text(),
|
|
47
|
+
proc.exited,
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
if (exitCode === 1) {
|
|
51
|
+
return "No matches found.";
|
|
52
|
+
}
|
|
53
|
+
if (exitCode > 1) {
|
|
54
|
+
throw new Error(stderr);
|
|
55
|
+
}
|
|
56
|
+
const MAX_OUTPUT_SIZE = 8 * 1024;
|
|
57
|
+
|
|
58
|
+
if (stdout.length <= MAX_OUTPUT_SIZE) {
|
|
59
|
+
return stdout;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
stdout.slice(0, MAX_OUTPUT_SIZE) +
|
|
64
|
+
`\n\n... Output truncated (${stdout.length - MAX_OUTPUT_SIZE} more bytes). ` +
|
|
65
|
+
`Refine your search or use read_file on a specific file.`
|
|
66
|
+
);
|
|
67
|
+
},
|
|
68
|
+
};
|
package/tools/index.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { terminalTool } from "./terminal";
|
|
2
|
+
import { readFileTool } from "./readFile";
|
|
3
|
+
import { listFilesTool } from "./listFiles";
|
|
4
|
+
import type { Tool } from "../config/types";
|
|
5
|
+
import { createFileTool } from "./createFile";
|
|
6
|
+
import { writeFileTool } from "./writeFile";
|
|
7
|
+
import { editFileTool } from "./editFile";
|
|
8
|
+
import { grepTool } from "./grep";
|
|
9
|
+
import { runTestsTool } from "./runTests";
|
|
10
|
+
import { findFilesTool } from "./findFiles";
|
|
11
|
+
|
|
12
|
+
export const toolRegistery: Tool[] = [
|
|
13
|
+
listFilesTool,
|
|
14
|
+
readFileTool,
|
|
15
|
+
terminalTool,
|
|
16
|
+
createFileTool,
|
|
17
|
+
writeFileTool,
|
|
18
|
+
editFileTool,
|
|
19
|
+
grepTool,
|
|
20
|
+
runTestsTool,
|
|
21
|
+
findFilesTool,
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
export function getTool(name: string): Tool | undefined {
|
|
25
|
+
return toolRegistery.find((tool) => tool.name === name);
|
|
26
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Tool } from "../config/types";
|
|
2
|
+
|
|
3
|
+
export const listFilesTool: Tool = {
|
|
4
|
+
name: "list_Files",
|
|
5
|
+
description: "List all the files in the directory",
|
|
6
|
+
parameters: [
|
|
7
|
+
{
|
|
8
|
+
name: "path",
|
|
9
|
+
description: "Directory path",
|
|
10
|
+
required: false,
|
|
11
|
+
},
|
|
12
|
+
],
|
|
13
|
+
|
|
14
|
+
async execute(args) {
|
|
15
|
+
const path = (args.path as string) || process.cwd();
|
|
16
|
+
|
|
17
|
+
const files: string[] = [];
|
|
18
|
+
|
|
19
|
+
for await (const file of new Bun.Glob("**/*").scan(path)) {
|
|
20
|
+
const ignoredDirs = [
|
|
21
|
+
"node_modules",
|
|
22
|
+
".git",
|
|
23
|
+
"dist",
|
|
24
|
+
"build",
|
|
25
|
+
".next",
|
|
26
|
+
"coverage",
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
if (
|
|
30
|
+
ignoredDirs.some((dir) => file === dir || file.startsWith(`${dir}/`))
|
|
31
|
+
) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
files.push(file);
|
|
36
|
+
}
|
|
37
|
+
const MAX_TOOL_OUTPUT = 8 * 1024; // 8 KB
|
|
38
|
+
const output = files.join("\n");
|
|
39
|
+
|
|
40
|
+
if (output.length <= MAX_TOOL_OUTPUT) {
|
|
41
|
+
return output;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
output.slice(0, MAX_TOOL_OUTPUT) +
|
|
46
|
+
`\n\n... Output truncated (${output.length - MAX_TOOL_OUTPUT} more characters)`
|
|
47
|
+
);
|
|
48
|
+
},
|
|
49
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { Tool } from "../config/types";
|
|
2
|
+
import { statSync } from "fs";
|
|
3
|
+
|
|
4
|
+
export const readFileTool: Tool = {
|
|
5
|
+
name: "read_file",
|
|
6
|
+
description: "Reads the contents of a file.",
|
|
7
|
+
parameters: [
|
|
8
|
+
{
|
|
9
|
+
name: "path",
|
|
10
|
+
description: "Path to the file",
|
|
11
|
+
required: true,
|
|
12
|
+
},
|
|
13
|
+
],
|
|
14
|
+
|
|
15
|
+
async execute(args) {
|
|
16
|
+
const path = args.path as string;
|
|
17
|
+
|
|
18
|
+
if (!path) {
|
|
19
|
+
throw Error("File path is required");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const file = Bun.file(path);
|
|
23
|
+
|
|
24
|
+
if (!(await file.exists())) {
|
|
25
|
+
throw Error(`File ${path} does not exist`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Check if path is a directory
|
|
29
|
+
try {
|
|
30
|
+
const stats = statSync(path);
|
|
31
|
+
if (stats.isDirectory()) {
|
|
32
|
+
throw Error(`Cannot read ${path}: it is a directory. Use list_files to see directory contents.`);
|
|
33
|
+
}
|
|
34
|
+
} catch (err) {
|
|
35
|
+
if (err instanceof Error && err.message.includes('is a directory')) {
|
|
36
|
+
throw err;
|
|
37
|
+
}
|
|
38
|
+
// If stat fails for other reasons, continue trying to read
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const MAX_OUTPUT = 16 * 1024; // 16 KB
|
|
42
|
+
const content = await file.text();
|
|
43
|
+
|
|
44
|
+
if (content.length > MAX_OUTPUT) {
|
|
45
|
+
return (
|
|
46
|
+
content.slice(0, MAX_OUTPUT) +
|
|
47
|
+
`\n\n... File truncated. Showing first ${MAX_OUTPUT} characters of ${content.length}.`
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return content;
|
|
52
|
+
},
|
|
53
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Tool } from "../config/types";
|
|
2
|
+
|
|
3
|
+
export const runTestsTool: Tool = {
|
|
4
|
+
name: "run_tests",
|
|
5
|
+
description: "Runs the project's test command.",
|
|
6
|
+
parameters: [
|
|
7
|
+
{ name: "command", required: false, description: "defaults to bun test" },
|
|
8
|
+
],
|
|
9
|
+
async execute(args) {
|
|
10
|
+
const command =
|
|
11
|
+
args.command && typeof args.command === "string"
|
|
12
|
+
? args.command
|
|
13
|
+
: "bun test";
|
|
14
|
+
|
|
15
|
+
const proc = Bun.spawn({
|
|
16
|
+
cmd: ["sh", "-c", command],
|
|
17
|
+
stdout: "pipe",
|
|
18
|
+
stderr: "pipe",
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
22
|
+
proc.stdout.text(),
|
|
23
|
+
proc.stderr.text(),
|
|
24
|
+
proc.exited,
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
return `Exit code: ${exitCode}\n\nSTDOUT:\n${stdout}\n\nSTDERR:\n${stderr}`;
|
|
28
|
+
},
|
|
29
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Tool } from "../config/types";
|
|
2
|
+
|
|
3
|
+
export const terminalTool: Tool = {
|
|
4
|
+
name: "run_terminal",
|
|
5
|
+
description: "Runs a terminal command in the current project.",
|
|
6
|
+
parameters: [
|
|
7
|
+
{
|
|
8
|
+
name: "command",
|
|
9
|
+
description: "Command to execute",
|
|
10
|
+
required: true,
|
|
11
|
+
},
|
|
12
|
+
],
|
|
13
|
+
|
|
14
|
+
async execute(args) {
|
|
15
|
+
const command = args.command as string;
|
|
16
|
+
|
|
17
|
+
if (!command) {
|
|
18
|
+
throw Error("command is required");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const proc = Bun.spawn({
|
|
22
|
+
cmd: ["sh", "-c", command],
|
|
23
|
+
stdout: "pipe",
|
|
24
|
+
stderr: "pipe",
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const stdout = await new Response(proc.stdout).text();
|
|
28
|
+
const stderr = await new Response(proc.stderr).text();
|
|
29
|
+
|
|
30
|
+
await proc.exited;
|
|
31
|
+
|
|
32
|
+
if (stderr) {
|
|
33
|
+
return stderr;
|
|
34
|
+
}
|
|
35
|
+
return stdout;
|
|
36
|
+
},
|
|
37
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { Tool } from "../config/types";
|
|
2
|
+
import { createTwoFilesPatch } from "diff";
|
|
3
|
+
import { store } from "../tui/src/store/ui-store";
|
|
4
|
+
import type { PendingEdit } from "../tui/src/types";
|
|
5
|
+
|
|
6
|
+
export const writeFileTool: Tool = {
|
|
7
|
+
name: "write_file",
|
|
8
|
+
|
|
9
|
+
description: "Overwrite an existing file.",
|
|
10
|
+
|
|
11
|
+
parameters: [
|
|
12
|
+
{
|
|
13
|
+
name: "path",
|
|
14
|
+
description: "File path",
|
|
15
|
+
required: true,
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
name: "content",
|
|
19
|
+
description: "New file contents",
|
|
20
|
+
required: true,
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
|
|
24
|
+
async execute(args) {
|
|
25
|
+
const path = args.path as string;
|
|
26
|
+
const content = args.content as string;
|
|
27
|
+
|
|
28
|
+
const file = Bun.file(path);
|
|
29
|
+
|
|
30
|
+
if (!(await file.exists())) {
|
|
31
|
+
throw new Error(`File not found: ${path}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Read current content
|
|
35
|
+
const oldContent = await file.text();
|
|
36
|
+
|
|
37
|
+
// If content is identical, skip diff preview
|
|
38
|
+
if (oldContent === content) {
|
|
39
|
+
return `No changes needed for ${path}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Generate unified diff
|
|
43
|
+
const diff = createTwoFilesPatch(path, path, oldContent, content, "", "", {
|
|
44
|
+
context: 3,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Create pending edit
|
|
48
|
+
const pendingEdit: PendingEdit = {
|
|
49
|
+
id: crypto.randomUUID(),
|
|
50
|
+
filePath: path,
|
|
51
|
+
oldContent,
|
|
52
|
+
newContent: content,
|
|
53
|
+
diff,
|
|
54
|
+
toolCallId: crypto.randomUUID(),
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Request approval from UI
|
|
58
|
+
let approved: boolean;
|
|
59
|
+
try {
|
|
60
|
+
approved = await store.setPendingEdit(pendingEdit);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
// User cancelled with Esc
|
|
63
|
+
return `Edit cancelled for ${path}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!approved) {
|
|
67
|
+
return `Edit rejected for ${path}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Write file after approval
|
|
71
|
+
await Bun.write(path, content);
|
|
72
|
+
|
|
73
|
+
return `Updated ${path}`;
|
|
74
|
+
},
|
|
75
|
+
};
|
package/tui/package.json
ADDED
|
File without changes
|
package/tui/src/app.tsx
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Box } from "ink";
|
|
2
|
+
import { Header } from "./header";
|
|
3
|
+
import { Timeline } from "./timeline";
|
|
4
|
+
import { ConnectedStatusBar } from "./statusBar";
|
|
5
|
+
import { Prompt } from "./prompt";
|
|
6
|
+
import { useUIStore } from "./store/useUIStore";
|
|
7
|
+
import { HomeScreen, type HomeScreenData } from "./components/HomeScreen";
|
|
8
|
+
import { DiffPreview } from "./components/DiffPreview";
|
|
9
|
+
import type { AgentController } from "../../commands/agentController";
|
|
10
|
+
import { useState } from "react";
|
|
11
|
+
|
|
12
|
+
interface AppProps {
|
|
13
|
+
controller: AgentController;
|
|
14
|
+
onExit: () => Promise<void>;
|
|
15
|
+
homeScreen: HomeScreenData;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function App({ controller, onExit, homeScreen }: AppProps) {
|
|
19
|
+
const state = useUIStore();
|
|
20
|
+
const [promptValue, setPromptValue] = useState("");
|
|
21
|
+
const showHome = state.timeline.length === 0;
|
|
22
|
+
const hasPendingEdit = state.pendingEdit !== null;
|
|
23
|
+
|
|
24
|
+
const promptProps = {
|
|
25
|
+
controller,
|
|
26
|
+
onExit,
|
|
27
|
+
value: promptValue,
|
|
28
|
+
onValueChange: setPromptValue,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<Box flexDirection="column" height="100%" paddingX={1}>
|
|
33
|
+
<Box marginBottom={1}>
|
|
34
|
+
<Header branch={homeScreen.branch} provider={homeScreen.providerName} />
|
|
35
|
+
</Box>
|
|
36
|
+
|
|
37
|
+
<Box flexDirection="column" flexGrow={1} paddingX={1}>
|
|
38
|
+
{hasPendingEdit ? (
|
|
39
|
+
<DiffPreview pendingEdit={state.pendingEdit!} />
|
|
40
|
+
) : showHome ? (
|
|
41
|
+
<HomeScreen
|
|
42
|
+
{...homeScreen}
|
|
43
|
+
renderPrompt={(placeholder) => (
|
|
44
|
+
<Prompt {...promptProps} placeholder={placeholder} />
|
|
45
|
+
)}
|
|
46
|
+
/>
|
|
47
|
+
) : (
|
|
48
|
+
<Timeline items={state.timeline} isThinking={state.isThinking} />
|
|
49
|
+
)}
|
|
50
|
+
</Box>
|
|
51
|
+
|
|
52
|
+
{!showHome && !hasPendingEdit && (
|
|
53
|
+
<Box flexDirection="column" marginTop={1}>
|
|
54
|
+
<ConnectedStatusBar />
|
|
55
|
+
<Prompt {...promptProps} />
|
|
56
|
+
</Box>
|
|
57
|
+
)}
|
|
58
|
+
</Box>
|
|
59
|
+
);
|
|
60
|
+
}
|