@j-o-r/hello-dave 0.0.10 → 0.1.1

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 (76) hide show
  1. package/README.md +2 -0
  2. package/README.md.bak.1779452127 +240 -0
  3. package/TODO.md +30 -8
  4. package/agents/code_agent.js +6 -6
  5. package/agents/daisy_agent.js +10 -7
  6. package/agents/minimax.js +173 -0
  7. package/agents/stability.js +173 -0
  8. package/bin/codeDave +1 -1
  9. package/bin/dave.js +1 -1
  10. package/docs/music-toolsets.md +137 -0
  11. package/docs/plans/minimax-music-generation.md +80 -0
  12. package/docs/plans/unified-agent-architecture.md +146 -0
  13. package/docs/plans/websocket-streaming-plan.md.bak +317 -0
  14. package/docs/prompt/task_clarification_and_documentation.md +35 -0
  15. package/lib/API/minimax/ImageToolset.js +169 -0
  16. package/lib/API/minimax/MusicToolset.js +290 -0
  17. package/lib/API/minimax/VideoToolset.js +296 -0
  18. package/lib/API/minimax/image.generation.md +239 -0
  19. package/lib/API/minimax/image.js +219 -0
  20. package/lib/API/minimax/image.to.image.md +257 -0
  21. package/lib/API/minimax/index.js +16 -0
  22. package/lib/API/minimax/music.cover.preprocess.md +206 -0
  23. package/lib/API/minimax/music.generation.md +346 -0
  24. package/lib/API/minimax/music.js +257 -0
  25. package/lib/API/minimax/music.lyrics.generation.md +205 -0
  26. package/lib/API/minimax/video.download.md +133 -0
  27. package/lib/API/minimax/video.first.last.image.md +186 -0
  28. package/lib/API/minimax/video.from.image.md +206 -0
  29. package/lib/API/minimax/video.from.subject.md +164 -0
  30. package/lib/API/minimax/video.generation.md +192 -0
  31. package/lib/API/minimax/video.js +339 -0
  32. package/lib/API/minimax/video.query.md +128 -0
  33. package/lib/API/stability.ai/ImageToolset.js +357 -0
  34. package/lib/API/stability.ai/MusicToolset.js +302 -0
  35. package/lib/API/stability.ai/audio-3.md +205 -0
  36. package/lib/API/stability.ai/audio.js +679 -0
  37. package/lib/API/stability.ai/image.js +911 -0
  38. package/lib/API/stability.ai/image.md +271 -0
  39. package/lib/API/stability.ai/index.js +11 -0
  40. package/lib/API/stability.ai/openapi.json +17118 -0
  41. package/lib/API/x.ai/ImageToolset.js +165 -0
  42. package/lib/API/x.ai/image.editing.md +86 -0
  43. package/lib/API/x.ai/image.js +393 -0
  44. package/lib/API/x.ai/image.md +213 -0
  45. package/lib/API/x.ai/image.to.generation.md +494 -0
  46. package/lib/API/x.ai/image.to.video.md +23 -0
  47. package/lib/API/x.ai/index.js +7 -0
  48. package/lib/AgentManager.js +1 -1
  49. package/lib/CdnToolset.js +191 -0
  50. package/lib/ToolSet.js +19 -1
  51. package/lib/cdn.js +373 -0
  52. package/lib/fafs.js +3 -1
  53. package/lib/genericToolset.js +43 -166
  54. package/lib/index.js +9 -1
  55. package/package.json +2 -2
  56. package/types/API/minimax/ImageToolset.d.ts +3 -0
  57. package/types/API/minimax/MusicToolset.d.ts +3 -0
  58. package/types/API/minimax/VideoToolset.d.ts +3 -0
  59. package/types/API/minimax/image.d.ts +109 -0
  60. package/types/API/minimax/index.d.ts +15 -0
  61. package/types/API/minimax/music.d.ts +46 -0
  62. package/types/API/minimax/video.d.ts +165 -0
  63. package/types/API/stability.ai/ImageToolset.d.ts +3 -0
  64. package/types/API/stability.ai/MusicToolset.d.ts +3 -0
  65. package/types/API/stability.ai/audio.d.ts +193 -0
  66. package/types/API/stability.ai/image.d.ts +274 -0
  67. package/types/API/stability.ai/index.d.ts +11 -0
  68. package/types/API/x.ai/ImageToolset.d.ts +3 -0
  69. package/types/API/x.ai/image.d.ts +82 -0
  70. package/types/API/x.ai/index.d.ts +7 -0
  71. package/types/AgentManager.d.ts +1 -1
  72. package/types/CdnToolset.d.ts +20 -0
  73. package/types/ToolSet.d.ts +8 -0
  74. package/types/cdn.d.ts +141 -0
  75. package/types/index.d.ts +9 -2
  76. package/docs/multi-agent-clusters.md.bak +0 -229
@@ -0,0 +1,23 @@
1
+ #### Model Capabilities
2
+
3
+ # Image-to-Video
4
+
5
+ Transform a still image into a video by providing a source image along with your prompt. The model animates the image content based on your instructions.
6
+
7
+ You can provide the source image as:
8
+
9
+ * A **public URL** pointing to an image
10
+ * A **base64-encoded data URI** (e.g., `data:image/jpeg;base64,...`)
11
+
12
+ The demo below shows this in action; hold to animate a still image:
13
+
14
+ In the Vercel AI SDK, the `prompt` parameter accepts an object with `image` and `text` fields for image-to-video generation. The `image` field can be a URL string, base64-encoded string, `Uint8Array`, `ArrayBuffer`, or `Buffer`.
15
+
16
+ ## Related
17
+
18
+ * [Video Generation](/developers/model-capabilities/video/generation) — Generate videos from text prompts
19
+ * [Reference-to-Video](/developers/model-capabilities/video/reference-to-video) — Guide a video with reference images
20
+ * [Video Editing](/developers/model-capabilities/video/editing) — Edit existing videos
21
+ * [API Reference](/developers/rest-api-reference) — Full endpoint documentation
22
+ * [Imagine API Landing Page](https://x.ai/api/imagine) — Showcase of the Imagine API in action
23
+
@@ -0,0 +1,7 @@
1
+ import * as image from './image.js';
2
+ import imageToolset from './ImageToolset.js'
3
+
4
+ export default {
5
+ image,
6
+ imageToolset
7
+ }
@@ -183,7 +183,7 @@ class AgentManager {
183
183
  }
184
184
 
185
185
  /**
186
- * Adds a pre-defined generic tool from toolsPool to this agent's ToolSet.
186
+ * Adds a pre-defined generic tool from toolsPool (genericToolset) to this agent's ToolSet.
187
187
  * @param {'execute_bash_script'|'execute_remote_script'|'get_user_env'|'history_search'|'javascript_interpreter'|'memory_recall'|'memory_write'|'open_link'|'read_file'|'send_email'|'syntax_check'|'write_file'} name - Tool name.
188
188
  * @throws {Error} Invalid name, no toolset, or tool missing.
189
189
  * @example mgr.addGenericToolcall('read_file');
@@ -0,0 +1,191 @@
1
+ import { ToolSet } from './index.js';
2
+ import cdn from './cdn.js';
3
+
4
+ /**
5
+ * @module lib/CdnToolset
6
+ * Dedicated ToolSet for CDN / remote public file publishing.
7
+ *
8
+ * Contains only the tools related to publishing files to a remote web server
9
+ * via SSH/SCP (defined by SSH_EP environment variable).
10
+ *
11
+ * This allows clean separation: generic tools + CDN tools + domain-specific toolsets.
12
+ *
13
+ * Usage:
14
+ * import cdnTools from './CdnToolset.js';
15
+ * myToolset.borrow(cdnTools);
16
+ *
17
+ * Or use directly:
18
+ * import cdnTools from './CdnToolset.js';
19
+ * await cdnTools.call('listProjects');
20
+ */
21
+
22
+ const tools = new ToolSet('auto');
23
+
24
+ /**
25
+ * Single source of truth for cleaning over-escaped strings from LLMs.
26
+ * (Duplicated here for self-containment of the CDN toolset)
27
+ */
28
+ const guessOverEscaping = (s) => {
29
+ if (typeof s !== 'string') return s;
30
+
31
+ let current = s.trim();
32
+ const seen = new Set();
33
+ let iterations = 0;
34
+ const MAX_ITER = 6;
35
+
36
+ while (iterations < MAX_ITER && !seen.has(current)) {
37
+ seen.add(current);
38
+ iterations++;
39
+
40
+ try {
41
+ const parsed = JSON.parse(current);
42
+ if (typeof parsed === 'string') {
43
+ current = parsed;
44
+ continue;
45
+ }
46
+ } catch (e) { }
47
+
48
+ if (current.startsWith('\\"') && current.endsWith('\\"')) {
49
+ current = current.slice(2, -2);
50
+ continue;
51
+ }
52
+
53
+ if (current.startsWith('"') && current.endsWith('"') && current.length > 2) {
54
+ const inner = current.slice(1, -1).trim();
55
+ if (!inner.startsWith('{') && !inner.startsWith('[')) {
56
+ current = inner;
57
+ continue;
58
+ }
59
+ }
60
+
61
+ if (!current.includes('\n') && (current.match(/\\"/g) || []).length >= 1) {
62
+ const lessEscaped = current.replace(/\\"/g, '"');
63
+ if (lessEscaped !== current) {
64
+ current = lessEscaped;
65
+ continue;
66
+ }
67
+ }
68
+
69
+ break;
70
+ }
71
+
72
+ return current;
73
+ };
74
+
75
+ /**
76
+ * ============================================================================
77
+ * CDN / PUBLIC FILE EXPOSURE TOOLS
78
+ * ============================================================================
79
+ *
80
+ * Purpose:
81
+ * Make local files (generated audio, images, references, outputs from
82
+ * lib/API/minimax/music.js, etc.) publicly accessible via HTTPS.
83
+ *
84
+ * This allows remote AI/LLM services to inspect, reference, or use the
85
+ * files as input for further generation.
86
+ *
87
+ * Underlying module:
88
+ * All functionality is provided by lib/cdn.js (SSH/SCP based publishing
89
+ * to a remote web server defined by the SSH_EP environment variable).
90
+ *
91
+ * Logical workflow for LLMs:
92
+ * 1. listProjects() → Discover what already exists
93
+ * 2. publishFile() → Quick single-file exposure
94
+ * 3. publishToProject() → Organize one or multiple files into a project
95
+ * 4. deleteProject() → Clean up when no longer needed
96
+ *
97
+ * Requirements:
98
+ * - SSH_EP environment variable must be set
99
+ * - Remote host serves the 'htdocs' folder publicly over HTTPS
100
+ */
101
+
102
+ tools.add(
103
+ 'list_projects',
104
+ 'List all existing projects published on the remote public server.\n' +
105
+ 'Returns slug, public URL, file count, and creation date for each project.\n' +
106
+ 'Use this first when you have no prior context about what has already been published.',
107
+ { type: 'object', properties: {} },
108
+ async () => {
109
+ return await cdn.listProjects();
110
+ }
111
+ );
112
+
113
+ tools.add(
114
+ 'publish_file',
115
+ 'Publish a single local file to a public HTTPS URL via SSH.\n' +
116
+ 'Automatically creates any missing parent directories.\n' +
117
+ 'Best for quick one-off references or when you only need a direct link to one file.',
118
+ {
119
+ type: 'object',
120
+ properties: {
121
+ localPath: { type: 'string', description: 'Path to the local file to publish' },
122
+ remoteRelativePath: { type: 'string', description: 'Target path under htdocs (e.g. "tmp/reference.mp3" or "projects/my-project/audio.wav")' }
123
+ },
124
+ required: ['localPath', 'remoteRelativePath']
125
+ },
126
+ async (params) => {
127
+ let local = guessOverEscaping(params.localPath).trim();
128
+ let remote = guessOverEscaping(params.remoteRelativePath).trim();
129
+ return await cdn.publishFile(local, remote);
130
+ }
131
+ );
132
+
133
+ tools.add(
134
+ 'publish_to_project',
135
+ 'Publish one or multiple local files into a structured public project folder.\n' +
136
+ 'Creates the project directory, uploads all files, and generates meta.json.\n' +
137
+ 'Optional description.md and plan.md can be added for context.\n' +
138
+ 'Returns an array of public URLs plus the project folder URL.\n' +
139
+ 'Recommended when you want to keep related files together for remote AI inspection.',
140
+ {
141
+ type: 'object',
142
+ properties: {
143
+ localPaths: {
144
+ oneOf: [
145
+ { type: 'string', description: 'Single file path' },
146
+ { type: 'array', items: { type: 'string' }, description: 'Array of file paths' }
147
+ ]
148
+ },
149
+ projectSlug: { type: 'string', description: 'Project folder name (will be sanitized to kebab-case)' },
150
+ options: {
151
+ type: 'object',
152
+ properties: {
153
+ description: { type: 'string', description: 'Human-readable description of the project' },
154
+ plan: { type: 'string', description: 'Planning notes or step-by-step evolution' },
155
+ filename: { type: 'string', description: 'Custom filename (only used for single-file uploads)' }
156
+ }
157
+ }
158
+ },
159
+ required: ['localPaths', 'projectSlug']
160
+ },
161
+ async (params) => {
162
+ let localPaths = guessOverEscaping(params.localPaths);
163
+ if (typeof localPaths === 'string') localPaths = [localPaths];
164
+
165
+ let projectSlug = guessOverEscaping(params.projectSlug).trim();
166
+ let options = params.options || {};
167
+ if (typeof options === 'string') options = JSON.parse(guessOverEscaping(options));
168
+
169
+ return await cdn.publishToProject(localPaths, projectSlug, options);
170
+ }
171
+ );
172
+
173
+ tools.add(
174
+ 'delete_project',
175
+ 'Permanently delete an entire project folder from the remote public server.\n' +
176
+ 'Removes the project and all its files.\n' +
177
+ 'Use this to clean up temporary or obsolete projects.',
178
+ {
179
+ type: 'object',
180
+ properties: {
181
+ projectSlug: { type: 'string', description: 'Project slug to delete' }
182
+ },
183
+ required: ['projectSlug']
184
+ },
185
+ async (params) => {
186
+ let slug = guessOverEscaping(params.projectSlug).trim();
187
+ return await cdn.deleteProject(slug);
188
+ }
189
+ );
190
+
191
+ export default tools;
package/lib/ToolSet.js CHANGED
@@ -132,6 +132,24 @@ class ToolSet {
132
132
  })).sort((a, b) => a.name.localeCompare(b.name));
133
133
  }
134
134
 
135
+ /**
136
+ * Borrows/copies all registered tools (function_calls) from another ToolSet into this one.
137
+ * Overwrites if a tool with the same name already exists in this ToolSet.
138
+ * Uses only public API for compatibility.
139
+ * @param {ToolSet} sourceToolSet - The ToolSet instance to borrow tools from (e.g. musicToolset)
140
+ * @returns {ToolSet} Returns this instance to allow chaining
141
+ */
142
+ borrow(sourceToolSet) {
143
+ if (!(sourceToolSet instanceof ToolSet)) {
144
+ throw new Error('borrow() expects a ToolSet instance');
145
+ }
146
+ for (const { name } of sourceToolSet.list()) {
147
+ const tool = sourceToolSet.get(name);
148
+ this.add(name, tool.description, tool.parameters, tool.method);
149
+ }
150
+ return this;
151
+ }
152
+
135
153
  /**
136
154
  * Getter for the current tool choice setting.
137
155
  * @returns {string} The tool choice: 'auto', 'none', or 'required'
@@ -206,4 +224,4 @@ class ToolSet {
206
224
  }
207
225
  }
208
226
 
209
- export default ToolSet;
227
+ export default ToolSet;
package/lib/cdn.js ADDED
@@ -0,0 +1,373 @@
1
+ /**
2
+ * @file lib/cdn.js
3
+ * @module cdn
4
+ * @description Reusable SSH-based CDN publishing module.
5
+ * Publishes any local files to a remote web server via SSH/SCP.
6
+ * Designed to be used by any toolset or agent (music, images, documents, etc.).
7
+ *
8
+ * Core capabilities:
9
+ * - Publish single or multiple files into organized project folders
10
+ * - Automatically create remote directory structures
11
+ * - List all existing projects on the remote server
12
+ * - Delete entire projects when no longer needed
13
+ *
14
+ * All public URLs are built dynamically from the SSH host (assumes a web server
15
+ * is serving the `htdocs` directory publicly).
16
+ *
17
+ * @requires SSH_EP environment variable (format: ssh://user@host:port)
18
+ * @requires Remote host must have a web server serving the 'htdocs' folder
19
+ *
20
+ * @example
21
+ * import cdn from './cdn.js';
22
+ *
23
+ * // Publish multiple files into one project
24
+ * const result = await cdn.publishToProject(
25
+ * ['/tmp/audio.mp3', '/tmp/cover.jpg'],
26
+ * 'my-song-cover-2026',
27
+ * {
28
+ * description: 'Cover of original song with new lyrics',
29
+ * plan: 'Two-step cover using Minimax voice features'
30
+ * }
31
+ * );
32
+ * console.log(result.public_urls);
33
+ *
34
+ * // List all projects
35
+ * const projects = await cdn.listProjects();
36
+ *
37
+ * // Delete a project
38
+ * await cdn.deleteProject('my-song-cover-2026');
39
+ */
40
+
41
+ import { SH } from '@j-o-r/sh';
42
+ import path from 'node:path';
43
+
44
+ /**
45
+ * Parse and validate the SSH_EP environment variable.
46
+ *
47
+ * Supports both `ssh://user@host:port` and `user@host:port` formats.
48
+ *
49
+ * @returns {{user: string, host: string, port: number, raw: string}}
50
+ * Parsed SSH connection details.
51
+ *
52
+ * @throws {Error} If SSH_EP is missing or has an invalid format.
53
+ *
54
+ * @example
55
+ * // Required environment variable:
56
+ * // export SSH_EP='ssh://jorrit@drive.duin.xyz:4301'
57
+ */
58
+ function getSshConfig() {
59
+ if (!process.env.SSH_EP) {
60
+ throw new Error(
61
+ 'Missing SSH_EP environment variable!\n' +
62
+ "Please set it like this: export SSH_EP='ssh://user@domain.xyz:4301'"
63
+ );
64
+ }
65
+
66
+ const ep = process.env.SSH_EP;
67
+ let user = '';
68
+ let host = '';
69
+ let port = 22;
70
+
71
+ try {
72
+ if (ep.startsWith('ssh://')) {
73
+ const withoutProto = ep.slice(6);
74
+ const [userHost, portStr] = withoutProto.split(':');
75
+ if (userHost.includes('@')) [user, host] = userHost.split('@');
76
+ else host = userHost;
77
+ if (portStr) port = parseInt(portStr, 10);
78
+ } else {
79
+ const [userHost, portStr] = ep.split(':');
80
+ if (userHost.includes('@')) [user, host] = userHost.split('@');
81
+ else host = userHost;
82
+ if (portStr) port = parseInt(portStr, 10);
83
+ }
84
+ } catch (e) {
85
+ throw new Error(`Failed to parse SSH_EP="${ep}": ${e.message}`);
86
+ }
87
+
88
+ if (!user || !host) {
89
+ throw new Error(`Invalid SSH_EP format: "${ep}". Expected ssh://user@host:port`);
90
+ }
91
+
92
+ return { user, host, port, raw: ep };
93
+ }
94
+
95
+ /**
96
+ * Ensure a remote directory exists under `htdocs/`.
97
+ * Creates all parent directories as needed (`mkdir -p`).
98
+ *
99
+ * @param {string} remoteRelativeDir - Relative path under htdocs (e.g. "tmp/cdn-demo-123" or "projects/my-project")
100
+ * @returns {Promise<string>} The full remote directory path (e.g. "htdocs/tmp/cdn-demo-123")
101
+ */
102
+ async function ensureRemoteDir(remoteRelativeDir) {
103
+ const ssh = getSshConfig();
104
+ const remoteDir = `htdocs/${remoteRelativeDir}`;
105
+
106
+ console.log(`[cdn] Ensuring remote directory: ${remoteDir}`);
107
+ await SH`ssh -p ${ssh.port} ${ssh.user}@${ssh.host} "mkdir -p ${remoteDir}"`.run();
108
+
109
+ return remoteDir;
110
+ }
111
+
112
+ /**
113
+ * Safely extract string output from an SH command result.
114
+ * Handles different return shapes from the @j-o-r/sh library.
115
+ *
116
+ * @param {any} result - Result object returned by SH.run()
117
+ * @returns {string} The output as a string (empty string if nothing available)
118
+ */
119
+ function getOutput(result) {
120
+ if (!result) return '';
121
+ if (typeof result === 'string') return result;
122
+ if (result.stdout) return result.stdout.toString();
123
+ if (result.toString) return result.toString();
124
+ return '';
125
+ }
126
+
127
+ /**
128
+ * List all existing projects on the remote server.
129
+ *
130
+ * Scans the `htdocs/projects/` directory and returns metadata for each project.
131
+ * Useful when starting without prior context about what has already been published.
132
+ *
133
+ * @returns {Promise<Array<{slug: string, url: string, fileCount: number, created?: string}>>}
134
+ * Array of project objects.
135
+ *
136
+ * @throws {Error} If the SSH connection or directory listing fails.
137
+ *
138
+ * @example
139
+ * const projects = await cdn.listProjects();
140
+ * console.log(projects[0].url); // https://drive.duin.xyz/projects/my-project/
141
+ */
142
+ async function listProjects() {
143
+ const ssh = getSshConfig();
144
+ const projectsBase = 'htdocs/projects';
145
+
146
+ console.log(`[cdn] Listing projects from ${ssh.host}...`);
147
+
148
+ try {
149
+ const lsResult = await SH`ssh -p ${ssh.port} ${ssh.user}@${ssh.host} "ls -1 ${projectsBase} 2>/dev/null || echo ''"`.run();
150
+ const output = getOutput(lsResult);
151
+ const slugs = output.trim().split('\n').filter(Boolean);
152
+
153
+ const projects = [];
154
+
155
+ for (const slug of slugs) {
156
+ const projectDir = `${projectsBase}/${slug}`;
157
+
158
+ const countResult = await SH`ssh -p ${ssh.port} ${ssh.user}@${ssh.host} "ls -1 ${projectDir} 2>/dev/null | wc -l"`.run();
159
+ const countOutput = getOutput(countResult);
160
+ const fileCount = parseInt(countOutput.trim(), 10) || 0;
161
+
162
+ let created;
163
+ try {
164
+ const metaResult = await SH`ssh -p ${ssh.port} ${ssh.user}@${ssh.host} "cat ${projectDir}/meta.json 2>/dev/null || echo '{}'"`.run();
165
+ const metaOutput = getOutput(metaResult);
166
+ const meta = JSON.parse(metaOutput.trim());
167
+ created = meta.created;
168
+ } catch (_) {
169
+ // meta.json may not exist yet
170
+ }
171
+
172
+ projects.push({
173
+ slug,
174
+ url: `https://${ssh.host}/projects/${slug}/`,
175
+ fileCount,
176
+ created
177
+ });
178
+ }
179
+
180
+ console.log(`[cdn] Found ${projects.length} project(s)`);
181
+ return projects;
182
+ } catch (err) {
183
+ throw new Error(`Failed to list projects: ${err.message}`);
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Delete an entire project folder from the remote server.
189
+ *
190
+ * Permanently removes `htdocs/projects/<slug>` and all its contents.
191
+ * Use with caution — this operation cannot be undone.
192
+ *
193
+ * @param {string} projectSlug - Project identifier (will be sanitized to kebab-case)
194
+ * @returns {Promise<{deleted: boolean, project: string}>}
195
+ *
196
+ * @example
197
+ * await cdn.deleteProject('my-old-project');
198
+ */
199
+ async function deleteProject(projectSlug) {
200
+ const ssh = getSshConfig();
201
+ const slug = projectSlug.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
202
+ const remoteDir = `htdocs/projects/${slug}`;
203
+
204
+ console.log(`[cdn] Deleting project: ${remoteDir}`);
205
+ await SH`ssh -p ${ssh.port} ${ssh.user}@${ssh.host} "rm -rf ${remoteDir}"`.run();
206
+
207
+ console.log(`[cdn] ✅ Project deleted: ${slug}`);
208
+ return { deleted: true, project: slug };
209
+ }
210
+
211
+ /**
212
+ * Ensure the remote project directory structure exists.
213
+ * Creates `htdocs/projects/<slug>` (and the `generated` subfolder).
214
+ *
215
+ * @param {string} projectSlug - Project identifier (will be sanitized)
216
+ * @returns {Promise<string>} The remote directory path under htdocs
217
+ */
218
+ async function ensureProjectStructure(projectSlug) {
219
+ const slug = projectSlug.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
220
+ const remoteRelative = `projects/${slug}`;
221
+ return await ensureRemoteDir(remoteRelative);
222
+ }
223
+
224
+ /**
225
+ * Internal helper to upload a single file via SCP.
226
+ *
227
+ * @param {string} localPath - Local file path
228
+ * @param {string} remoteFilePath - Full remote path (including htdocs)
229
+ * @param {object} ssh - SSH config object from getSshConfig()
230
+ */
231
+ async function uploadSingleFile(localPath, remoteFilePath, ssh) {
232
+ const scpTarget = `${ssh.user}@${ssh.host}:${remoteFilePath}`;
233
+ console.log(`[cdn] Uploading: ${path.basename(localPath)} → ${remoteFilePath}`);
234
+ await SH`scp -P ${ssh.port} ${localPath} ${scpTarget}`.run();
235
+ }
236
+
237
+ /**
238
+ * Publish one or more local files into an organized project folder on the public CDN.
239
+ *
240
+ * Supports both single file (string) and multiple files (array).
241
+ * Automatically creates the project directory and generates `meta.json`.
242
+ * Optional `description.md` and `plan.md` files can be created.
243
+ *
244
+ * @param {string|string[]} localPaths - Single path or array of local file paths
245
+ * @param {string} projectSlug - Project identifier (sanitized to kebab-case)
246
+ * @param {Object} [options]
247
+ * @param {string} [options.description] - Human-readable project description
248
+ * @param {string} [options.plan] - Planning notes or step-by-step evolution
249
+ * @param {string} [options.filename] - Custom filename (only used when uploading a single file)
250
+ *
251
+ * @returns {Promise<{public_urls: string[], project_folder: string, meta: object}>}
252
+ *
253
+ * @example
254
+ * // Multiple files
255
+ * const result = await cdn.publishToProject(
256
+ * ['audio.mp3', 'cover.jpg'],
257
+ * 'my-project',
258
+ * { description: 'Music cover project' }
259
+ * );
260
+ *
261
+ * // Single file with custom name
262
+ * await cdn.publishToProject('/tmp/file.mp3', 'my-project', { filename: 'final.mp3' });
263
+ */
264
+ async function publishToProject(localPaths, projectSlug, options = {}) {
265
+ const ssh = getSshConfig();
266
+ const slug = projectSlug.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
267
+ const remoteDir = await ensureProjectStructure(slug);
268
+
269
+ const paths = Array.isArray(localPaths) ? localPaths : [localPaths];
270
+ const uploadedFiles = [];
271
+
272
+ for (const localPath of paths) {
273
+ const filename = options.filename && paths.length === 1
274
+ ? options.filename
275
+ : path.basename(localPath);
276
+
277
+ const remoteFile = `${remoteDir}/${filename}`;
278
+ await uploadSingleFile(localPath, remoteFile, ssh);
279
+ uploadedFiles.push(filename);
280
+ }
281
+
282
+ const meta = {
283
+ project: slug,
284
+ created: new Date().toISOString(),
285
+ files: uploadedFiles,
286
+ source: 'cdn.js',
287
+ ssh_ep: ssh.raw
288
+ };
289
+
290
+ console.log(`[cdn] Writing meta.json`);
291
+ await SH`ssh -p ${ssh.port} ${ssh.user}@${ssh.host} "cat > ${remoteDir}/meta.json << 'EOF'
292
+ ${JSON.stringify(meta, null, 2)}
293
+ EOF"`.run();
294
+
295
+ if (options.description) {
296
+ await SH`ssh -p ${ssh.port} ${ssh.user}@${ssh.host} "cat > ${remoteDir}/description.md << 'EOF'
297
+ ${options.description}
298
+ EOF"`.run();
299
+ }
300
+
301
+ if (options.plan) {
302
+ await SH`ssh -p ${ssh.port} ${ssh.user}@${ssh.host} "cat > ${remoteDir}/plan.md << 'EOF'
303
+ ${options.plan}
304
+ EOF"`.run();
305
+ }
306
+
307
+ const publicUrls = uploadedFiles.map(f => `https://${ssh.host}/projects/${slug}/${f}`);
308
+ const projectFolder = `https://${ssh.host}/projects/${slug}/`;
309
+
310
+ return {
311
+ public_urls: publicUrls,
312
+ project_folder: projectFolder,
313
+ meta
314
+ };
315
+ }
316
+
317
+ /**
318
+ * Publish a single local file to an arbitrary location on the public CDN.
319
+ *
320
+ * Automatically creates any missing parent directories.
321
+ * Returns a direct public HTTPS URL.
322
+ *
323
+ * @param {string} localPath - Absolute or relative path to the local file
324
+ * @param {string} remoteRelativePath - Target path under htdocs (e.g. "tmp/audio-123.mp3" or "projects/my-project/reference.wav")
325
+ *
326
+ * @returns {Promise<{public_url: string, remote_path: string}>}
327
+ *
328
+ * @example
329
+ * const result = await cdn.publishFile(
330
+ * '/tmp/generated.mp3',
331
+ * 'tmp/quick-reference.mp3'
332
+ * );
333
+ * console.log(result.public_url);
334
+ */
335
+ async function publishFile(localPath, remoteRelativePath) {
336
+ const ssh = getSshConfig();
337
+ const remoteDir = path.dirname(remoteRelativePath);
338
+
339
+ if (remoteDir && remoteDir !== '.') {
340
+ await ensureRemoteDir(remoteDir);
341
+ }
342
+
343
+ const remoteTarget = `${ssh.user}@${ssh.host}:htdocs/${remoteRelativePath}`;
344
+
345
+ console.log(`[cdn] Uploading via SCP: ${localPath} → ${remoteTarget}`);
346
+ await SH`scp -P ${ssh.port} ${localPath} ${remoteTarget}`.run();
347
+ console.log(`[cdn] ✅ Upload successful`);
348
+
349
+ return {
350
+ public_url: `https://${ssh.host}/${remoteRelativePath}`,
351
+ remote_path: remoteTarget
352
+ };
353
+ }
354
+
355
+ export {
356
+ getSshConfig,
357
+ ensureRemoteDir,
358
+ ensureProjectStructure,
359
+ listProjects,
360
+ deleteProject,
361
+ publishFile,
362
+ publishToProject
363
+ };
364
+
365
+ export default {
366
+ getSshConfig,
367
+ ensureRemoteDir,
368
+ ensureProjectStructure,
369
+ listProjects,
370
+ deleteProject,
371
+ publishFile,
372
+ publishToProject
373
+ };
package/lib/fafs.js CHANGED
@@ -9,7 +9,9 @@ const RELATIVE_CACHE = path.resolve('.cache');
9
9
  new CacheAsync(RELATIVE_CACHE, true, 'bin');
10
10
  const APP_CACHE = path.resolve(RELATIVE_CACHE, APPNAME);
11
11
  new CacheAsync(APP_CACHE, true, 'bin');
12
-
12
+ // Create the minimax temp folder
13
+ new CacheAsync(path.resolve(RELATIVE_CACHE, 'minimax'), true, 'bin');
14
+ new CacheAsync(path.resolve(RELATIVE_CACHE, 'stability'), true, 'bin');
13
15
  /**
14
16
  * Frequently Asked Functions (FAFS) - Utility module providing environment and system information utilities.
15
17
  *