@openagents-org/agent-launcher 0.1.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/README.md +86 -0
- package/bin/agent-connector.js +4 -0
- package/package.json +42 -0
- package/registry.json +457 -0
- package/src/adapters/base.js +327 -0
- package/src/adapters/claude.js +420 -0
- package/src/adapters/codex.js +260 -0
- package/src/adapters/index.js +39 -0
- package/src/adapters/openclaw.js +264 -0
- package/src/adapters/utils.js +83 -0
- package/src/adapters/workspace-prompt.js +293 -0
- package/src/autostart.js +178 -0
- package/src/cli.js +556 -0
- package/src/config.js +322 -0
- package/src/daemon.js +666 -0
- package/src/env.js +111 -0
- package/src/index.js +205 -0
- package/src/installer.js +588 -0
- package/src/paths.js +276 -0
- package/src/registry.js +197 -0
- package/src/tui.js +540 -0
- package/src/utils.js +93 -0
- package/src/workspace-client.js +338 -0
package/src/config.js
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Manages ~/.openagents/daemon.yaml — the agent/network configuration file.
|
|
8
|
+
* Compatible with the Python SDK's daemon_config.py format.
|
|
9
|
+
*/
|
|
10
|
+
class Config {
|
|
11
|
+
constructor(configDir) {
|
|
12
|
+
this.configDir = configDir;
|
|
13
|
+
this.configFile = path.join(configDir, 'daemon.yaml');
|
|
14
|
+
this.statusFile = path.join(configDir, 'daemon.status.json');
|
|
15
|
+
this.pidFile = path.join(configDir, 'daemon.pid');
|
|
16
|
+
this.cmdFile = path.join(configDir, 'daemon.cmd');
|
|
17
|
+
this.logFile = path.join(configDir, 'daemon.log');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// -- Read --
|
|
21
|
+
|
|
22
|
+
load() {
|
|
23
|
+
try {
|
|
24
|
+
if (fs.existsSync(this.configFile)) {
|
|
25
|
+
const text = fs.readFileSync(this.configFile, 'utf-8');
|
|
26
|
+
return parseYaml(text);
|
|
27
|
+
}
|
|
28
|
+
} catch (err) {
|
|
29
|
+
console.error('Failed to load config:', err.message);
|
|
30
|
+
}
|
|
31
|
+
return { version: 2, agents: [], networks: [] };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
getAgents() {
|
|
35
|
+
return this.load().agents || [];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
getNetworks() {
|
|
39
|
+
return this.load().networks || [];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
getAgent(name) {
|
|
43
|
+
return this.getAgents().find((a) => a.name === name) || null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// -- Write --
|
|
47
|
+
|
|
48
|
+
save(config) {
|
|
49
|
+
fs.mkdirSync(this.configDir, { recursive: true });
|
|
50
|
+
fs.writeFileSync(this.configFile, serializeYaml(config), 'utf-8');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
addAgent({ name, type, role, path: agentPath }) {
|
|
54
|
+
const config = this.load();
|
|
55
|
+
if (config.agents.some((a) => a.name === name)) {
|
|
56
|
+
throw new Error(`Agent '${name}' already exists`);
|
|
57
|
+
}
|
|
58
|
+
const entry = { name, type: type || 'openclaw', role: role || 'worker' };
|
|
59
|
+
if (agentPath) entry.path = agentPath;
|
|
60
|
+
config.agents.push(entry);
|
|
61
|
+
this.save(config);
|
|
62
|
+
return entry;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
removeAgent(name) {
|
|
66
|
+
const config = this.load();
|
|
67
|
+
const idx = config.agents.findIndex((a) => a.name === name);
|
|
68
|
+
if (idx === -1) return false;
|
|
69
|
+
config.agents.splice(idx, 1);
|
|
70
|
+
this.save(config);
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
updateAgent(name, updates) {
|
|
75
|
+
const config = this.load();
|
|
76
|
+
const agent = config.agents.find((a) => a.name === name);
|
|
77
|
+
if (!agent) throw new Error(`Agent '${name}' not found`);
|
|
78
|
+
Object.assign(agent, updates);
|
|
79
|
+
this.save(config);
|
|
80
|
+
return agent;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
setAgentNetwork(agentName, networkSlug) {
|
|
84
|
+
const config = this.load();
|
|
85
|
+
const agent = config.agents.find((a) => a.name === agentName);
|
|
86
|
+
if (!agent) throw new Error(`Agent '${agentName}' not found`);
|
|
87
|
+
if (networkSlug) {
|
|
88
|
+
agent.network = networkSlug;
|
|
89
|
+
} else {
|
|
90
|
+
delete agent.network;
|
|
91
|
+
}
|
|
92
|
+
this.save(config);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
addNetwork({ id, slug, name, endpoint, token }) {
|
|
96
|
+
const config = this.load();
|
|
97
|
+
if (config.networks.some((n) => n.slug === slug || n.id === id)) {
|
|
98
|
+
return; // already exists
|
|
99
|
+
}
|
|
100
|
+
const entry = { id, slug };
|
|
101
|
+
if (name) entry.name = name;
|
|
102
|
+
if (endpoint) entry.endpoint = endpoint;
|
|
103
|
+
if (token) entry.token = token;
|
|
104
|
+
config.networks.push(entry);
|
|
105
|
+
this.save(config);
|
|
106
|
+
return entry;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
removeNetwork(slug) {
|
|
110
|
+
const config = this.load();
|
|
111
|
+
const idx = config.networks.findIndex((n) => n.slug === slug || n.id === slug);
|
|
112
|
+
if (idx === -1) return false;
|
|
113
|
+
config.networks.splice(idx, 1);
|
|
114
|
+
// Disconnect any agents that were on this network
|
|
115
|
+
for (const agent of config.agents) {
|
|
116
|
+
if (agent.network === slug) delete agent.network;
|
|
117
|
+
}
|
|
118
|
+
this.save(config);
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// -- Status / PID --
|
|
123
|
+
|
|
124
|
+
getStatus() {
|
|
125
|
+
try {
|
|
126
|
+
if (fs.existsSync(this.statusFile)) {
|
|
127
|
+
const data = JSON.parse(fs.readFileSync(this.statusFile, 'utf-8'));
|
|
128
|
+
return data.agents || {};
|
|
129
|
+
}
|
|
130
|
+
} catch {}
|
|
131
|
+
return {};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
getDaemonPid() {
|
|
135
|
+
try {
|
|
136
|
+
if (!fs.existsSync(this.pidFile)) return null;
|
|
137
|
+
const pid = parseInt(fs.readFileSync(this.pidFile, 'utf-8').trim(), 10);
|
|
138
|
+
return isNaN(pid) ? null : pid;
|
|
139
|
+
} catch {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
writeCommand(cmd) {
|
|
145
|
+
fs.writeFileSync(this.cmdFile, cmd + '\n', 'utf-8');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
getLogs(agentName, lines = 200) {
|
|
149
|
+
try {
|
|
150
|
+
if (!fs.existsSync(this.logFile)) return [];
|
|
151
|
+
const content = fs.readFileSync(this.logFile, 'utf-8');
|
|
152
|
+
let allLines = content.split('\n').filter(Boolean);
|
|
153
|
+
if (agentName) {
|
|
154
|
+
allLines = allLines.filter(
|
|
155
|
+
(l) => l.includes(agentName) || l.includes('daemon') || l.includes('Daemon')
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
return allLines.slice(-lines);
|
|
159
|
+
} catch {
|
|
160
|
+
return [];
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Tail log file with optional filter. Returns { lines, size }.
|
|
166
|
+
* @param {Object} opts - { agent, lines, offset }
|
|
167
|
+
* @param {string} [opts.agent] - Filter by agent name
|
|
168
|
+
* @param {number} [opts.lines=100] - Number of lines to return
|
|
169
|
+
* @param {number} [opts.offset=0] - Byte offset for incremental reads (0 = from end)
|
|
170
|
+
*/
|
|
171
|
+
tailLogs(opts = {}) {
|
|
172
|
+
const { agent, lines = 100, offset = 0 } = opts;
|
|
173
|
+
try {
|
|
174
|
+
if (!fs.existsSync(this.logFile)) return { lines: [], size: 0 };
|
|
175
|
+
const stat = fs.statSync(this.logFile);
|
|
176
|
+
if (offset > 0 && offset < stat.size) {
|
|
177
|
+
// Incremental read from offset
|
|
178
|
+
const fd = fs.openSync(this.logFile, 'r');
|
|
179
|
+
const buf = Buffer.alloc(stat.size - offset);
|
|
180
|
+
fs.readSync(fd, buf, 0, buf.length, offset);
|
|
181
|
+
fs.closeSync(fd);
|
|
182
|
+
let newLines = buf.toString('utf-8').split('\n').filter(Boolean);
|
|
183
|
+
if (agent) {
|
|
184
|
+
newLines = newLines.filter(l => l.includes(agent) || l.includes('Daemon'));
|
|
185
|
+
}
|
|
186
|
+
return { lines: newLines, size: stat.size };
|
|
187
|
+
}
|
|
188
|
+
// Full read, return last N
|
|
189
|
+
return { lines: this.getLogs(agent, lines), size: stat.size };
|
|
190
|
+
} catch {
|
|
191
|
+
return { lines: [], size: 0 };
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// -- YAML parser (compatible with Python SDK's daemon.yaml format) --
|
|
197
|
+
|
|
198
|
+
function parseYaml(text) {
|
|
199
|
+
const lines = text.split('\n');
|
|
200
|
+
const result = { version: 2, agents: [], networks: [] };
|
|
201
|
+
let currentList = null;
|
|
202
|
+
let currentItem = null;
|
|
203
|
+
|
|
204
|
+
for (const rawLine of lines) {
|
|
205
|
+
const line = rawLine.replace(/\r$/, '');
|
|
206
|
+
const stripped = line.trimStart();
|
|
207
|
+
|
|
208
|
+
if (!stripped || stripped.startsWith('#')) continue;
|
|
209
|
+
|
|
210
|
+
const indent = line.length - stripped.length;
|
|
211
|
+
|
|
212
|
+
// List item start (- key: value)
|
|
213
|
+
if (stripped.startsWith('- ') && currentList) {
|
|
214
|
+
if (currentItem) result[currentList].push(currentItem);
|
|
215
|
+
currentItem = {};
|
|
216
|
+
const rest = stripped.slice(2).trim();
|
|
217
|
+
if (rest.includes(':')) {
|
|
218
|
+
const [key, ...valParts] = rest.split(':');
|
|
219
|
+
currentItem[key.trim()] = parseYamlValue(valParts.join(':').trim());
|
|
220
|
+
}
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Top-level keys
|
|
225
|
+
if (indent === 0 && stripped.includes(':') && !stripped.startsWith('- ')) {
|
|
226
|
+
if (currentItem && currentList) {
|
|
227
|
+
result[currentList].push(currentItem);
|
|
228
|
+
currentItem = null;
|
|
229
|
+
}
|
|
230
|
+
const [key, ...rest] = stripped.split(':');
|
|
231
|
+
const val = rest.join(':').trim();
|
|
232
|
+
if (key === 'version') {
|
|
233
|
+
result.version = parseInt(val, 10) || 2;
|
|
234
|
+
currentList = null;
|
|
235
|
+
} else if (key === 'agents') {
|
|
236
|
+
currentList = 'agents';
|
|
237
|
+
} else if (key === 'networks') {
|
|
238
|
+
currentList = 'networks';
|
|
239
|
+
} else {
|
|
240
|
+
currentList = null;
|
|
241
|
+
}
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Continuation of current item (indented key: value)
|
|
246
|
+
if (currentItem && indent >= 2 && stripped.includes(':')) {
|
|
247
|
+
const [key, ...valParts] = stripped.split(':');
|
|
248
|
+
currentItem[key.trim()] = parseYamlValue(valParts.join(':').trim());
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (currentItem && currentList) result[currentList].push(currentItem);
|
|
253
|
+
return result;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function parseYamlValue(val) {
|
|
257
|
+
if (val === '' || val === 'null' || val === '~') return null;
|
|
258
|
+
if (val === 'true') return true;
|
|
259
|
+
if (val === 'false') return false;
|
|
260
|
+
if (/^\d+$/.test(val)) return parseInt(val, 10);
|
|
261
|
+
if ((val.startsWith("'") && val.endsWith("'")) ||
|
|
262
|
+
(val.startsWith('"') && val.endsWith('"'))) {
|
|
263
|
+
return val.slice(1, -1);
|
|
264
|
+
}
|
|
265
|
+
if (val.startsWith('{') || val.startsWith('[')) {
|
|
266
|
+
try { return JSON.parse(val); } catch { return val; }
|
|
267
|
+
}
|
|
268
|
+
return val;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// -- YAML serializer --
|
|
272
|
+
|
|
273
|
+
function serializeYaml(config) {
|
|
274
|
+
const lines = [`version: ${config.version || 2}`];
|
|
275
|
+
|
|
276
|
+
lines.push('agents:');
|
|
277
|
+
for (const agent of (config.agents || [])) {
|
|
278
|
+
const keys = Object.keys(agent);
|
|
279
|
+
if (keys.length === 0) continue;
|
|
280
|
+
lines.push(`- name: ${serializeYamlValue(agent.name)}`);
|
|
281
|
+
for (const key of keys) {
|
|
282
|
+
if (key === 'name') continue;
|
|
283
|
+
lines.push(` ${key}: ${serializeYamlValue(agent[key])}`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (!config.agents || config.agents.length === 0) {
|
|
287
|
+
lines[lines.length - 1] += ' []';
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
lines.push('networks:');
|
|
291
|
+
for (const net of (config.networks || [])) {
|
|
292
|
+
const keys = Object.keys(net);
|
|
293
|
+
if (keys.length === 0) continue;
|
|
294
|
+
const firstKey = keys[0];
|
|
295
|
+
lines.push(`- ${firstKey}: ${serializeYamlValue(net[firstKey])}`);
|
|
296
|
+
for (const key of keys.slice(1)) {
|
|
297
|
+
lines.push(` ${key}: ${serializeYamlValue(net[key])}`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (!config.networks || config.networks.length === 0) {
|
|
301
|
+
lines[lines.length - 1] += ' []';
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return lines.join('\n') + '\n';
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function serializeYamlValue(val) {
|
|
308
|
+
if (val === null || val === undefined) return 'null';
|
|
309
|
+
if (typeof val === 'boolean') return val ? 'true' : 'false';
|
|
310
|
+
if (typeof val === 'number') return String(val);
|
|
311
|
+
if (typeof val === 'object') return JSON.stringify(val);
|
|
312
|
+
const s = String(val);
|
|
313
|
+
// Quote strings that contain special YAML chars
|
|
314
|
+
if (s.includes(':') || s.includes('#') || s.includes("'") || s.includes('"') ||
|
|
315
|
+
s.includes('\n') || s.startsWith(' ') || s.endsWith(' ') ||
|
|
316
|
+
s === 'true' || s === 'false' || s === 'null' || /^\d+$/.test(s)) {
|
|
317
|
+
return `"${s.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
318
|
+
}
|
|
319
|
+
return s;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
module.exports = { Config, parseYaml, serializeYaml };
|