doer-agent 0.2.9 → 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.
Files changed (2) hide show
  1. package/dist/agent.js +210 -6
  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 [];
@@ -286,6 +290,25 @@ function normalizeEnvPatch(value) {
286
290
  }
287
291
  return out;
288
292
  }
293
+ function normalizeRunImagePaths(value) {
294
+ if (!Array.isArray(value)) {
295
+ return [];
296
+ }
297
+ const seen = new Set();
298
+ const out = [];
299
+ for (const item of value) {
300
+ if (typeof item !== "string") {
301
+ continue;
302
+ }
303
+ const normalized = item.trim();
304
+ if (!normalized || seen.has(normalized)) {
305
+ continue;
306
+ }
307
+ seen.add(normalized);
308
+ out.push(normalized);
309
+ }
310
+ return out;
311
+ }
289
312
  async function prepareTaskRuntimeConfig(args) {
290
313
  const bundle = await postJson(`${args.serverBaseUrl}/api/agent/tasks/${encodeURIComponent(args.taskId)}/runtime-config`, {
291
314
  userId: args.userId,
@@ -408,6 +431,7 @@ function normalizeRunRpcRequest(args) {
408
431
  }
409
432
  const runId = typeof args.request.runId === "string" && args.request.runId.trim() ? args.request.runId.trim() : null;
410
433
  const prompt = typeof args.request.prompt === "string" && args.request.prompt.trim() ? args.request.prompt.trim() : null;
434
+ const imagePaths = normalizeRunImagePaths(args.request.imagePaths);
411
435
  const sessionId = typeof args.request.sessionId === "string" && args.request.sessionId.trim() ? args.request.sessionId.trim() : null;
412
436
  const model = normalizeCodexModel(args.request.model);
413
437
  if (action === "start" && !prompt) {
@@ -426,6 +450,7 @@ function normalizeRunRpcRequest(args) {
426
450
  action,
427
451
  runId,
428
452
  prompt,
453
+ imagePaths,
429
454
  sessionId,
430
455
  model,
431
456
  cwd,
@@ -1185,13 +1210,14 @@ function normalizeCodexModel(value) {
1185
1210
  function buildManagedCodexArgs(args) {
1186
1211
  const promptArgs = ["--", args.prompt];
1187
1212
  const fixedArgs = ["--dangerously-bypass-approvals-and-sandbox"];
1213
+ const imageArgs = args.imagePaths.flatMap((imagePath) => ["--image", imagePath]);
1188
1214
  return [
1189
1215
  ...fixedArgs,
1190
1216
  "--model",
1191
1217
  args.model,
1192
1218
  ...(args.sessionId
1193
- ? ["exec", "resume", "--json", args.sessionId, ...promptArgs]
1194
- : ["exec", "--json", ...promptArgs]),
1219
+ ? ["exec", "resume", "--json", ...imageArgs, args.sessionId, ...promptArgs]
1220
+ : ["exec", "--json", ...imageArgs, ...promptArgs]),
1195
1221
  ];
1196
1222
  }
1197
1223
  function buildLocalCodexCliCommand(args) {
@@ -1234,11 +1260,12 @@ function spawnManagedCodexCommand(args) {
1234
1260
  child.stderr?.setEncoding("utf8");
1235
1261
  return child;
1236
1262
  }
1237
- async function runLocalCodexCli(args, timeoutMs) {
1263
+ async function runLocalCodexCli(args, timeoutMs, envPatch) {
1238
1264
  const command = buildLocalCodexCliCommand(args);
1239
1265
  const workspaceRoot = workspaceRootOverride ?? (process.env.WORKSPACE?.trim() || process.cwd());
1240
1266
  const env = {
1241
1267
  ...process.env,
1268
+ ...(envPatch ?? {}),
1242
1269
  WORKSPACE: workspaceRoot,
1243
1270
  CODEX_HOME: resolveCodexHomePath(),
1244
1271
  };
@@ -1284,6 +1311,152 @@ async function runLocalCodexCli(args, timeoutMs) {
1284
1311
  });
1285
1312
  });
1286
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
+ }
1287
1460
  function parseCodexDeviceAuthOutput(raw) {
1288
1461
  const text = stripAnsi(raw);
1289
1462
  const urlMatch = text.match(/https?:\/\/[^\s]+/i);
@@ -1962,6 +2135,7 @@ async function handleRunRpcMessage(args) {
1962
2135
  sessionId: request.sessionId,
1963
2136
  codexArgs: buildManagedCodexArgs({
1964
2137
  prompt: request.prompt ?? "",
2138
+ imagePaths: request.imagePaths,
1965
2139
  sessionId: request.sessionId,
1966
2140
  model: request.model,
1967
2141
  }),
@@ -2252,7 +2426,8 @@ function parseFsRpcAction(value) {
2252
2426
  value === "read_text" ||
2253
2427
  value === "read_file" ||
2254
2428
  value === "write_file" ||
2255
- value === "download_file") {
2429
+ value === "download_file" ||
2430
+ value === "delete_path") {
2256
2431
  return value;
2257
2432
  }
2258
2433
  throw new Error("unsupported action");
@@ -2412,6 +2587,15 @@ async function executeFsRpc(args) {
2412
2587
  mtimeMs: entry.mtimeMs,
2413
2588
  };
2414
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
+ }
2415
2599
  if (action === "download_file") {
2416
2600
  const downloadPath = typeof args.request.downloadPath === "string" ? args.request.downloadPath.trim() : "";
2417
2601
  if (!downloadPath) {
@@ -2611,9 +2795,19 @@ function sanitizeSessionRpcRawLine(line) {
2611
2795
  }
2612
2796
  try {
2613
2797
  const parsed = JSON.parse(line);
2614
- if (!isObjectRecord(parsed) || !isObjectRecord(parsed.payload) || parsed.type !== "response_item") {
2798
+ if (!isObjectRecord(parsed)) {
2615
2799
  return line;
2616
2800
  }
2801
+ if (parsed.type === "compacted" || parsed.type === "turn_context" || parsed.type === "session_meta") {
2802
+ return null;
2803
+ }
2804
+ if (!isObjectRecord(parsed.payload) || parsed.type !== "response_item") {
2805
+ return line;
2806
+ }
2807
+ const payloadType = typeof parsed.payload.type === "string" ? parsed.payload.type : "";
2808
+ if (payloadType === "message" || payloadType === "reasoning") {
2809
+ return null;
2810
+ }
2617
2811
  return JSON.stringify({
2618
2812
  ...parsed,
2619
2813
  payload: sanitizeSessionRpcPayload(parsed.payload),
@@ -3007,9 +3201,14 @@ async function getAgentSessionRawRows(args) {
3007
3201
  let lineNumber = startLineIndex + 1;
3008
3202
  for (const line of lines) {
3009
3203
  if (line.trim()) {
3204
+ const sanitized = sanitizeSessionRpcRawLine(line);
3205
+ if (!sanitized) {
3206
+ lineNumber += 1;
3207
+ continue;
3208
+ }
3010
3209
  rawRows.push({
3011
3210
  id: lineNumber,
3012
- raw: sanitizeSessionRpcRawLine(line),
3211
+ raw: sanitized,
3013
3212
  });
3014
3213
  }
3015
3214
  lineNumber += 1;
@@ -3841,6 +4040,11 @@ async function main() {
3841
4040
  userId,
3842
4041
  agentId: initialAgentId,
3843
4042
  });
4043
+ subscribeToSkillRpc({
4044
+ jetstream,
4045
+ userId,
4046
+ agentId: initialAgentId,
4047
+ });
3844
4048
  subscribeToRunRpc({
3845
4049
  jetstream,
3846
4050
  serverBaseUrl,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "doer-agent",
3
- "version": "0.2.9",
3
+ "version": "0.3.1",
4
4
  "description": "Reverse-polling agent runtime for doer",
5
5
  "type": "module",
6
6
  "main": "dist/agent.js",