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.
@@ -0,0 +1,4 @@
1
+ export declare function sendMessage(question: string, socketId: string, sessionId?: string): Promise<{
2
+ session_id: string;
3
+ response: string;
4
+ }>;
@@ -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,3 @@
1
+ import { type ChatSession } from '@google/generative-ai';
2
+ export declare function initGemini(apiKey: string): ChatSession;
3
+ export declare function sendMessage(chat: ChatSession, text: string): Promise<string>;
@@ -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
@@ -0,0 +1,2 @@
1
+ import React from 'react';
2
+ export default function App(): React.JSX.Element;
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
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
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,2 @@
1
+ import React from 'react';
2
+ export default function Header(): React.JSX.Element;
@@ -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,7 @@
1
+ import React from 'react';
2
+ type Props = {
3
+ onSubmit: (value: string) => void;
4
+ isLoading: boolean;
5
+ };
6
+ export default function Input({ onSubmit, isLoading }: Props): React.JSX.Element;
7
+ export {};
@@ -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,7 @@
1
+ import React from 'react';
2
+ type Props = {
3
+ role: 'user' | 'model';
4
+ text: string;
5
+ };
6
+ export default function Message({ role, text }: Props): React.JSX.Element;
7
+ export {};
@@ -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,7 @@
1
+ interface ShellCommandResult {
2
+ return_code: number;
3
+ stdout: string;
4
+ stderr: string;
5
+ }
6
+ export declare function runCommand(cmd_parts: string[], cwd: string, timeout: number): Promise<ShellCommandResult>;
7
+ export {};
@@ -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