@lightcone-ai/daemon 0.15.63 → 0.15.65

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightcone-ai/daemon",
3
- "version": "0.15.63",
3
+ "version": "0.15.65",
4
4
  "type": "module",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/mcp-config.js CHANGED
@@ -62,25 +62,40 @@ function resolveSkillArg(arg, config) {
62
62
  return arg;
63
63
  }
64
64
 
65
+ // MCP servers that talk to the lightcone server at runtime — either a full
66
+ // server-backed MCP (mysql / publisher / platform / domain-data services) or a
67
+ // thin-proxy (`daemon/mcp-servers/_thin-proxy/forward.js`) whose tools forward
68
+ // to /internal/agent/:agentId/mcp/:serverId/:toolName. All of these need the
69
+ // SERVER_URL / MACHINE_API_KEY / AGENT_ID triple injected, or the proxy throws
70
+ // "thin-proxy missing SERVER_URL / MACHINE_API_KEY / AGENT_ID env".
71
+ const SERVER_BACKED_MCP_SERVERS = new Set([
72
+ 'mysql',
73
+ 'publisher',
74
+ 'platform',
75
+ 'research-fetch',
76
+ 'wechat-mp-fetch',
77
+ 'market-data-query',
78
+ 'company-fundamentals',
79
+ 'industry-report',
80
+ 'risk-metrics',
81
+ 'compliance-check',
82
+ 'portfolio-read',
83
+ 'portfolio-analysis',
84
+ // thin-proxy MCP services migrated to the server (roadmap §4) — every one of
85
+ // these uses startThinProxy() and requires the SERVER_URL/MACHINE_API_KEY/AGENT_ID triple.
86
+ 'video-narration-planner',
87
+ 'page-understanding',
88
+ 'platform-policy-db',
89
+ 'keyword-research',
90
+ 'audience-research',
91
+ 'hook-pattern-library',
92
+ ]);
93
+
65
94
  function baseEnvForServer(serverKey, { serverUrl, authToken, agentId, workspaceId, workspaceDir }) {
66
95
  if (serverKey === 'workspace-migrate') {
67
96
  return { SERVER_URL: serverUrl, MACHINE_API_KEY: authToken, AGENT_ID: agentId };
68
97
  }
69
- if (
70
- serverKey === 'mysql'
71
- || serverKey === 'publisher'
72
- || serverKey === 'platform'
73
- || serverKey === 'research-fetch'
74
- || serverKey === 'wechat-mp-fetch'
75
- || serverKey === 'market-data-query'
76
- || serverKey === 'company-fundamentals'
77
- || serverKey === 'industry-report'
78
- || serverKey === 'risk-metrics'
79
- || serverKey === 'compliance-check'
80
- || serverKey === 'portfolio-read'
81
- || serverKey === 'portfolio-analysis'
82
- || serverKey === 'video-narration-planner'
83
- ) {
98
+ if (SERVER_BACKED_MCP_SERVERS.has(serverKey)) {
84
99
  return {
85
100
  SERVER_URL: serverUrl,
86
101
  MACHINE_API_KEY: authToken,
@@ -1,5 +1,6 @@
1
1
  import path from 'path';
2
2
  import os from 'os';
3
+ import fs from 'fs';
3
4
  import { randomUUID } from 'crypto';
4
5
  import { composeVideoV2 } from '../_vendor/video/composer-v2/index.js';
5
6
 
@@ -11,11 +12,22 @@ function toolError(text) {
11
12
  return { isError: true, content: [{ type: 'text', text }] };
12
13
  }
13
14
 
15
+ // A real screenshot / rendered card / video frame is at least tens of KB. A
16
+ // few-KB image is almost certainly a blank or broken capture (e.g. the page
17
+ // hadn't finished loading when screenshotted). Composing from those produces a
18
+ // near-blank video, so reject up front rather than emit garbage.
19
+ const MIN_IMAGE_BYTES = 4 * 1024;
20
+
21
+ function statSizeOrNull(p) {
22
+ try { return fs.statSync(p).size; } catch { return null; }
23
+ }
24
+
14
25
  export async function runComposeVideoV2Tool({ segments, outro_paths, format, resolution, output_path, workspaceDir }) {
15
26
  if (!Array.isArray(segments) || segments.length === 0) {
16
27
  return toolError('segments must be a non-empty array.');
17
28
  }
18
29
 
30
+ const imagePaths = [];
19
31
  for (let i = 0; i < segments.length; i++) {
20
32
  const seg = segments[i];
21
33
  const kind = seg.visual_kind;
@@ -30,6 +42,30 @@ export async function runComposeVideoV2Tool({ segments, outro_paths, format, res
30
42
  if (kind !== 'carousel' && !seg.visual_path) {
31
43
  return toolError(`segments[${i}]: visual_path required for kind=${kind}.`);
32
44
  }
45
+ if (kind === 'image' && seg.visual_path) imagePaths.push(seg.visual_path);
46
+ if (kind === 'carousel' && Array.isArray(seg.visual_paths)) imagePaths.push(...seg.visual_paths.filter(Boolean));
47
+ }
48
+
49
+ // Garbage-in guard: refuse to compose from blank/broken source images.
50
+ for (const p of new Set(imagePaths)) {
51
+ const size = statSizeOrNull(p);
52
+ if (size === null) {
53
+ return toolError(`Source image not found: ${p}. Re-capture/render it before composing.`);
54
+ }
55
+ if (size < MIN_IMAGE_BYTES) {
56
+ return toolError(
57
+ `Source image ${p} is only ${size} bytes — almost certainly a blank or broken capture `
58
+ + `(e.g. the page hadn't finished loading when screenshotted). Re-capture with the page fully loaded, then retry.`
59
+ );
60
+ }
61
+ }
62
+ // Heuristic warning: a multi-segment image video that reuses one single image
63
+ // will look near-static — usually a sign the source page didn't render and the
64
+ // agent fell back to one blank screenshot.
65
+ let warning = null;
66
+ if (imagePaths.length >= 2 && new Set(imagePaths).size === 1) {
67
+ warning = `WARNING: all ${imagePaths.length} image segments reuse the same file (${imagePaths[0]}). `
68
+ + 'The output will be near-static — verify the source page actually rendered before submitting this video.';
33
69
  }
34
70
 
35
71
  const outDir = workspaceDir
@@ -46,13 +82,15 @@ export async function runComposeVideoV2Tool({ segments, outro_paths, format, res
46
82
  output_path: outPath,
47
83
  });
48
84
 
49
- return toolText([
85
+ const lines = [
50
86
  'compose_video_v2 completed.',
51
87
  `path=${result.path}`,
52
88
  `duration_ms=${result.duration_ms}`,
53
89
  `segments=${segments.length}`,
54
90
  `outro_clips=${(outro_paths ?? []).length}`,
55
- ].join('\n'));
91
+ ];
92
+ if (warning) lines.push(warning);
93
+ return toolText(lines.join('\n'));
56
94
  } catch (error) {
57
95
  return toolError(`compose_video_v2 failed: ${error.message}`);
58
96
  }