glitool 1.0.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/README.md +45 -0
- package/dist/agent.js +115 -0
- package/dist/agents/coder.js +54 -0
- package/dist/agents/explainer.js +20 -0
- package/dist/agents/graph.js +28 -0
- package/dist/agents/planner.js +18 -0
- package/dist/agents/reviewer.js +25 -0
- package/dist/config.js +27 -0
- package/dist/confirmHandler.js +7 -0
- package/dist/index.js +106 -0
- package/dist/llm/router.js +25 -0
- package/dist/memory.js +98 -0
- package/dist/projectMemory.js +55 -0
- package/dist/readProject.js +51 -0
- package/dist/tools/analyzeProject.js +61 -0
- package/dist/tools/editFileTool.js +28 -0
- package/dist/tools/index.js +7 -0
- package/dist/tools/listFilesTool.js +27 -0
- package/dist/tools/readFileTool.js +20 -0
- package/dist/tools/readProject.js +64 -0
- package/dist/tools/searchCodeTool.js +18 -0
- package/dist/tools/writeFileTool.js +31 -0
- package/dist/trust/riskScorer.js +25 -0
- package/dist/ui/App.js +156 -0
- package/package.json +51 -0
package/README.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# glitool
|
|
2
|
+
|
|
3
|
+
AI coding assistant for your terminal. Powered by OpenAI.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm install -g glitool
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Setup
|
|
13
|
+
|
|
14
|
+
On first run, glitool will ask for your OpenAI API key. Get one at https://platform.openai.com/api-keys
|
|
15
|
+
|
|
16
|
+
Or set it manually:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
mkdir ~/.glitool
|
|
20
|
+
echo "OPENAI_API_KEY=sk-..." > ~/.glitool/.env
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
glitool # start AI chat session
|
|
27
|
+
glitool --explain # explain every change in simple language
|
|
28
|
+
glitool config --set-name "Your Name"
|
|
29
|
+
glitool config --set-model gpt-4o
|
|
30
|
+
glitool config --show
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Commands (inside chat)
|
|
34
|
+
|
|
35
|
+
| Command | Description |
|
|
36
|
+
|---------|-------------|
|
|
37
|
+
| /help | Show available commands |
|
|
38
|
+
| /clear | Clear current session |
|
|
39
|
+
| /reset | Clear session + memory |
|
|
40
|
+
| /exit | Save and exit |
|
|
41
|
+
|
|
42
|
+
## Requirements
|
|
43
|
+
|
|
44
|
+
- Node.js 18+
|
|
45
|
+
- OpenAI API key
|
package/dist/agent.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { writeFileTool, analyzeProjectTool, listFilesTool, readFileTool, searchCodeTool, editFileTool } from "./tools/index.js";
|
|
2
|
+
import { AIMessage, BaseMessage, HumanMessage, SystemMessage } from "@langchain/core/messages";
|
|
3
|
+
import { StructuredTool } from "@langchain/core/tools";
|
|
4
|
+
import { createReactAgent } from '@langchain/langgraph/prebuilt';
|
|
5
|
+
import { ChatOpenAI } from '@langchain/openai';
|
|
6
|
+
import { loadSession, loadSummary, saveSession, generateAndSaveSummary } from "./memory.js";
|
|
7
|
+
import { loadConfig } from "./config.js";
|
|
8
|
+
import { loadProjectMemory } from "./projectMemory.js";
|
|
9
|
+
import { config as loadEnv } from 'dotenv';
|
|
10
|
+
import { fileURLToPath } from 'url';
|
|
11
|
+
import { dirname, join } from 'path';
|
|
12
|
+
import { detectComplexity } from './llm/router.js';
|
|
13
|
+
import { runAgentGraph } from "./agents/graph.js";
|
|
14
|
+
import os from 'os';
|
|
15
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
16
|
+
const __dirname = dirname(__filename);
|
|
17
|
+
loadEnv({ path: join(os.homedir(), '.glitool', '.env') });
|
|
18
|
+
const simpleLlm = new ChatOpenAI({
|
|
19
|
+
model: 'gpt-4o-mini',
|
|
20
|
+
apiKey: process.env.OPENAI_API_KEY
|
|
21
|
+
});
|
|
22
|
+
export const llm = new ChatOpenAI({
|
|
23
|
+
model: 'gpt-4o-mini',
|
|
24
|
+
apiKey: process.env.OPENAI_API_KEY
|
|
25
|
+
});
|
|
26
|
+
const config = loadConfig();
|
|
27
|
+
const tools = [listFilesTool, readFileTool, searchCodeTool, writeFileTool, analyzeProjectTool, editFileTool];
|
|
28
|
+
export const sessionMessages = loadSession();
|
|
29
|
+
export function clearSession() {
|
|
30
|
+
sessionMessages.length = 0;
|
|
31
|
+
saveSession(sessionMessages);
|
|
32
|
+
}
|
|
33
|
+
function buildSystemPrompt() {
|
|
34
|
+
let summary = loadSummary();
|
|
35
|
+
const project = loadProjectMemory();
|
|
36
|
+
if (!summary) {
|
|
37
|
+
const rawSession = loadSession();
|
|
38
|
+
if (rawSession.length > 4) {
|
|
39
|
+
generateAndSaveSummary(rawSession, llm);
|
|
40
|
+
summary = loadSummary();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// const project = loadProjectMemory();
|
|
44
|
+
let prompt = `You are an expert coding assistant. Be concise and code-focused.
|
|
45
|
+
IMPORTANT: If any tool returns USER_CANCELLED, immediately stop all tool calls and tell the user the operation was cancelled. Never retry a cancelled operation.`;
|
|
46
|
+
if (summary)
|
|
47
|
+
prompt += `\n\nPrevious session summary:\n${summary}`;
|
|
48
|
+
if (project)
|
|
49
|
+
prompt += `\n\nProject facts:\n${JSON.stringify(project, null, 2)}`;
|
|
50
|
+
return prompt;
|
|
51
|
+
}
|
|
52
|
+
const systemPrompt = await buildSystemPrompt();
|
|
53
|
+
const simpleAgent = createReactAgent({
|
|
54
|
+
llm: simpleLlm,
|
|
55
|
+
tools,
|
|
56
|
+
stateModifier: new SystemMessage(buildSystemPrompt())
|
|
57
|
+
});
|
|
58
|
+
const complexAgent = createReactAgent({
|
|
59
|
+
llm,
|
|
60
|
+
tools,
|
|
61
|
+
stateModifier: new SystemMessage(buildSystemPrompt())
|
|
62
|
+
});
|
|
63
|
+
export async function chat(userInput, onToolCall, onStatus, onToken) {
|
|
64
|
+
const complexity = detectComplexity(userInput);
|
|
65
|
+
sessionMessages.push(new HumanMessage(userInput));
|
|
66
|
+
if (complexity === 'complex') {
|
|
67
|
+
const result = await runAgentGraph(userInput, buildSystemPrompt(), onToolCall, onStatus ?? (() => { }));
|
|
68
|
+
if (result !== null && result !== undefined) {
|
|
69
|
+
sessionMessages.push(new AIMessage(result));
|
|
70
|
+
saveSession(sessionMessages);
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
;
|
|
75
|
+
const eventStrem = simpleAgent.streamEvents({ messages: sessionMessages }, { version: 'v2' });
|
|
76
|
+
let finalResponse = '';
|
|
77
|
+
for await (const { event, data, name: eventName } of eventStrem) {
|
|
78
|
+
if (event === 'on_chat_model_stream') {
|
|
79
|
+
const chunk = data.chunk;
|
|
80
|
+
let token = '';
|
|
81
|
+
if (typeof chunk?.content === 'string') {
|
|
82
|
+
token = chunk.content;
|
|
83
|
+
}
|
|
84
|
+
else if (Array.isArray(chunk?.content)) {
|
|
85
|
+
token = chunk.content.filter((c) => c.type === 'text').map((c) => c.text ?? '').join('');
|
|
86
|
+
}
|
|
87
|
+
if (token) {
|
|
88
|
+
onToken?.(token);
|
|
89
|
+
finalResponse += token;
|
|
90
|
+
}
|
|
91
|
+
// const token = data.chunk?.content;
|
|
92
|
+
// if(token && typeof token === 'string'){
|
|
93
|
+
// onToken?.(token);
|
|
94
|
+
// finalResponse += token;
|
|
95
|
+
// }
|
|
96
|
+
}
|
|
97
|
+
if (event === 'on_tool_start') {
|
|
98
|
+
onToolCall(eventName, data.input);
|
|
99
|
+
}
|
|
100
|
+
if (event === 'on_chat_model_end') {
|
|
101
|
+
if (!finalResponse) {
|
|
102
|
+
const output = data.output;
|
|
103
|
+
if (typeof output?.content === 'string') {
|
|
104
|
+
finalResponse = output.content;
|
|
105
|
+
onToken?.(finalResponse);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (finalResponse) {
|
|
111
|
+
sessionMessages.push(new AIMessage(finalResponse));
|
|
112
|
+
}
|
|
113
|
+
saveSession(sessionMessages);
|
|
114
|
+
return finalResponse;
|
|
115
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { createReactAgent } from "@langchain/langgraph/prebuilt";
|
|
2
|
+
import { ChatOpenAI } from "@langchain/openai";
|
|
3
|
+
import { SystemMessage, HumanMessage, BaseMessage } from "@langchain/core/messages";
|
|
4
|
+
import { StructuredTool } from "@langchain/core/tools";
|
|
5
|
+
import { listFilesTool, readFileTool, searchCodeTool, editFileTool, writeFileTool } from '../tools/index.js';
|
|
6
|
+
import { scoreRisk, getRiskMessage } from "../trust/riskScorer.js";
|
|
7
|
+
import { requestConfirm } from "../confirmHandler.js";
|
|
8
|
+
const coderLlm = new ChatOpenAI({
|
|
9
|
+
model: 'gpt-4o-mini',
|
|
10
|
+
apiKey: process.env.OPENAI_API_KEY
|
|
11
|
+
});
|
|
12
|
+
const coderAgent = createReactAgent({
|
|
13
|
+
llm: coderLlm,
|
|
14
|
+
tools: [listFilesTool, readFileTool, searchCodeTool, editFileTool, writeFileTool],
|
|
15
|
+
stateModifier: new SystemMessage('You are a coding execution agent. Execute the given plan step by step using tools. Be precise and thorough.')
|
|
16
|
+
});
|
|
17
|
+
export async function runCoder(plan, userMessage, onToolCall) {
|
|
18
|
+
const stream = await coderAgent.stream({
|
|
19
|
+
messages: [new HumanMessage(`Plan to execute:\n${plan}\n\nOriginal request: ${userMessage}`)]
|
|
20
|
+
});
|
|
21
|
+
let result = '';
|
|
22
|
+
for await (const chunk of stream) {
|
|
23
|
+
if (chunk.agent?.messages) {
|
|
24
|
+
const msgs = chunk.agent.messages;
|
|
25
|
+
const msg = msgs.at(-1);
|
|
26
|
+
if (msg?.tool_calls?.length > 0) {
|
|
27
|
+
const toolCall = msg.tool_calls[0];
|
|
28
|
+
const risk = scoreRisk(toolCall.name, toolCall.args);
|
|
29
|
+
const riskMsg = getRiskMessage(toolCall.name, risk, toolCall.args);
|
|
30
|
+
if (risk === 'high') {
|
|
31
|
+
onToolCall(toolCall.name, toolCall.args);
|
|
32
|
+
result = `Blocked: I cannot write to sensitive files like ${toolCall.args?.filePath}.`;
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
onToolCall(toolCall.name, toolCall.args);
|
|
36
|
+
}
|
|
37
|
+
else if (msg?.content) {
|
|
38
|
+
result = msg.content;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// for await (const chunk of stream){
|
|
43
|
+
// if(chunk.agent?.messages){
|
|
44
|
+
// const msgs = chunk.agent.messages as BaseMessage[];
|
|
45
|
+
// const msg = msgs.at(-1);
|
|
46
|
+
// if((msg as any)?.tool_calls?.length > 0){
|
|
47
|
+
// onToolCall((msg as any).tool_calls[0].name, (msg as any).tool_calls[0].args);
|
|
48
|
+
// }else if (msg?.content){
|
|
49
|
+
// result = msg.content as string;
|
|
50
|
+
// }
|
|
51
|
+
// }
|
|
52
|
+
// }
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ChatOpenAI } from "@langchain/openai";
|
|
2
|
+
import { SystemMessage, HumanMessage } from "@langchain/core/messages";
|
|
3
|
+
const explainerLlm = new ChatOpenAI({
|
|
4
|
+
model: 'gpt-4o-mini',
|
|
5
|
+
apiKey: process.env.OPENAI_API_KEY
|
|
6
|
+
});
|
|
7
|
+
export async function explainResponse(response) {
|
|
8
|
+
if (!response || response.length < 50)
|
|
9
|
+
return '';
|
|
10
|
+
const result = await explainerLlm.invoke([
|
|
11
|
+
new SystemMessage(`You are a coding teacher. Give what an AI coding assistant just did, explain it in 2-4 simple sentences a biginner would underStand.
|
|
12
|
+
Focus on:
|
|
13
|
+
- What changed and why
|
|
14
|
+
- What concept was used
|
|
15
|
+
- What to learn next
|
|
16
|
+
Keep it friendly and short.`),
|
|
17
|
+
new HumanMessage(`What was just done:\n${response}`)
|
|
18
|
+
]);
|
|
19
|
+
return result.content;
|
|
20
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { runPlanner } from './planner.js';
|
|
2
|
+
import { runCoder } from './coder.js';
|
|
3
|
+
import { runReviewer } from './reviewer.js';
|
|
4
|
+
const MAX_ITERATIONS = 1;
|
|
5
|
+
export async function runAgentGraph(userMessage, systemPrompt, onToolCall, onStatus) {
|
|
6
|
+
onStatus('Planning...');
|
|
7
|
+
const plan = await runPlanner(userMessage, systemPrompt);
|
|
8
|
+
if (plan.trim() === 'SIMPLE') {
|
|
9
|
+
return '';
|
|
10
|
+
}
|
|
11
|
+
let coderOutPut = '';
|
|
12
|
+
let finalResponse = '';
|
|
13
|
+
let approved = false;
|
|
14
|
+
let iteration = 0;
|
|
15
|
+
while (!approved && iteration < MAX_ITERATIONS) {
|
|
16
|
+
onStatus(`Executing plan${iteration > 0 ? ' (fixing issues)' : ''}...`);
|
|
17
|
+
coderOutPut = await runCoder(plan, userMessage, onToolCall);
|
|
18
|
+
onStatus('Reviewing...');
|
|
19
|
+
const review = await runReviewer(plan, coderOutPut, userMessage);
|
|
20
|
+
approved = review.approved;
|
|
21
|
+
finalResponse = review.finalResponse;
|
|
22
|
+
if (!approved) {
|
|
23
|
+
onStatus(`Reviewer founder issues: ${review.feedback}`);
|
|
24
|
+
}
|
|
25
|
+
iteration++;
|
|
26
|
+
}
|
|
27
|
+
return finalResponse || coderOutPut;
|
|
28
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ChatOpenAI } from "@langchain/openai";
|
|
2
|
+
import { SystemMessage, HumanMessage } from "@langchain/core/messages";
|
|
3
|
+
const plannerLlm = new ChatOpenAI({
|
|
4
|
+
model: 'gpt-4o-mini',
|
|
5
|
+
apiKey: process.env.OPENAI_API_KEY
|
|
6
|
+
});
|
|
7
|
+
export async function runPlanner(userMessage, context) {
|
|
8
|
+
const response = await plannerLlm.invoke([
|
|
9
|
+
new SystemMessage(`You are a coding task planner. Given a user request, output a clear numbered plan.
|
|
10
|
+
Rules:
|
|
11
|
+
- Be specific about which files to read, edit, or create
|
|
12
|
+
- Do NOT write any code — only plan the steps
|
|
13
|
+
- Keep it to 3-6 steps maximum
|
|
14
|
+
- If the task is a simple question or explanation (not a coding task), output exactly: SIMPLE`),
|
|
15
|
+
new HumanMessage(`Context:\n${context}\n\nUser request: ${userMessage}`)
|
|
16
|
+
]);
|
|
17
|
+
return response.content;
|
|
18
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { ChatOpenAI } from "@langchain/openai";
|
|
2
|
+
import { SystemMessage, HumanMessage } from "@langchain/core/messages";
|
|
3
|
+
const reviewerLlm = new ChatOpenAI({
|
|
4
|
+
model: 'gpt-4o-mini',
|
|
5
|
+
apiKey: process.env.OPENAI_API_KEY
|
|
6
|
+
});
|
|
7
|
+
export async function runReviewer(plan, coderOutput, userMessage) {
|
|
8
|
+
const response = await reviewerLlm.invoke([
|
|
9
|
+
new SystemMessage(`You are a code reviewer. check if the coder's work correctly fulfills the user's request. Return valid JSON only:
|
|
10
|
+
{
|
|
11
|
+
"approved":true or false,
|
|
12
|
+
"feedback": "what needs fixing if not approved, otherwise empty string",
|
|
13
|
+
"finalResponse": "the final message to show the user summarizing what was done"
|
|
14
|
+
|
|
15
|
+
}`),
|
|
16
|
+
new HumanMessage(`User request: ${userMessage}\n\nPlan:\n${plan}\n\nWhat was done:\n${coderOutput}`)
|
|
17
|
+
]);
|
|
18
|
+
try {
|
|
19
|
+
const cleaned = response.content.replace(/```json|```/g, '').trim();
|
|
20
|
+
return JSON.parse(cleaned);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return { approved: true, feedback: '', finalResponse: coderOutput };
|
|
24
|
+
}
|
|
25
|
+
}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
const CONFIG_PATH = path.join(os.homedir(), '.glitool', 'config.json');
|
|
5
|
+
const DEFAULTS = {
|
|
6
|
+
name: 'Developer',
|
|
7
|
+
preferredLanguage: 'TypeScript',
|
|
8
|
+
codingStyle: 'spaces',
|
|
9
|
+
preferredModel: 'gpt-4o-mini'
|
|
10
|
+
};
|
|
11
|
+
export function loadConfig() {
|
|
12
|
+
try {
|
|
13
|
+
if (!fs.existsSync(CONFIG_PATH))
|
|
14
|
+
return DEFAULTS;
|
|
15
|
+
const data = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
|
|
16
|
+
return { ...DEFAULTS, ...data };
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return DEFAULTS;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function saveConfig(config) {
|
|
23
|
+
const current = loadConfig();
|
|
24
|
+
const updated = { ...current, ...config };
|
|
25
|
+
fs.mkdirSync(path.dirname(CONFIG_PATH), { recursive: true });
|
|
26
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(updated, null, 2), 'utf-8');
|
|
27
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import { config as dotenvConfig } from "dotenv";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
import { dirname, join } from "path";
|
|
7
|
+
import { readFileSync } from "fs";
|
|
8
|
+
import { render } from "ink";
|
|
9
|
+
import { App } from "./ui/App.js";
|
|
10
|
+
import { loadConfig, saveConfig } from "./config.js";
|
|
11
|
+
import { generateAndSaveSummary } from "./memory.js";
|
|
12
|
+
import { llm, sessionMessages } from "./agent.js";
|
|
13
|
+
import { extractAndSaveProjectMemory } from "./projectMemory.js";
|
|
14
|
+
import os from 'os';
|
|
15
|
+
import { createInflate } from "zlib";
|
|
16
|
+
import { mkdirSync, writeFileSync, existsSync } from 'fs';
|
|
17
|
+
import { createInterface } from "readline";
|
|
18
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
19
|
+
const __dirname = dirname(__filename);
|
|
20
|
+
dotenvConfig({ path: join(os.homedir(), '.glitool', '.env') });
|
|
21
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf-8'));
|
|
22
|
+
const program = new Command();
|
|
23
|
+
let explainMode = false;
|
|
24
|
+
program
|
|
25
|
+
.name('glitool')
|
|
26
|
+
.description('A CLI tool for glitool')
|
|
27
|
+
.version(pkg.version);
|
|
28
|
+
program
|
|
29
|
+
.command('hello')
|
|
30
|
+
.description('Say hello')
|
|
31
|
+
.argument('<name>', 'Name to greet')
|
|
32
|
+
.option('-s, --shout', 'Shout the greeting')
|
|
33
|
+
.action((name, option) => {
|
|
34
|
+
const msg = `Hello, ${name}!`;
|
|
35
|
+
console.log(option.shout ? msg.toUpperCase() : msg);
|
|
36
|
+
});
|
|
37
|
+
program
|
|
38
|
+
.command('config')
|
|
39
|
+
.description('Manage Your glitool profile')
|
|
40
|
+
.option('--set-name <name>', 'set your name')
|
|
41
|
+
.option('--set-language <lang>', 'Set preferred language')
|
|
42
|
+
.option('--set-style <style>', 'set coding style (tabs or spaces)')
|
|
43
|
+
.option('--set-model <model>', 'Set preferred model')
|
|
44
|
+
.option('--show', 'Show current config')
|
|
45
|
+
.action((options) => {
|
|
46
|
+
if (options.show) {
|
|
47
|
+
const cfg = loadConfig();
|
|
48
|
+
console.log(JSON.stringify(cfg, null, 2));
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (options.setName)
|
|
52
|
+
saveConfig({ name: options.setName });
|
|
53
|
+
if (options.setLanguage)
|
|
54
|
+
saveConfig({ preferredLanguage: options.setLanguage });
|
|
55
|
+
if (options.setStyle)
|
|
56
|
+
saveConfig({ codingStyle: options.setStyle });
|
|
57
|
+
if (options.setModel)
|
|
58
|
+
saveConfig({ preferredModel: options.setModel });
|
|
59
|
+
console.log('Config updated');
|
|
60
|
+
});
|
|
61
|
+
program
|
|
62
|
+
.option('-e, --explain', 'Explain every change in simple language')
|
|
63
|
+
.action((options) => {
|
|
64
|
+
explainMode = options.explain ?? false;
|
|
65
|
+
});
|
|
66
|
+
async function ensureApiKey() {
|
|
67
|
+
if (process.env.OPENAI_API_KEY)
|
|
68
|
+
return;
|
|
69
|
+
const envPath = join(os.homedir(), '.glitool', '.env');
|
|
70
|
+
if (existsSync(envPath)) {
|
|
71
|
+
dotenvConfig({ path: envPath });
|
|
72
|
+
if (process.env.OPENAI_API_KEY)
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
console.log('\nNo OpenAI API key found.');
|
|
76
|
+
console.log('Get one at https://platform.openai.com/api-keys\n');
|
|
77
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
78
|
+
const key = await new Promise(resolve => rl.question('Paste your API key: ', resolve));
|
|
79
|
+
rl.close();
|
|
80
|
+
if (!key.trim()) {
|
|
81
|
+
console.log('No key entered. Exiting.');
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
mkdirSync(join(os.homedir(), '.glitool'), { recursive: true });
|
|
85
|
+
writeFileSync(envPath, `OPENAI_API_KEY=${key.trim()}\n`, 'utf-8');
|
|
86
|
+
process.env.OPENAI_API_KEY = key.trim();
|
|
87
|
+
console.log('API key saved to ~/.glitool/.env\n');
|
|
88
|
+
}
|
|
89
|
+
const saveAndExit = async () => {
|
|
90
|
+
await generateAndSaveSummary(sessionMessages, llm);
|
|
91
|
+
await extractAndSaveProjectMemory(sessionMessages, llm);
|
|
92
|
+
process.exit(0);
|
|
93
|
+
};
|
|
94
|
+
process.on('SIGINT', saveAndExit);
|
|
95
|
+
process.on('SIGTERM', saveAndExit);
|
|
96
|
+
await ensureApiKey();
|
|
97
|
+
if (process.argv.length === 2 || process.argv.includes('--explain') || process.argv.includes('-e')) {
|
|
98
|
+
explainMode = process.argv.includes('--explain') || process.argv.includes('-e');
|
|
99
|
+
const { waitUntilExit } = render(_jsx(App, { explainMode: explainMode }));
|
|
100
|
+
await waitUntilExit();
|
|
101
|
+
// const { waitUntilExit } = render(<App/>)
|
|
102
|
+
// await waitUntilExit()
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
program.parse();
|
|
106
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const SIMPLE_PATTERNS = [
|
|
2
|
+
/^(what|who|when|where|how|why)\s.{0,60}\?$/i,
|
|
3
|
+
/^(explain|define|what is|what are|tell me about)\s/i,
|
|
4
|
+
/^(hi|hello|hey|thanks|thank you)/i,
|
|
5
|
+
/^\/\w+/,
|
|
6
|
+
];
|
|
7
|
+
const COMPLEX_PATTERNS = [
|
|
8
|
+
/refactor|rewrite|redesign/i,
|
|
9
|
+
/implement|build|create|add|generate/i,
|
|
10
|
+
/fix|debug|solve|resolve/i,
|
|
11
|
+
/optimize|improve|upgrade/i,
|
|
12
|
+
/migrate|convert|transform/i,
|
|
13
|
+
];
|
|
14
|
+
export function detectComplexity(message) {
|
|
15
|
+
const trimmed = message.trim();
|
|
16
|
+
for (const pattern of SIMPLE_PATTERNS) {
|
|
17
|
+
if (pattern.test(trimmed))
|
|
18
|
+
return 'simple';
|
|
19
|
+
}
|
|
20
|
+
for (const pattern of COMPLEX_PATTERNS) {
|
|
21
|
+
if (pattern.test(trimmed))
|
|
22
|
+
return 'complex';
|
|
23
|
+
}
|
|
24
|
+
return trimmed.length > 120 ? 'complex' : 'simple';
|
|
25
|
+
}
|
package/dist/memory.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import crypto from 'crypto';
|
|
5
|
+
import { BaseMessage, HumanMessage, SystemMessage, AIMessage, ToolMessage } from '@langchain/core/messages';
|
|
6
|
+
const SESSIONS_DIR = path.join(os.homedir(), '.glitool', 'sessions');
|
|
7
|
+
const SUMMARY_SUFFIX = '.summary.md';
|
|
8
|
+
const MAX_SAVED_MESSAGES = 40;
|
|
9
|
+
function getSessionPath() {
|
|
10
|
+
const projectHash = crypto.createHash('md5').update(process.cwd()).digest('hex').slice(0, 8);
|
|
11
|
+
return path.join(SESSIONS_DIR, `${projectHash}.json`);
|
|
12
|
+
}
|
|
13
|
+
function serialize(messages) {
|
|
14
|
+
return messages.filter(m => {
|
|
15
|
+
if (m instanceof ToolMessage)
|
|
16
|
+
return false;
|
|
17
|
+
if (m instanceof AIMessage && !m.content)
|
|
18
|
+
return false;
|
|
19
|
+
return true;
|
|
20
|
+
})
|
|
21
|
+
.map(m => {
|
|
22
|
+
if (m instanceof HumanMessage)
|
|
23
|
+
return { type: 'human', content: m.content };
|
|
24
|
+
if (m instanceof AIMessage)
|
|
25
|
+
return { type: 'ai', content: m.content };
|
|
26
|
+
return { type: 'system', content: m.content };
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
function deserialize(data) {
|
|
30
|
+
return data.map(m => {
|
|
31
|
+
if (m.type === 'human')
|
|
32
|
+
return new HumanMessage(m.content);
|
|
33
|
+
if (m.type === 'ai')
|
|
34
|
+
return new AIMessage(m.content);
|
|
35
|
+
if (m.type === 'tool')
|
|
36
|
+
return new ToolMessage({ content: m.content, tool_call_id: m.tool_call_id ?? '' });
|
|
37
|
+
return new SystemMessage(m.content);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
export function loadSession() {
|
|
41
|
+
try {
|
|
42
|
+
const file = getSessionPath();
|
|
43
|
+
if (!fs.existsSync(file))
|
|
44
|
+
return [];
|
|
45
|
+
const data = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
46
|
+
return deserialize(data);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export function saveSession(messages) {
|
|
53
|
+
try {
|
|
54
|
+
fs.mkdirSync(SESSIONS_DIR, { recursive: true });
|
|
55
|
+
const trimmed = messages.slice(-MAX_SAVED_MESSAGES);
|
|
56
|
+
fs.writeFileSync(getSessionPath(), JSON.stringify(serialize(trimmed), null, 2), 'utf-8');
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
//no-critical
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export function loadSummary() {
|
|
63
|
+
try {
|
|
64
|
+
const hash = crypto.createHash('md5').update(process.cwd()).digest('hex').slice(0, 8);
|
|
65
|
+
const file = path.join(SESSIONS_DIR, `${hash}${SUMMARY_SUFFIX}`);
|
|
66
|
+
if (!fs.existsSync(file))
|
|
67
|
+
return null;
|
|
68
|
+
return fs.readFileSync(file, 'utf-8');
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
export async function generateAndSaveSummary(messages, llm) {
|
|
75
|
+
const readable = messages
|
|
76
|
+
.filter(m => m instanceof HumanMessage || (m instanceof AIMessage && m.content))
|
|
77
|
+
.map(m => `${m instanceof HumanMessage ? 'User' : 'Assistant'}: ${m.content}`)
|
|
78
|
+
.join('\n');
|
|
79
|
+
if (!readable.trim())
|
|
80
|
+
return;
|
|
81
|
+
const response = await llm.invoke([
|
|
82
|
+
new SystemMessage('Summarize this coding session in 2-3 sentences. Focus on: what was built, what decisions ware made, and what the next step is.'),
|
|
83
|
+
new HumanMessage(readable)
|
|
84
|
+
]);
|
|
85
|
+
const hash = crypto.createHash('md5').update(process.cwd()).digest('hex').slice(0, 8);
|
|
86
|
+
const file = path.join(SESSIONS_DIR, `${hash}${SUMMARY_SUFFIX}`);
|
|
87
|
+
fs.mkdirSync(SESSIONS_DIR, { recursive: true });
|
|
88
|
+
fs.writeFileSync(file, response.content, 'utf-8');
|
|
89
|
+
}
|
|
90
|
+
export function clearSummary() {
|
|
91
|
+
try {
|
|
92
|
+
const hash = crypto.createHash('md5').update(process.cwd()).digest('hex').slice(0, 8);
|
|
93
|
+
const file = path.join(SESSIONS_DIR, `${hash}.summary.md`);
|
|
94
|
+
if (fs.existsSync(file))
|
|
95
|
+
fs.unlinkSync(file);
|
|
96
|
+
}
|
|
97
|
+
catch { }
|
|
98
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { HumanMessage, AIMessage, SystemMessage, BaseMessage } from '@langchain/core/messages';
|
|
4
|
+
const MEMORY_PATH = path.join(process.cwd(), '.glitool', 'memory.json');
|
|
5
|
+
export function loadProjectMemory() {
|
|
6
|
+
try {
|
|
7
|
+
if (!fs.existsSync(MEMORY_PATH))
|
|
8
|
+
return null;
|
|
9
|
+
return JSON.parse(fs.readFileSync(MEMORY_PATH, 'utf-8'));
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function saveProjectMemory(memory) {
|
|
16
|
+
try {
|
|
17
|
+
fs.mkdirSync(path.dirname(MEMORY_PATH), { recursive: true });
|
|
18
|
+
fs.writeFileSync(MEMORY_PATH, JSON.stringify(memory, null, 2), 'utf-8');
|
|
19
|
+
}
|
|
20
|
+
catch { }
|
|
21
|
+
}
|
|
22
|
+
export async function extractAndSaveProjectMemory(messages, llm) {
|
|
23
|
+
const readable = messages
|
|
24
|
+
.filter(m => m instanceof HumanMessage || (m instanceof AIMessage && m.content))
|
|
25
|
+
.slice(-20)
|
|
26
|
+
.map(m => `${m instanceof HumanMessage ? 'User' : 'Assistant'}: ${m.content}`)
|
|
27
|
+
.join('\n');
|
|
28
|
+
if (!readable.trim())
|
|
29
|
+
return;
|
|
30
|
+
const existing = loadProjectMemory();
|
|
31
|
+
const response = await llm.invoke([
|
|
32
|
+
new SystemMessage(`Extract structured project facts from this conversation. return valid JSON only:
|
|
33
|
+
{
|
|
34
|
+
"techStack": ["languages, frameworks, libraries mentioned"],
|
|
35
|
+
"architectureDecisions": ["key structural decisions made"],
|
|
36
|
+
"todos": ["next steps or TODOs mentioned"],
|
|
37
|
+
"lastUpdated": "${new Date().toISOString()}"
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
${existing ? `Merge with existing memory: ${JSON.stringify(existing)}` : ''}`),
|
|
41
|
+
new HumanMessage(readable)
|
|
42
|
+
]);
|
|
43
|
+
try {
|
|
44
|
+
const cleaned = response.content.replace(/```json|```/g, '').trim();
|
|
45
|
+
saveProjectMemory(JSON.parse(cleaned));
|
|
46
|
+
}
|
|
47
|
+
catch { }
|
|
48
|
+
}
|
|
49
|
+
export function clearProjectMemory() {
|
|
50
|
+
try {
|
|
51
|
+
if (fs.existsSync(MEMORY_PATH))
|
|
52
|
+
fs.unlinkSync(MEMORY_PATH);
|
|
53
|
+
}
|
|
54
|
+
catch { }
|
|
55
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import { get } from 'http';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
const SKIP_DIRS = ['node_modules', '.git', 'dist', 'build', '.next'];
|
|
5
|
+
const TEXT_EXTENSIONS = ['.ts', '.js', '.json', '.md', '.txt', '.tsx', '.jsx', '.css', '.html', '.env'];
|
|
6
|
+
const MAX_FILE_SIZE = 50 * 1024; // 50kb
|
|
7
|
+
function getFiles(dir, prefix = '') {
|
|
8
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
9
|
+
const lines = [];
|
|
10
|
+
for (const entry of entries) {
|
|
11
|
+
if (SKIP_DIRS.includes(entry.name))
|
|
12
|
+
continue;
|
|
13
|
+
const fullPath = path.join(dir, entry.name);
|
|
14
|
+
if (entry.isDirectory()) {
|
|
15
|
+
lines.push(`${prefix}${entry.name}/`);
|
|
16
|
+
lines.push(...getFiles(fullPath, prefix + ' '));
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
lines.push(`${prefix}${entry.name}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return lines;
|
|
23
|
+
}
|
|
24
|
+
function getFileContents(dir) {
|
|
25
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
26
|
+
let result = '';
|
|
27
|
+
for (const entry of entries) {
|
|
28
|
+
if (SKIP_DIRS.includes(entry.name))
|
|
29
|
+
continue;
|
|
30
|
+
const fullPath = path.join(dir, entry.name);
|
|
31
|
+
if (entry.isDirectory()) {
|
|
32
|
+
result += getFileContents(fullPath);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
const ext = path.extname(entry.name);
|
|
36
|
+
if (!TEXT_EXTENSIONS.includes(ext))
|
|
37
|
+
continue;
|
|
38
|
+
const stats = fs.statSync(fullPath);
|
|
39
|
+
if (stats.size > MAX_FILE_SIZE)
|
|
40
|
+
continue;
|
|
41
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
42
|
+
result += `\n--- File: ${fullPath} ---\n${content}\n`;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
export function getProjectContext(dir) {
|
|
48
|
+
const structure = getFiles(dir).join('\n');
|
|
49
|
+
const contents = getFileContents(dir);
|
|
50
|
+
return `Folder structure:\n${structure}\n\nFile contents:${contents}`;
|
|
51
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { tool } from "@langchain/core/tools";
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { execSync } from "child_process";
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
const SKIP_DIRS = ['node_modules', '.git', 'dist', 'build', '.next'];
|
|
7
|
+
function getDependencyList() {
|
|
8
|
+
const packageJsonPath = path.join(process.cwd(), 'package.json');
|
|
9
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
10
|
+
return packageJson.dependencies || {};
|
|
11
|
+
}
|
|
12
|
+
function checkForOutdatedDependencies() {
|
|
13
|
+
try {
|
|
14
|
+
const result = execSync('npm outdated --json', { timeout: 10000, stdio: 'pipe' }).toString();
|
|
15
|
+
return JSON.parse(result);
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
return {};
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function analyzeCodeQuality() {
|
|
22
|
+
try {
|
|
23
|
+
const result = execSync(`eslint . --format json`, { timeout: 10000, stdio: 'pipe' }).toString();
|
|
24
|
+
return JSON.parse(result);
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
return { error: [], warnings: [] };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function getFiles(dir) {
|
|
31
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
32
|
+
const files = [];
|
|
33
|
+
for (const entry of entries) {
|
|
34
|
+
if (SKIP_DIRS.includes(entry.name))
|
|
35
|
+
continue;
|
|
36
|
+
const fullPath = path.join(dir, entry.name);
|
|
37
|
+
if (entry.isDirectory()) {
|
|
38
|
+
files.push(...getFiles(fullPath));
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
files.push(fullPath);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return files;
|
|
45
|
+
}
|
|
46
|
+
export const analyzeProjectTool = tool(async () => {
|
|
47
|
+
const dependencies = getDependencyList();
|
|
48
|
+
const outdatedDependencies = checkForOutdatedDependencies();
|
|
49
|
+
const codeQualityResults = analyzeCodeQuality();
|
|
50
|
+
const projectFiles = getFiles(process.cwd());
|
|
51
|
+
return {
|
|
52
|
+
dependencies,
|
|
53
|
+
outdatedDependencies,
|
|
54
|
+
codeQualityResults,
|
|
55
|
+
projectFiles
|
|
56
|
+
};
|
|
57
|
+
}, {
|
|
58
|
+
name: 'analyzeProject',
|
|
59
|
+
description: 'analyze the current project for dependencies, code quality , and project structure . useful for getting an overview of the project and identifying potential issues.',
|
|
60
|
+
schema: z.object({})
|
|
61
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { tool } from "@langchain/core/tools";
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
export const editFileTool = tool(async ({ filePath, oldString, newString }) => {
|
|
6
|
+
const fullPath = path.resolve(process.cwd(), filePath);
|
|
7
|
+
if (!fullPath.startsWith(process.cwd())) {
|
|
8
|
+
throw new Error('Access denied: outside project root');
|
|
9
|
+
}
|
|
10
|
+
if (!fs.existsSync(fullPath)) {
|
|
11
|
+
throw new Error(`File not found: ${filePath}`);
|
|
12
|
+
}
|
|
13
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
14
|
+
if (!content.includes(oldString)) {
|
|
15
|
+
throw new Error(`String not found in file. Make sure oldString matches exactly, including whitespace and case.`);
|
|
16
|
+
}
|
|
17
|
+
const updated = content.replace(oldString, newString);
|
|
18
|
+
fs.writeFileSync(fullPath, updated, 'utf-8');
|
|
19
|
+
return `Successfully edited ${filePath}`;
|
|
20
|
+
}, {
|
|
21
|
+
name: 'editFile',
|
|
22
|
+
description: 'Make a targeted edit to an existing file by replacing an exact string. Use readFile first to get the current content, then provide the exact oldString to replace.',
|
|
23
|
+
schema: z.object({
|
|
24
|
+
filePath: z.string().describe('Relative path to the file from the project root'),
|
|
25
|
+
oldString: z.string().describe('The exact string to find and replace - must match exactly including whitespace and indentation'),
|
|
26
|
+
newString: z.string().describe('The string to replace it with')
|
|
27
|
+
})
|
|
28
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { readProjectTool } from './readProject.js';
|
|
2
|
+
export { writeFileTool } from './writeFileTool.js';
|
|
3
|
+
export { analyzeProjectTool } from './analyzeProject.js';
|
|
4
|
+
export { listFilesTool } from './listFilesTool.js';
|
|
5
|
+
export { readFileTool } from './readFileTool.js';
|
|
6
|
+
export { searchCodeTool } from './searchCodeTool.js';
|
|
7
|
+
export { editFileTool } from './editFileTool.js';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { tool } from '@langchain/core/tools';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
const SKIP_DIRS = ['node_modules', '.git', 'dist', 'build', '.next'];
|
|
6
|
+
function buildTree(dir, prefix = '') {
|
|
7
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
8
|
+
const lines = [];
|
|
9
|
+
for (const entry of entries) {
|
|
10
|
+
if (SKIP_DIRS.includes(entry.name))
|
|
11
|
+
continue;
|
|
12
|
+
if (entry.isSymbolicLink())
|
|
13
|
+
continue;
|
|
14
|
+
lines.push(`${prefix}${entry.isDirectory() ? '📁' : '📄'} ${entry.name}`);
|
|
15
|
+
if (entry.isDirectory()) {
|
|
16
|
+
const sub = buildTree(path.join(dir, entry.name), prefix + ' ');
|
|
17
|
+
if (sub)
|
|
18
|
+
lines.push(sub);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return lines.join('\n');
|
|
22
|
+
}
|
|
23
|
+
export const listFilesTool = tool(async () => buildTree(process.cwd()), {
|
|
24
|
+
name: 'listFiles',
|
|
25
|
+
description: 'Lists files and directories in the current working directory. It returns a tree-like structure with folders and files.',
|
|
26
|
+
schema: z.object({})
|
|
27
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { tool } from '@langchain/core/tools';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
export const readFileTool = tool(async ({ filePath }) => {
|
|
6
|
+
const fullPath = path.resolve(process.cwd(), filePath);
|
|
7
|
+
if (!fullPath.startsWith(process.cwd())) {
|
|
8
|
+
throw new Error('Access denied: outside of current working directory');
|
|
9
|
+
}
|
|
10
|
+
if (!fs.existsSync(fullPath)) {
|
|
11
|
+
throw new Error(`File not found: ${filePath}`);
|
|
12
|
+
}
|
|
13
|
+
return fs.readFileSync(fullPath, 'utf-8');
|
|
14
|
+
}, {
|
|
15
|
+
name: 'readFile',
|
|
16
|
+
description: 'Reads the contents of a specific file. Use this tool to read the contents of a file in the current working directory. Provide the relative path to the file you want to read.',
|
|
17
|
+
schema: z.object({
|
|
18
|
+
filePath: z.string().describe('Relative path to the file from the project root')
|
|
19
|
+
})
|
|
20
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { tool } from "@langchain/core/tools";
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
const SKIP_DIRS = ['node_modules', '.git', 'dist', 'build', '.next'];
|
|
6
|
+
const TEXT_EXTENSIONS = ['.ts', '.js', '.json', '.md', '.txt', '.tsx', '.jsx', '.css', '.html'];
|
|
7
|
+
const SKIP_FILE_PREFIXES = ['.env'];
|
|
8
|
+
const MAX_FILE_SIZE = 50 * 1024; // 50kb
|
|
9
|
+
function getFiles(dir, prefix = '') {
|
|
10
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
11
|
+
const lines = [];
|
|
12
|
+
for (const entry of entries) {
|
|
13
|
+
if (SKIP_DIRS.includes(entry.name))
|
|
14
|
+
continue;
|
|
15
|
+
if (entry.isSymbolicLink())
|
|
16
|
+
continue;
|
|
17
|
+
const fullPath = path.join(dir, entry.name);
|
|
18
|
+
if (entry.isDirectory()) {
|
|
19
|
+
lines.push(`${prefix}${entry.name}/`);
|
|
20
|
+
lines.push(...getFiles(fullPath, prefix + ' '));
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
lines.push(`${prefix}${entry.name}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return lines;
|
|
27
|
+
}
|
|
28
|
+
function getFileContents(dir) {
|
|
29
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
30
|
+
let result = '';
|
|
31
|
+
for (const entry of entries) {
|
|
32
|
+
if (SKIP_DIRS.includes(entry.name))
|
|
33
|
+
continue;
|
|
34
|
+
if (SKIP_FILE_PREFIXES.some(p => entry.name.startsWith(p)))
|
|
35
|
+
continue;
|
|
36
|
+
if (entry.isSymbolicLink())
|
|
37
|
+
continue;
|
|
38
|
+
const fullPath = path.join(dir, entry.name);
|
|
39
|
+
if (entry.isDirectory()) {
|
|
40
|
+
result += getFileContents(fullPath);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
const ext = path.extname(entry.name);
|
|
44
|
+
if (!TEXT_EXTENSIONS.includes(ext))
|
|
45
|
+
continue;
|
|
46
|
+
const stats = fs.statSync(fullPath);
|
|
47
|
+
if (stats.size > MAX_FILE_SIZE)
|
|
48
|
+
continue;
|
|
49
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
50
|
+
result += `\n--- File: ${fullPath} ---\n${content}\n`;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
export const readProjectTool = tool(async () => {
|
|
56
|
+
const dir = process.cwd();
|
|
57
|
+
const structure = getFiles(dir).join('\n');
|
|
58
|
+
const contents = getFileContents(dir);
|
|
59
|
+
return `Folder structure:\n${structure}\n\nFile contents:${contents}`;
|
|
60
|
+
}, {
|
|
61
|
+
name: 'readProject',
|
|
62
|
+
description: 'Read the current project structure and file contents. Useful for providing context to the assistant.',
|
|
63
|
+
schema: z.object({})
|
|
64
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { tool } from "@langchain/core/tools";
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
export const searchCodeTool = tool(async ({ keyword }) => {
|
|
5
|
+
try {
|
|
6
|
+
const result = execSync(`grep -rn "${keyword}" --include="*.ts" --include="*.js" --include="*.json" --exclude-dir=node_modules --exclude-dir=dist --exclude-dir=dist --exclude-dir=.git`, { cwd: process.cwd(), timeout: 10000, stdio: 'pipe' }).toString();
|
|
7
|
+
return result || 'No Matches found.';
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return 'No matches found.';
|
|
11
|
+
}
|
|
12
|
+
}, {
|
|
13
|
+
name: 'searchCode',
|
|
14
|
+
description: 'Search for a keyword or function name across all project files. Returns file paths and line numbers where the keyword is found. Use this tool to quickly locate where a specific function or variable is used in the codebase.',
|
|
15
|
+
schema: z.object({
|
|
16
|
+
keyword: z.string().describe('The Keyword, funtion name, or pattern to search for in the codebase')
|
|
17
|
+
})
|
|
18
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { tool } from "@langchain/core/tools";
|
|
2
|
+
import { confirm } from "@inquirer/prompts";
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { requestConfirm } from "../confirmHandler.js";
|
|
7
|
+
export const writeFileTool = tool(async ({ filePath, content }) => {
|
|
8
|
+
const projectRoot = process.cwd();
|
|
9
|
+
const fullPath = path.resolve(projectRoot, filePath);
|
|
10
|
+
if (!fullPath.startsWith(projectRoot + path.sep) && fullPath !== projectRoot) {
|
|
11
|
+
throw new Error('Access denied: cannot write outside project root');
|
|
12
|
+
}
|
|
13
|
+
console.log(`\n📄 Write to: ${filePath}`);
|
|
14
|
+
console.log('_'.repeat(40));
|
|
15
|
+
console.log(content.slice(0, 500) + (content.length > 500 ? '\n...(truncated)' : ''));
|
|
16
|
+
console.log('_'.repeat(40));
|
|
17
|
+
const ok = await requestConfirm(`Write to: ${filePath}?`);
|
|
18
|
+
if (!ok) {
|
|
19
|
+
return 'USER_CANCELLED: The user explicitly rejected this file write. Do NOT retry. Inform the user the write was cancelled.';
|
|
20
|
+
}
|
|
21
|
+
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
22
|
+
fs.writeFileSync(fullPath, content, 'utf-8');
|
|
23
|
+
return `File written: ${filePath}`;
|
|
24
|
+
}, {
|
|
25
|
+
name: 'writeFile',
|
|
26
|
+
description: 'Create or overwrite a file with the given content. Always use editFile for modifying existing files - only use this to creat new files or completely replace files. Provide the relative file path and the full content to write.',
|
|
27
|
+
schema: z.object({
|
|
28
|
+
filePath: z.string().describe('Relative path to the file from the project root. For example: src/utils/helpers.ts'),
|
|
29
|
+
content: z.string().describe('The full content to write to the file. The agent should provide the complete content of the file, not just the changes. Use editFile for making targeted edits to existing files.')
|
|
30
|
+
})
|
|
31
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const HIGH_RISK_PATTERNS = [
|
|
2
|
+
/\.env/,
|
|
3
|
+
/\.env\.\w+/,
|
|
4
|
+
/package\.json$/,
|
|
5
|
+
/tsconfig\.json$/,
|
|
6
|
+
/docker-compose/,
|
|
7
|
+
/\.git\//,
|
|
8
|
+
];
|
|
9
|
+
const LOW_RISK_TOOLS = ['listFiles', 'readFile', 'searchCode'];
|
|
10
|
+
export function scoreRisk(toolName, args) {
|
|
11
|
+
if (LOW_RISK_TOOLS.includes(toolName))
|
|
12
|
+
return 'low';
|
|
13
|
+
const filePath = args?.filePath ?? args?.path ?? '';
|
|
14
|
+
if (HIGH_RISK_PATTERNS.some(p => p.test(filePath)))
|
|
15
|
+
return 'high';
|
|
16
|
+
return 'medium';
|
|
17
|
+
}
|
|
18
|
+
export function getRiskMessage(toolName, riskLevel, args) {
|
|
19
|
+
const filePath = args?.filePath ?? '';
|
|
20
|
+
if (riskLevel === 'high')
|
|
21
|
+
return `Blocked: writing to ${filePath} is not allowed.`;
|
|
22
|
+
if (riskLevel === 'medium')
|
|
23
|
+
return `${toolName}${filePath ? ` -> ${filePath}` : ''}`;
|
|
24
|
+
return '';
|
|
25
|
+
}
|
package/dist/ui/App.js
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React, { useState, useCallback } from "react";
|
|
3
|
+
import { Box, Text, useApp } from 'ink';
|
|
4
|
+
import TextInput from "ink-text-input";
|
|
5
|
+
import { chat, clearSession, llm, sessionMessages } from '../agent.js';
|
|
6
|
+
import { clearSummary, generateAndSaveSummary } from "../memory.js";
|
|
7
|
+
import { clearProjectMemory, extractAndSaveProjectMemory } from "../projectMemory.js";
|
|
8
|
+
import { useInput } from 'ink';
|
|
9
|
+
import { setConfirmHandler } from "../confirmHandler.js";
|
|
10
|
+
import { explainResponse } from "../agents/explainer.js";
|
|
11
|
+
const COMMANDS = ['/help', '/clear', 'model', '/tools', '/exit', '/reset'];
|
|
12
|
+
export const App = ({ explainMode = false }) => {
|
|
13
|
+
const { exit } = useApp();
|
|
14
|
+
const [messages, setMessages] = useState([]);
|
|
15
|
+
const [input, setInput] = useState('');
|
|
16
|
+
const [isThinking, setIsThinking] = useState(false);
|
|
17
|
+
const [toolInfo, setToolInfo] = useState('');
|
|
18
|
+
const [suggestions, setSuggestions] = useState([]);
|
|
19
|
+
const [confirmMessage, setConfirmMessage] = useState('');
|
|
20
|
+
const [confirmInput, setConfirmInput] = useState('');
|
|
21
|
+
const [confirmResolver, setConfirmResolver] = useState(null);
|
|
22
|
+
const [streamingContent, setStreamingContent] = useState('');
|
|
23
|
+
const [inputHistory, setInputHistory] = useState([]);
|
|
24
|
+
const [historyIndex, setHistoryIndex] = useState(-1);
|
|
25
|
+
const handleChange = (value) => {
|
|
26
|
+
setInput(value);
|
|
27
|
+
if (value.startsWith('/')) {
|
|
28
|
+
const matches = COMMANDS.filter(c => c.startsWith(value));
|
|
29
|
+
setSuggestions(value === matches[0] ? [] : matches);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
setSuggestions([]);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const handleExit = async () => {
|
|
36
|
+
await generateAndSaveSummary(sessionMessages, llm);
|
|
37
|
+
await extractAndSaveProjectMemory(sessionMessages, llm);
|
|
38
|
+
exit();
|
|
39
|
+
};
|
|
40
|
+
useInput((_, key) => {
|
|
41
|
+
if (confirmMessage)
|
|
42
|
+
return;
|
|
43
|
+
if (isThinking)
|
|
44
|
+
return;
|
|
45
|
+
if (key.upArrow) {
|
|
46
|
+
if (inputHistory.length === 0)
|
|
47
|
+
return;
|
|
48
|
+
const newIndex = Math.min(historyIndex + 1, inputHistory.length - 1);
|
|
49
|
+
setHistoryIndex(newIndex);
|
|
50
|
+
setInput(inputHistory[inputHistory.length - 1 - newIndex] ?? '');
|
|
51
|
+
}
|
|
52
|
+
if (key.downArrow) {
|
|
53
|
+
if (historyIndex <= 0) {
|
|
54
|
+
setHistoryIndex(-1);
|
|
55
|
+
setInput('');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const newIndex = historyIndex - 1;
|
|
59
|
+
setHistoryIndex(newIndex);
|
|
60
|
+
setInput(inputHistory[inputHistory.length - 1 - newIndex] ?? '');
|
|
61
|
+
}
|
|
62
|
+
if (key.tab && suggestions.length > 0) {
|
|
63
|
+
setInput(suggestions[0] ?? '');
|
|
64
|
+
setSuggestions([]);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
React.useEffect(() => {
|
|
68
|
+
setConfirmHandler((messages) => {
|
|
69
|
+
return new Promise((resolve) => {
|
|
70
|
+
setConfirmMessage(messages);
|
|
71
|
+
setConfirmResolver(() => resolve);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
}, []);
|
|
75
|
+
const handleSubmit = useCallback(async (value) => {
|
|
76
|
+
const cmd = value.trim();
|
|
77
|
+
setInput('');
|
|
78
|
+
setSuggestions([]);
|
|
79
|
+
if (!cmd)
|
|
80
|
+
return;
|
|
81
|
+
setInputHistory(prev => prev[prev.length - 1] === cmd ? prev : [...prev, cmd]);
|
|
82
|
+
setHistoryIndex(-1);
|
|
83
|
+
if (cmd === '/exit') {
|
|
84
|
+
await handleExit();
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (cmd === '/clear') {
|
|
88
|
+
clearSession();
|
|
89
|
+
setMessages([]);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (cmd === '/reset') {
|
|
93
|
+
clearSession();
|
|
94
|
+
clearSummary();
|
|
95
|
+
clearProjectMemory();
|
|
96
|
+
setMessages([]);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (cmd === '/help') {
|
|
100
|
+
setMessages(prev => [...prev, {
|
|
101
|
+
role: 'assistant',
|
|
102
|
+
content: 'Commands: /exit /clear /model /tools /help'
|
|
103
|
+
}]);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (cmd === '/model') {
|
|
107
|
+
setMessages(prev => [...prev, { role: 'assistant', content: 'Model: gpr-5-mini' }]);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (cmd === '/tools') {
|
|
111
|
+
setMessages(prev => [...prev, {
|
|
112
|
+
role: 'assistant',
|
|
113
|
+
content: 'Tools: listFile readFile searchCode editFile writeFile analyzeProject'
|
|
114
|
+
}]);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
setMessages(prev => [...prev, { role: 'user', content: cmd }]);
|
|
118
|
+
setIsThinking(true);
|
|
119
|
+
try {
|
|
120
|
+
const reply = await chat(cmd, (toolName, args) => {
|
|
121
|
+
const argStr = args ? String(Object.values(args)[0]) : '';
|
|
122
|
+
setToolInfo(`⚙ ${toolName}${argStr ? ` -> ${argStr}` : ''} `);
|
|
123
|
+
}, (status) => {
|
|
124
|
+
setToolInfo(status);
|
|
125
|
+
}, (token) => {
|
|
126
|
+
setStreamingContent(prev => prev + token);
|
|
127
|
+
});
|
|
128
|
+
setStreamingContent('');
|
|
129
|
+
setMessages(prev => [...prev, { role: 'assistant', content: reply }]);
|
|
130
|
+
if (explainMode && reply) {
|
|
131
|
+
const explanation = await explainResponse(reply);
|
|
132
|
+
if (explanation) {
|
|
133
|
+
setMessages(prev => [...prev, { role: 'explain', content: explanation }]);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
setMessages(prev => [...prev, {
|
|
139
|
+
role: 'error',
|
|
140
|
+
content: err?.message ?? 'Something went wrong.'
|
|
141
|
+
}]);
|
|
142
|
+
}
|
|
143
|
+
finally {
|
|
144
|
+
setIsThinking(false);
|
|
145
|
+
setToolInfo('');
|
|
146
|
+
}
|
|
147
|
+
}, [exit]);
|
|
148
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, color: "green", children: "glitool" }), _jsx(Text, { dimColor: true, children: "- AI coding assistant" })] }), messages.map((msg, i) => (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [msg.role === 'user' && (_jsxs(Box, { borderStyle: "round", borderColor: "white", paddingX: 1, children: [_jsx(Text, { bold: true, color: "white", children: "You" }), _jsx(Text, { wrap: "wrap", children: msg.content })] })), msg.role === 'assistant' && (_jsxs(Box, { borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsx(Text, { bold: true, color: "red", children: " Assistant " }), _jsxs(Text, { color: "red", wrap: "wrap", children: [" ", msg.content, " "] })] })), msg.role === 'error' && (_jsxs(Box, { borderStyle: "round", borderColor: "red", paddingX: 1, children: [_jsx(Text, { bold: true, color: "red", children: " Error " }), _jsx(Text, { color: "red", wrap: "wrap", children: msg.content })] })), msg.role === 'explain' && (_jsxs(Box, { borderStyle: "round", borderColor: "magenta", paddingX: 1, children: [_jsx(Text, { bold: true, color: "magenta", children: "\uD83D\uDCA1 Explain " }), _jsx(Text, { wrap: "wrap", children: msg.content })] }))] }, i))), streamingContent !== '' && (_jsxs(Box, { borderStyle: "round", borderColor: "cyan", paddingX: 1, marginBottom: 1, children: [_jsx(Text, { bold: true, color: 'cyan', children: "Assistant" }), _jsxs(Text, { wrap: "wrap", children: [streamingContent, "\u258A"] })] })), isThinking && (_jsx(Box, { marginBottom: 1, padding: 1, children: _jsx(Text, { color: "yellow", children: toolInfo || 'Thinking...' }) })), suggestions.length > 0 && (_jsx(Box, { flexDirection: "column", marginBottom: 1, paddingX: 2, children: suggestions.map(s => (_jsx(Text, { dimColor: true, children: s }, s))) })), confirmMessage !== '' ? (_jsxs(Box, { borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [_jsxs(Text, { color: "yellow", children: [" ", confirmMessage, " (y/n): "] }), _jsx(TextInput, { value: confirmInput, onChange: setConfirmInput, onSubmit: (val) => {
|
|
149
|
+
const approved = val.toLowerCase() === 'y' || val === '';
|
|
150
|
+
confirmResolver?.(approved);
|
|
151
|
+
setConfirmMessage('');
|
|
152
|
+
setConfirmInput('');
|
|
153
|
+
setConfirmResolver(null);
|
|
154
|
+
} })] })) :
|
|
155
|
+
_jsxs(Box, { borderStyle: "round", borderColor: input.length > 200 ? 'yellow' : 'green', paddingX: 1, children: [_jsx(Text, { bold: true, color: "green", children: " You: " }), _jsx(TextInput, { value: input, onChange: handleChange, onSubmit: handleSubmit, placeholder: "Type a message or /help..." }), input.length > 50 && (_jsxs(Text, { dimColor: true, children: [" [", input.length, "]"] }))] })] }));
|
|
156
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "glitool",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"dev": "tsx src/index.ts",
|
|
10
|
+
"prepublishOnly": "npm run build"
|
|
11
|
+
},
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "https://github.com/deep9038/glitool"
|
|
15
|
+
},
|
|
16
|
+
"bin": {
|
|
17
|
+
"glitool": "./dist/index.js"
|
|
18
|
+
},
|
|
19
|
+
"keywords": ["ai", "cli", "coding-assistant", "openai", "terminal"],
|
|
20
|
+
"author": "Deep Sarkar <deep22sarkar@gmail.com>",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"description": "AI coding assistant for your terminal",
|
|
23
|
+
"homepage":"https://github.com/deep9038/glitool",
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@inquirer/prompts": "^8.4.1",
|
|
26
|
+
"@langchain/core": "^1.1.40",
|
|
27
|
+
"@langchain/langgraph": "^1.2.9",
|
|
28
|
+
"@langchain/ollama": "^1.2.6",
|
|
29
|
+
"@langchain/openai": "^1.4.4",
|
|
30
|
+
"chalk": "^5.6.2",
|
|
31
|
+
"commander": "^14.0.3",
|
|
32
|
+
"dotenv": "^17.4.2",
|
|
33
|
+
"ink": "^7.0.1",
|
|
34
|
+
"ink-text-input": "^6.0.0",
|
|
35
|
+
"ora": "^9.3.0",
|
|
36
|
+
"react": "^19.2.5"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/node": "^25.6.0",
|
|
40
|
+
"@types/react": "^19.2.14",
|
|
41
|
+
"tsx": "^4.21.0",
|
|
42
|
+
"typescript": "^6.0.2"
|
|
43
|
+
},
|
|
44
|
+
"type": "module",
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": ">=18.0.0"
|
|
47
|
+
},
|
|
48
|
+
"files": [
|
|
49
|
+
"dist/"
|
|
50
|
+
]
|
|
51
|
+
}
|