@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
@@ -0,0 +1,134 @@
1
+ // src/daemon/llm/manager.js
2
+ const OllamaAdapter = require('./adapters/ollama-adapter');
3
+ const OpenAIAdapter = require('./adapters/openai-adapter');
4
+ const AnthropicAdapter = require('./adapters/anthropic-adapter');
5
+ const GeminiAdapter = require('./adapters/gemini-adapter');
6
+
7
+ class LLMManager {
8
+ constructor() {
9
+ this.adapters = {
10
+ ollama: OllamaAdapter,
11
+ openai: OpenAIAdapter,
12
+ anthropic: AnthropicAdapter,
13
+ gemini: GeminiAdapter
14
+ // Provider baru tinggal ditambahkan di sini
15
+ };
16
+ this.activeAdapter = null;
17
+ this.retry = { attempts: 3, baseMs: 700 };
18
+ }
19
+
20
+ /**
21
+ * Inisialisasi adapter berdasarkan konfigurasi di liber.yaml
22
+ * @param {Object} config - Bagian 'engine' dari liber.yaml
23
+ */
24
+ initialize(config) {
25
+ this.engineConfig = { ...config };
26
+ const ProviderClass = this.adapters[config.provider];
27
+ if (!ProviderClass) {
28
+ throw new Error(`Unsupported LLM provider: ${config.provider}`);
29
+ }
30
+
31
+ console.log(`[Liber-Daemon] Initializing LLM engine: ${config.provider} (${config.model})`);
32
+ this.retry = {
33
+ attempts: Math.max(1, Math.min(parseInt(config.retry_attempts, 10) || 3, 6)),
34
+ baseMs: Math.max(100, parseInt(config.retry_base_ms, 10) || 700)
35
+ };
36
+ this.activeAdapter = new ProviderClass(config);
37
+ return this.activeAdapter;
38
+ }
39
+
40
+ isRetryable(err) {
41
+ const msg = String(err?.message || '');
42
+ if (/HTTP (400|401|403|404|422)\b/.test(msg)) return false; // client errors: retrying cannot help
43
+ return /HTTP (429|5\d\d)|timeout|timed out|ECONNRESET|ETIMEDOUT|EAI_AGAIN|ENOTFOUND|fetch failed|network|socket hang up|aborted/i.test(msg);
44
+ }
45
+
46
+ /**
47
+ * Retry the same request on the next model of the SAME provider/endpoint.
48
+ * That is all a single-key setup can do: same base URL and key, different
49
+ * model id, so a dead or over-quota model does not kill the turn.
50
+ */
51
+ async _withModelFallback(fn, canRetry = () => true) {
52
+ const base = this.engineConfig || {};
53
+ const chain = [base.model, ...(Array.isArray(base.fallback_models) ? base.fallback_models : [])]
54
+ .map(m => String(m || '').trim())
55
+ .filter((m, i, all) => m && all.indexOf(m) === i);
56
+ let index = 0;
57
+ let lastErr;
58
+ for (;;) {
59
+ try {
60
+ return await fn();
61
+ } catch (err) {
62
+ lastErr = err;
63
+ const next = chain[index + 1];
64
+ if (!next || !canRetry()) break;
65
+ try {
66
+ this.initialize({ ...base, model: next });
67
+ } catch (initErr) {
68
+ lastErr = initErr;
69
+ break;
70
+ }
71
+ this.lastFallback = { from: chain[index], to: next, error: String(err && err.message || err).slice(0, 200), at: Date.now() };
72
+ if (typeof this.onFallback === 'function') { try { this.onFallback(this.lastFallback); } catch { /* never break a turn */ } }
73
+ index++;
74
+ }
75
+ }
76
+ throw lastErr;
77
+ }
78
+
79
+ async generate(messages, options = {}) {
80
+ if (!this.activeAdapter) throw new Error("LLM Engine not initialized. Call initialize() first.");
81
+ return this._withModelFallback(() => this._withRetry(() => this.activeAdapter.generate(messages, options)));
82
+ }
83
+
84
+ /**
85
+ * Native tool calling: returns { content, tool_calls, finish_reason }.
86
+ */
87
+ async generateChat(messages, options = {}) {
88
+ if (!this.activeAdapter) throw new Error("LLM Engine not initialized. Call initialize() first.");
89
+ if (typeof this.activeAdapter.chat !== 'function') {
90
+ throw new Error('Active adapter does not support native tool calling.');
91
+ }
92
+ return this._withModelFallback(() => this._withRetry(() => this.activeAdapter.chat(messages, options)));
93
+ }
94
+
95
+ async _withRetry(fn) {
96
+ let lastErr;
97
+ for (let attempt = 1; attempt <= this.retry.attempts; attempt++) {
98
+ try {
99
+ return await fn();
100
+ } catch (err) {
101
+ lastErr = err;
102
+ const canRetry = attempt < this.retry.attempts && this.isRetryable(err);
103
+ if (!canRetry) throw err;
104
+ const wait = Math.min(this.retry.baseMs * 2 ** (attempt - 1), 8000);
105
+ console.warn(`[Liber-Daemon] LLM error (${err.message}) — retry ${attempt}/${this.retry.attempts} in ${wait}ms`);
106
+ await new Promise(r => setTimeout(r, wait));
107
+ }
108
+ }
109
+ throw lastErr;
110
+ }
111
+
112
+ getLastUsage() {
113
+ return this.activeAdapter?.lastUsage || null;
114
+ }
115
+
116
+ /** Chain of thought the provider returned on the most recent call, if any. */
117
+ getLastReasoning() {
118
+ return this.activeAdapter?.lastReasoning || null;
119
+ }
120
+
121
+ async stream(messages, onChunk, options = {}) {
122
+ if (!this.activeAdapter) throw new Error("LLM Engine not initialized. Call initialize() first.");
123
+ if (typeof this.activeAdapter.stream !== 'function') {
124
+ throw new Error('Active adapter does not support streaming.');
125
+ }
126
+ // A stream that already emitted text cannot be replayed: falling back would
127
+ // duplicate the partial answer, so retry only while nothing was sent.
128
+ let emitted = false;
129
+ const guard = (t) => { emitted = true; onChunk(t); };
130
+ return this._withModelFallback(() => this.activeAdapter.stream(messages, guard, options), () => !emitted);
131
+ }
132
+ }
133
+
134
+ module.exports = new LLMManager();
@@ -0,0 +1,109 @@
1
+ // src/utils/config-loader.js
2
+ const fs = require('fs');
3
+ const yaml = require('js-yaml');
4
+ const path = require('path');
5
+
6
+ const KEY_ENV = {
7
+ openai: ['OPENAI_API_KEY', 'OLLAMA_CLOUD_API_KEY'],
8
+ anthropic: ['ANTHROPIC_API_KEY'],
9
+ gemini: ['GEMINI_API_KEY', 'GOOGLE_API_KEY'],
10
+ ollama: ['OLLAMA_CLOUD_API_KEY', 'OPENAI_API_KEY']
11
+ };
12
+
13
+ // OpenAI-compatible vendors use their own key names — resolve by endpoint host.
14
+ const ENDPOINT_KEY_ENV = [
15
+ [/groq\.com/i, ['GROQ_API_KEY']],
16
+ [/deepseek\.com/i, ['DEEPSEEK_API_KEY']],
17
+ [/mistral\.ai/i, ['MISTRAL_API_KEY']],
18
+ [/openrouter\.ai/i, ['OPENROUTER_API_KEY']],
19
+ [/api\.x\.ai/i, ['XAI_API_KEY']],
20
+ [/together\.xyz/i, ['TOGETHER_API_KEY']],
21
+ [/ollama\.com/i, ['OLLAMA_CLOUD_API_KEY']],
22
+ [/api\.openai\.com/i, ['OPENAI_API_KEY']],
23
+ [/anthropic\.com/i, ['ANTHROPIC_API_KEY']],
24
+ [/generativelanguage\.googleapis\.com/i, ['GEMINI_API_KEY', 'GOOGLE_API_KEY']],
25
+ [/localhost|127\.0\.0\.1/i, []] // local Ollama needs no key
26
+ ];
27
+
28
+ function envVarsFromFile() {
29
+ try {
30
+ const envPath = path.resolve(process.cwd(), '.env');
31
+ if (!fs.existsSync(envPath)) return {};
32
+ return Object.fromEntries(
33
+ fs.readFileSync(envPath, 'utf8').split(/\r?\n/).filter(line => line.includes('=') && !line.trim().startsWith('#')).map(line => {
34
+ const idx = line.indexOf('=');
35
+ return [line.slice(0, idx).trim(), line.slice(idx + 1).trim()];
36
+ })
37
+ );
38
+ } catch {
39
+ return {};
40
+ }
41
+ }
42
+
43
+ function lookup(names) {
44
+ const fromEnv = names.map(n => process.env[n]).find(Boolean);
45
+ if (fromEnv) return fromEnv;
46
+ const file = envVarsFromFile();
47
+ return names.map(n => file[n]).find(Boolean) || null;
48
+ }
49
+
50
+ /**
51
+ * Provider + endpoint aware key resolution: the endpoint host decides the key
52
+ * name first (Groq/DeepSeek/Mistral/... each have their own), then the provider.
53
+ * Process environment always wins over .env.
54
+ */
55
+ function resolveApiKey(provider, endpoint) {
56
+ // A key stored through the UI (or imported from a CLI) wins over the
57
+ // environment, so the Providers page is the source of truth on this machine.
58
+ try {
59
+ const providers = require('../daemon/core/providers');
60
+ const stored = providers.resolveStored(provider, endpoint);
61
+ if (stored) return stored;
62
+ } catch { /* provider store unavailable: fall through to env */ }
63
+
64
+ const ep = String(endpoint || '');
65
+ for (const [re, names] of ENDPOINT_KEY_ENV) {
66
+ if (re.test(ep)) return names.length ? lookup(names) : null;
67
+ }
68
+ const envNames = KEY_ENV[String(provider || 'openai').toLowerCase()] || KEY_ENV.openai;
69
+ return lookup(envNames);
70
+ }
71
+
72
+ class ConfigLoader {
73
+ static load(configPath = 'liber.yaml') {
74
+ try {
75
+ // 1. Load YAML config
76
+ const absolutePath = path.resolve(process.cwd(), configPath);
77
+ const fileContents = fs.readFileSync(absolutePath, 'utf8');
78
+ const config = yaml.load(fileContents);
79
+
80
+ // 2. Secret Management: provider-aware key from environment or .env
81
+ const engine = config.agent?.engine;
82
+ if (engine && !engine.provider) {
83
+ // A null provider used to kill the whole daemon at boot
84
+ // ("Unsupported LLM provider: null"). Fall back to the OpenAI-compatible
85
+ // adapter the config comment already promises, and say so out loud.
86
+ engine.provider = 'openai';
87
+ console.warn('[config] engine.provider was empty — defaulting to the openai-compatible adapter.');
88
+ }
89
+ if (engine && !engine.endpoint) {
90
+ console.warn('[config] engine.endpoint is empty — every request will fail until a provider endpoint is set.');
91
+ }
92
+
93
+ const provider = (config.agent?.engine?.provider || 'openai').toLowerCase();
94
+ const envKey = resolveApiKey(provider);
95
+
96
+ if (envKey && config.agent?.engine) {
97
+ config.agent.engine.apiKey = envKey;
98
+ }
99
+
100
+ return config;
101
+ } catch (e) {
102
+ console.error(`[Error] Failed to load config: ${e.message}`);
103
+ process.exit(1);
104
+ }
105
+ }
106
+ }
107
+
108
+ module.exports = ConfigLoader;
109
+ module.exports.resolveApiKey = resolveApiKey;
@@ -0,0 +1,56 @@
1
+ // src/utils/logger.js
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+
5
+ class Logger {
6
+ constructor() {
7
+ this.logFile = path.join(process.cwd(), 'liber.log');
8
+ }
9
+
10
+ // Structured logging for production telemetry
11
+ info(agentId, event, details) {
12
+ this._write('INFO', agentId, event, details);
13
+ }
14
+
15
+ warn(agentId, event, details) {
16
+ this._write('WARN', agentId, event, details);
17
+ }
18
+
19
+ error(agentId, event, details) {
20
+ this._write('ERROR', agentId, event, details);
21
+ }
22
+
23
+ _write(level, agentId, event, details) {
24
+ const logEntry = {
25
+ timestamp: new Date().toISOString(),
26
+ level,
27
+ agentId,
28
+ event,
29
+ details
30
+ };
31
+ const logString = JSON.stringify(logEntry);
32
+
33
+ // Console for TUI, File for Telemetry
34
+ console.log(`[${level}] ${agentId} | ${event}`);
35
+ try {
36
+ this._rotateIfNeeded();
37
+ fs.appendFileSync(this.logFile, logString + '\n');
38
+ } catch {
39
+ // never let logging break the harness
40
+ }
41
+ }
42
+
43
+ _rotateIfNeeded() {
44
+ const MAX = 5 * 1024 * 1024; // 5 MB
45
+ try {
46
+ const stat = fs.statSync(this.logFile);
47
+ if (stat.size > MAX) {
48
+ fs.renameSync(this.logFile, `${this.logFile}.1`);
49
+ }
50
+ } catch {
51
+ // file missing or unreadable — nothing to rotate
52
+ }
53
+ }
54
+ }
55
+
56
+ module.exports = new Logger();