beast-agent 1.9.0 → 2.0.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.
- package/LICENSE +21 -21
- package/README.md +130 -130
- package/bin/beast-agent.js +137 -137
- package/package.json +1 -1
- package/scripts/fix-electron.js +138 -138
- package/scripts/release.js +137 -131
- package/scripts/swap-electron.js +40 -40
- package/src/agent/agentdefs.js +122 -122
- package/src/agent/bots.js +580 -580
- package/src/agent/bus.js +389 -389
- package/src/agent/computeruse.js +200 -200
- package/src/agent/config.js +284 -284
- package/src/agent/discord.js +332 -332
- package/src/agent/engine.js +75 -10
- package/src/agent/kb.js +123 -123
- package/src/agent/llm.js +430 -430
- package/src/agent/logger.js +90 -90
- package/src/agent/mcp.js +427 -427
- package/src/agent/mem0.js +605 -605
- package/src/agent/memory.js +427 -427
- package/src/agent/mqueue.js +124 -124
- package/src/agent/pdf.js +20 -20
- package/src/agent/research.js +133 -133
- package/src/agent/scripts/news.py +113 -113
- package/src/agent/scripts/stealthsearch.py +30 -30
- package/src/agent/scripts/websearch.py +225 -225
- package/src/agent/searxng.js +325 -325
- package/src/agent/seeds/brainstorming/SKILL.md +90 -90
- package/src/agent/seeds/dispatching-parallel-agents/SKILL.md +120 -120
- package/src/agent/seeds/executing-plans/SKILL.md +60 -60
- package/src/agent/seeds/subagent-driven-development/SKILL.md +167 -167
- package/src/agent/seeds/systematic-debugging/SKILL.md +131 -131
- package/src/agent/seeds/test-driven-development/SKILL.md +152 -152
- package/src/agent/seeds/verification-before-completion/SKILL.md +63 -63
- package/src/agent/seeds/writing-plans/SKILL.md +162 -162
- package/src/agent/seeds/writing-skills/SKILL.md +229 -229
- package/src/agent/skills.js +652 -652
- package/src/agent/store.js +378 -378
- package/src/agent/telegram.js +155 -155
- package/src/agent/tokens.js +39 -39
- package/src/agent/usage.js +125 -125
- package/src/agent/watchers.js +312 -312
- package/src/agent/watext.js +80 -80
- package/src/agent/whatsapp.js +555 -555
- package/src/cron.js +255 -255
- package/src/main.js +348 -6
- package/src/preload.js +9 -0
- package/src/renderer/browserPreload.js +73 -73
- package/src/renderer/i18n.js +4 -0
- package/src/renderer/index.html +448 -409
- package/src/renderer/renderer.js +824 -41
- package/src/renderer/style.css +264 -5
package/src/agent/config.js
CHANGED
|
@@ -1,284 +1,284 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const os = require('os');
|
|
6
|
-
|
|
7
|
-
/* Minimal indentation-based YAML parser, sufficient for Beast's config.yaml
|
|
8
|
-
(nested maps, lists of scalars/maps, quoted/plain scalars, inline {} / []). */
|
|
9
|
-
|
|
10
|
-
function beastDir() {
|
|
11
|
-
if (process.env.BEAST_DATA) return process.env.BEAST_DATA;
|
|
12
|
-
return process.env.APPDATA
|
|
13
|
-
? path.join(process.env.APPDATA, 'beast')
|
|
14
|
-
: path.join(os.homedir(), 'AppData', 'Roaming', 'beast');
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function parseScalar(raw) {
|
|
18
|
-
let s = String(raw).trim();
|
|
19
|
-
if (s === '{}' || s === '[]') return s === '{}' ? {} : [];
|
|
20
|
-
if (
|
|
21
|
-
(s.startsWith('"') && s.endsWith('"') && s.length > 1) ||
|
|
22
|
-
(s.startsWith("'") && s.endsWith("'") && s.length > 1)
|
|
23
|
-
) {
|
|
24
|
-
return s.slice(1, -1);
|
|
25
|
-
}
|
|
26
|
-
return s;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function stripComment(line) {
|
|
30
|
-
let out = '';
|
|
31
|
-
let inS = false;
|
|
32
|
-
let inD = false;
|
|
33
|
-
for (let i = 0; i < line.length; i++) {
|
|
34
|
-
const c = line[i];
|
|
35
|
-
if (c === "'" && !inD) inS = !inS;
|
|
36
|
-
else if (c === '"' && !inS) inD = !inD;
|
|
37
|
-
else if (c === '#' && !inS && !inD && (i === 0 || /\s/.test(line[i - 1]))) break;
|
|
38
|
-
out += c;
|
|
39
|
-
}
|
|
40
|
-
return out.replace(/\s+$/, '');
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function tokenize(text) {
|
|
44
|
-
const recs = [];
|
|
45
|
-
for (const rawLine of text.split(/\r?\n/)) {
|
|
46
|
-
if (!rawLine.trim() || /^\s*#/.test(rawLine)) continue;
|
|
47
|
-
const line = stripComment(rawLine);
|
|
48
|
-
if (!line.trim()) continue;
|
|
49
|
-
const indent = line.match(/^ */)[0].length;
|
|
50
|
-
const content = line.trim();
|
|
51
|
-
if (content.startsWith('- ') || content === '-') {
|
|
52
|
-
recs.push({ indent, listItem: true, value: content === '-' ? '' : content.slice(2).trim() });
|
|
53
|
-
} else {
|
|
54
|
-
const m = content.match(/^([^:]+):\s*(.*)$/);
|
|
55
|
-
if (!m) continue;
|
|
56
|
-
recs.push({
|
|
57
|
-
indent,
|
|
58
|
-
listItem: false,
|
|
59
|
-
key: m[1].trim().replace(/^["']|["']$/g, ''),
|
|
60
|
-
value: m[2],
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
return recs;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function build(recs, startIdx, indent) {
|
|
68
|
-
// Decide container kind from first record
|
|
69
|
-
const first = recs[startIdx];
|
|
70
|
-
if (!first) return [{}, startIdx];
|
|
71
|
-
if (first.listItem) {
|
|
72
|
-
const arr = [];
|
|
73
|
-
let i = startIdx;
|
|
74
|
-
while (i < recs.length && recs[i].indent >= indent && recs[i].listItem) {
|
|
75
|
-
const r = recs[i];
|
|
76
|
-
if (r.value === '') {
|
|
77
|
-
// list of maps: subsequent deeper records belong to this item
|
|
78
|
-
const deeper = [];
|
|
79
|
-
let j = i + 1;
|
|
80
|
-
while (j < recs.length && recs[j].indent > r.indent) {
|
|
81
|
-
deeper.push(recs[j]);
|
|
82
|
-
j++;
|
|
83
|
-
}
|
|
84
|
-
const [obj] = build(deeper, 0, deeper.length ? deeper[0].indent : 0);
|
|
85
|
-
arr.push(obj && typeof obj === 'object' && !Array.isArray(obj) ? obj : {});
|
|
86
|
-
i = j;
|
|
87
|
-
} else if (/^([^:]+):\s*(.*)$/.test(r.value) && !/^https?:/.test(r.value)) {
|
|
88
|
-
// inline "key: value" inside list item -> single-key map
|
|
89
|
-
const m = r.value.match(/^([^:]+):\s*(.*)$/);
|
|
90
|
-
const obj = { [m[1].trim().replace(/^["']|["']$/g, '')]: parseScalar(m[2]) };
|
|
91
|
-
// absorb following deeper keys into same item
|
|
92
|
-
let j = i + 1;
|
|
93
|
-
while (j < recs.length && recs[j].indent > r.indent && !recs[j].listItem) {
|
|
94
|
-
const rr = recs[j];
|
|
95
|
-
obj[rr.key] = rr.value === '' ? null : parseScalar(rr.value);
|
|
96
|
-
j++;
|
|
97
|
-
}
|
|
98
|
-
arr.push(obj);
|
|
99
|
-
i = Math.max(i + 1, j);
|
|
100
|
-
} else {
|
|
101
|
-
arr.push(parseScalar(r.value));
|
|
102
|
-
i++;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
return [arr, i];
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const map = {};
|
|
109
|
-
let i = startIdx;
|
|
110
|
-
while (i < recs.length && recs[i].indent >= indent && !recs[i].listItem) {
|
|
111
|
-
const r = recs[i];
|
|
112
|
-
if (r.indent > indent) {
|
|
113
|
-
i++; // stray deeper record, skip defensively
|
|
114
|
-
continue;
|
|
115
|
-
}
|
|
116
|
-
if (r.value !== undefined && r.value.trim() !== '') {
|
|
117
|
-
map[r.key] = parseScalar(r.value);
|
|
118
|
-
i++;
|
|
119
|
-
} else {
|
|
120
|
-
// nested block: collect all deeper records
|
|
121
|
-
const deeper = [];
|
|
122
|
-
let j = i + 1;
|
|
123
|
-
while (j < recs.length && recs[j].indent > r.indent) {
|
|
124
|
-
deeper.push(recs[j]);
|
|
125
|
-
j++;
|
|
126
|
-
}
|
|
127
|
-
if (deeper.length) {
|
|
128
|
-
const [child, consumed] = build(deeper, 0, deeper[0].indent);
|
|
129
|
-
map[r.key] = child;
|
|
130
|
-
i = i + 1 + consumed;
|
|
131
|
-
} else {
|
|
132
|
-
map[r.key] = {};
|
|
133
|
-
i++;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
return [map, i - startIdx];
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function parseYaml(text) {
|
|
141
|
-
try {
|
|
142
|
-
const recs = tokenize(text);
|
|
143
|
-
const [out] = build(recs, 0, recs.length ? recs[0].indent : 0);
|
|
144
|
-
return out || {};
|
|
145
|
-
} catch {
|
|
146
|
-
return {};
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function parseEnvFile(file) {
|
|
151
|
-
const env = {};
|
|
152
|
-
try {
|
|
153
|
-
const text = fs.readFileSync(file, 'utf8');
|
|
154
|
-
for (const line of text.split(/\r?\n/)) {
|
|
155
|
-
const t = line.trim();
|
|
156
|
-
if (!t || t.startsWith('#')) continue;
|
|
157
|
-
const m = t.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/);
|
|
158
|
-
if (!m) continue;
|
|
159
|
-
let val = m[2].trim();
|
|
160
|
-
if (
|
|
161
|
-
(val.startsWith('"') && val.endsWith('"')) ||
|
|
162
|
-
(val.startsWith("'") && val.endsWith("'"))
|
|
163
|
-
) {
|
|
164
|
-
val = val.slice(1, -1);
|
|
165
|
-
}
|
|
166
|
-
env[m[1]] = val;
|
|
167
|
-
}
|
|
168
|
-
} catch {}
|
|
169
|
-
return env;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function chatCompletionsUrl(base) {
|
|
173
|
-
let b = String(base).trim().replace(/\/+$/, '');
|
|
174
|
-
if (/\/v\d+$/.test(b)) return b + '/chat/completions';
|
|
175
|
-
return b + '/v1/chat/completions';
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function resolveProviders(cfg, env) {
|
|
179
|
-
const candidates = [];
|
|
180
|
-
for (const [id, p] of Object.entries(cfg.providers || {})) {
|
|
181
|
-
if (!p || typeof p !== 'object' || Array.isArray(p) || !p.base_url) continue;
|
|
182
|
-
let key = p.key_env ? env[p.key_env] : null;
|
|
183
|
-
if (!key) key = env[`${id.toUpperCase().replace(/-/g, '_')}_API_KEY`];
|
|
184
|
-
if (!key) continue;
|
|
185
|
-
const models = new Set();
|
|
186
|
-
if (p.model) models.add(p.model);
|
|
187
|
-
for (const k of Object.keys(p.models || {})) models.add(k);
|
|
188
|
-
if (models.size === 0 && cfg.model && cfg.model.default) models.add(cfg.model.default);
|
|
189
|
-
candidates.push({
|
|
190
|
-
id,
|
|
191
|
-
name: p.name || id,
|
|
192
|
-
baseUrl: p.base_url,
|
|
193
|
-
url: chatCompletionsUrl(p.base_url),
|
|
194
|
-
key,
|
|
195
|
-
models: [...models],
|
|
196
|
-
/* opsiyonel 1M-token fiyatları: maliyet sayacı için */
|
|
197
|
-
costIn: Number(p.price_in) || null,
|
|
198
|
-
costOut: Number(p.price_out) || null,
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
return candidates;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const CFG_TEMPLATE = `# Beast Agent sağlayıcı yapılandırması
|
|
205
|
-
# Anahtarlar .env dosyasında (key_env ile eşleşir). Örnek:
|
|
206
|
-
#
|
|
207
|
-
# providers:
|
|
208
|
-
# zhipu:
|
|
209
|
-
# name: Zhipu AI
|
|
210
|
-
# base_url: https://api.z.ai/api/paas/v4
|
|
211
|
-
# key_env: ZHIPU_API_KEY
|
|
212
|
-
# models:
|
|
213
|
-
# glm-4.6: {}
|
|
214
|
-
#
|
|
215
|
-
# model:
|
|
216
|
-
# provider: zhipu # varsayılan provider id
|
|
217
|
-
# default: glm-4.6 # varsayılan model
|
|
218
|
-
|
|
219
|
-
providers: {}
|
|
220
|
-
`;
|
|
221
|
-
|
|
222
|
-
const ENV_TEMPLATE = `# Beast Agent API anahtarları (Bu dosyayı kimseyle paylaşma!)
|
|
223
|
-
# Provider'ın key_env alanıyla eşleşmeli; yoksa <PROVIDER>_API_KEY denenir.
|
|
224
|
-
# Örnek:
|
|
225
|
-
# ZHIPU_API_KEY=xxxxx
|
|
226
|
-
# OPENAI_API_KEY=sk-xxx
|
|
227
|
-
`;
|
|
228
|
-
|
|
229
|
-
function loadBeastConfig() {
|
|
230
|
-
const dataDir = beastDir();
|
|
231
|
-
try {
|
|
232
|
-
fs.mkdirSync(dataDir, { recursive: true });
|
|
233
|
-
} catch {}
|
|
234
|
-
const cfgPath = path.join(dataDir, 'config.yaml');
|
|
235
|
-
const envPath = path.join(dataDir, '.env');
|
|
236
|
-
|
|
237
|
-
try {
|
|
238
|
-
if (!fs.existsSync(cfgPath)) fs.writeFileSync(cfgPath, CFG_TEMPLATE);
|
|
239
|
-
if (!fs.existsSync(envPath)) fs.writeFileSync(envPath, ENV_TEMPLATE);
|
|
240
|
-
} catch {}
|
|
241
|
-
|
|
242
|
-
let cfg = {};
|
|
243
|
-
try {
|
|
244
|
-
cfg = parseYaml(fs.readFileSync(cfgPath, 'utf8'));
|
|
245
|
-
} catch {}
|
|
246
|
-
|
|
247
|
-
const env = { ...parseEnvFile(envPath) };
|
|
248
|
-
for (const [k, v] of Object.entries(process.env)) {
|
|
249
|
-
if (!(k in env) && typeof v === 'string') env[k] = v;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
const providers = resolveProviders(cfg, env);
|
|
253
|
-
const m = cfg.model || {};
|
|
254
|
-
|
|
255
|
-
let active = m.provider ? providers.find((p) => p.id === m.provider) : null;
|
|
256
|
-
if (!active && providers.length) active = providers[0];
|
|
257
|
-
|
|
258
|
-
let activeSelection = null;
|
|
259
|
-
if (active) {
|
|
260
|
-
const keyOverride = m.key_env ? env[m.key_env] : null;
|
|
261
|
-
activeSelection = {
|
|
262
|
-
providerId: active.id,
|
|
263
|
-
providerName: active.name,
|
|
264
|
-
model: m.default || active.models[0],
|
|
265
|
-
url: m.base_url ? chatCompletionsUrl(m.base_url) : active.url,
|
|
266
|
-
key: keyOverride || active.key,
|
|
267
|
-
costIn: active.costIn,
|
|
268
|
-
costOut: active.costOut,
|
|
269
|
-
};
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
const chain = [];
|
|
273
|
-
if (activeSelection) chain.push(activeSelection);
|
|
274
|
-
for (const p of providers) {
|
|
275
|
-
if (active && p.id === active.id) continue;
|
|
276
|
-
for (const model of p.models.slice(0, 3)) {
|
|
277
|
-
chain.push({ providerId: p.id, providerName: p.name, model, url: p.url, key: p.key, costIn: p.costIn, costOut: p.costOut });
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
return { dataDir, configPath: cfgPath, envPath, providers, chain, defaultSelection: activeSelection || chain[0] || null };
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
module.exports = { parseYaml, parseEnvFile, loadBeastConfig, beastDir, chatCompletionsUrl };
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
|
|
7
|
+
/* Minimal indentation-based YAML parser, sufficient for Beast's config.yaml
|
|
8
|
+
(nested maps, lists of scalars/maps, quoted/plain scalars, inline {} / []). */
|
|
9
|
+
|
|
10
|
+
function beastDir() {
|
|
11
|
+
if (process.env.BEAST_DATA) return process.env.BEAST_DATA;
|
|
12
|
+
return process.env.APPDATA
|
|
13
|
+
? path.join(process.env.APPDATA, 'beast')
|
|
14
|
+
: path.join(os.homedir(), 'AppData', 'Roaming', 'beast');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function parseScalar(raw) {
|
|
18
|
+
let s = String(raw).trim();
|
|
19
|
+
if (s === '{}' || s === '[]') return s === '{}' ? {} : [];
|
|
20
|
+
if (
|
|
21
|
+
(s.startsWith('"') && s.endsWith('"') && s.length > 1) ||
|
|
22
|
+
(s.startsWith("'") && s.endsWith("'") && s.length > 1)
|
|
23
|
+
) {
|
|
24
|
+
return s.slice(1, -1);
|
|
25
|
+
}
|
|
26
|
+
return s;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function stripComment(line) {
|
|
30
|
+
let out = '';
|
|
31
|
+
let inS = false;
|
|
32
|
+
let inD = false;
|
|
33
|
+
for (let i = 0; i < line.length; i++) {
|
|
34
|
+
const c = line[i];
|
|
35
|
+
if (c === "'" && !inD) inS = !inS;
|
|
36
|
+
else if (c === '"' && !inS) inD = !inD;
|
|
37
|
+
else if (c === '#' && !inS && !inD && (i === 0 || /\s/.test(line[i - 1]))) break;
|
|
38
|
+
out += c;
|
|
39
|
+
}
|
|
40
|
+
return out.replace(/\s+$/, '');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function tokenize(text) {
|
|
44
|
+
const recs = [];
|
|
45
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
46
|
+
if (!rawLine.trim() || /^\s*#/.test(rawLine)) continue;
|
|
47
|
+
const line = stripComment(rawLine);
|
|
48
|
+
if (!line.trim()) continue;
|
|
49
|
+
const indent = line.match(/^ */)[0].length;
|
|
50
|
+
const content = line.trim();
|
|
51
|
+
if (content.startsWith('- ') || content === '-') {
|
|
52
|
+
recs.push({ indent, listItem: true, value: content === '-' ? '' : content.slice(2).trim() });
|
|
53
|
+
} else {
|
|
54
|
+
const m = content.match(/^([^:]+):\s*(.*)$/);
|
|
55
|
+
if (!m) continue;
|
|
56
|
+
recs.push({
|
|
57
|
+
indent,
|
|
58
|
+
listItem: false,
|
|
59
|
+
key: m[1].trim().replace(/^["']|["']$/g, ''),
|
|
60
|
+
value: m[2],
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return recs;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function build(recs, startIdx, indent) {
|
|
68
|
+
// Decide container kind from first record
|
|
69
|
+
const first = recs[startIdx];
|
|
70
|
+
if (!first) return [{}, startIdx];
|
|
71
|
+
if (first.listItem) {
|
|
72
|
+
const arr = [];
|
|
73
|
+
let i = startIdx;
|
|
74
|
+
while (i < recs.length && recs[i].indent >= indent && recs[i].listItem) {
|
|
75
|
+
const r = recs[i];
|
|
76
|
+
if (r.value === '') {
|
|
77
|
+
// list of maps: subsequent deeper records belong to this item
|
|
78
|
+
const deeper = [];
|
|
79
|
+
let j = i + 1;
|
|
80
|
+
while (j < recs.length && recs[j].indent > r.indent) {
|
|
81
|
+
deeper.push(recs[j]);
|
|
82
|
+
j++;
|
|
83
|
+
}
|
|
84
|
+
const [obj] = build(deeper, 0, deeper.length ? deeper[0].indent : 0);
|
|
85
|
+
arr.push(obj && typeof obj === 'object' && !Array.isArray(obj) ? obj : {});
|
|
86
|
+
i = j;
|
|
87
|
+
} else if (/^([^:]+):\s*(.*)$/.test(r.value) && !/^https?:/.test(r.value)) {
|
|
88
|
+
// inline "key: value" inside list item -> single-key map
|
|
89
|
+
const m = r.value.match(/^([^:]+):\s*(.*)$/);
|
|
90
|
+
const obj = { [m[1].trim().replace(/^["']|["']$/g, '')]: parseScalar(m[2]) };
|
|
91
|
+
// absorb following deeper keys into same item
|
|
92
|
+
let j = i + 1;
|
|
93
|
+
while (j < recs.length && recs[j].indent > r.indent && !recs[j].listItem) {
|
|
94
|
+
const rr = recs[j];
|
|
95
|
+
obj[rr.key] = rr.value === '' ? null : parseScalar(rr.value);
|
|
96
|
+
j++;
|
|
97
|
+
}
|
|
98
|
+
arr.push(obj);
|
|
99
|
+
i = Math.max(i + 1, j);
|
|
100
|
+
} else {
|
|
101
|
+
arr.push(parseScalar(r.value));
|
|
102
|
+
i++;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return [arr, i];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const map = {};
|
|
109
|
+
let i = startIdx;
|
|
110
|
+
while (i < recs.length && recs[i].indent >= indent && !recs[i].listItem) {
|
|
111
|
+
const r = recs[i];
|
|
112
|
+
if (r.indent > indent) {
|
|
113
|
+
i++; // stray deeper record, skip defensively
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (r.value !== undefined && r.value.trim() !== '') {
|
|
117
|
+
map[r.key] = parseScalar(r.value);
|
|
118
|
+
i++;
|
|
119
|
+
} else {
|
|
120
|
+
// nested block: collect all deeper records
|
|
121
|
+
const deeper = [];
|
|
122
|
+
let j = i + 1;
|
|
123
|
+
while (j < recs.length && recs[j].indent > r.indent) {
|
|
124
|
+
deeper.push(recs[j]);
|
|
125
|
+
j++;
|
|
126
|
+
}
|
|
127
|
+
if (deeper.length) {
|
|
128
|
+
const [child, consumed] = build(deeper, 0, deeper[0].indent);
|
|
129
|
+
map[r.key] = child;
|
|
130
|
+
i = i + 1 + consumed;
|
|
131
|
+
} else {
|
|
132
|
+
map[r.key] = {};
|
|
133
|
+
i++;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return [map, i - startIdx];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function parseYaml(text) {
|
|
141
|
+
try {
|
|
142
|
+
const recs = tokenize(text);
|
|
143
|
+
const [out] = build(recs, 0, recs.length ? recs[0].indent : 0);
|
|
144
|
+
return out || {};
|
|
145
|
+
} catch {
|
|
146
|
+
return {};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function parseEnvFile(file) {
|
|
151
|
+
const env = {};
|
|
152
|
+
try {
|
|
153
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
154
|
+
for (const line of text.split(/\r?\n/)) {
|
|
155
|
+
const t = line.trim();
|
|
156
|
+
if (!t || t.startsWith('#')) continue;
|
|
157
|
+
const m = t.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/);
|
|
158
|
+
if (!m) continue;
|
|
159
|
+
let val = m[2].trim();
|
|
160
|
+
if (
|
|
161
|
+
(val.startsWith('"') && val.endsWith('"')) ||
|
|
162
|
+
(val.startsWith("'") && val.endsWith("'"))
|
|
163
|
+
) {
|
|
164
|
+
val = val.slice(1, -1);
|
|
165
|
+
}
|
|
166
|
+
env[m[1]] = val;
|
|
167
|
+
}
|
|
168
|
+
} catch {}
|
|
169
|
+
return env;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function chatCompletionsUrl(base) {
|
|
173
|
+
let b = String(base).trim().replace(/\/+$/, '');
|
|
174
|
+
if (/\/v\d+$/.test(b)) return b + '/chat/completions';
|
|
175
|
+
return b + '/v1/chat/completions';
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function resolveProviders(cfg, env) {
|
|
179
|
+
const candidates = [];
|
|
180
|
+
for (const [id, p] of Object.entries(cfg.providers || {})) {
|
|
181
|
+
if (!p || typeof p !== 'object' || Array.isArray(p) || !p.base_url) continue;
|
|
182
|
+
let key = p.key_env ? env[p.key_env] : null;
|
|
183
|
+
if (!key) key = env[`${id.toUpperCase().replace(/-/g, '_')}_API_KEY`];
|
|
184
|
+
if (!key) continue;
|
|
185
|
+
const models = new Set();
|
|
186
|
+
if (p.model) models.add(p.model);
|
|
187
|
+
for (const k of Object.keys(p.models || {})) models.add(k);
|
|
188
|
+
if (models.size === 0 && cfg.model && cfg.model.default) models.add(cfg.model.default);
|
|
189
|
+
candidates.push({
|
|
190
|
+
id,
|
|
191
|
+
name: p.name || id,
|
|
192
|
+
baseUrl: p.base_url,
|
|
193
|
+
url: chatCompletionsUrl(p.base_url),
|
|
194
|
+
key,
|
|
195
|
+
models: [...models],
|
|
196
|
+
/* opsiyonel 1M-token fiyatları: maliyet sayacı için */
|
|
197
|
+
costIn: Number(p.price_in) || null,
|
|
198
|
+
costOut: Number(p.price_out) || null,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
return candidates;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const CFG_TEMPLATE = `# Beast Agent sağlayıcı yapılandırması
|
|
205
|
+
# Anahtarlar .env dosyasında (key_env ile eşleşir). Örnek:
|
|
206
|
+
#
|
|
207
|
+
# providers:
|
|
208
|
+
# zhipu:
|
|
209
|
+
# name: Zhipu AI
|
|
210
|
+
# base_url: https://api.z.ai/api/paas/v4
|
|
211
|
+
# key_env: ZHIPU_API_KEY
|
|
212
|
+
# models:
|
|
213
|
+
# glm-4.6: {}
|
|
214
|
+
#
|
|
215
|
+
# model:
|
|
216
|
+
# provider: zhipu # varsayılan provider id
|
|
217
|
+
# default: glm-4.6 # varsayılan model
|
|
218
|
+
|
|
219
|
+
providers: {}
|
|
220
|
+
`;
|
|
221
|
+
|
|
222
|
+
const ENV_TEMPLATE = `# Beast Agent API anahtarları (Bu dosyayı kimseyle paylaşma!)
|
|
223
|
+
# Provider'ın key_env alanıyla eşleşmeli; yoksa <PROVIDER>_API_KEY denenir.
|
|
224
|
+
# Örnek:
|
|
225
|
+
# ZHIPU_API_KEY=xxxxx
|
|
226
|
+
# OPENAI_API_KEY=sk-xxx
|
|
227
|
+
`;
|
|
228
|
+
|
|
229
|
+
function loadBeastConfig() {
|
|
230
|
+
const dataDir = beastDir();
|
|
231
|
+
try {
|
|
232
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
233
|
+
} catch {}
|
|
234
|
+
const cfgPath = path.join(dataDir, 'config.yaml');
|
|
235
|
+
const envPath = path.join(dataDir, '.env');
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
if (!fs.existsSync(cfgPath)) fs.writeFileSync(cfgPath, CFG_TEMPLATE);
|
|
239
|
+
if (!fs.existsSync(envPath)) fs.writeFileSync(envPath, ENV_TEMPLATE);
|
|
240
|
+
} catch {}
|
|
241
|
+
|
|
242
|
+
let cfg = {};
|
|
243
|
+
try {
|
|
244
|
+
cfg = parseYaml(fs.readFileSync(cfgPath, 'utf8'));
|
|
245
|
+
} catch {}
|
|
246
|
+
|
|
247
|
+
const env = { ...parseEnvFile(envPath) };
|
|
248
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
249
|
+
if (!(k in env) && typeof v === 'string') env[k] = v;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const providers = resolveProviders(cfg, env);
|
|
253
|
+
const m = cfg.model || {};
|
|
254
|
+
|
|
255
|
+
let active = m.provider ? providers.find((p) => p.id === m.provider) : null;
|
|
256
|
+
if (!active && providers.length) active = providers[0];
|
|
257
|
+
|
|
258
|
+
let activeSelection = null;
|
|
259
|
+
if (active) {
|
|
260
|
+
const keyOverride = m.key_env ? env[m.key_env] : null;
|
|
261
|
+
activeSelection = {
|
|
262
|
+
providerId: active.id,
|
|
263
|
+
providerName: active.name,
|
|
264
|
+
model: m.default || active.models[0],
|
|
265
|
+
url: m.base_url ? chatCompletionsUrl(m.base_url) : active.url,
|
|
266
|
+
key: keyOverride || active.key,
|
|
267
|
+
costIn: active.costIn,
|
|
268
|
+
costOut: active.costOut,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const chain = [];
|
|
273
|
+
if (activeSelection) chain.push(activeSelection);
|
|
274
|
+
for (const p of providers) {
|
|
275
|
+
if (active && p.id === active.id) continue;
|
|
276
|
+
for (const model of p.models.slice(0, 3)) {
|
|
277
|
+
chain.push({ providerId: p.id, providerName: p.name, model, url: p.url, key: p.key, costIn: p.costIn, costOut: p.costOut });
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return { dataDir, configPath: cfgPath, envPath, providers, chain, defaultSelection: activeSelection || chain[0] || null };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
module.exports = { parseYaml, parseEnvFile, loadBeastConfig, beastDir, chatCompletionsUrl };
|