@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,223 @@
1
+ // src/daemon/core/config-apply.js — whitelisted live config updates.
2
+ // Every key a client may change is declared here with a type; anything else is
3
+ // rejected. Persistence patches only the targeted YAML lines, comments intact.
4
+ const PROVIDERS = new Set(['openai', 'ollama', 'anthropic', 'gemini']);
5
+ const TERMINAL_MODES = new Set(['read_only', 'allowlist', 'prompt', 'open']);
6
+ const REASONING_EFFORTS = new Set(['auto', 'low', 'medium', 'high']);
7
+ const MODEL_RE = /^[A-Za-z0-9._:\/-]{1,64}$/;
8
+
9
+ const SCHEMA = {
10
+ 'engine.provider': { kind: 'enum', values: PROVIDERS },
11
+ 'engine.model': { kind: 'model' },
12
+ 'engine.endpoint': { kind: 'endpoint' },
13
+ 'engine.context_window_override': { kind: 'int', min: 0, max: 1_048_576 },
14
+ 'engine.reasoning_effort': { kind: 'enum', values: REASONING_EFFORTS },
15
+ 'engine.max_tokens': { kind: 'int', min: 0, max: 1_048_576 },
16
+ 'engine.temperature': { kind: 'float', min: 0, max: 2 },
17
+ 'engine.fallback_models': { kind: 'string_list' },
18
+ 'engine.top_p': { kind: 'float', min: 0, max: 1 },
19
+ 'grounding.enabled': { kind: 'boolean' },
20
+ 'grounding.max_repairs': { kind: 'int', min: 0, max: 5 },
21
+ 'grounding.require_readback': { kind: 'boolean' },
22
+ 'grounding.require_citations': { kind: 'boolean' },
23
+ 'grounding.semantic_verifier': { kind: 'boolean' },
24
+ 'policy.write_roots': { kind: 'string_list' },
25
+ 'policy.terminal.mode': { kind: 'enum', values: TERMINAL_MODES },
26
+ 'policy.terminal.allowlist': { kind: 'string_list' },
27
+ 'policy.terminal.denylist': { kind: 'string_list' },
28
+ 'policy.terminal.backend': { kind: 'enum', values: new Set(['powershell', 'docker']) },
29
+ 'policy.terminal.image': { kind: 'string' },
30
+ 'policy.terminal.allow_network': { kind: 'boolean' },
31
+ 'limits.max_tokens_per_day': { kind: 'int', min: 0 },
32
+ 'limits.max_cost_per_day': { kind: 'float', min: 0 },
33
+ 'limits.circuit_breaker.max_loops': { kind: 'int', min: 1, max: 100 },
34
+ 'memory.window_size': { kind: 'int', min: 1, max: 200 },
35
+ 'memory.persist': { kind: 'boolean' },
36
+ 'telemetry.trace': { kind: 'boolean' }
37
+ };
38
+
39
+ function validateConfigUpdate(body) {
40
+ if (!body || typeof body !== 'object') return { error: 'Missing request body.' };
41
+ const changes = {};
42
+ for (const [path, value] of Object.entries(body)) {
43
+ const spec = SCHEMA[path];
44
+ if (!spec) return { error: `Unknown config key '${path}'.` };
45
+ switch (spec.kind) {
46
+ case 'boolean':
47
+ if (typeof value !== 'boolean') return { error: `'${path}' must be a boolean.` };
48
+ break;
49
+ case 'int': {
50
+ const n = Number(value);
51
+ if (!Number.isInteger(n)) return { error: `'${path}' must be an integer.` };
52
+ if (n < spec.min || n > spec.max) return { error: `'${path}' must be within ${spec.min}..${spec.max}.` };
53
+ changes[path] = n;
54
+ continue;
55
+ }
56
+ case 'float': {
57
+ const n = Number(value);
58
+ if (!Number.isFinite(n) || n < 0) return { error: `'${path}' must be a non-negative number.` };
59
+ changes[path] = n;
60
+ continue;
61
+ }
62
+ case 'enum':
63
+ if (!spec.values.has(String(value))) return { error: `'${path}' must be one of: ${[...spec.values].join(', ')}.` };
64
+ break;
65
+ case 'model':
66
+ if (!MODEL_RE.test(String(value || ''))) return { error: `'${path}' has an invalid model name.` };
67
+ break;
68
+ case 'endpoint':
69
+ if (value !== '' && value != null && !/^https?:\/\/\S+$/.test(String(value))) {
70
+ return { error: `'${path}' must be a valid URL.` };
71
+ }
72
+ break;
73
+ case 'string_list':
74
+ if (!Array.isArray(value) || value.some(v => typeof v !== 'string')) {
75
+ return { error: `'${path}' must be an array of strings.` };
76
+ }
77
+ break;
78
+ }
79
+ changes[path] = value;
80
+ }
81
+ return { changes };
82
+ }
83
+
84
+ function yamlValue(value) {
85
+ if (typeof value === 'string') return JSON.stringify(value);
86
+ if (typeof value === 'boolean') return value ? 'true' : 'false';
87
+ if (typeof value === 'number') return String(value);
88
+ if (Array.isArray(value)) return `[${value.map(v => JSON.stringify(v)).join(', ')}]`;
89
+ return JSON.stringify(value);
90
+ }
91
+
92
+ /**
93
+ * Patch one dotted key (a.b.c) in liber.yaml text, keeping every comment and
94
+ * every other line. Returns the text unchanged when the path does not exist.
95
+ */
96
+ function patchYamlKey(text, path, value) {
97
+ const lines = String(text || '').split(/\r?\n/);
98
+ const parts = String(path).split('.');
99
+ let pos = 0;
100
+ let depth = 0;
101
+
102
+ for (const parent of parts.slice(0, -1)) {
103
+ let found = -1;
104
+ for (let i = pos; i < lines.length; i++) {
105
+ const m = lines[i].match(/^(\s*)([A-Za-z_][\w-]*):\s*(.*)$/);
106
+ if (m && m[2] === parent && m[1].length === depth) { found = i; break; }
107
+ }
108
+ if (found < 0) return text;
109
+ pos = found + 1;
110
+ depth += 2;
111
+ }
112
+
113
+ const last = parts[parts.length - 1];
114
+ for (let i = pos; i < lines.length; i++) {
115
+ const m = lines[i].match(/^(\s*)([A-Za-z_][\w-]*):\s*(.*)$/);
116
+ if (!m) continue;
117
+ if (m[1].length < depth) break;
118
+ if (m[2] === last && m[1].length === depth) {
119
+ const comment = m[3].match(/\s*(#.*)$/);
120
+ const trail = comment ? ` ${comment[1]}` : '';
121
+ lines[i] = `${m[1]}${last}: ${yamlValue(value)}${trail}`;
122
+ return lines.join('\n');
123
+ }
124
+ }
125
+
126
+ // The key does not exist yet: a setting the UI can change must be persistable,
127
+ // otherwise it silently disappears at the next daemon start. Insert it at the
128
+ // end of its parent block, matching that block's indentation.
129
+ if (depth > 0) {
130
+ let insertAt = pos;
131
+ for (let i = pos; i < lines.length; i++) {
132
+ const m = lines[i].match(/^(\s*)([A-Za-z_][\w-]*):/);
133
+ if (m && m[1].length < depth) break;
134
+ if (String(lines[i]).trim()) insertAt = i + 1;
135
+ }
136
+ const pad = ' '.repeat(depth);
137
+ lines.splice(insertAt, 0, `${pad}${last}: ${yamlValue(value)}`);
138
+ return lines.join('\n');
139
+ }
140
+ return text;
141
+ }
142
+
143
+ function patchYamlKeys(text, changes) {
144
+ let out = String(text || '');
145
+ for (const [path, value] of Object.entries(changes)) {
146
+ out = patchYamlKey(out, path, value);
147
+ }
148
+ return out;
149
+ }
150
+
151
+ /** Public view: the full effective grounding config, live. */
152
+ function sanitizeConfig(config, traceEnabled, cwd) {
153
+ const engine = config?.engine || {};
154
+ return {
155
+ agent: { name: config?.name || null, version: config?.version || null },
156
+ engine: {
157
+ provider: engine.provider || null,
158
+ model: engine.model || null,
159
+ endpoint: engine.endpoint || null,
160
+ context_window: engine.context_window || 32000,
161
+ context_window_override: engine.context_window_override || 0,
162
+ fallback_models: Array.isArray(engine.fallback_models) ? engine.fallback_models.slice(0, 8) : [],
163
+ reasoning_effort: engine.reasoning_effort || 'auto',
164
+ max_tokens: engine.max_tokens || null,
165
+ temperature: engine.temperature ?? null,
166
+ top_p: engine.top_p ?? null
167
+ },
168
+ grounding: { ...(config?.grounding || {}) },
169
+ policy: sanitizePolicy(),
170
+ limits: { ...(config?.limits || {}) },
171
+ memory: { ...(config?.memory || {}) },
172
+ telemetry: { trace: !!traceEnabled },
173
+ cwd
174
+ };
175
+ }
176
+
177
+ function sanitizePolicy() {
178
+ try {
179
+ const policy = require('./policy');
180
+ const cur = policy.current();
181
+ return {
182
+ write_roots: cur.write_roots,
183
+ allow_outside_workspace: cur.allow_outside_workspace,
184
+ terminal: policy.terminalConfig()
185
+ };
186
+ } catch {
187
+ return null;
188
+ }
189
+ }
190
+
191
+ module.exports = { SCHEMA, validateConfigUpdate, patchYamlKey, patchYamlKeys, sanitizeConfig, sanitizePolicy, defaults };
192
+
193
+ /** "Reset to defaults": the values this build ships with. */
194
+ const DEFAULTS = {
195
+ 'engine.reasoning_effort': 'auto',
196
+ 'engine.max_tokens': 4096,
197
+ 'engine.temperature': 0.2,
198
+ 'engine.fallback_models': [],
199
+ 'engine.top_p': 1,
200
+ 'engine.context_window_override': 0,
201
+ 'grounding.enabled': true,
202
+ 'grounding.max_repairs': 2,
203
+ 'grounding.require_readback': true,
204
+ 'grounding.require_citations': true,
205
+ 'grounding.semantic_verifier': true,
206
+ 'policy.terminal.mode': 'allowlist',
207
+ 'policy.terminal.allowlist': ['npm test', 'node --test', 'git status', 'git diff', 'git add', 'git commit', 'git push'],
208
+ 'policy.terminal.denylist': [],
209
+ 'policy.terminal.backend': 'docker',
210
+ 'policy.terminal.image': 'alpine:latest',
211
+ 'policy.terminal.allow_network': false,
212
+ 'policy.write_roots': ['.'],
213
+ 'limits.max_tokens_per_day': 2000000,
214
+ 'limits.max_cost_per_day': 1,
215
+ 'limits.circuit_breaker.max_loops': 12,
216
+ 'memory.window_size': 20,
217
+ 'memory.persist': true,
218
+ 'telemetry.trace': true
219
+ };
220
+
221
+ function defaults() {
222
+ return JSON.parse(JSON.stringify(DEFAULTS));
223
+ }
@@ -0,0 +1,131 @@
1
+ // src/daemon/core/connectors.js — HTTP connectors as first-class tools.
2
+ //
3
+ // A connector is a small declaration: a base URL, headers, and a list of actions.
4
+ // Each one becomes a tool the agent can call, so "check my repo issues" does not
5
+ // mean driving a browser. Secrets stay in the vault: a header written as
6
+ // "vault:github.token" is resolved at call time and never enters the prompt.
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const vault = require('./vault');
10
+
11
+ const FILE = () => path.resolve(process.env.LIBER_CONNECTORS_FILE || path.join(process.cwd(), '.liber', 'connectors.json'));
12
+ const ID_RE = /^[a-z][a-z0-9_-]{1,31}$/;
13
+ const MAX_BODY = 8000;
14
+
15
+ /** Read and validate the declaration file. Bad entries are reported, not used. */
16
+ function load() {
17
+ let raw;
18
+ try { raw = JSON.parse(fs.readFileSync(FILE(), 'utf8')); } catch { return { connectors: [], problems: [] }; }
19
+ const list = Array.isArray(raw) ? raw : (raw.connectors || []);
20
+ const problems = [];
21
+ const connectors = [];
22
+
23
+ for (const c of list) {
24
+ const id = String(c?.id || '');
25
+ if (!ID_RE.test(id)) { problems.push(`id '${id}' must be lowercase letters, digits, _ or -`); continue; }
26
+ let base;
27
+ try { base = new URL(String(c.baseUrl || '')); } catch { problems.push(`${id}: baseUrl is not a URL`); continue; }
28
+ if (!['http:', 'https:'].includes(base.protocol)) { problems.push(`${id}: baseUrl must be http(s)`); continue; }
29
+ const actions = (c.actions || []).filter(a => a && a.name && a.path).map(a => ({
30
+ name: String(a.name),
31
+ method: String(a.method || 'GET').toUpperCase(),
32
+ path: String(a.path),
33
+ description: String(a.description || ''),
34
+ params: a.params || null
35
+ }));
36
+ if (!actions.length) { problems.push(`${id}: no actions`); continue; }
37
+ connectors.push({
38
+ id,
39
+ label: String(c.label || id),
40
+ baseUrl: base.toString().replace(/\/$/, ''),
41
+ headers: c.headers || {},
42
+ actions
43
+ });
44
+ }
45
+ return { connectors, problems };
46
+ }
47
+
48
+ function fillPath(template, params) {
49
+ return String(template).replace(/\{(\w+)\}/g, (_, key) => {
50
+ const value = params[key];
51
+ if (value === undefined || value === null) throw new Error(`missing parameter '${key}' for path '${template}'`);
52
+ return encodeURIComponent(String(value));
53
+ });
54
+ }
55
+
56
+ function resolveHeaders(headers) {
57
+ const out = {};
58
+ for (const [k, v] of Object.entries(headers || {})) {
59
+ const value = vault.resolveRef(String(v));
60
+ if (value) out[k] = value;
61
+ }
62
+ return out;
63
+ }
64
+
65
+ async function call(connector, actionName, args = {}) {
66
+ const action = connector.actions.find(a => a.name === actionName);
67
+ if (!action) return { error: `'${actionName}' is not an action of ${connector.id}. Known: ${connector.actions.map(a => a.name).join(', ')}` };
68
+
69
+ const params = args.params || {};
70
+ let url;
71
+ try { url = new URL(connector.baseUrl + fillPath(action.path, params)); }
72
+ catch (e) { return { error: e.message }; }
73
+
74
+ const init = { method: action.method, headers: { accept: 'application/json', ...resolveHeaders(connector.headers) } };
75
+ if (action.method === 'GET' || action.method === 'DELETE') {
76
+ for (const [k, v] of Object.entries(params)) {
77
+ if (!action.path.includes(`{${k}}`)) url.searchParams.set(k, String(v));
78
+ }
79
+ } else {
80
+ init.headers['content-type'] = 'application/json';
81
+ init.body = JSON.stringify(args.body ?? params ?? {});
82
+ }
83
+
84
+ const controller = new AbortController();
85
+ const timer = setTimeout(() => controller.abort(), 15000);
86
+ try {
87
+ const res = await fetch(url, { ...init, signal: controller.signal });
88
+ const text = await res.text();
89
+ let body = text;
90
+ try { body = JSON.parse(text); } catch { /* keep text */ }
91
+ const clipped = typeof body === 'string' && body.length > MAX_BODY;
92
+ return {
93
+ connector: connector.id,
94
+ action: action.name,
95
+ status: res.status,
96
+ ok: res.ok,
97
+ body: clipped ? body.slice(0, MAX_BODY) + '\n… [truncated]' : body
98
+ };
99
+ } catch (e) {
100
+ return { error: `${connector.id}/${action.name} failed: ${e.name === 'AbortError' ? 'timed out after 15s' : e.message}` };
101
+ } finally {
102
+ clearTimeout(timer);
103
+ }
104
+ }
105
+
106
+ /** Register one tool per connector, named conn_<id>. */
107
+ function install(registry) {
108
+ const { connectors, problems } = load();
109
+ const installed = [];
110
+ for (const c of connectors) {
111
+ const toolName = `conn_${c.id}`;
112
+ registry.register({
113
+ name: toolName,
114
+ description: `${c.label} over HTTP. Actions: ${c.actions.map(a => `${a.name}${a.description ? ' (' + a.description + ')' : ''}`).join(' · ')}`,
115
+ parameters: {
116
+ type: 'object',
117
+ properties: {
118
+ action: { type: 'string', enum: c.actions.map(a => a.name), description: 'Which action to call' },
119
+ params: { type: 'object', description: 'Path/query parameters, e.g. {"owner":"me","repo":"app"}' },
120
+ body: { type: 'object', description: 'JSON body for POST/PATCH/PUT actions' }
121
+ },
122
+ required: ['action']
123
+ },
124
+ handler: async (args) => call(c, String(args.action || ''), args)
125
+ });
126
+ installed.push(toolName);
127
+ }
128
+ return { installed, problems };
129
+ }
130
+
131
+ module.exports = { load, call, install, fillPath, resolveHeaders, FILE };