@blockrun/franklin 3.15.57 → 3.15.59
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/dist/commands/content.d.ts +17 -0
- package/dist/commands/content.js +141 -0
- package/dist/index.js +5 -0
- package/dist/tools/videogen.js +25 -2
- package/package.json +1 -1
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `franklin content` CLI surface — human-facing read access to the
|
|
3
|
+
* Content library that lives at ~/.blockrun/content.json.
|
|
4
|
+
*
|
|
5
|
+
* Tools (ContentCreate / ContentAddAsset) write the library during agent
|
|
6
|
+
* sessions; before this command, there was no way to see the resulting
|
|
7
|
+
* spend without scripting against the JSON file. Verified 2026-05-04 in
|
|
8
|
+
* a live session: user asked "我花了多少钱做这个", agent ran
|
|
9
|
+
* `franklin content list` and got "no content subcommand", fell back to
|
|
10
|
+
* estimating from memory.
|
|
11
|
+
*
|
|
12
|
+
* Subcommands:
|
|
13
|
+
* - list : table of id, type, title, status, spent/budget, assets
|
|
14
|
+
* - show <idOrPrefix>: full detail of one Content, including each asset
|
|
15
|
+
*/
|
|
16
|
+
import { Command } from 'commander';
|
|
17
|
+
export declare function buildContentCommand(): Command;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `franklin content` CLI surface — human-facing read access to the
|
|
3
|
+
* Content library that lives at ~/.blockrun/content.json.
|
|
4
|
+
*
|
|
5
|
+
* Tools (ContentCreate / ContentAddAsset) write the library during agent
|
|
6
|
+
* sessions; before this command, there was no way to see the resulting
|
|
7
|
+
* spend without scripting against the JSON file. Verified 2026-05-04 in
|
|
8
|
+
* a live session: user asked "我花了多少钱做这个", agent ran
|
|
9
|
+
* `franklin content list` and got "no content subcommand", fell back to
|
|
10
|
+
* estimating from memory.
|
|
11
|
+
*
|
|
12
|
+
* Subcommands:
|
|
13
|
+
* - list : table of id, type, title, status, spent/budget, assets
|
|
14
|
+
* - show <idOrPrefix>: full detail of one Content, including each asset
|
|
15
|
+
*/
|
|
16
|
+
import os from 'node:os';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import { Command } from 'commander';
|
|
19
|
+
import { loadLibrary } from '../content/store.js';
|
|
20
|
+
const DEFAULT_PATH = path.join(os.homedir(), '.blockrun', 'content.json');
|
|
21
|
+
function fmtUsd(n) {
|
|
22
|
+
return `$${n.toFixed(2)}`;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Resolve a user-typed id-or-prefix to a single Content record. Returns
|
|
26
|
+
* either the matching record or an error message; the caller prints.
|
|
27
|
+
*
|
|
28
|
+
* Accepts the full UUID, a prefix (≥4 chars), or — for convenience — a
|
|
29
|
+
* substring match against the title (case-insensitive). Ambiguity returns
|
|
30
|
+
* the candidates so the user can disambiguate without rerunning blind.
|
|
31
|
+
*/
|
|
32
|
+
function resolveContent(all, input) {
|
|
33
|
+
const q = input.trim();
|
|
34
|
+
if (!q)
|
|
35
|
+
return { error: 'Provide an id, id-prefix, or title substring.' };
|
|
36
|
+
const exact = all.find(c => c.id === q);
|
|
37
|
+
if (exact)
|
|
38
|
+
return { found: exact };
|
|
39
|
+
const prefix = q.length >= 4 ? all.filter(c => c.id.startsWith(q)) : [];
|
|
40
|
+
if (prefix.length === 1)
|
|
41
|
+
return { found: prefix[0] };
|
|
42
|
+
if (prefix.length > 1) {
|
|
43
|
+
return { error: `Ambiguous prefix "${q}" — matches:\n${prefix.map(c => ` ${c.id} ${c.title}`).join('\n')}` };
|
|
44
|
+
}
|
|
45
|
+
const lower = q.toLowerCase();
|
|
46
|
+
const titled = all.filter(c => c.title.toLowerCase().includes(lower));
|
|
47
|
+
if (titled.length === 1)
|
|
48
|
+
return { found: titled[0] };
|
|
49
|
+
if (titled.length > 1) {
|
|
50
|
+
return { error: `Ambiguous title "${q}" — matches:\n${titled.map(c => ` ${c.id} ${c.title}`).join('\n')}` };
|
|
51
|
+
}
|
|
52
|
+
return { error: `No Content matches "${q}".` };
|
|
53
|
+
}
|
|
54
|
+
export function buildContentCommand() {
|
|
55
|
+
const cmd = new Command('content').description('Inspect Content library (assets, spend, budget)');
|
|
56
|
+
cmd
|
|
57
|
+
.command('list')
|
|
58
|
+
.description('List all Content records, newest first')
|
|
59
|
+
.action(() => {
|
|
60
|
+
const lib = loadLibrary(DEFAULT_PATH);
|
|
61
|
+
if (!lib) {
|
|
62
|
+
console.log('No Content library yet. Tools like ContentCreate populate it during agent sessions.');
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const all = lib.list();
|
|
66
|
+
if (all.length === 0) {
|
|
67
|
+
console.log('No Content records.');
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
// Header + rows. Truncate id to 8-char prefix and title to 40 chars
|
|
71
|
+
// so common terminal widths (80-100) fit a row on one line.
|
|
72
|
+
console.log(['id'.padEnd(8), 'type'.padEnd(8), 'status'.padEnd(10), 'spent/cap'.padEnd(13), 'assets', 'title'].join(' '));
|
|
73
|
+
for (const c of all) {
|
|
74
|
+
const id8 = c.id.slice(0, 8);
|
|
75
|
+
const spend = `${fmtUsd(c.spentUsd)}/${fmtUsd(c.budgetUsd)}`;
|
|
76
|
+
const title = c.title.length > 40 ? c.title.slice(0, 39) + '…' : c.title;
|
|
77
|
+
console.log([
|
|
78
|
+
id8.padEnd(8),
|
|
79
|
+
c.type.padEnd(8),
|
|
80
|
+
c.status.padEnd(10),
|
|
81
|
+
spend.padEnd(13),
|
|
82
|
+
String(c.assets.length).padEnd(6),
|
|
83
|
+
title,
|
|
84
|
+
].join(' '));
|
|
85
|
+
}
|
|
86
|
+
// Footer with rolled-up spend.
|
|
87
|
+
const totalSpent = all.reduce((s, c) => s + c.spentUsd, 0);
|
|
88
|
+
const totalBudget = all.reduce((s, c) => s + c.budgetUsd, 0);
|
|
89
|
+
console.log();
|
|
90
|
+
console.log(`Total: ${fmtUsd(totalSpent)} spent across ${all.length} content${all.length === 1 ? '' : 's'} (cap ${fmtUsd(totalBudget)}).`);
|
|
91
|
+
});
|
|
92
|
+
cmd
|
|
93
|
+
.command('show <idOrPrefix>')
|
|
94
|
+
.description('Show full detail for one Content record (id, prefix, or title substring)')
|
|
95
|
+
.action((input) => {
|
|
96
|
+
const lib = loadLibrary(DEFAULT_PATH);
|
|
97
|
+
if (!lib) {
|
|
98
|
+
console.log('No Content library yet.');
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
const result = resolveContent(lib.list(), input);
|
|
102
|
+
if ('error' in result) {
|
|
103
|
+
console.error(result.error);
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
const c = result.found;
|
|
107
|
+
console.log(`# ${c.title}`);
|
|
108
|
+
console.log();
|
|
109
|
+
console.log(`id: ${c.id}`);
|
|
110
|
+
console.log(`type: ${c.type}`);
|
|
111
|
+
console.log(`status: ${c.status}`);
|
|
112
|
+
console.log(`spent: ${fmtUsd(c.spentUsd)} / ${fmtUsd(c.budgetUsd)} cap`);
|
|
113
|
+
console.log(`created: ${new Date(c.createdAt).toISOString()}`);
|
|
114
|
+
if (c.publishedAt)
|
|
115
|
+
console.log(`published: ${new Date(c.publishedAt).toISOString()}`);
|
|
116
|
+
console.log();
|
|
117
|
+
if (c.assets.length > 0) {
|
|
118
|
+
console.log(`## Assets (${c.assets.length})`);
|
|
119
|
+
for (const a of c.assets) {
|
|
120
|
+
console.log(`- ${a.kind.padEnd(6)} ${fmtUsd(a.costUsd).padStart(7)} ${a.source}`);
|
|
121
|
+
console.log(` ${a.data}`);
|
|
122
|
+
}
|
|
123
|
+
console.log();
|
|
124
|
+
}
|
|
125
|
+
if (c.drafts.length > 0) {
|
|
126
|
+
console.log(`## Drafts (${c.drafts.length})`);
|
|
127
|
+
c.drafts.forEach((d, i) => {
|
|
128
|
+
const preview = d.text.length > 80 ? d.text.slice(0, 79) + '…' : d.text;
|
|
129
|
+
console.log(`- #${i + 1} ${preview}`);
|
|
130
|
+
});
|
|
131
|
+
console.log();
|
|
132
|
+
}
|
|
133
|
+
if (c.distribution.length > 0) {
|
|
134
|
+
console.log(`## Distribution (${c.distribution.length})`);
|
|
135
|
+
for (const dist of c.distribution) {
|
|
136
|
+
console.log(`- ${dist.channel}${dist.url ? ` ${dist.url}` : ''} (${new Date(dist.at).toISOString()})`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
return cmd;
|
|
141
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -24,6 +24,7 @@ import { initCommand } from './commands/init.js';
|
|
|
24
24
|
import { uninitCommand } from './commands/uninit.js';
|
|
25
25
|
import { proxyCommand } from './commands/proxy.js';
|
|
26
26
|
import { buildTaskCommand } from './commands/task.js';
|
|
27
|
+
import { buildContentCommand } from './commands/content.js';
|
|
27
28
|
import { VERSION as version } from './config.js';
|
|
28
29
|
const program = new Command();
|
|
29
30
|
program
|
|
@@ -221,6 +222,10 @@ program
|
|
|
221
222
|
// `franklin task <subcmd>` — human-facing CLI for detached background tasks.
|
|
222
223
|
// Defined in src/commands/task.ts; subcommands: list, tail, cancel, wait.
|
|
223
224
|
program.addCommand(buildTaskCommand());
|
|
225
|
+
// `franklin content <subcmd>` — read access to the Content library
|
|
226
|
+
// (~/.blockrun/content.json) so users + agent shell-outs can inspect
|
|
227
|
+
// spend without scripting against the JSON file. Subcommands: list, show.
|
|
228
|
+
program.addCommand(buildContentCommand());
|
|
224
229
|
// Hidden internal subcommand — invoked by startDetachedTask via spawn(detached).
|
|
225
230
|
// The underscore prefix signals "not for humans"; we still register it via
|
|
226
231
|
// commander so exit codes and arg parsing stay consistent with the rest of
|
package/dist/tools/videogen.js
CHANGED
|
@@ -44,7 +44,7 @@ function estimateVideoCostUsd(durationSeconds = DEFAULT_DURATION) {
|
|
|
44
44
|
function buildExecute(deps) {
|
|
45
45
|
return async function execute(input, ctx) {
|
|
46
46
|
const rawInput = input;
|
|
47
|
-
const { output_path, model, image_url, duration_seconds, contentId } = rawInput;
|
|
47
|
+
const { output_path, model, image_url, duration_seconds, contentId, aspect_ratio } = rawInput;
|
|
48
48
|
if (!rawInput.prompt)
|
|
49
49
|
return { output: 'Error: prompt is required', isError: true };
|
|
50
50
|
// Resolve image_url before sending. The gateway requires a URL (http(s)
|
|
@@ -156,6 +156,12 @@ function buildExecute(deps) {
|
|
|
156
156
|
prompt: chosenPrompt,
|
|
157
157
|
...(resolvedImageUrl ? { image_url: resolvedImageUrl } : {}),
|
|
158
158
|
...(duration_seconds ? { duration_seconds } : {}),
|
|
159
|
+
// aspect_ratio passes through to the gateway. Models that support it
|
|
160
|
+
// (newer Seedance / grok variants) honor it; models that ignore it
|
|
161
|
+
// produce their default size. If the gateway rejects an unknown
|
|
162
|
+
// value, the 400 body surfaces via 3.15.45 diagnostic so the agent
|
|
163
|
+
// can drop the param and retry.
|
|
164
|
+
...(aspect_ratio ? { aspect_ratio } : {}),
|
|
159
165
|
});
|
|
160
166
|
const headers = {
|
|
161
167
|
'Content-Type': 'application/json',
|
|
@@ -456,7 +462,15 @@ export function createVideoGenCapability(deps = {}) {
|
|
|
456
462
|
"xai/grok-imagine-video bills $0.05/s (8s default ≈ $0.42). Generation " +
|
|
457
463
|
"takes ~20–60s. ALWAYS confirm with the user before calling — videos " +
|
|
458
464
|
"are expensive and slow. Pass contentId to attach to a Content piece " +
|
|
459
|
-
"(budget is checked before paying; asset is recorded on success)."
|
|
465
|
+
"(budget is checked before paying; asset is recorded on success). " +
|
|
466
|
+
"PLATFORM TARGETING: when the user says they'll post to X / Twitter, " +
|
|
467
|
+
"set aspect_ratio: '16:9' AND plan a follow-up `ffmpeg -vf scale=1280:720` " +
|
|
468
|
+
"step — X rejects videos under 720p with 'aspect ratio too small'. " +
|
|
469
|
+
"TikTok / Reels / Shorts: aspect_ratio '9:16'. Instagram Square: '1:1'. " +
|
|
470
|
+
"MODERATION: bytedance/seedance-* refuses photorealistic human faces " +
|
|
471
|
+
"(`InputImageSensitiveContentDetected.PrivacyInformation`); when the " +
|
|
472
|
+
"seed image has a real-looking person, use xai/grok-imagine-video " +
|
|
473
|
+
"instead, or regenerate the keyframe in a more stylized style first.",
|
|
460
474
|
input_schema: {
|
|
461
475
|
type: 'object',
|
|
462
476
|
properties: {
|
|
@@ -471,6 +485,15 @@ export function createVideoGenCapability(deps = {}) {
|
|
|
471
485
|
},
|
|
472
486
|
image_url: { type: 'string', description: 'Optional seed image (image-to-video). Accepts http(s) URL, data: URI, or local file path — local paths get inlined as base64 data URIs automatically.' },
|
|
473
487
|
duration_seconds: { type: 'number', description: 'Duration billed for. Default depends on model (8s for grok-imagine-video).' },
|
|
488
|
+
aspect_ratio: {
|
|
489
|
+
type: 'string',
|
|
490
|
+
description: 'Optional aspect ratio hint passed to the model. Common values: ' +
|
|
491
|
+
'"16:9" (landscape — X/Twitter, YouTube, TikTok-landscape), ' +
|
|
492
|
+
'"9:16" (vertical — TikTok, Reels, Shorts), ' +
|
|
493
|
+
'"1:1" (square — Instagram feed). Models that don\'t support the ' +
|
|
494
|
+
'param ignore it; if the gateway 400s on an unknown value, the ' +
|
|
495
|
+
'error body surfaces — drop the param and retry.',
|
|
496
|
+
},
|
|
474
497
|
contentId: { type: 'string', description: 'Optional Content id to attach and budget against.' },
|
|
475
498
|
},
|
|
476
499
|
required: ['prompt'],
|
package/package.json
CHANGED