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.
Files changed (67) hide show
  1. package/CONTRIBUTING.md +329 -0
  2. package/LICENSE +21 -0
  3. package/README.md +582 -0
  4. package/cli.ts +17 -0
  5. package/commands/agent.tsx +139 -0
  6. package/commands/agentController.ts +138 -0
  7. package/commands/models.ts +21 -0
  8. package/commands/providers/index.ts +12 -0
  9. package/commands/providers/listProviders.ts +28 -0
  10. package/commands/providers/login.ts +28 -0
  11. package/commands/providers/logout.ts +33 -0
  12. package/commands/providers/setProvider.ts +34 -0
  13. package/config/authProvider.ts +54 -0
  14. package/config/client.ts +163 -0
  15. package/config/config.ts +118 -0
  16. package/config/conversation.json +1 -0
  17. package/config/models.json +50 -0
  18. package/config/paths.ts +96 -0
  19. package/config/providers.json +21 -0
  20. package/config/runtime.ts +130 -0
  21. package/config/systemPrompt.ts +54 -0
  22. package/config/types.ts +88 -0
  23. package/onboarding/FLOW.md +291 -0
  24. package/onboarding/index.ts +56 -0
  25. package/onboarding/providers.ts +47 -0
  26. package/onboarding/setupWizard.tsx +193 -0
  27. package/onboarding/test-reset.ts +55 -0
  28. package/package.json +86 -0
  29. package/tools/createFile.ts +24 -0
  30. package/tools/editFile.ts +85 -0
  31. package/tools/findFiles.ts +94 -0
  32. package/tools/grep.ts +68 -0
  33. package/tools/index.ts +26 -0
  34. package/tools/listFiles.ts +49 -0
  35. package/tools/readFile.ts +53 -0
  36. package/tools/runTests.ts +29 -0
  37. package/tools/terminal.ts +37 -0
  38. package/tools/writeFile.ts +75 -0
  39. package/tui/package.json +0 -0
  40. package/tui/src/app.tsx +60 -0
  41. package/tui/src/components/ApprovalFooter.tsx +29 -0
  42. package/tui/src/components/AsciiLogo.tsx +78 -0
  43. package/tui/src/components/BootScreen.tsx +76 -0
  44. package/tui/src/components/CapabilityRow.tsx +17 -0
  45. package/tui/src/components/CodeBlock.tsx +70 -0
  46. package/tui/src/components/DiffPreview.tsx +46 -0
  47. package/tui/src/components/DiffViewer.tsx +36 -0
  48. package/tui/src/components/HomeFooter.tsx +11 -0
  49. package/tui/src/components/HomeScreen.tsx +71 -0
  50. package/tui/src/components/InlineCode.tsx +13 -0
  51. package/tui/src/components/LogoReveal.tsx +158 -0
  52. package/tui/src/components/Markdown.tsx +280 -0
  53. package/tui/src/components/MessageRenderer.tsx +84 -0
  54. package/tui/src/components/PromptCard.tsx +52 -0
  55. package/tui/src/components/StreamingCursor.tsx +13 -0
  56. package/tui/src/components/ThinkingIndicator.tsx +14 -0
  57. package/tui/src/components/ToolStatus.tsx +26 -0
  58. package/tui/src/header.tsx +25 -0
  59. package/tui/src/hooks/useBootAnimation.ts +67 -0
  60. package/tui/src/hooks/useLogoAnimation.ts +114 -0
  61. package/tui/src/index.ts +5 -0
  62. package/tui/src/prompt.tsx +66 -0
  63. package/tui/src/statusBar.tsx +83 -0
  64. package/tui/src/store/ui-store.ts +234 -0
  65. package/tui/src/store/useUIStore.ts +9 -0
  66. package/tui/src/timeline.tsx +96 -0
  67. package/tui/src/types.ts +40 -0
@@ -0,0 +1,118 @@
1
+ import type { Message } from "./types";
2
+ import { getProvidersConfigPath, getConversationPath, initializeConfig } from "./paths";
3
+
4
+ export async function getConfig() {
5
+ await initializeConfig();
6
+ const configPath = getProvidersConfigPath();
7
+ return JSON.parse(await Bun.file(configPath).text());
8
+ }
9
+
10
+ export async function saveConfig(config: any) {
11
+ await initializeConfig();
12
+ const configPath = getProvidersConfigPath();
13
+ await Bun.write(configPath, JSON.stringify(config, null, 2));
14
+ }
15
+
16
+ // for storing and apending the conversation history
17
+ export async function getConversation() {
18
+ await initializeConfig();
19
+ const conversationPath = getConversationPath();
20
+ const file = Bun.file(conversationPath);
21
+
22
+ if (!(await file.exists())) {
23
+ return [];
24
+ }
25
+
26
+ return JSON.parse(await file.text());
27
+ }
28
+
29
+ export async function saveConversation(messages: Message[]) {
30
+ await initializeConfig();
31
+ const conversationPath = getConversationPath();
32
+ await Bun.write(
33
+ conversationPath,
34
+ JSON.stringify(messages, null, 2),
35
+ );
36
+ }
37
+
38
+ export async function appendMessage(message: any) {
39
+ const conversation = await getConversation();
40
+
41
+ conversation.push(message);
42
+
43
+ await saveConversation(conversation);
44
+ }
45
+
46
+ // for context building
47
+ export async function readPackageJson() {
48
+ const file = Bun.file(`${process.cwd()}/package.json`);
49
+
50
+ if (!(await file.exists())) {
51
+ return "";
52
+ }
53
+
54
+ return await file.text();
55
+ }
56
+
57
+ export async function readReadme() {
58
+ const file = Bun.file(`${process.cwd()}/README.md`);
59
+
60
+ if (!(await file.exists())) {
61
+ return "";
62
+ }
63
+
64
+ return await file.text();
65
+ }
66
+
67
+ export async function listRepositoryFiles() {
68
+ const root = process.cwd();
69
+
70
+ const files: string[] = [];
71
+
72
+ for await (const entry of new Bun.Glob("**/*").scan(root)) {
73
+ if (
74
+ entry.startsWith("node_modules") ||
75
+ entry.startsWith(".git") ||
76
+ entry.startsWith("dist")
77
+ ) {
78
+ continue;
79
+ }
80
+
81
+ files.push(entry);
82
+ }
83
+
84
+ return files;
85
+ }
86
+
87
+ export async function buildRepositoryContext() {
88
+ const packageJson = await readPackageJson();
89
+ const readme = await readReadme();
90
+ const files = await listRepositoryFiles();
91
+
92
+ return `Repository Context\n\nPackage.json:\n${packageJson}\n\nREADME:\n${readme}\n\nFiles:\n${files.join("\n")}`;
93
+ }
94
+
95
+ export function recentMessages(
96
+ message: Message[],
97
+ maxTurns: number,
98
+ ): Message[] {
99
+ if (maxTurns <= 0 || message.length === 0) {
100
+ return [];
101
+ }
102
+
103
+ let userTurn = 0;
104
+ let startIndex = 0;
105
+
106
+ for (let i = message.length - 1; i >= 0; i--) {
107
+ if (message[i]?.role === "user") {
108
+ userTurn++;
109
+
110
+ if (userTurn == maxTurns) {
111
+ startIndex = i;
112
+ break;
113
+ }
114
+ }
115
+ }
116
+
117
+ return message.slice(startIndex);
118
+ }
@@ -0,0 +1 @@
1
+ []
@@ -0,0 +1,50 @@
1
+ [
2
+ {
3
+ "id": "gpt-5.5",
4
+ "provider": "openai",
5
+ "name": "GPT-5.5",
6
+ "contextWindow": "400000"
7
+ },
8
+
9
+ {
10
+ "id": "claud-sonnet-4",
11
+ "provider": "anthropic",
12
+ "name": "Claude Sonnet 4",
13
+ "contextWindow": "200000"
14
+ },
15
+
16
+ {
17
+ "id": "gpt-5.5",
18
+ "provider": "openai",
19
+ "name": "GPT-5.5",
20
+ "contextWindow": "400000"
21
+ },
22
+
23
+ {
24
+ "id": "gemini-2.5-flash",
25
+ "provider": "google",
26
+ "name": "Gemini 2.5 Flash",
27
+ "contextWindow": 1000000
28
+ },
29
+
30
+ {
31
+ "id": "gemini-2.5-flash-lite",
32
+ "provider": "google",
33
+ "name": "Gemini 2.5 Flash Lite",
34
+ "contextWindow": 1000000
35
+ },
36
+
37
+ {
38
+ "id": "gemini-2.0-flash",
39
+ "provider": "google",
40
+ "name": "Gemini 2.0 Flash",
41
+ "contextWindow": 1000000
42
+ },
43
+
44
+ {
45
+ "id": "gemini-2.0-flash-lite",
46
+ "provider": "google",
47
+ "name": "Gemini 2.0 Flash Lite",
48
+ "contextWindow": 1000000
49
+ }
50
+ ]
@@ -0,0 +1,96 @@
1
+ import { homedir } from "os";
2
+ import { join } from "path";
3
+ import { existsSync, mkdirSync } from "fs";
4
+
5
+ /**
6
+ * Returns the user configuration directory for Woopcode.
7
+ * Creates the directory if it doesn't exist.
8
+ *
9
+ * Location:
10
+ * - macOS/Linux: ~/.config/woopcode/
11
+ * - Windows: %LOCALAPPDATA%\woopcode\
12
+ */
13
+ export function getConfigDir(): string {
14
+ const home = homedir();
15
+
16
+ let configDir: string;
17
+
18
+ if (process.platform === "win32") {
19
+ // Windows: Use LOCALAPPDATA
20
+ const localAppData = process.env.LOCALAPPDATA || join(home, "AppData", "Local");
21
+ configDir = join(localAppData, "woopcode");
22
+ } else {
23
+ // macOS/Linux: Use XDG Base Directory Specification
24
+ const xdgConfigHome = process.env.XDG_CONFIG_HOME || join(home, ".config");
25
+ configDir = join(xdgConfigHome, "woopcode");
26
+ }
27
+
28
+ // Ensure directory exists
29
+ if (!existsSync(configDir)) {
30
+ mkdirSync(configDir, { recursive: true });
31
+ }
32
+
33
+ return configDir;
34
+ }
35
+
36
+ /**
37
+ * Get the path to providers.json
38
+ */
39
+ export function getProvidersConfigPath(): string {
40
+ return join(getConfigDir(), "providers.json");
41
+ }
42
+
43
+ /**
44
+ * Get the path to conversation.json
45
+ */
46
+ export function getConversationPath(): string {
47
+ return join(getConfigDir(), "conversation.json");
48
+ }
49
+
50
+ /**
51
+ * Get the path to models.json
52
+ */
53
+ export function getModelsPath(): string {
54
+ return join(getConfigDir(), "models.json");
55
+ }
56
+
57
+ /**
58
+ * Initialize configuration directory with default files if they don't exist.
59
+ */
60
+ export async function initializeConfig(): Promise<void> {
61
+ const configDir = getConfigDir();
62
+ const providersPath = getProvidersConfigPath();
63
+ const conversationPath = getConversationPath();
64
+
65
+ // Create default providers.json if it doesn't exist
66
+ if (!existsSync(providersPath)) {
67
+ const defaultProviders = {
68
+ defaultProvider: "google",
69
+ providers: {
70
+ google: {
71
+ type: "api",
72
+ apiKey: ""
73
+ },
74
+ groq: {
75
+ type: "api",
76
+ apiKey: ""
77
+ },
78
+ openai: {
79
+ type: "api",
80
+ apiKey: ""
81
+ },
82
+ anthropic: {
83
+ type: "api",
84
+ apiKey: ""
85
+ }
86
+ }
87
+ };
88
+
89
+ await Bun.write(providersPath, JSON.stringify(defaultProviders, null, 2));
90
+ }
91
+
92
+ // Create empty conversation.json if it doesn't exist
93
+ if (!existsSync(conversationPath)) {
94
+ await Bun.write(conversationPath, JSON.stringify([], null, 2));
95
+ }
96
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "defaultProvider": "google",
3
+ "providers": {
4
+ "google": {
5
+ "type": "api",
6
+ "apiKey": ""
7
+ },
8
+ "groq": {
9
+ "type": "api",
10
+ "apiKey": ""
11
+ },
12
+ "openai": {
13
+ "type": "api",
14
+ "apiKey": ""
15
+ },
16
+ "anthropic": {
17
+ "type": "api",
18
+ "apiKey": ""
19
+ }
20
+ }
21
+ }
@@ -0,0 +1,130 @@
1
+ import { getTool } from "../tools";
2
+ import { recentMessages } from "./config";
3
+ import type {
4
+ AgentCallbacks,
5
+ Message,
6
+ ProviderClient,
7
+ StreamEvent,
8
+ } from "./types";
9
+
10
+ export async function agentLoop(
11
+ client: ProviderClient,
12
+ messages: Message[],
13
+ repoContext: string,
14
+ callbacks: AgentCallbacks,
15
+ signal?: AbortSignal,
16
+ ) {
17
+ const MAX_ITERATIONS = 10;
18
+ const MAX_TURNS = 8;
19
+ const executedTools = new Set<string>();
20
+
21
+ let iterations = 0;
22
+
23
+ try {
24
+ while (iterations < MAX_ITERATIONS) {
25
+ iterations++;
26
+
27
+ let assistantText = "";
28
+ let toolCall: Extract<StreamEvent, { type: "tool_call" }> | null = null;
29
+
30
+ for await (const event of client.stream(
31
+ recentMessages(messages, MAX_TURNS),
32
+ repoContext,
33
+ signal,
34
+ )) {
35
+ switch (event.type) {
36
+ case "text":
37
+ assistantText += event.content;
38
+ callbacks.onText?.(event.content);
39
+ break;
40
+
41
+ case "tool_call":
42
+ toolCall = event;
43
+ break;
44
+
45
+ case "done":
46
+ break;
47
+ }
48
+ }
49
+
50
+ if (signal?.aborted) {
51
+ callbacks.onCancel?.();
52
+ return "";
53
+ }
54
+
55
+ if (!toolCall) {
56
+ messages.push({
57
+ role: "assistant",
58
+ content: assistantText,
59
+ });
60
+
61
+ callbacks.onDone?.();
62
+
63
+ return assistantText;
64
+ }
65
+
66
+ const tool = getTool(toolCall.name);
67
+
68
+ if (!tool) {
69
+ throw new Error(`Unknown tool: ${toolCall.name}`);
70
+ }
71
+
72
+ const toolKey = `${toolCall.name}:${JSON.stringify(toolCall.arguments)}`;
73
+
74
+ if (executedTools.has(toolKey)) {
75
+ throw new Error("Tool loop detected");
76
+ }
77
+
78
+ executedTools.add(toolKey);
79
+ callbacks.onToolStart?.({
80
+ id: toolCall.id,
81
+ name: toolCall.name,
82
+ arguments: toolCall.arguments,
83
+ });
84
+
85
+ messages.push({
86
+ role: "assistant_tool_call",
87
+ toolName: toolCall.name,
88
+ toolCallId: toolCall.id,
89
+ arguments: toolCall.arguments,
90
+ thoughtSignature: toolCall.thoughtSignature,
91
+ });
92
+
93
+ const result = await tool.execute(toolCall.arguments);
94
+ const MAX_TOOL_RESULT = 4000;
95
+ const toolResult =
96
+ result.length > MAX_TOOL_RESULT
97
+ ? result.slice(0, MAX_TOOL_RESULT) + "\n\n...output truncated..."
98
+ : result;
99
+
100
+ callbacks.onToolFinish?.({
101
+ id: toolCall.id,
102
+ name: toolCall.name,
103
+ arguments: toolCall.arguments,
104
+ output: toolResult,
105
+ });
106
+
107
+ messages.push({
108
+ role: "tool",
109
+ toolName: toolCall.name,
110
+ toolCallId: toolCall.id,
111
+ content: toolResult,
112
+ } as Message);
113
+ }
114
+
115
+ throw new Error(
116
+ `Agent exceeded the maximum number of iterations (${MAX_ITERATIONS}).`,
117
+ );
118
+ } catch (error) {
119
+ if (signal?.aborted) {
120
+ callbacks.onCancel?.();
121
+ return "";
122
+ }
123
+
124
+ const agentError =
125
+ error instanceof Error ? error : new Error(String(error));
126
+
127
+ callbacks.onError?.(agentError);
128
+ throw agentError;
129
+ }
130
+ }
@@ -0,0 +1,54 @@
1
+ export const SYSTEM_PROMPT = `
2
+ You are WoopCode, an autonomous software engineering agent.
3
+
4
+ Your goal is to solve software engineering tasks accurately and safely.
5
+
6
+ Always gather enough information before making decisions. Never invent code, file contents, terminal output, repository structure, or test results.
7
+
8
+ You have access to tools. Use them whenever additional information is required.
9
+
10
+ Decision process:
11
+
12
+ 1. Understand the user's request.
13
+ 2. Decide whether you already have enough information.
14
+ 3. If not, choose the most appropriate tool.
15
+ 4. Continue gathering information until you are confident.
16
+ 5. Only then provide the final answer.
17
+
18
+ Tool selection rules:
19
+
20
+ - If the user is looking for a filename, directory, or files matching a name (for example: "find every runtime file", "locate client.ts", or "find config files"), ALWAYS use find_files first.
21
+ - If the user is looking for a symbol, function, class, interface, variable, import, TODO, or any text inside files, use grep.
22
+ - Never use grep when the goal is to find files by name.
23
+ - Use read_file only after you have identified the correct file to inspect.
24
+ - To inspect a file, use read_file.
25
+ - To create a new file, use create_file.
26
+ - To overwrite an entire file, use write_file.
27
+ - To modify part of an existing file, use edit_file.
28
+ - To execute shell commands, inspect git status, install packages, build projects, or run programs, use run_terminal.
29
+ - To run the project's test suite, prefer run_tests.
30
+ - If find_files already returned the requested files, answer the user instead of searching again.
31
+ - Use grep only when you need to search file contents.
32
+ - If a tool fully answers the user's request, respond to the user immediately.
33
+ - Do not call another tool to verify the same information unless the previous tool result explicitly indicates that more searching is required.
34
+ - For filename searches, use find_files. If find_files returns the matching files, answer the user instead of calling grep.
35
+
36
+ General rules:
37
+
38
+ - Never fabricate information.
39
+ - Never claim to have read a file unless you actually used read_file.
40
+ - Never claim to know repository contents unless they are provided or discovered using tools.
41
+ - Never claim terminal output unless it comes from run_terminal.
42
+ - Never claim test results unless they come from run_tests.
43
+ - Prefer using tools over making assumptions.
44
+ - If a tool provides insufficient information, use additional tools.
45
+ - Use as many tool calls as necessary before producing a final answer.
46
+ - Be concise but complete.
47
+ - Preserve existing code style when editing files.
48
+ - Make the smallest correct change that solves the user's request.
49
+ - Do not modify unrelated code.
50
+ - Explain what changed after completing a task.
51
+ - Do not call the same tool twice with identical arguments unless the previous result was insufficient.
52
+
53
+ Continue using tools until the task is complete or no additional information can be obtained.
54
+ `;
@@ -0,0 +1,88 @@
1
+ export type ModelResponse =
2
+ | {
3
+ type: "message";
4
+ content: string;
5
+ }
6
+ | {
7
+ type: "tool_call";
8
+ id: string;
9
+ name: string;
10
+ arguments: Record<string, unknown>;
11
+ thoughtSignature?: string;
12
+ };
13
+
14
+ export interface ProviderClient {
15
+ stream(
16
+ message: Message[],
17
+ repoContext: string,
18
+ signal?: AbortSignal,
19
+ ): AsyncGenerator<StreamEvent>;
20
+ }
21
+
22
+ export type Message =
23
+ | {
24
+ role: "user";
25
+ content: string;
26
+ }
27
+ | {
28
+ role: "assistant";
29
+ content: string;
30
+ }
31
+ | {
32
+ role: "assistant_tool_call";
33
+ toolName: string;
34
+ toolCallId: string;
35
+ arguments: Record<string, unknown>;
36
+ thoughtSignature?: string;
37
+ }
38
+ | {
39
+ role: "tool";
40
+ toolName: string;
41
+ toolCallId: string;
42
+ content: string;
43
+ };
44
+
45
+ export interface ToolParameter {
46
+ name: string;
47
+ description: string;
48
+ required: boolean;
49
+ }
50
+
51
+ export interface ToolCall {
52
+ id: string;
53
+ name: string;
54
+ arguments: Record<string, unknown>;
55
+ }
56
+
57
+ export interface ToolResult extends ToolCall {
58
+ output: string;
59
+ }
60
+
61
+ export interface AgentCallbacks {
62
+ onStatus?(status: string): void;
63
+ onText?(text: string): void;
64
+ onToolStart?(tool: ToolCall): void;
65
+ onToolFinish?(tool: ToolResult): void;
66
+ onDone?(): void;
67
+ onError?(error: Error): void;
68
+ onCancel?(): void;
69
+ }
70
+
71
+ export interface Tool {
72
+ name: string;
73
+ description: string;
74
+ parameters: ToolParameter[];
75
+
76
+ execute(args: Record<string, unknown>): Promise<string>;
77
+ }
78
+
79
+ export type StreamEvent =
80
+ | { type: "text"; content: string }
81
+ | {
82
+ type: "tool_call";
83
+ id: string;
84
+ name: string;
85
+ arguments: Record<string, unknown>;
86
+ thoughtSignature?: string;
87
+ }
88
+ | { type: "done" };