glitool 1.0.1 → 2.0.1
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 +115 -48
- package/dist/agent.js +233 -37
- package/dist/agents/coder.js +46 -34
- package/dist/agents/debugger.js +111 -0
- package/dist/agents/explainer.js +2 -5
- package/dist/agents/git-agent.js +90 -0
- package/dist/agents/graph.js +214 -23
- package/dist/agents/judge.js +61 -0
- package/dist/agents/planner.js +31 -12
- package/dist/agents/planningAgent.js +41 -0
- package/dist/agents/refactorer.js +97 -0
- package/dist/agents/reviewer-agent.js +87 -0
- package/dist/agents/reviewer.js +6 -9
- package/dist/agents/types.js +1 -0
- package/dist/agents/validator.js +93 -0
- package/dist/agents/workflow.js +45 -0
- package/dist/auth.js +87 -0
- package/dist/commands/version.js +1 -0
- package/dist/config.js +4 -1
- package/dist/confirmHandler.js +4 -2
- package/dist/index.js +12 -25
- package/dist/llm/classifier.js +61 -0
- package/dist/llm/factory.js +58 -0
- package/dist/llm/router.js +191 -22
- package/dist/logger.js +25 -0
- package/dist/processEvents.js +1 -0
- package/dist/tools/bashTool.js +90 -0
- package/dist/tools/editFileTool.js +14 -3
- package/dist/tools/index.js +3 -1
- package/dist/tools/listFilesTool.js +19 -21
- package/dist/tools/processRegistry.js +36 -0
- package/dist/tools/readBackgroundOutput.js +29 -0
- package/dist/tools/readFileTool.js +64 -9
- package/dist/tools/searchCodeTool.js +14 -4
- package/dist/tools/webFetchTool.js +45 -0
- package/dist/tools/writeFileTool.js +9 -5
- package/dist/trust/riskScorer.js +29 -2
- package/dist/ui/App.js +384 -47
- package/dist/ui/AuthFlow.js +76 -0
- package/dist/ui/ConfirmCard.js +53 -0
- package/dist/ui/EscalationCard.js +22 -0
- package/dist/ui/ExplainCard.js +5 -0
- package/dist/ui/Pipeline.js +37 -0
- package/dist/ui/ProcessTrace.js +79 -0
- package/dist/ui/RoleRow.js +16 -0
- package/dist/ui/RoleRow.test.js +8 -0
- package/dist/ui/SlashPalette.js +32 -0
- package/dist/ui/StatusBar.js +44 -0
- package/dist/ui/ToolLog.js +62 -0
- package/dist/ui/Welcome.js +11 -0
- package/dist/ui/renderMarkdown.js +41 -0
- package/dist/ui/symbols.js +19 -0
- package/dist/ui/tokens.js +13 -0
- package/dist/version.js +1 -0
- package/package.json +56 -54
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { tool } from "@langchain/core/tools";
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import TurndownService from 'turndown';
|
|
4
|
+
const turndown = new TurndownService({ headingStyle: 'atx', codeBlockStyle: 'fenced' });
|
|
5
|
+
const MAX_CHARS = 20_000;
|
|
6
|
+
export const webFetchTool = tool(async ({ url }) => {
|
|
7
|
+
let parsed;
|
|
8
|
+
try {
|
|
9
|
+
parsed = new URL(url);
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return `Invalid URL: ${url}`;
|
|
13
|
+
}
|
|
14
|
+
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
|
|
15
|
+
return `BLOCKED: Only http/https URLs are allowed. Got: ${parsed.protocol}`;
|
|
16
|
+
}
|
|
17
|
+
let response;
|
|
18
|
+
try {
|
|
19
|
+
response = await fetch(url, {
|
|
20
|
+
headers: { 'User-Agent': 'glitool/1.0' },
|
|
21
|
+
signal: AbortSignal.timeout(15_000)
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
return `Fetch failed: ${err.message}`;
|
|
26
|
+
}
|
|
27
|
+
if (!response.ok) {
|
|
28
|
+
return `HTTP ${response.status}: ${response.statusText}`;
|
|
29
|
+
}
|
|
30
|
+
const contentType = response.headers.get('content-type') ?? '';
|
|
31
|
+
if (contentType.includes('text/html')) {
|
|
32
|
+
const html = await response.text();
|
|
33
|
+
const markdown = turndown.turndown(html);
|
|
34
|
+
const trimmed = markdown.slice(0, MAX_CHARS);
|
|
35
|
+
return trimmed.length < markdown.length ? trimmed + '\n\n(content truncated at 20 00 chars)' : trimmed;
|
|
36
|
+
}
|
|
37
|
+
const text = await response.text();
|
|
38
|
+
return text.slice(0, MAX_CHARS);
|
|
39
|
+
}, {
|
|
40
|
+
name: 'webFetch',
|
|
41
|
+
description: 'Fetch a public URL and return its content as readable text. HTML pages are converted to markdown. Use for reading docs, changelogs, npm package pages, GitHub READMEs, or any public web resource. Only http/https allowed — file:// and other schemes are blocked.',
|
|
42
|
+
schema: z.object({
|
|
43
|
+
url: z.string().describe('The full URL to fetch (must start with http:// or https://)')
|
|
44
|
+
})
|
|
45
|
+
});
|
|
@@ -3,17 +3,21 @@ import fs from 'fs';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
import { requestConfirm } from "../confirmHandler.js";
|
|
6
|
+
import { log } from "../logger.js";
|
|
6
7
|
export const writeFileTool = tool(async ({ filePath, content }) => {
|
|
7
8
|
const projectRoot = process.cwd();
|
|
8
9
|
const fullPath = path.resolve(projectRoot, filePath);
|
|
9
10
|
if (!fullPath.startsWith(projectRoot + path.sep) && fullPath !== projectRoot) {
|
|
10
11
|
throw new Error('Access denied: cannot write outside project root');
|
|
11
12
|
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
log('write:before-confirm', { filePath });
|
|
14
|
+
const ok = await requestConfirm({
|
|
15
|
+
type: 'write',
|
|
16
|
+
filePath,
|
|
17
|
+
content,
|
|
18
|
+
risk: 'medium',
|
|
19
|
+
});
|
|
20
|
+
log('write:after-confirm', { filePath, ok });
|
|
17
21
|
if (!ok) {
|
|
18
22
|
return 'USER_CANCELLED: The user explicitly rejected this file write. Do NOT retry. Inform the user the write was cancelled.';
|
|
19
23
|
}
|
package/dist/trust/riskScorer.js
CHANGED
|
@@ -1,11 +1,30 @@
|
|
|
1
1
|
const HIGH_RISK_PATTERNS = [
|
|
2
2
|
/\.env/,
|
|
3
3
|
/\.env\.\w+/,
|
|
4
|
-
/package\.json$/,
|
|
5
|
-
/tsconfig\.json$/,
|
|
6
4
|
/docker-compose/,
|
|
7
5
|
/\.git\//,
|
|
8
6
|
];
|
|
7
|
+
const BLOCKED_SHELL_PATTERNS = [
|
|
8
|
+
/\brm\s+-rf?\s+\/(?!\w)/, // rm -rf / (not /something)
|
|
9
|
+
/\brm\s+-rf?\s+~(?!\w)/, // rm -rf ~
|
|
10
|
+
/\bsudo\b/,
|
|
11
|
+
/\|\s*(sh|bash|zsh)\b/, // piped to a shell — curl | sh etc.
|
|
12
|
+
/:\s*\(\s*\)\s*\{.*\|.*&\s*\}\s*;\s*:/, // fork bomb
|
|
13
|
+
/\bdd\s+if=/, // dd block-level copy
|
|
14
|
+
/>\s*\/dev\/(sd[a-z]|hd[a-z]|nvme)/, // writing to disk devices
|
|
15
|
+
/\bmkfs\b/, // formatting disks
|
|
16
|
+
/\bchmod\s+-R\s+777\s+\//, // chmod 777 root
|
|
17
|
+
];
|
|
18
|
+
const CONFIRM_SHELL_PATTERNS = [
|
|
19
|
+
/\bgit\s+push\b/,
|
|
20
|
+
/\bgit\s+reset\s+--hard\b/,
|
|
21
|
+
/\bgit\s+rebase\b/,
|
|
22
|
+
/\bgit\s+(force-?push|push\s+-f)\b/,
|
|
23
|
+
/\bnpm\s+(publish|install|i|uninstall|un|rm|remove)\b/,
|
|
24
|
+
/\byarn\s+(add|remove|publish)\b/,
|
|
25
|
+
/\bpnpm\s+(add|remove|publish)\b/,
|
|
26
|
+
/\bdocker\s+(rm|rmi|kill|exec)\b/,
|
|
27
|
+
];
|
|
9
28
|
const LOW_RISK_TOOLS = ['listFiles', 'readFile', 'searchCode'];
|
|
10
29
|
export function scoreRisk(toolName, args) {
|
|
11
30
|
if (LOW_RISK_TOOLS.includes(toolName))
|
|
@@ -23,3 +42,11 @@ export function getRiskMessage(toolName, riskLevel, args) {
|
|
|
23
42
|
return `${toolName}${filePath ? ` -> ${filePath}` : ''}`;
|
|
24
43
|
return '';
|
|
25
44
|
}
|
|
45
|
+
export function scoreShellRisk(command) {
|
|
46
|
+
const trimmed = command.trim();
|
|
47
|
+
if (BLOCKED_SHELL_PATTERNS.some(p => p.test(trimmed)))
|
|
48
|
+
return 'block';
|
|
49
|
+
if (CONFIRM_SHELL_PATTERNS.some(p => p.test(trimmed)))
|
|
50
|
+
return 'confirm';
|
|
51
|
+
return 'allow';
|
|
52
|
+
}
|
package/dist/ui/App.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import React, { useState,
|
|
3
|
-
import { Box, Text, useApp } from 'ink';
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import React, { useState, useRef } from "react";
|
|
3
|
+
import { Box, Text, useApp, Static } from 'ink';
|
|
4
4
|
import TextInput from "ink-text-input";
|
|
5
5
|
import { chat, clearSession, llm, sessionMessages } from '../agent.js';
|
|
6
6
|
import { clearSummary, generateAndSaveSummary } from "../memory.js";
|
|
@@ -8,40 +8,181 @@ import { clearProjectMemory, extractAndSaveProjectMemory } from "../projectMemor
|
|
|
8
8
|
import { useInput } from 'ink';
|
|
9
9
|
import { setConfirmHandler } from "../confirmHandler.js";
|
|
10
10
|
import { explainResponse } from "../agents/explainer.js";
|
|
11
|
-
|
|
11
|
+
import { loadConfig } from '../config.js';
|
|
12
|
+
import { Welcome } from './Welcome.js';
|
|
13
|
+
import { StatusBar } from './StatusBar.js';
|
|
14
|
+
import { execSync } from 'child_process';
|
|
15
|
+
import { SlashPalette, filterCommands } from './SlashPalette.js';
|
|
16
|
+
import { ToolLog } from "./ToolLog.js";
|
|
17
|
+
import { Pipeline } from "./Pipeline.js";
|
|
18
|
+
import { ConfirmCard } from './ConfirmCard.js';
|
|
19
|
+
import { ExplainCard } from "./ExplainCard.js";
|
|
20
|
+
import { colors } from "./tokens.js";
|
|
21
|
+
import { EscalationCard } from './EscalationCard.js';
|
|
22
|
+
import { log } from "../logger.js";
|
|
23
|
+
import { renderMarkdown } from "./renderMarkdown.js";
|
|
24
|
+
import { ProcessTrace } from './ProcessTrace.js';
|
|
25
|
+
import { AuthFlow } from './AuthFlow.js';
|
|
26
|
+
import { getAnonRequestCount, incrementAnonCount, isAnonLimitReached, isAuthenticated, readAuth, ANON_LIMIT, } from '../auth.js';
|
|
27
|
+
// const previousInputRef = useRef('');
|
|
28
|
+
const config = loadConfig();
|
|
29
|
+
function getWorkspaceStats() {
|
|
30
|
+
const cwd = process.cwd();
|
|
31
|
+
let branch = 'main';
|
|
32
|
+
let files = 0;
|
|
33
|
+
let loc = '';
|
|
34
|
+
try {
|
|
35
|
+
branch = execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }).trim();
|
|
36
|
+
}
|
|
37
|
+
catch { }
|
|
38
|
+
try {
|
|
39
|
+
files = parseInt(execSync('git ls-files | wc -l', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }).trim(), 10);
|
|
40
|
+
}
|
|
41
|
+
catch { }
|
|
42
|
+
try {
|
|
43
|
+
const raw = execSync('git ls-files | xargs wc -l 2>/dev/null | tail -1', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }).trim();
|
|
44
|
+
const n = parseInt(raw, 10);
|
|
45
|
+
if (!isNaN(n))
|
|
46
|
+
loc = `${(n / 1000).toFixed(1)}k LOC`;
|
|
47
|
+
}
|
|
48
|
+
catch { }
|
|
49
|
+
return {
|
|
50
|
+
path: cwd.replace(process.env.HOME ?? '', '~'),
|
|
51
|
+
branch,
|
|
52
|
+
files,
|
|
53
|
+
loc,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
const workspaceStats = getWorkspaceStats();
|
|
12
57
|
export const App = ({ explainMode = false }) => {
|
|
13
58
|
const { exit } = useApp();
|
|
59
|
+
const auth = readAuth();
|
|
60
|
+
const [anonCount, setAnonCount] = useState(getAnonRequestCount());
|
|
61
|
+
const isByok = !!process.env.OPENAI_API_KEY;
|
|
62
|
+
const anonLeft = (isAuthenticated() || isByok) ? undefined : Math.max(0, ANON_LIMIT - anonCount);
|
|
14
63
|
const [messages, setMessages] = useState([]);
|
|
15
64
|
const [input, setInput] = useState('');
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
const [
|
|
20
|
-
const [confirmInput, setConfirmInput] = useState('');
|
|
65
|
+
const previousInputRef = useRef('');
|
|
66
|
+
const ctrlVHandledRef = useRef(false);
|
|
67
|
+
// ← ADD HERE
|
|
68
|
+
const [pastedContent, setPastedContent] = useState('');
|
|
21
69
|
const [confirmResolver, setConfirmResolver] = useState(null);
|
|
22
70
|
const [streamingContent, setStreamingContent] = useState('');
|
|
23
71
|
const [inputHistory, setInputHistory] = useState([]);
|
|
24
72
|
const [historyIndex, setHistoryIndex] = useState(-1);
|
|
73
|
+
const [statusState, setStatusState] = useState('idle');
|
|
74
|
+
const [statusDetail, setStatusDetail] = useState('');
|
|
75
|
+
const [tokens, setTokens] = useState(0);
|
|
76
|
+
const [cost, setCost] = useState(0);
|
|
77
|
+
const [confirmRequest, setConfirmRequest] = useState(null);
|
|
78
|
+
const [toolLog, setToolLog] = useState([]);
|
|
79
|
+
const [stageEvents, setStageEvents] = useState([]);
|
|
80
|
+
const [planner, setPlanner] = useState({ status: 'idle' });
|
|
81
|
+
const [coder, setCoder] = useState({ status: 'idle' });
|
|
82
|
+
const [workflow, setWorkflow] = useState({ status: 'idle' });
|
|
83
|
+
const [validator, setValidator] = useState({ status: 'idle' });
|
|
84
|
+
const [escalation, setEscalation] = useState(null);
|
|
85
|
+
const [judge, setJudge] = useState({ status: 'idle' });
|
|
86
|
+
const [inputKey, setInputKey] = useState(0);
|
|
87
|
+
const [showAuth, setShowAuth] = useState(false);
|
|
88
|
+
const [paletteIndex, setPaletteIndex] = useState(0);
|
|
89
|
+
const paletteItems = filterCommands(input);
|
|
25
90
|
const handleChange = (value) => {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
91
|
+
if (ctrlVHandledRef.current) {
|
|
92
|
+
ctrlVHandledRef.current = false;
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const previous = previousInputRef.current;
|
|
96
|
+
const grew = value.length - previous.length;
|
|
97
|
+
const grewALot = grew > 20;
|
|
98
|
+
const newlineAppeared = /[\r\n]/.test(value) && !/[\r\n]/.test(previous);
|
|
99
|
+
if (grewALot || newlineAppeared) {
|
|
100
|
+
let pasted;
|
|
101
|
+
if (value.startsWith(previous)) {
|
|
102
|
+
pasted = value.slice(previous.length);
|
|
103
|
+
}
|
|
104
|
+
else if (value.endsWith(previous)) {
|
|
105
|
+
pasted = value.slice(0, value.length - previous.length);
|
|
106
|
+
}
|
|
107
|
+
else if (previous && value.includes(previous)) {
|
|
108
|
+
pasted = value.replace(previous, '');
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
pasted = value;
|
|
112
|
+
}
|
|
113
|
+
if (pasted.includes('\n')) {
|
|
114
|
+
// multi-line → clipping section
|
|
115
|
+
setPastedContent(prev => (prev ? `${prev}\n\n${pasted}` : pasted));
|
|
116
|
+
setInput(previous);
|
|
117
|
+
previousInputRef.current = previous;
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
// single line → stays in input field
|
|
121
|
+
setInput(value);
|
|
122
|
+
previousInputRef.current = value;
|
|
123
|
+
}
|
|
30
124
|
}
|
|
31
125
|
else {
|
|
32
|
-
|
|
126
|
+
setInput(value);
|
|
127
|
+
previousInputRef.current = value;
|
|
33
128
|
}
|
|
129
|
+
setPaletteIndex(0);
|
|
34
130
|
};
|
|
35
131
|
const handleExit = async () => {
|
|
36
132
|
await generateAndSaveSummary(sessionMessages, llm);
|
|
37
133
|
await extractAndSaveProjectMemory(sessionMessages, llm);
|
|
38
134
|
exit();
|
|
39
135
|
};
|
|
40
|
-
useInput((
|
|
41
|
-
if (
|
|
136
|
+
useInput((inputKey, key) => {
|
|
137
|
+
if (confirmRequest)
|
|
138
|
+
return;
|
|
139
|
+
if (escalation)
|
|
140
|
+
return;
|
|
141
|
+
if (statusState === 'working')
|
|
142
|
+
return;
|
|
143
|
+
// Ctrl+V — read clipboard directly
|
|
144
|
+
if (inputKey === '\x16' || (key.ctrl && inputKey === 'v')) {
|
|
145
|
+
try {
|
|
146
|
+
ctrlVHandledRef.current = true;
|
|
147
|
+
const text = execSync('xclip -selection clipboard -o 2>/dev/null || xsel --clipboard --output 2>/dev/null', { encoding: 'utf8', timeout: 2000 }).trimEnd();
|
|
148
|
+
if (!text)
|
|
149
|
+
return;
|
|
150
|
+
if (text.includes('\n')) {
|
|
151
|
+
// multi-line → clipping section
|
|
152
|
+
setPastedContent(prev => prev ? `${prev}\n\n${text}` : text);
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
// single line → goes straight into input field
|
|
156
|
+
setInput(prev => prev + text);
|
|
157
|
+
previousInputRef.current = previousInputRef.current + text;
|
|
158
|
+
setInputKey(k => k + 1);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
catch { }
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (key.escape && showAuth) {
|
|
165
|
+
setShowAuth(false);
|
|
42
166
|
return;
|
|
43
|
-
|
|
167
|
+
}
|
|
168
|
+
if (key.escape && pastedContent) {
|
|
169
|
+
setPastedContent('');
|
|
44
170
|
return;
|
|
171
|
+
}
|
|
172
|
+
if (paletteItems.length > 0) {
|
|
173
|
+
if (key.upArrow) {
|
|
174
|
+
setPaletteIndex(i => (i - 1 + paletteItems.length) % paletteItems.length);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (key.downArrow) {
|
|
178
|
+
setPaletteIndex(i => (i + 1) % paletteItems.length);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (key.escape) {
|
|
182
|
+
setInput('');
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
45
186
|
if (key.upArrow) {
|
|
46
187
|
if (inputHistory.length === 0)
|
|
47
188
|
return;
|
|
@@ -59,27 +200,45 @@ export const App = ({ explainMode = false }) => {
|
|
|
59
200
|
setHistoryIndex(newIndex);
|
|
60
201
|
setInput(inputHistory[inputHistory.length - 1 - newIndex] ?? '');
|
|
61
202
|
}
|
|
62
|
-
if (key.tab && suggestions.length > 0) {
|
|
63
|
-
setInput(suggestions[0] ?? '');
|
|
64
|
-
setSuggestions([]);
|
|
65
|
-
}
|
|
66
203
|
});
|
|
204
|
+
function pastePreview(content) {
|
|
205
|
+
const lines = content.split('\n');
|
|
206
|
+
const sizeKB = (content.length / 1024).toFixed(1);
|
|
207
|
+
const lineCount = lines.length;
|
|
208
|
+
const headline = `${lineCount} line${lineCount === 1 ? '' : 's'} · ${sizeKB} KB`;
|
|
209
|
+
const firstNonEmpty = lines.find(l => l.trim()) ?? '';
|
|
210
|
+
const truncated = firstNonEmpty.length > 80 ? firstNonEmpty.slice(0, 80) + '…' : firstNonEmpty;
|
|
211
|
+
const moreNote = lineCount > 1 ? ` · +${lineCount - 1} more line${lineCount - 1 === 1 ? '' : 's'}` : '';
|
|
212
|
+
return { headline, body: `${truncated}${moreNote}` };
|
|
213
|
+
}
|
|
67
214
|
React.useEffect(() => {
|
|
68
|
-
setConfirmHandler((
|
|
215
|
+
setConfirmHandler((req) => {
|
|
69
216
|
return new Promise((resolve) => {
|
|
70
|
-
|
|
217
|
+
setConfirmRequest(req);
|
|
218
|
+
setStatusState('awaiting');
|
|
71
219
|
setConfirmResolver(() => resolve);
|
|
220
|
+
log('confirm:resolver-set', { filePath: req.filePath });
|
|
72
221
|
});
|
|
73
222
|
});
|
|
74
223
|
}, []);
|
|
75
|
-
const handleSubmit =
|
|
76
|
-
|
|
224
|
+
const handleSubmit = async (value) => {
|
|
225
|
+
let cmd = value.trim();
|
|
226
|
+
if (pastedContent) {
|
|
227
|
+
cmd = cmd ? `${pastedContent}\n\n${cmd}` : pastedContent;
|
|
228
|
+
setPastedContent('');
|
|
229
|
+
}
|
|
77
230
|
setInput('');
|
|
78
|
-
setSuggestions([]);
|
|
79
231
|
if (!cmd)
|
|
80
232
|
return;
|
|
81
233
|
setInputHistory(prev => prev[prev.length - 1] === cmd ? prev : [...prev, cmd]);
|
|
82
234
|
setHistoryIndex(-1);
|
|
235
|
+
if (paletteItems.length > 0 && value.startsWith('/')) {
|
|
236
|
+
const selected = paletteItems[paletteIndex];
|
|
237
|
+
if (selected && selected.cmd !== value) {
|
|
238
|
+
setInput(selected.cmd);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
83
242
|
if (cmd === '/exit') {
|
|
84
243
|
await handleExit();
|
|
85
244
|
return;
|
|
@@ -99,33 +258,159 @@ export const App = ({ explainMode = false }) => {
|
|
|
99
258
|
if (cmd === '/help') {
|
|
100
259
|
setMessages(prev => [...prev, {
|
|
101
260
|
role: 'assistant',
|
|
102
|
-
content:
|
|
261
|
+
content: [
|
|
262
|
+
'Slash commands:',
|
|
263
|
+
' /plan — create or update plan.md',
|
|
264
|
+
' /coder — run the full coding pipeline',
|
|
265
|
+
' /debug — investigate and fix a bug',
|
|
266
|
+
' /refactor — structural code improvements',
|
|
267
|
+
' /review — read-only code analysis',
|
|
268
|
+
' /git — git operations',
|
|
269
|
+
' /explain — explain a concept or file',
|
|
270
|
+
' /quick — fast chat, no tools',
|
|
271
|
+
'',
|
|
272
|
+
'Session commands:',
|
|
273
|
+
' /clear — clear conversation (keep memory)',
|
|
274
|
+
' /reset — clear conversation + memory',
|
|
275
|
+
' /model — show current model',
|
|
276
|
+
' /tools — list available tools',
|
|
277
|
+
' /memory — show project memory',
|
|
278
|
+
' /exit — save session and exit',
|
|
279
|
+
' /signup — sign in with GitHub (50 req/month free)',
|
|
280
|
+
].join('\n')
|
|
103
281
|
}]);
|
|
104
282
|
return;
|
|
105
283
|
}
|
|
106
284
|
if (cmd === '/model') {
|
|
107
|
-
setMessages(prev => [...prev, { role: 'assistant', content:
|
|
285
|
+
setMessages(prev => [...prev, { role: 'assistant', content: `Model: ${config.preferredModel ?? 'gpt-4o-mini'}` }]);
|
|
108
286
|
return;
|
|
109
287
|
}
|
|
110
288
|
if (cmd === '/tools') {
|
|
111
289
|
setMessages(prev => [...prev, {
|
|
112
290
|
role: 'assistant',
|
|
113
|
-
content:
|
|
291
|
+
content: [
|
|
292
|
+
'Available tools:',
|
|
293
|
+
' listFiles — list project files (supports glob patterns)',
|
|
294
|
+
' readFile — read a file by name or path',
|
|
295
|
+
' searchCode — grep for symbols, functions, keywords',
|
|
296
|
+
' editFile — make targeted edits to existing files',
|
|
297
|
+
' writeFile — create or overwrite a file',
|
|
298
|
+
' bash — run shell commands (risk-gated)',
|
|
299
|
+
' webFetch — fetch a URL and return markdown content',
|
|
300
|
+
' readBackground — read output from a background process',
|
|
301
|
+
].join('\n')
|
|
114
302
|
}]);
|
|
115
303
|
return;
|
|
116
304
|
}
|
|
305
|
+
if (cmd === '/memory') {
|
|
306
|
+
const { loadProjectMemory } = await import('../projectMemory.js');
|
|
307
|
+
const mem = loadProjectMemory();
|
|
308
|
+
const content = mem
|
|
309
|
+
? [
|
|
310
|
+
`Tech stack: ${mem.techStack?.join(', ') ?? 'unknown'}`,
|
|
311
|
+
mem.architectureDecisions?.length ? `Architecture: ${mem.architectureDecisions.join(' · ')}` : '',
|
|
312
|
+
mem.todos?.length ? `TODOs: ${mem.todos.join(' · ')}` : '',
|
|
313
|
+
].filter(Boolean).join('\n')
|
|
314
|
+
: 'No project memory recorded yet. Keep chatting — it builds automatically on exit.';
|
|
315
|
+
setMessages(prev => [...prev, { role: 'assistant', content }]);
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
if (cmd === '/signup') {
|
|
319
|
+
setShowAuth(true);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
117
322
|
setMessages(prev => [...prev, { role: 'user', content: cmd }]);
|
|
118
|
-
|
|
323
|
+
setStatusState('working');
|
|
324
|
+
setToolLog([]);
|
|
325
|
+
setStageEvents([]);
|
|
326
|
+
setPlanner({ status: 'idle' });
|
|
327
|
+
setWorkflow({ status: 'idle' });
|
|
328
|
+
setCoder({ status: 'idle' });
|
|
329
|
+
setValidator({ status: 'idle' });
|
|
330
|
+
setJudge({ status: 'idle' });
|
|
331
|
+
if (!isAuthenticated() && !process.env.OPENAI_API_KEY && isAnonLimitReached()) {
|
|
332
|
+
setMessages(prev => [...prev, {
|
|
333
|
+
role: 'assistant',
|
|
334
|
+
content: [
|
|
335
|
+
"You've used your 5 free requests.",
|
|
336
|
+
'',
|
|
337
|
+
'Sign in with GitHub — free, 50 requests/month:',
|
|
338
|
+
' → https://glitool.dev/activate',
|
|
339
|
+
'',
|
|
340
|
+
'Type /signup to start the sign-in flow in your terminal.',
|
|
341
|
+
].join('\n'),
|
|
342
|
+
}]);
|
|
343
|
+
setStatusState('idle');
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
119
346
|
try {
|
|
120
347
|
const reply = await chat(cmd, (toolName, args) => {
|
|
121
|
-
|
|
122
|
-
|
|
348
|
+
let argStr = '';
|
|
349
|
+
if (args) {
|
|
350
|
+
const first = Object.values(args)[0];
|
|
351
|
+
if (typeof first === 'string') {
|
|
352
|
+
try {
|
|
353
|
+
const parsed = JSON.parse(first);
|
|
354
|
+
argStr = parsed.command ?? parsed.filePath ?? parsed.pattern ?? parsed.query ?? parsed.url ?? first;
|
|
355
|
+
}
|
|
356
|
+
catch {
|
|
357
|
+
argStr = first;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
else if (typeof first === 'object' && first !== null) {
|
|
361
|
+
argStr = first.command ?? first.filePath ?? JSON.stringify(first).slice(0, 60);
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
argStr = String(first ?? '');
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
setToolLog(prev => {
|
|
368
|
+
const updated = prev.map(e => e.status === 'running' ? { ...e, status: 'done' } : e);
|
|
369
|
+
return [...updated, { tool: toolName, target: argStr, status: 'running' }];
|
|
370
|
+
});
|
|
123
371
|
}, (status) => {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
372
|
+
if (status.startsWith('Planning')) {
|
|
373
|
+
setPlanner({ status: 'active', detail: 'planning steps' });
|
|
374
|
+
setWorkflow({ status: 'queued' });
|
|
375
|
+
setCoder({ status: 'queued' });
|
|
376
|
+
setValidator({ status: 'queued' });
|
|
377
|
+
setJudge({ status: 'queued' });
|
|
378
|
+
}
|
|
379
|
+
else if (status.startsWith('Building execution') || status.startsWith('Workflow')) {
|
|
380
|
+
setPlanner({ status: 'done' });
|
|
381
|
+
setWorkflow({ status: 'active', detail: 'building DAG' });
|
|
382
|
+
}
|
|
383
|
+
else if (status.startsWith('Executing')) {
|
|
384
|
+
setWorkflow({ status: 'done' });
|
|
385
|
+
setCoder({ status: 'active', detail: 'editing files' });
|
|
386
|
+
}
|
|
387
|
+
else if (status.startsWith('Validating')) {
|
|
388
|
+
setCoder({ status: 'done' });
|
|
389
|
+
setValidator({ status: 'active', detail: 'tsc + eslint' });
|
|
390
|
+
}
|
|
391
|
+
else if (status.startsWith('Validation failed')) {
|
|
392
|
+
setValidator({ status: 'active', detail: status.replace('Validation failed', '').trim() });
|
|
393
|
+
}
|
|
394
|
+
else if (status.startsWith('Judging') || status.startsWith('Judge:')) {
|
|
395
|
+
setValidator({ status: 'done' });
|
|
396
|
+
setJudge({ status: 'active', detail: 'reviewing output' });
|
|
397
|
+
}
|
|
398
|
+
else if (status.startsWith('Escalating')) {
|
|
399
|
+
setJudge({ status: 'active', detail: 'escalating' });
|
|
400
|
+
}
|
|
401
|
+
}, (token) => setStreamingContent(prev => prev + token), (payload) => setEscalation(payload), (newTokens, newCost) => {
|
|
402
|
+
setTokens(prev => prev + newTokens);
|
|
403
|
+
setCost(prev => prev + newCost);
|
|
404
|
+
}, (event) => setStageEvents(prev => [...prev, event]));
|
|
405
|
+
if (!isAuthenticated() && !process.env.OPENAI_API_KEY) {
|
|
406
|
+
const newCount = incrementAnonCount();
|
|
407
|
+
setAnonCount(newCount);
|
|
408
|
+
}
|
|
128
409
|
setStreamingContent('');
|
|
410
|
+
if (stageEvents.length > 0) {
|
|
411
|
+
setMessages(prev => [...prev, { role: 'trace', content: '', traceEvents: [...stageEvents] }]);
|
|
412
|
+
}
|
|
413
|
+
setStageEvents([]);
|
|
129
414
|
setMessages(prev => [...prev, { role: 'assistant', content: reply }]);
|
|
130
415
|
if (explainMode && reply) {
|
|
131
416
|
const explanation = await explainResponse(reply);
|
|
@@ -141,16 +426,68 @@ export const App = ({ explainMode = false }) => {
|
|
|
141
426
|
}]);
|
|
142
427
|
}
|
|
143
428
|
finally {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
429
|
+
setStageEvents([]); // ← add this
|
|
430
|
+
setToolLog(prev => prev.map(e => e.status === 'running' ? { ...e, status: 'done' } : e));
|
|
431
|
+
setPlanner(p => p.status === 'active' ? { status: 'done' } : p);
|
|
432
|
+
setWorkflow(w => w.status === 'active' ? { status: 'done' } : w);
|
|
433
|
+
setCoder(c => c.status === 'active' ? { status: 'done' } : c);
|
|
434
|
+
setValidator(v => v.status === 'active' ? { status: 'done' } : v);
|
|
435
|
+
setJudge(j => j.status === 'active' ? { status: 'done' } : j);
|
|
436
|
+
setStreamingContent('');
|
|
437
|
+
setStatusState('idle');
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: [1], children: (_, i) => (_jsx(Welcome, { name: config.name, version: "1.0.1", workspace: workspaceStats, runtime: {
|
|
441
|
+
model: config.preferredModel,
|
|
442
|
+
toolsCount: 6,
|
|
443
|
+
explainOn: explainMode,
|
|
444
|
+
routerOn: true,
|
|
445
|
+
} }, i)) }), _jsxs(Box, { flexDirection: "column", padding: 1, children: [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 === 'trace' && msg.traceEvents && (_jsx(Box, { marginLeft: 1, marginBottom: 1, children: _jsx(ProcessTrace, { events: msg.traceEvents, active: false }) })), msg.role === 'assistant' && (() => {
|
|
446
|
+
const rendered = renderMarkdown(msg.content);
|
|
447
|
+
const isLong = msg.content.split('\n').length > 6;
|
|
448
|
+
if (isLong) {
|
|
449
|
+
return (_jsxs(Box, { flexDirection: "column", marginLeft: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "Assistant" }), _jsx(Text, { children: rendered })] }));
|
|
450
|
+
}
|
|
451
|
+
return (_jsxs(Box, { borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: " Assistant " }), _jsxs(Text, { children: [" ", rendered, " "] })] }));
|
|
452
|
+
})(), 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: renderMarkdown(msg.content) })] })), msg.role === 'explain' && (_jsx(ExplainCard, { footer: "to switch: /explain off \u00B7 or set explainMode: false in ~/.glitool/config.json", children: renderMarkdown(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"] })] })), (statusState === 'working' || stageEvents.length > 0) && (stageEvents.length > 0
|
|
453
|
+
? _jsx(ProcessTrace, { events: stageEvents, active: statusState === 'working' })
|
|
454
|
+
: _jsx(ToolLog, { entries: toolLog })), _jsx(SlashPalette, { items: paletteItems, selectedIndex: paletteIndex }), confirmRequest ? (_jsx(ConfirmCard, { request: confirmRequest, onChoice: (choice) => {
|
|
455
|
+
if (choice === 'd')
|
|
456
|
+
return; // TODO: show full diff later
|
|
457
|
+
log('confirm:choice', { choice, hasResolver: !!confirmResolver });
|
|
458
|
+
const resolve = confirmResolver;
|
|
459
|
+
setConfirmRequest(null);
|
|
153
460
|
setConfirmResolver(null);
|
|
154
|
-
|
|
155
|
-
|
|
461
|
+
setStatusState('working');
|
|
462
|
+
resolve?.(choice === 'y');
|
|
463
|
+
log('confirm:resolved', { value: choice === 'y' });
|
|
464
|
+
} })) : showAuth ? (_jsx(AuthFlow, { onDone: (auth) => {
|
|
465
|
+
setShowAuth(false);
|
|
466
|
+
setMessages(prev => [...prev, {
|
|
467
|
+
role: 'assistant',
|
|
468
|
+
content: `✓ Signed in as ${auth.email} · ${auth.plan} · ${auth.requestsRemaining ?? 50} req/month`,
|
|
469
|
+
}]);
|
|
470
|
+
}, onCancel: () => setShowAuth(false) })) : (_jsxs(_Fragment, { children: [pastedContent && (() => {
|
|
471
|
+
const { headline, body } = pastePreview(pastedContent);
|
|
472
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: colors.mustard, paddingX: 1, marginBottom: 0, children: [_jsxs(Box, { children: [_jsx(Text, { color: colors.mustard, bold: true, children: "\uD83D\uDCCE PASTED " }), _jsx(Text, { color: colors.muted, children: headline }), _jsx(Text, { color: colors.muted, children: " \u00B7 " }), _jsx(Text, { color: colors.amber, bold: true, children: "Esc" }), _jsx(Text, { color: colors.muted, children: " to remove \u00B7 will prepend on send" })] }), _jsx(Text, { color: colors.muted, dimColor: true, children: body })] }));
|
|
473
|
+
})(), _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..." }, inputKey), input.length > 50 && (_jsxs(Text, { dimColor: true, children: [" [", input.length, "]"] }))] })] })), escalation && (_jsx(EscalationCard, { payload: escalation, onChoice: (choice) => {
|
|
474
|
+
if (choice === 'approve') {
|
|
475
|
+
setEscalation(null);
|
|
476
|
+
}
|
|
477
|
+
else if (choice === 'abort') {
|
|
478
|
+
setMessages(prev => prev.slice(0, -1));
|
|
479
|
+
setEscalation(null);
|
|
480
|
+
setMessages(prev => [...prev, {
|
|
481
|
+
role: 'error',
|
|
482
|
+
content: 'Aborted. The last response was discarded.',
|
|
483
|
+
}]);
|
|
484
|
+
}
|
|
485
|
+
else if (choice === 'correct') {
|
|
486
|
+
setEscalation(null);
|
|
487
|
+
setMessages(prev => [...prev, {
|
|
488
|
+
role: 'assistant',
|
|
489
|
+
content: 'Tell me what to fix and I will retry.',
|
|
490
|
+
}]);
|
|
491
|
+
}
|
|
492
|
+
} }))] }), _jsx(StatusBar, { state: statusState, detail: statusDetail, tier: auth?.plan, anonLeft: anonLeft, model: config.preferredModel, tokens: tokens, cost: cost })] }));
|
|
156
493
|
};
|