serpentstack 0.2.4 → 0.2.6
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/serpentstack.js +73 -23
- package/lib/commands/persistent.js +559 -0
- package/lib/commands/skills-init.js +51 -14
- package/lib/commands/stack-new.js +44 -9
- package/lib/utils/config.js +106 -0
- package/lib/utils/fs-helpers.js +1 -1
- package/lib/utils/models.js +156 -0
- package/lib/utils/ui.js +5 -5
- package/package.json +1 -1
- package/lib/commands/skills-persistent.js +0 -358
|
@@ -1,358 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
import { execFile, spawn } from 'node:child_process';
|
|
4
|
-
import { createInterface } from 'node:readline/promises';
|
|
5
|
-
import { stdin, stdout } from 'node:process';
|
|
6
|
-
import { info, success, warn, error, confirm, bold, dim, green, cyan, yellow, printBox, printHeader } from '../utils/ui.js';
|
|
7
|
-
import {
|
|
8
|
-
parseAgentMd,
|
|
9
|
-
discoverAgents,
|
|
10
|
-
generateWorkspace,
|
|
11
|
-
writePid,
|
|
12
|
-
readPid,
|
|
13
|
-
removePid,
|
|
14
|
-
listPids,
|
|
15
|
-
isProcessAlive,
|
|
16
|
-
cleanStalePids,
|
|
17
|
-
cleanWorkspace,
|
|
18
|
-
} from '../utils/agent-utils.js';
|
|
19
|
-
|
|
20
|
-
function which(cmd) {
|
|
21
|
-
return new Promise((resolve) => {
|
|
22
|
-
execFile('which', [cmd], (err) => resolve(!err));
|
|
23
|
-
});
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
async function askQuestion(rl, label, hint) {
|
|
27
|
-
const answer = await rl.question(` ${green('?')} ${bold(label)}${hint ? ` ${dim(hint)}` : ''}: `);
|
|
28
|
-
return answer.trim();
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// ─── Stop Flow ──────────────────────────────────────────────
|
|
32
|
-
|
|
33
|
-
async function stopAllAgents(projectDir) {
|
|
34
|
-
cleanStalePids(projectDir);
|
|
35
|
-
const running = listPids(projectDir);
|
|
36
|
-
|
|
37
|
-
if (running.length === 0) {
|
|
38
|
-
info('No agents are currently running.');
|
|
39
|
-
console.log();
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
console.log(` ${dim('Stopping')} ${bold(String(running.length))} ${dim('agent(s)...')}`);
|
|
44
|
-
console.log();
|
|
45
|
-
|
|
46
|
-
let stopped = 0;
|
|
47
|
-
for (const { name, pid } of running) {
|
|
48
|
-
try {
|
|
49
|
-
process.kill(pid, 'SIGTERM');
|
|
50
|
-
removePid(projectDir, name);
|
|
51
|
-
cleanWorkspace(projectDir, name);
|
|
52
|
-
success(`${bold(name)} stopped ${dim(`(PID ${pid})`)}`);
|
|
53
|
-
stopped++;
|
|
54
|
-
} catch (err) {
|
|
55
|
-
if (err.code === 'ESRCH') {
|
|
56
|
-
// Process already dead
|
|
57
|
-
removePid(projectDir, name);
|
|
58
|
-
success(`${bold(name)} already stopped`);
|
|
59
|
-
stopped++;
|
|
60
|
-
} else {
|
|
61
|
-
error(`Failed to stop ${bold(name)}: ${err.message}`);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
console.log();
|
|
67
|
-
success(`${green(String(stopped))} agent(s) stopped`);
|
|
68
|
-
console.log();
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// ─── Customize Workspace ────────────────────────────────────
|
|
72
|
-
|
|
73
|
-
async function customizeWorkspace(projectDir) {
|
|
74
|
-
const rl = createInterface({ input: stdin, output: stdout });
|
|
75
|
-
|
|
76
|
-
console.log();
|
|
77
|
-
console.log(` ${bold('Configure your project identity')}`);
|
|
78
|
-
console.log(` ${dim('Answer a few questions so all agents understand your project.')}`);
|
|
79
|
-
console.log();
|
|
80
|
-
|
|
81
|
-
try {
|
|
82
|
-
const name = await askQuestion(rl, 'Project name', '(e.g., Acme API)');
|
|
83
|
-
const lang = await askQuestion(rl, 'Primary language', '(e.g., Python, TypeScript)');
|
|
84
|
-
const framework = await askQuestion(rl, 'Framework', '(e.g., FastAPI, Next.js, Django)');
|
|
85
|
-
const devCmd = await askQuestion(rl, 'Dev server command', '(e.g., make dev, npm run dev)');
|
|
86
|
-
const testCmd = await askQuestion(rl, 'Test command', '(e.g., make test, pytest, npm test)');
|
|
87
|
-
const conventions = await askQuestion(rl, 'Key conventions', '(brief, e.g., "services flush, routes commit")');
|
|
88
|
-
|
|
89
|
-
console.log();
|
|
90
|
-
|
|
91
|
-
const soulPath = join(projectDir, '.openclaw/SOUL.md');
|
|
92
|
-
let soul = readFileSync(soulPath, 'utf8');
|
|
93
|
-
|
|
94
|
-
const projectContext = [
|
|
95
|
-
`# ${name} — Persistent Development Agents`,
|
|
96
|
-
'',
|
|
97
|
-
`**Project:** ${name}`,
|
|
98
|
-
`**Language:** ${lang}`,
|
|
99
|
-
`**Framework:** ${framework}`,
|
|
100
|
-
`**Dev server:** \`${devCmd}\``,
|
|
101
|
-
`**Tests:** \`${testCmd}\``,
|
|
102
|
-
`**Conventions:** ${conventions}`,
|
|
103
|
-
'',
|
|
104
|
-
'---',
|
|
105
|
-
'',
|
|
106
|
-
].join('\n');
|
|
107
|
-
|
|
108
|
-
const dashIndex = soul.indexOf('---');
|
|
109
|
-
if (dashIndex !== -1) {
|
|
110
|
-
soul = projectContext + soul.slice(dashIndex + 3).trimStart();
|
|
111
|
-
} else {
|
|
112
|
-
soul = projectContext + soul;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
writeFileSync(soulPath, soul, 'utf8');
|
|
116
|
-
success(`Updated ${bold('.openclaw/SOUL.md')} with ${green(name)} project context`);
|
|
117
|
-
|
|
118
|
-
return true;
|
|
119
|
-
} finally {
|
|
120
|
-
rl.close();
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// ─── Install OpenClaw ───────────────────────────────────────
|
|
125
|
-
|
|
126
|
-
async function installOpenClaw() {
|
|
127
|
-
console.log();
|
|
128
|
-
warn('OpenClaw is not installed.');
|
|
129
|
-
console.log();
|
|
130
|
-
console.log(` ${dim('OpenClaw is the persistent agent runtime. Each agent in')}`);
|
|
131
|
-
console.log(` ${dim('.openclaw/agents/ runs as a separate OpenClaw process.')}`);
|
|
132
|
-
console.log();
|
|
133
|
-
|
|
134
|
-
const install = await confirm('Install OpenClaw now? (npm install -g openclaw@latest)');
|
|
135
|
-
if (!install) {
|
|
136
|
-
console.log();
|
|
137
|
-
info('Install manually when ready:');
|
|
138
|
-
console.log(` ${dim('$')} ${bold('npm install -g openclaw@latest')}`);
|
|
139
|
-
console.log(` ${dim('$')} ${bold('serpentstack skills persistent')}`);
|
|
140
|
-
console.log();
|
|
141
|
-
return false;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
console.log();
|
|
145
|
-
info('Installing OpenClaw...');
|
|
146
|
-
await new Promise((resolve, reject) => {
|
|
147
|
-
const child = spawn('npm', ['install', '-g', 'openclaw@latest'], { stdio: 'inherit' });
|
|
148
|
-
child.on('close', (code) => code === 0 ? resolve() : reject(new Error(`npm install exited with code ${code}`)));
|
|
149
|
-
});
|
|
150
|
-
console.log();
|
|
151
|
-
success('OpenClaw installed');
|
|
152
|
-
return true;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// ─── Start Agents ───────────────────────────────────────────
|
|
156
|
-
|
|
157
|
-
function startAgent(projectDir, agentName, workspacePath) {
|
|
158
|
-
return new Promise((resolve, reject) => {
|
|
159
|
-
const child = spawn('openclaw', ['start', '--workspace', workspacePath], {
|
|
160
|
-
stdio: 'ignore',
|
|
161
|
-
detached: true,
|
|
162
|
-
cwd: projectDir,
|
|
163
|
-
env: {
|
|
164
|
-
...process.env,
|
|
165
|
-
OPENCLAW_STATE_DIR: join(workspacePath, '.state'),
|
|
166
|
-
},
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
child.unref();
|
|
170
|
-
|
|
171
|
-
child.on('error', (err) => {
|
|
172
|
-
reject(new Error(`Failed to start ${agentName}: ${err.message}`));
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
// Give it a moment to start, then record PID
|
|
176
|
-
setTimeout(() => {
|
|
177
|
-
writePid(projectDir, agentName, child.pid);
|
|
178
|
-
resolve(child.pid);
|
|
179
|
-
}, 500);
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// ─── Main Flow ──────────────────────────────────────────────
|
|
184
|
-
|
|
185
|
-
export async function skillsPersistent({ stop = false } = {}) {
|
|
186
|
-
const projectDir = process.cwd();
|
|
187
|
-
|
|
188
|
-
// --stop flag
|
|
189
|
-
if (stop) {
|
|
190
|
-
printHeader();
|
|
191
|
-
await stopAllAgents(projectDir);
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// === Guided setup flow ===
|
|
196
|
-
printHeader();
|
|
197
|
-
|
|
198
|
-
// Step 1: Check workspace
|
|
199
|
-
console.log(` ${bold('Step 1/4')} ${dim('\u2014 Check workspace')}`);
|
|
200
|
-
console.log();
|
|
201
|
-
|
|
202
|
-
const soulPath = join(projectDir, '.openclaw/SOUL.md');
|
|
203
|
-
if (!existsSync(soulPath)) {
|
|
204
|
-
error('No .openclaw/ workspace found.');
|
|
205
|
-
console.log();
|
|
206
|
-
console.log(` Run ${bold('serpentstack skills init')} first to download the workspace files.`);
|
|
207
|
-
console.log();
|
|
208
|
-
process.exit(1);
|
|
209
|
-
}
|
|
210
|
-
success('.openclaw/SOUL.md found');
|
|
211
|
-
|
|
212
|
-
const agents = discoverAgents(projectDir);
|
|
213
|
-
if (agents.length === 0) {
|
|
214
|
-
error('No agents found in .openclaw/agents/');
|
|
215
|
-
console.log();
|
|
216
|
-
console.log(` Run ${bold('serpentstack skills init')} to download the default agents,`);
|
|
217
|
-
console.log(` or create your own at ${bold('.openclaw/agents/<name>/AGENT.md')}`);
|
|
218
|
-
console.log();
|
|
219
|
-
process.exit(1);
|
|
220
|
-
}
|
|
221
|
-
success(`${green(String(agents.length))} agent(s) found in .openclaw/agents/`);
|
|
222
|
-
console.log();
|
|
223
|
-
|
|
224
|
-
// Clean up any stale PIDs from previous runs
|
|
225
|
-
cleanStalePids(projectDir);
|
|
226
|
-
|
|
227
|
-
// Check if any agents are already running
|
|
228
|
-
const alreadyRunning = listPids(projectDir);
|
|
229
|
-
if (alreadyRunning.length > 0) {
|
|
230
|
-
warn(`${bold(String(alreadyRunning.length))} agent(s) already running`);
|
|
231
|
-
for (const { name, pid } of alreadyRunning) {
|
|
232
|
-
console.log(` ${dim('\u2022')} ${bold(name)} ${dim(`(PID ${pid})`)}`);
|
|
233
|
-
}
|
|
234
|
-
console.log();
|
|
235
|
-
const restart = await confirm('Stop running agents and restart?');
|
|
236
|
-
if (restart) {
|
|
237
|
-
await stopAllAgents(projectDir);
|
|
238
|
-
} else {
|
|
239
|
-
console.log();
|
|
240
|
-
info('Keeping existing agents running.');
|
|
241
|
-
console.log();
|
|
242
|
-
return;
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// Step 2: Configure project identity
|
|
247
|
-
console.log(` ${bold('Step 2/4')} ${dim('\u2014 Configure project identity')}`);
|
|
248
|
-
|
|
249
|
-
const soulContent = readFileSync(soulPath, 'utf8');
|
|
250
|
-
const isCustomized = !soulContent.startsWith('# SerpentStack') && !soulContent.includes('{{PROJECT_NAME}}');
|
|
251
|
-
|
|
252
|
-
if (isCustomized) {
|
|
253
|
-
console.log();
|
|
254
|
-
success('SOUL.md already customized');
|
|
255
|
-
console.log();
|
|
256
|
-
const reconfigure = await confirm('Reconfigure? (will overwrite current settings)');
|
|
257
|
-
if (reconfigure) {
|
|
258
|
-
await customizeWorkspace(projectDir);
|
|
259
|
-
}
|
|
260
|
-
console.log();
|
|
261
|
-
} else {
|
|
262
|
-
await customizeWorkspace(projectDir);
|
|
263
|
-
console.log();
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// Step 3: Select agents
|
|
267
|
-
console.log(` ${bold('Step 3/4')} ${dim('\u2014 Review agents')}`);
|
|
268
|
-
console.log();
|
|
269
|
-
|
|
270
|
-
// Parse all agents and display them
|
|
271
|
-
const parsed = [];
|
|
272
|
-
for (const agent of agents) {
|
|
273
|
-
try {
|
|
274
|
-
const agentMd = parseAgentMd(agent.agentMdPath);
|
|
275
|
-
parsed.push({ ...agent, agentMd });
|
|
276
|
-
|
|
277
|
-
const model = agentMd.meta.model || 'default';
|
|
278
|
-
const modelShort = model.includes('haiku') ? 'Haiku' : model.includes('sonnet') ? 'Sonnet' : model.includes('opus') ? 'Opus' : model;
|
|
279
|
-
const schedule = (agentMd.meta.schedule || []).map(s => s.every).join(', ');
|
|
280
|
-
|
|
281
|
-
success(`${bold(agent.name)} ${dim(agentMd.meta.description || '')}`);
|
|
282
|
-
console.log(` ${dim(`Model: ${modelShort}`)}${schedule ? dim(` \u2022 Schedule: ${schedule}`) : ''}`);
|
|
283
|
-
} catch (err) {
|
|
284
|
-
error(`${bold(agent.name)}: ${err.message}`);
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
if (parsed.length === 0) {
|
|
289
|
-
console.log();
|
|
290
|
-
error('No valid agents found. Check your AGENT.md files.');
|
|
291
|
-
console.log();
|
|
292
|
-
process.exit(1);
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
console.log();
|
|
296
|
-
console.log(` ${dim(`${parsed.length} agent(s) will be started. Delete an agent's folder to disable it.`)}`);
|
|
297
|
-
console.log();
|
|
298
|
-
|
|
299
|
-
// Step 4: Install + start
|
|
300
|
-
console.log(` ${bold('Step 4/4')} ${dim('\u2014 Start agents')}`);
|
|
301
|
-
|
|
302
|
-
const hasOpenClaw = await which('openclaw');
|
|
303
|
-
if (!hasOpenClaw) {
|
|
304
|
-
const installed = await installOpenClaw();
|
|
305
|
-
if (!installed) return;
|
|
306
|
-
} else {
|
|
307
|
-
success('OpenClaw is installed');
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
const shouldStart = await confirm(`Start ${parsed.length} agent(s) now?`);
|
|
311
|
-
if (!shouldStart) {
|
|
312
|
-
console.log();
|
|
313
|
-
printBox('Start later', [
|
|
314
|
-
`${dim('$')} ${bold('serpentstack skills persistent')} ${dim('# run setup again')}`,
|
|
315
|
-
`${dim('$')} ${bold('serpentstack skills persistent --stop')} ${dim('# stop all agents')}`,
|
|
316
|
-
]);
|
|
317
|
-
return;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
console.log();
|
|
321
|
-
|
|
322
|
-
// Read shared SOUL.md
|
|
323
|
-
const sharedSoul = readFileSync(soulPath, 'utf8');
|
|
324
|
-
let started = 0;
|
|
325
|
-
|
|
326
|
-
for (const { name, agentMd } of parsed) {
|
|
327
|
-
try {
|
|
328
|
-
const workspacePath = generateWorkspace(projectDir, name, agentMd, sharedSoul);
|
|
329
|
-
const pid = await startAgent(projectDir, name, workspacePath);
|
|
330
|
-
|
|
331
|
-
const model = agentMd.meta.model || 'default';
|
|
332
|
-
const modelShort = model.includes('haiku') ? 'Haiku' : model.includes('sonnet') ? 'Sonnet' : model.includes('opus') ? 'Opus' : model;
|
|
333
|
-
const schedule = (agentMd.meta.schedule || []).map(s => s.every).join(', ');
|
|
334
|
-
|
|
335
|
-
success(`${bold(name)} started ${dim(`(${modelShort}, ${schedule || 'no schedule'}) PID ${pid}`)}`);
|
|
336
|
-
started++;
|
|
337
|
-
} catch (err) {
|
|
338
|
-
error(`${bold(name)}: ${err.message}`);
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
console.log();
|
|
343
|
-
if (started > 0) {
|
|
344
|
-
success(`${green(String(started))} agent(s) running`);
|
|
345
|
-
console.log();
|
|
346
|
-
printBox('Manage your agents', [
|
|
347
|
-
`${dim('$')} ${bold('serpentstack skills persistent --stop')} ${dim('# stop all agents')}`,
|
|
348
|
-
`${dim('$')} ${bold('serpentstack skills persistent')} ${dim('# restart / reconfigure')}`,
|
|
349
|
-
'',
|
|
350
|
-
`${dim('Add agents:')} ${dim('Create .openclaw/agents/<name>/AGENT.md')}`,
|
|
351
|
-
`${dim('Remove agents:')} ${dim('Delete the agent folder from .openclaw/agents/')}`,
|
|
352
|
-
`${dim('Customize:')} ${dim('Edit AGENT.md frontmatter (model, schedule, tools)')}`,
|
|
353
|
-
]);
|
|
354
|
-
} else {
|
|
355
|
-
error('No agents were started. Check the errors above.');
|
|
356
|
-
console.log();
|
|
357
|
-
}
|
|
358
|
-
}
|