@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.
- package/README.md +2 -0
- package/README.md.bak.1779452127 +240 -0
- package/TODO.md +30 -8
- package/agents/code_agent.js +6 -6
- package/agents/daisy_agent.js +10 -7
- package/agents/minimax.js +173 -0
- package/agents/stability.js +173 -0
- package/bin/codeDave +1 -1
- package/bin/dave.js +1 -1
- package/docs/music-toolsets.md +137 -0
- package/docs/plans/minimax-music-generation.md +80 -0
- package/docs/plans/unified-agent-architecture.md +146 -0
- package/docs/plans/websocket-streaming-plan.md.bak +317 -0
- package/docs/prompt/task_clarification_and_documentation.md +35 -0
- package/lib/API/minimax/ImageToolset.js +169 -0
- package/lib/API/minimax/MusicToolset.js +290 -0
- package/lib/API/minimax/VideoToolset.js +296 -0
- package/lib/API/minimax/image.generation.md +239 -0
- package/lib/API/minimax/image.js +219 -0
- package/lib/API/minimax/image.to.image.md +257 -0
- package/lib/API/minimax/index.js +16 -0
- package/lib/API/minimax/music.cover.preprocess.md +206 -0
- package/lib/API/minimax/music.generation.md +346 -0
- package/lib/API/minimax/music.js +257 -0
- package/lib/API/minimax/music.lyrics.generation.md +205 -0
- package/lib/API/minimax/video.download.md +133 -0
- package/lib/API/minimax/video.first.last.image.md +186 -0
- package/lib/API/minimax/video.from.image.md +206 -0
- package/lib/API/minimax/video.from.subject.md +164 -0
- package/lib/API/minimax/video.generation.md +192 -0
- package/lib/API/minimax/video.js +339 -0
- package/lib/API/minimax/video.query.md +128 -0
- package/lib/API/stability.ai/ImageToolset.js +357 -0
- package/lib/API/stability.ai/MusicToolset.js +302 -0
- package/lib/API/stability.ai/audio-3.md +205 -0
- package/lib/API/stability.ai/audio.js +679 -0
- package/lib/API/stability.ai/image.js +911 -0
- package/lib/API/stability.ai/image.md +271 -0
- package/lib/API/stability.ai/index.js +11 -0
- package/lib/API/stability.ai/openapi.json +17118 -0
- package/lib/API/x.ai/ImageToolset.js +165 -0
- package/lib/API/x.ai/image.editing.md +86 -0
- package/lib/API/x.ai/image.js +393 -0
- package/lib/API/x.ai/image.md +213 -0
- package/lib/API/x.ai/image.to.generation.md +494 -0
- package/lib/API/x.ai/image.to.video.md +23 -0
- package/lib/API/x.ai/index.js +7 -0
- package/lib/AgentManager.js +1 -1
- package/lib/CdnToolset.js +191 -0
- package/lib/ToolSet.js +19 -1
- package/lib/cdn.js +373 -0
- package/lib/fafs.js +3 -1
- package/lib/genericToolset.js +43 -166
- package/lib/index.js +9 -1
- package/package.json +2 -2
- package/types/API/minimax/ImageToolset.d.ts +3 -0
- package/types/API/minimax/MusicToolset.d.ts +3 -0
- package/types/API/minimax/VideoToolset.d.ts +3 -0
- package/types/API/minimax/image.d.ts +109 -0
- package/types/API/minimax/index.d.ts +15 -0
- package/types/API/minimax/music.d.ts +46 -0
- package/types/API/minimax/video.d.ts +165 -0
- package/types/API/stability.ai/ImageToolset.d.ts +3 -0
- package/types/API/stability.ai/MusicToolset.d.ts +3 -0
- package/types/API/stability.ai/audio.d.ts +193 -0
- package/types/API/stability.ai/image.d.ts +274 -0
- package/types/API/stability.ai/index.d.ts +11 -0
- package/types/API/x.ai/ImageToolset.d.ts +3 -0
- package/types/API/x.ai/image.d.ts +82 -0
- package/types/API/x.ai/index.d.ts +7 -0
- package/types/AgentManager.d.ts +1 -1
- package/types/CdnToolset.d.ts +20 -0
- package/types/ToolSet.d.ts +8 -0
- package/types/cdn.d.ts +141 -0
- package/types/index.d.ts +9 -2
- 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
|
+
|
package/lib/AgentManager.js
CHANGED
|
@@ -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
|
*
|