@jiggai/recipes 0.4.30 → 0.4.32
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/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jiggai/recipes",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.32",
|
|
4
4
|
"description": "ClawRecipes plugin for OpenClaw (markdown recipes -> scaffold agents/teams)",
|
|
5
5
|
"main": "index.ts",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
],
|
|
11
11
|
"compat": {
|
|
12
12
|
"pluginApi": "2026.3"
|
|
13
|
+
},
|
|
14
|
+
"build": {
|
|
15
|
+
"openclawVersion": "2026.3.28"
|
|
13
16
|
}
|
|
14
17
|
},
|
|
15
18
|
"publishConfig": {
|
|
@@ -47,6 +47,8 @@ export function normalizeWorkflow(raw: unknown): Workflow {
|
|
|
47
47
|
// LLM: allow either promptTemplatePath (preferred) or inline promptTemplate string
|
|
48
48
|
...(config['promptTemplate'] != null ? { promptTemplate: asString(config['promptTemplate']) } : {}),
|
|
49
49
|
...(config['promptTemplatePath'] != null ? { promptTemplatePath: asString(config['promptTemplatePath']) } : {}),
|
|
50
|
+
...(config['model'] != null ? { model: asString(config['model']) } : {}),
|
|
51
|
+
...(config['provider'] != null ? { provider: asString(config['provider']) } : {}),
|
|
50
52
|
|
|
51
53
|
// Tool
|
|
52
54
|
...(config['tool'] != null ? { tool: asString(config['tool']) } : {}),
|
|
@@ -418,6 +418,19 @@ export async function runWorkflowWorkerTick(api: OpenClawPluginApi, opts: {
|
|
|
418
418
|
|
|
419
419
|
const timeoutMsRaw = Number(asString(action['timeoutMs'] ?? (node as unknown as { config?: unknown })?.config?.['timeoutMs'] ?? '120000'));
|
|
420
420
|
const timeoutMs = Number.isFinite(timeoutMsRaw) && timeoutMsRaw > 0 ? timeoutMsRaw : 120000;
|
|
421
|
+
const configuredModel = asString(action['model'] ?? (node as unknown as { config?: unknown })?.config?.['model']).trim();
|
|
422
|
+
const configuredProvider = asString(action['provider'] ?? (node as unknown as { config?: unknown })?.config?.['provider']).trim();
|
|
423
|
+
let provider = configuredProvider;
|
|
424
|
+
let model = configuredModel;
|
|
425
|
+
if (model) {
|
|
426
|
+
const slash = model.indexOf('/');
|
|
427
|
+
if (slash > 0 && slash < model.length - 1) {
|
|
428
|
+
const modelProvider = model.slice(0, slash).trim();
|
|
429
|
+
const bareModel = model.slice(slash + 1).trim();
|
|
430
|
+
if (!provider) provider = modelProvider;
|
|
431
|
+
if (provider === modelProvider) model = bareModel;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
421
434
|
|
|
422
435
|
// Inject team memory context for LLM nodes
|
|
423
436
|
const memoryContext = await buildMemoryContext(teamDir);
|
|
@@ -430,6 +443,8 @@ export async function runWorkflowWorkerTick(api: OpenClawPluginApi, opts: {
|
|
|
430
443
|
prompt: promptWithMemory,
|
|
431
444
|
input: { teamId, runId, nodeId: node.id, agentId, ...priorInput },
|
|
432
445
|
timeoutMs,
|
|
446
|
+
...(provider ? { provider } : {}),
|
|
447
|
+
...(model ? { model } : {}),
|
|
433
448
|
},
|
|
434
449
|
});
|
|
435
450
|
|
|
@@ -438,16 +453,34 @@ export async function runWorkflowWorkerTick(api: OpenClawPluginApi, opts: {
|
|
|
438
453
|
const payload = details['json'] ?? (Object.keys(details).length ? details : llmRes) ?? null;
|
|
439
454
|
text = JSON.stringify(payload, null, 2);
|
|
440
455
|
} catch (e) {
|
|
441
|
-
|
|
442
|
-
const
|
|
456
|
+
const eRec = asRecord(e);
|
|
457
|
+
const errorDetails = {
|
|
458
|
+
message: e instanceof Error ? e.message : String(e),
|
|
459
|
+
name: e instanceof Error ? e.name : undefined,
|
|
460
|
+
stack: e instanceof Error ? e.stack : undefined,
|
|
461
|
+
error: eRec['error'],
|
|
462
|
+
details: eRec['details'],
|
|
463
|
+
data: eRec['data'],
|
|
464
|
+
cause: e instanceof Error && 'cause' in e ? (e as Error & { cause?: unknown }).cause : undefined,
|
|
465
|
+
};
|
|
466
|
+
const errMsg = `LLM execution failed for node ${nodeLabel(node)}: ${errorDetails.message}`;
|
|
443
467
|
const errorTs = new Date().toISOString();
|
|
444
468
|
await appendRunLog(runPath, (cur) => ({
|
|
445
469
|
...cur,
|
|
446
470
|
status: 'error',
|
|
447
471
|
updatedAt: errorTs,
|
|
448
|
-
nodeStates: {
|
|
449
|
-
|
|
450
|
-
|
|
472
|
+
nodeStates: {
|
|
473
|
+
...(cur.nodeStates ?? {}),
|
|
474
|
+
[node.id]: { status: 'error', ts: errorTs, error: errMsg, details: errorDetails },
|
|
475
|
+
},
|
|
476
|
+
events: [
|
|
477
|
+
...cur.events,
|
|
478
|
+
{ ts: errorTs, type: 'node.error', nodeId: node.id, kind: node.kind, message: errMsg, details: errorDetails },
|
|
479
|
+
],
|
|
480
|
+
nodeResults: [
|
|
481
|
+
...(cur.nodeResults ?? []),
|
|
482
|
+
{ nodeId: node.id, kind: node.kind, agentId: agentIdExec, error: errMsg, details: errorDetails },
|
|
483
|
+
],
|
|
451
484
|
}));
|
|
452
485
|
results.push({ taskId: task.id, runId: task.runId, nodeId: task.nodeId, status: 'error' });
|
|
453
486
|
continue;
|
|
@@ -923,63 +956,127 @@ export async function runWorkflowWorkerTick(api: OpenClawPluginApi, opts: {
|
|
|
923
956
|
const timeoutMsRaw = Number(asString(config['timeoutMs'] ?? '300000'));
|
|
924
957
|
const timeoutMs = Number.isFinite(timeoutMsRaw) && timeoutMsRaw > 0 ? timeoutMsRaw : 300000;
|
|
925
958
|
|
|
926
|
-
// ── Step 1:
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
args: { prompt: step1Prompt, timeoutMs: 60000 },
|
|
946
|
-
});
|
|
959
|
+
// ── Step 1: Prompt refinement (optional — skip for images, use llm-task for video) ──
|
|
960
|
+
let refinedPrompt = prompt.trim();
|
|
961
|
+
|
|
962
|
+
if (mediaType !== 'image') {
|
|
963
|
+
// Only use llm-task refinement for non-image media (video/audio)
|
|
964
|
+
const step1Text = [
|
|
965
|
+
`You are a media prompt engineer for teamId=${teamId}.`,
|
|
966
|
+
`Workflow: ${workflow.name ?? workflow.id ?? workflowFile}`,
|
|
967
|
+
`Node: ${nodeLabel(node)} | Media type: ${mediaType}`,
|
|
968
|
+
`Size: ${size} | Quality: ${quality} | Style: ${style}`,
|
|
969
|
+
`\n---\nINPUT PROMPT\n---\n`,
|
|
970
|
+
prompt.trim(),
|
|
971
|
+
`\n---\nINSTRUCTIONS\n---\n`,
|
|
972
|
+
`Refine the input into a detailed, production-ready ${mediaType} generation prompt.`,
|
|
973
|
+
`Return JSON with exactly one key: "${promptKey}" containing the refined prompt string.`,
|
|
974
|
+
`Example: {"${promptKey}": "A detailed description..."}`,
|
|
975
|
+
].filter(Boolean).join('\n');
|
|
976
|
+
|
|
977
|
+
const step1Prompt = memoryContext ? `${memoryContext}\n\n${step1Text}` : step1Text;
|
|
947
978
|
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
979
|
+
try {
|
|
980
|
+
const step1Res = await toolsInvoke<unknown>(api, {
|
|
981
|
+
tool: 'llm-task',
|
|
982
|
+
action: 'json',
|
|
983
|
+
args: { prompt: step1Prompt, timeoutMs: 60000 },
|
|
984
|
+
});
|
|
985
|
+
const step1Rec = asRecord(step1Res);
|
|
986
|
+
const step1Details = asRecord(step1Rec['details']);
|
|
987
|
+
const step1Json = (step1Details['json'] ?? step1Details ?? step1Res) as Record<string, unknown>;
|
|
988
|
+
const extracted = asString(
|
|
989
|
+
step1Json[promptKey] ?? step1Json['image_prompt'] ?? step1Json['video_prompt'] ?? step1Json['prompt']
|
|
990
|
+
).trim();
|
|
991
|
+
if (extracted) refinedPrompt = extracted;
|
|
992
|
+
} catch {
|
|
993
|
+
// Prompt refinement failed — fall through with original prompt
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
if (!refinedPrompt) throw new Error('Empty prompt for media generation');
|
|
955
998
|
|
|
956
|
-
|
|
999
|
+
// Truncate prompt to stay within DALL-E 3's 4000 char limit
|
|
1000
|
+
const MAX_IMAGE_PROMPT_LEN = 3800; // leave headroom
|
|
1001
|
+
if (mediaType === 'image' && refinedPrompt.length > MAX_IMAGE_PROMPT_LEN) {
|
|
1002
|
+
refinedPrompt = refinedPrompt.slice(0, MAX_IMAGE_PROMPT_LEN).replace(/\s+\S*$/, '') + '...';
|
|
1003
|
+
}
|
|
957
1004
|
|
|
958
1005
|
// ── Step 2: Invoke the skill script to generate actual media ─────
|
|
959
|
-
const skillName = provider.replace(/^skill-/, '');
|
|
960
1006
|
const homedir = process.env.HOME || '/home/control';
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
1007
|
+
const scriptCandidates = mediaType === 'image'
|
|
1008
|
+
? ['generate_image.py', 'generate_image.sh', 'generate.sh']
|
|
1009
|
+
: ['generate_video.py', 'generate_video.sh', 'generate.py', 'generate.sh'];
|
|
1010
|
+
|
|
1011
|
+
// Auto-discover: if provider specifies a skill, try that first, then scan all skills
|
|
1012
|
+
const providerSkill = provider.startsWith('skill-') ? provider.replace(/^skill-/, '') : '';
|
|
1013
|
+
const skillRoots = [
|
|
1014
|
+
path.join(homedir, '.openclaw', 'skills'),
|
|
1015
|
+
path.join(homedir, '.openclaw', 'workspace', 'skills'),
|
|
966
1016
|
];
|
|
1017
|
+
|
|
967
1018
|
let scriptPath = '';
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
1019
|
+
let skillName = providerSkill;
|
|
1020
|
+
|
|
1021
|
+
// Helper: search a specific skill directory for matching scripts
|
|
1022
|
+
const findScript = async (skillDir: string): Promise<string> => {
|
|
1023
|
+
for (const c of scriptCandidates) {
|
|
1024
|
+
const p = path.join(skillDir, c);
|
|
1025
|
+
try { await fs.access(p); return p; } catch { /* skip */ }
|
|
1026
|
+
}
|
|
1027
|
+
return '';
|
|
1028
|
+
};
|
|
1029
|
+
|
|
1030
|
+
// 1) Try the explicitly specified provider skill first
|
|
1031
|
+
if (providerSkill) {
|
|
1032
|
+
for (const root of skillRoots) {
|
|
1033
|
+
scriptPath = await findScript(path.join(root, providerSkill));
|
|
1034
|
+
if (scriptPath) break;
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
// 2) If not found, auto-discover any skill that has the right script
|
|
1039
|
+
if (!scriptPath) {
|
|
1040
|
+
for (const root of skillRoots) {
|
|
1041
|
+
try {
|
|
1042
|
+
const entries = await fs.readdir(root, { withFileTypes: true });
|
|
1043
|
+
for (const entry of entries) {
|
|
1044
|
+
if (!entry.isDirectory()) continue;
|
|
1045
|
+
const found = await findScript(path.join(root, entry.name));
|
|
1046
|
+
if (found) {
|
|
1047
|
+
scriptPath = found;
|
|
1048
|
+
skillName = entry.name;
|
|
1049
|
+
break;
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
} catch { /* root dir doesn't exist */ }
|
|
1053
|
+
if (scriptPath) break;
|
|
973
1054
|
}
|
|
974
|
-
if (scriptPath) break;
|
|
975
1055
|
}
|
|
976
1056
|
|
|
1057
|
+
const skillSearchDirs = providerSkill
|
|
1058
|
+
? skillRoots.map(r => path.join(r, providerSkill))
|
|
1059
|
+
: skillRoots;
|
|
1060
|
+
|
|
977
1061
|
let payload: Record<string, unknown>;
|
|
978
1062
|
if (scriptPath) {
|
|
979
|
-
// Run the skill script
|
|
1063
|
+
// Run the skill script with the refined prompt
|
|
1064
|
+
// Inject env vars from OpenClaw config (gateway doesn't expose them to process.env)
|
|
1065
|
+
let configEnv: Record<string, string> = {};
|
|
1066
|
+
try {
|
|
1067
|
+
const cfgRaw = await fs.readFile(path.join(homedir, '.openclaw', 'openclaw.json'), 'utf8');
|
|
1068
|
+
const cfgParsed = JSON.parse(cfgRaw);
|
|
1069
|
+
if (cfgParsed?.env && typeof cfgParsed.env === 'object') {
|
|
1070
|
+
configEnv = Object.fromEntries(
|
|
1071
|
+
Object.entries(cfgParsed.env).filter(([, v]) => typeof v === 'string')
|
|
1072
|
+
) as Record<string, string>;
|
|
1073
|
+
}
|
|
1074
|
+
} catch { /* config read failed — proceed with process.env only */ }
|
|
1075
|
+
|
|
1076
|
+
const runner = scriptPath.endsWith('.py') ? 'python3' : 'bash';
|
|
980
1077
|
const scriptOutput = execSync(
|
|
981
|
-
|
|
982
|
-
{ cwd: mediaDir, timeout: timeoutMs, encoding: 'utf8', env: { ...process.env, HOME: homedir } }
|
|
1078
|
+
`${runner} ${JSON.stringify(scriptPath)}`,
|
|
1079
|
+
{ cwd: mediaDir, timeout: timeoutMs, encoding: 'utf8', input: refinedPrompt, env: { ...process.env, ...configEnv, HOME: homedir } }
|
|
983
1080
|
).trim();
|
|
984
1081
|
|
|
985
1082
|
// Parse the output — skill scripts print "MEDIA:/path/to/file"
|
|
@@ -1006,7 +1103,10 @@ export async function runWorkflowWorkerTick(api: OpenClawPluginApi, opts: {
|
|
|
1006
1103
|
}
|
|
1007
1104
|
text = JSON.stringify(payload, null, 2);
|
|
1008
1105
|
} catch (e) {
|
|
1009
|
-
const
|
|
1106
|
+
const errDetails = e instanceof Error
|
|
1107
|
+
? { message: e.message, name: e.name, stack: e.stack?.split('\n').slice(0, 5).join(' | ') }
|
|
1108
|
+
: { message: String(e) };
|
|
1109
|
+
const errMsg = `Media generation failed for node ${nodeLabel(node)}: ${JSON.stringify(errDetails)}`;
|
|
1010
1110
|
const errorTs = new Date().toISOString();
|
|
1011
1111
|
await appendRunLog(runPath, (cur) => ({
|
|
1012
1112
|
...cur,
|