doer-agent 0.3.0 → 0.3.1
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/dist/agent.js +168 -2
- package/package.json +1 -1
package/dist/agent.js
CHANGED
|
@@ -16,6 +16,7 @@ const sessionRpcCodec = StringCodec();
|
|
|
16
16
|
const codexAuthRpcCodec = StringCodec();
|
|
17
17
|
const settingsRpcCodec = StringCodec();
|
|
18
18
|
const gitRpcCodec = StringCodec();
|
|
19
|
+
const skillRpcCodec = StringCodec();
|
|
19
20
|
const retainedRuns = new Map();
|
|
20
21
|
const activeSessionWatchers = new Map();
|
|
21
22
|
const sessionLineIndexCache = new Map();
|
|
@@ -40,6 +41,9 @@ function buildAgentSettingsRpcSubject(userId, agentId) {
|
|
|
40
41
|
function buildAgentGitRpcSubject(userId, agentId) {
|
|
41
42
|
return `doer.agent.git.rpc.${sanitizeUserId(userId)}.${agentId.trim()}`;
|
|
42
43
|
}
|
|
44
|
+
function buildAgentSkillRpcSubject(userId, agentId) {
|
|
45
|
+
return `doer.agent.skill.rpc.${sanitizeUserId(userId)}.${agentId.trim()}`;
|
|
46
|
+
}
|
|
43
47
|
function normalizeNatsServers(value) {
|
|
44
48
|
if (!Array.isArray(value)) {
|
|
45
49
|
return [];
|
|
@@ -1256,11 +1260,12 @@ function spawnManagedCodexCommand(args) {
|
|
|
1256
1260
|
child.stderr?.setEncoding("utf8");
|
|
1257
1261
|
return child;
|
|
1258
1262
|
}
|
|
1259
|
-
async function runLocalCodexCli(args, timeoutMs) {
|
|
1263
|
+
async function runLocalCodexCli(args, timeoutMs, envPatch) {
|
|
1260
1264
|
const command = buildLocalCodexCliCommand(args);
|
|
1261
1265
|
const workspaceRoot = workspaceRootOverride ?? (process.env.WORKSPACE?.trim() || process.cwd());
|
|
1262
1266
|
const env = {
|
|
1263
1267
|
...process.env,
|
|
1268
|
+
...(envPatch ?? {}),
|
|
1264
1269
|
WORKSPACE: workspaceRoot,
|
|
1265
1270
|
CODEX_HOME: resolveCodexHomePath(),
|
|
1266
1271
|
};
|
|
@@ -1306,6 +1311,152 @@ async function runLocalCodexCli(args, timeoutMs) {
|
|
|
1306
1311
|
});
|
|
1307
1312
|
});
|
|
1308
1313
|
}
|
|
1314
|
+
function buildSkillGeneratorPrompt(userPrompt) {
|
|
1315
|
+
return [
|
|
1316
|
+
"Create a Codex skill from the user's description.",
|
|
1317
|
+
"",
|
|
1318
|
+
"Return JSON only with this shape:",
|
|
1319
|
+
'{ "skillName": "kebab-or-dot-or-underscore-name", "skillMd": "full SKILL.md content" }',
|
|
1320
|
+
"",
|
|
1321
|
+
"Requirements:",
|
|
1322
|
+
"- skillName must be lowercase ASCII and use only letters, numbers, dot, underscore, or dash.",
|
|
1323
|
+
"- skillName must not start with a dot and must not be .system.",
|
|
1324
|
+
"- skillMd must be a complete SKILL.md file.",
|
|
1325
|
+
"- skillMd must start with YAML frontmatter containing name and description.",
|
|
1326
|
+
"- Keep the skill concise and action-oriented.",
|
|
1327
|
+
"- Include when to use the skill, the core workflow, and any important constraints.",
|
|
1328
|
+
"- Do not add README, changelog, or any extra files.",
|
|
1329
|
+
"",
|
|
1330
|
+
"User request:",
|
|
1331
|
+
userPrompt.trim(),
|
|
1332
|
+
].join("\n");
|
|
1333
|
+
}
|
|
1334
|
+
function extractJsonObject(value) {
|
|
1335
|
+
const trimmed = value.trim();
|
|
1336
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
1337
|
+
return trimmed;
|
|
1338
|
+
}
|
|
1339
|
+
const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
1340
|
+
if (fenced?.[1]) {
|
|
1341
|
+
return fenced[1].trim();
|
|
1342
|
+
}
|
|
1343
|
+
const start = trimmed.indexOf("{");
|
|
1344
|
+
const end = trimmed.lastIndexOf("}");
|
|
1345
|
+
if (start >= 0 && end > start) {
|
|
1346
|
+
return trimmed.slice(start, end + 1);
|
|
1347
|
+
}
|
|
1348
|
+
throw new Error("Codex did not return JSON");
|
|
1349
|
+
}
|
|
1350
|
+
function slugifySkillName(value) {
|
|
1351
|
+
return value
|
|
1352
|
+
.trim()
|
|
1353
|
+
.toLowerCase()
|
|
1354
|
+
.replace(/[^a-z0-9._-]+/g, "-")
|
|
1355
|
+
.replace(/^-+|-+$/g, "")
|
|
1356
|
+
.replace(/-{2,}/g, "-");
|
|
1357
|
+
}
|
|
1358
|
+
function normalizeGeneratedSkill(value) {
|
|
1359
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1360
|
+
throw new Error("Invalid generated skill payload");
|
|
1361
|
+
}
|
|
1362
|
+
const row = value;
|
|
1363
|
+
const skillName = slugifySkillName(typeof row.skillName === "string" ? row.skillName : "");
|
|
1364
|
+
const skillMd = typeof row.skillMd === "string" ? row.skillMd.trim() : "";
|
|
1365
|
+
if (!skillName || skillName.startsWith(".") || skillName === ".system") {
|
|
1366
|
+
throw new Error("Codex returned an invalid skill name");
|
|
1367
|
+
}
|
|
1368
|
+
if (!skillMd) {
|
|
1369
|
+
throw new Error("Codex returned an empty SKILL.md");
|
|
1370
|
+
}
|
|
1371
|
+
if (!/^---\s*\n[\s\S]*?\n---\s*\n/m.test(skillMd)) {
|
|
1372
|
+
throw new Error("Generated SKILL.md is missing YAML frontmatter");
|
|
1373
|
+
}
|
|
1374
|
+
if (!/\nname:\s*[^\n]+/i.test(skillMd) || !/\ndescription:\s*[^\n]+/i.test(skillMd)) {
|
|
1375
|
+
throw new Error("Generated SKILL.md frontmatter is incomplete");
|
|
1376
|
+
}
|
|
1377
|
+
return { skillName, skillMd };
|
|
1378
|
+
}
|
|
1379
|
+
function buildSkillGeneratorCodexArgs(prompt, model) {
|
|
1380
|
+
return ["--dangerously-bypass-approvals-and-sandbox", "--model", model, "exec", "--", prompt];
|
|
1381
|
+
}
|
|
1382
|
+
async function generateSkillViaCodex(userPrompt) {
|
|
1383
|
+
const localAgentSettings = await readAgentSettingsConfig(null);
|
|
1384
|
+
const envPatch = buildAgentSettingsEnvPatch(localAgentSettings);
|
|
1385
|
+
const prompt = buildSkillGeneratorPrompt(userPrompt);
|
|
1386
|
+
const result = await runLocalCodexCli(buildSkillGeneratorCodexArgs(prompt, localAgentSettings.codex.model || "gpt-5.4"), 120_000, envPatch);
|
|
1387
|
+
if (result.timedOut) {
|
|
1388
|
+
throw new Error("Codex timed out while generating the skill");
|
|
1389
|
+
}
|
|
1390
|
+
if ((result.code ?? 1) !== 0) {
|
|
1391
|
+
const details = stripAnsi(result.stderr || result.stdout).trim();
|
|
1392
|
+
throw new Error(details || `Codex exited with code ${result.code ?? "null"}`);
|
|
1393
|
+
}
|
|
1394
|
+
const payload = JSON.parse(extractJsonObject(stripAnsi(result.stdout)));
|
|
1395
|
+
const generated = normalizeGeneratedSkill(payload);
|
|
1396
|
+
const skillPath = path.join(resolveCodexHomePath(), "skills", generated.skillName);
|
|
1397
|
+
const skillFilePath = path.join(skillPath, "SKILL.md");
|
|
1398
|
+
try {
|
|
1399
|
+
await stat(skillPath);
|
|
1400
|
+
throw new Error("A skill with that name already exists");
|
|
1401
|
+
}
|
|
1402
|
+
catch (error) {
|
|
1403
|
+
if (!(error instanceof Error) || !/ENOENT/i.test(error.message)) {
|
|
1404
|
+
throw error;
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
await mkdir(skillPath, { recursive: true });
|
|
1408
|
+
await writeFile(skillFilePath, `${generated.skillMd}\n`, "utf8");
|
|
1409
|
+
return {
|
|
1410
|
+
skillName: generated.skillName,
|
|
1411
|
+
skillPath: `.codex/skills/${generated.skillName}`,
|
|
1412
|
+
skillFilePath: `.codex/skills/${generated.skillName}/SKILL.md`,
|
|
1413
|
+
};
|
|
1414
|
+
}
|
|
1415
|
+
async function handleSkillRpcMessage(args) {
|
|
1416
|
+
let payload = {};
|
|
1417
|
+
try {
|
|
1418
|
+
payload = JSON.parse(skillRpcCodec.decode(args.msg.data));
|
|
1419
|
+
if (typeof payload.agentId === "string" && payload.agentId.trim() && payload.agentId !== args.agentId) {
|
|
1420
|
+
throw new Error("agent id mismatch");
|
|
1421
|
+
}
|
|
1422
|
+
const prompt = typeof payload.prompt === "string" ? payload.prompt.trim() : "";
|
|
1423
|
+
if (!prompt) {
|
|
1424
|
+
throw new Error("prompt is required");
|
|
1425
|
+
}
|
|
1426
|
+
const result = await generateSkillViaCodex(prompt);
|
|
1427
|
+
args.msg.respond(skillRpcCodec.encode(JSON.stringify({
|
|
1428
|
+
ok: true,
|
|
1429
|
+
skillName: result.skillName,
|
|
1430
|
+
skillPath: result.skillPath,
|
|
1431
|
+
skillFilePath: result.skillFilePath,
|
|
1432
|
+
})));
|
|
1433
|
+
}
|
|
1434
|
+
catch (error) {
|
|
1435
|
+
const message = error instanceof Error ? error.message : "unknown error";
|
|
1436
|
+
args.msg.respond(skillRpcCodec.encode(JSON.stringify({
|
|
1437
|
+
ok: false,
|
|
1438
|
+
error: message,
|
|
1439
|
+
})));
|
|
1440
|
+
writeAgentError(`skill rpc failed error=${message}`);
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
function subscribeToSkillRpc(args) {
|
|
1444
|
+
const subject = buildAgentSkillRpcSubject(args.userId, args.agentId);
|
|
1445
|
+
args.jetstream.nc.subscribe(subject, {
|
|
1446
|
+
callback: (error, msg) => {
|
|
1447
|
+
if (error) {
|
|
1448
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1449
|
+
writeAgentError(`skill rpc subscription error: ${message}`);
|
|
1450
|
+
return;
|
|
1451
|
+
}
|
|
1452
|
+
void handleSkillRpcMessage({
|
|
1453
|
+
msg,
|
|
1454
|
+
agentId: args.agentId,
|
|
1455
|
+
});
|
|
1456
|
+
},
|
|
1457
|
+
});
|
|
1458
|
+
writeAgentInfo(`skill rpc subscribed subject=${subject}`);
|
|
1459
|
+
}
|
|
1309
1460
|
function parseCodexDeviceAuthOutput(raw) {
|
|
1310
1461
|
const text = stripAnsi(raw);
|
|
1311
1462
|
const urlMatch = text.match(/https?:\/\/[^\s]+/i);
|
|
@@ -2275,7 +2426,8 @@ function parseFsRpcAction(value) {
|
|
|
2275
2426
|
value === "read_text" ||
|
|
2276
2427
|
value === "read_file" ||
|
|
2277
2428
|
value === "write_file" ||
|
|
2278
|
-
value === "download_file"
|
|
2429
|
+
value === "download_file" ||
|
|
2430
|
+
value === "delete_path") {
|
|
2279
2431
|
return value;
|
|
2280
2432
|
}
|
|
2281
2433
|
throw new Error("unsupported action");
|
|
@@ -2435,6 +2587,15 @@ async function executeFsRpc(args) {
|
|
|
2435
2587
|
mtimeMs: entry.mtimeMs,
|
|
2436
2588
|
};
|
|
2437
2589
|
}
|
|
2590
|
+
if (action === "delete_path") {
|
|
2591
|
+
await rm(abs, { recursive: true, force: true });
|
|
2592
|
+
return {
|
|
2593
|
+
ok: true,
|
|
2594
|
+
action,
|
|
2595
|
+
path: formatPath(abs),
|
|
2596
|
+
absolutePath: abs.split(path.sep).join("/"),
|
|
2597
|
+
};
|
|
2598
|
+
}
|
|
2438
2599
|
if (action === "download_file") {
|
|
2439
2600
|
const downloadPath = typeof args.request.downloadPath === "string" ? args.request.downloadPath.trim() : "";
|
|
2440
2601
|
if (!downloadPath) {
|
|
@@ -3879,6 +4040,11 @@ async function main() {
|
|
|
3879
4040
|
userId,
|
|
3880
4041
|
agentId: initialAgentId,
|
|
3881
4042
|
});
|
|
4043
|
+
subscribeToSkillRpc({
|
|
4044
|
+
jetstream,
|
|
4045
|
+
userId,
|
|
4046
|
+
agentId: initialAgentId,
|
|
4047
|
+
});
|
|
3882
4048
|
subscribeToRunRpc({
|
|
3883
4049
|
jetstream,
|
|
3884
4050
|
serverBaseUrl,
|