@neta-art/cohub-cli 10.8.2 → 10.10.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 +18 -4
- package/dist/board-export-resources.d.ts +3 -1
- package/dist/board-export-resources.js +13 -2
- package/dist/board-export.d.ts +23 -8
- package/dist/board-export.js +104 -53
- package/dist/board-fonts.d.ts +5 -1
- package/dist/board-fonts.js +8 -8
- package/dist/board-kernel.d.ts +2 -2
- package/dist/board-kernel.js +6088 -470
- package/dist/board-media.d.ts +4 -0
- package/dist/board-media.js +63 -0
- package/dist/commands/boards/authoring.js +3 -1
- package/dist/commands/boards.js +16 -10
- package/dist/commands/feedback.d.ts +2 -0
- package/dist/commands/feedback.js +129 -0
- package/dist/fonts/figtree-latin-wght-normal.woff2 +0 -0
- package/dist/fonts/geist-latin-wght-normal.woff2 +0 -0
- package/dist/fonts/geist-mono-latin-wght-normal.woff2 +0 -0
- package/dist/index.js +3 -0
- package/dist/runtime/native/attach.js +21 -12
- package/dist/runtime/native/transcript.js +19 -4
- package/dist/runtime/supervisor.js +13 -1
- package/dist/space-files/operands.d.ts +1 -0
- package/dist/space-files/operands.js +1 -1
- package/package.json +6 -5
- package/dist/fonts/geist-latin-400-normal.woff2 +0 -0
- package/dist/fonts/geist-latin-500-normal.woff2 +0 -0
- package/dist/fonts/geist-latin-600-normal.woff2 +0 -0
- package/dist/fonts/geist-latin-700-normal.woff2 +0 -0
- package/dist/fonts/geist-mono-latin-500-normal.woff2 +0 -0
- package/dist/fonts/geist-mono-latin-600-normal.woff2 +0 -0
- package/dist/fonts/geist-mono-latin-700-normal.woff2 +0 -0
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type BoardDocument, type BoardMediaFileResolver } from "@neta-art/cohub/board";
|
|
2
|
+
export declare function boardMediaFileResolver(spaceId: string): BoardMediaFileResolver;
|
|
3
|
+
export declare function settleBoardDocument(spaceId: string, document: BoardDocument): Promise<BoardDocument>;
|
|
4
|
+
export declare function settleBoardPatch<T extends Record<string, unknown>>(spaceId: string, patch: T, current: (ids: string[]) => Promise<Record<string, unknown> | null>): Promise<T>;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
import { diffMergePatch, mergePatch, parseBoardItem, settleBoardMediaSizes, } from "@neta-art/cohub/board";
|
|
5
|
+
import { createClient } from "./client.js";
|
|
6
|
+
import { WORKSPACE_ROOT } from "./space-files/operands.js";
|
|
7
|
+
const run = promisify(execFile);
|
|
8
|
+
const isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
9
|
+
const oriented = (width, height, turned) => width > 0 && height > 0 ? (turned ? { width: height, height: width } : { width, height }) : null;
|
|
10
|
+
async function localMediaSize(file, type, signal) {
|
|
11
|
+
try {
|
|
12
|
+
if (type === "image") {
|
|
13
|
+
const { default: sharp } = await import("sharp");
|
|
14
|
+
const { width = 0, height = 0, orientation = 1 } = await sharp(file).metadata();
|
|
15
|
+
return oriented(width, height, orientation >= 5);
|
|
16
|
+
}
|
|
17
|
+
const { stdout } = await run("ffprobe", [
|
|
18
|
+
"-v", "error", "-select_streams", "v:0",
|
|
19
|
+
"-show_entries", "stream=width,height:stream_side_data=rotation", "-of", "json", file,
|
|
20
|
+
], { signal, timeout: 5_000 });
|
|
21
|
+
const stream = JSON.parse(stdout).streams?.[0];
|
|
22
|
+
const rotation = stream?.side_data_list?.find((data) => data.rotation !== undefined)?.rotation ?? 0;
|
|
23
|
+
return oriented(stream?.width ?? 0, stream?.height ?? 0, Math.abs(rotation) % 180 === 90);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export function boardMediaFileResolver(spaceId) {
|
|
30
|
+
const local = process.env.COHUB_SPACE_ID === spaceId;
|
|
31
|
+
return async (path, type, signal) => {
|
|
32
|
+
const file = resolve(WORKSPACE_ROOT, path);
|
|
33
|
+
const size = local && file.startsWith(`${WORKSPACE_ROOT}/`) ? await localMediaSize(file, type, signal) : null;
|
|
34
|
+
return size ?? createClient().space(spaceId).files.resolveUrl(path, { purpose: "playback", signal }).catch(() => null);
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export async function settleBoardDocument(spaceId, document) {
|
|
38
|
+
const settled = await settleBoardMediaSizes(document.items, { resolveFile: boardMediaFileResolver(spaceId) });
|
|
39
|
+
return Object.keys(settled).length ? { ...document, items: { ...document.items, ...settled } } : document;
|
|
40
|
+
}
|
|
41
|
+
export async function settleBoardPatch(spaceId, patch, current) {
|
|
42
|
+
const items = isRecord(patch.items) ? patch.items : {};
|
|
43
|
+
const created = {};
|
|
44
|
+
for (const [id, value] of Object.entries(items)) {
|
|
45
|
+
if (!isRecord(value) || (value.type !== "image" && value.type !== "video"))
|
|
46
|
+
continue;
|
|
47
|
+
const parsed = parseBoardItem(value, `items.${id}`);
|
|
48
|
+
if (parsed.ok)
|
|
49
|
+
created[id] = parsed.item;
|
|
50
|
+
}
|
|
51
|
+
const existing = Object.keys(created).length ? await current(Object.keys(created)) : null;
|
|
52
|
+
if (!existing)
|
|
53
|
+
return patch;
|
|
54
|
+
for (const [id, item] of Object.entries(existing)) {
|
|
55
|
+
if (isRecord(item) && item.type === created[id]?.type)
|
|
56
|
+
delete created[id];
|
|
57
|
+
}
|
|
58
|
+
const settled = await settleBoardMediaSizes(created, { resolveFile: boardMediaFileResolver(spaceId) });
|
|
59
|
+
if (!Object.keys(settled).length)
|
|
60
|
+
return patch;
|
|
61
|
+
const adopted = Object.entries(settled).map(([id, item]) => [id, mergePatch(items[id], diffMergePatch(created[id], item))]);
|
|
62
|
+
return { ...patch, items: { ...items, ...Object.fromEntries(adopted) } };
|
|
63
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readBoardJsonObject } from "../../board-command-support.js";
|
|
2
2
|
import { loadBoardDocument, resolveBundledFonts } from "../../board-export.js";
|
|
3
|
+
import { settleBoardDocument } from "../../board-media.js";
|
|
3
4
|
import { boardPathTrack, boardSequencePatch, buildBoardScene, createBoardAnimationSampler, createNodeBoardRenderer, inspectBoardItems, } from "../../board-kernel.js";
|
|
4
5
|
import { parseBoardTimes } from "../../board-time.js";
|
|
5
6
|
import { handleHttp, json as outJson } from "../../output.js";
|
|
@@ -19,7 +20,8 @@ Checks native layout only, not camera cropping or content drawn inside scripts.
|
|
|
19
20
|
Example: cohub boards inspect lesson.board --animation lecture --at 0:20s:1s --json`)
|
|
20
21
|
.action(async (target, options) => {
|
|
21
22
|
try {
|
|
22
|
-
const
|
|
23
|
+
const spaceId = await resolveSpace(boards);
|
|
24
|
+
const document = await settleBoardDocument(spaceId, (await loadBoardDocument(spaceId, target)).document);
|
|
23
25
|
const times = parseBoardTimes(options.at, "--at", 100);
|
|
24
26
|
const sampler = options.animation ? createBoardAnimationSampler(document, options.animation) : null;
|
|
25
27
|
const renderer = await createNodeBoardRenderer({ fonts: resolveBundledFonts() });
|
package/dist/commands/boards.js
CHANGED
|
@@ -2,9 +2,10 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { mkdir } from "node:fs/promises";
|
|
4
4
|
import { dirname } from "node:path";
|
|
5
|
-
import { BOARD_PRESET_NAMES, BOARD_SCHEMA_TARGETS, BOARD_SCRIPT_API, boardJsonSchema, boardPresetTracks, boardSchemaOverview, isBoardPresetName, listBoardPresets, } from "@neta-art/cohub/board";
|
|
5
|
+
import { BOARD_PRESET_NAMES, BOARD_SCHEMA_TARGETS, BOARD_SCRIPT_API, BOARD_THEME_IDS, boardJsonSchema, boardPresetTracks, boardSchemaOverview, isBoardPresetName, listBoardPresets, } from "@neta-art/cohub/board";
|
|
6
6
|
import { BOARD_CREATE_INPUT_MAX_BYTES, BOARD_TRANSACTION_INPUT_MAX_BYTES, parseBoardJsonObject, readBoardJsonObject, resolveBoardId, writeBoardOutput, } from "../board-command-support.js";
|
|
7
7
|
import { BOARD_EXPORT_IMAGE_MEMORY_MIB } from "../board-export-resources.js";
|
|
8
|
+
import { settleBoardPatch } from "../board-media.js";
|
|
8
9
|
import { formatBoardTime, parseBoardTime, parseBoardTimes } from "../board-time.js";
|
|
9
10
|
import { createClient, createRealtimeClient } from "../client.js";
|
|
10
11
|
import { error, handleHttp, json as outJson, jsonRequested, ok, table } from "../output.js";
|
|
@@ -52,8 +53,8 @@ function list(value) {
|
|
|
52
53
|
const entries = value?.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
53
54
|
return entries?.length ? entries : undefined;
|
|
54
55
|
}
|
|
55
|
-
async function boardClient(boards, target) {
|
|
56
|
-
|
|
56
|
+
async function boardClient(boards, target, spaceId) {
|
|
57
|
+
spaceId ??= await resolveSpace(boards);
|
|
57
58
|
return createClient().space(spaceId).board(await resolveBoardId(spaceId, target));
|
|
58
59
|
}
|
|
59
60
|
/** A patch from an inline argument, a file, or stdin (`-`). */
|
|
@@ -158,7 +159,7 @@ function registerExportCommand(boards) {
|
|
|
158
159
|
.option("--rect <rect>", "World rect as x,y,width,height")
|
|
159
160
|
.option("--scale <factor>", "Output pixels per board unit", "2")
|
|
160
161
|
.option("--padding <units>", "Padding around the content in board units")
|
|
161
|
-
.option("--theme <
|
|
162
|
+
.option("--theme <name>", `Cohub theme: ${BOARD_THEME_IDS.join(", ")}`, "dark")
|
|
162
163
|
.option("--paper <mode>", "Output paper: paper or transparent", "paper")
|
|
163
164
|
.option("--format <format>", `Override the format (${BOARD_EXPORT_FORMATS.join(", ")}, ${BOARD_VIDEO_FORMATS.join(", ")})`)
|
|
164
165
|
.option("--quality <q>", "JPEG/WebP quality from 0 to 1", "0.92")
|
|
@@ -230,7 +231,7 @@ Examples:
|
|
|
230
231
|
...(options.animation ? { animation: options.animation } : {}),
|
|
231
232
|
scale: parseNumber(options.scale ?? "2", "--scale", { min: 0.01, max: 16 }),
|
|
232
233
|
...(options.padding === undefined ? {} : { padding: parseNumber(options.padding, "--padding", { min: 0 }) }),
|
|
233
|
-
|
|
234
|
+
theme: parseChoice(options.theme, "--theme", BOARD_THEME_IDS),
|
|
234
235
|
background: parseChoice(options.paper, "--paper", ["paper", "transparent"]),
|
|
235
236
|
format: videoFormat,
|
|
236
237
|
fps,
|
|
@@ -271,7 +272,7 @@ Examples:
|
|
|
271
272
|
...(options.animation ? { animation: options.animation } : {}),
|
|
272
273
|
scale: parseNumber(options.scale ?? "2", "--scale", { min: 0.01, max: 16 }),
|
|
273
274
|
...(options.padding === undefined ? {} : { padding: parseNumber(options.padding, "--padding", { min: 0 }) }),
|
|
274
|
-
|
|
275
|
+
theme: parseChoice(options.theme, "--theme", BOARD_THEME_IDS),
|
|
275
276
|
background: parseChoice(options.paper, "--paper", ["paper", "transparent"]),
|
|
276
277
|
format: parseExportFormat(options, out),
|
|
277
278
|
quality: parseNumber(options.quality ?? "0.92", "--quality", { min: 0, max: 1 }),
|
|
@@ -327,14 +328,16 @@ Times are milliseconds; 500ms, 12.5s and 2m also work on the command line.`);
|
|
|
327
328
|
cohub boards create lesson.board -i lesson.json`)
|
|
328
329
|
.action(async (path, options) => {
|
|
329
330
|
try {
|
|
330
|
-
const
|
|
331
|
+
const spaceId = await resolveSpace(boards);
|
|
332
|
+
const read = options.input ? await readBoardJsonObject(options.input, BOARD_CREATE_INPUT_MAX_BYTES) : undefined;
|
|
333
|
+
const document = read && await settleBoardPatch(spaceId, read, async () => ({}));
|
|
331
334
|
const input = {
|
|
332
335
|
path,
|
|
333
336
|
mutationId: options.mutationId ?? randomUUID(),
|
|
334
337
|
...(options.title ? { title: options.title } : {}),
|
|
335
338
|
...(document ? { document: document } : {}),
|
|
336
339
|
};
|
|
337
|
-
const result = await createClient().space(
|
|
340
|
+
const result = await createClient().space(spaceId).boards.create(input);
|
|
338
341
|
if (jsonRequested(options))
|
|
339
342
|
return outJson({ path, id: result.id, title: result.title, version: result.version });
|
|
340
343
|
ok(`Created ${path}`);
|
|
@@ -411,8 +414,11 @@ Times are milliseconds; 500ms, 12.5s and 2m also work on the command line.`);
|
|
|
411
414
|
cohub boards preset rise --targets a,b --animation intro | cohub boards apply plan.board -i -`)
|
|
412
415
|
.action(async (target, inline, options) => {
|
|
413
416
|
try {
|
|
414
|
-
const
|
|
415
|
-
const
|
|
417
|
+
const spaceId = await resolveSpace(boards);
|
|
418
|
+
const board = await boardClient(boards, target, spaceId);
|
|
419
|
+
const read = await readPatch(inline, options.input, BOARD_TRANSACTION_INPUT_MAX_BYTES);
|
|
420
|
+
const patch = await settleBoardPatch(spaceId, read, async (items) => options.replace ? {} : board.get({ only: ["items"], items }).then((result) => result.items ?? {}, () => null));
|
|
421
|
+
const result = await board.apply(patch, {
|
|
416
422
|
...(options.replace ? { replace: true } : {}),
|
|
417
423
|
...(options.cascade ? { cascade: true } : {}),
|
|
418
424
|
...(options.dryRun ? { dryRun: true } : {}),
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { openAsBlob } from "node:fs";
|
|
2
|
+
import { stat } from "node:fs/promises";
|
|
3
|
+
import { arch, platform, release } from "node:os";
|
|
4
|
+
import { basename, resolve } from "node:path";
|
|
5
|
+
import { mimeTypeForPath, readRequestSourceFromEnv, resolveApiBaseUrl, resolveCohubEnvironment } from "@neta-art/cohub";
|
|
6
|
+
import { authSource } from "../auth.js";
|
|
7
|
+
import { createClient } from "../client.js";
|
|
8
|
+
import { collectOption, readPromptContent } from "../command-input.js";
|
|
9
|
+
import { describeError } from "../network-error.js";
|
|
10
|
+
import { error, json as outJson, jsonRequested, spinner } from "../output.js";
|
|
11
|
+
import { readRuntimeDiagnosticEvents } from "../runtime/diagnostics.js";
|
|
12
|
+
import { resolveRuntimeTarget } from "../runtime/launch.js";
|
|
13
|
+
import { runtimeStateRoot } from "../runtime/native/config.js";
|
|
14
|
+
import { explicitSpace } from "../space.js";
|
|
15
|
+
// Shared with the web feedback dialog; cfeed enforces attachment limits server-side.
|
|
16
|
+
const CFEED_ENDPOINT = "https://cfeed.neta.art/api/v1/feedback";
|
|
17
|
+
const CFEED_APP = "cohub";
|
|
18
|
+
const RUNTIME_LOG_MAX_BYTES = 5 * 1024 * 1024;
|
|
19
|
+
const RUNTIME_LOG_EVENTS = 2_000;
|
|
20
|
+
const USER_LOOKUP_TIMEOUT_MS = 3_000;
|
|
21
|
+
const SUBMIT_TIMEOUT_MS = 120_000;
|
|
22
|
+
async function readAttachment(path) {
|
|
23
|
+
const absolute = resolve(path);
|
|
24
|
+
const info = await stat(absolute).catch(() => null);
|
|
25
|
+
if (!info?.isFile())
|
|
26
|
+
return error(`Cannot attach ${path}`, "Pass a path to an existing file");
|
|
27
|
+
const type = mimeTypeForPath(absolute) ?? "application/octet-stream";
|
|
28
|
+
return new File([await openAsBlob(absolute, { type })], basename(absolute), { type });
|
|
29
|
+
}
|
|
30
|
+
async function runtimeLogFile(program, now) {
|
|
31
|
+
const spaceId = await resolveRuntimeTarget(program).catch((cause) => error("Cannot attach Runtime logs", describeError(cause)));
|
|
32
|
+
let events = await readRuntimeDiagnosticEvents(runtimeStateRoot(spaceId), { limit: RUNTIME_LOG_EVENTS });
|
|
33
|
+
let dropped = 0;
|
|
34
|
+
let text = JSON.stringify({ spaceId, dropped, events });
|
|
35
|
+
// Keep the newest events when the log exceeds the budget.
|
|
36
|
+
while (Buffer.byteLength(text) > RUNTIME_LOG_MAX_BYTES && events.length > 0) {
|
|
37
|
+
const count = Math.ceil(events.length / 2);
|
|
38
|
+
events = events.slice(count);
|
|
39
|
+
dropped += count;
|
|
40
|
+
text = JSON.stringify({ spaceId, dropped, events });
|
|
41
|
+
}
|
|
42
|
+
const stamp = now.toISOString().replace(/[-:]/g, "").replace(/\..+$/, "");
|
|
43
|
+
return new File([text], `cohub-runtime-${stamp}.json`, { type: "application/json" });
|
|
44
|
+
}
|
|
45
|
+
async function currentUserUuid() {
|
|
46
|
+
if (!authSource())
|
|
47
|
+
return null;
|
|
48
|
+
let timer;
|
|
49
|
+
const timeout = new Promise((done) => { timer = setTimeout(() => done(null), USER_LOOKUP_TIMEOUT_MS); });
|
|
50
|
+
const lookup = createClient().user.getMe().then((user) => user.uuid ?? null, () => null);
|
|
51
|
+
try {
|
|
52
|
+
return await Promise.race([lookup, timeout]);
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
clearTimeout(timer);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async function submitFeedback(content, extra, files) {
|
|
59
|
+
const body = new FormData();
|
|
60
|
+
body.set("app", CFEED_APP);
|
|
61
|
+
body.set("content", content);
|
|
62
|
+
body.set("extra", JSON.stringify(extra));
|
|
63
|
+
for (const file of files)
|
|
64
|
+
body.append("files", file, file.name);
|
|
65
|
+
const response = await fetch(CFEED_ENDPOINT, { method: "POST", body, signal: AbortSignal.timeout(SUBMIT_TIMEOUT_MS) });
|
|
66
|
+
const payload = (await response.json().catch(() => null));
|
|
67
|
+
if (!response.ok)
|
|
68
|
+
throw new Error(payload?.error?.message ?? `${response.status} ${response.statusText}`);
|
|
69
|
+
if (!payload)
|
|
70
|
+
throw new Error("Feedback service returned an empty response");
|
|
71
|
+
return payload;
|
|
72
|
+
}
|
|
73
|
+
export function registerFeedback(program) {
|
|
74
|
+
program
|
|
75
|
+
.command("feedback [message...]")
|
|
76
|
+
.description("Send feedback or a bug report to the Cohub team")
|
|
77
|
+
.option("-f, --file <path>", "Attach a file; repeatable", collectOption, [])
|
|
78
|
+
.option("--runtime-logs", "Attach recent local Runtime diagnostics for the target Space")
|
|
79
|
+
.option("--json", "Output as JSON")
|
|
80
|
+
.addHelpText("after", `
|
|
81
|
+
The message comes from arguments or stdin. CLI version, OS, environment, account,
|
|
82
|
+
and COHUB_* Space/Chat context are attached automatically; your working directory is not.
|
|
83
|
+
|
|
84
|
+
Examples:
|
|
85
|
+
$ cohub feedback "runtime up hangs after sign-in"
|
|
86
|
+
$ cohub feedback "Board export is blurry" -f board.png -f export.log
|
|
87
|
+
$ cohub feedback --runtime-logs "Codex chats stopped syncing"
|
|
88
|
+
$ cat report.md | cohub feedback`)
|
|
89
|
+
.action(async (words, opts) => {
|
|
90
|
+
const asJson = jsonRequested(opts);
|
|
91
|
+
const content = await readPromptContent(words);
|
|
92
|
+
const now = new Date();
|
|
93
|
+
const [files, userUuid] = await Promise.all([
|
|
94
|
+
Promise.all([
|
|
95
|
+
...opts.file.map(readAttachment),
|
|
96
|
+
...(opts.runtimeLogs ? [runtimeLogFile(program, now)] : []),
|
|
97
|
+
]),
|
|
98
|
+
currentUserUuid(),
|
|
99
|
+
]);
|
|
100
|
+
const extra = {
|
|
101
|
+
user_uuid: userUuid,
|
|
102
|
+
cli_version: program.version() ?? null,
|
|
103
|
+
node: process.versions.node,
|
|
104
|
+
os: `${platform()} ${release()}`,
|
|
105
|
+
arch: arch(),
|
|
106
|
+
env: resolveCohubEnvironment(),
|
|
107
|
+
api_base_url: resolveApiBaseUrl(),
|
|
108
|
+
space: explicitSpace(program),
|
|
109
|
+
source: readRequestSourceFromEnv(process.env, { via: "cli" }),
|
|
110
|
+
locale: Intl.DateTimeFormat().resolvedOptions().locale,
|
|
111
|
+
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
112
|
+
tty: Boolean(process.stdout.isTTY),
|
|
113
|
+
has_runtime_logs: Boolean(opts.runtimeLogs),
|
|
114
|
+
};
|
|
115
|
+
const sp = asJson ? null : spinner();
|
|
116
|
+
sp?.start("Sending feedback");
|
|
117
|
+
try {
|
|
118
|
+
const result = await submitFeedback(content, extra, files);
|
|
119
|
+
if (asJson)
|
|
120
|
+
return outJson(result);
|
|
121
|
+
sp?.stop(`Feedback sent · ${result.id}`);
|
|
122
|
+
}
|
|
123
|
+
catch (cause) {
|
|
124
|
+
if (sp)
|
|
125
|
+
process.stderr.write("\n");
|
|
126
|
+
error("Couldn't send feedback", describeError(cause));
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import { registerAuth } from "./commands/auth.js";
|
|
|
6
6
|
import { registerBoards } from "./commands/boards.js";
|
|
7
7
|
import { registerChannels } from "./commands/channels.js";
|
|
8
8
|
import { registerCronJobs } from "./commands/cron-jobs.js";
|
|
9
|
+
import { registerFeedback } from "./commands/feedback.js";
|
|
9
10
|
import { registerGenerations } from "./commands/generations.js";
|
|
10
11
|
import { registerMe } from "./commands/me.js";
|
|
11
12
|
import { registerModels } from "./commands/models.js";
|
|
@@ -71,6 +72,7 @@ Common commands:
|
|
|
71
72
|
cohub models ls
|
|
72
73
|
cohub models ls --model-type multimodal
|
|
73
74
|
cohub generate "A calm lake at sunrise" --model <model> --output lake.png
|
|
75
|
+
cohub feedback "Describe the problem" -f screenshot.png
|
|
74
76
|
|
|
75
77
|
Target space:
|
|
76
78
|
-s <space>, then COHUB_SPACE_ID, then the current directory Runtime binding.
|
|
@@ -108,6 +110,7 @@ registerTasks(program);
|
|
|
108
110
|
registerCronJobs(program);
|
|
109
111
|
registerApps(program);
|
|
110
112
|
registerRooms(program);
|
|
113
|
+
registerFeedback(program);
|
|
111
114
|
registerDesktop(program);
|
|
112
115
|
registerLegacyUi(program);
|
|
113
116
|
const argv = process.argv.slice(2);
|
|
@@ -29,22 +29,21 @@ export async function ensureNativeSync(input, log = (line) => process.stderr.wri
|
|
|
29
29
|
return { enabled: false, harnesses: [], codexShared: false, importHistory: false, note: "unsupported" };
|
|
30
30
|
const stateRoot = runtimeStateRoot(input.spaceId);
|
|
31
31
|
const previous = await readNativeConfig(stateRoot, input.identity).catch(() => null);
|
|
32
|
-
|
|
33
|
-
await maintain(previous, log, input.executables?.pi);
|
|
34
|
-
return { enabled: true, harnesses: previous.harnesses, codexShared: previous.codexShared, importHistory: false };
|
|
35
|
-
}
|
|
32
|
+
const alreadySynced = previous && nativeSyncSatisfied(previous, input);
|
|
36
33
|
const interactive = process.stdin.isTTY === true;
|
|
37
34
|
const confirm = async (question) => input.yes ? true : interactive ? await ask(question) : false;
|
|
38
|
-
if (!
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
35
|
+
if (!alreadySynced) {
|
|
36
|
+
if (!input.yes && !interactive) {
|
|
37
|
+
log("Native sync off · rerun interactively or with --yes to enable\n");
|
|
38
|
+
return { enabled: false, harnesses: input.harnesses, codexShared: false, importHistory: false, note: "non-interactive" };
|
|
39
|
+
}
|
|
40
|
+
const names = input.harnesses.map((harness) => LOCAL_HARNESS_SPECS[harness].label).join(" and ");
|
|
41
|
+
if (!await confirm(`Sync ${names} chats in this folder to this Space so they continue on the web? Their history may contain secrets. [Y/n] `)) {
|
|
42
|
+
return { enabled: false, harnesses: input.harnesses, codexShared: false, importHistory: false, note: "declined" };
|
|
43
|
+
}
|
|
45
44
|
}
|
|
46
45
|
let codexShared = previous?.codexShared ?? false;
|
|
47
|
-
if (input.harnesses.includes("codex") && !codexShared) {
|
|
46
|
+
if (input.harnesses.includes("codex") && !codexShared && (input.yes || interactive)) {
|
|
48
47
|
const version = await harnessVersion(input.executables?.codex || "codex");
|
|
49
48
|
if (!sharedServerAvailable) {
|
|
50
49
|
log("Shared Codex sessions are unavailable on Windows; terminal Codex chats sync read-only\n");
|
|
@@ -56,6 +55,16 @@ export async function ensureNativeSync(input, log = (line) => process.stderr.wri
|
|
|
56
55
|
codexShared = await confirm("Let terminal Codex and the web drive the same live sessions? Cohub starts Codex's shared app-server, which keeps running in the background. [Y/n] ");
|
|
57
56
|
}
|
|
58
57
|
}
|
|
58
|
+
if (previous && alreadySynced) {
|
|
59
|
+
if (codexShared !== previous.codexShared) {
|
|
60
|
+
await writeNativeConfig(stateRoot, { ...previous, codexShared }).catch((error) => {
|
|
61
|
+
log(`Shared Codex sessions unavailable; Runtime continues without them — ${serializeDiagnosticError(error).message}\n`);
|
|
62
|
+
codexShared = previous.codexShared;
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
await maintain(previous, log, input.executables?.pi);
|
|
66
|
+
return { enabled: true, harnesses: previous.harnesses, codexShared, importHistory: false };
|
|
67
|
+
}
|
|
59
68
|
const harnesses = [...new Set([...(previous?.root === input.root ? previous.harnesses : []), ...input.harnesses])];
|
|
60
69
|
const config = {
|
|
61
70
|
version: 2, identity: input.identity, spaceId: input.spaceId, root: input.root, harnesses,
|
|
@@ -158,11 +158,22 @@ async function scanCodexRollouts(signal) {
|
|
|
158
158
|
codexRolloutIndexCache = { at: Date.now(), home, index };
|
|
159
159
|
return index;
|
|
160
160
|
}
|
|
161
|
+
async function* compressedCodexRolloutBytes(path, signal) {
|
|
162
|
+
const decoder = createZstdDecompress();
|
|
163
|
+
// pipeline keeps error listeners attached, so stopping at a header or prefix cannot crash with an unhandled decoder error.
|
|
164
|
+
const pumping = pipeline(createReadStream(path), decoder, { signal }).catch(() => { });
|
|
165
|
+
try {
|
|
166
|
+
yield* decoder;
|
|
167
|
+
}
|
|
168
|
+
finally {
|
|
169
|
+
decoder.destroy();
|
|
170
|
+
await pumping;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
161
173
|
/** Raw JSONL bytes of a rollout, transparently decompressing `.zst` and honoring a lineage cutoff. */
|
|
162
174
|
export async function* codexRolloutBytes(path, endBytes, signal) {
|
|
163
|
-
// compose (not pipe) so a source error propagates and early exit destroys both streams.
|
|
164
175
|
const stream = path.endsWith(".zst")
|
|
165
|
-
?
|
|
176
|
+
? compressedCodexRolloutBytes(path, signal)
|
|
166
177
|
: createReadStream(path, endBytes != null ? { end: endBytes - 1, signal } : { signal });
|
|
167
178
|
let total = 0, last = -1;
|
|
168
179
|
for await (const chunk of stream) {
|
|
@@ -522,9 +533,10 @@ function parseCodexLines(lines, segment) {
|
|
|
522
533
|
calls.get(id)?.content.push({ type: "tool_result", tool_use_id: id, content: typeof payload.output === "string" ? payload.output : JSON.stringify(payload.output ?? null) });
|
|
523
534
|
}
|
|
524
535
|
}
|
|
536
|
+
// Cohub tags a Turn it starts with its own cloud Turn id (`client_id`), so the file itself records
|
|
537
|
+
// which Turns already belong to a cloud Turn and must not be ingested twice. Older Codex writes it
|
|
538
|
+
// on `item_completed`'s UserMessage, newer Codex on `user_message`.
|
|
525
539
|
if (entry.type === "event_msg" && payload.type === "item_completed") {
|
|
526
|
-
// Cohub tags a Turn it starts with its own cloud Turn id, so the file itself records which
|
|
527
|
-
// Turns already belong to a cloud Turn (and must not be ingested twice).
|
|
528
540
|
const item = record(payload.item);
|
|
529
541
|
if (item.type === "UserMessage") {
|
|
530
542
|
const clientId = text(item.client_id);
|
|
@@ -540,6 +552,9 @@ function parseCodexLines(lines, segment) {
|
|
|
540
552
|
}
|
|
541
553
|
}
|
|
542
554
|
if (entry.type === "event_msg" && payload.type === "user_message") {
|
|
555
|
+
const clientId = text(payload.client_id);
|
|
556
|
+
if (clientId)
|
|
557
|
+
current.cloudTurnId = clientId;
|
|
543
558
|
if (!userFromResponse)
|
|
544
559
|
current.userContent = [{ type: "text", text: text(payload.message) }];
|
|
545
560
|
userFromResponse = true;
|
|
@@ -152,11 +152,23 @@ export async function runRuntime(config, onState, externalSignal, onDiagnostic)
|
|
|
152
152
|
});
|
|
153
153
|
bridge.stdin?.on("error", () => undefined);
|
|
154
154
|
const closed = new Promise((resolve) => {
|
|
155
|
+
let drainTimer;
|
|
155
156
|
bridge.once("error", (error) => diagnostics.log("error", "sandboxd.process_error", { error: serializeDiagnosticError(error) }));
|
|
156
|
-
bridge.once("
|
|
157
|
+
bridge.once("exit", (code, exitSignal) => {
|
|
157
158
|
childStopped = true;
|
|
158
159
|
if (!signal.aborted)
|
|
159
160
|
diagnostics.log("warn", "sandboxd.process_exit", { code, signal: exitSignal });
|
|
161
|
+
// Descendants may still hold inherited pipes. After draining buffered
|
|
162
|
+
// output, close only our ends so supervision can continue.
|
|
163
|
+
drainTimer = setTimeout(() => {
|
|
164
|
+
diagnostics.log("warn", "sandboxd.stdio_drain_timeout");
|
|
165
|
+
for (const stream of bridge.stdio)
|
|
166
|
+
stream?.destroy();
|
|
167
|
+
}, 1000);
|
|
168
|
+
});
|
|
169
|
+
bridge.once("close", () => {
|
|
170
|
+
childStopped = true;
|
|
171
|
+
clearTimeout(drainTimer);
|
|
160
172
|
resolve();
|
|
161
173
|
});
|
|
162
174
|
});
|
|
@@ -11,6 +11,7 @@ export type FileOperand = SpaceOperand | LocalOperand;
|
|
|
11
11
|
export declare class InvalidFileOperandError extends Error {
|
|
12
12
|
name: string;
|
|
13
13
|
}
|
|
14
|
+
export declare const WORKSPACE_ROOT = "/workspace";
|
|
14
15
|
export declare function normalizeSpacePath(raw: string): string;
|
|
15
16
|
export declare function parseSpaceFileOperand(raw: string): SpaceOperand;
|
|
16
17
|
export declare function parseCopyOperand(raw: string, currentSpaceDeclared: boolean): FileOperand;
|
|
@@ -3,7 +3,7 @@ import { parseSpaceRef } from "@neta-art/cohub";
|
|
|
3
3
|
export class InvalidFileOperandError extends Error {
|
|
4
4
|
name = "InvalidFileOperandError";
|
|
5
5
|
}
|
|
6
|
-
const WORKSPACE_ROOT = "/workspace";
|
|
6
|
+
export const WORKSPACE_ROOT = "/workspace";
|
|
7
7
|
const isWorkspacePath = (path) => path === WORKSPACE_ROOT || path.startsWith(`${WORKSPACE_ROOT}/`);
|
|
8
8
|
export function normalizeSpacePath(raw) {
|
|
9
9
|
let path = raw.trim();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neta-art/cohub-cli",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.10.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": "12.
|
|
21
|
+
"@neta-art/cohub": "12.2.0",
|
|
22
22
|
"@neta-art/generation": "^0.1.40",
|
|
23
23
|
"commander": "^15.0.0",
|
|
24
24
|
"sharp": "^0.35.4",
|
|
@@ -28,8 +28,9 @@
|
|
|
28
28
|
"access": "public"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
|
-
"@fontsource/
|
|
32
|
-
"@fontsource/geist
|
|
31
|
+
"@fontsource-variable/figtree": "^5.3.0",
|
|
32
|
+
"@fontsource-variable/geist": "^5.3.0",
|
|
33
|
+
"@fontsource-variable/geist-mono": "^5.3.0",
|
|
33
34
|
"@types/node": "26.6.4",
|
|
34
35
|
"@types/ws": "^8.18.1",
|
|
35
36
|
"pixi.js": "^8.20.1",
|
|
@@ -37,7 +38,7 @@
|
|
|
37
38
|
"typescript": "^7.0.2"
|
|
38
39
|
},
|
|
39
40
|
"optionalDependencies": {
|
|
40
|
-
"@napi-rs/canvas": "^1.0.
|
|
41
|
+
"@napi-rs/canvas": "^1.0.13"
|
|
41
42
|
},
|
|
42
43
|
"scripts": {
|
|
43
44
|
"build": "tsc -p tsconfig.build.json && tsdown",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|