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.
@@ -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
- process.kill(pid, 'SIGTERM');
442
- removePid(projectDir, name);
443
- cleanWorkspace(projectDir, name);
444
- success(`Stopped ${bold(name)} ${dim(`(PID ${pid})`)}`);
445
- stopped++;
446
- } catch (err) {
447
- if (err.code === 'ESRCH') {
448
- removePid(projectDir, name);
449
- } else {
450
- error(`Failed to stop ${bold(name)}: ${err.message}`);
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 started = 0;
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
- const openclawCmd = `OPENCLAW_STATE_DIR='${join(absWorkspace, '.state')}' openclaw start --workspace '${absWorkspace}'`;
770
- const method = openInTerminal(`SerpentStack: ${name}`, openclawCmd, absProject);
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
- if (method) {
773
- writePid(projectDir, name, -1);
774
- success(`${bold(name)} opened in ${method} ${dim(`(${modelShortName(effectiveModel)})`)}`);
775
- started++;
776
- } else {
777
- warn(`No terminal detected — starting ${bold(name)} in background`);
778
- const child = spawn('openclaw', ['start', '--workspace', absWorkspace], {
779
- stdio: 'ignore',
780
- detached: true,
781
- cwd: absProject,
782
- env: { ...process.env, OPENCLAW_STATE_DIR: join(absWorkspace, '.state') },
783
- });
784
- child.unref();
785
- writePid(projectDir, name, child.pid);
786
- success(`${bold(name)} started ${dim(`PID ${child.pid}`)}`);
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
- console.log();
795
- if (started > 0) {
796
- success(`${started} agent(s) launched — fangs out 🐍`);
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}── ${RESET}${BOLD}${label}${RESET} ${DIM}${'─'.repeat(Math.max(0, 50 - stripAnsi(label).length))}${RESET}`);
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "serpentstack",
3
- "version": "0.2.15",
3
+ "version": "0.2.16",
4
4
  "description": "CLI for SerpentStack — AI-driven development standards with project-specific skills and persistent agents",
5
5
  "type": "module",
6
6
  "bin": {