@jiggai/recipes 0.4.71 → 0.4.73

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/docs/COMMANDS.md CHANGED
@@ -221,12 +221,6 @@ openclaw recipes handoff --team-id development-team --ticket 0007 --tester test
221
221
  openclaw recipes complete --team-id development-team --ticket 0007
222
222
  ```
223
223
 
224
- ### Clean up stale assignment stubs for done work
225
-
226
- ```bash
227
- openclaw recipes cleanup-closed-assignments --team-id development-team
228
- openclaw recipes cleanup-closed-assignments --team-id development-team --ticket 0050 0064
229
- ```
230
224
 
231
225
  ---
232
226
 
@@ -157,11 +157,6 @@ openclaw recipes move-ticket --team-id development-team --ticket 0007 --to done
157
157
  openclaw recipes assign --team-id development-team --ticket 0007 --owner devops
158
158
  ```
159
159
 
160
- ### Clean up stale assignment stubs for closed work
161
-
162
- ```bash
163
- openclaw recipes cleanup-closed-assignments --team-id development-team
164
- ```
165
160
 
166
161
  ---
167
162
 
package/index.ts CHANGED
@@ -27,7 +27,6 @@ import {
27
27
  } from "./src/handlers/install";
28
28
  import {
29
29
  handleAssign,
30
- handleCleanupClosedAssignments,
31
30
  handleDispatch,
32
31
  handleHandoff,
33
32
  handleMoveTicket,
@@ -797,19 +796,6 @@ workflows
797
796
  console.log(JSON.stringify({ ok: true, moved: { from: res.from, to: res.to } }, null, 2));
798
797
  });
799
798
 
800
- cmd
801
- .command("cleanup-closed-assignments")
802
- .description("Archive assignment stubs for tickets already in work/done (prevents done work resurfacing)")
803
- .requiredOption("--team-id <teamId>", "Team id")
804
- .option("--ticket <ticketNums...>", "Optional ticket numbers to target (e.g. 0050 0064)")
805
- .action(async (options: { teamId?: string; ticket?: string[] }) => {
806
- if (!options.teamId) throw new Error("--team-id is required");
807
- const res = await handleCleanupClosedAssignments(api, {
808
- teamId: options.teamId,
809
- ticketNums: options.ticket,
810
- });
811
- console.log(JSON.stringify(res, null, 2));
812
- });
813
799
 
814
800
  cmd
815
801
  .command("assign")
@@ -2,7 +2,7 @@
2
2
  "id": "recipes",
3
3
  "name": "Recipes",
4
4
  "description": "Markdown recipes that scaffold agents and teams (workspace-local).",
5
- "version": "0.4.71",
5
+ "version": "0.4.73",
6
6
  "configSchema": {
7
7
  "type": "object",
8
8
  "additionalProperties": false,
@@ -72,5 +72,5 @@
72
72
  "help": "Controls whether recipe-defined cron jobs are installed during scaffold. off=never, prompt=ask, on=auto-install."
73
73
  }
74
74
  },
75
- "main": "index.ts"
75
+ "main": "dist/index.js"
76
76
  }
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@jiggai/recipes",
3
- "version": "0.4.71",
3
+ "version": "0.4.73",
4
4
  "description": "ClawRecipes plugin for OpenClaw (markdown recipes -> scaffold agents/teams)",
5
- "main": "index.ts",
5
+ "main": "dist/index.js",
6
6
  "type": "commonjs",
7
7
  "openclaw": {
8
8
  "extensions": [
@@ -10,16 +10,20 @@
10
10
  ],
11
11
  "compat": {
12
12
  "pluginApi": ">=1.0.0",
13
- "pluginApiRange": ">=2026.3"
13
+ "pluginApiRange": ">=2026.5"
14
14
  },
15
15
  "build": {
16
- "openclawVersion": "2026.4.20"
17
- }
16
+ "openclawVersion": "2026.5.7"
17
+ },
18
+ "runtimeExtensions": [
19
+ "./dist/index.js"
20
+ ]
18
21
  },
19
22
  "publishConfig": {
20
23
  "access": "public"
21
24
  },
22
25
  "files": [
26
+ "dist/",
23
27
  "index.ts",
24
28
  "src/",
25
29
  "openclaw.plugin.json",
@@ -44,9 +48,11 @@
44
48
  "jscpd": "jscpd src/ index.ts --min-lines 8 --min-tokens 50",
45
49
  "prepare": "husky || true",
46
50
  "check:plugin-version": "node scripts/check-openclaw-plugin-version.mjs",
47
- "prepack": "npm run -s sync:plugin-version && npm run -s check:plugin-version",
48
- "prepublishOnly": "npm run -s sync:plugin-version && npm run -s check:plugin-version",
49
- "sync:plugin-version": "node scripts/sync-openclaw-plugin-version.mjs"
51
+ "prepack": "npm run -s build:plugin && npm run -s sync:plugin-version && npm run -s check:plugin-version",
52
+ "prepublishOnly": "npm run -s build:plugin && npm run -s sync:plugin-version && npm run -s check:plugin-version",
53
+ "sync:plugin-version": "node scripts/sync-openclaw-plugin-version.mjs",
54
+ "build:plugin": "esbuild index.ts --bundle --platform=node --format=cjs --target=node20 --outfile=dist/index.js --external:openclaw/plugin-sdk",
55
+ "verify:runtime-package": "node scripts/verify-runtime-package.mjs"
50
56
  },
51
57
  "keywords": [
52
58
  "openclaw",
@@ -69,7 +75,8 @@
69
75
  "jscpd": "^4.0.5",
70
76
  "lint-staged": "^16.2.7",
71
77
  "typescript-eslint": "^8.18.0",
72
- "vitest": "^3.2.4"
78
+ "vitest": "^3.2.4",
79
+ "esbuild": "^0.27.3"
73
80
  },
74
81
  "lint-staged": {
75
82
  "*.ts": [
@@ -18,7 +18,8 @@ export function workspacePath(api: OpenClawPluginApi, ...parts: string[]) {
18
18
  * @returns Array of { source, path }
19
19
  */
20
20
  export async function listRecipeFiles(api: OpenClawPluginApi, cfg: Required<RecipesConfig>) {
21
- const builtinDir = path.resolve(__dirname, "..", "..", "recipes", "default");
21
+ const pluginRoot = api.rootDir ?? path.resolve(__dirname, "..", "..");
22
+ const builtinDir = path.resolve(pluginRoot, "recipes", "default");
22
23
  const workspaceDir = workspacePath(api, cfg.workspaceRecipesDir);
23
24
 
24
25
  const out: Array<{ source: "builtin" | "workspace"; path: string }> = [];
@@ -305,14 +305,12 @@ export async function executeWorkflowNodes(opts: {
305
305
  `- openclaw recipes workflows resume --team-id ${teamId} --run-id ${runId}`,
306
306
  ].join('\n');
307
307
 
308
- // Deliver the approval prompt. Wrap in try/catch + Telegram bot-API
309
- // fallback because OpenClaw 2026.4.26's gateway has been observed to
310
- // return "Tool not available: message" from /tools/invoke even though
311
- // the channel itself is healthy. The approval.json file is durable
312
- // either way, so we never fail the run on delivery error — operators
313
- // can still approve via the kitchen UI or `openclaw recipes workflows
314
- // approve` CLI when this falls through.
315
- let approvalDelivered = false;
308
+ // Deliver the approval prompt via the OpenClaw `message` tool. The tool
309
+ // requires the calling agent to have `group:messaging` (or `message`)
310
+ // in its tools.allow policy see openclaw/openclaw#74780. If delivery
311
+ // fails (misconfigured policy, channel adapter down, etc.) we log and
312
+ // continue; approval.json is durable, so operators can still approve
313
+ // via the kitchen UI or `openclaw recipes workflows approve` CLI.
316
314
  try {
317
315
  await toolsInvoke<ToolTextResult>(api, {
318
316
  tool: 'message',
@@ -324,39 +322,10 @@ export async function executeWorkflowNodes(opts: {
324
322
  message: msg,
325
323
  },
326
324
  });
327
- approvalDelivered = true;
328
325
  } catch (err) {
329
326
  const errMsg = err instanceof Error ? err.message : String(err);
330
327
  console.warn(`[workflow] tools.invoke('message') failed for run ${runId} on node ${node.id}: ${errMsg}`);
331
- if (channel === 'telegram') {
332
- try {
333
- const cfg = await loadOpenClawConfig(api);
334
- const tgToken = (cfg as { channels?: { telegram?: { botToken?: string } } })
335
- .channels?.telegram?.botToken;
336
- if (tgToken) {
337
- const tgRes = await fetch(`https://api.telegram.org/bot${tgToken}/sendMessage`, {
338
- method: 'POST',
339
- headers: { 'content-type': 'application/json' },
340
- body: JSON.stringify({ chat_id: target, text: msg }),
341
- });
342
- if (tgRes.ok) {
343
- approvalDelivered = true;
344
- console.log(`[workflow] approval delivered via direct telegram bot API for run ${runId}`);
345
- } else {
346
- const tgBody = await tgRes.text().catch(() => '');
347
- console.error(`[workflow] telegram fallback failed (${tgRes.status}) for run ${runId}: ${tgBody}`);
348
- }
349
- } else {
350
- console.error(`[workflow] telegram fallback skipped for run ${runId}: missing channels.telegram.botToken in openclaw config`);
351
- }
352
- } catch (fbErr) {
353
- const fbMsg = fbErr instanceof Error ? fbErr.message : String(fbErr);
354
- console.error(`[workflow] telegram fallback threw for run ${runId}: ${fbMsg}`);
355
- }
356
- }
357
- if (!approvalDelivered) {
358
- console.warn(`[workflow] approval message not delivered for run ${runId}; approve via kitchen UI or CLI`);
359
- }
328
+ console.warn(`[workflow] approval message not delivered for run ${runId}; approve via kitchen UI or CLI`);
360
329
  }
361
330
 
362
331
  const waitingTs = new Date().toISOString();
@@ -12,7 +12,7 @@ import type { WorkflowLane, WorkflowNode, RunLog } from './workflow-types';
12
12
  import { dequeueNextTask, enqueueTask, hasPendingTaskFor, releaseTaskClaim, compactQueue } from './workflow-queue';
13
13
  import { currentLockOwner, isLockHolderDead } from './lock-liveness';
14
14
  import { loadPriorLlmInput, loadProposedPostTextFromPriorNode } from './workflow-node-output-readers';
15
- import { readTextFile } from './workflow-runner-io';
15
+ import { readTextFile, readJsonFile } from './workflow-runner-io';
16
16
  import { resolveApprovalBindingTarget } from './workflow-node-executor';
17
17
  import { buildKitchenWorkflowReviewUrl } from './kitchen-review-url';
18
18
  import {
@@ -45,7 +45,7 @@ async function buildMemoryContext(teamDir: string): Promise<string> {
45
45
  // Read pinned items first (highest priority)
46
46
  const pinnedPath = path.join(memoryDir, 'pinned.jsonl');
47
47
  if (await fileExists(pinnedPath)) {
48
- const pinnedContent = await fs.readFile(pinnedPath, 'utf8');
48
+ const pinnedContent = await readTextFile(pinnedPath);
49
49
  const pinnedItems = pinnedContent.trim().split('\n').filter(Boolean);
50
50
 
51
51
  if (pinnedItems.length > 0) {
@@ -75,7 +75,7 @@ async function buildMemoryContext(teamDir: string): Promise<string> {
75
75
  if (currentTokens > maxTokens * 0.8) break; // Leave room for recent items
76
76
 
77
77
  const filePath = path.join(memoryDir, filename);
78
- const content = await fs.readFile(filePath, 'utf8');
78
+ const content = await readTextFile(filePath);
79
79
  const items = content.trim().split('\n').filter(Boolean);
80
80
 
81
81
  if (items.length > 0) {
@@ -151,7 +151,7 @@ async function buildTemplateVars(
151
151
  if (nid && nrOutPath) {
152
152
  try {
153
153
  const outAbs = path.resolve(teamDir, nrOutPath);
154
- const outputContent = await fs.readFile(outAbs, 'utf8');
154
+ const outputContent = await readTextFile(outAbs);
155
155
  vars[`${nid}.output`] = outputContent;
156
156
 
157
157
  try {
@@ -314,7 +314,7 @@ async function checkWaitingHandoffs(api: OpenClawPluginApi, teamId: string, team
314
314
  const runPath = path.join(runDir, 'run.json');
315
315
  let run: RunLog;
316
316
  try {
317
- const raw = await fs.readFile(runPath, 'utf8');
317
+ const raw = await readTextFile(runPath);
318
318
  run = JSON.parse(raw) as RunLog;
319
319
  } catch { continue; }
320
320
 
@@ -335,7 +335,7 @@ async function checkWaitingHandoffs(api: OpenClawPluginApi, teamId: string, team
335
335
  nodeOutputRel: string;
336
336
  };
337
337
  try {
338
- marker = JSON.parse(await fs.readFile(waitPath, 'utf8'));
338
+ marker = await readJsonFile<typeof marker>(waitPath);
339
339
  } catch { continue; }
340
340
 
341
341
  // Check timeout
@@ -416,7 +416,7 @@ async function checkWaitingHandoffs(api: OpenClawPluginApi, teamId: string, team
416
416
  const workflowsDir = path.join(teamDir, 'shared-context', 'workflows');
417
417
  let workflow;
418
418
  try {
419
- const wfRaw = await fs.readFile(path.join(workflowsDir, run.workflow.file), 'utf8');
419
+ const wfRaw = await readTextFile(path.join(workflowsDir, run.workflow.file));
420
420
  workflow = normalizeWorkflow(JSON.parse(wfRaw));
421
421
  } catch { workflow = null; }
422
422
 
@@ -1505,7 +1505,7 @@ export async function runWorkflowWorkerTick(api: OpenClawPluginApi, opts: {
1505
1505
  if (!wf.endsWith('.json')) continue;
1506
1506
  try {
1507
1507
  const wfPath = path.join(targetWorkflowsDir, wf);
1508
- const wfRaw = await fs.readFile(wfPath, 'utf8');
1508
+ const wfRaw = await readTextFile(wfPath);
1509
1509
  const wfParsed = JSON.parse(wfRaw);
1510
1510
  if (String(wfParsed.id ?? '') === targetWorkflowId || String(wfParsed.name ?? '') === targetWorkflowId) {
1511
1511
  targetWorkflowFile = wf;