@neta-art/cohub-cli 8.4.1 → 9.0.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 +25 -2
- package/dist/board-export.d.ts +21 -36
- package/dist/board-export.js +163 -88
- package/dist/board-kernel.d.ts +2 -3
- package/dist/board-kernel.js +3428 -3770
- package/dist/board-time.d.ts +3 -0
- package/dist/board-time.js +28 -0
- package/dist/commands/apps.js +2 -2
- package/dist/commands/auth.js +2 -2
- package/dist/commands/boards/examples.d.ts +8 -2
- package/dist/commands/boards/examples.js +79 -411
- package/dist/commands/boards.d.ts +4 -9
- package/dist/commands/boards.js +346 -286
- package/dist/commands/desktop.d.ts +1 -5
- package/dist/commands/desktop.js +5 -16
- package/dist/commands/generations.js +1 -1
- package/dist/commands/me.js +1 -1
- package/dist/commands/public.js +3 -3
- package/dist/commands/references.js +3 -0
- package/dist/commands/referrals.js +2 -2
- package/dist/commands/run.js +3 -3
- package/dist/commands/runtime.d.ts +1 -1
- package/dist/commands/runtime.js +12 -8
- package/dist/commands/search.js +15 -7
- package/dist/commands/space-activity.js +3 -5
- package/dist/commands/space-displays.d.ts +2 -0
- package/dist/commands/space-displays.js +347 -0
- package/dist/commands/space-files.d.ts +5 -0
- package/dist/commands/space-files.js +215 -0
- package/dist/commands/space-invitations.js +3 -3
- package/dist/commands/spaces.d.ts +3 -11
- package/dist/commands/spaces.js +146 -75
- package/dist/index.js +7 -4
- package/dist/runtime/launch.d.ts +2 -0
- package/dist/runtime/launch.js +35 -8
- package/dist/runtime/presentation.d.ts +1 -0
- package/dist/runtime/presentation.js +3 -2
- package/dist/runtime/supervisor.d.ts +1 -0
- package/dist/runtime/supervisor.js +2 -1
- package/dist/space-files/limit.d.ts +2 -0
- package/dist/space-files/limit.js +16 -0
- package/dist/space-files/operands.d.ts +16 -0
- package/dist/space-files/operands.js +33 -0
- package/dist/space-files/transfer.d.ts +53 -0
- package/dist/space-files/transfer.js +296 -0
- package/dist/space-files/upload.d.ts +14 -0
- package/dist/space-files/upload.js +53 -0
- package/dist/space.d.ts +18 -16
- package/dist/space.js +86 -39
- package/dist/web.d.ts +1 -0
- package/dist/web.js +6 -0
- package/package.json +2 -2
- package/dist/commands/board-domain.d.ts +0 -2
- package/dist/commands/board-domain.js +0 -16
- package/dist/commands/boards/animation.d.ts +0 -2
- package/dist/commands/boards/animation.js +0 -171
- package/dist/commands/boards/appearance.d.ts +0 -2
- package/dist/commands/boards/appearance.js +0 -127
- package/dist/commands/boards/batch.d.ts +0 -4
- package/dist/commands/boards/batch.js +0 -62
- package/dist/commands/boards/connections.d.ts +0 -2
- package/dist/commands/boards/connections.js +0 -95
- package/dist/commands/boards/context.d.ts +0 -31
- package/dist/commands/boards/context.js +0 -71
- package/dist/commands/boards/items.d.ts +0 -2
- package/dist/commands/boards/items.js +0 -184
- package/dist/commands/boards/transactions.d.ts +0 -12
- package/dist/commands/boards/transactions.js +0 -69
package/dist/commands/spaces.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
1
|
import { readdir, stat } from "node:fs/promises";
|
|
3
2
|
import { basename, dirname, relative, resolve, sep } from "node:path";
|
|
4
|
-
import { resolveCohubEnvironment } from "@neta-art/cohub";
|
|
3
|
+
import { readSentTurns, readSessionTurnOrigin, resolveCohubEnvironment } from "@neta-art/cohub";
|
|
5
4
|
import { uploadAvatarAsset, uploadChatImageAsset } from "../avatar.js";
|
|
6
5
|
import { createClient } from "../client.js";
|
|
7
|
-
import { putLocalFile } from "../http-put.js";
|
|
8
6
|
import { table, json as outJson, jsonRequested, ok, error, handleHttp, formatEpochMs, truncateText } from "../output.js";
|
|
9
7
|
import { resolveSpace } from "../space.js";
|
|
8
|
+
import { uploadLocalFiles } from "../space-files/upload.js";
|
|
10
9
|
import { registerSpaceCommerce } from "./space-commerce.js";
|
|
10
|
+
import { registerSpaceFileTransfer } from "./space-files.js";
|
|
11
11
|
import { registerSpaceActivity } from "./space-activity.js";
|
|
12
|
+
import { registerSpaceDisplays } from "./space-displays.js";
|
|
12
13
|
import { registerSpaceInvitations } from "./space-invitations.js";
|
|
13
14
|
import { registerSpaceTurns } from "./space-turns.js";
|
|
14
15
|
import { registerSpaceWebhooks } from "./space-webhooks.js";
|
|
@@ -121,16 +122,8 @@ const walkUploadPath = async (localPath, root) => {
|
|
|
121
122
|
}
|
|
122
123
|
if (!info.isFile())
|
|
123
124
|
return [];
|
|
124
|
-
return [{
|
|
125
|
-
id: randomUploadEntryId(),
|
|
126
|
-
localPath,
|
|
127
|
-
relativePath,
|
|
128
|
-
name,
|
|
129
|
-
size: info.size,
|
|
130
|
-
mimeType: null,
|
|
131
|
-
}];
|
|
125
|
+
return [{ localPath, relativePath, size: info.size }];
|
|
132
126
|
};
|
|
133
|
-
const randomUploadEntryId = () => randomUUID();
|
|
134
127
|
// Upload semantics: a file keeps its name, and a directory contributes its
|
|
135
128
|
// contents directly under the target dir — like `aws s3 cp dir remote:path`
|
|
136
129
|
// or `rclone copy`.
|
|
@@ -158,45 +151,14 @@ export async function collectUploadFiles(paths) {
|
|
|
158
151
|
}
|
|
159
152
|
return files;
|
|
160
153
|
}
|
|
161
|
-
async function putUploadEntry(entry, uploadUrl, headers) {
|
|
162
|
-
await putLocalFile({
|
|
163
|
-
url: uploadUrl,
|
|
164
|
-
filePath: entry.localPath,
|
|
165
|
-
size: entry.size,
|
|
166
|
-
headers,
|
|
167
|
-
label: entry.relativePath,
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
154
|
async function uploadFiles(command, paths, opts) {
|
|
171
155
|
const spaceId = await resolveSpace(command);
|
|
172
156
|
const client = createClient();
|
|
173
157
|
try {
|
|
174
158
|
const files = await collectUploadFiles(paths);
|
|
175
|
-
const
|
|
176
|
-
destination: { kind: "workspace", targetDir: opts.dir },
|
|
177
|
-
entries: files.map((file) => ({
|
|
178
|
-
id: file.id,
|
|
179
|
-
name: file.name,
|
|
180
|
-
relativePath: file.relativePath,
|
|
181
|
-
size: file.size,
|
|
182
|
-
mimeType: file.mimeType,
|
|
183
|
-
})),
|
|
184
|
-
});
|
|
185
|
-
const byId = new Map(files.map((file) => [file.id, file]));
|
|
186
|
-
for (const entry of plan.entries) {
|
|
187
|
-
const file = byId.get(entry.id);
|
|
188
|
-
if (!file)
|
|
189
|
-
throw new Error(`Missing upload entry: ${entry.id}`);
|
|
190
|
-
// Remote downloadUrl entries have no uploadUrl; complete pulls them server-side.
|
|
191
|
-
if (!entry.uploadUrl)
|
|
192
|
-
continue;
|
|
193
|
-
await putUploadEntry(file, entry.uploadUrl, entry.headers);
|
|
194
|
-
}
|
|
195
|
-
const result = await client.space(spaceId).files.completeUpload(plan.uploadId, {
|
|
196
|
-
entries: plan.entries.map((entry) => ({ id: entry.id })),
|
|
197
|
-
});
|
|
159
|
+
const { uploaded, uploadIds } = await uploadLocalFiles(client.space(spaceId).files, files, { targetDir: opts.dir });
|
|
198
160
|
if (jsonRequested(opts))
|
|
199
|
-
return outJson({
|
|
161
|
+
return outJson({ ok: true, uploaded, uploadId: uploadIds[0], uploadIds, files: files.length });
|
|
200
162
|
ok(`Uploaded ${files.length} file${files.length === 1 ? "" : "s"}`);
|
|
201
163
|
}
|
|
202
164
|
catch (e) {
|
|
@@ -242,7 +204,7 @@ async function sendPrompt(command, words, opts) {
|
|
|
242
204
|
&& !new Set(["off", "minimal", "low", "medium", "high", "xhigh", "max"]).has(thinkingLevel)) {
|
|
243
205
|
return error("Invalid thinking level", "Use off|minimal|low|medium|high|xhigh|max");
|
|
244
206
|
}
|
|
245
|
-
const spaceId = await resolveSpace(command);
|
|
207
|
+
const spaceId = await resolveSpace(command, { home: true });
|
|
246
208
|
const client = createClient();
|
|
247
209
|
try {
|
|
248
210
|
const schedule = opts.delayMs
|
|
@@ -305,7 +267,7 @@ async function sendPrompt(command, words, opts) {
|
|
|
305
267
|
}
|
|
306
268
|
}
|
|
307
269
|
async function runCompletionCommand(command, words, opts) {
|
|
308
|
-
const spaceId = await resolveSpace(command);
|
|
270
|
+
const spaceId = await resolveSpace(command, { home: true });
|
|
309
271
|
const content = words.join(" ").trim();
|
|
310
272
|
if (!content && process.stdin.isTTY) {
|
|
311
273
|
return error("Message required", "Pass content args or pipe via stdin");
|
|
@@ -447,9 +409,66 @@ export function registerSpaceCreate(spacesCmd, dependencies = {}) {
|
|
|
447
409
|
}
|
|
448
410
|
});
|
|
449
411
|
}
|
|
412
|
+
function registerUserLabels(program) {
|
|
413
|
+
const labels = program.command("labels").description("Manage personal labels");
|
|
414
|
+
labels.command("ls").description("List personal labels").option("--json", "Output as JSON").action(async (opts) => {
|
|
415
|
+
const client = createClient();
|
|
416
|
+
try {
|
|
417
|
+
const result = await client.user.labels.list();
|
|
418
|
+
const flatten = (items, parent = "") => items.flatMap((item) => [
|
|
419
|
+
{ id: item.id, ref: parent ? `${parent}/${item.name}` : item.name, source: item.source, rank: item.rank },
|
|
420
|
+
...flatten(item.children ?? [], parent ? `${parent}/${item.name}` : item.name),
|
|
421
|
+
]);
|
|
422
|
+
const rows = flatten(result.labels);
|
|
423
|
+
if (jsonRequested(opts))
|
|
424
|
+
return outJson(rows);
|
|
425
|
+
table(rows, [{ key: "ref", label: "Label" }, { key: "source", label: "Source" }, { key: "rank", label: "Rank" }]);
|
|
426
|
+
}
|
|
427
|
+
catch (e) {
|
|
428
|
+
handleHttp(e);
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
labels.command("create <ref>").description("Create a personal label").action(async (ref) => {
|
|
432
|
+
try {
|
|
433
|
+
await createClient().user.labels.create(ref);
|
|
434
|
+
ok(`Created label: ${ref}`);
|
|
435
|
+
}
|
|
436
|
+
catch (e) {
|
|
437
|
+
handleHttp(e);
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
labels.command("rename <ref> <name>").description("Rename a personal label").action(async (ref, name) => {
|
|
441
|
+
try {
|
|
442
|
+
await createClient().user.labels.update(ref, { name });
|
|
443
|
+
ok(`Renamed label: ${ref}`);
|
|
444
|
+
}
|
|
445
|
+
catch (e) {
|
|
446
|
+
handleHttp(e);
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
labels.command("rm <ref>").description("Delete a personal label").action(async (ref) => {
|
|
450
|
+
try {
|
|
451
|
+
await createClient().user.labels.delete(ref);
|
|
452
|
+
ok(`Deleted label: ${ref}`);
|
|
453
|
+
}
|
|
454
|
+
catch (e) {
|
|
455
|
+
handleHttp(e);
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
labels.command("reorder <refs...>").description("Set personal label order").action(async (refs) => {
|
|
459
|
+
try {
|
|
460
|
+
await createClient().user.labels.reorder(refs);
|
|
461
|
+
ok(`Reordered ${refs.length} label(s)`);
|
|
462
|
+
}
|
|
463
|
+
catch (e) {
|
|
464
|
+
handleHttp(e);
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
}
|
|
450
468
|
export function registerSpaces(program) {
|
|
451
469
|
const spacesCmd = program.command("spaces").description("Space management");
|
|
452
470
|
registerSpaceInvitations(spacesCmd);
|
|
471
|
+
registerUserLabels(program);
|
|
453
472
|
// ── spaces ls ──
|
|
454
473
|
spacesCmd
|
|
455
474
|
.command("ls")
|
|
@@ -457,20 +476,27 @@ export function registerSpaces(program) {
|
|
|
457
476
|
.description("List all spaces")
|
|
458
477
|
.option("--mine", "Only spaces you own")
|
|
459
478
|
.option("--pinned", "Only pinned spaces")
|
|
479
|
+
.option("--archived", "Only archived spaces")
|
|
460
480
|
.option("--json", "Output as JSON")
|
|
461
481
|
.action(async (opts) => {
|
|
462
482
|
const client = createClient();
|
|
463
483
|
try {
|
|
464
|
-
const
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
484
|
+
const items = [];
|
|
485
|
+
let cursor = null;
|
|
486
|
+
do {
|
|
487
|
+
const page = await client.spaces.list({
|
|
488
|
+
limit: 100,
|
|
489
|
+
cursor,
|
|
490
|
+
filter: opts.archived ? "archived" : opts.pinned ? "pinned" : opts.mine ? "mine" : "all",
|
|
491
|
+
});
|
|
492
|
+
items.push(...page.items);
|
|
493
|
+
cursor = page.pageInfo.nextCursor;
|
|
494
|
+
} while (cursor);
|
|
469
495
|
const filtered = items.filter((item) => {
|
|
470
|
-
if (opts.mine && myUuid && item.userUuid !== myUuid)
|
|
471
|
-
return false;
|
|
472
496
|
if (opts.pinned && !item.isPinned)
|
|
473
497
|
return false;
|
|
498
|
+
if (opts.archived && !item.isArchived)
|
|
499
|
+
return false;
|
|
474
500
|
return true;
|
|
475
501
|
});
|
|
476
502
|
if (jsonRequested(opts))
|
|
@@ -487,6 +513,28 @@ export function registerSpaces(program) {
|
|
|
487
513
|
handleHttp(e);
|
|
488
514
|
}
|
|
489
515
|
});
|
|
516
|
+
for (const archive of [true, false]) {
|
|
517
|
+
spacesCmd
|
|
518
|
+
.command(archive ? "archive <ids...>" : "unarchive <ids...>")
|
|
519
|
+
.description(archive ? "Archive spaces in your personal view" : "Restore archived spaces")
|
|
520
|
+
.option("--json", "Output as JSON")
|
|
521
|
+
.action(async (ids, opts) => {
|
|
522
|
+
const client = createClient();
|
|
523
|
+
try {
|
|
524
|
+
const chunks = Array.from({ length: Math.ceil(ids.length / 200) }, (_, index) => ids.slice(index * 200, (index + 1) * 200));
|
|
525
|
+
for (const chunk of chunks)
|
|
526
|
+
await client.user.labels.patchResources(chunk, archive
|
|
527
|
+
? { addLabelRefs: ["Archived"], removeLabelRefs: ["Pinned"] }
|
|
528
|
+
: { removeLabelRefs: ["Archived"] });
|
|
529
|
+
if (jsonRequested(opts))
|
|
530
|
+
return outJson({ ids, archived: archive });
|
|
531
|
+
ok(`${archive ? "Archived" : "Unarchived"} ${ids.length} space(s)`);
|
|
532
|
+
}
|
|
533
|
+
catch (e) {
|
|
534
|
+
handleHttp(e);
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
}
|
|
490
538
|
// ── spaces get ──
|
|
491
539
|
spacesCmd
|
|
492
540
|
.command("get [id]")
|
|
@@ -652,6 +700,7 @@ export function registerSpaces(program) {
|
|
|
652
700
|
.action((words, opts) => runCompletionCommand(spacesCmd, words, opts));
|
|
653
701
|
// ── spaces files ──
|
|
654
702
|
registerFiles(spacesCmd);
|
|
703
|
+
registerSpaceDisplays(spacesCmd);
|
|
655
704
|
// ── spaces sessions ──
|
|
656
705
|
registerSessions(spacesCmd);
|
|
657
706
|
// ── spaces turns ──
|
|
@@ -712,9 +761,9 @@ export function registerSpaces(program) {
|
|
|
712
761
|
if (jsonRequested(opts))
|
|
713
762
|
return outJson(usage);
|
|
714
763
|
console.log("\n Summary:");
|
|
715
|
-
table([usage.
|
|
764
|
+
table([usage.totals], [
|
|
716
765
|
{ key: "totalTokens", label: "Tokens" },
|
|
717
|
-
{ key: "costTotal", label: "Cost ($)" },
|
|
766
|
+
...(usage.totals.costTotal ? [{ key: "costTotal", label: "Cost ($)" }] : []),
|
|
718
767
|
{ key: "requestCount", label: "Requests" },
|
|
719
768
|
{ key: "successCount", label: "Success" },
|
|
720
769
|
{ key: "errorCount", label: "Errors" },
|
|
@@ -1145,25 +1194,7 @@ function registerFiles(spacesCmd) {
|
|
|
1145
1194
|
handleHttp(e);
|
|
1146
1195
|
}
|
|
1147
1196
|
});
|
|
1148
|
-
filesCmd
|
|
1149
|
-
.command("cat <path>")
|
|
1150
|
-
.description("Read file content")
|
|
1151
|
-
.action(async (path) => {
|
|
1152
|
-
const spaceId = await resolveSpace(spacesCmd);
|
|
1153
|
-
const client = createClient();
|
|
1154
|
-
try {
|
|
1155
|
-
const file = await client.space(spaceId).files.read(path);
|
|
1156
|
-
if (!("content" in file))
|
|
1157
|
-
return error("File is being prepared. Please retry shortly.");
|
|
1158
|
-
if (file.delivery === "url" && file.url) {
|
|
1159
|
-
console.log(`[CDN] ${file.url}`);
|
|
1160
|
-
}
|
|
1161
|
-
console.log(file.content);
|
|
1162
|
-
}
|
|
1163
|
-
catch (e) {
|
|
1164
|
-
handleHttp(e);
|
|
1165
|
-
}
|
|
1166
|
-
});
|
|
1197
|
+
registerSpaceFileTransfer(filesCmd, spacesCmd);
|
|
1167
1198
|
filesCmd
|
|
1168
1199
|
.command("write <path>")
|
|
1169
1200
|
.description("Write file content")
|
|
@@ -1354,6 +1385,37 @@ function registerSessions(spacesCmd) {
|
|
|
1354
1385
|
handleHttp(e);
|
|
1355
1386
|
}
|
|
1356
1387
|
});
|
|
1388
|
+
sessionsCmd
|
|
1389
|
+
.command("stats <id>")
|
|
1390
|
+
.description("Session statistics for settled turns (including separate inherited history)")
|
|
1391
|
+
.option("--json", "Output as JSON")
|
|
1392
|
+
.action(async (id, opts) => {
|
|
1393
|
+
const spaceId = await resolveSpace(spacesCmd);
|
|
1394
|
+
try {
|
|
1395
|
+
const { stats } = await createClient().space(spaceId).session(id).stats();
|
|
1396
|
+
if (jsonRequested(opts))
|
|
1397
|
+
return outJson({ stats });
|
|
1398
|
+
const own = stats.own;
|
|
1399
|
+
const rows = [
|
|
1400
|
+
{ metric: "Turns", value: own.turns },
|
|
1401
|
+
{ metric: "Model calls", value: own.calls },
|
|
1402
|
+
{ metric: "Tool calls", value: own.toolCalls },
|
|
1403
|
+
{ metric: "Compactions", value: own.compactions },
|
|
1404
|
+
{ metric: "Tokens", value: own.usage?.totalTokens },
|
|
1405
|
+
{ metric: "Execution time (ms)", value: own.elapsedMs },
|
|
1406
|
+
{ metric: "Model time (ms)", value: own.modelMs == null ? null : Math.round(own.modelMs) },
|
|
1407
|
+
{ metric: "Model cost (USD)", value: own.modelCostUsd },
|
|
1408
|
+
{ metric: "Content generation cost (USD)", value: own.generationCostUsd },
|
|
1409
|
+
{ metric: "Inherited turns", value: stats.inherited.turns },
|
|
1410
|
+
].filter((row) => row.value != null);
|
|
1411
|
+
table(rows, [{ key: "metric", label: "Metric" }, { key: "value", label: "Value" }]);
|
|
1412
|
+
if (own.partial)
|
|
1413
|
+
console.log("Some execution details were not recorded.");
|
|
1414
|
+
}
|
|
1415
|
+
catch (e) {
|
|
1416
|
+
handleHttp(e);
|
|
1417
|
+
}
|
|
1418
|
+
});
|
|
1357
1419
|
sessionsCmd
|
|
1358
1420
|
.command("files <id>")
|
|
1359
1421
|
.description("List Space files changed by a session")
|
|
@@ -1514,6 +1576,15 @@ function registerTurns(sessionsCmd) {
|
|
|
1514
1576
|
{ key: "stopReason", label: "Stop" },
|
|
1515
1577
|
{ key: "errorMessage", label: "Error" },
|
|
1516
1578
|
]);
|
|
1579
|
+
const origin = readSessionTurnOrigin(result.turn.meta, spaceId);
|
|
1580
|
+
if (origin)
|
|
1581
|
+
console.log(`\nOrigin (${origin.kind}): space=${origin.spaceId} session=${origin.sessionId} turn=${origin.turnId}${origin.toolCallId ? ` toolCall=${origin.toolCallId}` : ""}`);
|
|
1582
|
+
const sent = readSentTurns(result.turn.meta);
|
|
1583
|
+
if (sent.length > 0) {
|
|
1584
|
+
console.log(`\nSent (${sent.length}):`);
|
|
1585
|
+
for (const ref of sent)
|
|
1586
|
+
console.log(` ${ref.spaceId ? `space=${ref.spaceId} ` : ""}session=${ref.sessionId} turn=${ref.turnId}${ref.toolCallId ? ` toolCall=${ref.toolCallId}` : ""}`);
|
|
1587
|
+
}
|
|
1517
1588
|
if (result.turn.userText)
|
|
1518
1589
|
console.log(`\nUser:\n${result.turn.userText}`);
|
|
1519
1590
|
if (result.turn.assistantText)
|
package/dist/index.js
CHANGED
|
@@ -37,7 +37,7 @@ program
|
|
|
37
37
|
.summary("Work with Cohub from your terminal")
|
|
38
38
|
.description("Send prompts, manage Space files, and publish public output.")
|
|
39
39
|
.version(VERSION, "-v, --version", "Show version")
|
|
40
|
-
.option("-s, --space <
|
|
40
|
+
.option("-s, --space <space>", "Target Space: ID, slug, or username/slug")
|
|
41
41
|
.option("--json", "Print machine-readable JSON when supported")
|
|
42
42
|
.helpOption("-h, --help", "Show help")
|
|
43
43
|
.addHelpText("after", `
|
|
@@ -59,7 +59,7 @@ Common commands:
|
|
|
59
59
|
cohub -s <space-id> boards inspect <board-id>
|
|
60
60
|
cohub -s <space-id> spaces turns ls --author others
|
|
61
61
|
cohub -s <space-id> spaces sessions turns ls <session-id>
|
|
62
|
-
cohub -s
|
|
62
|
+
cohub -s home spaces files ls
|
|
63
63
|
cohub -s <space-id> public upload ./dist demo
|
|
64
64
|
cohub -s <space-id> apps publish demo --file dist/index.html
|
|
65
65
|
cohub desktop open <app-id> --call selection.get
|
|
@@ -69,10 +69,13 @@ Common commands:
|
|
|
69
69
|
cohub generate "A calm lake at sunrise" --model <model> --output lake.png
|
|
70
70
|
|
|
71
71
|
Target space:
|
|
72
|
-
-s <space
|
|
72
|
+
-s <space>, then COHUB_SPACE_ID, then the current directory Runtime binding.
|
|
73
|
+
<space> is an ID, a slug you own such as home, or username/slug.
|
|
74
|
+
prompt, completion, generate, apps, and public fall back to your Home Space;
|
|
75
|
+
other Space commands need a target.
|
|
73
76
|
|
|
74
77
|
Environment:
|
|
75
|
-
COHUB_SPACE_ID Target Space
|
|
78
|
+
COHUB_SPACE_ID Target Space when -s is omitted
|
|
76
79
|
COHUB_EXECUTION_TOKEN Use this token instead of the stored Logto session
|
|
77
80
|
ENV=dev Use the development Cohub environment
|
|
78
81
|
HTTPS_PROXY Honored for API and uploads (also HTTP_PROXY, NO_PROXY)
|
package/dist/runtime/launch.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export type RuntimeUpOptions = {
|
|
|
8
8
|
harness: string[];
|
|
9
9
|
pi?: string;
|
|
10
10
|
codex?: string;
|
|
11
|
+
display?: string | boolean;
|
|
11
12
|
yes?: boolean;
|
|
12
13
|
json?: boolean;
|
|
13
14
|
detach?: boolean;
|
|
@@ -15,6 +16,7 @@ export type RuntimeUpOptions = {
|
|
|
15
16
|
};
|
|
16
17
|
export declare const resolveLocalSpaceName: (root: string, name?: string) => string;
|
|
17
18
|
export declare function parseRuntimeHarnesses(values: string[]): ("pi" | "codex")[];
|
|
19
|
+
export declare function parseRuntimeDisplay(value: string | boolean | undefined): string | undefined;
|
|
18
20
|
export declare function resolveRuntimeTarget(program: Command, target?: string): Promise<string>;
|
|
19
21
|
export declare function startBackgroundRuntime(config: RuntimeLaunch): Promise<RuntimeSummary>;
|
|
20
22
|
export declare function runtimeUp(program: Command, dir: string | undefined, options: RuntimeUpOptions): Promise<void>;
|
package/dist/runtime/launch.js
CHANGED
|
@@ -7,7 +7,7 @@ import { isLocalHarness } from "@neta-art/cohub";
|
|
|
7
7
|
import { discoverHarnesses, installedHarnesses } from "./harness.js";
|
|
8
8
|
import { requireAccessToken } from "../auth.js";
|
|
9
9
|
import { createClient } from "../client.js";
|
|
10
|
-
import { currentIdentityKey, explicitSpace } from "../space.js";
|
|
10
|
+
import { currentIdentityKey, explicitSpace, resolveSpaceRef } from "../space.js";
|
|
11
11
|
import { canonicalRuntimeRoot, getRuntimeSpaceBinding, resolveRuntimeSpace } from "./space-binding.js";
|
|
12
12
|
import { controlRuntimeInstance, requestRuntimeInstance, runtimeInstanceDirectory } from "./instance.js";
|
|
13
13
|
import { ensureNativeSync, repairIntegrations } from "./native/attach.js";
|
|
@@ -20,10 +20,30 @@ export function parseRuntimeHarnesses(values) {
|
|
|
20
20
|
throw new Error("Harness must be pi or codex");
|
|
21
21
|
return [...new Set(names.length ? names : ["pi"])];
|
|
22
22
|
}
|
|
23
|
+
export function parseRuntimeDisplay(value) {
|
|
24
|
+
if (value === undefined || value === false)
|
|
25
|
+
return undefined;
|
|
26
|
+
const spec = value === true ? "auto" : value.trim();
|
|
27
|
+
if (!/^(auto|macos|x11(:[\w.:-]+)?|xvfb(:\d{3,4}x\d{3,4})?)$/.test(spec)) {
|
|
28
|
+
throw new Error("Display must be auto, macos, x11[:display] or xvfb[:WIDTHxHEIGHT]");
|
|
29
|
+
}
|
|
30
|
+
if (process.platform === "linux" && spec === "auto" && !process.env.DISPLAY) {
|
|
31
|
+
throw new Error(process.env.WAYLAND_DISPLAY
|
|
32
|
+
? "Wayland sessions cannot be shared yet: log in with an X11 session, or use --display xvfb for a virtual screen"
|
|
33
|
+
: "No X11 session to share (DISPLAY is unset): use --display xvfb for a virtual screen");
|
|
34
|
+
}
|
|
35
|
+
return spec;
|
|
36
|
+
}
|
|
37
|
+
const isVirtualDisplay = (spec) => spec.startsWith("xvfb");
|
|
38
|
+
async function requestedRuntimeSpace(program, target) {
|
|
39
|
+
const ref = target?.trim() || explicitSpace(program);
|
|
40
|
+
return ref ? resolveSpaceRef(ref) : null;
|
|
41
|
+
}
|
|
23
42
|
export async function resolveRuntimeTarget(program, target) {
|
|
24
|
-
const spaceId =
|
|
43
|
+
const spaceId = await requestedRuntimeSpace(program, target)
|
|
44
|
+
?? (await getRuntimeSpaceBinding(process.cwd(), currentIdentityKey()))?.spaceId;
|
|
25
45
|
if (!spaceId)
|
|
26
|
-
throw new Error("No directory binding. Use --space <
|
|
46
|
+
throw new Error("No directory binding. Use --space <space> or runtime up");
|
|
27
47
|
return spaceId;
|
|
28
48
|
}
|
|
29
49
|
export async function startBackgroundRuntime(config) {
|
|
@@ -72,11 +92,12 @@ export async function runtimeUp(program, dir, options) {
|
|
|
72
92
|
if (!(await stat(requestedRoot)).isDirectory())
|
|
73
93
|
throw new Error("Workspace is not a directory");
|
|
74
94
|
const root = await canonicalRuntimeRoot(requestedRoot);
|
|
75
|
-
const
|
|
76
|
-
if (options.new &&
|
|
95
|
+
const requestedRef = options.space?.trim() || explicitSpace(program);
|
|
96
|
+
if (options.new && requestedRef)
|
|
77
97
|
throw new Error("--new cannot be combined with --space or COHUB_SPACE_ID");
|
|
78
|
-
if (options.name &&
|
|
98
|
+
if (options.name && requestedRef)
|
|
79
99
|
throw new Error("--name only applies to a new Space");
|
|
100
|
+
const requested = await requestedRuntimeSpace(program, options.space);
|
|
80
101
|
const identity = currentIdentityKey();
|
|
81
102
|
if (!identity) {
|
|
82
103
|
await requireAccessToken();
|
|
@@ -85,10 +106,11 @@ export async function runtimeUp(program, dir, options) {
|
|
|
85
106
|
const binding = await getRuntimeSpaceBinding(root, identity);
|
|
86
107
|
const existingId = requested || binding?.spaceId;
|
|
87
108
|
let harnesses = parseRuntimeHarnesses(options.harness);
|
|
109
|
+
let display = parseRuntimeDisplay(options.display);
|
|
88
110
|
if (existingId && !options.new) {
|
|
89
111
|
const existing = await requestRuntimeInstance(runtimeInstanceDirectory(identity, existingId));
|
|
90
112
|
if (existing) {
|
|
91
|
-
if (existing.root !== root || options.harness.length && [...existing.harnesses].sort().join() !== [...harnesses].sort().join() || options.pi || options.codex) {
|
|
113
|
+
if (existing.root !== root || options.harness.length && [...existing.harnesses].sort().join() !== [...harnesses].sort().join() || options.pi || options.codex || display && display !== existing.display) {
|
|
92
114
|
throw new Error("Runtime is running with a different configuration. Use down first");
|
|
93
115
|
}
|
|
94
116
|
// `up` is the single idempotent entry: a reused instance adopts its running
|
|
@@ -134,6 +156,11 @@ export async function runtimeUp(program, dir, options) {
|
|
|
134
156
|
const answer = await rl.question("Collaborators can execute as your OS user, beyond this folder. Allow? [Y/n] ");
|
|
135
157
|
if (/^n(o)?$/i.test(answer.trim()))
|
|
136
158
|
return;
|
|
159
|
+
if (display && !isVirtualDisplay(display)) {
|
|
160
|
+
const share = await rl.question("Collaborators and agents can see and control this screen. Share it? [y/N] ");
|
|
161
|
+
if (!/^y(es)?$/i.test(share.trim()))
|
|
162
|
+
display = undefined;
|
|
163
|
+
}
|
|
137
164
|
}
|
|
138
165
|
finally {
|
|
139
166
|
rl.close();
|
|
@@ -160,7 +187,7 @@ export async function runtimeUp(program, dir, options) {
|
|
|
160
187
|
});
|
|
161
188
|
// Consent (or --yes) precedes any user-level integration install; failure never blocks startup.
|
|
162
189
|
const native = await ensureNativeSync({ root, spaceId, identity, harnesses, yes: options.yes, executables: { pi: options.pi, codex: options.codex } });
|
|
163
|
-
const config = { spaceId, root, identity, harnesses, capabilities, executables: { pi: options.pi, codex: options.codex }, background: Boolean(options.detach), verbose: options.verbose, importHistory: native.importHistory };
|
|
190
|
+
const config = { spaceId, root, identity, harnesses, capabilities, executables: { pi: options.pi, codex: options.codex }, background: Boolean(options.detach), verbose: options.verbose, importHistory: native.importHistory, display };
|
|
164
191
|
const existing = await requestRuntimeInstance(runtimeInstanceDirectory(identity, spaceId));
|
|
165
192
|
if (existing) {
|
|
166
193
|
if (existing.root !== root)
|
|
@@ -18,6 +18,7 @@ export type RuntimeSummary = {
|
|
|
18
18
|
workspaceConnected: boolean;
|
|
19
19
|
diagnosticsPath: string;
|
|
20
20
|
background: boolean;
|
|
21
|
+
display?: string;
|
|
21
22
|
native?: NativeStatus;
|
|
22
23
|
};
|
|
23
24
|
export declare function printRuntimeSummary(summary: RuntimeSummary, json?: boolean, reused?: boolean): void;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { webUrl } from "../web.js";
|
|
2
2
|
export const diagnosticLevels = ["debug", "info", "warn", "error"];
|
|
3
3
|
export const atLeastLevel = (level, minimum) => diagnosticLevels.indexOf(level) >= diagnosticLevels.indexOf(minimum);
|
|
4
|
-
export const runtimeWebUrl = (spaceId) =>
|
|
4
|
+
export const runtimeWebUrl = (spaceId) => webUrl(`/spaces/${spaceId}`);
|
|
5
5
|
const messages = {
|
|
6
6
|
"runtime.ready": "Harness connected",
|
|
7
7
|
"runtime.available": "Runtime ready",
|
|
@@ -69,6 +69,7 @@ export function printRuntimeSummary(summary, json = false, reused = false) {
|
|
|
69
69
|
["Directory", summary.root],
|
|
70
70
|
["Harness", summary.harnesses.join(" · ")],
|
|
71
71
|
["Mode", summary.background ? "Background" : "Foreground"],
|
|
72
|
+
...(summary.display ? [["Screen", summary.display === "auto" ? "This computer" : summary.display]] : []),
|
|
72
73
|
["PID", String(summary.pid)],
|
|
73
74
|
["Logs", summary.diagnosticsPath],
|
|
74
75
|
];
|
|
@@ -13,6 +13,7 @@ export type RuntimeLaunch = {
|
|
|
13
13
|
verbose?: boolean;
|
|
14
14
|
/** Start importing earlier conversations as soon as the Runtime is connected. */
|
|
15
15
|
importHistory?: boolean;
|
|
16
|
+
display?: string;
|
|
16
17
|
};
|
|
17
18
|
export declare function sandboxOutputLevel(value: unknown, stream: "stdout" | "stderr"): RuntimeDiagnosticLevel;
|
|
18
19
|
export declare function runRuntime(config: RuntimeLaunch, onState: (status: RuntimeSummary) => void, externalSignal?: AbortSignal, onDiagnostic?: (event: RuntimeDiagnostic) => void): Promise<void>;
|
|
@@ -48,6 +48,7 @@ export async function runRuntime(config, onState, externalSignal, onDiagnostic)
|
|
|
48
48
|
let status = {
|
|
49
49
|
spaceId: config.spaceId, root: config.root, runtimeId: diagnostics.runtimeId,
|
|
50
50
|
pid: process.pid, harnesses: config.harnesses, background: config.background,
|
|
51
|
+
...(config.display ? { display: config.display } : {}),
|
|
51
52
|
state: "starting", harnessConnected: false, workspaceConnected: false,
|
|
52
53
|
diagnosticsPath: diagnostics.directory,
|
|
53
54
|
};
|
|
@@ -123,7 +124,7 @@ export async function runRuntime(config, onState, externalSignal, onDiagnostic)
|
|
|
123
124
|
signal.throwIfAborted();
|
|
124
125
|
const bridge = spawn(binary, ["--local", "--space", config.spaceId, "--root", config.root, "--relay", process.env.COHUB_RELAY_URL?.trim() || relay.toString()], {
|
|
125
126
|
stdio: ["pipe", "pipe", "pipe", "pipe"],
|
|
126
|
-
env: { ...process.env, COHUB_RELAY_TOKEN: initialToken, COHUB_RUNTIME_ID: status.runtimeId, COHUB_LOG_FORMAT: "json", COHUB_RUNTIME_MANAGED: "1" },
|
|
127
|
+
env: { ...process.env, COHUB_RELAY_TOKEN: initialToken, COHUB_RUNTIME_ID: status.runtimeId, COHUB_LOG_FORMAT: "json", COHUB_RUNTIME_MANAGED: "1", COHUB_DISPLAY: config.display ?? "" },
|
|
127
128
|
});
|
|
128
129
|
bridge.stdin?.on("error", () => undefined);
|
|
129
130
|
const closed = new Promise((resolve) => {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function createLimiter(concurrency) {
|
|
2
|
+
let active = 0;
|
|
3
|
+
const waiting = [];
|
|
4
|
+
return async (task) => {
|
|
5
|
+
if (active >= concurrency)
|
|
6
|
+
await new Promise((resolve) => waiting.push(resolve));
|
|
7
|
+
active += 1;
|
|
8
|
+
try {
|
|
9
|
+
return await task();
|
|
10
|
+
}
|
|
11
|
+
finally {
|
|
12
|
+
active -= 1;
|
|
13
|
+
waiting.shift()?.();
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type SpaceOperand = {
|
|
2
|
+
kind: "space";
|
|
3
|
+
space: string | null;
|
|
4
|
+
path: string;
|
|
5
|
+
};
|
|
6
|
+
export type LocalOperand = {
|
|
7
|
+
kind: "local";
|
|
8
|
+
path: string;
|
|
9
|
+
};
|
|
10
|
+
export type FileOperand = SpaceOperand | LocalOperand;
|
|
11
|
+
export declare class InvalidFileOperandError extends Error {
|
|
12
|
+
name: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function normalizeSpacePath(raw: string): string;
|
|
15
|
+
export declare function parseSpaceFileOperand(raw: string): SpaceOperand;
|
|
16
|
+
export declare function parseCopyOperand(raw: string, currentSpaceDeclared: boolean): FileOperand;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { posix, resolve } from "node:path";
|
|
2
|
+
import { parseSpaceRef } from "@neta-art/cohub";
|
|
3
|
+
export class InvalidFileOperandError extends Error {
|
|
4
|
+
name = "InvalidFileOperandError";
|
|
5
|
+
}
|
|
6
|
+
const WORKSPACE_ROOT = "/workspace";
|
|
7
|
+
const isWorkspacePath = (path) => path === WORKSPACE_ROOT || path.startsWith(`${WORKSPACE_ROOT}/`);
|
|
8
|
+
export function normalizeSpacePath(raw) {
|
|
9
|
+
let path = raw.trim();
|
|
10
|
+
if (isWorkspacePath(path))
|
|
11
|
+
path = path.slice(WORKSPACE_ROOT.length + 1);
|
|
12
|
+
else if (path.startsWith("/"))
|
|
13
|
+
throw new InvalidFileOperandError(`'${raw}' is not a Space path`);
|
|
14
|
+
const normalized = posix.normalize(path || ".").replace(/\/+$/, "");
|
|
15
|
+
return normalized === "." ? "" : normalized;
|
|
16
|
+
}
|
|
17
|
+
function parseSpacePrefix(raw) {
|
|
18
|
+
const colon = raw.indexOf(":");
|
|
19
|
+
if (colon <= 0)
|
|
20
|
+
return null;
|
|
21
|
+
const space = raw.slice(0, colon);
|
|
22
|
+
return parseSpaceRef(space) ? { kind: "space", space, path: normalizeSpacePath(raw.slice(colon + 1)) } : null;
|
|
23
|
+
}
|
|
24
|
+
export function parseSpaceFileOperand(raw) {
|
|
25
|
+
return parseSpacePrefix(raw) ?? { kind: "space", space: null, path: normalizeSpacePath(raw) };
|
|
26
|
+
}
|
|
27
|
+
export function parseCopyOperand(raw, currentSpaceDeclared) {
|
|
28
|
+
const space = parseSpacePrefix(raw);
|
|
29
|
+
if (space)
|
|
30
|
+
return space;
|
|
31
|
+
const local = !currentSpaceDeclared || (raw.startsWith("/") && !isWorkspacePath(raw));
|
|
32
|
+
return local ? { kind: "local", path: resolve(raw) } : { kind: "space", space: null, path: normalizeSpacePath(raw) };
|
|
33
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type SpaceFsCopyResult, type SpaceFsCopyStats, type SpaceFsTreeResponse } from "@neta-art/cohub";
|
|
2
|
+
import { type SpaceFiles } from "./upload.js";
|
|
3
|
+
export type CopyOptions = {
|
|
4
|
+
recursive: boolean;
|
|
5
|
+
noClobber: boolean;
|
|
6
|
+
preserveTimestamps: boolean;
|
|
7
|
+
};
|
|
8
|
+
export declare class CopyAbortError extends Error {
|
|
9
|
+
name: string;
|
|
10
|
+
}
|
|
11
|
+
export declare const emptyCopyResult: () => SpaceFsCopyResult;
|
|
12
|
+
export declare function mergeCopyResults(results: SpaceFsCopyResult[]): SpaceFsCopyResult;
|
|
13
|
+
type SpaceNode = {
|
|
14
|
+
type: "dir";
|
|
15
|
+
tree: SpaceFsTreeResponse;
|
|
16
|
+
} | {
|
|
17
|
+
type: "file";
|
|
18
|
+
} | null;
|
|
19
|
+
export declare function statSpacePath(files: SpaceFiles, path: string): Promise<SpaceNode>;
|
|
20
|
+
export declare function checkDestination(input: {
|
|
21
|
+
sourceCount: number;
|
|
22
|
+
display: string;
|
|
23
|
+
stat: (path: string) => Promise<"dir" | "file" | null>;
|
|
24
|
+
path: string;
|
|
25
|
+
parent: string;
|
|
26
|
+
}): Promise<boolean>;
|
|
27
|
+
export declare const spaceNodeType: (files: SpaceFiles, path: string) => Promise<"dir" | "file" | null>;
|
|
28
|
+
export declare const localNodeType: (path: string) => Promise<"dir" | "file" | null>;
|
|
29
|
+
export declare function uploadToSpace(input: {
|
|
30
|
+
files: SpaceFiles;
|
|
31
|
+
sources: string[];
|
|
32
|
+
destination: string;
|
|
33
|
+
intoDirectory: boolean;
|
|
34
|
+
options: CopyOptions;
|
|
35
|
+
onProgress: (stats: SpaceFsCopyStats) => void;
|
|
36
|
+
}): Promise<SpaceFsCopyResult>;
|
|
37
|
+
export declare function downloadFromSpace(input: {
|
|
38
|
+
sources: Array<{
|
|
39
|
+
files: SpaceFiles;
|
|
40
|
+
path: string;
|
|
41
|
+
}>;
|
|
42
|
+
destination: string;
|
|
43
|
+
intoDirectory: boolean;
|
|
44
|
+
options: CopyOptions;
|
|
45
|
+
onProgress: (stats: SpaceFsCopyStats) => void;
|
|
46
|
+
}): Promise<SpaceFsCopyResult>;
|
|
47
|
+
export declare function copyLocal(input: {
|
|
48
|
+
sources: string[];
|
|
49
|
+
destination: string;
|
|
50
|
+
intoDirectory: boolean;
|
|
51
|
+
options: CopyOptions;
|
|
52
|
+
}): Promise<SpaceFsCopyResult>;
|
|
53
|
+
export {};
|