@jiggai/recipes 0.4.51 → 0.4.53

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.
@@ -56,7 +56,7 @@ When a node's `text` field contains JSON, ClawRecipes automatically extracts nes
56
56
  ### Available Template Variables
57
57
  - `{{nodeId.text}}` — Raw JSON string
58
58
  - `{{nodeId.title}}` — Extracted: "Product Launch"
59
- - `{{nodeId.approved_json}}` — For non-string values: "true"
59
+ - `{{nodeId.approved}}` — For non-string values: "true"
60
60
 
61
61
  ### Deeply Nested Extraction
62
62
  If the JSON contains nested objects, fields are flattened:
@@ -68,7 +68,7 @@ If the JSON contains nested objects, fields are flattened:
68
68
  ```
69
69
 
70
70
  Available as:
71
- - `{{nodeId.meta_json}}` — Full meta object as JSON string
71
+ - `{{nodeId.meta}}` — Full meta object as JSON string
72
72
  - `{{nodeId.author}}` — "John" (if meta.author is a string)
73
73
 
74
74
  ## LLM Node Structured Output
@@ -92,7 +92,7 @@ LLM nodes with `outputFields` configuration produce predictable JSON structures:
92
92
  ```
93
93
  Title: {{nodeId.title}}
94
94
  Tags: {{nodeId.tags}}
95
- Metadata: {{nodeId.metadata_json}}
95
+ Metadata: {{nodeId.metadata}}
96
96
  ```
97
97
 
98
98
  ## Usage Examples
@@ -144,7 +144,7 @@ Template substitution happens in the workflow worker during node execution. The
144
144
  1. **Global vars** are built from run metadata and timestamps
145
145
  2. **Node outputs** are loaded from each completed node's output file
146
146
  3. **JSON parsing** attempts to extract fields from the `text` field
147
- 4. **Nested extraction** flattens nested objects with `_json` suffixes for non-strings
147
+ 4. **Nested extraction** flattens nested objects, stringifying non-string values under their declared field name
148
148
  5. **Template replacement** applies all variables using simple string substitution
149
149
 
150
150
  ### Performance Notes
@@ -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.51",
5
+ "version": "0.4.53",
6
6
  "configSchema": {
7
7
  "type": "object",
8
8
  "additionalProperties": false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jiggai/recipes",
3
- "version": "0.4.51",
3
+ "version": "0.4.53",
4
4
  "description": "ClawRecipes plugin for OpenClaw (markdown recipes -> scaffold agents/teams)",
5
5
  "main": "index.ts",
6
6
  "type": "commonjs",
@@ -161,26 +161,42 @@ export async function generateKitchenManifest(opts: GenerateManifestOptions): Pr
161
161
  };
162
162
  }
163
163
 
164
- // Fetch agents and recipes via CLI (reuses existing OpenClaw infrastructure)
164
+ // Read agents directly from config (avoids subprocess which can silently fail)
165
165
  let agents: AgentManifestEntry[] = [];
166
166
  try {
167
- const res = await api.runtime.system.runCommandWithTimeout(
168
- ['openclaw', 'agents', 'list', '--json'],
169
- { timeoutMs: 15_000 },
170
- );
171
- if (res.code === 0 && res.stdout) {
172
- agents = JSON.parse(res.stdout) as AgentManifestEntry[];
167
+ const list = (api.config as { agents?: { list?: Array<Record<string, unknown>> } }).agents?.list;
168
+ if (Array.isArray(list)) {
169
+ agents = list.map((a) => ({
170
+ id: String(a.id ?? ''),
171
+ identityName: typeof (a.identity as Record<string, unknown> | undefined)?.name === 'string'
172
+ ? (a.identity as { name: string }).name
173
+ : undefined,
174
+ workspace: typeof a.workspace === 'string' ? a.workspace : undefined,
175
+ model: typeof a.model === 'string' ? a.model : undefined,
176
+ isDefault: a.default === true,
177
+ })).filter((a) => a.id);
173
178
  }
174
179
  } catch { /* best-effort */ }
175
180
 
176
- let recipes: RecipeManifestEntry[] = [];
181
+ // Read recipes from filesystem (avoids subprocess which can silently fail)
182
+ const recipes: RecipeManifestEntry[] = [];
177
183
  try {
178
- const res = await api.runtime.system.runCommandWithTimeout(
179
- ['openclaw', 'recipes', 'list'],
180
- { timeoutMs: 15_000 },
181
- );
182
- if (res.code === 0 && res.stdout) {
183
- recipes = JSON.parse(res.stdout) as RecipeManifestEntry[];
184
+ const { listRecipeFiles } = await import('./recipes');
185
+ const { getRecipesConfig } = await import('./config');
186
+ const { parseFrontmatter } = await import('./recipe-frontmatter');
187
+ const cfg = getRecipesConfig(api.config);
188
+ const files = await listRecipeFiles(api, cfg);
189
+ for (const f of files) {
190
+ const md = await fs.readFile(f.path, 'utf8');
191
+ const { frontmatter } = parseFrontmatter(md);
192
+ if (frontmatter.id && frontmatter.name) {
193
+ recipes.push({
194
+ id: frontmatter.id,
195
+ name: frontmatter.name,
196
+ kind: frontmatter.kind === 'team' ? 'team' : 'agent',
197
+ source: f.source,
198
+ });
199
+ }
184
200
  }
185
201
  } catch { /* best-effort */ }
186
202
 
@@ -0,0 +1,49 @@
1
+ import type { OpenClawPluginApi } from 'openclaw/plugin-sdk';
2
+
3
+ function asRecord(v: unknown): Record<string, unknown> | null {
4
+ return v && typeof v === 'object' && !Array.isArray(v) ? (v as Record<string, unknown>) : null;
5
+ }
6
+
7
+ function asString(v: unknown): string {
8
+ return typeof v === 'string' ? v : (v == null ? '' : String(v));
9
+ }
10
+
11
+ function asPort(v: unknown): number | null {
12
+ if (typeof v === 'number' && Number.isFinite(v) && v > 0) return v;
13
+ if (typeof v === 'string' && v.trim()) {
14
+ const parsed = Number(v.trim());
15
+ if (Number.isFinite(parsed) && parsed > 0) return parsed;
16
+ }
17
+ return null;
18
+ }
19
+
20
+ function trimTrailingSlash(url: string): string {
21
+ return url.replace(/\/+$/, '');
22
+ }
23
+
24
+ function buildBaseUrl(host: string, port: number | null): string {
25
+ const trimmedHost = host.trim();
26
+ if (!trimmedHost) return 'http://localhost:7777';
27
+ if (/^https?:\/\//i.test(trimmedHost)) return trimTrailingSlash(trimmedHost);
28
+ const safeHost = trimmedHost.includes(':') && !trimmedHost.startsWith('[') ? `[${trimmedHost}]` : trimmedHost;
29
+ return trimTrailingSlash(`http://${safeHost}${port ? `:${port}` : ''}`);
30
+ }
31
+
32
+ export function getKitchenBaseUrl(api: OpenClawPluginApi): string {
33
+ const config = asRecord((api as unknown as { config?: unknown }).config) ?? {};
34
+ const envVars = asRecord(asRecord(config.env)?.vars);
35
+ const envBaseUrl = asString(envVars?.CK_BASE_URL).trim();
36
+ if (envBaseUrl) return trimTrailingSlash(envBaseUrl);
37
+
38
+ const kitchenConfig = asRecord(asRecord(asRecord(config.plugins)?.entries)?.kitchen)?.config;
39
+ const host = asString(kitchenConfig?.host).trim();
40
+ const port = asPort(kitchenConfig?.port);
41
+ if (host) return buildBaseUrl(host, port);
42
+
43
+ return 'http://localhost:7777';
44
+ }
45
+
46
+ export function buildKitchenWorkflowReviewUrl(api: OpenClawPluginApi, teamId: string, workflowId: string): string {
47
+ const baseUrl = getKitchenBaseUrl(api);
48
+ return `${baseUrl}/teams/${encodeURIComponent(teamId)}/workflows/${encodeURIComponent(workflowId)}`;
49
+ }
@@ -13,6 +13,7 @@ import { dequeueNextTask, enqueueTask, hasPendingTaskFor, releaseTaskClaim, comp
13
13
  import { loadPriorLlmInput, loadProposedPostTextFromPriorNode } from './workflow-node-output-readers';
14
14
  import { readTextFile } from './workflow-runner-io';
15
15
  import { resolveApprovalBindingTarget } from './workflow-node-executor';
16
+ import { buildKitchenWorkflowReviewUrl } from './kitchen-review-url';
16
17
  import {
17
18
  asRecord, asString, isRecord,
18
19
  normalizeWorkflow,
@@ -166,14 +167,14 @@ async function buildTemplateVars(
166
167
  if (typeof nestedValue === 'string') {
167
168
  vars[`${nid}.${nestedKey}`] = nestedValue;
168
169
  } else if (nestedValue !== null && nestedValue !== undefined) {
169
- vars[`${nid}.${nestedKey}_json`] = JSON.stringify(nestedValue);
170
+ vars[`${nid}.${nestedKey}`] = JSON.stringify(nestedValue);
170
171
  }
171
172
  }
172
173
  }
173
174
  } catch { /* nested parse fail is fine */ }
174
175
  }
175
176
  } else if (value !== null && value !== undefined) {
176
- vars[`${nid}.${key}_json`] = JSON.stringify(value);
177
+ vars[`${nid}.${key}`] = JSON.stringify(value);
177
178
  }
178
179
  }
179
180
  }
@@ -1032,6 +1033,7 @@ export async function runWorkflowWorkerTick(api: OpenClawPluginApi, opts: {
1032
1033
  }
1033
1034
  proposed = sanitizeDraftOnlyText(proposed);
1034
1035
 
1036
+ const kitchenReviewUrl = buildKitchenWorkflowReviewUrl(api, teamId, String(workflow.id ?? ''));
1035
1037
  const msg = [
1036
1038
  `Approval requested: ${workflow.name ?? workflow.id ?? workflowFile}`,
1037
1039
  `Ticket: ${path.relative(teamDir, curTicketPath)}`,
@@ -1040,7 +1042,7 @@ export async function runWorkflowWorkerTick(api: OpenClawPluginApi, opts: {
1040
1042
  `\nReply with:`,
1041
1043
  `- approve ${code}`,
1042
1044
  `- decline ${code} <what to change>`,
1043
- `\n(You can also review in Kitchen: ${process.env['CK_BASE_URL'] || 'http://localhost:7777'}/teams/${teamId}/workflows/${workflow.id ?? ''})`,
1045
+ `\n(You can also review in Kitchen: ${kitchenReviewUrl})`,
1044
1046
  ].join('\n');
1045
1047
 
1046
1048
  await toolsInvoke<ToolTextResult>(api, {