@effectnode/media 0.1.0

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.
Files changed (133) hide show
  1. package/bin/effectnode-media +2 -0
  2. package/dist/backend/index.d.ts +8 -0
  3. package/dist/backend/index.js +13 -0
  4. package/dist/backend/movie-backend/agent/agent-backend.d.ts +16 -0
  5. package/dist/backend/movie-backend/agent/agent-backend.js +703 -0
  6. package/dist/backend/movie-backend/agent/storyprompt.d.ts +1 -0
  7. package/dist/backend/movie-backend/agent/storyprompt.js +90 -0
  8. package/dist/backend/movie-backend/agent/tools/edit-image.d.ts +3 -0
  9. package/dist/backend/movie-backend/agent/tools/edit-image.js +192 -0
  10. package/dist/backend/movie-backend/agent/tools/get-time.d.ts +3 -0
  11. package/dist/backend/movie-backend/agent/tools/get-time.js +7 -0
  12. package/dist/backend/movie-backend/agent/tools/grep-files.d.ts +3 -0
  13. package/dist/backend/movie-backend/agent/tools/grep-files.js +59 -0
  14. package/dist/backend/movie-backend/agent/tools/image-to-video-generation.d.ts +3 -0
  15. package/dist/backend/movie-backend/agent/tools/image-to-video-generation.js +160 -0
  16. package/dist/backend/movie-backend/agent/tools/index.d.ts +7 -0
  17. package/dist/backend/movie-backend/agent/tools/index.js +59 -0
  18. package/dist/backend/movie-backend/agent/tools/list-files.d.ts +3 -0
  19. package/dist/backend/movie-backend/agent/tools/list-files.js +17 -0
  20. package/dist/backend/movie-backend/agent/tools/read-file.d.ts +3 -0
  21. package/dist/backend/movie-backend/agent/tools/read-file.js +28 -0
  22. package/dist/backend/movie-backend/agent/tools/remove-file.d.ts +3 -0
  23. package/dist/backend/movie-backend/agent/tools/remove-file.js +22 -0
  24. package/dist/backend/movie-backend/agent/tools/rename-file.d.ts +3 -0
  25. package/dist/backend/movie-backend/agent/tools/rename-file.js +37 -0
  26. package/dist/backend/movie-backend/agent/tools/show-image.d.ts +3 -0
  27. package/dist/backend/movie-backend/agent/tools/show-image.js +28 -0
  28. package/dist/backend/movie-backend/agent/tools/stitch-videos.d.ts +3 -0
  29. package/dist/backend/movie-backend/agent/tools/stitch-videos.js +96 -0
  30. package/dist/backend/movie-backend/agent/tools/text-to-video-generation.d.ts +3 -0
  31. package/dist/backend/movie-backend/agent/tools/text-to-video-generation.js +125 -0
  32. package/dist/backend/movie-backend/agent/tools/types.d.ts +15 -0
  33. package/dist/backend/movie-backend/agent/tools/types.js +1 -0
  34. package/dist/backend/movie-backend/agent/tools/update-file.d.ts +3 -0
  35. package/dist/backend/movie-backend/agent/tools/update-file.js +27 -0
  36. package/dist/backend/movie-backend/agent/tools/write-file.d.ts +3 -0
  37. package/dist/backend/movie-backend/agent/tools/write-file.js +26 -0
  38. package/dist/backend/movie-backend/agent/workspace.d.ts +25 -0
  39. package/dist/backend/movie-backend/agent/workspace.js +124 -0
  40. package/dist/backend/movie-backend/core.d.ts +4 -0
  41. package/dist/backend/movie-backend/core.js +1167 -0
  42. package/dist/backend/movie-backend/generation-queue.d.ts +24 -0
  43. package/dist/backend/movie-backend/generation-queue.js +766 -0
  44. package/dist/backend/movie-backend/process.d.ts +21 -0
  45. package/dist/backend/movie-backend/process.js +98 -0
  46. package/dist/backend/movie-backend/render-media.d.ts +48 -0
  47. package/dist/backend/movie-backend/render-media.js +3311 -0
  48. package/dist/backend/routes/health.d.ts +1 -0
  49. package/dist/backend/routes/health.js +9 -0
  50. package/dist/backend/routes/index.d.ts +1 -0
  51. package/dist/backend/routes/index.js +11 -0
  52. package/dist/backend/routes/media.d.ts +1 -0
  53. package/dist/backend/routes/media.js +13 -0
  54. package/dist/backend/routes/projects.d.ts +8 -0
  55. package/dist/backend/routes/projects.js +68 -0
  56. package/dist/backend/routes/voice.d.ts +1 -0
  57. package/dist/backend/routes/voice.js +39 -0
  58. package/dist/backend/routes.d.ts +1 -0
  59. package/dist/backend/routes.js +20 -0
  60. package/dist/backend/server.d.ts +1 -0
  61. package/dist/backend/server.js +23 -0
  62. package/dist/backend/workspace.d.ts +5 -0
  63. package/dist/backend/workspace.js +27 -0
  64. package/dist/backend/ws/index.d.ts +3 -0
  65. package/dist/backend/ws/index.js +27 -0
  66. package/dist/backend/ws/messages.d.ts +8 -0
  67. package/dist/backend/ws/messages.js +21 -0
  68. package/dist/backend/ws.d.ts +3 -0
  69. package/dist/backend/ws.js +29 -0
  70. package/dist/index.d.ts +2 -0
  71. package/dist/index.js +80 -0
  72. package/frontend/index.html +22 -0
  73. package/frontend/public/ffmpeg/ffmpeg-core.js +16 -0
  74. package/frontend/public/ffmpeg/ffmpeg-core.wasm +0 -0
  75. package/frontend/src/index.css +46 -0
  76. package/frontend/src/main.tsx +13 -0
  77. package/frontend/src/movie-app/AppRouter.tsx +14 -0
  78. package/frontend/src/movie-app/MediaStudio.tsx +9 -0
  79. package/frontend/src/movie-app/SetupPage.tsx +293 -0
  80. package/frontend/src/movie-app/components/AgentWorkspace.tsx +574 -0
  81. package/frontend/src/movie-app/components/ChatUI/ChatUI.tsx +655 -0
  82. package/frontend/src/movie-app/components/ChatUI/MarkdownMessage.tsx +267 -0
  83. package/frontend/src/movie-app/components/EditorTabs/AgentTab.tsx +61 -0
  84. package/frontend/src/movie-app/components/EditorTabs/BatchImageToVideoTab.tsx +927 -0
  85. package/frontend/src/movie-app/components/EditorTabs/BatchVideoTab.tsx +777 -0
  86. package/frontend/src/movie-app/components/EditorTabs/BatchVoiceVideoTab.tsx +913 -0
  87. package/frontend/src/movie-app/components/EditorTabs/CharacterSheet.tsx +234 -0
  88. package/frontend/src/movie-app/components/EditorTabs/CharactersTab.tsx +664 -0
  89. package/frontend/src/movie-app/components/EditorTabs/CropTool.tsx +251 -0
  90. package/frontend/src/movie-app/components/EditorTabs/ExtendVideoTab.tsx +305 -0
  91. package/frontend/src/movie-app/components/EditorTabs/ExtractImageTab.tsx +249 -0
  92. package/frontend/src/movie-app/components/EditorTabs/FastImageEditTab.tsx +555 -0
  93. package/frontend/src/movie-app/components/EditorTabs/GenerateVideoTab.tsx +722 -0
  94. package/frontend/src/movie-app/components/EditorTabs/LlmServerTab.tsx +40 -0
  95. package/frontend/src/movie-app/components/EditorTabs/MlxVlmServerPanel.tsx +490 -0
  96. package/frontend/src/movie-app/components/EditorTabs/MovieStudioTab.tsx +938 -0
  97. package/frontend/src/movie-app/components/EditorTabs/ReferencesToVideoTab.tsx +881 -0
  98. package/frontend/src/movie-app/components/EditorTabs/SceneVisualTab.tsx +267 -0
  99. package/frontend/src/movie-app/components/EditorTabs/StoryWriterTab.tsx +51 -0
  100. package/frontend/src/movie-app/components/EditorTabs/TaskQueuePanel.tsx +246 -0
  101. package/frontend/src/movie-app/components/EditorTabs/TerminalLogPanel.tsx +49 -0
  102. package/frontend/src/movie-app/components/EditorTabs/TextToImageTab.tsx +574 -0
  103. package/frontend/src/movie-app/components/EditorTabs/VoiceChatPanel.tsx +320 -0
  104. package/frontend/src/movie-app/components/ProjectEditorPage.tsx +636 -0
  105. package/frontend/src/movie-app/components/ProjectManager.tsx +381 -0
  106. package/frontend/src/movie-app/index.css +46 -0
  107. package/frontend/src/movie-app/index.html +16 -0
  108. package/frontend/src/movie-app/lambobo.png +0 -0
  109. package/frontend/src/movie-app/lib/batchImageToVideoStorage.ts +69 -0
  110. package/frontend/src/movie-app/lib/batchVideoStorage.ts +68 -0
  111. package/frontend/src/movie-app/lib/batchVoiceStorage.ts +75 -0
  112. package/frontend/src/movie-app/lib/ffmpeg.ts +42 -0
  113. package/frontend/src/movie-app/lib/referencesToVideoStorage.ts +64 -0
  114. package/frontend/src/movie-app/lib/textToImageStorage.ts +61 -0
  115. package/frontend/src/movie-app/public/ffmpeg/ffmpeg-core.js +16 -0
  116. package/frontend/src/movie-app/public/ffmpeg/ffmpeg-core.wasm +0 -0
  117. package/frontend/src/movie-app/stores/batchImageToVideoStore.ts +736 -0
  118. package/frontend/src/movie-app/stores/batchVideoStore.ts +670 -0
  119. package/frontend/src/movie-app/stores/batchVoiceStore.ts +990 -0
  120. package/frontend/src/movie-app/stores/characterStore.ts +109 -0
  121. package/frontend/src/movie-app/stores/chatStore.ts +459 -0
  122. package/frontend/src/movie-app/stores/generationStore.ts +2140 -0
  123. package/frontend/src/movie-app/stores/logStore.ts +25 -0
  124. package/frontend/src/movie-app/stores/movieStudioStore.ts +709 -0
  125. package/frontend/src/movie-app/stores/projectStore.ts +124 -0
  126. package/frontend/src/movie-app/stores/queueStore.ts +214 -0
  127. package/frontend/src/movie-app/stores/referencesToVideoStore.ts +467 -0
  128. package/frontend/src/movie-app/stores/sceneVisualStore.ts +251 -0
  129. package/frontend/src/movie-app/stores/voiceChatStore.ts +170 -0
  130. package/frontend/src/movie-app/stores/workspaceStore.ts +158 -0
  131. package/frontend/src/vite-env.d.ts +1 -0
  132. package/frontend/tsconfig.json +19 -0
  133. package/package.json +65 -0
@@ -0,0 +1,28 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { resolveWorkspacePath, classifyFile } from "../workspace";
3
+ const tool = {
4
+ name: "read_file",
5
+ description: "Read the text content of a file in the workspace.",
6
+ parameters: {
7
+ type: "object",
8
+ properties: {
9
+ path: {
10
+ type: "string",
11
+ description: "File path relative to the workspace (e.g. memories/foo.md)",
12
+ },
13
+ },
14
+ required: ["path"],
15
+ },
16
+ run: (args, ctx) => {
17
+ const path = typeof args.path === "string" ? args.path : "";
18
+ const abs = resolveWorkspacePath(ctx.projectId, path);
19
+ if (!abs || !existsSync(abs))
20
+ return `File not found: ${path}`;
21
+ const kind = classifyFile(path);
22
+ if (kind === "image" || kind === "video") {
23
+ return `Cannot read binary file: ${path}`;
24
+ }
25
+ return readFileSync(abs, "utf-8");
26
+ },
27
+ };
28
+ export default tool;
@@ -0,0 +1,3 @@
1
+ import type { AgentTool } from "./types";
2
+ declare const tool: AgentTool;
3
+ export default tool;
@@ -0,0 +1,22 @@
1
+ import { existsSync, rmSync } from "node:fs";
2
+ import { resolveWorkspacePath } from "../workspace";
3
+ const tool = {
4
+ name: "remove_file",
5
+ description: "Delete a file (or directory) in the workspace.",
6
+ parameters: {
7
+ type: "object",
8
+ properties: {
9
+ path: { type: "string", description: "Path relative to the workspace" },
10
+ },
11
+ required: ["path"],
12
+ },
13
+ run: (args, ctx) => {
14
+ const path = typeof args.path === "string" ? args.path : "";
15
+ const abs = resolveWorkspacePath(ctx.projectId, path);
16
+ if (!abs || !existsSync(abs))
17
+ return `File not found: ${path}`;
18
+ rmSync(abs, { recursive: true, force: true });
19
+ return `Removed ${path}`;
20
+ },
21
+ };
22
+ export default tool;
@@ -0,0 +1,3 @@
1
+ import type { AgentTool } from "./types";
2
+ declare const tool: AgentTool;
3
+ export default tool;
@@ -0,0 +1,37 @@
1
+ import { existsSync, renameSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { resolveWorkspacePath } from "../workspace";
4
+ const tool = {
5
+ name: "rename_file",
6
+ description: "Rename a file in the workspace.",
7
+ parameters: {
8
+ type: "object",
9
+ properties: {
10
+ path: {
11
+ type: "string",
12
+ description: "Current file path relative to the workspace",
13
+ },
14
+ new_name: {
15
+ type: "string",
16
+ description: "New file name (no directory separators)",
17
+ },
18
+ },
19
+ required: ["path", "new_name"],
20
+ },
21
+ run: (args, ctx) => {
22
+ const path = typeof args.path === "string" ? args.path.trim() : "";
23
+ if (!path)
24
+ return "rename_file requires a path.";
25
+ const newName = typeof args.new_name === "string" ? args.new_name.trim() : "";
26
+ // Only bare filenames — no separators, `..`, or leading dots.
27
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(newName)) {
28
+ return `Invalid new name: ${newName}`;
29
+ }
30
+ const abs = resolveWorkspacePath(ctx.projectId, path);
31
+ if (!abs || !existsSync(abs))
32
+ return `File not found: ${path}`;
33
+ renameSync(abs, join(dirname(abs), newName));
34
+ return `Renamed ${path} to ${newName}`;
35
+ },
36
+ };
37
+ export default tool;
@@ -0,0 +1,3 @@
1
+ import type { AgentTool } from "./types";
2
+ declare const tool: AgentTool;
3
+ export default tool;
@@ -0,0 +1,28 @@
1
+ import { existsSync } from "node:fs";
2
+ import { resolveWorkspacePath, classifyFile } from "../workspace";
3
+ const tool = {
4
+ name: "show_image",
5
+ description: "Display an image from the workspace to the user.",
6
+ parameters: {
7
+ type: "object",
8
+ properties: {
9
+ path: {
10
+ type: "string",
11
+ description: "Path of the image file relative to the workspace",
12
+ },
13
+ },
14
+ required: ["path"],
15
+ },
16
+ run: (args, ctx) => {
17
+ const path = typeof args.path === "string" ? args.path : "";
18
+ const abs = resolveWorkspacePath(ctx.projectId, path);
19
+ if (!abs || !existsSync(abs))
20
+ return `Image not found: ${path}`;
21
+ if (classifyFile(path) !== "image")
22
+ return `Not an image file: ${path}`;
23
+ const url = `/api/agent/file/preview?projectId=${encodeURIComponent(ctx.projectId)}&path=${encodeURIComponent(path)}`;
24
+ ctx.emit?.("image", { url, path });
25
+ return `Displayed image "${path}" to the user.`;
26
+ },
27
+ };
28
+ export default tool;
@@ -0,0 +1,3 @@
1
+ import type { AgentTool } from "./types";
2
+ declare const tool: AgentTool;
3
+ export default tool;
@@ -0,0 +1,96 @@
1
+ import { FFmpeg } from "@ffmpeg/ffmpeg";
2
+ import { toBlobURL } from "@ffmpeg/util";
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { isValidProjectId, resolveWorkspacePath, classifyFile, workspaceDir, } from "../workspace";
6
+ const CORE_BASE = "https://unpkg.com/@ffmpeg/core@0.12.10/dist/esm";
7
+ const tool = {
8
+ name: "stitch_videos",
9
+ description: "Concatenate multiple videos in the workspace into a single video file.",
10
+ parameters: {
11
+ type: "object",
12
+ properties: {
13
+ videos: {
14
+ type: "array",
15
+ items: { type: "string" },
16
+ description: "List of workspace video paths, in order",
17
+ },
18
+ output: {
19
+ type: "string",
20
+ description: "Output filename (default stitched-<timestamp>.mp4)",
21
+ },
22
+ },
23
+ required: ["videos"],
24
+ },
25
+ run: async (args, ctx) => {
26
+ if (!isValidProjectId(ctx.projectId))
27
+ return "Invalid project ID.";
28
+ const videos = (Array.isArray(args.videos) ? args.videos : [])
29
+ .filter((v) => typeof v === "string" && v.trim().length > 0)
30
+ .map((v) => v.trim());
31
+ if (videos.length < 2)
32
+ return "stitch_videos requires at least two videos.";
33
+ // Resolve and validate each input video.
34
+ const resolved = [];
35
+ for (const v of videos) {
36
+ const abs = resolveWorkspacePath(ctx.projectId, v);
37
+ if (!abs || !existsSync(abs) || classifyFile(v) !== "video") {
38
+ return `Video not found or not a video: ${v}`;
39
+ }
40
+ resolved.push(abs);
41
+ }
42
+ const outputName = (typeof args.output === "string" && args.output.trim()
43
+ ? args.output.trim()
44
+ : `stitched-${Date.now()}.mp4`).replace(/[^a-zA-Z0-9._-]/g, "_");
45
+ ctx.emit?.("notice", { text: "Loading ffmpeg..." });
46
+ const ffmpeg = new FFmpeg();
47
+ try {
48
+ await ffmpeg.load({
49
+ coreURL: await toBlobURL(`${CORE_BASE}/ffmpeg-core.js`, "text/javascript"),
50
+ wasmURL: await toBlobURL(`${CORE_BASE}/ffmpeg-core.wasm`, "application/wasm"),
51
+ });
52
+ // Stream stitching progress via SSE.
53
+ let lastPct = -1;
54
+ ffmpeg.on("progress", ({ progress }) => {
55
+ if (typeof progress !== "number")
56
+ return;
57
+ const pct = Math.round(progress * 100);
58
+ if (pct >= lastPct + 5) {
59
+ lastPct = pct;
60
+ ctx.emit?.("notice", { text: `Stitching: ${pct}%` });
61
+ }
62
+ });
63
+ // Write inputs and a concat list into ffmpeg's virtual filesystem.
64
+ const listLines = [];
65
+ for (let i = 0; i < resolved.length; i++) {
66
+ const name = `input${i}.mp4`;
67
+ await ffmpeg.writeFile(name, readFileSync(resolved[i]));
68
+ listLines.push(`file '${name}'`);
69
+ }
70
+ await ffmpeg.writeFile("list.txt", listLines.join("\n"));
71
+ await ffmpeg.exec([
72
+ "-f",
73
+ "concat",
74
+ "-safe",
75
+ "0",
76
+ "-i",
77
+ "list.txt",
78
+ "-c",
79
+ "copy",
80
+ outputName,
81
+ ]);
82
+ const outData = await ffmpeg.readFile(outputName);
83
+ const outDir = workspaceDir(ctx.projectId);
84
+ if (!existsSync(outDir))
85
+ mkdirSync(outDir, { recursive: true });
86
+ writeFileSync(join(outDir, outputName), Buffer.from(outData));
87
+ const url = `/api/agent/file/preview?projectId=${encodeURIComponent(ctx.projectId)}&path=${encodeURIComponent(outputName)}`;
88
+ ctx.emit?.("video", { url });
89
+ return `Stitched ${videos.length} videos into ${outputName}.`;
90
+ }
91
+ finally {
92
+ ffmpeg.terminate();
93
+ }
94
+ },
95
+ };
96
+ export default tool;
@@ -0,0 +1,3 @@
1
+ import type { AgentTool } from "./types";
2
+ declare const tool: AgentTool;
3
+ export default tool;
@@ -0,0 +1,125 @@
1
+ import { isValidProjectId, workspaceDir } from "../workspace";
2
+ async function readSSE(response, onEvent) {
3
+ const reader = response.body?.getReader();
4
+ if (!reader)
5
+ return;
6
+ const decoder = new TextDecoder();
7
+ let buffer = "";
8
+ try {
9
+ while (true) {
10
+ const { done, value } = await reader.read();
11
+ if (done)
12
+ break;
13
+ buffer += decoder.decode(value, { stream: true });
14
+ const lines = buffer.split("\n");
15
+ buffer = lines.pop() || "";
16
+ let eventType = "message";
17
+ for (const line of lines) {
18
+ if (line.startsWith("event: ")) {
19
+ eventType = line.slice(7).trim();
20
+ }
21
+ else if (line.startsWith("data: ")) {
22
+ try {
23
+ onEvent(eventType, JSON.parse(line.slice(6)));
24
+ }
25
+ catch {
26
+ // skip malformed lines
27
+ }
28
+ eventType = "message";
29
+ }
30
+ }
31
+ }
32
+ }
33
+ finally {
34
+ reader.releaseLock();
35
+ }
36
+ }
37
+ const tool = {
38
+ name: "text_to_video_generation",
39
+ description: "Generate a video from a text prompt.",
40
+ parameters: {
41
+ type: "object",
42
+ properties: {
43
+ prompt: { type: "string", description: "The video prompt" },
44
+ duration: {
45
+ type: "number",
46
+ description: "Video duration in seconds (default 5)",
47
+ },
48
+ mode: {
49
+ type: "string",
50
+ enum: ["distilled", "one-stage", "two-stage"],
51
+ description: "Generation mode: distilled (default), one-stage, or two-stage",
52
+ },
53
+ },
54
+ required: ["prompt"],
55
+ },
56
+ run: async (args, ctx) => {
57
+ if (!isValidProjectId(ctx.projectId))
58
+ return "Invalid project ID.";
59
+ const prompt = typeof args.prompt === "string" ? args.prompt.trim() : "";
60
+ if (!prompt)
61
+ return "text_to_video_generation requires a prompt.";
62
+ if (!ctx.backendPort)
63
+ return "Video backend not available.";
64
+ const duration = typeof args.duration === "number" && args.duration > 0
65
+ ? args.duration
66
+ : 5;
67
+ const frames = Math.round(duration * 24 + 1);
68
+ ctx.emit?.("notice", { text: `Generating video from prompt...` });
69
+ try {
70
+ const body = {
71
+ prompt,
72
+ projectId: ctx.projectId,
73
+ outputDir: workspaceDir(ctx.projectId),
74
+ width: 448,
75
+ height: 448,
76
+ frames,
77
+ frameRate: 24,
78
+ };
79
+ if (typeof args.mode === "string" &&
80
+ ["distilled", "one-stage", "two-stage"].includes(args.mode)) {
81
+ body.mode = args.mode;
82
+ }
83
+ const res = await fetch(`http://localhost:${ctx.backendPort}/api/render/text-to-video`, {
84
+ method: "POST",
85
+ headers: { "Content-Type": "application/json" },
86
+ body: JSON.stringify(body),
87
+ signal: ctx.signal,
88
+ });
89
+ if (!res.ok) {
90
+ return `Video generation failed: ${await res.text()}`;
91
+ }
92
+ let resultPath = null;
93
+ let resultFilename = null;
94
+ let error = null;
95
+ await readSSE(res, (event, data) => {
96
+ switch (event) {
97
+ case "log":
98
+ ctx.emit?.("notice", { text: data.text });
99
+ break;
100
+ case "complete":
101
+ resultPath = data.path;
102
+ resultFilename = data.filename;
103
+ break;
104
+ case "error":
105
+ error = data.error || "Video generation failed";
106
+ break;
107
+ }
108
+ });
109
+ if (error)
110
+ return error;
111
+ if (!resultPath)
112
+ return "Video generation did not produce a result.";
113
+ const relPath = resultFilename ||
114
+ resultPath.split("/").pop() ||
115
+ "video.mp4";
116
+ const url = `/api/agent/file/preview?projectId=${encodeURIComponent(ctx.projectId)}&path=${encodeURIComponent(relPath)}`;
117
+ ctx.emit?.("video", { url });
118
+ return `Generated video: ${resultPath}`;
119
+ }
120
+ catch (e) {
121
+ return `Video generation error: ${String(e)}`;
122
+ }
123
+ },
124
+ };
125
+ export default tool;
@@ -0,0 +1,15 @@
1
+ export interface ToolRunContext {
2
+ projectId: string;
3
+ /** Optional SSE emit function so tools can stream events to the client. */
4
+ emit?: (event: string, data: Record<string, unknown>) => void;
5
+ /** The app backend port, for tools that call internal endpoints. */
6
+ backendPort?: number;
7
+ /** Abort signal derived from the incoming request, for cancellable calls. */
8
+ signal?: AbortSignal;
9
+ }
10
+ export interface AgentTool {
11
+ name: string;
12
+ description: string;
13
+ parameters: Record<string, unknown>;
14
+ run: (args: Record<string, unknown>, ctx: ToolRunContext) => string | Promise<string>;
15
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ import type { AgentTool } from "./types";
2
+ declare const tool: AgentTool;
3
+ export default tool;
@@ -0,0 +1,27 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ import { resolveWorkspacePath, ensureDir } from "../workspace";
4
+ const tool = {
5
+ name: "update_file",
6
+ description: "Append text to the end of a file in the workspace.",
7
+ parameters: {
8
+ type: "object",
9
+ properties: {
10
+ path: { type: "string", description: "File path relative to the workspace" },
11
+ content: { type: "string", description: "Text content to append" },
12
+ },
13
+ required: ["path", "content"],
14
+ },
15
+ run: (args, ctx) => {
16
+ const path = typeof args.path === "string" ? args.path : "";
17
+ const content = typeof args.content === "string" ? args.content : "";
18
+ const abs = resolveWorkspacePath(ctx.projectId, path);
19
+ if (!abs)
20
+ return `Invalid path: ${path}`;
21
+ ensureDir(dirname(abs));
22
+ const existing = existsSync(abs) ? readFileSync(abs, "utf-8") : "";
23
+ writeFileSync(abs, existing + content, "utf-8");
24
+ return `Appended ${content.length} chars to ${path}`;
25
+ },
26
+ };
27
+ export default tool;
@@ -0,0 +1,3 @@
1
+ import type { AgentTool } from "./types";
2
+ declare const tool: AgentTool;
3
+ export default tool;
@@ -0,0 +1,26 @@
1
+ import { writeFileSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ import { resolveWorkspacePath, ensureDir } from "../workspace";
4
+ const tool = {
5
+ name: "write_file",
6
+ description: "Write (create or overwrite) a file with the given text content.",
7
+ parameters: {
8
+ type: "object",
9
+ properties: {
10
+ path: { type: "string", description: "File path relative to the workspace" },
11
+ content: { type: "string", description: "Text content to write" },
12
+ },
13
+ required: ["path", "content"],
14
+ },
15
+ run: (args, ctx) => {
16
+ const path = typeof args.path === "string" ? args.path : "";
17
+ const content = typeof args.content === "string" ? args.content : "";
18
+ const abs = resolveWorkspacePath(ctx.projectId, path);
19
+ if (!abs)
20
+ return `Invalid path: ${path}`;
21
+ ensureDir(dirname(abs));
22
+ writeFileSync(abs, content, "utf-8");
23
+ return `Wrote ${content.length} chars to ${path}`;
24
+ },
25
+ };
26
+ export default tool;
@@ -0,0 +1,25 @@
1
+ export declare const AGENTS_DIR: string;
2
+ export declare const UPLOAD_DIR: string;
3
+ export declare const AGENT_UPLOAD_DIR: string;
4
+ export declare const MEMORIES_DIR_NAME = "memories";
5
+ export declare const PROJECTS_FILE: string;
6
+ export declare const STUDIO_DIR: string;
7
+ export declare function ensureDir(dir: string): void;
8
+ export declare function workspaceDir(projectId: string): string;
9
+ export declare function movieStudioDataDir(projectId: string): string;
10
+ export declare function movieStudioStateFile(projectId: string): string;
11
+ export declare function memoriesDir(projectId: string): string;
12
+ export declare function isValidProjectId(id: string): boolean;
13
+ /** Resolve a workspace-relative path safely (no traversal / absolute paths). */
14
+ export declare function resolveWorkspacePath(projectId: string, relativePath: string): string | null;
15
+ export type FileKind = "image" | "video" | "text" | "other";
16
+ export declare function classifyFile(name: string): FileKind;
17
+ export interface WorkspaceFile {
18
+ path: string;
19
+ name: string;
20
+ ext: string;
21
+ size: number;
22
+ mtime: number;
23
+ kind: FileKind;
24
+ }
25
+ export declare function walkFiles(base: string, prefix?: string): WorkspaceFile[];
@@ -0,0 +1,124 @@
1
+ import { existsSync, mkdirSync, readdirSync, statSync, realpathSync, } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join, sep } from "node:path";
4
+ const APP_DATA_DIR = join(homedir(), "media-studio");
5
+ const JSON_DIR = join(APP_DATA_DIR, "json");
6
+ export const AGENTS_DIR = join(APP_DATA_DIR, "agents");
7
+ export const UPLOAD_DIR = join(APP_DATA_DIR, "upload");
8
+ export const AGENT_UPLOAD_DIR = join(APP_DATA_DIR, "agent-upload");
9
+ export const MEMORIES_DIR_NAME = "memories";
10
+ export const PROJECTS_FILE = join(JSON_DIR, "projects.json");
11
+ export const STUDIO_DIR = join(APP_DATA_DIR, "studio");
12
+ export function ensureDir(dir) {
13
+ if (!existsSync(dir))
14
+ mkdirSync(dir, { recursive: true });
15
+ }
16
+ export function workspaceDir(projectId) {
17
+ return join(AGENTS_DIR, projectId);
18
+ }
19
+ export function movieStudioDataDir(projectId) {
20
+ return join(STUDIO_DIR, projectId, "data");
21
+ }
22
+ export function movieStudioStateFile(projectId) {
23
+ return join(STUDIO_DIR, projectId, "state.json");
24
+ }
25
+ export function memoriesDir(projectId) {
26
+ return join(AGENTS_DIR, projectId, MEMORIES_DIR_NAME);
27
+ }
28
+ const PROJECT_ID_RE = /^[a-zA-Z0-9_-]{1,64}$/;
29
+ export function isValidProjectId(id) {
30
+ return PROJECT_ID_RE.test(id);
31
+ }
32
+ /** Resolve a workspace-relative path safely (no traversal / absolute paths). */
33
+ export function resolveWorkspacePath(projectId, relativePath) {
34
+ if (!isValidProjectId(projectId))
35
+ return null;
36
+ if (typeof relativePath !== "string" || !relativePath)
37
+ return null;
38
+ if (relativePath.includes("..") || relativePath.includes("\0"))
39
+ return null;
40
+ const normalized = relativePath.replace(/\\/g, "/");
41
+ if (normalized.startsWith("/"))
42
+ return null;
43
+ const base = workspaceDir(projectId);
44
+ const resolved = join(base, normalized);
45
+ if (resolved !== base && !resolved.startsWith(base + sep))
46
+ return null;
47
+ // Defense in depth: if the target exists, ensure its real path (symlinks
48
+ // resolved) is still inside the real agent workspace.
49
+ if (existsSync(resolved)) {
50
+ try {
51
+ const realResolved = realpathSync(resolved);
52
+ const realAgents = realpathSync(AGENTS_DIR);
53
+ if (!realResolved.startsWith(realAgents + sep))
54
+ return null;
55
+ }
56
+ catch {
57
+ return null;
58
+ }
59
+ }
60
+ return resolved;
61
+ }
62
+ const IMAGE_EXTS = [".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp"];
63
+ const VIDEO_EXTS = [".mp4", ".mov", ".m4v", ".webm", ".mkv", ".avi"];
64
+ const TEXT_EXTS = [
65
+ ".md",
66
+ ".txt",
67
+ ".json",
68
+ ".csv",
69
+ ".log",
70
+ ".html",
71
+ ".js",
72
+ ".ts",
73
+ ".tsx",
74
+ ".jsx",
75
+ ".py",
76
+ ".css",
77
+ ".yml",
78
+ ".yaml",
79
+ ];
80
+ export function classifyFile(name) {
81
+ const ext = name.slice(name.lastIndexOf(".")).toLowerCase();
82
+ if (IMAGE_EXTS.includes(ext))
83
+ return "image";
84
+ if (VIDEO_EXTS.includes(ext))
85
+ return "video";
86
+ if (TEXT_EXTS.includes(ext))
87
+ return "text";
88
+ return "other";
89
+ }
90
+ export function walkFiles(base, prefix = "") {
91
+ const results = [];
92
+ let entries;
93
+ try {
94
+ entries = readdirSync(base);
95
+ }
96
+ catch {
97
+ return results;
98
+ }
99
+ for (const entry of entries) {
100
+ const full = join(base, entry);
101
+ const rel = prefix ? `${prefix}/${entry}` : entry;
102
+ let st;
103
+ try {
104
+ st = statSync(full);
105
+ }
106
+ catch {
107
+ continue;
108
+ }
109
+ if (st.isDirectory()) {
110
+ results.push(...walkFiles(full, rel));
111
+ }
112
+ else {
113
+ results.push({
114
+ path: rel,
115
+ name: entry,
116
+ ext: entry.slice(entry.lastIndexOf(".")).toLowerCase(),
117
+ size: st.size,
118
+ mtime: st.mtimeMs,
119
+ kind: classifyFile(entry),
120
+ });
121
+ }
122
+ }
123
+ return results;
124
+ }
@@ -0,0 +1,4 @@
1
+ export declare function runSetup({ port, }: {
2
+ port: number;
3
+ }): Promise<any>;
4
+ export declare function getUvPath(): Promise<string>;