@lightcone-ai/daemon 0.15.49 → 0.15.50

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.49",
3
+ "version": "0.15.50",
4
4
  "type": "module",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -6,11 +6,7 @@ import { createReadStream, existsSync, mkdirSync, readFileSync, writeFileSync }
6
6
  import { createHash, randomUUID } from 'crypto';
7
7
  import path, { extname } from 'path';
8
8
  import { recordUrlNarration } from './_vendor/video/recorder/index.js';
9
- import {
10
- VIDEO_EXT,
11
- VIDEO_MIME,
12
- writeLocalFileToWorkspace,
13
- } from './workspace-file-upload.js';
9
+ import { writeLocalFileToWorkspace } from './workspace-file-upload.js';
14
10
  import { runRecordUrlNarrationTool } from './record-url-narration-tool.js';
15
11
  import { runSubmitToLibraryTool } from './submit-to-library-tool.js';
16
12
  import { runRenderTextToImageTool } from './tools/render-text-to-image.js';
@@ -491,7 +487,7 @@ async function directApi(method, apiPath, body) {
491
487
  async function directApiVideoUpload(apiPath, {
492
488
  localPath,
493
489
  filename,
494
- contentType = VIDEO_MIME,
490
+ contentType = 'video/mp4',
495
491
  }) {
496
492
  const url = `${SERVER_URL}/internal/agent/${AGENT_ID}${apiPath}`;
497
493
  const headers = {
@@ -1090,44 +1086,22 @@ server.tool('write_workspace_file', 'Write a local file directly to the shared w
1090
1086
  uploadWorkspaceMemory: async ({ workspacePath, workspaceId, content }) => {
1091
1087
  await api('PUT', `/workspace-memory?path=${encodeURIComponent(workspacePath)}&workspaceId=${encodeURIComponent(workspaceId)}`, { content });
1092
1088
  },
1093
- uploadVideo: async ({
1094
- localPath: videoLocalPath,
1095
- workspacePath,
1096
- workspaceId,
1097
- filename,
1098
- mime,
1099
- }) => {
1100
- const uploadResult = await runMandatoryLocalTool({
1101
- toolName: 'write_workspace_file',
1102
- toolInput: {
1103
- file_path,
1104
- path: workspacePath,
1105
- upload_mode: 'upload_video',
1106
- },
1107
- executor: async () => directApiVideoUpload(
1108
- `/upload-video?workspaceId=${encodeURIComponent(workspaceId)}&filename=${encodeURIComponent(filename)}&path=${encodeURIComponent(workspacePath)}`,
1109
- {
1110
- localPath: videoLocalPath,
1111
- filename,
1112
- contentType: mime,
1113
- }
1114
- ),
1115
- });
1116
- return {
1117
- mode: 'upload-video',
1118
- mime: VIDEO_MIME,
1119
- bytes: Number(uploadResult?.sizeBytes) || 0,
1120
- };
1089
+ presign: async ({ workspaceId, path: filePath, size, mime, sha256 }) => {
1090
+ return api('POST', '/storage/presign', { workspaceId, path: filePath, size, mime, sha256 });
1091
+ },
1092
+ confirmUpload: async ({ workspaceId, path: filePath, objectKey }) => {
1093
+ await api('POST', '/storage/confirm', { workspaceId, path: filePath, objectKey });
1121
1094
  },
1122
1095
  });
1123
1096
 
1124
- const finalMime = result?.mime ?? (extname(path || localPath).toLowerCase() === VIDEO_EXT ? VIDEO_MIME : 'application/octet-stream');
1097
+ const finalMime = result?.mime ?? 'application/octet-stream';
1125
1098
  const finalBytes = Number.isFinite(result?.bytes) ? result.bytes : 0;
1126
1099
  const sizeText = finalBytes > 0 ? formatBytes(finalBytes) : 'unknown size';
1100
+ const asyncNote = result?.async ? ' (uploading in background)' : '';
1127
1101
  return {
1128
1102
  content: [{
1129
1103
  type: 'text',
1130
- text: `Saved local file to workspace: ${path} (${finalMime}, ${sizeText})`,
1104
+ text: `Saved local file to workspace: ${path} (${finalMime}, ${sizeText})${asyncNote}`,
1131
1105
  }],
1132
1106
  };
1133
1107
  });
@@ -294,7 +294,6 @@ export async function materializeWorkspaceMedia({
294
294
  machineApiKey,
295
295
  jobId,
296
296
  fetchWorkspaceFile = fetchPublishJobWorkspaceFile,
297
- streamVideoFile = streamPublishJobVideo,
298
297
  writeBinaryFile = writeFileAtomically,
299
298
  }) {
300
299
  if (!filePath) return filePath;
@@ -317,20 +316,33 @@ export async function materializeWorkspaceMedia({
317
316
  jobId,
318
317
  relPath: workspacePath.relPath,
319
318
  });
320
- if (isWorkspaceCacheValid(localPath, data)) return localPath;
321
319
 
322
- if (data?.videoId) {
323
- await streamVideoFile({
324
- serverUrl,
325
- machineApiKey,
326
- jobId,
327
- videoId: data.videoId,
328
- localPath,
329
- });
330
- } else {
320
+ // New: signed URL from object storage
321
+ if (data?.signedUrl) {
322
+ // Use sha256-based cache key in tmp/cache to avoid duplicate downloads
323
+ const cacheDir = path.join(workspaceRootDir, 'tmp', 'cache');
324
+ const ext = path.extname(workspacePath.relPath) || '';
325
+ const cacheKey = data.object_hash ?? data.size_bytes?.toString() ?? 'nokey';
326
+ const cachePath = path.join(cacheDir, `${cacheKey}${ext}`);
327
+
328
+ if (existsSync(cachePath)) return cachePath;
329
+
330
+ mkdirSync(cacheDir, { recursive: true });
331
+ const res = await fetch(data.signedUrl);
332
+ if (!res.ok) throw new Error(`Failed to download from signed URL: ${res.status}`);
333
+ const buf = Buffer.from(await res.arrayBuffer());
334
+ writeFileAtomically(cachePath, buf);
335
+ return cachePath;
336
+ }
337
+
338
+ // Legacy: text content in DB
339
+ if (data?.content != null) {
340
+ if (isWorkspaceCacheValid(localPath, data)) return localPath;
331
341
  writeBinaryFile(localPath, decodeWorkspaceContent(data.content));
342
+ return localPath;
332
343
  }
333
- return localPath;
344
+
345
+ throw new Error(`No content or signedUrl in workspace file response for: ${filePath}`);
334
346
  }
335
347
 
336
348
  async function materializeMedia({
@@ -1,36 +1,90 @@
1
1
  import path, { extname } from 'node:path';
2
- import { readFileSync } from 'node:fs';
2
+ import { readFileSync, createReadStream } from 'node:fs';
3
+ import { createHash } from 'node:crypto';
3
4
 
4
5
  export const WORKSPACE_BINARY_MIME = {
5
- '.png': 'image/png',
6
- '.jpg': 'image/jpeg',
6
+ '.mp4': 'video/mp4',
7
+ '.png': 'image/png',
8
+ '.jpg': 'image/jpeg',
7
9
  '.jpeg': 'image/jpeg',
8
10
  '.webp': 'image/webp',
9
- '.gif': 'image/gif',
10
- '.pdf': 'application/pdf',
11
+ '.gif': 'image/gif',
12
+ '.pdf': 'application/pdf',
13
+ '.mp3': 'audio/mpeg',
14
+ '.wav': 'audio/wav',
15
+ '.m4a': 'audio/mp4',
11
16
  };
12
17
 
13
- export const VIDEO_EXT = '.mp4';
14
- export const VIDEO_MIME = 'video/mp4';
18
+ const TEXT_EXTS = new Set([
19
+ '.md', '.txt', '.json', '.js', '.ts', '.html', '.css',
20
+ '.csv', '.xml', '.yaml', '.yml', '.sh',
21
+ ]);
22
+
23
+ const LARGE_FILE_THRESHOLD = 5 * 1024 * 1024; // 5MB
15
24
 
16
25
  export function resolveWorkspaceFileUploadPlan({ localPath, workspacePath }) {
17
26
  const target = String(workspacePath ?? '').trim() || String(localPath ?? '').trim();
18
27
  const ext = extname(target).toLowerCase();
19
28
  const filename = path.basename(target || localPath);
20
- if (ext === VIDEO_EXT) {
21
- return {
22
- mode: 'upload-video',
23
- ext,
24
- filename,
25
- mime: VIDEO_MIME,
26
- };
29
+ const mime = WORKSPACE_BINARY_MIME[ext] ?? 'application/octet-stream';
30
+ const isText = TEXT_EXTS.has(ext);
31
+ return { ext, filename, mime, isText };
32
+ }
33
+
34
+ function sha256ofBuffer(buf) {
35
+ return createHash('sha256').update(buf).digest('hex');
36
+ }
37
+
38
+ /**
39
+ * Upload a local file to the workspace via the new presign → PUT → confirm flow.
40
+ * For large files (>= 5MB), presign returns immediately (DB status = uploading)
41
+ * and the PUT + confirm happen in the background.
42
+ */
43
+ async function uploadBinaryFile({
44
+ localPath,
45
+ workspacePath,
46
+ workspaceId,
47
+ mime,
48
+ presign, // async fn({ workspaceId, path, size, mime, sha256 }) → { uploadUrl, objectKey, alreadyExists }
49
+ confirmUpload, // async fn({ workspaceId, path, objectKey }) → void
50
+ }) {
51
+ const buf = readFileSync(localPath);
52
+ const size = buf.length;
53
+ const sha256 = sha256ofBuffer(buf);
54
+
55
+ const { uploadUrl, objectKey, alreadyExists } = await presign({
56
+ workspaceId,
57
+ path: workspacePath,
58
+ size,
59
+ mime,
60
+ sha256,
61
+ });
62
+
63
+ if (alreadyExists) {
64
+ return { mode: 'object-storage', mime, bytes: size, objectKey, deduped: true };
27
65
  }
28
- return {
29
- mode: 'workspace-memory',
30
- ext,
31
- filename,
32
- mime: WORKSPACE_BINARY_MIME[ext] ?? 'application/octet-stream',
66
+
67
+ const isLarge = size >= LARGE_FILE_THRESHOLD;
68
+
69
+ const doPut = async () => {
70
+ const { default: fetch } = await import('node-fetch');
71
+ const resp = await fetch(uploadUrl, {
72
+ method: 'PUT',
73
+ body: buf,
74
+ headers: { 'Content-Type': mime, 'Content-Length': String(size) },
75
+ });
76
+ if (!resp.ok) throw new Error(`COS PUT failed: ${resp.status} ${await resp.text()}`);
77
+ await confirmUpload({ workspaceId, path: workspacePath, objectKey });
33
78
  };
79
+
80
+ if (isLarge) {
81
+ // Fire-and-forget for large files; server already wrote `uploading` status
82
+ doPut().catch(err => console.error('[workspace-upload] large file background upload failed:', err?.message));
83
+ return { mode: 'object-storage', mime, bytes: size, objectKey, async: true };
84
+ }
85
+
86
+ await doPut();
87
+ return { mode: 'object-storage', mime, bytes: size, objectKey, async: false };
34
88
  }
35
89
 
36
90
  export async function writeLocalFileToWorkspace({
@@ -38,34 +92,33 @@ export async function writeLocalFileToWorkspace({
38
92
  workspacePath,
39
93
  workspaceId,
40
94
  readFileSyncFn = readFileSync,
41
- uploadWorkspaceMemory,
42
- uploadVideo,
95
+ uploadWorkspaceMemory, // legacy text path
96
+ presign,
97
+ confirmUpload,
43
98
  }) {
44
99
  const plan = resolveWorkspaceFileUploadPlan({ localPath, workspacePath });
45
- if (plan.mode === 'upload-video') {
46
- return uploadVideo({
100
+
101
+ if (!plan.isText) {
102
+ return uploadBinaryFile({
47
103
  localPath,
48
104
  workspacePath,
49
105
  workspaceId,
50
- filename: plan.filename,
51
106
  mime: plan.mime,
52
- mode: plan.mode,
107
+ presign,
108
+ confirmUpload,
53
109
  });
54
110
  }
55
111
 
112
+ // Text files: existing path via workspace-memory
56
113
  const buf = readFileSyncFn(localPath);
57
- const content = `data:${plan.mime};base64,${buf.toString('base64')}`;
114
+ const content = buf.toString('utf8');
58
115
  await uploadWorkspaceMemory({
59
116
  workspacePath,
60
117
  workspaceId,
61
118
  content,
62
119
  mime: plan.mime,
63
120
  bytes: buf.length,
64
- mode: plan.mode,
121
+ mode: 'workspace-memory',
65
122
  });
66
- return {
67
- mode: plan.mode,
68
- mime: plan.mime,
69
- bytes: buf.length,
70
- };
123
+ return { mode: 'workspace-memory', mime: plan.mime, bytes: buf.length };
71
124
  }