@love-moon/conductor-cli 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/bin/conductor-chrome.js +376 -0
- package/bin/conductor-config.js +82 -0
- package/bin/conductor-daemon.js +67 -0
- package/bin/conductor-fire.js +903 -0
- package/package.json +34 -0
- package/src/daemon.js +376 -0
- package/src/fire/history.js +605 -0
- package/src/pageAutomation.js +131 -0
- package/src/providers/deepseek.js +405 -0
- package/src/providers/generic.js +6 -0
- package/src/providers/qwen.js +203 -0
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
import { promises as fs } from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import net from 'node:net';
|
|
7
|
+
import yaml from 'js-yaml';
|
|
8
|
+
import yargs from 'yargs';
|
|
9
|
+
import { hideBin } from 'yargs/helpers';
|
|
10
|
+
import { launch as launchChrome } from 'chrome-launcher';
|
|
11
|
+
import CDP from 'chrome-remote-interface';
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
const BACKEND_URLS = {
|
|
15
|
+
gemini: 'https://aistudio.google.com/prompts/new_chat?model=gemini-3-pro-preview',
|
|
16
|
+
chatgpt: 'https://chat.openai.com',
|
|
17
|
+
claude: 'https://claude.ai',
|
|
18
|
+
grok: 'https://grok.com',
|
|
19
|
+
deepseek: 'https://chat.deepseek.com',
|
|
20
|
+
qwen: 'https://chat.qwen.ai'
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const argv = yargs(hideBin(process.argv))
|
|
24
|
+
.option('backend', {
|
|
25
|
+
type: 'string',
|
|
26
|
+
choices: Object.keys(BACKEND_URLS),
|
|
27
|
+
default: 'deepseek',
|
|
28
|
+
describe: 'Target backend tab to open',
|
|
29
|
+
})
|
|
30
|
+
.option('action', {
|
|
31
|
+
type: 'string',
|
|
32
|
+
choices: ['create_task', 'send_message', 'receive_message'],
|
|
33
|
+
describe: 'Run a page automation helper once after the tab loads',
|
|
34
|
+
})
|
|
35
|
+
.option('launch-browser', {
|
|
36
|
+
type: 'boolean',
|
|
37
|
+
default: false,
|
|
38
|
+
describe: 'Only launch Chrome; skip opening tabs or running automation',
|
|
39
|
+
})
|
|
40
|
+
.option('message', {
|
|
41
|
+
type: 'string',
|
|
42
|
+
describe: 'Text to send when --action=send_message',
|
|
43
|
+
})
|
|
44
|
+
.parseSync();
|
|
45
|
+
|
|
46
|
+
const TARGET_URL = BACKEND_URLS[argv.backend];
|
|
47
|
+
const CACHE_DIR = path.join(homedir(), '.cache', 'conductor');
|
|
48
|
+
const PORT_STORE = path.join(CACHE_DIR, '.chrome-port');
|
|
49
|
+
|
|
50
|
+
const AUTOMATION_SRC_ROOT = new URL('../src/', import.meta.url);
|
|
51
|
+
const PAGE_AUTOMATION_PATH = new URL('pageAutomation.js', AUTOMATION_SRC_ROOT);
|
|
52
|
+
const PROVIDERS_DIR = new URL('providers/', AUTOMATION_SRC_ROOT);
|
|
53
|
+
let automationScriptCache;
|
|
54
|
+
|
|
55
|
+
const CONFIG_PATH = path.join(homedir(), '.conductor', 'config.yaml');
|
|
56
|
+
|
|
57
|
+
function expandHomeDir(maybePath) {
|
|
58
|
+
if (!maybePath) {
|
|
59
|
+
return maybePath;
|
|
60
|
+
}
|
|
61
|
+
const home = homedir();
|
|
62
|
+
return maybePath
|
|
63
|
+
.replace(/^~(?=$|\/)/, home)
|
|
64
|
+
.replace(/\$HOME/g, home)
|
|
65
|
+
.replace(/\${HOME}/g, home);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let cachedUserConfig;
|
|
69
|
+
async function loadUserConfig() {
|
|
70
|
+
if (cachedUserConfig !== undefined) {
|
|
71
|
+
return cachedUserConfig;
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
const raw = await fs.readFile(CONFIG_PATH, 'utf-8');
|
|
75
|
+
const parsed = yaml.load(raw);
|
|
76
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
77
|
+
cachedUserConfig = parsed;
|
|
78
|
+
} else {
|
|
79
|
+
cachedUserConfig = {};
|
|
80
|
+
}
|
|
81
|
+
} catch (error) {
|
|
82
|
+
if (error.code !== 'ENOENT') {
|
|
83
|
+
console.warn('Failed to read ~/.conductor/config.yaml:', error);
|
|
84
|
+
}
|
|
85
|
+
cachedUserConfig = {};
|
|
86
|
+
}
|
|
87
|
+
return cachedUserConfig;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function pickConfiguredUserDataDir(config) {
|
|
91
|
+
if (typeof config?.cdp_user_data_dir === 'string' && config.cdp_user_data_dir.trim()) {
|
|
92
|
+
return config.cdp_user_data_dir;
|
|
93
|
+
}
|
|
94
|
+
if (typeof config?.cdpUserDataDir === 'string' && config.cdpUserDataDir.trim()) {
|
|
95
|
+
return config.cdpUserDataDir;
|
|
96
|
+
}
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function resolveUserDataDir() {
|
|
101
|
+
const envValue = expandHomeDir(process.env.CDP_USER_DATA_DIR);
|
|
102
|
+
if (envValue) {
|
|
103
|
+
return envValue;
|
|
104
|
+
}
|
|
105
|
+
const config = await loadUserConfig();
|
|
106
|
+
const configValue = pickConfiguredUserDataDir(config);
|
|
107
|
+
return expandHomeDir(configValue);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function readStoredPort() {
|
|
111
|
+
try {
|
|
112
|
+
const raw = await fs.readFile(PORT_STORE, 'utf-8');
|
|
113
|
+
const port = Number(raw.trim());
|
|
114
|
+
if (Number.isFinite(port) && port > 0) {
|
|
115
|
+
return port;
|
|
116
|
+
}
|
|
117
|
+
} catch (error) {
|
|
118
|
+
if (error.code !== 'ENOENT') {
|
|
119
|
+
console.warn('Failed to read cached Chrome port:', error);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function isPortListening(port) {
|
|
126
|
+
return new Promise((resolve) => {
|
|
127
|
+
const client = net.createConnection({ port, host: '127.0.0.1' });
|
|
128
|
+
const cleanup = () => client.destroy();
|
|
129
|
+
client.once('connect', () => {
|
|
130
|
+
cleanup();
|
|
131
|
+
resolve(true);
|
|
132
|
+
});
|
|
133
|
+
client.once('error', () => {
|
|
134
|
+
cleanup();
|
|
135
|
+
resolve(false);
|
|
136
|
+
});
|
|
137
|
+
setTimeout(() => {
|
|
138
|
+
cleanup();
|
|
139
|
+
resolve(false);
|
|
140
|
+
}, 200);
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function storePort(port) {
|
|
145
|
+
await fs.mkdir(CACHE_DIR, { recursive: true });
|
|
146
|
+
await fs.writeFile(PORT_STORE, String(port), 'utf-8');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function clearPortStore() {
|
|
150
|
+
await fs.unlink(PORT_STORE).catch((error) => {
|
|
151
|
+
if (error.code !== 'ENOENT') {
|
|
152
|
+
console.warn('Failed to remove cached Chrome port:', error);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function tryReusePort() {
|
|
158
|
+
const storedPort = await readStoredPort();
|
|
159
|
+
if (!storedPort) {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
if (await isPortListening(storedPort)) {
|
|
163
|
+
return storedPort;
|
|
164
|
+
}
|
|
165
|
+
await clearPortStore();
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
let chrome;
|
|
170
|
+
let browserClient;
|
|
171
|
+
let tabClient;
|
|
172
|
+
let exitResolve;
|
|
173
|
+
let shuttingDown = false;
|
|
174
|
+
let ownsChromeInstance = false;
|
|
175
|
+
|
|
176
|
+
const exitPromise = new Promise((resolve) => {
|
|
177
|
+
exitResolve = resolve;
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
async function launchBrowser() {
|
|
181
|
+
if (chrome && chrome.process && chrome.process.exitCode === null) {
|
|
182
|
+
console.log(`Reusing existing Chrome instance on port ${chrome.port}`);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const reusedPort = await tryReusePort();
|
|
186
|
+
if (reusedPort) {
|
|
187
|
+
console.log(`Reusing existing Chrome process on port ${reusedPort}`);
|
|
188
|
+
chrome = {
|
|
189
|
+
port: reusedPort,
|
|
190
|
+
process: null,
|
|
191
|
+
kill: async () => {
|
|
192
|
+
console.log('Leaving reused Chrome running.');
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
ownsChromeInstance = false;
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const launchOptions = {
|
|
199
|
+
startingUrl: 'about:blank',
|
|
200
|
+
chromeFlags: [
|
|
201
|
+
'--disable-gpu',
|
|
202
|
+
'--no-first-run',
|
|
203
|
+
'--no-default-browser-check',
|
|
204
|
+
'--disable-popup-blocking',
|
|
205
|
+
],
|
|
206
|
+
port: 0
|
|
207
|
+
};
|
|
208
|
+
const userDataDir = await resolveUserDataDir();
|
|
209
|
+
if (userDataDir) {
|
|
210
|
+
launchOptions.userDataDir = userDataDir;
|
|
211
|
+
console.log(`Launching Chrome with user data directory ${userDataDir}`);
|
|
212
|
+
}
|
|
213
|
+
chrome = await launchChrome(launchOptions);
|
|
214
|
+
ownsChromeInstance = true;
|
|
215
|
+
await storePort(chrome.port);
|
|
216
|
+
console.log(`Chrome launched with remote debugging port ${chrome.port}`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function createTab(url) {
|
|
220
|
+
browserClient = await CDP({ port: chrome.port });
|
|
221
|
+
const { Target } = browserClient;
|
|
222
|
+
const { targetId } = await Target.createTarget({ url: url });
|
|
223
|
+
tabClient = await CDP({ port: chrome.port, target: targetId });
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function navigateTab(url) {
|
|
227
|
+
const { Page, Runtime } = tabClient;
|
|
228
|
+
await Page.enable();
|
|
229
|
+
await Runtime.enable();
|
|
230
|
+
await installAutomationScript(Page);
|
|
231
|
+
await Page.navigate({ url: url });
|
|
232
|
+
await Page.loadEventFired();
|
|
233
|
+
console.log(`Opened new tab that navigated to ${url}`);
|
|
234
|
+
return Runtime;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function installAutomationScript(Page) {
|
|
238
|
+
const script = await getAutomationScript();
|
|
239
|
+
await Page.addScriptToEvaluateOnNewDocument({ source: script });
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function getAutomationScript() {
|
|
243
|
+
if (automationScriptCache) {
|
|
244
|
+
return automationScriptCache;
|
|
245
|
+
}
|
|
246
|
+
automationScriptCache = await buildAutomationScript();
|
|
247
|
+
return automationScriptCache;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async function buildAutomationScript() {
|
|
251
|
+
const deepseek = await fs.readFile(new URL('deepseek.js', PROVIDERS_DIR), 'utf-8');
|
|
252
|
+
const generic = await fs.readFile(new URL('generic.js', PROVIDERS_DIR), 'utf-8');
|
|
253
|
+
const qwen = await fs.readFile(new URL('qwen.js', PROVIDERS_DIR), 'utf-8');
|
|
254
|
+
const automation = await fs.readFile(PAGE_AUTOMATION_PATH, 'utf-8');
|
|
255
|
+
|
|
256
|
+
const deepseekClean = deepseek.replace('export default function createDeepseekProvider', 'function createDeepseekProvider');
|
|
257
|
+
const genericClean = generic
|
|
258
|
+
.replace(/^import .*?;\s*\n/, '')
|
|
259
|
+
.replace(/deepseekProvider/g, 'createDeepseekProvider')
|
|
260
|
+
.replace('export default function createGenericProvider', 'function createGenericProvider');
|
|
261
|
+
const qwenClean = qwen
|
|
262
|
+
.replace(/^import .*?;\s*\n/, '')
|
|
263
|
+
.replace(/deepseekProvider/g, 'createDeepseekProvider')
|
|
264
|
+
.replace('export default function createQwenProvider', 'function createQwenProvider');
|
|
265
|
+
const automationClean = automation
|
|
266
|
+
.replace(/import .*?;\s*\n/g, '')
|
|
267
|
+
.replace(/export function ([a-zA-Z0-9_]+)/g, 'function $1');
|
|
268
|
+
|
|
269
|
+
return `
|
|
270
|
+
(() => {
|
|
271
|
+
${deepseekClean}
|
|
272
|
+
|
|
273
|
+
${genericClean}
|
|
274
|
+
|
|
275
|
+
${qwenClean}
|
|
276
|
+
|
|
277
|
+
${automationClean}
|
|
278
|
+
|
|
279
|
+
if (typeof window === 'undefined') {
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
window.__conductorAutomation = {
|
|
284
|
+
create_task,
|
|
285
|
+
send_message,
|
|
286
|
+
receive_message,
|
|
287
|
+
highlightDetectedElements,
|
|
288
|
+
};
|
|
289
|
+
})();
|
|
290
|
+
`;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async function runAutomationAction(Runtime, action, message) {
|
|
294
|
+
if (!Runtime) {
|
|
295
|
+
return { ok: false, message: 'Runtime channel unavailable' };
|
|
296
|
+
}
|
|
297
|
+
const argument = action === 'send_message' ? JSON.stringify(message ?? '') : '';
|
|
298
|
+
const callExpression =
|
|
299
|
+
action === 'send_message' ? `automation.send_message(${argument})` : `automation.${action}()`;
|
|
300
|
+
const expression = `(async () => {
|
|
301
|
+
const automation = window.__conductorAutomation;
|
|
302
|
+
if (!automation || typeof automation.${action} !== 'function') {
|
|
303
|
+
return { ok: false, message: 'Page automation unavailable' };
|
|
304
|
+
}
|
|
305
|
+
return ${callExpression};
|
|
306
|
+
})();`;
|
|
307
|
+
|
|
308
|
+
const { result } = await Runtime.evaluate({ expression, awaitPromise: true, returnByValue: true });
|
|
309
|
+
return result?.value;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async function shutdown() {
|
|
313
|
+
if (shuttingDown) return;
|
|
314
|
+
shuttingDown = true;
|
|
315
|
+
console.log('Cleaning up Chrome connection...');
|
|
316
|
+
const closePromises = [];
|
|
317
|
+
if (tabClient) {
|
|
318
|
+
closePromises.push(tabClient.close());
|
|
319
|
+
tabClient = null;
|
|
320
|
+
}
|
|
321
|
+
if (browserClient) {
|
|
322
|
+
closePromises.push(browserClient.close());
|
|
323
|
+
browserClient = null;
|
|
324
|
+
}
|
|
325
|
+
if (chrome) {
|
|
326
|
+
if (ownsChromeInstance && typeof chrome.kill === 'function') {
|
|
327
|
+
closePromises.push((async () => {
|
|
328
|
+
await chrome.kill();
|
|
329
|
+
await clearPortStore();
|
|
330
|
+
})());
|
|
331
|
+
} else {
|
|
332
|
+
console.log('Leaving reused Chrome running.');
|
|
333
|
+
}
|
|
334
|
+
chrome = null;
|
|
335
|
+
ownsChromeInstance = false;
|
|
336
|
+
}
|
|
337
|
+
await Promise.allSettled(closePromises);
|
|
338
|
+
process.stdin.pause();
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
async function handleSignal() {
|
|
342
|
+
await shutdown();
|
|
343
|
+
exitResolve?.();
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
process.on('SIGINT', handleSignal);
|
|
347
|
+
process.on('SIGTERM', handleSignal);
|
|
348
|
+
|
|
349
|
+
async function main() {
|
|
350
|
+
try {
|
|
351
|
+
await launchBrowser();
|
|
352
|
+
if (argv.launchBrowser) {
|
|
353
|
+
console.log('Chrome launched with --launch-browser; no tabs will be opened.');
|
|
354
|
+
console.log('Press Ctrl+C to close Chrome and exit.');
|
|
355
|
+
process.stdin.resume();
|
|
356
|
+
await exitPromise;
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
await createTab(TARGET_URL);
|
|
360
|
+
const runtime = await navigateTab(TARGET_URL);
|
|
361
|
+
if (argv.action) {
|
|
362
|
+
const message = argv.action === 'send_message' ? argv.message ?? '' : undefined;
|
|
363
|
+
const result = await runAutomationAction(runtime, argv.action, message);
|
|
364
|
+
console.log(`Automation "${argv.action}" result:`, result);
|
|
365
|
+
}
|
|
366
|
+
console.log('Press Ctrl+C to close Chrome and exit.');
|
|
367
|
+
process.stdin.resume();
|
|
368
|
+
await exitPromise;
|
|
369
|
+
} catch (error) {
|
|
370
|
+
console.error('Failed to open tab via CDP:', error);
|
|
371
|
+
await shutdown();
|
|
372
|
+
process.exit(1);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
main();
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import process from "node:process";
|
|
7
|
+
import readline from "node:readline/promises";
|
|
8
|
+
|
|
9
|
+
const CONFIG_DIR = path.join(os.homedir(), ".conductor");
|
|
10
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, "config.yaml");
|
|
11
|
+
|
|
12
|
+
const backendUrl =
|
|
13
|
+
process.env.CONDUCTOR_BACKEND_URL ||
|
|
14
|
+
process.env.BACKEND_URL ||
|
|
15
|
+
"http://localhost:6152";
|
|
16
|
+
|
|
17
|
+
const websocketUrl =
|
|
18
|
+
process.env.CONDUCTOR_WS_URL ||
|
|
19
|
+
process.env.PUBLIC_WS_URL ||
|
|
20
|
+
deriveWebsocketUrl(backendUrl);
|
|
21
|
+
|
|
22
|
+
async function main() {
|
|
23
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
24
|
+
process.stderr.write(`Config already exists at ${CONFIG_FILE}. Remove it to recreate.\n`);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const token = await promptForToken();
|
|
29
|
+
if (!token) {
|
|
30
|
+
process.stderr.write("No token provided. Aborting.\n");
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
35
|
+
|
|
36
|
+
const lines = [
|
|
37
|
+
`agent_token: ${yamlQuote(token)}`,
|
|
38
|
+
`backend_url: ${yamlQuote(backendUrl)}`,
|
|
39
|
+
];
|
|
40
|
+
if (websocketUrl) {
|
|
41
|
+
lines.push(`websocket_url: ${yamlQuote(websocketUrl)}`);
|
|
42
|
+
}
|
|
43
|
+
lines.push("log_level: info", "");
|
|
44
|
+
|
|
45
|
+
fs.writeFileSync(CONFIG_FILE, lines.join("\n"), "utf-8");
|
|
46
|
+
process.stdout.write(`Wrote Conductor config to ${CONFIG_FILE}\n`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function promptForToken() {
|
|
50
|
+
const rl = readline.createInterface({
|
|
51
|
+
input: process.stdin,
|
|
52
|
+
output: process.stdout,
|
|
53
|
+
});
|
|
54
|
+
try {
|
|
55
|
+
let token = "";
|
|
56
|
+
while (!token) {
|
|
57
|
+
token = (await rl.question("Enter Conductor agent token: ")).trim();
|
|
58
|
+
}
|
|
59
|
+
return token;
|
|
60
|
+
} finally {
|
|
61
|
+
rl.close();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function deriveWebsocketUrl(url) {
|
|
66
|
+
try {
|
|
67
|
+
const parsed = new URL(url);
|
|
68
|
+
const scheme = parsed.protocol === "https:" ? "wss" : "ws";
|
|
69
|
+
return `${scheme}://${parsed.host}/ws/agent`;
|
|
70
|
+
} catch {
|
|
71
|
+
return "";
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function yamlQuote(value) {
|
|
76
|
+
return JSON.stringify(value);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
main().catch((error) => {
|
|
80
|
+
process.stderr.write(`Failed to write config: ${error?.message || error}\n`);
|
|
81
|
+
process.exit(1);
|
|
82
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import { spawn } from "node:child_process";
|
|
7
|
+
import yargs from "yargs/yargs";
|
|
8
|
+
import { hideBin } from "yargs/helpers";
|
|
9
|
+
|
|
10
|
+
import { startDaemon } from "../src/daemon.js";
|
|
11
|
+
|
|
12
|
+
const argv = hideBin(process.argv);
|
|
13
|
+
|
|
14
|
+
const args = yargs(argv)
|
|
15
|
+
.scriptName("conductor-daemon")
|
|
16
|
+
.usage("Usage: $0 [--name <daemon-name>] [--clean-all] [--config-file <path>] [--nohup]")
|
|
17
|
+
.option("name", {
|
|
18
|
+
alias: "n",
|
|
19
|
+
type: "string",
|
|
20
|
+
demandOption: false,
|
|
21
|
+
describe: "Unique daemon name (used as agent host)",
|
|
22
|
+
})
|
|
23
|
+
.option("nohup", {
|
|
24
|
+
type: "boolean",
|
|
25
|
+
default: false,
|
|
26
|
+
describe: "Run in background and write logs to ~/.conductor/logs/<timestamp>.log",
|
|
27
|
+
})
|
|
28
|
+
.option("clean-all", {
|
|
29
|
+
type: "boolean",
|
|
30
|
+
default: false,
|
|
31
|
+
describe: "Prune all stale daemon instances on backend before starting",
|
|
32
|
+
})
|
|
33
|
+
.option("config-file", {
|
|
34
|
+
type: "string",
|
|
35
|
+
describe: "Path to Conductor config file",
|
|
36
|
+
})
|
|
37
|
+
.example(
|
|
38
|
+
"$0 --config-file ~/.conductor/config.yaml --name agent-1",
|
|
39
|
+
"Use custom config file and daemon name",
|
|
40
|
+
)
|
|
41
|
+
.example("$0 --nohup", "Run daemon in background with logfile")
|
|
42
|
+
.help()
|
|
43
|
+
.strict()
|
|
44
|
+
.parse();
|
|
45
|
+
|
|
46
|
+
if (args.nohup) {
|
|
47
|
+
const logsDir = path.join(os.homedir(), ".conductor", "logs");
|
|
48
|
+
fs.mkdirSync(logsDir, { recursive: true });
|
|
49
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
50
|
+
const logPath = path.join(logsDir, `${timestamp}.log`);
|
|
51
|
+
const filteredArgv = argv.filter((arg) => !(arg === "--nohup" || arg.startsWith("--nohup=")));
|
|
52
|
+
const logFd = fs.openSync(logPath, "a");
|
|
53
|
+
const child = spawn(process.execPath, [path.resolve(process.argv[1]), ...filteredArgv], {
|
|
54
|
+
detached: true,
|
|
55
|
+
stdio: ["ignore", logFd, logFd],
|
|
56
|
+
env: { ...process.env },
|
|
57
|
+
});
|
|
58
|
+
child.unref();
|
|
59
|
+
process.stdout.write(`conductor-daemon running in background. Logs: ${logPath}\n`);
|
|
60
|
+
process.exit(0);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
startDaemon({
|
|
64
|
+
NAME: args.name,
|
|
65
|
+
CLEAN_ALL: args.cleanAll,
|
|
66
|
+
CONFIG_FILE: args.configFile,
|
|
67
|
+
});
|