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]
2026-07-10 09:42:29 +02:00
// ─── Konfiguration (via ENV überschreibbar) ──────────────────────────────────
const API = process . argv [ 2 ] || process . env . LLAMA _API || 'http://127.0.0.1:8080/v1' ;
const MODEL = process . argv [ 3 ] || process . env . LLAMA _MODEL || '' ;
const CONFIG = {
timeout : parseInt ( process . env . LLAMA _TIMEOUT || '120000' ) , // Streaming-Timeout (ms)
ctxSize : parseInt ( process . env . LLAMA _CTX _SIZE || '4096' ) , // Kontext-Fenster
gpuLayers : parseInt ( process . env . LLAMA _GPU _LAYERS || '0' ) , // GPU-Layer für --n-gpu-layers
} ;
2026-07-09 10:44:22 +02:00
// ─── 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 ( ) ;
2026-07-10 09:42:29 +02:00
const SYSTEM _PROMPT = ` You are CrowdCode, a concise coding assistant with FULL shell access.
FILE OPERATIONS ( no confirmation needed ) :
- DIR : path / to / dir — create a directory
- FILE : path / to / file on its own line , then fenced code block with COMPLETE file content — create or overwrite a file
- EDIT : path / to / file , then -- - OLD , old text , -- - NEW , new text , -- - — edit a file
SHELL COMMANDS ( user confirms each ) :
- Use \ ` \` \` bash for: cp, mv, rm, grep, find, git, npm, and other shell operations that aren't file/dir creation
- When the user asks about files / dirs / system , use a \ ` \` \` bash block
Examples :
- user : "erstelle test.txt mit Hallo" → DIR : tests \ n FILE : tests / test . txt \ n \ ` \` \` \n Hallo \n \` \` \`
- user : "liste dateien" → \ ` \` \` bash \n ls -la \n \` \` \`
- user : "kopiere test.txt als test2.txt" → \ ` \` \` bash \n cp test.txt test2.txt \n \` \` \`
Never explain — just output the commands .
NEVER use echo , touch , mkdir , cat , printf to create files — use FILE : and DIR : instead . ` ;
2026-07-09 10:44:22 +02:00
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) ───────────────────────────────────────────────
2026-07-10 09:42:29 +02:00
async function chatComplete ( messages , onToken , maxTokens ) {
2026-07-09 10:44:22 +02:00
const model = MODEL || ( await fetchModels ( ) ) [ 0 ] ? . id || 'unknown' ;
2026-07-10 09:42:29 +02:00
const controller = new AbortController ( ) ;
const timeoutId = setTimeout ( ( ) => controller . abort ( ) , CONFIG . timeout ) ;
try {
const body = JSON . stringify ( { model , messages , stream : true , temperature : 0.2 , top _p : 0.9 , ... ( maxTokens ? { max _tokens : maxTokens } : { } ) } ) ;
const res = await fetch ( ` ${ API } /chat/completions ` , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' , 'Authorization' : 'Bearer local' } ,
body ,
signal : controller . signal ,
} ) ;
clearTimeout ( timeoutId ) ;
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 */ }
}
2026-07-09 10:44:22 +02:00
}
2026-07-10 09:42:29 +02:00
return fullText ;
} finally {
clearTimeout ( timeoutId ) ;
2026-07-09 10:44:22 +02:00
}
}
// ─── @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 = [ ] ;
2026-07-10 09:42:29 +02:00
// === FILE:/path: Pattern (8B + 24B kompatibel) ===
// 8B: FILE: pfad\n```\n...```
// 24B: path: name.md\n```markdown\n...```
// 24B: Path: name.md\n```\n...```
const fileWithPrefixRegex = /(?:^|\n)[ \t]*(?:FILE|Path|path|Datei|datei|file|File)[:\s]\s*`?\s*(.+?)\s*`?\n```(?:\w+)?\n([\s\S]+?)```/g ;
2026-07-09 10:44:22 +02:00
let m ;
2026-07-10 09:42:29 +02:00
const matchedRanges = [ ] ;
while ( ( m = fileWithPrefixRegex . exec ( response ) ) !== null ) {
let filePath = m [ 1 ] . trim ( ) . replace ( /^`+|`+$/g , '' ) ;
if ( filePath && filePath . length < 300 ) {
actions . push ( { type : 'file' , path : filePath , content : m [ 2 ] . trim ( ) } ) ;
matchedRanges . push ( { start : m . index , end : m . index + m [ 0 ] . length } ) ;
}
2026-07-09 10:44:22 +02:00
}
2026-07-10 09:42:29 +02:00
// === DIR: pattern ===
2026-07-09 10:44:22 +02:00
const dirRegex = /^DIR:\s*(.+)$/gm ;
while ( ( m = dirRegex . exec ( response ) ) !== null ) {
actions . push ( { type : 'dir' , path : m [ 1 ] . trim ( ) } ) ;
}
2026-07-10 09:42:29 +02:00
// === EDIT: pattern ===
2026-07-09 10:44:22 +02:00
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 ] } ) ;
}
2026-07-10 09:42:29 +02:00
// === Bash pattern ===
2026-07-09 10:44:22 +02:00
const bashRegex = /```bash\n([\s\S]+?)```/g ;
while ( ( m = bashRegex . exec ( response ) ) !== null ) {
actions . push ( { type : 'bash' , command : m [ 1 ] . trim ( ) } ) ;
}
2026-07-10 09:42:29 +02:00
// === Namenlose Markdown-Codeblöcke (24B-Fallback) ===
// Erfasst Codeblöcke ohne FILE:-Prefix, die wie Wiki-Seiten aussehen
const orphanCodeRegex = /```(?:\w+)?\n([\s\S]+?)```/g ;
while ( ( m = orphanCodeRegex . exec ( response ) ) !== null ) {
// Bereits abgedeckt?
const alreadyMatched = matchedRanges . some ( r => m . index >= r . start && m . index < r . end ) ;
if ( alreadyMatched ) continue ;
const content = m [ 1 ] . trim ( ) ;
// Nur Markdown-ähnliche Inhalte mit Substanz erkennen
if ( content . length > 50 &&
( content . startsWith ( '#' ) || content . includes ( '\n##' ) ||
content . includes ( '|' ) || content . includes ( 'Siehe auch' ) ||
content . includes ( 'Gliederung' ) || content . includes ( 'Inhalt' ) ||
content . includes ( 'Übersicht' ) || content . includes ( 'Grundlagen' ) ) ) {
actions . push ( { type : 'file-orphan' , content } ) ;
}
}
2026-07-09 10:44:22 +02:00
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 ;
}
2026-07-10 09:42:29 +02:00
case 'file-orphan' : {
// Namenloser Codeblock — User nach Pfad fragen
const firstLine = a . content . split ( '\n' ) [ 0 ] . trim ( ) . replace ( /^#+\s*/ , '' ) ;
console . log ( ` ${ C . cyan } 📄 ${ C . reset } Codeblock erkannt: ${ C . grey } " ${ firstLine } " ${ C . reset } ` ) ;
const pfad = await askSingleLine ( ` ${ C . yellow } Speichern als? ${ C . reset } (wiki/Dateiname.md / Enter=überspringen) ` ) ;
if ( pfad && pfad . trim ( ) ) {
const fullPath = path . resolve ( WORKSPACE , pfad . trim ( ) ) ;
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 ) } ` ) ;
} else {
console . log ( ` ${ C . dim } (übersprungen) ${ C . reset } ` ) ;
}
break ;
}
2026-07-09 10:44:22 +02:00
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' ,
2026-07-10 09:42:29 +02:00
'--ctx-size' , String ( CONFIG . ctxSize ) ,
'--n-gpu-layers' , String ( CONFIG . gpuLayers ) ,
2026-07-09 10:57:19 +02:00
] , {
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
} ) ;
}
2026-07-10 09:42:29 +02:00
// ─── Wiki-Ingest (Chunked: eine Seite pro Request) ──────────────────────────
async function wikiIngest ( chatCompleteFn ) {
const wikiDir = path . join ( WORKSPACE , 'wiki' ) ;
const rawDir = path . join ( WORKSPACE , 'raw' ) ;
const agentsFile = path . join ( WORKSPACE , 'AGENTS.md' ) ;
// Prüfe ob Wiki-Umgebung
if ( ! fs . existsSync ( rawDir ) || ! fs . existsSync ( agentsFile ) ) {
console . log ( ` ${ C . red } ❌ ${ C . reset } Keine Wiki-Umgebung gefunden. ` ) ;
console . log ( ` ${ C . dim } Erwartet: raw/ (Quellen) und AGENTS.md (Konfiguration) im Workspace. ${ C . reset } ` ) ;
return ;
}
const rawFiles = fs . readdirSync ( rawDir ) . filter ( f => f . endsWith ( '.md' ) ) ;
if ( rawFiles . length === 0 ) {
console . log ( ` ${ C . red } ❌ ${ C . reset } Keine .md-Quellen in raw/ gefunden. ` ) ;
return ;
}
// AGENTS.md lesen
const agents = fs . readFileSync ( agentsFile , 'utf8' ) ;
const assistantsFile = path . join ( WORKSPACE , 'wiki-assistant.md' ) ;
const assistants = fs . existsSync ( assistantsFile ) ? fs . readFileSync ( assistantsFile , 'utf8' ) : '' ;
// Rohdaten lesen
const rawContents = { } ;
const rawCompact = { } ;
for ( const f of rawFiles ) {
const content = fs . readFileSync ( path . join ( rawDir , f ) , 'utf8' ) ;
rawContents [ f ] = content ;
// Kompakte Version: erste 800 Zeichen reichen als Kontext
rawCompact [ f ] = ( content . length > 800 ? content . substring ( 0 , 800 ) + '\n...' : content ) ;
}
// Kompakte Gesamt-Rohdaten (max ~1600 Zeichen für alle Quellen)
const compactSources = Object . entries ( rawCompact ) . map ( ( [ name , c ] ) => ` --- ${ name } --- \n ${ c } ` ) . join ( '\n\n' ) ;
const fullSources = Object . entries ( rawContents ) . map ( ( [ name , c ] ) => ` --- ${ name } --- \n ${ c } ` ) . join ( '\n\n' ) ;
// Themen aus AGENTS.md extrahieren
const topicMatch = agents . match ( /## Aktuelle Wiki-Themen\n\n([\s\S]*?)(?:\n##|\n$)/ ) ;
const topics = [ ] ;
if ( topicMatch ) {
const lines = topicMatch [ 1 ] . split ( '\n' ) . filter ( l => l . trim ( ) . startsWith ( '- **' ) ) ;
for ( const line of lines ) {
const m = line . match ( /- \*\*(.+?)\*\*\s*[\u2014\u2013-]\s*(.+)/ ) ;
if ( m ) topics . push ( { title : m [ 1 ] . trim ( ) , description : m [ 2 ] . trim ( ) } ) ;
}
}
if ( topics . length === 0 ) {
console . log ( ` ${ C . yellow } ⚠️ ${ C . reset } Keine Themen in AGENTS.md gefunden. ` ) ;
console . log ( ` ${ C . dim } Bitte pflege "## Aktuelle Wiki-Themen" in AGENTS.md. ${ C . reset } ` ) ;
return ;
}
// Inhalt.md als erstes — Gesamtüberblick
const pages = [
{ file : 'Inhalt.md' , title : 'Inhalt (Startseite)' , prompt : ` Create wiki/Inhalt.md — the index page for this wiki about IFTMIN (EDIFACT Transportauftrag) for OBI. Include a brief overview paragraph and a table of contents linking to these pages: \n ${ topics . map ( t => ` - ${ t . title } — ${ t . description } ` ) . join ( '\n' ) } \n \n Write in German. Use markdown links like [Titel](Datei.md). ` }
] ;
// Content-Pages: Nur ERSTE Seite bekommt volle Rohdaten, restliche nur Kurzfassung
for ( let i = 0 ; i < topics . length ; i ++ ) {
const t = topics [ i ] ;
const sourceSection = i === 0
? ` Quellen: \n ${ fullSources } `
: ` Quellen (Auszug): \n ${ compactSources } ` ;
pages . push ( {
file : ` ${ t . title . replace ( /[^a-zA-Z0-9äöüÄÖÜß-]/g , '' ) . replace ( /\s+/g , '-' ) } .md ` ,
title : t . title ,
prompt : ` Create wiki/ ${ t . title . replace ( /[^a-zA-Z0-9äöüÄÖÜß-]/g , '' ) . replace ( /\s+/g , '-' ) } .md in German for the IFTMIN OBI wiki. \n \n Topic: ${ t . title } — ${ t . description } \n \n ${ sourceSection } \n \n Write a well-structured markdown page with sections, tables where appropriate, and 'Siehe auch' links at the end referencing the other wiki pages. `
} ) ;
}
fs . mkdirSync ( wikiDir , { recursive : true } ) ;
console . log ( ` \n ${ C . bold } 📚 Wiki-Ingest gestartet: ${ pages . length } Seiten ${ C . reset } \n ` ) ;
for ( let i = 0 ; i < pages . length ; i ++ ) {
const page = pages [ i ] ;
console . log ( ` ${ C . cyan } [ ${ i + 1 } / ${ pages . length } ] ${ C . reset } ${ page . title } ${ C . dim } ( ${ page . file } ) ${ C . reset } ` ) ;
const history = [
{ role : 'system' , content : ` You are a wiki builder. Create markdown wiki pages from source material. Output the page content directly as markdown (no code block wrapper needed). ` } ,
{ role : 'user' , content : page . prompt }
] ;
let result = '' ;
let hasError = false ;
try {
result = await chatCompleteFn ( history , ( ) => { } , 1500 ) ;
} catch ( err ) {
console . log ( ` ${ C . red } ✗ ${ C . reset } Fehler: ${ err . message } ` ) ;
hasError = true ;
}
if ( ! hasError ) {
result = result . trim ( ) ;
// Codeblock extrahieren falls vorhanden (8B mit FILE:-Pattern, 24B-Roh-Output)
const cb = result . match ( /```(?:\w+)?\n([\s\S]+?)```/ ) ;
const saveContent = cb ? cb [ 1 ] . trim ( ) : result ;
fs . writeFileSync ( path . join ( wikiDir , page . file ) , saveContent , 'utf8' ) ;
console . log ( ` ${ C . green } ✓ ${ C . reset } Gespeichert ( ${ saveContent . length } Zeichen ${ cb ? '' : ', Roh-Output' } ) ` ) ;
}
// Kleine Pause zwischen Requests (Server-Entlastung)
if ( i < pages . length - 1 ) await new Promise ( r => setTimeout ( r , 500 ) ) ;
}
console . log ( ` \n ${ C . green } ✅ ${ C . bold } Wiki-Ingest abgeschlossen ${ C . reset } ` ) ;
console . log ( ` ${ C . dim } ${ pages . length } Seiten in ${ path . relative ( WORKSPACE , wikiDir ) } ${ C . reset } ` ) ;
}
2026-07-09 10:44:22 +02:00
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
2026-07-10 09:42:29 +02:00
: wiki Wiki - Ingest aus raw / + AGENTS . md ( Chunked - Modus )
2026-07-09 10:44:22 +02:00
$ { 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
2026-07-10 09:42:29 +02:00
( 24 B : auch path : , Datei : oder Codeblock ohne Prefix )
$ { C . dim } Konfiguration ( via ENV ) :
LLAMA _TIMEOUT = 300000 Streaming - Timeout in ms ( Default : 120000 )
LLAMA _CTX _SIZE = 8192 Kontext - Fenster ( Default : 4096 )
LLAMA _GPU _LAYERS = 8 GPU - Offload - Layer ( Default : 0 )
LLAMA _API = url API - Endpoint ( Default : http : //127.0.0.1:8080/v1)${C.reset}
2026-07-09 10:44:22 +02:00
` );
continue ;
}
if ( trimmed === ':clear' || trimmed === ':reset' ) {
history . length = 1 ; // nur System-Prompt behalten
console . log ( ` ${ C . dim } Chat-Verlauf gelöscht. ${ C . reset } ` ) ;
continue ;
}
2026-07-10 09:42:29 +02:00
if ( trimmed === ':wiki' ) {
await wikiIngest ( chatComplete ) ;
continue ;
}
2026-07-09 10:44:22 +02:00
if ( ! trimmed ) continue ;
2026-07-09 10:59:21 +02:00
// ─── Shell-Kommando-Erkennung (direkt ausführen, nicht ans LLM) ─────
const SHELL _COMMANDS = [
'pwd' , 'ls' , 'll' , 'cd' , 'git' , 'npm' , 'npx' , 'node' , 'python' , 'python3' ,
'pip' , 'curl' , 'wget' , 'docker' , 'docker-compose' , 'ps' , 'top' , 'htop' ,
'df' , 'du' , 'whoami' , 'env' , 'which' , 'whereis' , 'ssh' , 'scp' , 'make' ,
'cargo' , 'go' , 'rustc' , 'date' , 'uptime' , 'uname' , 'hostname' , 'id' , 'free' ,
'netstat' , 'ss' , 'ip' , 'ping' , 'traceroute' , 'nslookup' , 'dig' , 'tree' ,
'wc' , 'sort' , 'uniq' , 'cut' , 'awk' , 'sed' , 'tr' , 'diff' , 'cmp' , 'stat' ,
'file' , 'type' , 'printenv' , 'history' , 'screen' , 'tmux' , 'bash' , 'sh' ,
'zsh' , 'source' , '.' ,
] ;
2026-07-10 09:42:29 +02:00
// Shell-Prompt-Präfixe entfernen ($, >, #) und erstes Wort prüfen
const cmdTrimmed = trimmed . replace ( /^[$>#]\s*/ , '' ) ;
const firstWord = cmdTrimmed . split ( /\s+/ ) [ 0 ] ;
const isShellCmd = SHELL _COMMANDS . includes ( firstWord ) && ! /[?!.]$/ . test ( cmdTrimmed ) ;
2026-07-09 10:59:21 +02:00
if ( isShellCmd ) {
2026-07-10 09:42:29 +02:00
const lines = cmdTrimmed . split ( '\n' ) . filter ( l => l . trim ( ) ) ;
2026-07-09 10:59:21 +02:00
process . stdout . write ( ` \n ${ C . dim } ⚡ ${ trimmed } ${ C . reset } \n ` ) ;
for ( const cmd of lines ) {
try {
const out = execSync ( cmd . trim ( ) , { cwd : WORKSPACE , timeout : 10000 , encoding : 'utf8' , maxBuffer : 1024 * 512 } ) ;
console . log ( ` ${ C . grey } ${ out || '(keine Ausgabe)' } ${ C . reset } ` ) ;
} catch ( e ) {
console . log ( ` ${ C . red } ${ e . stderr || e . message } ${ C . reset } ` ) ;
}
}
console . log ( '' ) ;
continue ;
}
2026-07-09 10:44:22 +02:00
// @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 ) ;
} ) ;