codebase-rag-tui 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/dist/api/chat.d.ts +4 -0
- package/dist/api/chat.js +14 -0
- package/dist/api/gemini.d.ts +3 -0
- package/dist/api/gemini.js +10 -0
- package/dist/app.d.ts +2 -0
- package/dist/app.js +105 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +11 -0
- package/dist/components/Header.d.ts +2 -0
- package/dist/components/Header.js +6 -0
- package/dist/components/Input.d.ts +7 -0
- package/dist/components/Input.js +20 -0
- package/dist/components/Message.d.ts +7 -0
- package/dist/components/Message.js +14 -0
- package/dist/contexts/SocketContext.d.ts +18 -0
- package/dist/contexts/SocketContext.js +120 -0
- package/dist/contexts/WorkspaceContext.d.ts +10 -0
- package/dist/contexts/WorkspaceContext.js +17 -0
- package/dist/utils/file-operations.d.ts +21 -0
- package/dist/utils/file-operations.js +58 -0
- package/dist/utils/shell-operations.d.ts +7 -0
- package/dist/utils/shell-operations.js +55 -0
- package/dist/utils/workspace.d.ts +17 -0
- package/dist/utils/workspace.js +44 -0
- package/package.json +83 -0
- package/readme.md +58 -0
package/dist/api/chat.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export async function sendMessage(question, socketId, sessionId) {
|
|
2
|
+
const res = await fetch(`${process.env['BACKEND_URI']}/remote/repo/query`, {
|
|
3
|
+
method: "POST",
|
|
4
|
+
headers: {
|
|
5
|
+
"Content-Type": "application/json", // Add this!
|
|
6
|
+
},
|
|
7
|
+
body: JSON.stringify({
|
|
8
|
+
question: question,
|
|
9
|
+
socket_id: socketId,
|
|
10
|
+
session_id: sessionId,
|
|
11
|
+
})
|
|
12
|
+
});
|
|
13
|
+
return res.json();
|
|
14
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { GoogleGenerativeAI } from '@google/generative-ai';
|
|
2
|
+
export function initGemini(apiKey) {
|
|
3
|
+
const genAI = new GoogleGenerativeAI(apiKey);
|
|
4
|
+
const model = genAI.getGenerativeModel({ model: 'gemini-2.5-flash' });
|
|
5
|
+
return model.startChat();
|
|
6
|
+
}
|
|
7
|
+
export async function sendMessage(chat, text) {
|
|
8
|
+
const result = await chat.sendMessage(text);
|
|
9
|
+
return result.response.text();
|
|
10
|
+
}
|
package/dist/app.d.ts
ADDED
package/dist/app.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { Box, Text, useApp } from 'ink';
|
|
3
|
+
import Header from './components/Header.js';
|
|
4
|
+
import Message from './components/Message.js';
|
|
5
|
+
import Input from './components/Input.js';
|
|
6
|
+
import { sendMessage } from './api/chat.js';
|
|
7
|
+
import { useSocket } from './contexts/SocketContext.js';
|
|
8
|
+
import { getWorkspaceInfo } from './utils/workspace.js';
|
|
9
|
+
import { useWorkspace } from './contexts/WorkspaceContext.js';
|
|
10
|
+
export default function App() {
|
|
11
|
+
const { exit } = useApp();
|
|
12
|
+
const { socket, isConnected, isConnecting, connectionError } = useSocket();
|
|
13
|
+
const { workspace, setWorkspace } = useWorkspace();
|
|
14
|
+
const [messages, setMessages] = useState([]);
|
|
15
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
16
|
+
const [error, setError] = useState(null);
|
|
17
|
+
const [sessionId, setSessionId] = useState(null);
|
|
18
|
+
const welcomeMessage = 'Start you session by entering the path to your repository';
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
setMessages([{ role: 'model', text: welcomeMessage }]);
|
|
21
|
+
}, []); // Only run once on mount
|
|
22
|
+
const handleSubmit = async (text) => {
|
|
23
|
+
if (text === '/help') {
|
|
24
|
+
const helpText = `Available commands:
|
|
25
|
+
• /help - Show this help message
|
|
26
|
+
• /clear - Clear conversation
|
|
27
|
+
• /quit - Leave current session and reset workspace
|
|
28
|
+
• /exit - Exit the application`;
|
|
29
|
+
setMessages((prev) => [...prev, { role: 'model', text: helpText }]);
|
|
30
|
+
}
|
|
31
|
+
if (text === '/exit') {
|
|
32
|
+
exit();
|
|
33
|
+
socket?.disconnect();
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (text === '/clear') {
|
|
37
|
+
setMessages([]);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (text === '/quit') {
|
|
41
|
+
setSessionId(null);
|
|
42
|
+
setWorkspace(process.cwd());
|
|
43
|
+
setMessages([{ role: 'model', text: welcomeMessage }]);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
setMessages((prev) => [...prev, { role: 'user', text }]);
|
|
47
|
+
if (workspace == process.cwd()) {
|
|
48
|
+
try {
|
|
49
|
+
const info = await getWorkspaceInfo(text);
|
|
50
|
+
if (info.exists && info.isDirectory) {
|
|
51
|
+
setWorkspace(info.absolutePath);
|
|
52
|
+
setMessages((prev) => [...prev, { role: 'model', text: `Workspace is set to: ${info.absolutePath}` }]);
|
|
53
|
+
}
|
|
54
|
+
else if (info.exists) {
|
|
55
|
+
setMessages((prev) => [...prev, { role: 'model', text: 'Path exists but is not a directory. Please enter a valid directory path.' }]);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
setMessages((prev) => [...prev, { role: 'model', text: 'Directory does not exist. Please enter a valid project directory path.' }]);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
const message = err instanceof Error ? err.message : 'Unknown error occurred';
|
|
63
|
+
setMessages((prev) => [...prev, { role: 'model', text: `Error validating path: ${message}` }]);
|
|
64
|
+
}
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
setIsLoading(true);
|
|
68
|
+
setError(null);
|
|
69
|
+
if (!socket || !isConnected || !socket.id) {
|
|
70
|
+
setMessages((prev) => [...prev, { role: 'model', text: 'Failed to connect to server' }]);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
if (sessionId) {
|
|
75
|
+
const res = await sendMessage(text, socket.id, sessionId);
|
|
76
|
+
setMessages((prev) => [...prev, { role: 'model', text: res.response }]);
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
const res = await sendMessage(text, socket.id);
|
|
80
|
+
setSessionId(res.session_id);
|
|
81
|
+
setMessages((prev) => [...prev, { role: 'model', text: res.response }]);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
const message = err instanceof Error ? err.message : 'Unknown error occurred';
|
|
86
|
+
setError(`Error: ${message}`);
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
setIsLoading(false);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
return (React.createElement(Box, { flexDirection: "column", padding: 1 },
|
|
93
|
+
React.createElement(Header, null),
|
|
94
|
+
React.createElement(Box, { flexDirection: "column", marginTop: 1 }, messages.map((msg, i) => (
|
|
95
|
+
// eslint-disable-next-line react/no-array-index-key
|
|
96
|
+
React.createElement(Message, { key: i, role: msg.role, text: msg.text })))),
|
|
97
|
+
(error || connectionError) && (React.createElement(Box, { marginTop: 1 },
|
|
98
|
+
React.createElement(Text, { color: "red" }, error || connectionError))),
|
|
99
|
+
isConnecting && (React.createElement(Box, { marginTop: 1 },
|
|
100
|
+
React.createElement(Text, { color: "yellow" }, "Connecting to server..."))),
|
|
101
|
+
!isConnected && !isConnecting && !connectionError && (React.createElement(Box, { marginTop: 1 },
|
|
102
|
+
React.createElement(Text, { color: "gray" }, "Disconnected from server"))),
|
|
103
|
+
React.createElement(Box, { marginTop: 1 },
|
|
104
|
+
React.createElement(Input, { onSubmit: handleSubmit, isLoading: isLoading }))));
|
|
105
|
+
}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { render } from 'ink';
|
|
4
|
+
import App from './app.js';
|
|
5
|
+
import { SocketProvider } from './contexts/SocketContext.js';
|
|
6
|
+
import { WorkspaceProvider } from './contexts/WorkspaceContext.js';
|
|
7
|
+
import { config } from 'dotenv';
|
|
8
|
+
config();
|
|
9
|
+
render(React.createElement(WorkspaceProvider, null,
|
|
10
|
+
React.createElement(SocketProvider, null,
|
|
11
|
+
React.createElement(App, null))));
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
export default function Header() {
|
|
4
|
+
return (React.createElement(Box, { borderStyle: "round", borderColor: "cyan", paddingX: 1 },
|
|
5
|
+
React.createElement(Text, { bold: true, color: "cyan" }, "Codebase RAG Agent")));
|
|
6
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import TextInput from 'ink-text-input';
|
|
4
|
+
export default function Input({ onSubmit, isLoading }) {
|
|
5
|
+
const [value, setValue] = useState('');
|
|
6
|
+
const handleSubmit = (text) => {
|
|
7
|
+
if (text.trim().length === 0) {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
onSubmit(text.trim());
|
|
11
|
+
setValue('');
|
|
12
|
+
};
|
|
13
|
+
if (isLoading) {
|
|
14
|
+
return (React.createElement(Box, null,
|
|
15
|
+
React.createElement(Text, { dimColor: true }, "Thinking...")));
|
|
16
|
+
}
|
|
17
|
+
return (React.createElement(Box, null,
|
|
18
|
+
React.createElement(Text, { bold: true, color: "green" }, '> '),
|
|
19
|
+
React.createElement(TextInput, { value: value, onChange: setValue, onSubmit: handleSubmit, placeholder: "Type a message...", focus: !isLoading })));
|
|
20
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
export default function Message({ role, text }) {
|
|
4
|
+
if (role === 'user') {
|
|
5
|
+
return (React.createElement(Box, null,
|
|
6
|
+
React.createElement(Text, { color: "green" },
|
|
7
|
+
'> ',
|
|
8
|
+
text)));
|
|
9
|
+
}
|
|
10
|
+
return (React.createElement(Box, null,
|
|
11
|
+
React.createElement(Text, { color: "blue" },
|
|
12
|
+
"Codebase Rag Agent: ",
|
|
13
|
+
text)));
|
|
14
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React, { ReactNode } from 'react';
|
|
2
|
+
import { Socket } from 'socket.io-client';
|
|
3
|
+
interface SocketContextType {
|
|
4
|
+
socket: Socket | null;
|
|
5
|
+
isConnected: boolean;
|
|
6
|
+
isConnecting: boolean;
|
|
7
|
+
connectionError: string | null;
|
|
8
|
+
connect: () => void;
|
|
9
|
+
disconnect: () => void;
|
|
10
|
+
}
|
|
11
|
+
declare const SocketContext: React.Context<SocketContextType | undefined>;
|
|
12
|
+
interface SocketProviderProps {
|
|
13
|
+
children: ReactNode;
|
|
14
|
+
serverUrl?: string;
|
|
15
|
+
}
|
|
16
|
+
export declare const SocketProvider: React.FC<SocketProviderProps>;
|
|
17
|
+
export declare const useSocket: () => SocketContextType;
|
|
18
|
+
export default SocketContext;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import React, { createContext, useContext, useEffect, useState, useRef } from 'react';
|
|
2
|
+
import { io } from 'socket.io-client';
|
|
3
|
+
import { listDirectory, readFileBytes, readFileText, writeFile } from '../utils/file-operations.js';
|
|
4
|
+
import { useWorkspace } from './WorkspaceContext.js';
|
|
5
|
+
import { runCommand } from '../utils/shell-operations.js';
|
|
6
|
+
const SocketContext = createContext(undefined);
|
|
7
|
+
export const SocketProvider = ({ children, serverUrl = process.env['BACKEND_URI'], }) => {
|
|
8
|
+
const [socket, setSocket] = useState(null);
|
|
9
|
+
const [isConnected, setIsConnected] = useState(false);
|
|
10
|
+
const [isConnecting, setIsConnecting] = useState(false);
|
|
11
|
+
const [connectionError, setConnectionError] = useState(null);
|
|
12
|
+
const { workspace } = useWorkspace();
|
|
13
|
+
const workspaceRef = useRef(workspace);
|
|
14
|
+
useEffect(() => { workspaceRef.current = workspace; }, [workspace]);
|
|
15
|
+
const connect = () => {
|
|
16
|
+
if (socket && socket.connected) {
|
|
17
|
+
return; // Already connected
|
|
18
|
+
}
|
|
19
|
+
setIsConnecting(true);
|
|
20
|
+
setConnectionError(null);
|
|
21
|
+
const newSocket = io(serverUrl, {
|
|
22
|
+
autoConnect: true,
|
|
23
|
+
reconnection: true,
|
|
24
|
+
reconnectionAttempts: 5,
|
|
25
|
+
reconnectionDelay: 1000,
|
|
26
|
+
});
|
|
27
|
+
newSocket.on('connect', () => {
|
|
28
|
+
setIsConnected(true);
|
|
29
|
+
setIsConnecting(false);
|
|
30
|
+
setConnectionError(null);
|
|
31
|
+
});
|
|
32
|
+
newSocket.on('disconnect', (reason) => {
|
|
33
|
+
setIsConnected(false);
|
|
34
|
+
setIsConnecting(false);
|
|
35
|
+
if (reason === 'io server disconnect') {
|
|
36
|
+
// Server initiated disconnect, don't reconnect automatically
|
|
37
|
+
setConnectionError('Server disconnected');
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
newSocket.on('connect_error', (error) => {
|
|
41
|
+
setIsConnecting(false);
|
|
42
|
+
setConnectionError(`Failed to connect: ${error.message}`);
|
|
43
|
+
});
|
|
44
|
+
newSocket.on('reconnect', () => {
|
|
45
|
+
setIsConnected(true);
|
|
46
|
+
setIsConnecting(false);
|
|
47
|
+
setConnectionError(null);
|
|
48
|
+
});
|
|
49
|
+
newSocket.on('reconnect_error', (error) => {
|
|
50
|
+
setConnectionError(`Reconnection failed: ${error.message}`);
|
|
51
|
+
});
|
|
52
|
+
newSocket.on('reconnect_failed', () => {
|
|
53
|
+
setIsConnecting(false);
|
|
54
|
+
setConnectionError('Failed to reconnect to server');
|
|
55
|
+
});
|
|
56
|
+
// File system event handlers
|
|
57
|
+
newSocket.on('dir:list', async (payload, ack) => {
|
|
58
|
+
console.log('dir:list event heard');
|
|
59
|
+
const result = await listDirectory(payload.dir_path, workspaceRef.current);
|
|
60
|
+
ack(result);
|
|
61
|
+
});
|
|
62
|
+
newSocket.on('bytes:read', async (payload, ack) => {
|
|
63
|
+
console.log('bytes:read event heard');
|
|
64
|
+
const result = await readFileBytes(payload.file_path, workspaceRef.current);
|
|
65
|
+
ack(result);
|
|
66
|
+
});
|
|
67
|
+
newSocket.on('file:read', async (payload, ack) => {
|
|
68
|
+
console.log('file:read event heard');
|
|
69
|
+
const result = await readFileText(payload.file_path, workspaceRef.current);
|
|
70
|
+
ack(result);
|
|
71
|
+
});
|
|
72
|
+
newSocket.on('file:write', async (payload, ack) => {
|
|
73
|
+
console.log('file:write event heard');
|
|
74
|
+
const result = await writeFile(payload.file_path, payload.content, workspaceRef.current);
|
|
75
|
+
ack(result);
|
|
76
|
+
});
|
|
77
|
+
newSocket.on("command:run", async (payload, ack) => {
|
|
78
|
+
const res = await runCommand(payload.cmd_parts, workspace, payload.timeout);
|
|
79
|
+
ack(res);
|
|
80
|
+
});
|
|
81
|
+
setSocket(newSocket);
|
|
82
|
+
};
|
|
83
|
+
const disconnect = () => {
|
|
84
|
+
if (socket) {
|
|
85
|
+
socket.disconnect();
|
|
86
|
+
setSocket(null);
|
|
87
|
+
setIsConnected(false);
|
|
88
|
+
setIsConnecting(false);
|
|
89
|
+
setConnectionError(null);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
// Auto-connect on mount if serverUrl is available
|
|
94
|
+
if (serverUrl) {
|
|
95
|
+
connect();
|
|
96
|
+
}
|
|
97
|
+
return () => {
|
|
98
|
+
if (socket) {
|
|
99
|
+
socket.disconnect();
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
}, [serverUrl]);
|
|
103
|
+
const value = {
|
|
104
|
+
socket,
|
|
105
|
+
isConnected,
|
|
106
|
+
isConnecting,
|
|
107
|
+
connectionError,
|
|
108
|
+
connect,
|
|
109
|
+
disconnect,
|
|
110
|
+
};
|
|
111
|
+
return (React.createElement(SocketContext.Provider, { value: value }, children));
|
|
112
|
+
};
|
|
113
|
+
export const useSocket = () => {
|
|
114
|
+
const context = useContext(SocketContext);
|
|
115
|
+
if (context === undefined) {
|
|
116
|
+
throw new Error('useSocket must be used within a SocketProvider');
|
|
117
|
+
}
|
|
118
|
+
return context;
|
|
119
|
+
};
|
|
120
|
+
export default SocketContext;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React, { ReactNode } from 'react';
|
|
2
|
+
interface WorkspaceContextType {
|
|
3
|
+
workspace: string;
|
|
4
|
+
setWorkspace: (newPath: string) => void;
|
|
5
|
+
}
|
|
6
|
+
export declare const WorkspaceProvider: ({ children }: {
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
}) => React.JSX.Element;
|
|
9
|
+
export declare const useWorkspace: () => WorkspaceContextType;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import React, { createContext, useContext, useState } from 'react';
|
|
2
|
+
const WorkspaceContext = createContext(undefined);
|
|
3
|
+
export const WorkspaceProvider = ({ children }) => {
|
|
4
|
+
const [workspace, setWorkspace] = useState(process.cwd());
|
|
5
|
+
const value = {
|
|
6
|
+
workspace,
|
|
7
|
+
setWorkspace,
|
|
8
|
+
};
|
|
9
|
+
return (React.createElement(WorkspaceContext.Provider, { value: value }, children));
|
|
10
|
+
};
|
|
11
|
+
export const useWorkspace = () => {
|
|
12
|
+
const context = useContext(WorkspaceContext);
|
|
13
|
+
if (!context) {
|
|
14
|
+
throw new Error('WorkspacePath must be used within a WorkspaceProvider');
|
|
15
|
+
}
|
|
16
|
+
return context;
|
|
17
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface FileSystemResult {
|
|
2
|
+
ok: boolean;
|
|
3
|
+
content?: string | Uint8Array;
|
|
4
|
+
error?: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* List contents of a directory
|
|
8
|
+
*/
|
|
9
|
+
export declare function listDirectory(dirPath: string, workspace: string): Promise<FileSystemResult>;
|
|
10
|
+
/**
|
|
11
|
+
* Read file as raw bytes
|
|
12
|
+
*/
|
|
13
|
+
export declare function readFileBytes(filePath: string, workspace: string): Promise<FileSystemResult>;
|
|
14
|
+
/**
|
|
15
|
+
* Read file as UTF-8 text
|
|
16
|
+
*/
|
|
17
|
+
export declare function readFileText(filePath: string, workspace: string): Promise<FileSystemResult>;
|
|
18
|
+
/**
|
|
19
|
+
* Write content to file
|
|
20
|
+
*/
|
|
21
|
+
export declare function writeFile(filePath: string, content: string, workspace: string): Promise<FileSystemResult>;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
/**
|
|
4
|
+
* List contents of a directory
|
|
5
|
+
*/
|
|
6
|
+
export async function listDirectory(dirPath, workspace) {
|
|
7
|
+
try {
|
|
8
|
+
const fullPath = workspace ? path.join(workspace, dirPath) : dirPath;
|
|
9
|
+
const entries = await fs.readdir(fullPath, { withFileTypes: true });
|
|
10
|
+
const content = entries.map(entry => entry.name).join('\n');
|
|
11
|
+
return { ok: true, content };
|
|
12
|
+
}
|
|
13
|
+
catch (err) {
|
|
14
|
+
return { ok: false, error: err.message };
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Read file as raw bytes
|
|
19
|
+
*/
|
|
20
|
+
export async function readFileBytes(filePath, workspace) {
|
|
21
|
+
try {
|
|
22
|
+
const fullPath = workspace ? path.join(workspace, filePath) : filePath;
|
|
23
|
+
const content = await fs.readFile(fullPath);
|
|
24
|
+
return { ok: true, content };
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
return { ok: false, error: err.message };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Read file as UTF-8 text
|
|
32
|
+
*/
|
|
33
|
+
export async function readFileText(filePath, workspace) {
|
|
34
|
+
try {
|
|
35
|
+
const fullPath = workspace ? path.join(workspace, filePath) : filePath;
|
|
36
|
+
const content = await fs.readFile(fullPath, 'utf8');
|
|
37
|
+
return { ok: true, content };
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
return { ok: false, error: err.message };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Write content to file
|
|
45
|
+
*/
|
|
46
|
+
export async function writeFile(filePath, content, workspace) {
|
|
47
|
+
try {
|
|
48
|
+
const fullPath = workspace ? path.join(workspace, filePath) : filePath;
|
|
49
|
+
const parentDir = path.dirname(fullPath);
|
|
50
|
+
// Create parent directory if it doesn't exist
|
|
51
|
+
await fs.mkdir(parentDir, { recursive: true });
|
|
52
|
+
await fs.writeFile(fullPath, content, 'utf8');
|
|
53
|
+
return { ok: true };
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
return { ok: false, error: err.message };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
export async function runCommand(cmd_parts, cwd, timeout) {
|
|
3
|
+
return new Promise((resolve) => {
|
|
4
|
+
if (cmd_parts.length === 0) {
|
|
5
|
+
resolve({
|
|
6
|
+
return_code: -1,
|
|
7
|
+
stdout: '',
|
|
8
|
+
stderr: 'Command array is empty.'
|
|
9
|
+
});
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
const [command, ...args] = cmd_parts;
|
|
13
|
+
// 1. Create the subprocess
|
|
14
|
+
const process = spawn(command, args, {
|
|
15
|
+
cwd: cwd,
|
|
16
|
+
shell: false // Use false for security if using cmd_parts array
|
|
17
|
+
});
|
|
18
|
+
let stdout = '';
|
|
19
|
+
let stderr = '';
|
|
20
|
+
// 2. Set up a timeout timer
|
|
21
|
+
const timer = setTimeout(() => {
|
|
22
|
+
process.kill();
|
|
23
|
+
resolve({
|
|
24
|
+
return_code: -1,
|
|
25
|
+
stdout: '',
|
|
26
|
+
stderr: `Command timed out after ${timeout / 1000} seconds.`
|
|
27
|
+
});
|
|
28
|
+
}, timeout);
|
|
29
|
+
// 3. Capture output
|
|
30
|
+
process.stdout.on('data', (data) => {
|
|
31
|
+
stdout += data.toString();
|
|
32
|
+
});
|
|
33
|
+
process.stderr.on('data', (data) => {
|
|
34
|
+
stderr += data.toString();
|
|
35
|
+
});
|
|
36
|
+
// 4. Handle completion
|
|
37
|
+
process.on('close', (code) => {
|
|
38
|
+
clearTimeout(timer);
|
|
39
|
+
resolve({
|
|
40
|
+
return_code: code ?? -1,
|
|
41
|
+
stdout: stdout.trim(),
|
|
42
|
+
stderr: stderr.trim()
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
// 5. Handle immediate execution errors (e.g., command not found)
|
|
46
|
+
process.on('error', (err) => {
|
|
47
|
+
clearTimeout(timer);
|
|
48
|
+
resolve({
|
|
49
|
+
return_code: -1,
|
|
50
|
+
stdout: '',
|
|
51
|
+
stderr: err.message
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validates if a given path is a valid directory
|
|
3
|
+
*/
|
|
4
|
+
export declare function isValidDirectory(dirPath: string): Promise<boolean>;
|
|
5
|
+
/**
|
|
6
|
+
* Resolves a path relative to the current working directory
|
|
7
|
+
*/
|
|
8
|
+
export declare function resolvePath(inputPath: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* Gets basic info about a workspace directory
|
|
11
|
+
*/
|
|
12
|
+
export declare function getWorkspaceInfo(dirPath: string): Promise<{
|
|
13
|
+
exists: boolean;
|
|
14
|
+
isDirectory: boolean;
|
|
15
|
+
absolutePath: string;
|
|
16
|
+
name: string;
|
|
17
|
+
}>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
/**
|
|
4
|
+
* Validates if a given path is a valid directory
|
|
5
|
+
*/
|
|
6
|
+
export async function isValidDirectory(dirPath) {
|
|
7
|
+
try {
|
|
8
|
+
const stats = await fs.stat(dirPath);
|
|
9
|
+
return stats.isDirectory();
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Resolves a path relative to the current working directory
|
|
17
|
+
*/
|
|
18
|
+
export function resolvePath(inputPath) {
|
|
19
|
+
return path.resolve(inputPath);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Gets basic info about a workspace directory
|
|
23
|
+
*/
|
|
24
|
+
export async function getWorkspaceInfo(dirPath) {
|
|
25
|
+
const absolutePath = resolvePath(dirPath);
|
|
26
|
+
const name = path.basename(absolutePath);
|
|
27
|
+
try {
|
|
28
|
+
const stats = await fs.stat(absolutePath);
|
|
29
|
+
return {
|
|
30
|
+
exists: true,
|
|
31
|
+
isDirectory: stats.isDirectory(),
|
|
32
|
+
absolutePath,
|
|
33
|
+
name
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return {
|
|
38
|
+
exists: false,
|
|
39
|
+
isDirectory: false,
|
|
40
|
+
absolutePath,
|
|
41
|
+
name
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "codebase-rag-tui",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Terminal-based AI agent for codebase RAG (Retrieval-Augmented Generation)",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "johnsonafool",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/johnsonafool/codebase-rag-tui.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/johnsonafool/codebase-rag-tui#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/johnsonafool/codebase-rag-tui/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"cli",
|
|
17
|
+
"terminal",
|
|
18
|
+
"ai",
|
|
19
|
+
"rag",
|
|
20
|
+
"codebase",
|
|
21
|
+
"tui",
|
|
22
|
+
"ink",
|
|
23
|
+
"react"
|
|
24
|
+
],
|
|
25
|
+
"bin": {
|
|
26
|
+
"codebase-rag-tui": "dist/cli.js"
|
|
27
|
+
},
|
|
28
|
+
"type": "module",
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=16"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "tsc",
|
|
34
|
+
"dev": "tsc --watch",
|
|
35
|
+
"prepublishOnly": "pnpm build",
|
|
36
|
+
"test": "prettier --check . && xo && ava"
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"dist"
|
|
40
|
+
],
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@google/generative-ai": "^0.24.1",
|
|
43
|
+
"dotenv": "^17.2.4",
|
|
44
|
+
"ink": "^4.1.0",
|
|
45
|
+
"ink-text-input": "^6.0.0",
|
|
46
|
+
"meow": "^11.0.0",
|
|
47
|
+
"react": "^18.2.0",
|
|
48
|
+
"socket.io": "^4.8.3",
|
|
49
|
+
"socket.io-client": "^4.8.3"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@sindresorhus/tsconfig": "^3.0.1",
|
|
53
|
+
"@types/react": "^18.0.32",
|
|
54
|
+
"@vdemedes/prettier-config": "^2.0.1",
|
|
55
|
+
"ava": "^5.2.0",
|
|
56
|
+
"chalk": "^5.2.0",
|
|
57
|
+
"eslint-config-xo-react": "^0.27.0",
|
|
58
|
+
"eslint-plugin-react": "^7.32.2",
|
|
59
|
+
"eslint-plugin-react-hooks": "^4.6.0",
|
|
60
|
+
"ink-testing-library": "^3.0.0",
|
|
61
|
+
"prettier": "^2.8.7",
|
|
62
|
+
"ts-node": "^10.9.1",
|
|
63
|
+
"typescript": "^5.0.3",
|
|
64
|
+
"xo": "^0.53.1"
|
|
65
|
+
},
|
|
66
|
+
"ava": {
|
|
67
|
+
"extensions": {
|
|
68
|
+
"ts": "module",
|
|
69
|
+
"tsx": "module"
|
|
70
|
+
},
|
|
71
|
+
"nodeArguments": [
|
|
72
|
+
"--loader=ts-node/esm"
|
|
73
|
+
]
|
|
74
|
+
},
|
|
75
|
+
"xo": {
|
|
76
|
+
"extends": "xo-react",
|
|
77
|
+
"prettier": true,
|
|
78
|
+
"rules": {
|
|
79
|
+
"react/prop-types": "off"
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
"prettier": "@vdemedes/prettier-config"
|
|
83
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# codebase-rag-tui
|
|
2
|
+
|
|
3
|
+
A terminal-based AI agent for codebase RAG (Retrieval-Augmented Generation). Built with [Ink](https://github.com/vadimdemedes/ink) (React for the terminal).
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g codebase-rag-tui
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Configuration
|
|
12
|
+
|
|
13
|
+
Set the following environment variables:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
export BACKEND_URI=http://localhost:3000
|
|
17
|
+
export GEMINI_API_KEY=your-gemini-api-key
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Or create a `.env` file in the directory where you run the command:
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
BACKEND_URI=http://localhost:3000
|
|
24
|
+
GEMINI_API_KEY=your-gemini-api-key
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
codebase-rag-tui
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The app will prompt you to enter the path to your repository, then you can start asking questions about your codebase.
|
|
34
|
+
|
|
35
|
+
## Commands
|
|
36
|
+
|
|
37
|
+
| Command | Description |
|
|
38
|
+
| -------- | ---------------------------------------- |
|
|
39
|
+
| `/help` | Show help message |
|
|
40
|
+
| `/clear` | Wipe the chat history |
|
|
41
|
+
| `/quit` | Leave current session and reset workspace|
|
|
42
|
+
| `/exit` | Quit the application |
|
|
43
|
+
|
|
44
|
+
## Development
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pnpm install
|
|
48
|
+
pnpm dev # watch mode
|
|
49
|
+
node dist/cli.js
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Tech Stack
|
|
53
|
+
|
|
54
|
+
- **TypeScript** + **React** via [Ink](https://github.com/vadimdemedes/ink)
|
|
55
|
+
- **Socket.IO** for real-time communication with the backend
|
|
56
|
+
- **Google Gemini API** for AI responses
|
|
57
|
+
- **ink-text-input** for terminal text input
|
|
58
|
+
- **dotenv** for environment configuration
|