@lightcone-ai/daemon 0.15.23 → 0.15.25

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.23",
3
+ "version": "0.15.25",
4
4
  "type": "module",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -157,6 +157,26 @@ async function scrollToTop(page) {
157
157
  });
158
158
  }
159
159
 
160
+ function scalePhaseY(phase, zoom) {
161
+ if (!phase || typeof phase !== 'object') return phase;
162
+ const scale = (v) => (Number.isFinite(Number(v)) ? Math.round(Number(v) * zoom) : v);
163
+ const scaledVisualAction = phase.visual_action && typeof phase.visual_action === 'object'
164
+ ? {
165
+ ...phase.visual_action,
166
+ ...(phase.visual_action.target_y != null ? { target_y: scale(phase.visual_action.target_y) } : {}),
167
+ ...(phase.visual_action.to_y != null ? { to_y: scale(phase.visual_action.to_y) } : {}),
168
+ ...(phase.visual_action.from_y != null ? { from_y: scale(phase.visual_action.from_y) } : {}),
169
+ }
170
+ : phase.visual_action;
171
+ return {
172
+ ...phase,
173
+ ...(phase.target_y != null ? { target_y: scale(phase.target_y) } : {}),
174
+ ...(phase.to_y != null ? { to_y: scale(phase.to_y) } : {}),
175
+ ...(phase.from_y != null ? { from_y: scale(phase.from_y) } : {}),
176
+ ...(scaledVisualAction !== phase.visual_action ? { visual_action: scaledVisualAction } : {}),
177
+ };
178
+ }
179
+
160
180
  export async function recordUrlNarration({
161
181
  plan,
162
182
  output_path,
@@ -167,6 +187,7 @@ export async function recordUrlNarration({
167
187
  viewport = DEFAULT_VIEWPORT,
168
188
  fps = DEFAULT_FPS,
169
189
  settle_ms = 4000,
190
+ page_zoom = 1.0,
170
191
  displayPool = defaultDisplayPool,
171
192
  ffmpegDurationBufferSec = 8,
172
193
  startupProbeMs = 1200,
@@ -174,7 +195,9 @@ export async function recordUrlNarration({
174
195
  xvfbStopTimeoutMs = 5000,
175
196
  postPlanTailMs = 600,
176
197
  } = {}) {
177
- const phases = normalizePlanPhases(plan);
198
+ const zoom = Number.isFinite(Number(page_zoom)) && Number(page_zoom) > 0 ? Number(page_zoom) : 1.0;
199
+ const rawPhases = normalizePlanPhases(plan);
200
+ const phases = zoom !== 1.0 ? rawPhases.map(p => scalePhaseY(p, zoom)) : rawPhases;
178
201
  const executablePlan = {
179
202
  ...(plan && typeof plan === 'object' ? plan : {}),
180
203
  phases,
@@ -220,6 +243,13 @@ export async function recordUrlNarration({
220
243
  settleMs: settle_ms,
221
244
  });
222
245
 
246
+ if (zoom !== 1.0) {
247
+ await browserSession.page.evaluate((z) => {
248
+ document.documentElement.style.zoom = String(z);
249
+ }, zoom);
250
+ await browserSession.page.waitForTimeout(300);
251
+ }
252
+
223
253
  const estimatedDurationMs = estimatePlanDurationMs(executablePlan);
224
254
  const estimatedDurationSec = Math.max(
225
255
  5,
@@ -204,6 +204,55 @@ async function extractStructure(page, { binHeight, minBinChars }) {
204
204
  return rangeFromMatches(matches, root.scrollHeight);
205
205
  }
206
206
 
207
+ function trimNoiseFromBottom(range, pageHeight, detectionStrategy) {
208
+ // Site-specific selectors already point to the exact content element — trust them.
209
+ if (detectionStrategy && detectionStrategy.startsWith('site:')) return range;
210
+
211
+ const [coreTop, rawBottom] = range;
212
+ const minContentHeight = 600; // always keep at least 600px of core content
213
+
214
+ // Selectors that reliably identify non-article noise below the body text.
215
+ const noiseSelectors = [
216
+ // Comment sections
217
+ '[class*="comment-list"]', '[class*="comment-wrap"]', '[class*="commentList"]',
218
+ '[id*="comment"]', '[class*="discuss"]', '[class*="reply-list"]',
219
+ // Related / recommended articles
220
+ '[class*="related-read"]', '[class*="related-article"]', '[class*="relatedArticle"]',
221
+ '[class*="recommend"]', '[class*="more-article"]', '[class*="further-reading"]',
222
+ // Article footer / bottom toolbars
223
+ '[class*="article-footer"]', '[class*="post-footer"]', '[class*="article-bottom"]',
224
+ // Social sharing bars that sit below the body
225
+ '[class*="share-bar"]', '[class*="share-wrap"]', '[class*="article-share"]',
226
+ // Generic page footer inside the core element
227
+ 'footer',
228
+ ];
229
+
230
+ const root = document.scrollingElement || document.documentElement;
231
+ let trimBottom = rawBottom;
232
+
233
+ for (const sel of noiseSelectors) {
234
+ try {
235
+ const elements = document.querySelectorAll(sel);
236
+ for (const el of elements) {
237
+ if (!isVisibleEnough(el, 40)) continue;
238
+ const rect = el.getBoundingClientRect();
239
+ const elTop = Math.round(rect.top + root.scrollTop);
240
+ if (
241
+ elTop > coreTop + minContentHeight
242
+ && elTop < trimBottom
243
+ && rect.height > 60
244
+ ) {
245
+ trimBottom = elTop;
246
+ }
247
+ }
248
+ } catch {
249
+ // ignore selector errors for individual sites
250
+ }
251
+ }
252
+
253
+ return [coreTop, trimBottom];
254
+ }
255
+
207
256
  function collectTextBins({ range, coreElement }) {
208
257
  const [rangeTop, rangeBottom] = range;
209
258
  const root = document.scrollingElement || document.documentElement;
@@ -353,6 +402,10 @@ async function extractStructure(page, { binHeight, minBinChars }) {
353
402
  strategy = `${strategy}:range_fixed`;
354
403
  }
355
404
 
405
+ // Trim coreBottom to exclude footer noise (comments, related articles, recommendations)
406
+ // that commonly appear below the article body.
407
+ coreRange = trimNoiseFromBottom(coreRange, totalHeight, strategy);
408
+
356
409
  const bins = collectTextBins({ range: coreRange, coreElement });
357
410
  const focusRange = (() => {
358
411
  const span = coreRange[1] - coreRange[0];
@@ -1480,6 +1480,7 @@ server.tool('record_url_narration',
1480
1480
  }).optional().describe('Default 1080x1920 (mobile portrait). Override only if the plan requires a different shape.'),
1481
1481
  fps: z.number().optional().describe('Default 30. Do not change unless needed.'),
1482
1482
  settle_ms: z.number().optional().describe('Default 4000. Settle wait after navigation before recording starts.'),
1483
+ page_zoom: z.number().optional().describe('Browser zoom factor applied before recording. Default 1.0 (no zoom). Use e.g. 1.1 to zoom in 10% so text appears larger. Plan Y coordinates are automatically scaled.'),
1483
1484
  },
1484
1485
  async (args) => {
1485
1486
  if (isBlockedCvmaxEditorVideoTool('record_url_narration')) {
package/src/mcp-config.js CHANGED
@@ -111,7 +111,7 @@ export function buildSkillMcpServers({
111
111
  const resolvedArgs = (mc.args ?? []).map(arg => resolveSkillArg(arg, config));
112
112
  const resolvedEnv = {};
113
113
  for (const envKey of (mc.env ?? [])) {
114
- resolvedEnv[envKey] = agentEnv[envKey] ?? process.env[envKey] ?? '';
114
+ resolvedEnv[envKey] = agentEnv[envKey] || process.env[envKey] || '';
115
115
  }
116
116
 
117
117
  if (mcpServers[mc.server]) {
@@ -141,6 +141,7 @@ export async function runRecordUrlNarrationTool({
141
141
  viewport: finalInput.viewport,
142
142
  fps: finalInput.fps,
143
143
  settle_ms: finalInput.settle_ms,
144
+ page_zoom: finalInput.page_zoom,
144
145
  });
145
146
 
146
147
  return {