serpentstack 0.2.15 → 0.2.18
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 +140 -39
- package/lib/utils/agent-utils.js +3 -0
- 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}: `);
|
|
@@ -302,10 +315,12 @@ async function pickModel(rl, agentName, currentModel, available) {
|
|
|
302
315
|
return currentModel;
|
|
303
316
|
}
|
|
304
317
|
|
|
305
|
-
// If current model isn't in any list,
|
|
318
|
+
// If current model isn't in any list, append it at the end (never unshift — it breaks numbering)
|
|
306
319
|
if (!choices.some(c => c.id === currentModel)) {
|
|
307
|
-
|
|
308
|
-
|
|
320
|
+
const idx = choices.length;
|
|
321
|
+
choices.push({ id: currentModel, name: modelShortName(currentModel), tier: 'custom', action: 'use' });
|
|
322
|
+
console.log(` ${dim('── Current ─────────────────────────')}`);
|
|
323
|
+
console.log(` ${green('>')} ${dim(`${idx + 1}.`)} ${bold(modelShortName(currentModel))} ${dim('(not installed)')} ${green('← current')}`);
|
|
309
324
|
}
|
|
310
325
|
|
|
311
326
|
const currentIdx = choices.findIndex(c => c.id === currentModel);
|
|
@@ -425,7 +440,7 @@ end tell`;
|
|
|
425
440
|
|
|
426
441
|
// ─── Stop Flow ──────────────────────────────────────────────
|
|
427
442
|
|
|
428
|
-
function stopAllAgents(projectDir) {
|
|
443
|
+
async function stopAllAgents(projectDir) {
|
|
429
444
|
cleanStalePids(projectDir);
|
|
430
445
|
const running = listPids(projectDir);
|
|
431
446
|
|
|
@@ -437,19 +452,31 @@ function stopAllAgents(projectDir) {
|
|
|
437
452
|
|
|
438
453
|
let stopped = 0;
|
|
439
454
|
for (const { name, pid } of running) {
|
|
455
|
+
// Remove cron jobs for this agent
|
|
440
456
|
try {
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
457
|
+
await execPromise('openclaw', ['cron', 'list', '--json']).then(out => {
|
|
458
|
+
const jobs = JSON.parse(out);
|
|
459
|
+
const agentJobs = (Array.isArray(jobs) ? jobs : jobs.jobs || [])
|
|
460
|
+
.filter(j => j.agent === name || (j.name && j.name.startsWith(`${name}-`)));
|
|
461
|
+
return Promise.all(agentJobs.map(j =>
|
|
462
|
+
execPromise('openclaw', ['cron', 'rm', j.id || j.name]).catch(() => {})
|
|
463
|
+
));
|
|
464
|
+
});
|
|
465
|
+
} catch { /* cron cleanup is best-effort */ }
|
|
466
|
+
|
|
467
|
+
// Remove agent from OpenClaw
|
|
468
|
+
try {
|
|
469
|
+
await execPromise('openclaw', ['agents', 'delete', name, '--force']);
|
|
470
|
+
} catch { /* best-effort */ }
|
|
471
|
+
|
|
472
|
+
// Clean up PID and workspace
|
|
473
|
+
if (pid > 0) {
|
|
474
|
+
try { process.kill(pid, 'SIGTERM'); } catch { /* already dead */ }
|
|
452
475
|
}
|
|
476
|
+
removePid(projectDir, name);
|
|
477
|
+
cleanWorkspace(projectDir, name);
|
|
478
|
+
success(`Stopped ${bold(name)}`);
|
|
479
|
+
stopped++;
|
|
453
480
|
}
|
|
454
481
|
|
|
455
482
|
if (stopped > 0) {
|
|
@@ -752,8 +779,9 @@ async function runStart(projectDir, parsed, config, soulPath, hasOpenClaw) {
|
|
|
752
779
|
console.log();
|
|
753
780
|
|
|
754
781
|
const sharedSoul = readFileSync(soulPath, 'utf8');
|
|
755
|
-
let
|
|
782
|
+
let registered = 0;
|
|
756
783
|
|
|
784
|
+
// Step 1: Generate workspaces and register agents with OpenClaw
|
|
757
785
|
for (const { name, agentMd } of toStart) {
|
|
758
786
|
try {
|
|
759
787
|
const effectiveModel = getEffectiveModel(name, agentMd.meta, config);
|
|
@@ -764,38 +792,111 @@ async function runStart(projectDir, parsed, config, soulPath, hasOpenClaw) {
|
|
|
764
792
|
|
|
765
793
|
const workspacePath = generateWorkspace(projectDir, name, overriddenMd, sharedSoul);
|
|
766
794
|
const absWorkspace = resolve(workspacePath);
|
|
767
|
-
const absProject = resolve(projectDir);
|
|
768
795
|
|
|
769
|
-
|
|
770
|
-
|
|
796
|
+
// Register agent with OpenClaw (idempotent — will update if exists)
|
|
797
|
+
try {
|
|
798
|
+
await execPromise('openclaw', [
|
|
799
|
+
'agents', 'add', name,
|
|
800
|
+
'--workspace', absWorkspace,
|
|
801
|
+
'--model', effectiveModel,
|
|
802
|
+
'--non-interactive',
|
|
803
|
+
]);
|
|
804
|
+
success(`Registered ${bold(name)} ${dim(`(${modelShortName(effectiveModel)})`)}`);
|
|
805
|
+
} catch (err) {
|
|
806
|
+
// Agent may already exist — that's fine
|
|
807
|
+
if (err.message && err.message.includes('already exists')) {
|
|
808
|
+
info(`${bold(name)} already registered with OpenClaw`);
|
|
809
|
+
} else {
|
|
810
|
+
warn(`Could not register ${bold(name)}: ${err.message || 'unknown error'}`);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
771
813
|
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
814
|
+
// Add cron jobs for the agent's schedule
|
|
815
|
+
const schedules = agentMd.meta.schedule || [];
|
|
816
|
+
for (const sched of schedules) {
|
|
817
|
+
try {
|
|
818
|
+
await execPromise('openclaw', [
|
|
819
|
+
'cron', 'add',
|
|
820
|
+
'--agent', name,
|
|
821
|
+
'--model', effectiveModel,
|
|
822
|
+
'--every', sched.every,
|
|
823
|
+
'--message', `Run task: ${sched.task}`,
|
|
824
|
+
'--name', `${name}-${sched.task}`,
|
|
825
|
+
'--light-context',
|
|
826
|
+
]);
|
|
827
|
+
} catch {
|
|
828
|
+
// Cron job may already exist — non-fatal
|
|
829
|
+
}
|
|
788
830
|
}
|
|
831
|
+
|
|
832
|
+
writePid(projectDir, name, -1); // marker
|
|
833
|
+
registered++;
|
|
789
834
|
} catch (err) {
|
|
790
835
|
error(`${bold(name)}: ${err.message}`);
|
|
791
836
|
}
|
|
792
837
|
}
|
|
793
838
|
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
839
|
+
if (registered === 0) {
|
|
840
|
+
console.log();
|
|
841
|
+
error('No agents were registered.');
|
|
797
842
|
console.log();
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
console.log();
|
|
847
|
+
|
|
848
|
+
// Step 2: Check if gateway is running, start it if not
|
|
849
|
+
let gatewayRunning = false;
|
|
850
|
+
try {
|
|
851
|
+
const healthResp = await fetch('http://127.0.0.1:18789/health');
|
|
852
|
+
gatewayRunning = healthResp.ok;
|
|
853
|
+
} catch {
|
|
854
|
+
// not running
|
|
798
855
|
}
|
|
856
|
+
|
|
857
|
+
if (!gatewayRunning) {
|
|
858
|
+
info('Starting OpenClaw gateway...');
|
|
859
|
+
|
|
860
|
+
const method = openInTerminal(
|
|
861
|
+
'OpenClaw Gateway',
|
|
862
|
+
'openclaw gateway',
|
|
863
|
+
resolve(projectDir),
|
|
864
|
+
);
|
|
865
|
+
|
|
866
|
+
if (method) {
|
|
867
|
+
success(`Gateway opened in ${method}`);
|
|
868
|
+
} else {
|
|
869
|
+
// Fallback: start in background
|
|
870
|
+
const child = spawn('openclaw', ['gateway'], {
|
|
871
|
+
stdio: 'ignore',
|
|
872
|
+
detached: true,
|
|
873
|
+
cwd: resolve(projectDir),
|
|
874
|
+
});
|
|
875
|
+
child.unref();
|
|
876
|
+
success(`Gateway started in background ${dim(`(PID ${child.pid})`)}`);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// Give gateway a moment to start
|
|
880
|
+
console.log(` ${dim('Waiting for gateway...')}`);
|
|
881
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
882
|
+
} else {
|
|
883
|
+
success('Gateway is already running');
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
console.log();
|
|
887
|
+
success(`${registered} agent(s) registered — fangs out 🐍`);
|
|
888
|
+
console.log();
|
|
889
|
+
|
|
890
|
+
printBox('Your agents are running', [
|
|
891
|
+
`${dim('The OpenClaw gateway manages your agents on their schedules.')}`,
|
|
892
|
+
`${dim('View agent activity with:')}`,
|
|
893
|
+
'',
|
|
894
|
+
`${dim('$')} ${bold('openclaw tui')} ${dim('# interactive terminal UI')}`,
|
|
895
|
+
`${dim('$')} ${bold('openclaw cron list')} ${dim('# see scheduled tasks')}`,
|
|
896
|
+
`${dim('$')} ${bold('openclaw agents list')} ${dim('# see registered agents')}`,
|
|
897
|
+
`${dim('$')} ${bold('serpentstack persistent --stop')} ${dim('# stop all agents')}`,
|
|
898
|
+
]);
|
|
899
|
+
console.log();
|
|
799
900
|
}
|
|
800
901
|
|
|
801
902
|
// ─── Main Entry Point ───────────────────────────────────────
|
|
@@ -808,7 +909,7 @@ export async function persistent({ stop = false, configure = false, agents = fal
|
|
|
808
909
|
// ── Stop (doesn't need full preflight) ──
|
|
809
910
|
if (stop) {
|
|
810
911
|
cleanStalePids(projectDir);
|
|
811
|
-
stopAllAgents(projectDir);
|
|
912
|
+
await stopAllAgents(projectDir);
|
|
812
913
|
return;
|
|
813
914
|
}
|
|
814
915
|
|
package/lib/utils/agent-utils.js
CHANGED
|
@@ -250,6 +250,9 @@ export function listPids(projectDir) {
|
|
|
250
250
|
* Check if a process is alive.
|
|
251
251
|
*/
|
|
252
252
|
export function isProcessAlive(pid) {
|
|
253
|
+
// PID -1 is a marker for "terminal-managed" — we can't check those.
|
|
254
|
+
// process.kill(-1, 0) sends to ALL processes, always succeeds — never use it.
|
|
255
|
+
if (!pid || pid <= 0) return false;
|
|
253
256
|
try {
|
|
254
257
|
process.kill(pid, 0);
|
|
255
258
|
return true;
|
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
|
}
|