@jiggai/recipes 0.4.31 → 0.4.33

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.
@@ -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.31",
5
+ "version": "0.4.33",
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.31",
3
+ "version": "0.4.33",
4
4
  "description": "ClawRecipes plugin for OpenClaw (markdown recipes -> scaffold agents/teams)",
5
5
  "main": "index.ts",
6
6
  "type": "commonjs",
@@ -956,63 +956,127 @@ export async function runWorkflowWorkerTick(api: OpenClawPluginApi, opts: {
956
956
  const timeoutMsRaw = Number(asString(config['timeoutMs'] ?? '300000'));
957
957
  const timeoutMs = Number.isFinite(timeoutMsRaw) && timeoutMsRaw > 0 ? timeoutMsRaw : 300000;
958
958
 
959
- // ── Step 1: LLM refines the prompt ──────────────────────────────
960
- const step1Text = [
961
- `You are a media prompt engineer for teamId=${teamId}.`,
962
- `Workflow: ${workflow.name ?? workflow.id ?? workflowFile}`,
963
- `Node: ${nodeLabel(node)} | Media type: ${mediaType}`,
964
- `Size: ${size} | Quality: ${quality} | Style: ${style}`,
965
- `\n---\nINPUT PROMPT\n---\n`,
966
- prompt.trim(),
967
- `\n---\nINSTRUCTIONS\n---\n`,
968
- `Refine the input into a detailed, production-ready ${mediaType} generation prompt.`,
969
- `Return JSON with exactly one key: "${promptKey}" containing the refined prompt string.`,
970
- `Example: {"${promptKey}": "A detailed description..."}`,
971
- ].filter(Boolean).join('\n');
972
-
973
- const step1Prompt = memoryContext ? `${memoryContext}\n\n${step1Text}` : step1Text;
974
-
975
- const step1Res = await toolsInvoke<unknown>(api, {
976
- tool: 'llm-task',
977
- action: 'json',
978
- args: { prompt: step1Prompt, timeoutMs: 60000 },
979
- });
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;
980
978
 
981
- // Extract the refined prompt
982
- const step1Rec = asRecord(step1Res);
983
- const step1Details = asRecord(step1Rec['details']);
984
- const step1Json = (step1Details['json'] ?? step1Details ?? step1Res) as Record<string, unknown>;
985
- const refinedPrompt = asString(
986
- step1Json[promptKey] ?? step1Json['image_prompt'] ?? step1Json['video_prompt'] ?? step1Json['prompt'] ?? prompt
987
- ).trim();
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
+ }
988
996
 
989
- if (!refinedPrompt) throw new Error('LLM returned empty refined prompt');
997
+ if (!refinedPrompt) throw new Error('Empty prompt for media generation');
998
+
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
+ }
990
1004
 
991
1005
  // ── Step 2: Invoke the skill script to generate actual media ─────
992
- const skillName = provider.replace(/^skill-/, '');
993
1006
  const homedir = process.env.HOME || '/home/control';
994
-
995
- // Search for the skill's executable script
996
- const skillSearchDirs = [
997
- path.join(homedir, '.openclaw', 'skills', skillName),
998
- path.join(homedir, '.openclaw', 'workspace', 'skills', skillName),
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'),
999
1016
  ];
1017
+
1000
1018
  let scriptPath = '';
1001
- for (const dir of skillSearchDirs) {
1002
- const candidates = [`generate_image.sh`, `generate_video.sh`, `generate.sh`];
1003
- for (const c of candidates) {
1004
- const p = path.join(dir, c);
1005
- try { await fs.access(p); scriptPath = p; break; } catch { /* skip */ }
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;
1006
1054
  }
1007
- if (scriptPath) break;
1008
1055
  }
1009
1056
 
1057
+ const skillSearchDirs = providerSkill
1058
+ ? skillRoots.map(r => path.join(r, providerSkill))
1059
+ : skillRoots;
1060
+
1010
1061
  let payload: Record<string, unknown>;
1011
1062
  if (scriptPath) {
1012
- // Run the skill script directly with the refined prompt
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';
1013
1077
  const scriptOutput = execSync(
1014
- `bash ${JSON.stringify(scriptPath)} ${JSON.stringify(refinedPrompt)}`,
1015
- { 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 } }
1016
1080
  ).trim();
1017
1081
 
1018
1082
  // Parse the output — skill scripts print "MEDIA:/path/to/file"
@@ -1039,7 +1103,10 @@ export async function runWorkflowWorkerTick(api: OpenClawPluginApi, opts: {
1039
1103
  }
1040
1104
  text = JSON.stringify(payload, null, 2);
1041
1105
  } catch (e) {
1042
- const errMsg = `Media generation failed for node ${nodeLabel(node)}: ${e instanceof Error ? e.message : String(e)}`;
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)}`;
1043
1110
  const errorTs = new Date().toISOString();
1044
1111
  await appendRunLog(runPath, (cur) => ({
1045
1112
  ...cur,