feat: CrowdCode CLI für Terminal/CMD (Linux + Windows)
- cli/crowdcode.js: Node.js CLI mit Streaming-Chat, @mentions, FILE:/DIR:/EDIT:/bash Pattern-Erkennung + Ausführung - crowdcode.sh: Linux-Wrapper - crowdcode.bat: Windows-Wrapper - Funktioniert ohne VSCode, nutzt llama.cpp API
This commit is contained in:
parent
75fb3cc343
commit
da86c2d83b
3 changed files with 480 additions and 0 deletions
464
cli/crowdcode.js
Normal file
464
cli/crowdcode.js
Normal file
|
|
@ -0,0 +1,464 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
// crowdcode.js — MiniLlama Terminal CLI (Linux + Windows)
|
||||||
|
// Chat + Dateioperationen via llama.cpp API
|
||||||
|
// Aufruf: node crowdcode.js [endpoint] [model]
|
||||||
|
|
||||||
|
// ─── Konfiguration ────────────────────────────────────────────────────────────
|
||||||
|
const API = process.argv[2] || 'http://127.0.0.1:8080/v1';
|
||||||
|
const MODEL = process.argv[3] || '';
|
||||||
|
|
||||||
|
// ─── Terminal-Farben (ANSI, funktioniert auf Win/Lin) ─────────────────────────
|
||||||
|
const C = {
|
||||||
|
reset: '\x1b[0m',
|
||||||
|
bold: '\x1b[1m',
|
||||||
|
dim: '\x1b[2m',
|
||||||
|
green: '\x1b[32m',
|
||||||
|
yellow: '\x1b[33m',
|
||||||
|
blue: '\x1b[34m',
|
||||||
|
magenta: '\x1b[35m',
|
||||||
|
cyan: '\x1b[36m',
|
||||||
|
red: '\x1b[31m',
|
||||||
|
grey: '\x1b[90m',
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── async/await readline ─────────────────────────────────────────────────────
|
||||||
|
const readline = require('readline');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { execSync } = require('child_process');
|
||||||
|
|
||||||
|
const WORKSPACE = process.argv[4] ? path.resolve(process.argv[4]) : process.cwd();
|
||||||
|
|
||||||
|
const SYSTEM_PROMPT = `You are CrowdCode, a concise coding assistant.
|
||||||
|
Context: @file reads a file, @dir/ lists a directory, @dir/** shows the tree, @?term searches in files.
|
||||||
|
When writing or rewriting a file, ALWAYS output FILE: path/to/file on its own line BEFORE the fenced code block with the complete file content.
|
||||||
|
To create a directory: DIR: path/to/dir on its own line.
|
||||||
|
For targeted edits: EDIT: path/to/file followed by ---OLD, old text, ---NEW, new text, --- on separate lines.
|
||||||
|
To run shell commands (only when user explicitly asks): output a fenced \`\`\`bash code block.
|
||||||
|
No explanations unless the user asks a question.
|
||||||
|
NEVER output touch, mkdir, echo, cat commands — use FILE: and DIR: instead.`;
|
||||||
|
|
||||||
|
function askSingleLine(prompt) {
|
||||||
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
rl.question(prompt, (answer) => { rl.close(); resolve(answer.trim()); });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function askInput() {
|
||||||
|
const line = await askSingleLine(`${C.cyan}›${C.reset} `);
|
||||||
|
if (line === ':m') {
|
||||||
|
// Multiline-Modus
|
||||||
|
return askMultiline();
|
||||||
|
}
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
|
||||||
|
function askMultiline() {
|
||||||
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const lines = [];
|
||||||
|
console.log(`${C.dim} (Multiline — ${C.reset}:send${C.dim} zum Senden, ${C.reset}:cancel${C.dim} zum Abbrechen)${C.reset}`);
|
||||||
|
rl.on('line', (line) => {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (trimmed === ':send' || trimmed === ':s') { rl.close(); return; }
|
||||||
|
if (trimmed === ':cancel' || trimmed === ':c') { rl.close(); resolve(null); return; }
|
||||||
|
lines.push(line);
|
||||||
|
});
|
||||||
|
rl.on('close', () => resolve(lines.join('\n').replace(/\n+$/, '')));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Modelle auflisten ─────────────────────────────────────────────────────────
|
||||||
|
async function fetchModels() {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API}/models`);
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
|
const data = await res.json();
|
||||||
|
return data.data || data.models || [];
|
||||||
|
} catch { return []; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Chat Completion (Streaming) ───────────────────────────────────────────────
|
||||||
|
async function chatComplete(messages, onToken) {
|
||||||
|
const model = MODEL || (await fetchModels())[0]?.id || 'unknown';
|
||||||
|
const res = await fetch(`${API}/chat/completions`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer local' },
|
||||||
|
body: JSON.stringify({ model, messages, stream: true, temperature: 0.2, top_p: 0.9 }),
|
||||||
|
});
|
||||||
|
if (!res.ok || !res.body) throw new Error(`HTTP ${res.status}`);
|
||||||
|
const reader = res.body.getReader();
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
let fullText = '';
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
const chunk = decoder.decode(value, { stream: true });
|
||||||
|
for (const line of chunk.split('\n')) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed.startsWith('data:')) continue;
|
||||||
|
const data = trimmed.slice(5).trim();
|
||||||
|
if (data === '[DONE]') continue;
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(data);
|
||||||
|
// reasoning_content für DeepSeek-artige Modelle
|
||||||
|
const reasoning = parsed.choices?.[0]?.delta?.reasoning_content || '';
|
||||||
|
const token = parsed.choices?.[0]?.delta?.content || '';
|
||||||
|
if (reasoning) {
|
||||||
|
fullText += reasoning;
|
||||||
|
onToken(reasoning, true);
|
||||||
|
}
|
||||||
|
if (token) {
|
||||||
|
fullText += token;
|
||||||
|
onToken(token, false);
|
||||||
|
}
|
||||||
|
} catch { /* skip malformed JSON */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fullText;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── @mention Auflösung ───────────────────────────────────────────────────────
|
||||||
|
async function resolveMentions(text) {
|
||||||
|
const mentionRegex = /@(\S+)/g;
|
||||||
|
let result = text;
|
||||||
|
for (const match of text.matchAll(mentionRegex)) {
|
||||||
|
const ref = match[1];
|
||||||
|
let replacement = '';
|
||||||
|
try {
|
||||||
|
const targetPath = path.resolve(WORKSPACE, ref);
|
||||||
|
// @dir/** — tree
|
||||||
|
if (ref.endsWith('/**')) {
|
||||||
|
const dirPath = ref.slice(0, -3);
|
||||||
|
replacement = await buildTree(path.resolve(WORKSPACE, dirPath), '');
|
||||||
|
}
|
||||||
|
// @dir/ — listing
|
||||||
|
else if (ref.endsWith('/')) {
|
||||||
|
const dirPath = ref.slice(0, -1);
|
||||||
|
const entries = fs.readdirSync(path.resolve(WORKSPACE, dirPath), { withFileTypes: true });
|
||||||
|
entries.sort((a, b) => {
|
||||||
|
if (a.isDirectory() !== b.isDirectory()) return a.isDirectory() ? -1 : 1;
|
||||||
|
return a.name.localeCompare(b.name);
|
||||||
|
});
|
||||||
|
replacement = entries.map(e => e.isDirectory() ? e.name + '/' : e.name).join('\n');
|
||||||
|
}
|
||||||
|
// @?term — grep
|
||||||
|
else if (ref.startsWith('?')) {
|
||||||
|
const term = ref.slice(1);
|
||||||
|
const isGlob = term.includes('*');
|
||||||
|
replacement = isGlob ? await globSearch(term) : await grepSearch(term);
|
||||||
|
}
|
||||||
|
// @file.ext — file read
|
||||||
|
else if (fs.existsSync(targetPath) && fs.statSync(targetPath).isFile()) {
|
||||||
|
const content = fs.readFileSync(targetPath, 'utf8');
|
||||||
|
const ext = ref.split('.').pop() || '';
|
||||||
|
replacement = `\n[Dateiinhalt: ${ref}]\n\`\`\`${ext}\n${content}\n\`\`\`\n`;
|
||||||
|
}
|
||||||
|
} catch { replacement = `(${ref}: nicht gefunden) ⚠️`; }
|
||||||
|
result = result.replace(match[0], replacement);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildTree(dirPath, prefix) {
|
||||||
|
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
||||||
|
entries.sort((a, b) => {
|
||||||
|
if (a.isDirectory() !== b.isDirectory()) return a.isDirectory() ? -1 : 1;
|
||||||
|
return a.name.localeCompare(b.name);
|
||||||
|
});
|
||||||
|
const lines = [];
|
||||||
|
for (let i = 0; i < entries.length; i++) {
|
||||||
|
const e = entries[i];
|
||||||
|
const isLast = i === entries.length - 1;
|
||||||
|
const conn = isLast ? '└── ' : '├── ';
|
||||||
|
lines.push(`${prefix}${conn}${e.name}${e.isDirectory() ? '/' : ''}`);
|
||||||
|
if (e.isDirectory()) {
|
||||||
|
const childPrefix = prefix + (isLast ? ' ' : '│ ');
|
||||||
|
const sub = await buildTree(path.join(dirPath, e.name), childPrefix);
|
||||||
|
lines.push(...sub);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function grepSearch(term) {
|
||||||
|
const results = [];
|
||||||
|
const maxFiles = 20;
|
||||||
|
const maxPerFile = 5;
|
||||||
|
let filesWithMatches = 0;
|
||||||
|
const allFiles = walkSync(WORKSPACE).filter(f => !f.includes('node_modules') && !f.includes('.git'));
|
||||||
|
for (const file of allFiles) {
|
||||||
|
if (filesWithMatches >= maxFiles) break;
|
||||||
|
try {
|
||||||
|
const content = fs.readFileSync(file, 'utf8');
|
||||||
|
const fileResults = [];
|
||||||
|
const lines = content.split('\n');
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
if (lines[i].toLowerCase().includes(term.toLowerCase())) {
|
||||||
|
const relPath = path.relative(WORKSPACE, file);
|
||||||
|
fileResults.push(` ${relPath}:${i + 1}: ${lines[i].trim()}`);
|
||||||
|
if (fileResults.length >= maxPerFile) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (fileResults.length > 0) { results.push(...fileResults); filesWithMatches++; }
|
||||||
|
} catch { /* skip */ }
|
||||||
|
}
|
||||||
|
return results.length > 0 ? results.join('\n') : `(keine Treffer für "${term}")`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function globSearch(pattern) {
|
||||||
|
const allFiles = walkSync(WORKSPACE).filter(f => !f.includes('node_modules') && !f.includes('.git'));
|
||||||
|
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*').replace(/\?/g, '.') + '$', 'i');
|
||||||
|
const matches = allFiles.map(f => path.relative(WORKSPACE, f)).filter(f => regex.test(f));
|
||||||
|
return matches.length > 0 ? matches.join('\n') : `(keine Dateien für "${pattern}")`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function walkSync(dir) {
|
||||||
|
const files = [];
|
||||||
|
try {
|
||||||
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||||
|
const fullPath = path.join(dir, entry.name);
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
if (entry.name !== 'node_modules' && entry.name !== '.git') files.push(...walkSync(fullPath));
|
||||||
|
} else { files.push(fullPath); }
|
||||||
|
}
|
||||||
|
} catch { /* permission denied, skip */ }
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Aktionen aus LLM-Response ausführen ─────────────────────────────────────
|
||||||
|
function executeActions(response) {
|
||||||
|
const actions = [];
|
||||||
|
|
||||||
|
// FILE: pattern
|
||||||
|
const fileRegex = /FILE:\s*(.+)\n```(?:\w+)?\n([\s\S]+?)```/g;
|
||||||
|
let m;
|
||||||
|
while ((m = fileRegex.exec(response)) !== null) {
|
||||||
|
actions.push({ type: 'file', path: m[1].trim(), content: m[2] });
|
||||||
|
}
|
||||||
|
|
||||||
|
// DIR: pattern
|
||||||
|
const dirRegex = /^DIR:\s*(.+)$/gm;
|
||||||
|
while ((m = dirRegex.exec(response)) !== null) {
|
||||||
|
actions.push({ type: 'dir', path: m[1].trim() });
|
||||||
|
}
|
||||||
|
|
||||||
|
// EDIT: pattern
|
||||||
|
const editRegex = /EDIT:\s*(.+?)\n---OLD\n([\s\S]*?)\n---NEW\n([\s\S]*?)\n---/gm;
|
||||||
|
while ((m = editRegex.exec(response)) !== null) {
|
||||||
|
actions.push({ type: 'edit', path: m[1].trim(), oldText: m[2], newText: m[3] });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bash pattern
|
||||||
|
const bashRegex = /```bash\n([\s\S]+?)```/g;
|
||||||
|
while ((m = bashRegex.exec(response)) !== null) {
|
||||||
|
actions.push({ type: 'bash', command: m[1].trim() });
|
||||||
|
}
|
||||||
|
|
||||||
|
return actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyActions(actions) {
|
||||||
|
for (const a of actions) {
|
||||||
|
switch (a.type) {
|
||||||
|
case 'file': {
|
||||||
|
const fullPath = path.resolve(WORKSPACE, a.path);
|
||||||
|
const parentDir = path.dirname(fullPath);
|
||||||
|
fs.mkdirSync(parentDir, { recursive: true });
|
||||||
|
fs.writeFileSync(fullPath, a.content, 'utf8');
|
||||||
|
console.log(` ${C.green}✓${C.reset} Datei gespeichert: ${path.relative(WORKSPACE, fullPath)}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'dir': {
|
||||||
|
const fullPath = path.resolve(WORKSPACE, a.path);
|
||||||
|
fs.mkdirSync(fullPath, { recursive: true });
|
||||||
|
console.log(` ${C.yellow}✓${C.reset} Verzeichnis erstellt: ${path.relative(WORKSPACE, fullPath)}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'edit': {
|
||||||
|
const fullPath = path.resolve(WORKSPACE, a.path);
|
||||||
|
if (fs.existsSync(fullPath)) {
|
||||||
|
let content = fs.readFileSync(fullPath, 'utf8');
|
||||||
|
if (content.includes(a.oldText)) {
|
||||||
|
content = content.replace(a.oldText, a.newText);
|
||||||
|
fs.writeFileSync(fullPath, content, 'utf8');
|
||||||
|
console.log(` ${C.magenta}✓${C.reset} Edit angewendet: ${path.relative(WORKSPACE, fullPath)}`);
|
||||||
|
} else {
|
||||||
|
console.log(` ${C.red}✗${C.reset} Edit fehlgeschlagen: "${a.oldText}" nicht gefunden in ${path.relative(WORKSPACE, fullPath)}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(` ${C.red}✗${C.reset} Datei nicht gefunden: ${path.relative(WORKSPACE, fullPath)}`);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'bash': {
|
||||||
|
console.log(` ${C.cyan}⚡${C.reset} Bash-Befehl erkannt:`);
|
||||||
|
console.log(` ${C.grey}${a.command}${C.reset}`);
|
||||||
|
const confirm = await ask(` ${C.yellow}Ausführen?${C.reset} (j/N) `);
|
||||||
|
if (confirm.toLowerCase() === 'j' || confirm.toLowerCase() === 'ja') {
|
||||||
|
try {
|
||||||
|
const out = execSync(a.command, { cwd: WORKSPACE, timeout: 30000, encoding: 'utf8', maxBuffer: 1024 * 512 });
|
||||||
|
if (out) console.log(` ${C.grey}${out.split('\n').map(l => ' ' + l).join('\n')}${C.reset}`);
|
||||||
|
console.log(` ${C.green}✓${C.reset} Bash beendet (exit 0)`);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(` ${C.red}✗${C.reset} Bash-Fehler: ${e.stderr || e.message}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(` ${C.dim} (übersprungen)${C.reset}`);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Modelle aus dem MiniLlama/models-Ordner anzeigen ─────────────────────────
|
||||||
|
function showAvailableModels(modelsDir) {
|
||||||
|
if (!fs.existsSync(modelsDir)) return [];
|
||||||
|
const files = fs.readdirSync(modelsDir).filter(f => f.endsWith('.gguf'));
|
||||||
|
if (files.length === 0) return [];
|
||||||
|
console.log(`\n${C.bold}📦 Verfügbare Modelle:${C.reset}`);
|
||||||
|
files.forEach((f, i) => {
|
||||||
|
try {
|
||||||
|
const stat = fs.statSync(path.join(modelsDir, f));
|
||||||
|
const size = (stat.size / 1024 / 1024 / 1024).toFixed(1);
|
||||||
|
console.log(` ${C.cyan}${i + 1})${C.reset} ${f} ${C.grey}(${size} GB)${C.reset}`);
|
||||||
|
} catch { console.log(` ${C.cyan}${i + 1})${C.reset} ${f}`); }
|
||||||
|
});
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Hauptprogramm ────────────────────────────────────────────────────────────
|
||||||
|
async function main() {
|
||||||
|
console.clear ? console.clear() : null;
|
||||||
|
console.log(`\n${C.bold}${C.cyan}╔══════════════════════════════════════════╗${C.reset}`);
|
||||||
|
console.log(`${C.bold}${C.cyan}║ 🦙 CrowdCode CLI — ${C.reset}miniLLM Chat ${C.bold}${C.cyan}║${C.reset}`);
|
||||||
|
console.log(`${C.bold}${C.cyan}╚══════════════════════════════════════════╝${C.reset}`);
|
||||||
|
console.log(`${C.grey} API: ${API} | Workspace: ${WORKSPACE}${C.reset}\n`);
|
||||||
|
|
||||||
|
// Verfügbare Modelle prüfen
|
||||||
|
const modelsDir = path.join(__dirname, '..', 'models');
|
||||||
|
const localModels = showAvailableModels(modelsDir);
|
||||||
|
|
||||||
|
// API-Verbindung testen
|
||||||
|
console.log(`\n${C.dim}🔌 Teste Verbindung zu ${API}...${C.reset}`);
|
||||||
|
let connected = false;
|
||||||
|
try {
|
||||||
|
const models = await fetchModels();
|
||||||
|
if (models.length > 0) {
|
||||||
|
const modelName = MODEL || models[0]?.id || models[0]?.name || models[0]?.model || '(unbekannt)';
|
||||||
|
console.log(` ${C.green}✅${C.reset} Verbunden. Modell: ${C.bold}${modelName}${C.reset}`);
|
||||||
|
connected = true;
|
||||||
|
} else {
|
||||||
|
console.log(` ${C.yellow}⚠️${C.reset} Server verbunden, aber keine Modelle geladen.`);
|
||||||
|
console.log(` ${C.dim} Stelle sicher, dass ein Modell geladen ist.${C.reset}`);
|
||||||
|
connected = true; // Server läuft trotzdem
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log(` ${C.red}❌${C.reset} Keine Verbindung zu ${API}`);
|
||||||
|
console.log(` ${C.dim} Starte zuerst den Server: ./start-server.sh${C.reset}`);
|
||||||
|
console.log(` ${C.dim} Oder: ./bin/linux/llama-server -m models/<modell>.gguf --host 127.0.0.1 --port 8080${C.reset}`);
|
||||||
|
if (localModels.length > 0) {
|
||||||
|
const ans = await ask(` ${C.yellow}Server lokal starten?${C.reset} (j/N) `);
|
||||||
|
if (ans.toLowerCase() === 'j' || ans.toLowerCase() === 'ja') {
|
||||||
|
const serverPath = path.join(__dirname, '..', 'start-server.sh');
|
||||||
|
if (fs.existsSync(serverPath)) {
|
||||||
|
const child = require('child_process').spawn(serverPath, [], { stdio: 'inherit', cwd: path.join(__dirname, '..') });
|
||||||
|
console.log(` ${C.dim}Server gestartet, drücke Enter wenn bereit...${C.reset}`);
|
||||||
|
await ask('');
|
||||||
|
// Erneut testen
|
||||||
|
try { await fetchModels(); connected = true; } catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!connected) {
|
||||||
|
console.log(`\n${C.red}Bitte zuerst llama-server starten.${C.reset}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Chat-Loop ──────────────────────────────────────────────────────────
|
||||||
|
const history = [{ role: 'system', content: SYSTEM_PROMPT }];
|
||||||
|
|
||||||
|
console.log(`\n${C.green}💬 Chat bereit.${C.reset} ${C.dim}Tippe deine Nachricht, :q zum Beenden, :h für Hilfe.${C.reset}`);
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const input = await askInput();
|
||||||
|
if (input === null) continue; // :c (abbrechen)
|
||||||
|
const trimmed = input.trim();
|
||||||
|
if (trimmed === ':q' || trimmed === ':quit' || trimmed === ':exit') break;
|
||||||
|
if (trimmed === ':h' || trimmed === ':help') {
|
||||||
|
console.log(`\n${C.bold}Hilfe:${C.reset}
|
||||||
|
:h, :help Diese Hilfe
|
||||||
|
:q, :quit Beenden
|
||||||
|
:m Multiline-Eingabe (für Code, :send zum Senden)
|
||||||
|
:clear Chat-Verlauf löschen
|
||||||
|
|
||||||
|
${C.cyan}@datei.py${C.reset} Dateiinhalt lesen
|
||||||
|
${C.cyan}@verz/${C.reset} Verzeichnis auflisten
|
||||||
|
${C.cyan}@verz/**${C.reset} Rekursiver Dateibaum
|
||||||
|
${C.cyan}@?begriff${C.reset} Grep-Suche
|
||||||
|
${C.cyan}@?*.ts${C.reset} Glob-Suche
|
||||||
|
|
||||||
|
Das LLM kann: FILE:, DIR:, EDIT:, \`\`\`bash ausgeben
|
||||||
|
`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (trimmed === ':clear' || trimmed === ':reset') {
|
||||||
|
history.length = 1; // nur System-Prompt behalten
|
||||||
|
console.log(` ${C.dim}Chat-Verlauf gelöscht.${C.reset}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!trimmed) continue;
|
||||||
|
|
||||||
|
// @mentions auflösen
|
||||||
|
const enhanced = await resolveMentions(trimmed);
|
||||||
|
history.push({ role: 'user', content: enhanced });
|
||||||
|
|
||||||
|
// Streaming-Antwort
|
||||||
|
let fullResponse = '';
|
||||||
|
let thinkingDone = false;
|
||||||
|
process.stdout.write(`\n${C.cyan}──${C.reset}`);
|
||||||
|
try {
|
||||||
|
await chatComplete(history, (token, isReasoning) => {
|
||||||
|
if (isReasoning) {
|
||||||
|
if (!thinkingDone) {
|
||||||
|
process.stdout.write(`\n${C.dim}🤔 `);
|
||||||
|
thinkingDone = true;
|
||||||
|
}
|
||||||
|
process.stdout.write(`${C.grey}${token}${C.reset}`);
|
||||||
|
} else {
|
||||||
|
if (thinkingDone) {
|
||||||
|
process.stdout.write(`${C.reset}\n${C.cyan}──${C.reset}`);
|
||||||
|
thinkingDone = false;
|
||||||
|
}
|
||||||
|
process.stdout.write(token);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
process.stdout.write(`\n${C.red}❌ Fehler: ${err.message}${C.reset}`);
|
||||||
|
console.log(`\n ${C.dim}Versuche erneute Verbindung...${C.reset}`);
|
||||||
|
history.pop(); // fehlgeschlagene User-Nachricht entfernen
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
process.stdout.write('\n');
|
||||||
|
history.push({ role: 'assistant', content: fullResponse });
|
||||||
|
|
||||||
|
// Aktionen ausführen
|
||||||
|
const actions = executeActions(fullResponse);
|
||||||
|
if (actions.length > 0) {
|
||||||
|
console.log(`\n${C.bold}Aktionen:${C.reset}`);
|
||||||
|
await applyActions(actions);
|
||||||
|
}
|
||||||
|
console.log(''); // Leerzeile vor nächstem Prompt
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n${C.cyan}Tschüss!${C.reset}\n`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error(`${C.red}❌ Fatal:${C.reset}`, err.message);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
11
crowdcode.bat
Normal file
11
crowdcode.bat
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
@echo off
|
||||||
|
:: crowdcode.bat — MiniLlama Terminal CLI (Windows)
|
||||||
|
:: Aufruf: crowdcode.bat [endpoint] [model]
|
||||||
|
cd /d "%~dp0"
|
||||||
|
echo CrowdCode CLI — MiniLlama Chat
|
||||||
|
node cli\crowdcode.js %*
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo.
|
||||||
|
echo Fehler beim Start von CrowdCode.
|
||||||
|
pause
|
||||||
|
)
|
||||||
5
crowdcode.sh
Executable file
5
crowdcode.sh
Executable file
|
|
@ -0,0 +1,5 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# crowdcode.sh — MiniLlama Terminal CLI (Linux)
|
||||||
|
# Aufruf: ./crowdcode.sh [endpoint] [model]
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
node "$SCRIPT_DIR/cli/crowdcode.js" "$@"
|
||||||
Loading…
Add table
Reference in a new issue