@officexapp/vidfarm-devcli 0.21.51 → 0.21.53
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/.agents/skills/vidfarm/SKILL.md +5 -4
- package/.agents/skills/vidfarm/harnesses/hooks.HARNESS.md +10 -3
- package/.agents/skills/vidfarm/harnesses/product-explainer.HARNESS.md +1 -1
- package/.agents/skills/vidfarm/harnesses/short-form.HARNESS.md +2 -0
- package/.agents/skills/vidfarm/references/assets-and-sourcing.md +89 -5
- package/.agents/skills/vidfarm/references/automation-and-local-dev.md +2 -2
- package/.agents/skills/vidfarm/references/content-ideas.md +72 -1
- package/.agents/skills/vidfarm/references/core-workflows.md +16 -3
- package/.agents/skills/vidfarm/references/editor-workflows.md +32 -8
- package/.agents/skills/vidfarm/references/rest-api.md +1 -1
- package/SKILL.director.md +219 -32
- package/SKILL.md +6 -2
- package/clipper.md +22 -0
- package/dist/src/cli.js +185 -18
- package/dist/src/devcli/marketplace-gigs.js +55 -7
- package/dist/src/devcli/qa-check.js +1 -1
- package/dist/src/devcli/shared-folder.js +333 -44
- package/dist/src/devcli/skill-docs.js +2 -2
- package/experimental/meme-recaption.md +1756 -0
- package/marketplace.md +99 -11
- package/package.json +5 -1
- package/public/assets/file-directory-app.js +33 -33
|
@@ -31,30 +31,53 @@ const GREEN = "\x1b[32m";
|
|
|
31
31
|
const RESET = "\x1b[0m";
|
|
32
32
|
export const SHARED_HELP = `vidfarm shared — use a folder someone shared with you (no account, no API key)
|
|
33
33
|
|
|
34
|
+
THE FAST PATH for a gigworker's agent: one command finds the footage by meaning
|
|
35
|
+
and puts it on disk.
|
|
36
|
+
|
|
37
|
+
vidfarm shared grab <link> "founder talking head, kitchen, no captions" --out ./assets
|
|
38
|
+
|
|
34
39
|
shared info <link> What the link is: folder, mode, what you may do
|
|
35
40
|
shared ls <link> [subfolder] Browse the shared folder → GET /api/v1/share/:token/directory
|
|
41
|
+
--tree Walk every subfolder, not just this one
|
|
36
42
|
--limit <n> --offset <n> --json
|
|
37
|
-
shared search <link> "<query>" Find files by MEANING
|
|
38
|
-
--subfolder <name>
|
|
43
|
+
shared search <link> "<query>" Find files AND folders by MEANING → POST /api/v1/share/:token/directory/search
|
|
44
|
+
--subfolder <name> Search inside one subfolder only
|
|
45
|
+
--limit <n> --json
|
|
46
|
+
shared grab <link> "<query>" Search by meaning, then DOWNLOAD the hits → search + download
|
|
47
|
+
--limit <n> How many to take (default 5)
|
|
48
|
+
--out <dir> Where to write them (default ./)
|
|
49
|
+
--json
|
|
50
|
+
shared note <link> <file|folder> Read the VECTOR NOTE — what the thing is → GET /api/v1/share/:token/directory/note
|
|
51
|
+
--set "<text>" Write it (upload links may FILL an empty note;
|
|
52
|
+
only edit links / the owner may overwrite one)
|
|
53
|
+
--subfolder <name> --json
|
|
39
54
|
shared mkdir <link> <subfolder> Make a subfolder (upload/edit links only) → POST /api/v1/share/:token/directory/folders
|
|
40
55
|
shared put <link> <file…> Upload files into the share (upload/edit) → presign → PUT → finalize
|
|
41
56
|
--subfolder <name> Land them in a subfolder of the shared folder
|
|
57
|
+
--note "<text>" Describe what you uploaded (vector-searchable)
|
|
42
58
|
--json
|
|
43
|
-
shared get <link> <
|
|
59
|
+
shared get <link> <name|path> Download one file out of the share
|
|
44
60
|
--subfolder <name> Look inside a subfolder
|
|
45
61
|
--out <dir|file> Where to write it (default: this folder)
|
|
46
62
|
--all Download every file in the folder instead
|
|
63
|
+
--tree With --all: walk subfolders too
|
|
47
64
|
|
|
48
65
|
<link> is the URL you were given, or just the dsh_… token (then --host <url>).
|
|
49
66
|
|
|
50
67
|
e.g. vidfarm shared info https://vidfarm.cc/directory/preview/dsh_abc/files/project1
|
|
51
|
-
vidfarm shared
|
|
52
|
-
vidfarm shared
|
|
53
|
-
vidfarm shared
|
|
54
|
-
vidfarm shared
|
|
68
|
+
vidfarm shared ls <link> --tree
|
|
69
|
+
vidfarm shared grab <link> "product close-up on a white desk" --limit 3 --out ./b-roll
|
|
70
|
+
vidfarm shared note <link> clip-01.mp4
|
|
71
|
+
vidfarm shared put <link> cut-v2.mp4 --subfolder yvette-batch-01 --note "Final cut, 9:16, captions burned in"
|
|
55
72
|
|
|
56
73
|
Modes: read = browse + search · upload = + add files and subfolders (never delete)
|
|
57
|
-
edit = + rename and delete.
|
|
74
|
+
edit = + rename and delete. /files, /temp and /raws shares accept uploads;
|
|
75
|
+
a /raws share is a drop-box for footage. /projects and /approved are read-only.
|
|
76
|
+
|
|
77
|
+
VECTOR NOTES: every file and folder carries a plain-text note saying what it IS.
|
|
78
|
+
That note is what \`search\` and \`grab\` match on, so a folder full of IMG_4821.mp4
|
|
79
|
+
is only as findable as its notes. Read them before you pick; write one when you
|
|
80
|
+
upload, so the next agent can find your work.`;
|
|
58
81
|
// Accepts a preview URL, an API URL, or a bare token. The trailing path of a
|
|
59
82
|
// preview URL is kept as the default subfolder so a task brief can point at the
|
|
60
83
|
// exact folder the worker should use.
|
|
@@ -125,6 +148,17 @@ async function sharePost(link, route, payload) {
|
|
|
125
148
|
fail(`share ${route}`, response.status, body);
|
|
126
149
|
return body;
|
|
127
150
|
}
|
|
151
|
+
async function sharePut(link, route, payload) {
|
|
152
|
+
const response = await fetch(apiBase(link) + route, {
|
|
153
|
+
method: "PUT",
|
|
154
|
+
headers: { "content-type": "application/json", accept: "application/json" },
|
|
155
|
+
body: JSON.stringify(payload)
|
|
156
|
+
});
|
|
157
|
+
const body = await readJson(response);
|
|
158
|
+
if (!response.ok)
|
|
159
|
+
fail(`share ${route}`, response.status, body);
|
|
160
|
+
return body;
|
|
161
|
+
}
|
|
128
162
|
// The path a command works on: the link's own folder, plus an optional
|
|
129
163
|
// --subfolder. Never an absolute path — a token cannot leave its own subtree.
|
|
130
164
|
function targetPath(link, basePath, subfolder) {
|
|
@@ -144,6 +178,71 @@ async function loadShare(link, subfolder) {
|
|
|
144
178
|
const listing = await shareGet(link, "/directory", { path: wanted });
|
|
145
179
|
return { listing, basePath, path: wanted };
|
|
146
180
|
}
|
|
181
|
+
// Walk the share depth-first and return every folder + file under `startPath`.
|
|
182
|
+
// A share is a small working folder, so a bounded full walk is cheaper for the
|
|
183
|
+
// agent than N round-trips of guessing folder names.
|
|
184
|
+
const SHARE_WALK_MAX_FOLDERS = 200;
|
|
185
|
+
async function walkShare(link, startPath) {
|
|
186
|
+
const folders = [];
|
|
187
|
+
const files = [];
|
|
188
|
+
const queue = [startPath];
|
|
189
|
+
const seen = new Set();
|
|
190
|
+
while (queue.length && seen.size < SHARE_WALK_MAX_FOLDERS) {
|
|
191
|
+
const current = queue.shift();
|
|
192
|
+
if (seen.has(current))
|
|
193
|
+
continue;
|
|
194
|
+
seen.add(current);
|
|
195
|
+
const listing = await shareGet(link, "/directory", { path: current, limit: "500" });
|
|
196
|
+
for (const file of listing.files ?? [])
|
|
197
|
+
files.push(file);
|
|
198
|
+
for (const folder of listing.folders ?? []) {
|
|
199
|
+
folders.push(folder);
|
|
200
|
+
if (folder.path)
|
|
201
|
+
queue.push(folder.path);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return { folders, files };
|
|
205
|
+
}
|
|
206
|
+
/** One-line note preview for list output. */
|
|
207
|
+
function noteLine(item) {
|
|
208
|
+
const note = typeof item?.note === "string" ? item.note.replace(/\s+/g, " ").trim() : "";
|
|
209
|
+
if (!note)
|
|
210
|
+
return "";
|
|
211
|
+
return note.length > 100 ? note.slice(0, 99) + "…" : note;
|
|
212
|
+
}
|
|
213
|
+
/** The "does this have a note?" marker, so a gap is visible at a glance. */
|
|
214
|
+
function noteMark(item) {
|
|
215
|
+
if (item?.note === undefined)
|
|
216
|
+
return " "; // backend carries no note
|
|
217
|
+
return item.note ? `${GREEN}✓${RESET} ` : `${DIM}✎${RESET} `;
|
|
218
|
+
}
|
|
219
|
+
function fileSize(item) {
|
|
220
|
+
return typeof item?.sizeBytes === "number" && item.sizeBytes > 0
|
|
221
|
+
? `${(item.sizeBytes / (1024 * 1024)).toFixed(1)} MB`
|
|
222
|
+
: "";
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Find one entry in the share by exact path, by file name, or by a name
|
|
226
|
+
* fragment — looking in the current folder first, then the whole subtree. This
|
|
227
|
+
* is what lets an agent act on a name it read out of a search result.
|
|
228
|
+
*/
|
|
229
|
+
async function resolveShareEntry(link, basePath, wanted, subfolder) {
|
|
230
|
+
const target = (wanted || "").trim();
|
|
231
|
+
if (!target)
|
|
232
|
+
throw new Error("Name a file or folder from the share.");
|
|
233
|
+
const here = targetPath(link, basePath, subfolder);
|
|
234
|
+
const local = await shareGet(link, "/directory", { path: here, limit: "500" });
|
|
235
|
+
const pools = [...(local.files ?? []), ...(local.folders ?? [])];
|
|
236
|
+
const exact = pools.find((it) => it.path === target || it.name === target);
|
|
237
|
+
if (exact)
|
|
238
|
+
return exact;
|
|
239
|
+
const walked = await walkShare(link, here);
|
|
240
|
+
const all = [...walked.files, ...walked.folders];
|
|
241
|
+
return (all.find((it) => it.path === target)
|
|
242
|
+
?? all.find((it) => it.name === target)
|
|
243
|
+
?? all.find((it) => String(it.name ?? "").toLowerCase().includes(target.toLowerCase()))
|
|
244
|
+
?? (() => { throw new Error(`Nothing named "${target}" in ${here}. Browse it with: vidfarm shared ls <link> --tree`); })());
|
|
245
|
+
}
|
|
147
246
|
// ── commands ─────────────────────────────────────────────────────────────────
|
|
148
247
|
function options() {
|
|
149
248
|
return {
|
|
@@ -154,15 +253,19 @@ function options() {
|
|
|
154
253
|
out: { type: "string" },
|
|
155
254
|
limit: { type: "string" },
|
|
156
255
|
offset: { type: "string" },
|
|
157
|
-
all: { type: "boolean" }
|
|
256
|
+
all: { type: "boolean" },
|
|
257
|
+
tree: { type: "boolean" },
|
|
258
|
+
set: { type: "string" },
|
|
259
|
+
note: { type: "string" },
|
|
260
|
+
"file-id": { type: "string" }
|
|
158
261
|
};
|
|
159
262
|
}
|
|
160
263
|
function modeVerbs(mode) {
|
|
161
264
|
if (mode === "edit")
|
|
162
|
-
return "browse · search · upload · make folders · rename · delete";
|
|
265
|
+
return "browse · search · upload · make folders · rename · delete · write any note";
|
|
163
266
|
if (mode === "upload")
|
|
164
|
-
return "browse · search · upload · make folders (no delete)";
|
|
165
|
-
return "browse · search (read only)";
|
|
267
|
+
return "browse · search · upload · make folders · fill an EMPTY note (no delete, no overwrite)";
|
|
268
|
+
return "browse · search · read notes (read only)";
|
|
166
269
|
}
|
|
167
270
|
async function runInfo(argv) {
|
|
168
271
|
const parsed = parseArgs({ args: argv, allowPositionals: true, options: options() });
|
|
@@ -177,6 +280,11 @@ async function runInfo(argv) {
|
|
|
177
280
|
console.log(` folder : ${basePath}`);
|
|
178
281
|
console.log(` mode : ${mode} ${DIM}(${modeVerbs(mode)})${RESET}`);
|
|
179
282
|
console.log(` files : ${(listing.files ?? []).length} folders: ${(listing.folders ?? []).length}`);
|
|
283
|
+
const noted = (listing.files ?? []).filter((f) => f?.note).length;
|
|
284
|
+
const notable = (listing.files ?? []).filter((f) => f?.note !== undefined).length;
|
|
285
|
+
if (notable)
|
|
286
|
+
console.log(` notes : ${noted}/${notable} files described ${DIM}(search matches on these)${RESET}`);
|
|
287
|
+
console.log(` ${DIM}Find footage by meaning : vidfarm shared grab <link> "<what you need>" --out ./assets${RESET}`);
|
|
180
288
|
if (mode !== "read") {
|
|
181
289
|
console.log(` ${DIM}Make your own subfolder first: vidfarm shared mkdir <link> <your-name>${RESET}`);
|
|
182
290
|
}
|
|
@@ -186,19 +294,42 @@ async function runLs(argv) {
|
|
|
186
294
|
const link = parseSharedLink(parsed.positionals[0], parsed.values.host);
|
|
187
295
|
const subfolder = parsed.values.subfolder ?? parsed.positionals[1];
|
|
188
296
|
const { listing, path: shownPath } = await loadShare(link, subfolder);
|
|
297
|
+
const walked = parsed.values.tree ? await walkShare(link, shownPath) : null;
|
|
298
|
+
const folders = walked ? walked.folders : (listing.folders ?? []);
|
|
299
|
+
const files = walked ? walked.files : (listing.files ?? []);
|
|
189
300
|
if (parsed.values.json) {
|
|
190
|
-
console.log(JSON.stringify(listing, null, 2));
|
|
301
|
+
console.log(JSON.stringify(walked ? { path: shownPath, share: listing.share, ...walked } : listing, null, 2));
|
|
191
302
|
return;
|
|
192
303
|
}
|
|
193
304
|
console.log(`${BOLD}${shownPath}${RESET} ${DIM}(${listing.share?.mode ?? "read"} link)${RESET}`);
|
|
194
|
-
for (const folder of
|
|
195
|
-
console.log(` ${BOLD}${folder.name}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
305
|
+
for (const folder of folders) {
|
|
306
|
+
console.log(` ${noteMark(folder)}${BOLD}${walked ? folder.path : folder.name + "/"}${RESET}`);
|
|
307
|
+
const note = noteLine(folder);
|
|
308
|
+
if (note)
|
|
309
|
+
console.log(` ${DIM}${note}${RESET}`);
|
|
199
310
|
}
|
|
200
|
-
|
|
311
|
+
for (const file of files) {
|
|
312
|
+
const size = fileSize(file);
|
|
313
|
+
console.log(` ${noteMark(file)}${walked ? file.path : file.name} ${DIM}${size}${RESET}`);
|
|
314
|
+
const note = noteLine(file);
|
|
315
|
+
if (note)
|
|
316
|
+
console.log(` ${DIM}${note}${RESET}`);
|
|
317
|
+
}
|
|
318
|
+
if (!folders.length && !files.length)
|
|
201
319
|
console.log(` ${DIM}empty${RESET}`);
|
|
320
|
+
else
|
|
321
|
+
console.log(`\n ${DIM}${GREEN}✓${DIM} = has a vector note · ✎ = no note yet (read one with: vidfarm shared note <link> <name>)${RESET}`);
|
|
322
|
+
}
|
|
323
|
+
// Search the share by MEANING. Returns files AND folders — a well-noted folder
|
|
324
|
+
// is a legitimate hit ("all the kitchen b-roll is in here").
|
|
325
|
+
async function searchShare(link, argv) {
|
|
326
|
+
const scope = targetPath(link, argv.basePath, argv.subfolder);
|
|
327
|
+
const body = await sharePost(link, "/directory/search", {
|
|
328
|
+
query: argv.query,
|
|
329
|
+
path: scope,
|
|
330
|
+
...(argv.limit != null && Number.isFinite(argv.limit) ? { limit: argv.limit } : {})
|
|
331
|
+
});
|
|
332
|
+
return { scope, body, results: body.results ?? body.items ?? [] };
|
|
202
333
|
}
|
|
203
334
|
async function runSearch(argv) {
|
|
204
335
|
const parsed = parseArgs({ args: argv, allowPositionals: true, options: options() });
|
|
@@ -207,25 +338,112 @@ async function runSearch(argv) {
|
|
|
207
338
|
if (!query)
|
|
208
339
|
throw new Error('shared search needs a query: vidfarm shared search <link> "founder talking head"');
|
|
209
340
|
const { basePath } = await loadShare(link);
|
|
210
|
-
const scope =
|
|
211
|
-
const limit = parsed.values.limit != null ? Number(parsed.values.limit) : undefined;
|
|
212
|
-
const body = await sharePost(link, "/directory/search", {
|
|
341
|
+
const { scope, body, results } = await searchShare(link, {
|
|
213
342
|
query,
|
|
214
|
-
|
|
215
|
-
|
|
343
|
+
basePath,
|
|
344
|
+
subfolder: parsed.values.subfolder,
|
|
345
|
+
limit: parsed.values.limit != null ? Number(parsed.values.limit) : undefined
|
|
216
346
|
});
|
|
217
347
|
if (parsed.values.json) {
|
|
218
348
|
console.log(JSON.stringify(body, null, 2));
|
|
219
349
|
return;
|
|
220
350
|
}
|
|
221
|
-
const
|
|
351
|
+
const how = body.semantic === true ? "by meaning" : "keyword/path only (the owner has no embedding key)";
|
|
352
|
+
console.log(`${BOLD}"${query}"${RESET} ${DIM}in ${scope} · ${how}${RESET} → ${results.length} result(s)\n`);
|
|
222
353
|
if (!results.length) {
|
|
223
|
-
console.log(
|
|
354
|
+
console.log(` ${DIM}No matches. Try \`vidfarm shared ls <link> --tree\` to see everything.${RESET}`);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
results.forEach((hit, i) => {
|
|
358
|
+
const score = typeof hit.score === "number" ? ` ${DIM}score=${hit.score.toFixed(3)}${RESET}` : "";
|
|
359
|
+
const kind = hit.kind === "folder" ? "dir " : "file";
|
|
360
|
+
console.log(` ${String(i + 1).padStart(2)}. ${kind} ${noteMark(hit)}${hit.path ?? hit.name}${score}`);
|
|
361
|
+
const note = noteLine(hit);
|
|
362
|
+
if (note)
|
|
363
|
+
console.log(` ${DIM}${note}${RESET}`);
|
|
364
|
+
const size = fileSize(hit);
|
|
365
|
+
if (size)
|
|
366
|
+
console.log(` ${DIM}${size}${RESET}`);
|
|
367
|
+
});
|
|
368
|
+
console.log(`\n ${DIM}Take them: vidfarm shared grab <link> "${query}" --out ./assets${RESET}`);
|
|
369
|
+
}
|
|
370
|
+
// search + download, in one command. This is the command a gigworker's agent
|
|
371
|
+
// actually runs: "give me the footage that looks like X".
|
|
372
|
+
async function runGrab(argv) {
|
|
373
|
+
const parsed = parseArgs({ args: argv, allowPositionals: true, options: options() });
|
|
374
|
+
const link = parseSharedLink(parsed.positionals[0], parsed.values.host);
|
|
375
|
+
const query = parsed.positionals.slice(1).join(" ").trim();
|
|
376
|
+
if (!query)
|
|
377
|
+
throw new Error('shared grab needs a query: vidfarm shared grab <link> "kitchen b-roll" --out ./assets');
|
|
378
|
+
const limit = parsed.values.limit != null ? Number(parsed.values.limit) : 5;
|
|
379
|
+
const out = parsed.values.out || ".";
|
|
380
|
+
const { basePath } = await loadShare(link);
|
|
381
|
+
const { scope, results } = await searchShare(link, {
|
|
382
|
+
query,
|
|
383
|
+
basePath,
|
|
384
|
+
subfolder: parsed.values.subfolder,
|
|
385
|
+
limit: Number.isFinite(limit) ? limit : 5
|
|
386
|
+
});
|
|
387
|
+
const downloadable = results.filter((hit) => hit.kind !== "folder" && hit.viewUrl);
|
|
388
|
+
if (!downloadable.length) {
|
|
389
|
+
const folders = results.filter((hit) => hit.kind === "folder");
|
|
390
|
+
if (folders.length) {
|
|
391
|
+
console.log(`${DIM}Only folders matched. Look inside one: vidfarm shared ls <link> ${folders[0].path}${RESET}`);
|
|
392
|
+
}
|
|
393
|
+
else {
|
|
394
|
+
console.log(`${DIM}Nothing to download for "${query}" in ${scope}.${RESET}`);
|
|
395
|
+
}
|
|
224
396
|
return;
|
|
225
397
|
}
|
|
226
|
-
|
|
227
|
-
|
|
398
|
+
const taken = [];
|
|
399
|
+
for (const hit of downloadable) {
|
|
400
|
+
const dest = path.join(out, hit.name);
|
|
401
|
+
await downloadTo(hit.viewUrl, dest, link);
|
|
402
|
+
taken.push({ path: hit.path, name: hit.name, note: hit.note ?? null, file: dest, score: hit.score });
|
|
403
|
+
if (!parsed.values.json) {
|
|
404
|
+
console.log(`${GREEN}✓${RESET} ${dest} ${DIM}${hit.path}${RESET}`);
|
|
405
|
+
const note = noteLine(hit);
|
|
406
|
+
if (note)
|
|
407
|
+
console.log(` ${DIM}${note}${RESET}`);
|
|
408
|
+
}
|
|
228
409
|
}
|
|
410
|
+
if (parsed.values.json)
|
|
411
|
+
console.log(JSON.stringify({ ok: true, query, scope, taken }, null, 2));
|
|
412
|
+
else
|
|
413
|
+
console.log(`\n ${DIM}${taken.length} file(s) for "${query}"${RESET}`);
|
|
414
|
+
}
|
|
415
|
+
// Read or write the vector note of one entry in the share.
|
|
416
|
+
async function runNote(argv) {
|
|
417
|
+
const parsed = parseArgs({ args: argv, allowPositionals: true, options: options() });
|
|
418
|
+
const link = parseSharedLink(parsed.positionals[0], parsed.values.host);
|
|
419
|
+
const wanted = parsed.positionals.slice(1).join(" ").trim();
|
|
420
|
+
const { listing, basePath } = await loadShare(link);
|
|
421
|
+
const mode = listing.share?.mode ?? "read";
|
|
422
|
+
const entry = await resolveShareEntry(link, basePath, wanted, parsed.values.subfolder);
|
|
423
|
+
const setValue = parsed.values.set;
|
|
424
|
+
if (setValue == null) {
|
|
425
|
+
const body = await shareGet(link, "/directory/note", { path: entry.path, id: entry.id });
|
|
426
|
+
if (parsed.values.json) {
|
|
427
|
+
console.log(JSON.stringify(body, null, 2));
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
console.log(`${BOLD}${entry.path}${RESET}`);
|
|
431
|
+
console.log(body.note ? ` ${body.note}` : ` ${DIM}(no vector note yet)${RESET}`);
|
|
432
|
+
if (body.can_edit)
|
|
433
|
+
console.log(` ${DIM}Write one: vidfarm shared note <link> ${entry.name} --set "what this is"${RESET}`);
|
|
434
|
+
else if (mode === "upload")
|
|
435
|
+
console.log(` ${DIM}This note is already written — an upload link may only fill an EMPTY note.${RESET}`);
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
const body = await sharePut(link, "/directory/note", { path: entry.path, id: entry.id ?? null, note: setValue });
|
|
439
|
+
if (parsed.values.json) {
|
|
440
|
+
console.log(JSON.stringify(body, null, 2));
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
console.log(`${GREEN}✓${RESET} note saved on ${BOLD}${entry.path}${RESET}`);
|
|
444
|
+
console.log(body.embedded
|
|
445
|
+
? ` ${DIM}searchable by meaning${RESET}`
|
|
446
|
+
: ` ${DIM}saved, but keyword-search only — the folder's owner has no embedding key saved${RESET}`);
|
|
229
447
|
}
|
|
230
448
|
async function runMkdir(argv) {
|
|
231
449
|
const parsed = parseArgs({ args: argv, allowPositionals: true, options: options() });
|
|
@@ -309,52 +527,118 @@ async function runPut(argv) {
|
|
|
309
527
|
// The API wants the folder RELATIVE to the root (no /files prefix).
|
|
310
528
|
const wanted = targetPath(link, basePath, subfolder);
|
|
311
529
|
const folderPath = wanted.replace(/^\/(files|temp|raws|approved|projects)\/?/, "");
|
|
530
|
+
const note = parsed.values.note?.trim();
|
|
312
531
|
const saved = [];
|
|
313
532
|
for (const file of files) {
|
|
314
533
|
const record = await uploadOne(link, file, folderPath);
|
|
315
534
|
saved.push(record);
|
|
316
535
|
if (!parsed.values.json)
|
|
317
536
|
console.log(`${GREEN}✓${RESET} ${path.basename(file)} → ${BOLD}${wanted}${RESET}`);
|
|
537
|
+
// Say what you uploaded, so the next agent can find it by meaning. An
|
|
538
|
+
// upload link may fill an empty note, which a fresh upload always has —
|
|
539
|
+
// unless the server already auto-described it on the owner's AI key.
|
|
540
|
+
if (note) {
|
|
541
|
+
const uploadedPath = `${wanted.replace(/\/+$/, "")}/${record?.fileName ?? path.basename(file)}`;
|
|
542
|
+
await sharePut(link, "/directory/note", { path: uploadedPath, id: record?.id ?? null, note })
|
|
543
|
+
.then(() => { if (!parsed.values.json)
|
|
544
|
+
console.log(` ${DIM}note saved${RESET}`); })
|
|
545
|
+
.catch((error) => {
|
|
546
|
+
if (!parsed.values.json)
|
|
547
|
+
console.log(` ${DIM}note not saved: ${error.message}${RESET}`);
|
|
548
|
+
});
|
|
549
|
+
}
|
|
318
550
|
}
|
|
319
551
|
if (parsed.values.json)
|
|
320
552
|
console.log(JSON.stringify({ ok: true, uploaded: saved }, null, 2));
|
|
553
|
+
else if (!note) {
|
|
554
|
+
console.log(`\n ${DIM}Tip: add --note "what this is" so the owner's agent can find it by meaning.${RESET}`);
|
|
555
|
+
}
|
|
321
556
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
557
|
+
// Fetch one file out of the share. A stored view URL is not always reachable as
|
|
558
|
+
// written: it may be relative, or absolute against the host the owner's server
|
|
559
|
+
// had configured when the file was saved (a self-hosted box, a moved domain).
|
|
560
|
+
// The link we were handed is the host we KNOW answers, so fall back to it,
|
|
561
|
+
// keeping the URL's own path.
|
|
562
|
+
async function downloadTo(url, outPath, link) {
|
|
563
|
+
const candidates = [];
|
|
564
|
+
if (/^https?:\/\//i.test(url)) {
|
|
565
|
+
candidates.push(url);
|
|
566
|
+
if (link) {
|
|
567
|
+
try {
|
|
568
|
+
const parsed = new URL(url);
|
|
569
|
+
const viaLink = link.host + parsed.pathname + parsed.search;
|
|
570
|
+
if (viaLink !== url)
|
|
571
|
+
candidates.push(viaLink);
|
|
572
|
+
}
|
|
573
|
+
catch { /* unparseable — the direct attempt is all we have */ }
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
else if (link) {
|
|
577
|
+
candidates.push(link.host + (url.startsWith("/") ? url : `/${url}`));
|
|
578
|
+
}
|
|
579
|
+
else {
|
|
580
|
+
candidates.push(url);
|
|
581
|
+
}
|
|
582
|
+
let lastError = "";
|
|
583
|
+
for (const candidate of candidates) {
|
|
584
|
+
try {
|
|
585
|
+
const response = await fetch(candidate);
|
|
586
|
+
if (!response.ok) {
|
|
587
|
+
lastError = `HTTP ${response.status}`;
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
const bytes = Buffer.from(await response.arrayBuffer());
|
|
591
|
+
mkdirSync(path.dirname(outPath), { recursive: true });
|
|
592
|
+
writeFileSync(outPath, bytes);
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
catch (error) {
|
|
596
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
throw new Error(`download failed (${lastError}): ${url}`);
|
|
329
600
|
}
|
|
330
601
|
async function runGet(argv) {
|
|
331
602
|
const parsed = parseArgs({ args: argv, allowPositionals: true, options: options() });
|
|
332
603
|
const link = parseSharedLink(parsed.positionals[0], parsed.values.host);
|
|
333
604
|
const wantedName = parsed.positionals[1];
|
|
334
|
-
const { listing, path: shownPath } = await loadShare(link, parsed.values.subfolder);
|
|
335
|
-
const files = listing.files ?? [];
|
|
605
|
+
const { listing, basePath, path: shownPath } = await loadShare(link, parsed.values.subfolder);
|
|
336
606
|
const out = parsed.values.out || ".";
|
|
337
607
|
if (parsed.values.all || !wantedName) {
|
|
338
608
|
if (!parsed.values.all)
|
|
339
609
|
throw new Error("shared get needs a file name, or --all to take the whole folder.");
|
|
610
|
+
// --tree takes the whole subtree, mirroring the folder layout on disk so a
|
|
611
|
+
// deep share doesn't collapse into one flat directory of colliding names.
|
|
612
|
+
const files = parsed.values.tree ? (await walkShare(link, shownPath)).files : (listing.files ?? []);
|
|
613
|
+
let taken = 0;
|
|
340
614
|
for (const file of files) {
|
|
341
615
|
if (!file.viewUrl)
|
|
342
616
|
continue;
|
|
343
|
-
const
|
|
344
|
-
|
|
617
|
+
const relative = parsed.values.tree && typeof file.path === "string" && file.path.startsWith(shownPath)
|
|
618
|
+
? file.path.slice(shownPath.length).replace(/^\/+/, "")
|
|
619
|
+
: file.name;
|
|
620
|
+
const dest = path.join(out, relative);
|
|
621
|
+
await downloadTo(file.viewUrl, dest, link);
|
|
622
|
+
taken += 1;
|
|
345
623
|
console.log(`${GREEN}✓${RESET} ${dest}`);
|
|
346
624
|
}
|
|
347
|
-
console.log(`${DIM}${
|
|
625
|
+
console.log(`${DIM}${taken} file(s) from ${shownPath}${RESET}`);
|
|
348
626
|
return;
|
|
349
627
|
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
628
|
+
// Accepts a bare name, a name fragment, or the full canonical path a search
|
|
629
|
+
// result printed — an agent should never have to translate between them.
|
|
630
|
+
const match = await resolveShareEntry(link, basePath, wantedName, parsed.values.subfolder);
|
|
631
|
+
if (match.kind === "folder") {
|
|
632
|
+
throw new Error(`"${match.path}" is a folder. Take it with: vidfarm shared get <link> --all --tree --subfolder ${match.name}`);
|
|
633
|
+
}
|
|
353
634
|
if (!match.viewUrl)
|
|
354
635
|
throw new Error(`"${match.name}" has no downloadable URL.`);
|
|
355
636
|
const dest = /\.[A-Za-z0-9]+$/.test(out) ? out : path.join(out, match.name);
|
|
356
|
-
await downloadTo(match.viewUrl, dest);
|
|
637
|
+
await downloadTo(match.viewUrl, dest, link);
|
|
357
638
|
console.log(`${GREEN}✓${RESET} ${dest}`);
|
|
639
|
+
const note = noteLine(match);
|
|
640
|
+
if (note)
|
|
641
|
+
console.log(` ${DIM}${note}${RESET}`);
|
|
358
642
|
}
|
|
359
643
|
export async function runSharedCommand(argv) {
|
|
360
644
|
const sub = argv[0];
|
|
@@ -366,6 +650,11 @@ export async function runSharedCommand(argv) {
|
|
|
366
650
|
case "list": return runLs(rest);
|
|
367
651
|
case "search":
|
|
368
652
|
case "find": return runSearch(rest);
|
|
653
|
+
case "grab":
|
|
654
|
+
case "fetch": return runGrab(rest);
|
|
655
|
+
case "note":
|
|
656
|
+
case "notes": return runNote(rest);
|
|
657
|
+
case "tree": return runLs([...rest, "--tree"]);
|
|
369
658
|
case "mkdir":
|
|
370
659
|
case "folder": return runMkdir(rest);
|
|
371
660
|
case "put":
|
|
@@ -117,8 +117,8 @@ export const PACK_TOPICS = [
|
|
|
117
117
|
blurb: "The 5-stage ladder — what the viewer knows, what the video must do, and what it may ask for" },
|
|
118
118
|
{ topic: "problem-angles", aliases: ["angle", "lenses", "problem-angle"], doc: "references/content-ideas.md", heading: "The problem angles",
|
|
119
119
|
blurb: "44 angles on the problem — hold the frame, change the angle when a topic is \"already covered\"" },
|
|
120
|
-
{ topic: "meme-recaption", aliases: ["meme", "recaption", "meme-caption"], doc: "references/editor-workflows.md", heading: "Writing a meme recaption",
|
|
121
|
-
blurb: "Recaption a meme at a pain or a win the niche knows — the cold-viewer test" },
|
|
120
|
+
{ topic: "meme-recaption", aliases: ["meme", "recaption", "meme-caption", "meme_recaption"], doc: "references/editor-workflows.md", heading: "Writing a meme recaption",
|
|
121
|
+
blurb: "Recaption a meme at a pain or a win the niche knows — the cold-viewer test. Building one from scratch? the full format is vidfarm.cc/experimental/meme-recaption.md" },
|
|
122
122
|
{ topic: "product-explainer", aliases: ["product-explainers"], doc: "harnesses/product-explainer.HARNESS.md",
|
|
123
123
|
blurb: "The product-explainer harness — the bundled base for explaining what a product does" },
|
|
124
124
|
{ topic: "product-demo", aliases: ["demo"], doc: "harnesses/product-demo.HARNESS.md",
|