serpentstack 0.2.11 → 0.2.12

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.
@@ -68,9 +68,11 @@ function showHelp() {
68
68
  divider('Any project');
69
69
  console.log(` ${cyan('skills')} Download all skills + persistent agent configs`);
70
70
  console.log(` ${cyan('skills update')} Update base skills to latest versions`);
71
- console.log(` ${cyan('persistent')} Manage and launch persistent background agents`);
71
+ console.log(` ${cyan('persistent')} Status dashboard (first run = full setup)`);
72
+ console.log(` ${cyan('persistent')} ${dim('--configure')} Edit project settings`);
73
+ console.log(` ${cyan('persistent')} ${dim('--agents')} Change agent models, enable/disable`);
74
+ console.log(` ${cyan('persistent')} ${dim('--start')} Launch enabled agents`);
72
75
  console.log(` ${cyan('persistent')} ${dim('--stop')} Stop all running agents`);
73
- console.log(` ${cyan('persistent')} ${dim('--reconfigure')} Change models, enable/disable agents`);
74
76
  console.log();
75
77
 
76
78
  divider('Options');
@@ -134,7 +136,12 @@ async function main() {
134
136
  }
135
137
  } else if (noun === 'persistent') {
136
138
  const { persistent } = await import('../lib/commands/persistent.js');
137
- await persistent({ stop: !!flags.stop, reconfigure: !!flags.reconfigure });
139
+ await persistent({
140
+ stop: !!flags.stop,
141
+ configure: !!flags.configure,
142
+ agents: !!flags.agents,
143
+ start: !!flags.start,
144
+ });
138
145
  } else {
139
146
  error(`Unknown command: ${bold(noun)}`);
140
147
  const suggestion = suggestCommand(noun);
@@ -65,7 +65,7 @@ async function pickModel(rl, agentName, currentModel, available) {
65
65
  const params = m.params ? dim(` ${m.params}`) : '';
66
66
  const quant = m.quant ? dim(` ${m.quant}`) : '';
67
67
  const size = m.size ? dim(` (${m.size})`) : '';
68
- const tag = isCurrent ? green(' \u2190 current') : '';
68
+ const tag = isCurrent ? green(' current') : '';
69
69
  console.log(` ${marker} ${num} ${label}${params}${quant}${size}${tag}`);
70
70
  }
71
71
  }
@@ -82,7 +82,7 @@ async function pickModel(rl, agentName, currentModel, available) {
82
82
  const num = dim(`${idx + 1}.`);
83
83
  const label = isCurrent ? bold(m.name) : m.name;
84
84
  const provider = m.provider ? dim(` (${m.provider})`) : '';
85
- const tag = isCurrent ? green(' \u2190 current') : '';
85
+ const tag = isCurrent ? green(' current') : '';
86
86
  console.log(` ${marker} ${num} ${label}${provider}${tag}`);
87
87
  }
88
88
  }
@@ -90,7 +90,6 @@ async function pickModel(rl, agentName, currentModel, available) {
90
90
  // If current model isn't in either list, add it
91
91
  if (!choices.some(c => c.id === currentModel)) {
92
92
  choices.unshift({ id: currentModel, name: modelShortName(currentModel), tier: 'custom' });
93
- // Re-render isn't needed since we'll just note it
94
93
  console.log(` ${dim(`Current: ${modelShortName(currentModel)} (not in detected models)`)}`);
95
94
  }
96
95
 
@@ -157,7 +156,6 @@ end tell`;
157
156
  try {
158
157
  const child = spawn(bin, args, { stdio: 'ignore', detached: true });
159
158
  child.unref();
160
- // Verify it didn't immediately fail
161
159
  const alive = child.pid && !child.killed;
162
160
  if (alive) return bin;
163
161
  } catch { continue; }
@@ -197,7 +195,6 @@ function stopAllAgents(projectDir) {
197
195
  } catch (err) {
198
196
  if (err.code === 'ESRCH') {
199
197
  removePid(projectDir, name);
200
- // Don't count already-dead processes as "stopped"
201
198
  } else {
202
199
  error(`Failed to stop ${bold(name)}: ${err.message}`);
203
200
  }
@@ -235,124 +232,24 @@ function printAgentLine(name, agentMd, config, statusInfo) {
235
232
  }
236
233
  }
237
234
 
238
- // ─── Main Flow ──────────────────────────────────────────────
239
-
240
- export async function persistent({ stop = false, reconfigure = false } = {}) {
241
- const projectDir = process.cwd();
242
-
243
- printHeader();
244
-
245
- // ── Stop ──
246
- if (stop) {
247
- stopAllAgents(projectDir);
248
- return;
249
- }
250
-
251
- // ── Preflight checks ──
252
- const soulPath = join(projectDir, '.openclaw/SOUL.md');
253
- if (!existsSync(soulPath)) {
254
- error('No .openclaw/ workspace found.');
255
- console.log(` Run ${bold('serpentstack skills')} first to download the workspace files.`);
256
- console.log();
257
- process.exit(1);
258
- }
259
-
260
- const agents = discoverAgents(projectDir);
261
- if (agents.length === 0) {
262
- error('No agents found in .openclaw/agents/');
263
- console.log(` Run ${bold('serpentstack skills')} to download the default agents,`);
264
- console.log(` or create your own at ${bold('.openclaw/agents/<name>/AGENT.md')}`);
265
- console.log();
266
- process.exit(1);
267
- }
268
-
269
- // Check OpenClaw early — don't waste time configuring if it's missing
270
- const hasOpenClaw = await which('openclaw');
271
- if (!hasOpenClaw) {
272
- warn('OpenClaw is not installed.');
273
- console.log();
274
- console.log(` ${dim('OpenClaw is the persistent agent runtime.')}`);
275
- console.log(` ${dim('Install it first, then re-run this command:')}`);
276
- console.log();
277
- console.log(` ${dim('$')} ${bold('npm install -g openclaw@latest')}`);
278
- console.log(` ${dim('$')} ${bold('serpentstack persistent')}`);
279
- console.log();
280
- process.exit(1);
281
- }
282
-
283
- cleanStalePids(projectDir);
284
-
285
- // Parse agent definitions
286
- const parsed = [];
287
- for (const agent of agents) {
288
- try {
289
- const agentMd = parseAgentMd(agent.agentMdPath);
290
- parsed.push({ ...agent, agentMd });
291
- } catch (err) {
292
- warn(`Skipping ${bold(agent.name)}: ${err.message}`);
293
- }
294
- }
295
- if (parsed.length === 0) {
296
- error('No valid AGENT.md files found.');
297
- console.log();
298
- process.exit(1);
299
- }
300
-
301
- // Load config
302
- let config = readConfig(projectDir) || { project: {}, agents: {} };
303
- const needsSetup = !config.project?.name || reconfigure;
304
-
305
- // Detect models in background while we show status
306
- const modelsPromise = detectModels();
307
-
308
- // ── If configured, show status dashboard ──
309
- if (!needsSetup) {
310
- console.log(` ${bold(config.project.name)} ${dim(`— ${config.project.framework}`)}`);
311
- console.log(` ${dim(`Dev: ${config.project.devCmd} · Test: ${config.project.testCmd}`)}`);
312
- console.log();
313
-
314
- for (const { name, agentMd } of parsed) {
315
- const statusInfo = getAgentStatus(projectDir, name, config);
316
- printAgentLine(name, agentMd, config, statusInfo);
317
- }
318
- console.log();
319
-
320
- // Determine what to do
321
- const enabledAgents = parsed.filter(a => isAgentEnabled(a.name, config));
322
- const runningNames = new Set(listPids(projectDir).map(p => p.name));
323
- const startable = enabledAgents.filter(a => !runningNames.has(a.name));
324
-
325
- if (startable.length === 0 && runningNames.size > 0) {
326
- info('All enabled agents are running.');
327
- console.log(` ${dim('Run')} ${bold('serpentstack persistent --stop')} ${dim('to stop them.')}`);
328
- console.log(` ${dim('Run')} ${bold('serpentstack persistent --reconfigure')} ${dim('to change settings.')}`);
329
- console.log();
330
- return;
331
- }
332
-
333
- if (startable.length === 0) {
334
- info('No agents are enabled.');
335
- console.log(` ${dim('Run')} ${bold('serpentstack persistent --reconfigure')} ${dim('to enable agents.')}`);
336
- console.log();
337
- return;
338
- }
235
+ function printStatusDashboard(config, parsed, projectDir) {
236
+ console.log(` ${bold(config.project.name)} ${dim(`— ${config.project.framework}`)}`);
237
+ console.log(` ${dim(`Dev: ${config.project.devCmd} · Test: ${config.project.testCmd}`)}`);
238
+ console.log();
339
239
 
340
- // Start startable agents
341
- await launchAgents(projectDir, startable, config, soulPath);
342
- return;
240
+ for (const { name, agentMd } of parsed) {
241
+ const statusInfo = getAgentStatus(projectDir, name, config);
242
+ printAgentLine(name, agentMd, config, statusInfo);
343
243
  }
244
+ console.log();
245
+ }
344
246
 
345
- // ── First-time setup / reconfigure ──
346
- if (reconfigure) {
347
- info('Reconfiguring...');
348
- console.log();
349
- }
247
+ // ─── Configure Flow (project settings) ─────────────────────
350
248
 
249
+ async function runConfigure(projectDir, config, soulPath) {
351
250
  const rl = createInterface({ input: stdin, output: stdout });
352
- let configDirty = false;
353
251
 
354
252
  try {
355
- // ── Project configuration ──
356
253
  const detected = detectProjectDefaults(projectDir);
357
254
  const template = detectTemplateDefaults(projectDir);
358
255
  const existing = config.project || {};
@@ -382,13 +279,12 @@ export async function persistent({ stop = false, reconfigure = false } = {}) {
382
279
  testCmd: await ask(rl, 'Test command', defaults.testCmd),
383
280
  conventions: await ask(rl, 'Key conventions', defaults.conventions),
384
281
  };
385
- configDirty = true;
386
282
 
387
- // Update SOUL.md
283
+ // Update SOUL.md with project context
388
284
  if (existsSync(soulPath)) {
389
285
  let soul = readFileSync(soulPath, 'utf8');
390
286
  const ctx = [
391
- `# ${config.project.name} \u2014 Persistent Development Agents`,
287
+ `# ${config.project.name} Persistent Development Agents`,
392
288
  '',
393
289
  `**Project:** ${config.project.name}`,
394
290
  `**Language:** ${config.project.language}`,
@@ -405,25 +301,40 @@ export async function persistent({ stop = false, reconfigure = false } = {}) {
405
301
  console.log();
406
302
  success(`Updated ${bold('.openclaw/SOUL.md')}`);
407
303
  console.log();
304
+ } finally {
305
+ rl.close();
306
+ }
408
307
 
409
- // ── Agent configuration ──
410
- const available = await modelsPromise;
308
+ // Mark as user-confirmed
309
+ config._configured = true;
310
+ writeConfig(projectDir, config);
311
+ success(`Saved ${bold('.openclaw/config.json')}`);
312
+ console.log();
313
+ }
411
314
 
412
- if (available.local.length > 0) {
413
- info(`${available.local.length} local model(s) detected via Ollama`);
414
- } else {
415
- warn('No local models found. Install Ollama and pull a model for free persistent agents:');
416
- console.log(` ${dim('$')} ${bold('ollama pull llama3.2')}`);
417
- }
418
- if (available.hasApiKey) {
419
- info('API key configured for cloud models');
420
- }
421
- console.log();
315
+ // ─── Agents Flow (enable/disable + model selection) ─────────
422
316
 
423
- divider('Agents');
424
- console.log(` ${dim('Enable/disable each agent and pick a model.')}`);
425
- console.log();
317
+ async function runAgents(projectDir, config, parsed) {
318
+ const available = await detectModels();
426
319
 
320
+ if (available.local.length > 0) {
321
+ info(`${available.local.length} local model(s) detected via Ollama`);
322
+ } else {
323
+ warn('No local models found. Install Ollama and pull a model for free persistent agents:');
324
+ console.log(` ${dim('$')} ${bold('ollama pull llama3.2')}`);
325
+ }
326
+ if (available.hasApiKey) {
327
+ info('API key configured for cloud models');
328
+ }
329
+ console.log();
330
+
331
+ divider('Agents');
332
+ console.log(` ${dim('Enable/disable each agent and pick a model.')}`);
333
+ console.log();
334
+
335
+ const rl = createInterface({ input: stdin, output: stdout });
336
+
337
+ try {
427
338
  for (const { name, agentMd } of parsed) {
428
339
  const existingAgent = config.agents?.[name];
429
340
  const currentEnabled = existingAgent?.enabled !== false;
@@ -443,43 +354,41 @@ export async function persistent({ stop = false, reconfigure = false } = {}) {
443
354
 
444
355
  config.agents[name] = { enabled, model };
445
356
 
446
- const status = enabled ? green('\u2713 enabled') : dim('\u2717 disabled');
357
+ const status = enabled ? green(' enabled') : dim(' disabled');
447
358
  const modelLabel = enabled ? `, ${modelShortName(model)}` : '';
448
359
  console.log(` ${status}${modelLabel}`);
449
360
  console.log();
450
361
  }
451
-
452
- configDirty = true;
453
362
  } finally {
454
363
  rl.close();
455
- // Only save if we completed configuration
456
- if (configDirty) {
457
- writeConfig(projectDir, config);
458
- success(`Saved ${bold('.openclaw/config.json')}`);
459
- console.log();
460
- }
461
364
  }
462
365
 
463
- // Show status and launch
464
- for (const { name, agentMd } of parsed) {
465
- const statusInfo = getAgentStatus(projectDir, name, config);
466
- printAgentLine(name, agentMd, config, statusInfo);
467
- }
366
+ config._configured = true;
367
+ writeConfig(projectDir, config);
368
+ success(`Saved ${bold('.openclaw/config.json')}`);
468
369
  console.log();
370
+ }
371
+
372
+ // ─── Start Flow ─────────────────────────────────────────────
469
373
 
374
+ async function runStart(projectDir, parsed, config, soulPath) {
470
375
  const enabledAgents = parsed.filter(a => isAgentEnabled(a.name, config));
471
- if (enabledAgents.length === 0) {
472
- info('No agents enabled. Run with --reconfigure to enable agents.');
376
+ const runningNames = new Set(listPids(projectDir).map(p => p.name));
377
+ const startable = enabledAgents.filter(a => !runningNames.has(a.name));
378
+
379
+ if (startable.length === 0 && runningNames.size > 0) {
380
+ info('All enabled agents are already running.');
473
381
  console.log();
474
382
  return;
475
383
  }
476
384
 
477
- await launchAgents(projectDir, enabledAgents, config, soulPath);
478
- }
479
-
480
- // ─── Launch Flow ────────────────────────────────────────────
385
+ if (startable.length === 0) {
386
+ info('No agents are enabled.');
387
+ console.log(` ${dim('Run')} ${bold('serpentstack persistent --agents')} ${dim('to enable agents.')}`);
388
+ console.log();
389
+ return;
390
+ }
481
391
 
482
- async function launchAgents(projectDir, agentsToStart, config, soulPath) {
483
392
  const rl = createInterface({ input: stdin, output: stdout });
484
393
  const toStart = [];
485
394
 
@@ -487,7 +396,7 @@ async function launchAgents(projectDir, agentsToStart, config, soulPath) {
487
396
  divider('Launch');
488
397
  console.log();
489
398
 
490
- for (const agent of agentsToStart) {
399
+ for (const agent of startable) {
491
400
  const model = getEffectiveModel(agent.name, agent.agentMd.meta, config);
492
401
  const yes = await askYesNo(rl, `Start ${bold(agent.name)} ${dim(`(${modelShortName(model)})`)}?`, true);
493
402
  if (yes) toStart.push(agent);
@@ -525,14 +434,11 @@ async function launchAgents(projectDir, agentsToStart, config, soulPath) {
525
434
  const method = openInTerminal(`SerpentStack: ${name}`, openclawCmd, absProject);
526
435
 
527
436
  if (method) {
528
- // For terminal-spawned agents, record workspace path so we can track it
529
- // The terminal process will create its own PID — we record ours as a marker
530
437
  writePid(projectDir, name, -1); // -1 = terminal-managed
531
438
  success(`${bold(name)} opened in ${method} ${dim(`(${modelShortName(effectiveModel)})`)}`);
532
439
  started++;
533
440
  } else {
534
- // Fallback: background process
535
- warn(`No terminal detected \u2014 starting ${bold(name)} in background`);
441
+ warn(`No terminal detected — starting ${bold(name)} in background`);
536
442
  const child = spawn('openclaw', ['start', '--workspace', absWorkspace], {
537
443
  stdio: 'ignore',
538
444
  detached: true,
@@ -554,9 +460,145 @@ async function launchAgents(projectDir, agentsToStart, config, soulPath) {
554
460
  success(`${started} agent(s) launched — fangs out 🐍`);
555
461
  console.log();
556
462
  printBox('Manage agents', [
557
- `${dim('$')} ${bold('serpentstack persistent')} ${dim('# status + start')}`,
558
- `${dim('$')} ${bold('serpentstack persistent --stop')} ${dim('# stop all')}`,
559
- `${dim('$')} ${bold('serpentstack persistent --reconfigure')} ${dim('# change models')}`,
463
+ `${dim('$')} ${bold('serpentstack persistent')} ${dim('# status dashboard')}`,
464
+ `${dim('$')} ${bold('serpentstack persistent --start')} ${dim('# launch agents')}`,
465
+ `${dim('$')} ${bold('serpentstack persistent --stop')} ${dim('# stop all')}`,
466
+ `${dim('$')} ${bold('serpentstack persistent --configure')} ${dim('# edit project settings')}`,
467
+ `${dim('$')} ${bold('serpentstack persistent --agents')} ${dim('# change models')}`,
560
468
  ]);
561
469
  }
562
470
  }
471
+
472
+ // ─── Main Entry Point ───────────────────────────────────────
473
+
474
+ export async function persistent({ stop = false, configure = false, agents = false, start = false } = {}) {
475
+ const projectDir = process.cwd();
476
+
477
+ printHeader();
478
+
479
+ // ── Stop ──
480
+ if (stop) {
481
+ stopAllAgents(projectDir);
482
+ return;
483
+ }
484
+
485
+ // ── Preflight checks ──
486
+ const soulPath = join(projectDir, '.openclaw/SOUL.md');
487
+ if (!existsSync(soulPath)) {
488
+ error('No .openclaw/ workspace found.');
489
+ console.log(` Run ${bold('serpentstack skills')} first to download the workspace files.`);
490
+ console.log();
491
+ process.exit(1);
492
+ }
493
+
494
+ const agentDirs = discoverAgents(projectDir);
495
+ if (agentDirs.length === 0) {
496
+ error('No agents found in .openclaw/agents/');
497
+ console.log(` Run ${bold('serpentstack skills')} to download the default agents,`);
498
+ console.log(` or create your own at ${bold('.openclaw/agents/<name>/AGENT.md')}`);
499
+ console.log();
500
+ process.exit(1);
501
+ }
502
+
503
+ // Check OpenClaw early
504
+ const hasOpenClaw = await which('openclaw');
505
+ if (!hasOpenClaw) {
506
+ warn('OpenClaw is not installed.');
507
+ console.log();
508
+ console.log(` ${dim('OpenClaw is the persistent agent runtime.')}`);
509
+ console.log(` ${dim('Install it first, then re-run this command:')}`);
510
+ console.log();
511
+ console.log(` ${dim('$')} ${bold('npm install -g openclaw@latest')}`);
512
+ console.log(` ${dim('$')} ${bold('serpentstack persistent')}`);
513
+ console.log();
514
+ process.exit(1);
515
+ }
516
+
517
+ cleanStalePids(projectDir);
518
+
519
+ // Parse agent definitions
520
+ const parsed = [];
521
+ for (const agent of agentDirs) {
522
+ try {
523
+ const agentMd = parseAgentMd(agent.agentMdPath);
524
+ parsed.push({ ...agent, agentMd });
525
+ } catch (err) {
526
+ warn(`Skipping ${bold(agent.name)}: ${err.message}`);
527
+ }
528
+ }
529
+ if (parsed.length === 0) {
530
+ error('No valid AGENT.md files found.');
531
+ console.log();
532
+ process.exit(1);
533
+ }
534
+
535
+ // Load config
536
+ let config = readConfig(projectDir) || { project: {}, agents: {} };
537
+ const isConfigured = !!config._configured;
538
+
539
+ // ── Explicit flag: --configure ──
540
+ if (configure) {
541
+ await runConfigure(projectDir, config, soulPath);
542
+ return;
543
+ }
544
+
545
+ // ── Explicit flag: --agents ──
546
+ if (agents) {
547
+ config = readConfig(projectDir) || config; // re-read in case --configure was run first
548
+ await runAgents(projectDir, config, parsed);
549
+ return;
550
+ }
551
+
552
+ // ── Explicit flag: --start ──
553
+ if (start) {
554
+ await runStart(projectDir, parsed, config, soulPath);
555
+ return;
556
+ }
557
+
558
+ // ── Default: bare `serpentstack persistent` ──
559
+ if (isConfigured) {
560
+ // Already set up — show dashboard
561
+ printStatusDashboard(config, parsed, projectDir);
562
+
563
+ const enabledAgents = parsed.filter(a => isAgentEnabled(a.name, config));
564
+ const runningNames = new Set(listPids(projectDir).map(p => p.name));
565
+ const startable = enabledAgents.filter(a => !runningNames.has(a.name));
566
+
567
+ if (startable.length === 0 && runningNames.size > 0) {
568
+ info('All enabled agents are running.');
569
+ } else if (startable.length === 0) {
570
+ info('No agents are enabled.');
571
+ } else {
572
+ info(`${startable.length} agent(s) ready to start.`);
573
+ }
574
+
575
+ console.log();
576
+ printBox('Commands', [
577
+ `${dim('$')} ${bold('serpentstack persistent --start')} ${dim('# launch agents')}`,
578
+ `${dim('$')} ${bold('serpentstack persistent --stop')} ${dim('# stop all')}`,
579
+ `${dim('$')} ${bold('serpentstack persistent --configure')} ${dim('# edit project settings')}`,
580
+ `${dim('$')} ${bold('serpentstack persistent --agents')} ${dim('# change models')}`,
581
+ ]);
582
+ console.log();
583
+ return;
584
+ }
585
+
586
+ // ── First-time setup: full walkthrough ──
587
+ info('First-time setup — let\'s configure your project and agents.');
588
+ console.log();
589
+
590
+ // Step 1: Project settings
591
+ await runConfigure(projectDir, config, soulPath);
592
+
593
+ // Re-read config (runConfigure saved it)
594
+ config = readConfig(projectDir) || config;
595
+
596
+ // Step 2: Agent settings
597
+ await runAgents(projectDir, config, parsed);
598
+
599
+ // Re-read config (runAgents saved it)
600
+ config = readConfig(projectDir) || config;
601
+
602
+ // Step 3: Launch
603
+ await runStart(projectDir, parsed, config, soulPath);
604
+ }
@@ -139,11 +139,10 @@ export async function skillsInit({ force = false } = {}) {
139
139
  ]);
140
140
 
141
141
  printBox('Want persistent background agents too?', [
142
- `${dim('$')} ${bold('serpentstack persistent')}`,
142
+ `${dim('$')} ${bold('serpentstack persistent')} ${dim('# first-time setup walkthrough')}`,
143
143
  '',
144
- `${dim('Agents that watch your dev server, run tests, and keep')}`,
145
- `${dim('skills fresh — each in its own terminal. Pick which to')}`,
146
- `${dim('run and choose local or cloud models.')}`,
144
+ `${dim('Configures your project, picks models, and launches')}`,
145
+ `${dim('agents — each in its own terminal window.')}`,
147
146
  ]);
148
147
  console.log();
149
148
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "serpentstack",
3
- "version": "0.2.11",
3
+ "version": "0.2.12",
4
4
  "description": "CLI for SerpentStack — AI-driven development standards with project-specific skills and persistent agents",
5
5
  "type": "module",
6
6
  "bin": {