@arthovlabs/liber 0.0.0-stage → 0.5.0

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.
Files changed (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +348 -2
  3. package/bin/liber +706 -0
  4. package/bin/liber-tui +244 -0
  5. package/bin/liber-tui.py +825 -0
  6. package/bin/liberd-entry +17 -0
  7. package/liber.yaml +84 -0
  8. package/package.json +43 -4
  9. package/resources/icons/appIcon.png +0 -0
  10. package/resources/icons/appIcon.svg +20 -0
  11. package/resources/icons/appIcon@512.png +0 -0
  12. package/resources/icons/atom-transparent-256.png +0 -0
  13. package/resources/icons/atom-transparent.png +0 -0
  14. package/resources/icons/atom-transparent.svg +19 -0
  15. package/resources/icons/make-atom.js +168 -0
  16. package/src/cli/ui.js +520 -0
  17. package/src/daemon/core/auth.js +217 -0
  18. package/src/daemon/core/automations.js +130 -0
  19. package/src/daemon/core/budget.js +145 -0
  20. package/src/daemon/core/circuit-breaker.js +31 -0
  21. package/src/daemon/core/config-apply.js +223 -0
  22. package/src/daemon/core/connectors.js +131 -0
  23. package/src/daemon/core/engine.js +1029 -0
  24. package/src/daemon/core/evidence.js +306 -0
  25. package/src/daemon/core/executor.js +10 -0
  26. package/src/daemon/core/grounding.js +420 -0
  27. package/src/daemon/core/injection.js +24 -0
  28. package/src/daemon/core/journal.js +81 -0
  29. package/src/daemon/core/liber-soul.js +157 -0
  30. package/src/daemon/core/lock.js +32 -0
  31. package/src/daemon/core/memory.js +106 -0
  32. package/src/daemon/core/model-switch.js +58 -0
  33. package/src/daemon/core/policy.js +102 -0
  34. package/src/daemon/core/providers.js +503 -0
  35. package/src/daemon/core/registry.js +356 -0
  36. package/src/daemon/core/secrets.js +35 -0
  37. package/src/daemon/core/skills.js +264 -0
  38. package/src/daemon/core/tools/browser-tool.js +280 -0
  39. package/src/daemon/core/tools/file-tools.js +277 -0
  40. package/src/daemon/core/tools/memory-tool.js +127 -0
  41. package/src/daemon/core/tools/skill-tool.js +44 -0
  42. package/src/daemon/core/tools/task-tool.js +41 -0
  43. package/src/daemon/core/tools/terminal-tool.js +449 -0
  44. package/src/daemon/core/tools/todo-tool.js +100 -0
  45. package/src/daemon/core/tools/web-tool.js +181 -0
  46. package/src/daemon/core/trace.js +40 -0
  47. package/src/daemon/core/validate.js +105 -0
  48. package/src/daemon/core/vault.js +158 -0
  49. package/src/daemon/core/verifier.js +98 -0
  50. package/src/daemon/core/voice.js +83 -0
  51. package/src/daemon/core/workspace.js +199 -0
  52. package/src/daemon/liberd.js +1343 -0
  53. package/src/daemon/llm/adapters/anthropic-adapter.js +190 -0
  54. package/src/daemon/llm/adapters/gemini-adapter.js +148 -0
  55. package/src/daemon/llm/adapters/ollama-adapter.js +74 -0
  56. package/src/daemon/llm/adapters/openai-adapter.js +148 -0
  57. package/src/daemon/llm/base.js +31 -0
  58. package/src/daemon/llm/manager.js +134 -0
  59. package/src/utils/config-loader.js +109 -0
  60. package/src/utils/logger.js +56 -0
package/src/cli/ui.js ADDED
@@ -0,0 +1,520 @@
1
+ // src/cli/ui.js — Liber Agy-Style Terminal Interface
2
+ const readline = require('readline');
3
+ const path = require('path');
4
+ const fs = require('fs');
5
+
6
+ // Single source of truth for branding: never hardcode a version in the UI.
7
+ const pkg = require('../../package.json');
8
+ const VERSION = pkg.version || '0.0.0';
9
+
10
+ // Terminal capability detection: default to rich colors unless explicitly disabled with NO_COLOR
11
+ const noColor = process.env.NO_COLOR !== undefined;
12
+
13
+ // Color and styling engine with TrueColor + 256-color VT100 fallbacks
14
+ const C = {
15
+ // Brand colors
16
+ cyan: (s) => (noColor ? s : `\x1b[38;2;6;182;212m${s}\x1b[0m`),
17
+ sky: (s) => (noColor ? s : `\x1b[38;2;56;189;248m${s}\x1b[0m`),
18
+ blue: (s) => (noColor ? s : `\x1b[38;2;59;130;246m${s}\x1b[0m`),
19
+ purple: (s) => (noColor ? s : `\x1b[38;2;168;85;247m${s}\x1b[0m`),
20
+ green: (s) => (noColor ? s : `\x1b[38;2;16;185;129m${s}\x1b[0m`),
21
+ amber: (s) => (noColor ? s : `\x1b[38;2;245;158;11m${s}\x1b[0m`),
22
+ gold: (s) => (noColor ? s : `\x1b[38;2;229;178;92m${s}\x1b[0m`),
23
+ red: (s) => (noColor ? s : `\x1b[38;2;239;68;68m${s}\x1b[0m`),
24
+ dim: (s) => (noColor ? s : `\x1b[38;2;148;163;184m${s}\x1b[0m`),
25
+ darkDim: (s) => (noColor ? s : `\x1b[38;2;71;85;105m${s}\x1b[0m`),
26
+ bold: (s) => (noColor ? s : `\x1b[1m${s}\x1b[22m`),
27
+ italic: (s) => (noColor ? s : `\x1b[3m${s}\x1b[23m`),
28
+ underline: (s) => (noColor ? s : `\x1b[4m${s}\x1b[24m`),
29
+
30
+ // Solid background badges / pills
31
+ badgeCyan: (s) => (noColor ? `[${s}]` : `\x1b[48;2;6;182;212m\x1b[38;2;15;23;42m\x1b[1m ${s} \x1b[0m`),
32
+ badgeBlue: (s) => (noColor ? `[${s}]` : `\x1b[48;2;30;64;175m\x1b[38;2;241;245;249m\x1b[1m ${s} \x1b[0m`),
33
+ badgeGreen: (s) => (noColor ? `[${s}]` : `\x1b[48;2;16;185;129m\x1b[38;2;15;23;42m\x1b[1m ${s} \x1b[0m`),
34
+ badgeAmber: (s) => (noColor ? `[${s}]` : `\x1b[48;2;217;119;6m\x1b[38;2;15;23;42m\x1b[1m ${s} \x1b[0m`),
35
+ badgePurple: (s) => (noColor ? `[${s}]` : `\x1b[48;2;147;51;234m\x1b[38;2;241;245;249m\x1b[1m ${s} \x1b[0m`),
36
+ badgeRed: (s) => (noColor ? `[${s}]` : `\x1b[48;2;220;38;38m\x1b[38;2;255;255;255m\x1b[1m ${s} \x1b[0m`),
37
+ badgeDark: (s) => (noColor ? `[${s}]` : `\x1b[48;2;30;41;59m\x1b[38;2;148;163;184m\x1b[1m ${s} \x1b[0m`),
38
+ };
39
+
40
+ // Strip ANSI escape sequences to compute true visual string width
41
+ function stripAnsi(str) {
42
+ return String(str).replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
43
+ }
44
+
45
+ // Window width helper
46
+ function getCols() {
47
+ return Math.min(process.stdout.columns || 80, 100);
48
+ }
49
+
50
+ // Format a single box row with pixel-perfect right border alignment
51
+ function formatBoxRow(content, innerWidth) {
52
+ const visibleLen = stripAnsi(content).length;
53
+ const pad = Math.max(0, innerWidth - visibleLen);
54
+ return ' ' + C.darkDim('│') + ' ' + content + ' '.repeat(pad) + ' ' + C.darkDim('│');
55
+ }
56
+
57
+ // Modern LiBER Terminal Header Banner
58
+ function printAgyBanner(info = {}) {
59
+ const width = Math.max(getCols(), 76);
60
+ const innerWidth = width - 2;
61
+ const agent = info.agent || 'dev-bot';
62
+ const model = info.model || 'glm-5.3-flash:cloud';
63
+ const port = info.port || 4567;
64
+ const status = info.online ? C.green('● Online') : C.amber('○ Offline');
65
+ const cwd = process.cwd();
66
+ const displayCwd = cwd.length > (innerWidth - 8) ? '...' + cwd.slice(-(innerWidth - 11)) : cwd;
67
+
68
+ const titleBar = C.cyan(C.bold(` LIBER v${VERSION} `));
69
+ const titleLen = stripAnsi(titleBar).length;
70
+ const topBar = C.darkDim(' ╭─') + titleBar + C.darkDim('─'.repeat(Math.max(0, innerWidth - titleLen)) + '─╮');
71
+ const bottomBar = C.darkDim(' ╰─') + C.darkDim('─'.repeat(innerWidth) + '─╯');
72
+
73
+ const label = C.gold('◢') + C.bold(' L i B E R ') + C.gold('◣') + C.dim(' Autonomous AI Agent Orchestrator');
74
+ const ver = C.purple(`v${VERSION}`);
75
+ const padLen = Math.max(0, innerWidth - stripAnsi(label).length - stripAnsi(ver).length);
76
+ const row1 = label + ' '.repeat(padLen) + ver;
77
+ const host = info.host || '127.0.0.1';
78
+ const row2 = C.dim('Agent: ') + C.bold(C.sky(agent)) + ' ' + C.dim('Model: ') + C.amber(model) + ' ' + C.dim('Daemon: ') + C.dim(`${host}:${port}`) + ' [' + status + ']';
79
+ const row3 = C.dim('Dir: ') + C.dim(displayCwd);
80
+
81
+ console.log();
82
+ console.log(topBar);
83
+ console.log(formatBoxRow(row1, innerWidth));
84
+ console.log(formatBoxRow(row2, innerWidth));
85
+ console.log(formatBoxRow(row3, innerWidth));
86
+ console.log(bottomBar);
87
+ console.log(
88
+ ' ' + C.dim('Type prompt to begin, or ') +
89
+ C.cyan('/help') + C.dim(' for commands (') +
90
+ C.dim('/status, /tools, /model, /logs, /tui, /clear, /exit') + C.dim(')')
91
+ );
92
+ console.log();
93
+ }
94
+
95
+ // Spinner for agent thought process
96
+ function startSpinner(label = 'Thinking...') {
97
+ const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
98
+ let i = 0;
99
+ const startTime = Date.now();
100
+ let timer = null;
101
+
102
+ if (process.stdout.isTTY) {
103
+ process.stdout.write(` ${C.cyan(frames[0])} ${C.dim(label)}`);
104
+ timer = setInterval(() => {
105
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
106
+ process.stdout.write(`\r\x1b[2K ${C.cyan(frames[i++ % frames.length])} ${C.dim(label)} ${C.darkDim(`(${elapsed}s)`)}`);
107
+ }, 80);
108
+ } else {
109
+ console.log(` ${C.cyan('◇')} ${C.dim(label)}`);
110
+ }
111
+
112
+ return {
113
+ stop() {
114
+ if (timer) {
115
+ clearInterval(timer);
116
+ process.stdout.write('\r\x1b[2K');
117
+ }
118
+ }
119
+ };
120
+ }
121
+
122
+ // Markdown formatter with syntax boxes and highlights
123
+ function formatMarkdown(text) {
124
+ const lines = String(text).split('\n');
125
+ const result = [];
126
+ let inCode = false;
127
+ let codeLang = '';
128
+ const width = Math.max(getCols(), 70);
129
+
130
+ for (let line of lines) {
131
+ const trimmed = line.trim();
132
+
133
+ // Code fence toggle
134
+ if (trimmed.startsWith('```')) {
135
+ if (!inCode) {
136
+ inCode = true;
137
+ codeLang = trimmed.slice(3).trim() || 'code';
138
+ result.push(C.darkDim(' ╭─ ') + C.blue(C.bold(codeLang)) + ' ' + C.darkDim('─'.repeat(Math.max(0, width - codeLang.length - 10))));
139
+ } else {
140
+ inCode = false;
141
+ result.push(C.darkDim(' ╰─' + '─'.repeat(Math.max(0, width - 8))));
142
+ }
143
+ continue;
144
+ }
145
+
146
+ if (inCode) {
147
+ result.push(C.darkDim(' │ ') + C.amber(line));
148
+ continue;
149
+ }
150
+
151
+ // Markdown Headers
152
+ if (/^###\s+/.test(trimmed)) {
153
+ result.push('\n ' + C.bold(C.sky('■ ' + trimmed.replace(/^###\s+/, ''))));
154
+ continue;
155
+ }
156
+ if (/^##\s+/.test(trimmed)) {
157
+ result.push('\n ' + C.bold(C.cyan('▲ ' + trimmed.replace(/^##\s+/, ''))));
158
+ continue;
159
+ }
160
+ if (/^#\s+/.test(trimmed)) {
161
+ result.push('\n ' + C.bold(C.cyan('◆ ' + trimmed.replace(/^#\s+/, ''))));
162
+ continue;
163
+ }
164
+
165
+ // Bullet points
166
+ if (/^[\*\-]\s+/.test(trimmed)) {
167
+ line = ' ' + C.cyan('• ') + line.replace(/^[\*\-]\s+/, '');
168
+ } else {
169
+ line = ' ' + line;
170
+ }
171
+
172
+ // Bold, inline code, links
173
+ line = line
174
+ .replace(/\*\*(.*?)\*\*/g, (_, m) => C.bold(m))
175
+ .replace(/`([^`]+)`/g, (_, m) => C.amber(m))
176
+ .replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, text, url) => C.cyan(C.underline(text)) + C.dim(` (${url})`));
177
+
178
+ result.push(line);
179
+ }
180
+
181
+ return result.join('\n');
182
+ }
183
+
184
+ // Render cognitive pulse / reasoning block
185
+ function printThought(thought) {
186
+ const width = Math.max(getCols(), 70);
187
+ console.log(C.darkDim(' ╭─ ') + C.amber('🧠 REASONING ') + C.darkDim('─'.repeat(Math.max(0, width - 22))));
188
+ String(thought).split('\n').forEach(line => {
189
+ console.log(C.darkDim(' │ ') + C.dim(line));
190
+ });
191
+ console.log(C.darkDim(' ╰─' + '─'.repeat(Math.max(0, width - 8))));
192
+ console.log();
193
+ }
194
+
195
+ // Render tool execution card
196
+ function printToolExecution(tool) {
197
+ const width = Math.max(getCols(), 70);
198
+ const toolName = tool.name || (tool.command ? 'terminal' : 'tool');
199
+ const details = tool.command || (tool.args ? JSON.stringify(tool.args) : '');
200
+ console.log(C.darkDim(' ╭─ ') + C.purple(`⚡ Tool: ${toolName} `) + C.darkDim('─'.repeat(Math.max(0, width - toolName.length - 17))));
201
+ if (details) {
202
+ console.log(C.darkDim(' │ ') + C.dim('Args: ') + C.cyan(C.bold(details)));
203
+ }
204
+
205
+ if (tool.result) {
206
+ const out = tool.result.output || (tool.result.error ? C.red(tool.result.error) : JSON.stringify(tool.result));
207
+ const lines = String(out).trim().split('\n').slice(0, 8);
208
+ console.log(C.darkDim(' │ ') + C.dim('Output:'));
209
+ lines.forEach(l => console.log(C.darkDim(' │ ') + C.dim(l)));
210
+ }
211
+
212
+ const duration = tool.durationMs !== undefined ? ` in ${tool.durationMs}ms` : '';
213
+ console.log(C.darkDim(' │ ') + C.green(`✔ Success${duration}`));
214
+ console.log(C.darkDim(' ╰─' + '─'.repeat(Math.max(0, width - 8))));
215
+ console.log();
216
+ }
217
+
218
+ // Render agent answer
219
+ function printAgentResponse(agent, responseData) {
220
+ const isString = typeof responseData === 'string';
221
+ const text = isString ? responseData : responseData.response || '';
222
+ const trace = (!isString && responseData.trace) || [];
223
+ const durationMs = (!isString && responseData.durationMs) || null;
224
+ const iterations = (!isString && responseData.iterations) || null;
225
+
226
+ // Print execution traces (thoughts & tool calls)
227
+ for (const step of trace) {
228
+ if (step.type === 'thought' && step.content) {
229
+ printThought(step.content);
230
+ } else if (step.type === 'tool') {
231
+ printToolExecution(step);
232
+ }
233
+ }
234
+
235
+ // Header for agent response
236
+ console.log(' ' + C.badgeCyan(agent.toUpperCase()) + ' ' + C.dim(new Date().toTimeString().slice(0, 8)));
237
+ console.log();
238
+ console.log(formatMarkdown(text));
239
+ console.log();
240
+
241
+ // Metrics footer
242
+ const grounding = (!isString && responseData.grounding) || null;
243
+ const stats = [];
244
+ if (durationMs) stats.push(`took ${(durationMs / 1000).toFixed(1)}s`);
245
+ if (iterations) stats.push(`${iterations} iteration${iterations > 1 ? 's' : ''}`);
246
+ let statsColor = C.darkDim;
247
+ if (grounding) {
248
+ if (grounding.status === 'PASS') {
249
+ const receipts = (grounding.ledger && grounding.ledger.receipts) || [];
250
+ stats.push(`grounded · ${receipts.length} receipt${receipts.length === 1 ? '' : 's'}`);
251
+ statsColor = C.green;
252
+ } else if (grounding.status === 'REPAIR') {
253
+ stats.push(`⚠ ${grounding.violations.length} unverified claim${grounding.violations.length === 1 ? '' : 's'}`);
254
+ statsColor = C.amber;
255
+ }
256
+ }
257
+ if (stats.length) {
258
+ console.log(' ' + statsColor(`[${stats.join(' · ')}]`));
259
+ console.log();
260
+ }
261
+ }
262
+
263
+ // Print only the answer (used when the stream already rendered reasoning and tools)
264
+ function printAnswerOnly(agent, responseData) {
265
+ printAgentResponse(agent, { ...responseData, trace: [] });
266
+ }
267
+
268
+ function truncateText(value, max = 400) {
269
+ const s = String(value == null ? '' : value).replace(/\r/g, '').trim();
270
+ return s.length > max ? s.slice(0, max) + '…' : s;
271
+ }
272
+
273
+ // Live DRAFT preview (raw model output, not yet audited)
274
+ let draftBuffer = '';
275
+
276
+ function renderDraftPreview() {
277
+ if (!process.stdout.isTTY) return;
278
+ const clean = draftBuffer.replace(/\s+/g, ' ');
279
+ const limit = Math.max(40, getCols() - 10);
280
+ const tail = clean.length > limit ? '…' + clean.slice(-limit) : clean;
281
+ process.stdout.write('\r\x1b[2K ' + C.purple('✎ draft ') + C.dim(tail));
282
+ }
283
+
284
+ // Render a daemon SSE event live in the terminal
285
+ function printStreamEvent(event, data = {}) {
286
+ switch (event) {
287
+ case 'draft_start':
288
+ draftBuffer = '';
289
+ renderDraftPreview();
290
+ break;
291
+
292
+ case 'draft':
293
+ draftBuffer += data.delta || '';
294
+ renderDraftPreview();
295
+ break;
296
+
297
+ case 'draft_end':
298
+ if (process.stdout.isTTY) process.stdout.write('\r\x1b[2K');
299
+ if (data.failed) {
300
+ console.log(' ' + C.amber('✎ draft interrupted — falling back to a complete, audited completion'));
301
+ } else if (draftBuffer.length) {
302
+ console.log(' ' + C.darkDim(`✎ draft · ${draftBuffer.length} chars streamed · UNVERIFIED until the grounding audit passes`));
303
+ }
304
+ draftBuffer = '';
305
+ break;
306
+
307
+ case 'thought':
308
+ if (data.content) printThought(data.content);
309
+ break;
310
+
311
+ case 'tool_dispatch': {
312
+ const name = data.tool || 'tool';
313
+ const detail = data.command || JSON.stringify(data.args || {});
314
+ console.log(' ' + C.purple('⚡ ' + name) + ' ' + C.dim(truncateText(detail, 160)));
315
+ break;
316
+ }
317
+
318
+ case 'observation': {
319
+ const out = String(data.output || '').trim();
320
+ const all = out.split('\n');
321
+ const ok = data.ok !== false;
322
+ console.log(' ' + (ok ? C.green('✓ ') : C.red('✗ ')) + C.dim(`[${data.evidence || '?'}] ${data.tool || ''}${data.exit_code !== undefined ? ` · exit ${data.exit_code}` : ''}`));
323
+ all.slice(0, 6).forEach(line => console.log(' ' + C.darkDim('│ ') + C.dim(truncateText(line, 160))));
324
+ if (all.length > 6) console.log(' ' + C.darkDim('│ ') + C.dim(`… ${all.length - 6} more line(s)`));
325
+ break;
326
+ }
327
+
328
+ case 'grounding_audit': {
329
+ const codes = [...new Set((data.violations || []).map(v => v.code))].join(', ');
330
+ console.log(' ' + C.amber(`⚠ grounding: blocked ${(data.violations || []).length} unverified claim(s) — repair ${data.attempt}/${data.max_repairs}`) + C.dim(codes ? ` · ${codes}` : ''));
331
+ break;
332
+ }
333
+
334
+ case 'grounding_warning':
335
+ console.log(' ' + C.red(`⚠ unverified: ${(data.violations || []).length} claim(s) shipped with a GROUNDING AUDIT footer`));
336
+ break;
337
+
338
+ case 'loop_guard':
339
+ console.log(' ' + C.amber(`↻ duplicate ${data.tool} call suppressed (receipt ${data.receipt || 'n/a'})`));
340
+ break;
341
+
342
+ case 'subagent_start':
343
+ console.log(' ' + C.cyan(`⇢ subagent ${data.id}`) + C.dim(` ${truncateText(data.goal, 120)}`));
344
+ break;
345
+
346
+ case 'subagent_end':
347
+ console.log(' ' + (data.grounded === false ? C.amber('⇠ subagent') : C.green('⇠ subagent')) + C.dim(` ${data.id} · ${data.status || 'done'}`));
348
+ break;
349
+
350
+ case 'hitl_intercept':
351
+ console.log(' ' + C.amber(`⚠ HITL: auto-approving ${data.tool} (use the TUI for interactive approval)`));
352
+ break;
353
+
354
+ case 'error':
355
+ console.log(' ' + C.red(`✖ ${data.error || 'stream error'}`));
356
+ break;
357
+
358
+ default:
359
+ break;
360
+ }
361
+ }
362
+ function startAgySession({ agent, port, ask, commands, getAgentInfo }) {
363
+ let activeAgent = agent || 'dev-bot';
364
+ let activeModel = 'glm-5.3-flash:cloud';
365
+ let isOnline = true;
366
+
367
+ // Load config details if available
368
+ try {
369
+ const configPath = path.resolve(process.cwd(), process.env.LIBER_CONFIG || 'liber.yaml');
370
+ if (fs.existsSync(configPath)) {
371
+ const yaml = require('js-yaml');
372
+ const doc = yaml.load(fs.readFileSync(configPath, 'utf8'));
373
+ if (doc?.agent?.name) activeAgent = doc.agent.name;
374
+ if (doc?.agent?.engine?.model) activeModel = doc.agent.engine.model;
375
+ }
376
+ } catch { /* use defaults */ }
377
+
378
+ printAgyBanner({ agent: activeAgent, model: activeModel, port, online: isOnline });
379
+
380
+ const rl = readline.createInterface({
381
+ input: process.stdin,
382
+ output: process.stdout,
383
+ terminal: true,
384
+ });
385
+
386
+ let isBusy = false;
387
+ let messageCount = 0;
388
+
389
+ function renderPrompt() {
390
+ const promptText = `\n${C.darkDim('╭─')} ${C.sky(C.bold(activeAgent))} ${C.darkDim('·')} ${C.darkDim('~/Liber')}\n${C.darkDim('╰─')}${C.green(C.bold('❯'))} `;
391
+ rl.setPrompt(promptText);
392
+ rl.prompt(true);
393
+ }
394
+
395
+ // Help command display
396
+ function showHelp() {
397
+ const width = Math.max(getCols(), 70);
398
+ console.log();
399
+ console.log(C.darkDim(' ╭─ ') + C.cyan(C.bold('AVAILABLE COMMANDS')) + ' ' + C.darkDim('─'.repeat(Math.max(0, width - 26))));
400
+ const list = [
401
+ ['/help', 'Display this command reference'],
402
+ ['/status, /ps', 'Show real-time agent metrics and health'],
403
+ ['/tools', 'List registered tool capabilities'],
404
+ ['/models', 'List all available AI models from the API endpoint'],
405
+ ['/model <name>', 'Switch the active AI model dynamically on the fly'],
406
+ ['/logs [n]', 'View recent telemetry events from liber.log'],
407
+ ['/clear', 'Clear screen and refresh LiBER banner'],
408
+ ['/tui', 'Launch the Tactical Cyber-Obsidian TUI dashboard'],
409
+ ['/exit, /quit', 'Exit the LiBER Terminal'],
410
+ ];
411
+
412
+ list.forEach(([cmd, desc]) => {
413
+ console.log(' ' + C.darkDim('│ ') + C.sky(cmd.padEnd(16)) + C.dim(desc));
414
+ });
415
+ console.log(C.darkDim(' ╰─' + '─'.repeat(Math.max(0, width - 8))));
416
+ console.log();
417
+ }
418
+
419
+ rl.on('line', async (line) => {
420
+ if (isBusy) return;
421
+ const input = line.trim();
422
+ if (!input) {
423
+ renderPrompt();
424
+ return;
425
+ }
426
+
427
+ // Handle slash commands
428
+ if (input.startsWith('/')) {
429
+ const parts = input.slice(1).split(/\s+/);
430
+ const cmd = parts[0].toLowerCase();
431
+ const arg = parts.slice(1).join(' ');
432
+
433
+ if (cmd === 'exit' || cmd === 'quit') {
434
+ console.log(C.dim('\n Goodbye from Liber. 👋\n'));
435
+ process.exit(0);
436
+ }
437
+
438
+ if (cmd === 'clear') {
439
+ console.clear();
440
+ printAgyBanner({ agent: activeAgent, model: activeModel, port, online: isOnline });
441
+ renderPrompt();
442
+ return;
443
+ }
444
+
445
+ if (cmd === 'help') {
446
+ showHelp();
447
+ renderPrompt();
448
+ return;
449
+ }
450
+
451
+ if (cmd === 'tui') {
452
+ console.log(C.cyan('\n Launching Tactical Cyber-Obsidian TUI Dashboard...\n'));
453
+ const { spawn } = require('child_process');
454
+ const pythonTui = path.join(__dirname, '..', '..', 'bin', 'liber-tui.py');
455
+ if (fs.existsSync(pythonTui)) {
456
+ spawn('python', [pythonTui], { stdio: 'inherit' });
457
+ } else {
458
+ spawn(process.execPath, [path.join(__dirname, '..', '..', 'bin', 'liber-tui')], { stdio: 'inherit' });
459
+ }
460
+ return;
461
+ }
462
+
463
+ if (commands && commands[cmd]) {
464
+ try {
465
+ const res = await commands[cmd](arg);
466
+ if (res) console.log(res);
467
+ } catch (err) {
468
+ console.log(` ${C.red('✖ Error:')} ${err.message}`);
469
+ }
470
+ renderPrompt();
471
+ return;
472
+ }
473
+
474
+ console.log(` ${C.red('✖ Unknown command:')} ${C.dim(input)}. Type ${C.cyan('/help')} for list.`);
475
+ renderPrompt();
476
+ return;
477
+ }
478
+
479
+ // Regular conversation input
480
+ isBusy = true;
481
+ console.log();
482
+ const spinner = startSpinner('Cognitive Pulse thinking...');
483
+
484
+ try {
485
+ const data = await ask(input);
486
+ spinner.stop();
487
+ messageCount++;
488
+ printAgentResponse(activeAgent, data);
489
+ } catch (err) {
490
+ spinner.stop();
491
+ console.log(` ${C.red('✖ Error:')} ${err.message}\n`);
492
+ }
493
+
494
+ isBusy = false;
495
+ renderPrompt();
496
+ });
497
+
498
+ rl.on('SIGINT', () => {
499
+ console.log(C.dim('\n\n Exiting Liber. Bye! 👋\n'));
500
+ process.exit(0);
501
+ });
502
+
503
+ rl.on('close', () => {
504
+ console.log(C.dim('\n Session closed.\n'));
505
+ process.exit(0);
506
+ });
507
+
508
+ renderPrompt();
509
+ }
510
+
511
+ module.exports = {
512
+ startAgySession,
513
+ printAgyBanner,
514
+ formatMarkdown,
515
+ printAgentResponse,
516
+ printAnswerOnly,
517
+ printStreamEvent,
518
+ printThought,
519
+ C
520
+ };