@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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -161,26 +161,42 @@ export async function generateKitchenManifest(opts: GenerateManifestOptions): Pr
|
|
|
161
161
|
};
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
//
|
|
164
|
+
// Read agents directly from config (avoids subprocess which can silently fail)
|
|
165
165
|
let agents: AgentManifestEntry[] = [];
|
|
166
166
|
try {
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
181
|
+
// Read recipes from filesystem (avoids subprocess which can silently fail)
|
|
182
|
+
const recipes: RecipeManifestEntry[] = [];
|
|
177
183
|
try {
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
);
|
|
182
|
-
|
|
183
|
-
|
|
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}
|
|
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}
|
|
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: ${
|
|
1045
|
+
`\n(You can also review in Kitchen: ${kitchenReviewUrl})`,
|
|
1044
1046
|
].join('\n');
|
|
1045
1047
|
|
|
1046
1048
|
await toolsInvoke<ToolTextResult>(api, {
|