2026-07-09 10:44:22 +02:00
#!/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 } ` ) ;
2026-07-09 10:57:19 +02:00
const confirm = await askSingleLine ( ` ${ C . yellow } Ausführen? ${ C . reset } (j/N) ` ) ;
2026-07-09 10:44:22 +02:00
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 ─────────────────────────
2026-07-09 10:57:19 +02:00
function getLocalModels ( ) {
const modelsDir = path . join ( _ _dirname , '..' , 'models' ) ;
2026-07-09 10:44:22 +02:00
if ( ! fs . existsSync ( modelsDir ) ) return [ ] ;
2026-07-09 10:57:19 +02:00
return fs . readdirSync ( modelsDir )
. filter ( f => f . endsWith ( '.gguf' ) )
. sort ( )
. map ( ( name , i ) => {
const fullPath = path . join ( modelsDir , name ) ;
try {
const size = ( fs . statSync ( fullPath ) . size / 1024 / 1024 / 1024 ) . toFixed ( 1 ) ;
return { index : i + 1 , name , path : fullPath , size } ;
} catch { return { index : i + 1 , name , path : fullPath , size : '?' } ; }
} ) ;
}
function showLocalModels ( models ) {
if ( models . length === 0 ) return ;
2026-07-09 10:44:22 +02:00
console . log ( ` \n ${ C . bold } 📦 Verfügbare Modelle: ${ C . reset } ` ) ;
2026-07-09 10:57:19 +02:00
models . forEach ( m => {
console . log ( ` ${ C . cyan } ${ m . index } ) ${ C . reset } ${ m . name } ${ C . grey } ( ${ m . size } GB) ${ C . reset } ` ) ;
} ) ;
}
// ─── Server starten ───────────────────────────────────────────────────────────
function findLlamaServer ( ) {
// Pfade für llama-server Binary suchen
const candidates = [
path . join ( _ _dirname , '..' , 'bin' , 'linux' , 'llama-server' ) ,
path . join ( _ _dirname , '..' , 'bin' , 'win' , 'llama-server.exe' ) ,
] ;
for ( const p of candidates ) {
if ( fs . existsSync ( p ) ) return p ;
}
return null ;
}
async function startServerWithModel ( localModels ) {
showLocalModels ( localModels ) ;
if ( localModels . length === 0 ) {
console . log ( ` \n ${ C . red } ❌ Keine .gguf-Modelle im MiniLlama/models/-Ordner gefunden. ${ C . reset } ` ) ;
process . exit ( 1 ) ;
}
// User wählt Modell per Ziffer
let modelPath = null ;
while ( ! modelPath ) {
const input = await askSingleLine ( ` \n ${ C . yellow } 🔌 Server starten mit Modell Nr. ${ C . reset } [1- ${ localModels . length } ] ${ C . dim } (Enter=3=kleinstes) ${ C . reset } : ` ) ;
const choice = input ? parseInt ( input ) : 3 ;
if ( ! isNaN ( choice ) && choice >= 1 && choice <= localModels . length ) {
modelPath = localModels [ choice - 1 ] ;
} else {
console . log ( ` ${ C . red } Bitte eine Zahl zwischen 1 und ${ localModels . length } ${ C . reset } ` ) ;
}
}
const serverBin = findLlamaServer ( ) ;
if ( ! serverBin ) {
console . log ( ` \n ${ C . red } ❌ Kein llama-server Binary gefunden. ${ C . reset } ` ) ;
console . log ( ` ${ C . dim } Erwartet in bin/linux/llama-server oder bin/win/llama-server.exe ${ C . reset } ` ) ;
process . exit ( 1 ) ;
}
console . log ( ` \n ${ C . dim } 🚀 Starte Server mit ${ modelPath . name } ... ${ C . reset } \n ` ) ;
const { spawn } = require ( 'child_process' ) ;
const MINILLAMA _DIR = path . join ( _ _dirname , '..' ) ;
return new Promise ( ( resolve , reject ) => {
const server = spawn ( serverBin , [
'-m' , modelPath . path ,
'--host' , '127.0.0.1' ,
'--port' , '8080' ,
'--ctx-size' , '4096' ,
'--n-gpu-layers' , '0' ,
] , {
cwd : MINILLAMA _DIR ,
stdio : [ 'ignore' , 'pipe' , 'pipe' ] ,
} ) ;
let started = false ;
server . stdout . on ( 'data' , ( data ) => {
const text = data . toString ( ) ;
process . stdout . write ( text ) ; // Live-Logs zeigen
if ( ! started && ( text . includes ( 'model loaded' ) || text . includes ( 'listening on' ) || text . includes ( 'start' ) ) ) {
started = true ;
// Kurz warten bis Server wirklich bereit
setTimeout ( ( ) => resolve ( server ) , 500 ) ;
}
} ) ;
server . stderr . on ( 'data' , ( data ) => {
const text = data . toString ( ) ;
process . stdout . write ( text ) ; // Live-Logs zeigen
if ( ! started && ( text . includes ( 'model loaded' ) || text . includes ( 'listening on' ) || text . includes ( 'start' ) ) && ! text . includes ( 'error' ) ) {
started = true ;
setTimeout ( ( ) => resolve ( server ) , 500 ) ;
}
} ) ;
server . on ( 'error' , ( err ) => {
reject ( new Error ( ` Server-Start fehlgeschlagen: ${ err . message } ` ) ) ;
} ) ;
server . on ( 'exit' , ( code ) => {
if ( ! started ) reject ( new Error ( ` Server beendet mit Code ${ code } ` ) ) ;
} ) ;
// Timeout: Wenn Server nicht innerhalb von 120s startet (große Modelle vom Stick)
setTimeout ( ( ) => {
if ( ! started ) reject ( new Error ( 'Timeout: Server nicht innerhalb von 120s gestartet. Möglicherweise zu großes Modell oder USB-Stick zu langsam.' ) ) ;
} , 120000 ) ;
2026-07-09 10:44:22 +02:00
} ) ;
}
// ─── 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 ` ) ;
2026-07-09 10:57:19 +02:00
// Verfügbare Modelle anzeigen
const localModels = getLocalModels ( ) ;
2026-07-09 10:44:22 +02:00
2026-07-09 10:57:19 +02:00
// Lokale Modelle IMMER anzeigen (auch wenn Server schon läuft)
if ( localModels . length > 0 ) {
console . log ( ` ${ C . bold } 📦 Verfügbare lokale Modelle: ${ C . reset } ` ) ;
localModels . forEach ( m => {
console . log ( ` ${ C . cyan } ${ m . index } ) ${ C . reset } ${ m . name } ${ C . grey } ( ${ m . size } GB) ${ C . reset } ` ) ;
} ) ;
}
// API-Verbindung testen → falls Server schon läuft, direkt loslegen
let serverProcess = null ;
2026-07-09 10:44:22 +02:00
let connected = false ;
2026-07-09 10:57:19 +02:00
console . log ( ` \n ${ C . dim } 🔌 Teste Verbindung zu ${ API } ... ${ C . reset } ` ) ;
2026-07-09 10:44:22 +02:00
try {
const models = await fetchModels ( ) ;
if ( models . length > 0 ) {
const modelName = MODEL || models [ 0 ] ? . id || models [ 0 ] ? . name || models [ 0 ] ? . model || '(unbekannt)' ;
2026-07-09 10:57:19 +02:00
console . log ( ` ${ C . green } ✅ ${ C . reset } Server läuft bereits. Modell: ${ C . bold } ${ modelName } ${ C . reset } ` ) ;
2026-07-09 10:44:22 +02:00
connected = true ;
} else {
2026-07-09 10:57:19 +02:00
console . log ( ` ${ C . yellow } ⚠️ ${ C . reset } Server antwortet, aber kein Modell geladen. ` ) ;
console . log ( ` ${ C . dim } → Starte Server neu mit einem lokalen Modell. ${ C . reset } ` ) ;
2026-07-09 10:44:22 +02:00
}
2026-07-09 10:57:19 +02:00
} catch {
console . log ( ` ${ C . red } ❌ ${ C . reset } Kein Server an ${ API } ` ) ;
console . log ( ` ${ C . dim } → Starte Server automatisch mit lokalem Modell. ${ C . reset } ` ) ;
}
// Server starten, falls nicht verbunden
if ( ! connected ) {
if ( localModels . length === 0 ) {
console . log ( ` \n ${ C . red } ❌ Keine .gguf-Modelle gefunden. ${ C . reset } ` ) ;
console . log ( ` ${ C . dim } Lege Modelle in models/ ab oder starte llama-server manuell. ${ C . reset } ` ) ;
process . exit ( 1 ) ;
2026-07-09 10:44:22 +02:00
}
2026-07-09 10:57:19 +02:00
try {
serverProcess = await startServerWithModel ( localModels ) ;
connected = true ;
console . log ( ` \n ${ C . green } ✅ Server bereit. ${ C . reset } ` ) ;
} catch ( err ) {
console . log ( ` \n ${ C . red } ❌ Server-Start fehlgeschlagen: ${ err . message } ${ C . reset } ` ) ;
2026-07-09 10:44:22 +02:00
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 ) ;
} ) ;