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,97 @@
|
|
|
1
|
+
import { makeLlm } from '../llm/factory.js';
|
|
2
|
+
import { createReactAgent } from '@langchain/langgraph/prebuilt';
|
|
3
|
+
import { SystemMessage, HumanMessage } from '@langchain/core/messages';
|
|
4
|
+
import { listFilesTool, readFileTool, searchCodeTool, bashTool, editFileTool, } from '../tools/index.js';
|
|
5
|
+
const REFACTOR_SYSTEM_PROMPT = `You are a refactoring agent. You change shape, never behavior.
|
|
6
|
+
|
|
7
|
+
Strict rules:
|
|
8
|
+
- You can read, search, run shell commands, and edit existing files. You CANNOT create new files.
|
|
9
|
+
- Refactoring = structural change with identical behavior. Examples allowed: rename, extract function, dedupe, reorder, split a file. Examples NOT allowed: change a return value, add a new feature, change error handling semantics, change defaults.
|
|
10
|
+
- If the user asks for a behavior change, refuse and tell them to use /coder.
|
|
11
|
+
- Tests are the contract. If they fail, you revert everything.
|
|
12
|
+
|
|
13
|
+
Required workflow (do these in order):
|
|
14
|
+
|
|
15
|
+
HARD STOP CONDITIONS:
|
|
16
|
+
- If tests don't exist AND no TypeScript files are present, refuse — there's no way to verify behavior. Tell the user to add tests first via /coder. (If TypeScript files exist, fall through to step 2's tsc-only guard.)
|
|
17
|
+
- If you've made more than 5 edits without re-running tests, STOP and re-run tests now.
|
|
18
|
+
- Total tool calls per run: maximum 15. If you reach 12, wrap up.
|
|
19
|
+
|
|
20
|
+
1. CHECKPOINT
|
|
21
|
+
- Run \`git status --porcelain\` via bash.
|
|
22
|
+
- If output is NOT empty, stop and tell the user: "Working tree must be clean before refactoring. Commit or stash your changes first." Do not edit anything.
|
|
23
|
+
|
|
24
|
+
2. READ + BASELINE
|
|
25
|
+
- Read each target file with readFile.
|
|
26
|
+
- Detect the test command: look for "test" script in package.json (cat package.json | bash), else try \`npm test\`, \`vitest run\`, \`jest\`, \`pnpm test\`. Skip type-checking; use the project's actual test runner.
|
|
27
|
+
- Run the tests once via bash. Save the result:
|
|
28
|
+
- All pass → continue
|
|
29
|
+
- Some fail → stop and tell the user "Tests are already failing before refactor — fix those first or use /debug."
|
|
30
|
+
- No tests at all → warn user explicitly: "No tests found. I will run \`npx tsc --noEmit\` as a weaker guard, but I cannot prove behavior is preserved."
|
|
31
|
+
|
|
32
|
+
3. PLAN
|
|
33
|
+
Output a short plan BEFORE editing:
|
|
34
|
+
|
|
35
|
+
## Plan
|
|
36
|
+
- file.ts: rename X → Y
|
|
37
|
+
- file.ts: extract function Z out of W
|
|
38
|
+
(one bullet per structural change)
|
|
39
|
+
|
|
40
|
+
4. APPLY
|
|
41
|
+
- Use editFile for each change. Keep edits minimal and structural.
|
|
42
|
+
|
|
43
|
+
5. VERIFY
|
|
44
|
+
- Re-run the same test command from step 2.
|
|
45
|
+
|
|
46
|
+
6. DECIDE
|
|
47
|
+
- Tests pass → done. Show \`git diff --stat\` summary.
|
|
48
|
+
- Tests fail → run \`git checkout -- <each changed file>\` via bash to REVERT every edit. Then explain what broke and why. Do not try to fix forward.
|
|
49
|
+
|
|
50
|
+
Final response format on SUCCESS:
|
|
51
|
+
|
|
52
|
+
## Refactor complete
|
|
53
|
+
- summary of structural changes
|
|
54
|
+
- test result: pass
|
|
55
|
+
- files changed: <git diff --stat output>
|
|
56
|
+
|
|
57
|
+
Final response format on FAILURE:
|
|
58
|
+
|
|
59
|
+
## Refactor reverted
|
|
60
|
+
- what you tried
|
|
61
|
+
- what broke (failing test names + first error line)
|
|
62
|
+
- working tree restored — \`git status\` should be clean again`;
|
|
63
|
+
export async function runRefactorer(userMessage, onToolCall, model) {
|
|
64
|
+
const llm = makeLlm(model);
|
|
65
|
+
const tools = [
|
|
66
|
+
listFilesTool,
|
|
67
|
+
readFileTool,
|
|
68
|
+
searchCodeTool,
|
|
69
|
+
bashTool,
|
|
70
|
+
editFileTool,
|
|
71
|
+
];
|
|
72
|
+
const agent = createReactAgent({
|
|
73
|
+
llm,
|
|
74
|
+
tools,
|
|
75
|
+
stateModifier: new SystemMessage(REFACTOR_SYSTEM_PROMPT),
|
|
76
|
+
});
|
|
77
|
+
let finalText = '';
|
|
78
|
+
const stream = agent.streamEvents({ messages: [new HumanMessage(userMessage)] }, { version: 'v2', recursionLimit: 50 });
|
|
79
|
+
for await (const { event, data, name: eventName } of stream) {
|
|
80
|
+
if (event === 'on_tool_start') {
|
|
81
|
+
onToolCall(eventName, data.input);
|
|
82
|
+
}
|
|
83
|
+
if (event === 'on_chat_model_end') {
|
|
84
|
+
const output = data.output;
|
|
85
|
+
if (typeof output?.content === 'string') {
|
|
86
|
+
finalText = output.content;
|
|
87
|
+
}
|
|
88
|
+
else if (Array.isArray(output?.content)) {
|
|
89
|
+
finalText = output.content
|
|
90
|
+
.filter((c) => c.type === 'text')
|
|
91
|
+
.map((c) => c.text ?? '')
|
|
92
|
+
.join('');
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return finalText || 'Refactorer produced no output.';
|
|
97
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { makeLlm } from '../llm/factory.js';
|
|
2
|
+
import { createReactAgent } from '@langchain/langgraph/prebuilt';
|
|
3
|
+
import { SystemMessage, HumanMessage } from '@langchain/core/messages';
|
|
4
|
+
import { listFilesTool, readFileTool, searchCodeTool, bashTool } from '../tools/index.js';
|
|
5
|
+
import { log } from '../logger.js';
|
|
6
|
+
const REVIEW_SYSTEM_PROMPT = `You are a read-only code review agent.
|
|
7
|
+
|
|
8
|
+
Your job: analyze the user's target files and return a structured report.
|
|
9
|
+
|
|
10
|
+
HARD STOP CONDITIONS:
|
|
11
|
+
- After reading the target file(s) and running tsc/eslint once, you have all the data you need — produce the report. Do not re-read files or re-run analyses.
|
|
12
|
+
- Total tool calls per run: maximum 8.
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
Strict rules:
|
|
16
|
+
- You cannot write or edit any file. You have no writeFile / editFile tool.
|
|
17
|
+
- If the user asks you to fix or refactor anything, refuse and tell them to use /coder or /refactor instead.
|
|
18
|
+
- Be specific: cite file paths and line numbers when possible.
|
|
19
|
+
- Severity guide:
|
|
20
|
+
- CRITICAL: bugs, security holes, type errors, runtime crashes
|
|
21
|
+
- WARNING: bad practice, missing error handling, unclear logic, dead code
|
|
22
|
+
- SUGGESTION: style, naming, small improvements, doc gaps
|
|
23
|
+
|
|
24
|
+
Workflow:
|
|
25
|
+
1. If the user named files, read them with readFile.
|
|
26
|
+
If they said "this project" or similar, use listFiles + searchCode to find relevant files first.
|
|
27
|
+
2. Run \`npx tsc --noEmit\` via the bash tool and capture type errors.
|
|
28
|
+
3. Run \`npx eslint <changed-files>\` via the bash tool (skip if eslint isn't configured).
|
|
29
|
+
4. Read the files. Combine static-analysis output with your own reading.
|
|
30
|
+
5. Return a markdown report in EXACTLY this shape:
|
|
31
|
+
|
|
32
|
+
## Summary
|
|
33
|
+
<one or two sentences — overall health>
|
|
34
|
+
|
|
35
|
+
## Issues
|
|
36
|
+
|
|
37
|
+
### CRITICAL
|
|
38
|
+
- file.ts:LINE — what is wrong and why it matters
|
|
39
|
+
|
|
40
|
+
### WARNING
|
|
41
|
+
- file.ts:LINE — what is wrong and how to think about it
|
|
42
|
+
|
|
43
|
+
### SUGGESTION
|
|
44
|
+
- file.ts:LINE — small improvement
|
|
45
|
+
|
|
46
|
+
## What's good
|
|
47
|
+
- one or two things the code does well
|
|
48
|
+
|
|
49
|
+
If a section has no items, write "none" under its heading. Do not omit sections.
|
|
50
|
+
|
|
51
|
+
TERMINAL OUTPUT RULES — these override everything else about formatting:
|
|
52
|
+
- No markdown headers (no ##, no ###). Use plain uppercase labels: SUMMARY, CRITICAL, WARNING, SUGGESTION, GOOD.
|
|
53
|
+
- No **bold** or *italic*. Plain text only.
|
|
54
|
+
- No bullet dashes longer than needed — one short line per issue.
|
|
55
|
+
- Each issue: filename:LINE — one sentence max.
|
|
56
|
+
- Total response: 25 lines maximum. Cut low-value suggestions if needed to stay under.`;
|
|
57
|
+
export async function runReviewer(userMessage, onToolCall, model) {
|
|
58
|
+
const llm = makeLlm(model);
|
|
59
|
+
const tools = [listFilesTool, readFileTool, searchCodeTool, bashTool];
|
|
60
|
+
const agent = createReactAgent({
|
|
61
|
+
llm,
|
|
62
|
+
tools,
|
|
63
|
+
stateModifier: new SystemMessage(REVIEW_SYSTEM_PROMPT),
|
|
64
|
+
});
|
|
65
|
+
let finalText = '';
|
|
66
|
+
const stream = agent.streamEvents({ messages: [new HumanMessage(userMessage)] }, { version: 'v2', recursionLimit: 50 });
|
|
67
|
+
for await (const { event, data, name: eventName } of stream) {
|
|
68
|
+
if (event === 'on_tool_start') {
|
|
69
|
+
onToolCall(eventName, data.input);
|
|
70
|
+
log('review:tool', { tool: eventName, input: JSON.stringify(data.input).slice(0, 80) });
|
|
71
|
+
}
|
|
72
|
+
if (event === 'on_chat_model_end') {
|
|
73
|
+
const output = data.output;
|
|
74
|
+
if (typeof output?.content === 'string') {
|
|
75
|
+
finalText = output.content;
|
|
76
|
+
}
|
|
77
|
+
else if (Array.isArray(output?.content)) {
|
|
78
|
+
finalText = output.content
|
|
79
|
+
.filter((c) => c.type === 'text')
|
|
80
|
+
.map((c) => c.text ?? '')
|
|
81
|
+
.join('');
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
log('review:done', { lines: finalText.split('\n').length });
|
|
86
|
+
return finalText || 'Review produced no output.';
|
|
87
|
+
}
|
package/dist/agents/reviewer.js
CHANGED
|
@@ -1,17 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
// REPLACE the whole file with:
|
|
2
|
+
import { makeLlm } from '../llm/factory.js';
|
|
2
3
|
import { SystemMessage, HumanMessage } from "@langchain/core/messages";
|
|
3
|
-
|
|
4
|
-
model
|
|
5
|
-
|
|
6
|
-
});
|
|
7
|
-
export async function runReviewer(plan, coderOutput, userMessage) {
|
|
8
|
-
const response = await reviewerLlm.invoke([
|
|
4
|
+
export async function runReviewer(plan, coderOutput, userMessage, model) {
|
|
5
|
+
const llm = makeLlm(model);
|
|
6
|
+
const response = await llm.invoke([
|
|
9
7
|
new SystemMessage(`You are a code reviewer. check if the coder's work correctly fulfills the user's request. Return valid JSON only:
|
|
10
8
|
{
|
|
11
|
-
"approved":true or false,
|
|
9
|
+
"approved": true or false,
|
|
12
10
|
"feedback": "what needs fixing if not approved, otherwise empty string",
|
|
13
11
|
"finalResponse": "the final message to show the user summarizing what was done"
|
|
14
|
-
|
|
15
12
|
}`),
|
|
16
13
|
new HumanMessage(`User request: ${userMessage}\n\nPlan:\n${plan}\n\nWhat was done:\n${coderOutput}`)
|
|
17
14
|
]);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import fg from 'fast-glob';
|
|
5
|
+
function runCommand(cmd, args, cwd, timeoutMs = 60_000) {
|
|
6
|
+
return new Promise(resolve => {
|
|
7
|
+
const proc = spawn(cmd, args, { cwd, timeout: timeoutMs, shell: true });
|
|
8
|
+
let stdout = '', stderr = '';
|
|
9
|
+
proc.stdout?.on('data', d => { stdout += d.toString(); });
|
|
10
|
+
proc.stderr?.on('data', d => { stderr += d.toString(); });
|
|
11
|
+
proc.on('close', code => resolve({ ok: code === 0, stdout, stderr }));
|
|
12
|
+
proc.on('error', err => resolve({ ok: false, stdout, stderr: err.message }));
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
async function findTsConfigs() {
|
|
16
|
+
// Walk down from cwd (max 2 levels deep) to find tsconfig.json files,
|
|
17
|
+
// ignoring node_modules / dist / .next / .git
|
|
18
|
+
return fg(['tsconfig.json', '*/tsconfig.json', '*/*/tsconfig.json'], {
|
|
19
|
+
cwd: process.cwd(),
|
|
20
|
+
ignore: ['node_modules/**', 'dist/**', '.next/**', '.git/**', 'build/**'],
|
|
21
|
+
onlyFiles: true,
|
|
22
|
+
suppressErrors: true,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
async function findEslintConfigs() {
|
|
26
|
+
return fg([
|
|
27
|
+
'.eslintrc.json', '.eslintrc.js', '.eslintrc.cjs', 'eslint.config.js',
|
|
28
|
+
'*/.eslintrc.json', '*/.eslintrc.js', '*/eslint.config.js',
|
|
29
|
+
], {
|
|
30
|
+
cwd: process.cwd(),
|
|
31
|
+
ignore: ['node_modules/**', 'dist/**', '.git/**'],
|
|
32
|
+
onlyFiles: true,
|
|
33
|
+
suppressErrors: true,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
const MAX_ERROR_LINES = 30;
|
|
37
|
+
export async function runValidator() {
|
|
38
|
+
const result = {
|
|
39
|
+
tsc: { ok: true, errors: [], ran: false },
|
|
40
|
+
eslint: { ok: true, errors: [], ran: false },
|
|
41
|
+
overallOk: true,
|
|
42
|
+
};
|
|
43
|
+
const tsConfigs = await findTsConfigs();
|
|
44
|
+
if (tsConfigs.length === 0) {
|
|
45
|
+
result.tsc = { ok: true, errors: [], ran: false };
|
|
46
|
+
result.overallOk = true;
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
for (const cfg of tsConfigs) {
|
|
50
|
+
result.tsc.ran = true;
|
|
51
|
+
const projectDir = path.dirname(path.resolve(process.cwd(), cfg));
|
|
52
|
+
const tscRes = await runCommand('npx', ['tsc', '--noEmit'], projectDir);
|
|
53
|
+
if (!tscRes.ok) {
|
|
54
|
+
const lines = (tscRes.stdout + tscRes.stderr)
|
|
55
|
+
.split('\n')
|
|
56
|
+
.map(l => l.trim())
|
|
57
|
+
.filter(l => l.length > 0 && /error TS\d+/i.test(l));
|
|
58
|
+
if (lines.length > 0) {
|
|
59
|
+
result.tsc.ok = false;
|
|
60
|
+
result.tsc.errors.push(`[${cfg}]`, ...lines.slice(0, MAX_ERROR_LINES));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const eslintConfigs = await findEslintConfigs();
|
|
65
|
+
for (const cfg of eslintConfigs) {
|
|
66
|
+
result.eslint.ran = true;
|
|
67
|
+
const projectDir = path.dirname(path.resolve(process.cwd(), cfg));
|
|
68
|
+
const eslintRes = await runCommand('npx', ['eslint', '.', '--format', 'compact'], projectDir);
|
|
69
|
+
if (!eslintRes.ok) {
|
|
70
|
+
const lines = (eslintRes.stdout + eslintRes.stderr)
|
|
71
|
+
.split('\n')
|
|
72
|
+
.map(l => l.trim())
|
|
73
|
+
.filter(l => l.length > 0 && !l.startsWith('npm '))
|
|
74
|
+
.filter(l => /error|warning/i.test(l));
|
|
75
|
+
if (lines.length > 0) {
|
|
76
|
+
result.eslint.ok = false;
|
|
77
|
+
result.eslint.errors.push(`[${cfg}]`, ...lines.slice(0, MAX_ERROR_LINES));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
result.overallOk = result.tsc.ok && result.eslint.ok;
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
export function formatValidationErrors(v) {
|
|
85
|
+
const parts = [];
|
|
86
|
+
if (!v.tsc.ran)
|
|
87
|
+
parts.push('No tsconfig found — TypeScript was not validated. Treat output as UNVERIFIED.');
|
|
88
|
+
if (!v.tsc.ok)
|
|
89
|
+
parts.push(`TypeScript errors:\n${v.tsc.errors.join('\n')}`);
|
|
90
|
+
if (!v.eslint.ok)
|
|
91
|
+
parts.push(`ESLint errors:\n${v.eslint.errors.join('\n')}`);
|
|
92
|
+
return parts.join('\n\n');
|
|
93
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export function buildTopology(steps) {
|
|
2
|
+
const levelMap = new Map();
|
|
3
|
+
function getLevel(id) {
|
|
4
|
+
if (levelMap.has(id))
|
|
5
|
+
return levelMap.get(id);
|
|
6
|
+
const step = steps.find(s => s.id === id);
|
|
7
|
+
if (!step || step.depends_on.length === 0) {
|
|
8
|
+
levelMap.set(id, 0);
|
|
9
|
+
return 0;
|
|
10
|
+
}
|
|
11
|
+
const level = Math.max(...step.depends_on.map(getLevel)) + 1;
|
|
12
|
+
levelMap.set(id, level);
|
|
13
|
+
return level;
|
|
14
|
+
}
|
|
15
|
+
steps.forEach(s => getLevel(s.id));
|
|
16
|
+
const byLevel = new Map();
|
|
17
|
+
for (const [id, level] of levelMap) {
|
|
18
|
+
if (!byLevel.has(level))
|
|
19
|
+
byLevel.set(level, []);
|
|
20
|
+
byLevel.get(level).push(id);
|
|
21
|
+
}
|
|
22
|
+
const groups = [...byLevel.keys()]
|
|
23
|
+
.sort((a, b) => a - b)
|
|
24
|
+
.map(level => {
|
|
25
|
+
const stepIds = byLevel.get(level);
|
|
26
|
+
return { type: stepIds.length > 1 ? `parallel` : `seq`, stepIds };
|
|
27
|
+
});
|
|
28
|
+
return { groups };
|
|
29
|
+
}
|
|
30
|
+
export function formatTopologyAsPlan(topology, steps) {
|
|
31
|
+
return topology.groups.map((group, i) => {
|
|
32
|
+
const label = group.type === 'parallel' ? `Group ${i + 1} - run in parallel` : `Group ${i + 1}`;
|
|
33
|
+
const lines = group.stepIds.map(id => {
|
|
34
|
+
const step = steps.find(s => s.id === id);
|
|
35
|
+
return step ? ` Step ${step.id} [${step.action}]\n Why: ${step.why}`
|
|
36
|
+
: ` Step ${id}`;
|
|
37
|
+
});
|
|
38
|
+
return `${label}:\n${lines.join('\n')}`;
|
|
39
|
+
}).join(`\n\n`);
|
|
40
|
+
}
|
|
41
|
+
export function topologySummary(topology) {
|
|
42
|
+
const parallel = topology.groups.filter(g => g.type === 'parallel').length;
|
|
43
|
+
const total = topology.groups.length;
|
|
44
|
+
return `${total} group${total !== 1 ? 's' : ''}, ${parallel} parallel`;
|
|
45
|
+
}
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import { randomUUID } from 'crypto';
|
|
5
|
+
const GLITOOL_DIR = join(os.homedir(), '.glitool');
|
|
6
|
+
const ANON_FILE = join(GLITOOL_DIR, 'anon.json');
|
|
7
|
+
const AUTH_FILE = join(GLITOOL_DIR, 'auth.json');
|
|
8
|
+
const BACKEND_URL = process.env.GLITOOL_BACKEND ?? 'https://api.glit.in';
|
|
9
|
+
export const ANON_LIMIT = 5;
|
|
10
|
+
function ensureDir() {
|
|
11
|
+
mkdirSync(GLITOOL_DIR, { recursive: true });
|
|
12
|
+
}
|
|
13
|
+
function readAnon() {
|
|
14
|
+
ensureDir();
|
|
15
|
+
if (!existsSync(ANON_FILE)) {
|
|
16
|
+
const data = {
|
|
17
|
+
uuid: randomUUID(),
|
|
18
|
+
requestCount: 0,
|
|
19
|
+
createdAt: new Date().toISOString(),
|
|
20
|
+
};
|
|
21
|
+
writeFileSync(ANON_FILE, JSON.stringify(data, null, 2), 'utf-8');
|
|
22
|
+
return data;
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
return JSON.parse(readFileSync(ANON_FILE, 'utf-8'));
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
const data = { uuid: randomUUID(), requestCount: 0, createdAt: new Date().toISOString() };
|
|
29
|
+
writeFileSync(ANON_FILE, JSON.stringify(data, null, 2), 'utf-8');
|
|
30
|
+
return data;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function writeAnon(data) {
|
|
34
|
+
ensureDir();
|
|
35
|
+
writeFileSync(ANON_FILE, JSON.stringify(data, null, 2), 'utf-8');
|
|
36
|
+
}
|
|
37
|
+
export function getOrCreateAnonId() {
|
|
38
|
+
return readAnon().uuid;
|
|
39
|
+
}
|
|
40
|
+
export function getAnonRequestCount() {
|
|
41
|
+
return readAnon().requestCount;
|
|
42
|
+
}
|
|
43
|
+
export function incrementAnonCount() {
|
|
44
|
+
const data = readAnon();
|
|
45
|
+
data.requestCount += 1;
|
|
46
|
+
writeAnon(data);
|
|
47
|
+
return data.requestCount;
|
|
48
|
+
}
|
|
49
|
+
export function isAnonLimitReached() {
|
|
50
|
+
return readAnon().requestCount >= ANON_LIMIT;
|
|
51
|
+
}
|
|
52
|
+
export function readAuth() {
|
|
53
|
+
ensureDir();
|
|
54
|
+
if (!existsSync(AUTH_FILE))
|
|
55
|
+
return null;
|
|
56
|
+
try {
|
|
57
|
+
return JSON.parse(readFileSync(AUTH_FILE, 'utf-8'));
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export function saveAuth(data) {
|
|
64
|
+
ensureDir();
|
|
65
|
+
writeFileSync(AUTH_FILE, JSON.stringify(data, null, 2), 'utf-8');
|
|
66
|
+
}
|
|
67
|
+
export function getAuthToken() {
|
|
68
|
+
return readAuth()?.token ?? null;
|
|
69
|
+
}
|
|
70
|
+
export function isAuthenticated() {
|
|
71
|
+
return !!readAuth()?.token;
|
|
72
|
+
}
|
|
73
|
+
export async function startDeviceFlow() {
|
|
74
|
+
const res = await fetch(`${BACKEND_URL}/auth/device`, {
|
|
75
|
+
method: 'POST',
|
|
76
|
+
headers: { 'Content-Type': 'application/json' },
|
|
77
|
+
});
|
|
78
|
+
if (!res.ok)
|
|
79
|
+
throw new Error(`Server error: ${res.status}`);
|
|
80
|
+
return res.json();
|
|
81
|
+
}
|
|
82
|
+
export async function pollDeviceFlow(deviceCode) {
|
|
83
|
+
const res = await fetch(`${BACKEND_URL}/auth/device/poll?device_code=${deviceCode}`);
|
|
84
|
+
if (!res.ok)
|
|
85
|
+
throw new Error(`Poll error: ${res.status}`);
|
|
86
|
+
return res.json();
|
|
87
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/config.js
CHANGED
|
@@ -6,7 +6,10 @@ const DEFAULTS = {
|
|
|
6
6
|
name: 'Developer',
|
|
7
7
|
preferredLanguage: 'TypeScript',
|
|
8
8
|
codingStyle: 'spaces',
|
|
9
|
-
preferredModel: '
|
|
9
|
+
preferredModel: 'meta-llama/Llama-3.3-70B-Instruct-Turbo',
|
|
10
|
+
routing: {
|
|
11
|
+
useLlmClassifier: true,
|
|
12
|
+
},
|
|
10
13
|
};
|
|
11
14
|
export function loadConfig() {
|
|
12
15
|
try {
|
package/dist/confirmHandler.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { log } from "./logger.js";
|
|
1
2
|
let handler = async () => true;
|
|
2
3
|
export function setConfirmHandler(fn) {
|
|
3
4
|
handler = fn;
|
|
4
5
|
}
|
|
5
|
-
export function requestConfirm(
|
|
6
|
-
|
|
6
|
+
export async function requestConfirm(req) {
|
|
7
|
+
log('confirm:requested', { filePath: req.filePath, type: req.type });
|
|
8
|
+
return handler(req);
|
|
7
9
|
}
|
package/dist/index.js
CHANGED
|
@@ -12,9 +12,9 @@ import { generateAndSaveSummary } from "./memory.js";
|
|
|
12
12
|
import { llm, sessionMessages } from "./agent.js";
|
|
13
13
|
import { extractAndSaveProjectMemory } from "./projectMemory.js";
|
|
14
14
|
import os from 'os';
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
15
|
+
// import { mkdirSync, writeFileSync, existsSync } from 'fs';
|
|
16
|
+
// import { createInterface } from "readline";
|
|
17
|
+
import { mkdirSync, existsSync } from 'fs';
|
|
18
18
|
const __filename = fileURLToPath(import.meta.url);
|
|
19
19
|
const __dirname = dirname(__filename);
|
|
20
20
|
dotenvConfig({ path: join(os.homedir(), '.glitool', '.env') });
|
|
@@ -65,30 +65,17 @@ program
|
|
|
65
65
|
});
|
|
66
66
|
async function ensureApiKey() {
|
|
67
67
|
if (process.env.OPENAI_API_KEY)
|
|
68
|
-
return;
|
|
69
|
-
|
|
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');
|
|
68
|
+
return; // BYOK mode — use OpenAI directly
|
|
69
|
+
// Otherwise Glitool backend handles auth (anon trial or glt_ token)
|
|
88
70
|
}
|
|
89
71
|
const saveAndExit = async () => {
|
|
90
|
-
|
|
91
|
-
|
|
72
|
+
try {
|
|
73
|
+
await generateAndSaveSummary(sessionMessages, llm);
|
|
74
|
+
await extractAndSaveProjectMemory(sessionMessages, llm);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// exit cleanly even if LLM is unreachable
|
|
78
|
+
}
|
|
92
79
|
process.exit(0);
|
|
93
80
|
};
|
|
94
81
|
process.on('SIGINT', saveAndExit);
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { makeLlm } from './factory.js';
|
|
2
|
+
import { SystemMessage, HumanMessage } from "@langchain/core/messages";
|
|
3
|
+
import { makeInternalLlm } from './factory.js';
|
|
4
|
+
const VALID_DOMAINS = [
|
|
5
|
+
'chat', 'coding', 'explanation', 'planning', 'debugging', 'refactoring', 'review', 'git'
|
|
6
|
+
];
|
|
7
|
+
const classifierLlm = makeInternalLlm('gpt-4o-mini', {
|
|
8
|
+
temperature: 0,
|
|
9
|
+
modelKwargs: { response_format: { type: 'json_object' } },
|
|
10
|
+
});
|
|
11
|
+
export async function classifyWithLlm(prompt, recentMessages) {
|
|
12
|
+
try {
|
|
13
|
+
const history = recentMessages.slice(-6).map(m => {
|
|
14
|
+
const role = m._getType() === 'human' ? 'User' : 'Assistant';
|
|
15
|
+
const text = (typeof m.content === 'string' ? m.content : '').slice(0, 300);
|
|
16
|
+
return `${role}: ${text}`;
|
|
17
|
+
}).join('\n').slice(0, 1000);
|
|
18
|
+
const response = await classifierLlm.invoke([
|
|
19
|
+
new SystemMessage(`You are a routing classifier for a coding assistant CLI.
|
|
20
|
+
Classify the user's message into exactly ONE domain. Use the conversation history to resolve pronouns and references.
|
|
21
|
+
|
|
22
|
+
CRITICAL: Classify by the user's INTENT, not by words appearing in file paths or identifiers. A file named "git-agent.ts" being mentioned does NOT make the request a git task. "agent" in a path does NOT mean coding. Look at the verb and what the user wants to happen.
|
|
23
|
+
|
|
24
|
+
Domains:
|
|
25
|
+
- chat: greetings, opinions, casual questions with no file or code operation needed. Examples: "hi", "thanks", "what do you think about X".
|
|
26
|
+
- explanation: the user wants to UNDERSTAND something that already exists. Reading a file, summarizing code, explaining what a function does. Examples: "read router.ts", "what does this function do", "show me package.json", "how does auth work here".
|
|
27
|
+
- coding: the user wants to CREATE or MODIFY code to add functionality. New features, new files, new logic. Examples: "build a todo CLI", "add a login route", "implement caching".
|
|
28
|
+
- debugging: the user has an ERROR, crash, failing test, or broken behavior they want fixed. Examples: "fix this crash", "tests are failing", "why does X throw".
|
|
29
|
+
- refactoring: the user wants STRUCTURAL change with identical behavior. Rename, extract, dedupe, simplify. Examples: "clean up router.ts", "extract this into a helper", "rename X to Y".
|
|
30
|
+
- review: the user wants a READ-ONLY audit or quality assessment. Examples: "review my router", "any security issues here", "is this code good".
|
|
31
|
+
- planning: the user wants a DESIGN DOCUMENT or roadmap written to plan.md. Examples: "plan a payment system", "design the auth flow", "roadmap for v2".
|
|
32
|
+
- git: ONLY for actual git/version-control operations — commit, diff, push, pull, branch, log, status, stash. Reading source files via paths like "src/foo.ts" is NEVER git, even when the filename contains the word "git". Examples: "commit my changes", "what's on this branch", "write a commit message".
|
|
33
|
+
|
|
34
|
+
Tie-breakers:
|
|
35
|
+
- "read/show/open/cat/view <path>" → explanation
|
|
36
|
+
- "fix <error>" → debugging
|
|
37
|
+
- "add/build/create <feature>" → coding
|
|
38
|
+
- "clean up/refactor/rename <code>" → refactoring
|
|
39
|
+
- "review/audit/check <code>" → review
|
|
40
|
+
- "plan/design/roadmap <thing>" → planning
|
|
41
|
+
- starts with the literal word "git" → git
|
|
42
|
+
- short greeting or opinion question → chat
|
|
43
|
+
|
|
44
|
+
Return JSON: {"domain":"<domain>","confidence":"high" or "low", "reason":"<one sentence>"}`),
|
|
45
|
+
new HumanMessage(history ? `Conversation so far:\n${history}\n\nNew message: ${prompt}` : `Message: ${prompt}`)
|
|
46
|
+
], { timeout: 3000 });
|
|
47
|
+
const raw = typeof response.content === 'string' ? response.content : '';
|
|
48
|
+
const parsed = JSON.parse(raw);
|
|
49
|
+
if (!VALID_DOMAINS.includes(parsed.domain))
|
|
50
|
+
return null;
|
|
51
|
+
return {
|
|
52
|
+
domain: parsed.domain,
|
|
53
|
+
confidence: parsed.confidence === 'high' ? 'high' : 'low',
|
|
54
|
+
reason: parsed.reason ?? '',
|
|
55
|
+
tokens: response.usage_metadata?.total_tokens ?? 0,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { ChatOpenAI } from '@langchain/openai';
|
|
2
|
+
import { randomUUID } from 'crypto';
|
|
3
|
+
import { getAuthToken, getOrCreateAnonId } from '../auth.js';
|
|
4
|
+
const BACKEND_URL = process.env.GLITOOL_BACKEND ?? 'https://api.glit.in';
|
|
5
|
+
let currentRequestId = null;
|
|
6
|
+
export function startNewRequest() {
|
|
7
|
+
currentRequestId = randomUUID();
|
|
8
|
+
return currentRequestId;
|
|
9
|
+
}
|
|
10
|
+
function requestIdHeader() {
|
|
11
|
+
return currentRequestId ? { 'X-Glitool-Request-ID': currentRequestId } : {};
|
|
12
|
+
}
|
|
13
|
+
export function makeLlm(model, extras = {}) {
|
|
14
|
+
if (process.env.OPENAI_API_KEY) {
|
|
15
|
+
return new ChatOpenAI({ model, apiKey: process.env.OPENAI_API_KEY, ...extras });
|
|
16
|
+
}
|
|
17
|
+
const token = getAuthToken();
|
|
18
|
+
if (token) {
|
|
19
|
+
return new ChatOpenAI({
|
|
20
|
+
model,
|
|
21
|
+
apiKey: token,
|
|
22
|
+
configuration: {
|
|
23
|
+
baseURL: `${BACKEND_URL}/v1`,
|
|
24
|
+
defaultHeaders: requestIdHeader(),
|
|
25
|
+
},
|
|
26
|
+
...extras,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
return new ChatOpenAI({
|
|
30
|
+
model,
|
|
31
|
+
apiKey: 'anon',
|
|
32
|
+
configuration: {
|
|
33
|
+
baseURL: `${BACKEND_URL}/v1`,
|
|
34
|
+
defaultHeaders: { 'X-Anon-ID': getOrCreateAnonId(), ...requestIdHeader() },
|
|
35
|
+
},
|
|
36
|
+
...extras,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
export function makeInternalLlm(model, extras = {}) {
|
|
40
|
+
if (process.env.OPENAI_API_KEY) {
|
|
41
|
+
return new ChatOpenAI({ model, apiKey: process.env.OPENAI_API_KEY, ...extras });
|
|
42
|
+
}
|
|
43
|
+
const token = getAuthToken();
|
|
44
|
+
const anonHeaders = token ? {} : { 'X-Anon-ID': getOrCreateAnonId() };
|
|
45
|
+
return new ChatOpenAI({
|
|
46
|
+
model,
|
|
47
|
+
apiKey: token ?? 'anon',
|
|
48
|
+
configuration: {
|
|
49
|
+
baseURL: `${BACKEND_URL}/v1`,
|
|
50
|
+
defaultHeaders: {
|
|
51
|
+
...anonHeaders,
|
|
52
|
+
'X-Glitool-Internal': 'true',
|
|
53
|
+
...requestIdHeader(),
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
...extras,
|
|
57
|
+
});
|
|
58
|
+
}
|