@neta-art/cohub-cli 10.6.0 → 10.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -404,6 +404,8 @@ cohub generate "Lip-sync to this spoken take" \
|
|
|
404
404
|
--image reference_image=https://example.com/portrait.png \
|
|
405
405
|
--audio reference_audio=https://example.com/speech.mp3
|
|
406
406
|
|
|
407
|
+
cohub generate -m song2 --text "prompt=upbeat synthwave" --text "lyrics=[Verse] Neon lights" --param seed=42
|
|
408
|
+
|
|
407
409
|
cohub generate "a calm lake" \
|
|
408
410
|
--model <model> \
|
|
409
411
|
--async
|
|
@@ -414,6 +416,7 @@ cohub tasks get <taskRunId> --json
|
|
|
414
416
|
Supported inputs:
|
|
415
417
|
|
|
416
418
|
```bash
|
|
419
|
+
--text <role>=<text>
|
|
417
420
|
--image <path-or-url>
|
|
418
421
|
--image first_frame=<path-or-url>
|
|
419
422
|
--image last_frame=<path-or-url>
|
|
@@ -424,7 +427,7 @@ Supported inputs:
|
|
|
424
427
|
--audio reference_audio=<path-or-url>
|
|
425
428
|
```
|
|
426
429
|
|
|
427
|
-
Role-qualified
|
|
430
|
+
Role-qualified values add `meta.role` to that content block. Text roles come from the model's text inputs (`cohub models show <model>`); repeat `--text <role>=<text>` for each text block. Repeat `--image reference_image=...` for multiple reference images. Seedance role-qualified media should use public URL inputs. Do not mix first/last frame roles with reference roles (`reference_image`, `reference_video`, `reference_audio`) in one request.
|
|
428
431
|
|
|
429
432
|
Pass generation parameters with `--param key=value` or `--parameters '<json>'`.
|
|
430
433
|
|
package/dist/board-kernel.js
CHANGED
|
@@ -27490,9 +27490,14 @@ function fitTextToWidth(text, value, width) {
|
|
|
27490
27490
|
}
|
|
27491
27491
|
//#endregion
|
|
27492
27492
|
//#region ../sdk/src/board/render/text-resolution.ts
|
|
27493
|
-
const MAX_BOARD_RESOLUTION =
|
|
27494
|
-
|
|
27495
|
-
|
|
27493
|
+
const MAX_BOARD_RESOLUTION = 3;
|
|
27494
|
+
const MAX_BOARD_PIXELS = 8e6;
|
|
27495
|
+
const MAX_TEXT_RESOLUTION = 6;
|
|
27496
|
+
function getBoardResolution(size, devicePixelRatio = globalThis.devicePixelRatio) {
|
|
27497
|
+
const density = Number.isFinite(devicePixelRatio) && devicePixelRatio > 0 ? devicePixelRatio : 1;
|
|
27498
|
+
const pixels = size ? size.width * size.height : 0;
|
|
27499
|
+
const budget = pixels > 0 ? Math.sqrt(MAX_BOARD_PIXELS / pixels) : MAX_BOARD_RESOLUTION;
|
|
27500
|
+
return Math.min(density, MAX_BOARD_RESOLUTION, budget);
|
|
27496
27501
|
}
|
|
27497
27502
|
/**
|
|
27498
27503
|
* Zoom buckets for text re-rasterisation. Pixi Text is a bitmap: at high zoom it
|
|
@@ -27518,12 +27523,12 @@ function boardCardDetailed(zoom) {
|
|
|
27518
27523
|
/** Effective text resolution for a given camera zoom. */
|
|
27519
27524
|
function textResolutionForZoom(zoom) {
|
|
27520
27525
|
const bucket = textZoomBucket(zoom);
|
|
27521
|
-
return Math.min(getBoardResolution() * Math.max(1, bucket),
|
|
27526
|
+
return Math.min(getBoardResolution() * Math.max(1, bucket), MAX_TEXT_RESOLUTION);
|
|
27522
27527
|
}
|
|
27523
27528
|
/** Update a Pixi text texture only when zoom crosses a resolution bucket. */
|
|
27524
27529
|
function syncTextResolution(text, state, zoom) {
|
|
27525
27530
|
const resolution = textResolutionForZoom(zoom);
|
|
27526
|
-
if (resolution === state.resolution) return;
|
|
27531
|
+
if (resolution === state.resolution && resolution === text.resolution) return;
|
|
27527
27532
|
text.resolution = resolution;
|
|
27528
27533
|
state.resolution = resolution;
|
|
27529
27534
|
}
|
|
@@ -28056,13 +28061,10 @@ function cardElevation(context, id) {
|
|
|
28056
28061
|
}
|
|
28057
28062
|
//#endregion
|
|
28058
28063
|
//#region ../sdk/src/board/render/renderers/base-card-renderer.ts
|
|
28059
|
-
const TEXT_OPTIONS = {
|
|
28060
|
-
resolution: getBoardResolution(),
|
|
28061
|
-
roundPixels: true
|
|
28062
|
-
};
|
|
28063
28064
|
function createLabel(text, style) {
|
|
28064
28065
|
return new Text({
|
|
28065
|
-
|
|
28066
|
+
resolution: getBoardResolution(),
|
|
28067
|
+
roundPixels: true,
|
|
28066
28068
|
text,
|
|
28067
28069
|
style: {
|
|
28068
28070
|
fontFamily: BOARD_FONT_STACK,
|
|
@@ -29495,6 +29497,7 @@ function sync$5(container, item, context) {
|
|
|
29495
29497
|
parts.id = item.id;
|
|
29496
29498
|
}
|
|
29497
29499
|
positionShell(parts.root, item);
|
|
29500
|
+
syncTextResolution(parts.label, parts.label, context.zoom);
|
|
29498
29501
|
const { width, height } = item.frame;
|
|
29499
29502
|
const resolution = Math.min(4, Math.max(.25, context.zoom * getBoardResolution()));
|
|
29500
29503
|
const texture = context.scripts?.frame(item, context.time, resolution) ?? null;
|
|
@@ -32,6 +32,20 @@ function parseValue(value) {
|
|
|
32
32
|
}
|
|
33
33
|
return value;
|
|
34
34
|
}
|
|
35
|
+
function parseTextInputs(values) {
|
|
36
|
+
return values.map((value) => {
|
|
37
|
+
const index = value.indexOf("=");
|
|
38
|
+
if (index < 0)
|
|
39
|
+
return error("Invalid --text", "Pass --text <role>=<text>.");
|
|
40
|
+
const role = value.slice(0, index).trim();
|
|
41
|
+
if (!role)
|
|
42
|
+
return error("Invalid --text", "Pass a role: --text <role>=<text>.");
|
|
43
|
+
const text = value.slice(index + 1);
|
|
44
|
+
if (!text)
|
|
45
|
+
return error("Invalid --text", 'Pass text after "=": --text <role>=<text>.');
|
|
46
|
+
return { role, text };
|
|
47
|
+
});
|
|
48
|
+
}
|
|
35
49
|
function parseParams(param, parameters) {
|
|
36
50
|
const result = parameters ? JSON.parse(parameters) : {};
|
|
37
51
|
for (const item of param ?? []) {
|
|
@@ -260,8 +274,9 @@ export function registerGenerations(program) {
|
|
|
260
274
|
program
|
|
261
275
|
.command("generate")
|
|
262
276
|
.description("Generate multimodal outputs")
|
|
263
|
-
.argument("
|
|
277
|
+
.argument("[prompt]", "Prompt text; optional when --text is given")
|
|
264
278
|
.requiredOption("-m, --model <model>", "Multimodal model ID from `cohub models ls --model-type multimodal`")
|
|
279
|
+
.option("--text <role=text>", "Text input with a role from the model's text inputs (`cohub models show <model>`); repeatable, split at the first =, text sent unchanged", collect, [])
|
|
265
280
|
.option("--image <path-or-url>", "Image input file path or URL; prefix with first_frame=, last_frame=, or reference_image= when needed; repeatable", collect, [])
|
|
266
281
|
.option("--video <path-or-url>", "Video input file path or URL; prefix with reference_video= when needed; repeatable", collect, [])
|
|
267
282
|
.option("--audio <path-or-url>", "Audio input file path or URL; prefix with reference_audio= when needed; repeatable", collect, [])
|
|
@@ -285,10 +300,15 @@ Examples:
|
|
|
285
300
|
cohub -s <space-id> generate "Smooth transition" -m seedance-2-0-fast --image first_frame=https://example.com/first.png --image last_frame=https://example.com/last.png
|
|
286
301
|
cohub -s <space-id> generate "Use these references" -m seedance-2-0-fast --image reference_image=https://example.com/a.png --image reference_image=https://example.com/b.png
|
|
287
302
|
cohub -s <space-id> generate "Lip-sync to the reference take" -m seedance-2-0-fast --image reference_image=https://example.com/portrait.png --audio reference_audio=https://example.com/speech.mp3
|
|
303
|
+
cohub -s <space-id> generate -m song2 --text "prompt=upbeat synthwave" --text "lyrics=[Verse] Neon lights" --param seed=42
|
|
288
304
|
cohub -s <space-id> generate "A calm lake" -m <model> --async
|
|
289
305
|
`)
|
|
290
306
|
.action(async (prompt, opts) => {
|
|
291
307
|
try {
|
|
308
|
+
const texts = parseTextInputs(opts.text);
|
|
309
|
+
if (prompt === undefined && texts.length === 0) {
|
|
310
|
+
return error("Missing prompt", "Pass a prompt or at least one --text <role>=<text>.");
|
|
311
|
+
}
|
|
292
312
|
const spaceId = await resolveSpace(program, { home: true });
|
|
293
313
|
const sessionId = envValue("COHUB_SESSION_ID");
|
|
294
314
|
const inputs = await Promise.all([
|
|
@@ -330,7 +350,8 @@ Examples:
|
|
|
330
350
|
return asset.publicUrl;
|
|
331
351
|
};
|
|
332
352
|
const content = [
|
|
333
|
-
{ type: "text", text: prompt },
|
|
353
|
+
...(prompt === undefined ? [] : [{ type: "text", text: prompt }]),
|
|
354
|
+
...texts.map(({ role, text }) => ({ type: "text", text, meta: { role } })),
|
|
334
355
|
...await Promise.all(inputs.map((input) => mediaContent(input, upload))),
|
|
335
356
|
];
|
|
336
357
|
const created = await client.generations.create({
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { homedir } from "node:os";
|
|
2
3
|
import { join } from "node:path";
|
|
4
|
+
import { JsonRpcProcess, record } from "../json-rpc.js";
|
|
3
5
|
import { copyNativeArchive } from "../native/archive.js";
|
|
4
6
|
import { projectChainedSession, projectionMeta, projectionToolId, rebindSessionId } from "./chained-projection.js";
|
|
7
|
+
import { array, harnessCommand, text } from "./process.js";
|
|
5
8
|
import { DEFAULT_MODEL, runStreamJson, streamJsonHelp, streamJsonModel } from "./stream-json.js";
|
|
6
9
|
const MAX_PROJECT_NAME = 200;
|
|
10
|
+
const INITIALIZE_TIMEOUT_MS = 30_000;
|
|
7
11
|
const dataRoot = () => process.env.CLAUDE_CONFIG_DIR?.trim() || join(homedir(), ".claude");
|
|
8
12
|
/**
|
|
9
13
|
* Claude Code names a project directory by replacing every non-alphanumeric UTF-16 unit; a longer
|
|
@@ -47,11 +51,66 @@ function withAssistantIds(projection) {
|
|
|
47
51
|
...entry, record: { ...entry.record, message: { id: `msg_${String(entry.record.uuid).replaceAll("-", "")}`, ...entry.record.message } },
|
|
48
52
|
}) };
|
|
49
53
|
}
|
|
54
|
+
/** Claude Code's own `/model` list (what the SDK's `supportedModels()` reads), gateway-discovered models included. */
|
|
55
|
+
async function claudeInitialize(executable, cwd) {
|
|
56
|
+
const args = ["-p", "--input-format", "stream-json", "--output-format", "stream-json", "--verbose", "--no-session-persistence", "--strict-mcp-config"];
|
|
57
|
+
const host = new JsonRpcProcess(...harnessCommand(executable, args, cwd), cwd, "stream");
|
|
58
|
+
const requestId = randomUUID();
|
|
59
|
+
let timer;
|
|
60
|
+
try {
|
|
61
|
+
return await new Promise((resolve, reject) => {
|
|
62
|
+
timer = setTimeout(() => reject(new Error("Claude Code initialize timed out")), INITIALIZE_TIMEOUT_MS);
|
|
63
|
+
host.onFailure(reject);
|
|
64
|
+
host.onEvent((event) => {
|
|
65
|
+
const response = record(event.response);
|
|
66
|
+
if (event.type !== "control_response" || response.request_id !== requestId)
|
|
67
|
+
return;
|
|
68
|
+
if (response.subtype === "success")
|
|
69
|
+
resolve(record(response.response));
|
|
70
|
+
else
|
|
71
|
+
reject(new Error(text(response.error) || "Claude Code initialize failed"));
|
|
72
|
+
});
|
|
73
|
+
host.write({ type: "control_request", request_id: requestId, request: { subtype: "initialize" } });
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
clearTimeout(timer);
|
|
78
|
+
await host.close();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/** Gateway models are served by `ANTHROPIC_BASE_URL`, so they carry its host as provider. */
|
|
82
|
+
function claudeModelCatalog(entries, baseUrl) {
|
|
83
|
+
let gateway = "gateway";
|
|
84
|
+
try {
|
|
85
|
+
if (baseUrl)
|
|
86
|
+
gateway = new URL(baseUrl).hostname.slice(0, 100) || gateway;
|
|
87
|
+
}
|
|
88
|
+
catch { /* Keep the generic name. */ }
|
|
89
|
+
const seen = new Set();
|
|
90
|
+
return entries.flatMap((value) => {
|
|
91
|
+
const model = record(value);
|
|
92
|
+
const id = text(model.value);
|
|
93
|
+
if (!id || id.length > 255 || seen.has(id))
|
|
94
|
+
return [];
|
|
95
|
+
seen.add(id);
|
|
96
|
+
const effortLevels = model.supportsEffort === true ? array(model.supportedEffortLevels).filter((level) => typeof level === "string") : [];
|
|
97
|
+
return [streamJsonModel(profile, id, (text(model.displayName) || id).slice(0, 255), {
|
|
98
|
+
reasoning: effortLevels.length > 0, effortLevels, isDefault: id === DEFAULT_MODEL,
|
|
99
|
+
provider: text(model.description).startsWith("From gateway") ? gateway : profile.provider,
|
|
100
|
+
})];
|
|
101
|
+
});
|
|
102
|
+
}
|
|
50
103
|
export const claudeHarness = {
|
|
51
104
|
id: "claude",
|
|
52
105
|
binary: profile.binary,
|
|
53
106
|
async models(executable, cwd) {
|
|
54
107
|
await streamJsonHelp(profile, executable, cwd);
|
|
108
|
+
try {
|
|
109
|
+
const models = claudeModelCatalog(array((await claudeInitialize(executable, cwd)).models), process.env.ANTHROPIC_BASE_URL);
|
|
110
|
+
if (models.some((model) => model.isDefault))
|
|
111
|
+
return models;
|
|
112
|
+
}
|
|
113
|
+
catch { /* A CLI without the initialize model list keeps the account aliases. */ }
|
|
55
114
|
// Aliases follow the signed-in account; Haiku takes no effort level.
|
|
56
115
|
return [
|
|
57
116
|
streamJsonModel(profile, DEFAULT_MODEL, "Default", { reasoning: true, isDefault: true }),
|
|
@@ -20,6 +20,8 @@ export declare function streamJsonHelp(profile: StreamJsonProfile, executable: s
|
|
|
20
20
|
export declare function streamJsonModel(profile: StreamJsonProfile, id: string, name: string, options: {
|
|
21
21
|
reasoning: boolean;
|
|
22
22
|
isDefault?: boolean;
|
|
23
|
+
provider?: string;
|
|
24
|
+
effortLevels?: readonly string[];
|
|
23
25
|
}): HarnessModel;
|
|
24
26
|
/**
|
|
25
27
|
* One Turn per process. Unattended execution never grants an approval: workspace edits follow
|
|
@@ -29,10 +29,11 @@ export async function streamJsonHelp(profile, executable, cwd) {
|
|
|
29
29
|
return stdout;
|
|
30
30
|
}
|
|
31
31
|
export function streamJsonModel(profile, id, name, options) {
|
|
32
|
+
const effortLevels = options.effortLevels ?? profile.effortLevels;
|
|
32
33
|
return {
|
|
33
|
-
harness: profile.harness, provider: profile.provider, id, name,
|
|
34
|
+
harness: profile.harness, provider: options.provider ?? profile.provider, id, name,
|
|
34
35
|
...(options.isDefault ? { isDefault: true } : {}),
|
|
35
|
-
...(options.reasoning ? { reasoning: true, thinkingLevelMap: Object.fromEntries(THINKING_LEVELS.map((level) => [level,
|
|
36
|
+
...(options.reasoning ? { reasoning: true, thinkingLevelMap: Object.fromEntries(THINKING_LEVELS.map((level) => [level, effortLevels.includes(level) ? level : null])) } : {}),
|
|
36
37
|
};
|
|
37
38
|
}
|
|
38
39
|
/** A session's file is found by id; the project directory naming is only the fast path. */
|
|
@@ -208,7 +209,7 @@ export async function runStreamJson(profile, executor, turn) {
|
|
|
208
209
|
signal.throwIfAborted();
|
|
209
210
|
const host = new JsonRpcProcess(...harnessCommand(executor.executables[profile.harness] || profile.binary, args, executor.cwd), executor.cwd, "stream", turnEnvironment(input));
|
|
210
211
|
watchHost(host, executor.diagnostics, context);
|
|
211
|
-
const translator = streamJsonTranslator(turn.emit, { provider: profile.provider, model: input.model && input.model !== DEFAULT_MODEL ? input.model : null });
|
|
212
|
+
const translator = streamJsonTranslator(turn.emit, { provider: input.provider || profile.provider, model: input.model && input.model !== DEFAULT_MODEL ? input.model : null });
|
|
212
213
|
let message = { ordinal: 0, content: [] };
|
|
213
214
|
let begun = false;
|
|
214
215
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neta-art/cohub-cli",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.7.0",
|
|
4
4
|
"description": "CLI for Cohub — spaces, sessions, and agent collaboration.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"NOTICE"
|
|
19
19
|
],
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@neta-art/cohub": "11.5.
|
|
21
|
+
"@neta-art/cohub": "11.5.1",
|
|
22
22
|
"@neta-art/generation": "^0.1.39",
|
|
23
23
|
"commander": "^15.0.0",
|
|
24
24
|
"sharp": "^0.35.4",
|