serpentstack 0.2.15 → 0.2.16
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/lib/commands/persistent.js +134 -36
- package/lib/utils/ui.js +1 -1
- package/package.json +1 -1
|
@@ -33,6 +33,19 @@ function which(cmd) {
|
|
|
33
33
|
});
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
function execPromise(cmd, args) {
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
execFile(cmd, args, { timeout: 15000 }, (err, stdout, stderr) => {
|
|
39
|
+
if (err) {
|
|
40
|
+
const msg = stderr?.trim() || stdout?.trim() || err.message;
|
|
41
|
+
reject(new Error(msg));
|
|
42
|
+
} else {
|
|
43
|
+
resolve(stdout);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
36
49
|
async function ask(rl, label, defaultValue) {
|
|
37
50
|
const hint = defaultValue ? ` ${dim(`[${defaultValue}]`)}` : '';
|
|
38
51
|
const answer = await rl.question(` ${green('?')} ${bold(label)}${hint}: `);
|
|
@@ -425,7 +438,7 @@ end tell`;
|
|
|
425
438
|
|
|
426
439
|
// ─── Stop Flow ──────────────────────────────────────────────
|
|
427
440
|
|
|
428
|
-
function stopAllAgents(projectDir) {
|
|
441
|
+
async function stopAllAgents(projectDir) {
|
|
429
442
|
cleanStalePids(projectDir);
|
|
430
443
|
const running = listPids(projectDir);
|
|
431
444
|
|
|
@@ -437,19 +450,31 @@ function stopAllAgents(projectDir) {
|
|
|
437
450
|
|
|
438
451
|
let stopped = 0;
|
|
439
452
|
for (const { name, pid } of running) {
|
|
453
|
+
// Remove cron jobs for this agent
|
|
440
454
|
try {
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
455
|
+
await execPromise('openclaw', ['cron', 'list', '--json']).then(out => {
|
|
456
|
+
const jobs = JSON.parse(out);
|
|
457
|
+
const agentJobs = (Array.isArray(jobs) ? jobs : jobs.jobs || [])
|
|
458
|
+
.filter(j => j.agent === name || (j.name && j.name.startsWith(`${name}-`)));
|
|
459
|
+
return Promise.all(agentJobs.map(j =>
|
|
460
|
+
execPromise('openclaw', ['cron', 'rm', j.id || j.name]).catch(() => {})
|
|
461
|
+
));
|
|
462
|
+
});
|
|
463
|
+
} catch { /* cron cleanup is best-effort */ }
|
|
464
|
+
|
|
465
|
+
// Remove agent from OpenClaw
|
|
466
|
+
try {
|
|
467
|
+
await execPromise('openclaw', ['agents', 'delete', name, '--force']);
|
|
468
|
+
} catch { /* best-effort */ }
|
|
469
|
+
|
|
470
|
+
// Clean up PID and workspace
|
|
471
|
+
if (pid > 0) {
|
|
472
|
+
try { process.kill(pid, 'SIGTERM'); } catch { /* already dead */ }
|
|
452
473
|
}
|
|
474
|
+
removePid(projectDir, name);
|
|
475
|
+
cleanWorkspace(projectDir, name);
|
|
476
|
+
success(`Stopped ${bold(name)}`);
|
|
477
|
+
stopped++;
|
|
453
478
|
}
|
|
454
479
|
|
|
455
480
|
if (stopped > 0) {
|
|
@@ -752,8 +777,9 @@ async function runStart(projectDir, parsed, config, soulPath, hasOpenClaw) {
|
|
|
752
777
|
console.log();
|
|
753
778
|
|
|
754
779
|
const sharedSoul = readFileSync(soulPath, 'utf8');
|
|
755
|
-
let
|
|
780
|
+
let registered = 0;
|
|
756
781
|
|
|
782
|
+
// Step 1: Generate workspaces and register agents with OpenClaw
|
|
757
783
|
for (const { name, agentMd } of toStart) {
|
|
758
784
|
try {
|
|
759
785
|
const effectiveModel = getEffectiveModel(name, agentMd.meta, config);
|
|
@@ -764,38 +790,110 @@ async function runStart(projectDir, parsed, config, soulPath, hasOpenClaw) {
|
|
|
764
790
|
|
|
765
791
|
const workspacePath = generateWorkspace(projectDir, name, overriddenMd, sharedSoul);
|
|
766
792
|
const absWorkspace = resolve(workspacePath);
|
|
767
|
-
const absProject = resolve(projectDir);
|
|
768
793
|
|
|
769
|
-
|
|
770
|
-
|
|
794
|
+
// Register agent with OpenClaw (idempotent — will update if exists)
|
|
795
|
+
try {
|
|
796
|
+
await execPromise('openclaw', [
|
|
797
|
+
'agents', 'add', name,
|
|
798
|
+
'--workspace', absWorkspace,
|
|
799
|
+
'--model', effectiveModel,
|
|
800
|
+
'--non-interactive',
|
|
801
|
+
]);
|
|
802
|
+
success(`${green('✓')} Registered ${bold(name)} ${dim(`(${modelShortName(effectiveModel)})`)}`);
|
|
803
|
+
} catch (err) {
|
|
804
|
+
// Agent may already exist — that's fine
|
|
805
|
+
if (err.message && err.message.includes('already exists')) {
|
|
806
|
+
info(`${bold(name)} already registered with OpenClaw`);
|
|
807
|
+
} else {
|
|
808
|
+
warn(`Could not register ${bold(name)}: ${err.message || 'unknown error'}`);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
771
811
|
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
started++;
|
|
812
|
+
// Add cron jobs for the agent's schedule
|
|
813
|
+
const schedules = agentMd.meta.schedule || [];
|
|
814
|
+
for (const sched of schedules) {
|
|
815
|
+
try {
|
|
816
|
+
await execPromise('openclaw', [
|
|
817
|
+
'cron', 'add',
|
|
818
|
+
'--agent', name,
|
|
819
|
+
'--every', sched.every,
|
|
820
|
+
'--message', `Run task: ${sched.task}`,
|
|
821
|
+
'--name', `${name}-${sched.task}`,
|
|
822
|
+
'--light-context',
|
|
823
|
+
]);
|
|
824
|
+
} catch {
|
|
825
|
+
// Cron job may already exist — non-fatal
|
|
826
|
+
}
|
|
788
827
|
}
|
|
828
|
+
|
|
829
|
+
writePid(projectDir, name, -1); // marker
|
|
830
|
+
registered++;
|
|
789
831
|
} catch (err) {
|
|
790
832
|
error(`${bold(name)}: ${err.message}`);
|
|
791
833
|
}
|
|
792
834
|
}
|
|
793
835
|
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
836
|
+
if (registered === 0) {
|
|
837
|
+
console.log();
|
|
838
|
+
error('No agents were registered.');
|
|
797
839
|
console.log();
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
console.log();
|
|
844
|
+
|
|
845
|
+
// Step 2: Check if gateway is running, start it if not
|
|
846
|
+
let gatewayRunning = false;
|
|
847
|
+
try {
|
|
848
|
+
const healthResp = await fetch('http://127.0.0.1:18789/health');
|
|
849
|
+
gatewayRunning = healthResp.ok;
|
|
850
|
+
} catch {
|
|
851
|
+
// not running
|
|
798
852
|
}
|
|
853
|
+
|
|
854
|
+
if (!gatewayRunning) {
|
|
855
|
+
info('Starting OpenClaw gateway...');
|
|
856
|
+
|
|
857
|
+
const method = openInTerminal(
|
|
858
|
+
'OpenClaw Gateway',
|
|
859
|
+
'openclaw gateway',
|
|
860
|
+
resolve(projectDir),
|
|
861
|
+
);
|
|
862
|
+
|
|
863
|
+
if (method) {
|
|
864
|
+
success(`Gateway opened in ${method}`);
|
|
865
|
+
} else {
|
|
866
|
+
// Fallback: start in background
|
|
867
|
+
const child = spawn('openclaw', ['gateway'], {
|
|
868
|
+
stdio: 'ignore',
|
|
869
|
+
detached: true,
|
|
870
|
+
cwd: resolve(projectDir),
|
|
871
|
+
});
|
|
872
|
+
child.unref();
|
|
873
|
+
success(`Gateway started in background ${dim(`(PID ${child.pid})`)}`);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// Give gateway a moment to start
|
|
877
|
+
console.log(` ${dim('Waiting for gateway...')}`);
|
|
878
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
879
|
+
} else {
|
|
880
|
+
success('Gateway is already running');
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
console.log();
|
|
884
|
+
success(`${registered} agent(s) registered — fangs out 🐍`);
|
|
885
|
+
console.log();
|
|
886
|
+
|
|
887
|
+
printBox('Your agents are running', [
|
|
888
|
+
`${dim('The OpenClaw gateway manages your agents on their schedules.')}`,
|
|
889
|
+
`${dim('View agent activity with:')}`,
|
|
890
|
+
'',
|
|
891
|
+
`${dim('$')} ${bold('openclaw tui')} ${dim('# interactive terminal UI')}`,
|
|
892
|
+
`${dim('$')} ${bold('openclaw cron list')} ${dim('# see scheduled tasks')}`,
|
|
893
|
+
`${dim('$')} ${bold('openclaw agents list')} ${dim('# see registered agents')}`,
|
|
894
|
+
`${dim('$')} ${bold('serpentstack persistent --stop')} ${dim('# stop all agents')}`,
|
|
895
|
+
]);
|
|
896
|
+
console.log();
|
|
799
897
|
}
|
|
800
898
|
|
|
801
899
|
// ─── Main Entry Point ───────────────────────────────────────
|
|
@@ -808,7 +906,7 @@ export async function persistent({ stop = false, configure = false, agents = fal
|
|
|
808
906
|
// ── Stop (doesn't need full preflight) ──
|
|
809
907
|
if (stop) {
|
|
810
908
|
cleanStalePids(projectDir);
|
|
811
|
-
stopAllAgents(projectDir);
|
|
909
|
+
await stopAllAgents(projectDir);
|
|
812
910
|
return;
|
|
813
911
|
}
|
|
814
912
|
|
package/lib/utils/ui.js
CHANGED
|
@@ -94,7 +94,7 @@ export function printHeader() {
|
|
|
94
94
|
|
|
95
95
|
export function divider(label) {
|
|
96
96
|
if (label) {
|
|
97
|
-
console.log(` ${DIM}
|
|
97
|
+
console.log(` ${DIM}──${RESET} ${GREEN}${BOLD}${label}${RESET} ${DIM}${'─'.repeat(Math.max(0, 50 - stripAnsi(label).length))}${RESET}`);
|
|
98
98
|
} else {
|
|
99
99
|
console.log(` ${DIM}${'─'.repeat(54)}${RESET}`);
|
|
100
100
|
}
|