@neta-art/cohub-cli 3.9.6 → 3.10.1
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 +32 -2
- package/dist/commands/boards.js +17 -0
- package/dist/commands/me.js +25 -7
- package/dist/commands/public.d.ts +20 -0
- package/dist/commands/public.js +300 -0
- package/dist/commands/references.js +2 -2
- package/dist/commands/referrals.js +1 -1
- package/dist/commands/sandbox.js +2 -2
- package/dist/commands/search.js +2 -2
- package/dist/commands/space-invitations.js +1 -1
- package/dist/commands/ui.js +1 -1
- package/dist/commands/works.js +114 -0
- package/dist/index.js +5 -2
- package/dist/output.js +13 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @neta-art/cohub-cli
|
|
2
2
|
|
|
3
|
-
CLI for [Cohub](https://cohub.
|
|
3
|
+
CLI for [Cohub](https://cohub.live) — work with Spaces, Chats, files, Saves, Tasks, scheduled prompts, search, and multimodal generation from your terminal.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -158,7 +158,37 @@ cohub -s <spaceId> boards watch <boardId> --json
|
|
|
158
158
|
```
|
|
159
159
|
|
|
160
160
|
Pass nodes, effects, and sequences as JSON when creating a Board. The path and
|
|
161
|
-
title stay explicit in the command
|
|
161
|
+
title stay explicit in the command. Inspect `boards capabilities --json` for the
|
|
162
|
+
supported node types, enums, and coordinate spaces.
|
|
163
|
+
|
|
164
|
+
```json
|
|
165
|
+
{
|
|
166
|
+
"nodes": [
|
|
167
|
+
{
|
|
168
|
+
"nodeId": "goal",
|
|
169
|
+
"type": "geo",
|
|
170
|
+
"parentId": null,
|
|
171
|
+
"orderKey": null,
|
|
172
|
+
"x": 80,
|
|
173
|
+
"y": 80,
|
|
174
|
+
"width": 240,
|
|
175
|
+
"height": 120,
|
|
176
|
+
"rotation": 0,
|
|
177
|
+
"refKind": null,
|
|
178
|
+
"refPath": null,
|
|
179
|
+
"refUrl": null,
|
|
180
|
+
"view": {},
|
|
181
|
+
"style": {},
|
|
182
|
+
"data": {
|
|
183
|
+
"geo": "rectangle",
|
|
184
|
+
"text": "Ship",
|
|
185
|
+
"color": "green",
|
|
186
|
+
"fillOpacity": 0.12
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
]
|
|
190
|
+
}
|
|
191
|
+
```
|
|
162
192
|
|
|
163
193
|
```bash
|
|
164
194
|
cohub -s <spaceId> boards create boards/plan.board \
|
package/dist/commands/boards.js
CHANGED
|
@@ -347,6 +347,23 @@ export function registerBoards(program) {
|
|
|
347
347
|
{ key: "renderers", label: "Renderers" },
|
|
348
348
|
{ key: "digest", label: "Digest" },
|
|
349
349
|
]);
|
|
350
|
+
const nodes = result.nodes;
|
|
351
|
+
if (nodes) {
|
|
352
|
+
console.log();
|
|
353
|
+
table([{
|
|
354
|
+
types: nodes.types.join(", "),
|
|
355
|
+
colors: nodes.colors.join(", "),
|
|
356
|
+
geos: nodes.geos.join(", "),
|
|
357
|
+
drawPoints: nodes.coordinates.drawPoints,
|
|
358
|
+
arrowEndpoints: nodes.coordinates.arrowEndpoints,
|
|
359
|
+
}], [
|
|
360
|
+
{ key: "types", label: "Node types" },
|
|
361
|
+
{ key: "colors", label: "Colors" },
|
|
362
|
+
{ key: "geos", label: "Geo kinds" },
|
|
363
|
+
{ key: "drawPoints", label: "Draw points" },
|
|
364
|
+
{ key: "arrowEndpoints", label: "Arrow endpoints" },
|
|
365
|
+
]);
|
|
366
|
+
}
|
|
350
367
|
}
|
|
351
368
|
catch (cause) {
|
|
352
369
|
handleHttp(cause);
|
package/dist/commands/me.js
CHANGED
|
@@ -43,17 +43,35 @@ export function registerMe(program) {
|
|
|
43
43
|
}
|
|
44
44
|
});
|
|
45
45
|
meCmd
|
|
46
|
-
.command("
|
|
47
|
-
.description("
|
|
46
|
+
.command("activity")
|
|
47
|
+
.description("Show your activity across all spaces")
|
|
48
|
+
.option("--days <n>", "Show the last N days (default: 30)")
|
|
49
|
+
.option("--from <date>", "Start at this ISO 8601 date")
|
|
50
|
+
.option("--to <date>", "Stop before this ISO 8601 date")
|
|
48
51
|
.option("--json", "Output as JSON")
|
|
49
|
-
.
|
|
52
|
+
.addHelpText("after", "\nExamples:\n $ cohub me activity --days 7\n $ cohub me activity --from 2026-01-01 --to 2026-02-01")
|
|
53
|
+
.action(async (opts) => {
|
|
54
|
+
if (opts.days && (opts.from || opts.to)) {
|
|
55
|
+
process.stderr.write("\n ✗ Invalid range\n --days cannot be combined with --from or --to\n\n");
|
|
56
|
+
process.exitCode = 1;
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (opts.to && !opts.from) {
|
|
60
|
+
process.stderr.write("\n ✗ Invalid range\n --from is required when --to is provided\n\n");
|
|
61
|
+
process.exitCode = 1;
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
50
64
|
const client = createClient();
|
|
51
65
|
try {
|
|
52
|
-
const
|
|
66
|
+
const activity = await client.user.getActivity({
|
|
67
|
+
days: opts.days ? parseInteger(opts.days, "days", 1) : undefined,
|
|
68
|
+
from: opts.from,
|
|
69
|
+
to: opts.to,
|
|
70
|
+
});
|
|
53
71
|
if (jsonRequested(opts))
|
|
54
|
-
return outJson(
|
|
55
|
-
console.log(
|
|
56
|
-
table([
|
|
72
|
+
return outJson(activity);
|
|
73
|
+
console.log(`\n ${activity.range.from} → ${activity.range.to}`);
|
|
74
|
+
table([activity.summary], [
|
|
57
75
|
{ key: "totalTokens", label: "Tokens" },
|
|
58
76
|
{ key: "costTotal", label: "Cost ($)" },
|
|
59
77
|
{ key: "requestCount", label: "Requests" },
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { CohubHttpClient } from "@neta-art/cohub";
|
|
2
|
+
import type { Command } from "commander";
|
|
3
|
+
type LocalPublicFile = {
|
|
4
|
+
id: string;
|
|
5
|
+
localPath: string;
|
|
6
|
+
publicPath: string;
|
|
7
|
+
size: number;
|
|
8
|
+
mimeType: string;
|
|
9
|
+
};
|
|
10
|
+
type PublicCommandDeps = {
|
|
11
|
+
createClient?: () => CohubHttpClient;
|
|
12
|
+
fetch?: typeof fetch;
|
|
13
|
+
};
|
|
14
|
+
export declare function collectPublicUpload(source: string, destination?: string): Promise<{
|
|
15
|
+
files: LocalPublicFile[];
|
|
16
|
+
destination: string;
|
|
17
|
+
entryPath: string | null;
|
|
18
|
+
}>;
|
|
19
|
+
export declare function registerPublic(program: Command, deps?: PublicCommandDeps): Command;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { createReadStream } from "node:fs";
|
|
3
|
+
import { lstat, readdir } from "node:fs/promises";
|
|
4
|
+
import { basename, extname, relative, resolve } from "node:path";
|
|
5
|
+
import { createClient } from "../client.js";
|
|
6
|
+
import { error, handleHttp, json as outJson, jsonRequested } from "../output.js";
|
|
7
|
+
import { resolveSpace } from "../space.js";
|
|
8
|
+
const UPLOAD_CONCURRENCY = 4;
|
|
9
|
+
const MIME_TYPES = {
|
|
10
|
+
".aac": "audio/aac",
|
|
11
|
+
".avi": "video/x-msvideo",
|
|
12
|
+
".avif": "image/avif",
|
|
13
|
+
".css": "text/css; charset=utf-8",
|
|
14
|
+
".csv": "text/csv; charset=utf-8",
|
|
15
|
+
".gif": "image/gif",
|
|
16
|
+
".htm": "text/html; charset=utf-8",
|
|
17
|
+
".html": "text/html; charset=utf-8",
|
|
18
|
+
".ico": "image/x-icon",
|
|
19
|
+
".jpeg": "image/jpeg",
|
|
20
|
+
".jpg": "image/jpeg",
|
|
21
|
+
".js": "text/javascript; charset=utf-8",
|
|
22
|
+
".json": "application/json; charset=utf-8",
|
|
23
|
+
".m4a": "audio/mp4",
|
|
24
|
+
".md": "text/markdown; charset=utf-8",
|
|
25
|
+
".mov": "video/quicktime",
|
|
26
|
+
".mp3": "audio/mpeg",
|
|
27
|
+
".mp4": "video/mp4",
|
|
28
|
+
".ogg": "audio/ogg",
|
|
29
|
+
".ogv": "video/ogg",
|
|
30
|
+
".pdf": "application/pdf",
|
|
31
|
+
".png": "image/png",
|
|
32
|
+
".svg": "image/svg+xml",
|
|
33
|
+
".txt": "text/plain; charset=utf-8",
|
|
34
|
+
".wasm": "application/wasm",
|
|
35
|
+
".wav": "audio/wav",
|
|
36
|
+
".webm": "video/webm",
|
|
37
|
+
".webp": "image/webp",
|
|
38
|
+
".woff": "font/woff",
|
|
39
|
+
".woff2": "font/woff2",
|
|
40
|
+
".xml": "application/xml; charset=utf-8",
|
|
41
|
+
".zip": "application/zip",
|
|
42
|
+
};
|
|
43
|
+
function hasControlCharacters(value) {
|
|
44
|
+
return [...value].some((char) => {
|
|
45
|
+
const code = char.charCodeAt(0);
|
|
46
|
+
return code <= 0x1f || (code >= 0x7f && code <= 0x9f);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
function terminalText(value) {
|
|
50
|
+
return [...value].map((char) => {
|
|
51
|
+
const code = char.charCodeAt(0);
|
|
52
|
+
return code <= 0x1f || (code >= 0x7f && code <= 0x9f)
|
|
53
|
+
? `\\u${code.toString(16).padStart(4, "0")}`
|
|
54
|
+
: char;
|
|
55
|
+
}).join("");
|
|
56
|
+
}
|
|
57
|
+
function normalizePublicPath(input) {
|
|
58
|
+
const value = input.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
|
|
59
|
+
if (!value || value.startsWith("/") || hasControlCharacters(value)) {
|
|
60
|
+
return error("Invalid public path");
|
|
61
|
+
}
|
|
62
|
+
const parts = value.split("/");
|
|
63
|
+
if (parts.some((part) => !part || part === "." || part === "..")) {
|
|
64
|
+
return error("Invalid public path");
|
|
65
|
+
}
|
|
66
|
+
return parts.join("/");
|
|
67
|
+
}
|
|
68
|
+
function mimeTypeForPath(path) {
|
|
69
|
+
return MIME_TYPES[extname(path).toLowerCase()] ?? "application/octet-stream";
|
|
70
|
+
}
|
|
71
|
+
async function walkDirectory(root, directory, destination) {
|
|
72
|
+
const names = await readdir(directory);
|
|
73
|
+
names.sort((left, right) => left.localeCompare(right));
|
|
74
|
+
const files = [];
|
|
75
|
+
for (const name of names) {
|
|
76
|
+
const localPath = resolve(directory, name);
|
|
77
|
+
const info = await lstat(localPath);
|
|
78
|
+
if (info.isSymbolicLink())
|
|
79
|
+
return error("Symlinks are not supported", relative(root, localPath));
|
|
80
|
+
if (info.isDirectory()) {
|
|
81
|
+
files.push(...await walkDirectory(root, localPath, destination));
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (!info.isFile())
|
|
85
|
+
continue;
|
|
86
|
+
const nestedPath = relative(root, localPath).replace(/\\/g, "/");
|
|
87
|
+
const publicPath = normalizePublicPath(`${destination}/${nestedPath}`);
|
|
88
|
+
files.push({
|
|
89
|
+
id: randomUUID(),
|
|
90
|
+
localPath,
|
|
91
|
+
publicPath,
|
|
92
|
+
size: info.size,
|
|
93
|
+
mimeType: mimeTypeForPath(localPath),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return files;
|
|
97
|
+
}
|
|
98
|
+
export async function collectPublicUpload(source, destination) {
|
|
99
|
+
const localPath = resolve(source);
|
|
100
|
+
const info = await lstat(localPath).catch(() => null);
|
|
101
|
+
if (!info)
|
|
102
|
+
return error("Source not found", source);
|
|
103
|
+
if (info.isSymbolicLink())
|
|
104
|
+
return error("Symlinks are not supported", source);
|
|
105
|
+
if (info.isFile()) {
|
|
106
|
+
const publicPath = normalizePublicPath(destination
|
|
107
|
+
? destination.endsWith("/") ? `${destination}${basename(localPath)}` : destination
|
|
108
|
+
: basename(localPath));
|
|
109
|
+
return {
|
|
110
|
+
destination: publicPath,
|
|
111
|
+
entryPath: publicPath,
|
|
112
|
+
files: [{
|
|
113
|
+
id: randomUUID(),
|
|
114
|
+
localPath,
|
|
115
|
+
publicPath,
|
|
116
|
+
size: info.size,
|
|
117
|
+
mimeType: mimeTypeForPath(localPath),
|
|
118
|
+
}],
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
if (!info.isDirectory())
|
|
122
|
+
return error("Source must be a file or directory", source);
|
|
123
|
+
const target = normalizePublicPath(destination ?? basename(localPath));
|
|
124
|
+
const files = await walkDirectory(localPath, localPath, target);
|
|
125
|
+
if (files.length === 0)
|
|
126
|
+
return error("Directory contains no files", source);
|
|
127
|
+
const indexPath = `${target}/index.html`;
|
|
128
|
+
return {
|
|
129
|
+
files,
|
|
130
|
+
destination: `${target}/`,
|
|
131
|
+
entryPath: files.some((file) => file.publicPath === indexPath) ? indexPath : null,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
async function mapSettledWithConcurrency(items, concurrency, mapper) {
|
|
135
|
+
const errors = [];
|
|
136
|
+
let nextIndex = 0;
|
|
137
|
+
const workers = Array.from({ length: Math.min(concurrency, items.length) }, async () => {
|
|
138
|
+
while (nextIndex < items.length) {
|
|
139
|
+
const index = nextIndex++;
|
|
140
|
+
try {
|
|
141
|
+
await mapper(items[index]);
|
|
142
|
+
}
|
|
143
|
+
catch (cause) {
|
|
144
|
+
errors.push(cause instanceof Error ? cause : new Error(String(cause)));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
await Promise.all(workers);
|
|
149
|
+
return errors;
|
|
150
|
+
}
|
|
151
|
+
async function putPublicFile(file, plan, fetchImpl) {
|
|
152
|
+
const response = await fetchImpl(plan.uploadUrl, {
|
|
153
|
+
method: "PUT",
|
|
154
|
+
headers: plan.headers,
|
|
155
|
+
body: createReadStream(file.localPath),
|
|
156
|
+
duplex: "half",
|
|
157
|
+
});
|
|
158
|
+
if (response.ok)
|
|
159
|
+
return;
|
|
160
|
+
const detail = await response.text().catch(() => "");
|
|
161
|
+
if (response.status === 409 || response.status === 412) {
|
|
162
|
+
throw new Error(`${file.publicPath} already exists. Use --overwrite.`);
|
|
163
|
+
}
|
|
164
|
+
throw new Error(`Failed to upload ${file.publicPath}: HTTP ${response.status}${detail ? ` — ${detail}` : ""}`);
|
|
165
|
+
}
|
|
166
|
+
function uploadFailure(errors) {
|
|
167
|
+
if (errors.length === 1)
|
|
168
|
+
return errors[0];
|
|
169
|
+
const first = errors[0]?.message ?? "Upload failed";
|
|
170
|
+
return new Error(`${first} (${errors.length} files failed)`);
|
|
171
|
+
}
|
|
172
|
+
function publicUrlPrefix(destination, entry) {
|
|
173
|
+
const encodedPath = entry.path.split("/").map(encodeURIComponent).join("/");
|
|
174
|
+
if (!entry.publicUrl.endsWith(encodedPath)) {
|
|
175
|
+
throw new Error(`Invalid public URL for ${entry.path}`);
|
|
176
|
+
}
|
|
177
|
+
const encodedDestination = destination.split("/").map(encodeURIComponent).join("/");
|
|
178
|
+
return `${entry.publicUrl.slice(0, -encodedPath.length)}${encodedDestination}`;
|
|
179
|
+
}
|
|
180
|
+
async function uploadPublic(command, source, destination, opts, deps) {
|
|
181
|
+
const client = deps.createClient?.() ?? createClient();
|
|
182
|
+
const spaceId = resolveSpace(command);
|
|
183
|
+
try {
|
|
184
|
+
const upload = await collectPublicUpload(source, destination);
|
|
185
|
+
const plan = await client.space(spaceId).publicFiles.createUpload({
|
|
186
|
+
overwrite: Boolean(opts.overwrite),
|
|
187
|
+
entries: upload.files.map((file) => ({
|
|
188
|
+
id: file.id,
|
|
189
|
+
relativePath: file.publicPath,
|
|
190
|
+
size: file.size,
|
|
191
|
+
mimeType: file.mimeType,
|
|
192
|
+
})),
|
|
193
|
+
});
|
|
194
|
+
if (opts.overwrite && !jsonRequested(opts)) {
|
|
195
|
+
process.stderr.write(`Overwrite enabled for ${upload.destination}\n`);
|
|
196
|
+
}
|
|
197
|
+
const filesById = new Map(upload.files.map((file) => [file.id, file]));
|
|
198
|
+
const plansByPath = new Map(plan.entries.map((entry) => [entry.path, entry]));
|
|
199
|
+
const entryPlan = upload.entryPath ? plansByPath.get(upload.entryPath) : undefined;
|
|
200
|
+
const assetPlans = entryPlan
|
|
201
|
+
? plan.entries.filter((entry) => entry.id !== entryPlan.id)
|
|
202
|
+
: plan.entries;
|
|
203
|
+
const uploadPlans = async (entries) => mapSettledWithConcurrency(entries, UPLOAD_CONCURRENCY, async (entry) => {
|
|
204
|
+
const file = filesById.get(entry.id);
|
|
205
|
+
if (!file)
|
|
206
|
+
throw new Error(`Missing local file for ${entry.path}`);
|
|
207
|
+
await putPublicFile(file, entry, deps.fetch ?? fetch);
|
|
208
|
+
});
|
|
209
|
+
const assetErrors = await uploadPlans(assetPlans);
|
|
210
|
+
if (assetErrors.length > 0)
|
|
211
|
+
throw uploadFailure(assetErrors);
|
|
212
|
+
if (entryPlan) {
|
|
213
|
+
const entryErrors = await uploadPlans([entryPlan]);
|
|
214
|
+
if (entryErrors.length > 0)
|
|
215
|
+
throw uploadFailure(entryErrors);
|
|
216
|
+
}
|
|
217
|
+
const firstPlan = plan.entries[0];
|
|
218
|
+
if (!firstPlan)
|
|
219
|
+
throw new Error("Upload plan contains no files");
|
|
220
|
+
if (jsonRequested(opts)) {
|
|
221
|
+
outJson({
|
|
222
|
+
destination: upload.destination,
|
|
223
|
+
urlPrefix: publicUrlPrefix("", firstPlan),
|
|
224
|
+
files: upload.files.map((file) => ({
|
|
225
|
+
path: file.publicPath,
|
|
226
|
+
size: file.size,
|
|
227
|
+
mimeType: file.mimeType,
|
|
228
|
+
})),
|
|
229
|
+
});
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (entryPlan) {
|
|
233
|
+
console.log(entryPlan.publicUrl);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
const fileLabel = upload.files.length === 1 ? "file" : "files";
|
|
237
|
+
console.log(`Uploaded ${upload.files.length} ${fileLabel} to ${upload.destination}`);
|
|
238
|
+
console.log(`URL prefix: ${publicUrlPrefix(upload.destination, firstPlan)}`);
|
|
239
|
+
}
|
|
240
|
+
catch (exception) {
|
|
241
|
+
handleHttp(exception);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
async function listPublic(command, path, opts, deps) {
|
|
245
|
+
const client = deps.createClient?.() ?? createClient();
|
|
246
|
+
try {
|
|
247
|
+
const publicFiles = client.space(resolveSpace(command)).publicFiles;
|
|
248
|
+
const entries = [];
|
|
249
|
+
let cursor;
|
|
250
|
+
do {
|
|
251
|
+
const page = await publicFiles.list(path ?? "", {
|
|
252
|
+
recursive: opts.recursive,
|
|
253
|
+
limit: 1000,
|
|
254
|
+
cursor,
|
|
255
|
+
});
|
|
256
|
+
if (jsonRequested(opts))
|
|
257
|
+
entries.push(...page.entries);
|
|
258
|
+
else
|
|
259
|
+
for (const entry of page.entries) {
|
|
260
|
+
console.log(`${terminalText(entry.name)}${entry.kind === "directory" ? "/" : ""}`);
|
|
261
|
+
}
|
|
262
|
+
cursor = page.nextCursor ?? undefined;
|
|
263
|
+
} while (cursor);
|
|
264
|
+
if (jsonRequested(opts))
|
|
265
|
+
return outJson({ path: path ?? "", entries, nextCursor: null });
|
|
266
|
+
}
|
|
267
|
+
catch (exception) {
|
|
268
|
+
handleHttp(exception);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
async function printPublicUrl(command, path, deps) {
|
|
272
|
+
const client = deps.createClient?.() ?? createClient();
|
|
273
|
+
try {
|
|
274
|
+
const result = await client.space(resolveSpace(command)).publicFiles.url(path);
|
|
275
|
+
console.log(result.url);
|
|
276
|
+
}
|
|
277
|
+
catch (exception) {
|
|
278
|
+
handleHttp(exception);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
export function registerPublic(program, deps = {}) {
|
|
282
|
+
const publicCommand = program
|
|
283
|
+
.command("public")
|
|
284
|
+
.description("Upload and manage public Space files");
|
|
285
|
+
publicCommand
|
|
286
|
+
.command("upload <source> [destination]")
|
|
287
|
+
.description("Upload a file or directory")
|
|
288
|
+
.option("--overwrite", "Replace existing public files")
|
|
289
|
+
.action((source, destination, opts, command) => uploadPublic(command, source, destination, opts, deps));
|
|
290
|
+
publicCommand
|
|
291
|
+
.command("ls [path]")
|
|
292
|
+
.description("List public files")
|
|
293
|
+
.option("-r, --recursive", "List files recursively")
|
|
294
|
+
.action((path, opts, command) => listPublic(command, path, opts, deps));
|
|
295
|
+
publicCommand
|
|
296
|
+
.command("url <path>")
|
|
297
|
+
.description("Print a public file URL")
|
|
298
|
+
.action((path, _opts, command) => printPublicUrl(command, path, deps));
|
|
299
|
+
return publicCommand;
|
|
300
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isUuid, } from "@neta-art/cohub";
|
|
1
2
|
import { createClient } from "../client.js";
|
|
2
3
|
import { table, json as outJson, jsonRequested, error, handleHttp } from "../output.js";
|
|
3
4
|
const RESOURCE_TYPES = new Set([
|
|
@@ -22,7 +23,6 @@ const REFERENCE_KINDS = new Set([
|
|
|
22
23
|
]);
|
|
23
24
|
const DIRECTIONS = new Set(["out", "in", "both"]);
|
|
24
25
|
const GROUP_BYS = new Set(["kind", "targetType", "target", "sourceType", "day"]);
|
|
25
|
-
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
26
26
|
function parseSource(value) {
|
|
27
27
|
const idx = value.indexOf(":");
|
|
28
28
|
if (idx <= 0)
|
|
@@ -146,7 +146,7 @@ Examples:
|
|
|
146
146
|
.action(async (spaceId, opts) => {
|
|
147
147
|
const client = createClient();
|
|
148
148
|
try {
|
|
149
|
-
if (!
|
|
149
|
+
if (!isUuid(spaceId.trim()))
|
|
150
150
|
return error("Invalid space id");
|
|
151
151
|
const groupBy = (opts.groupBy ?? "kind");
|
|
152
152
|
if (!GROUP_BYS.has(groupBy))
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createClient } from "../client.js";
|
|
2
2
|
import { error, handleHttp, json as outJson, jsonRequested, ok, table } from "../output.js";
|
|
3
3
|
function referralUrl(code) {
|
|
4
|
-
const origin = process.env.COHUB_WEB_URL?.replace(/\/+$/, "") ?? "https://cohub.
|
|
4
|
+
const origin = process.env.COHUB_WEB_URL?.replace(/\/+$/, "") ?? "https://cohub.live";
|
|
5
5
|
return `${origin}/referrals/${code}`;
|
|
6
6
|
}
|
|
7
7
|
async function confirmRotate(opts) {
|
package/dist/commands/sandbox.js
CHANGED
|
@@ -9,7 +9,7 @@ import { error, json as outJson, jsonRequested, ok, spinner } from "../output.js
|
|
|
9
9
|
import { resolveSpace } from "../space.js";
|
|
10
10
|
import { ensureSandboxdBinary, SandboxdDownloadError } from "./sandboxd-binary.js";
|
|
11
11
|
// Derive the gateway relay control endpoint from the realtime websocket URL,
|
|
12
|
-
// e.g. wss://gateway.cohub.
|
|
12
|
+
// e.g. wss://gateway.cohub.live/ws -> wss://gateway.cohub.live/sandbox/relay.
|
|
13
13
|
const resolveRelayUrl = () => {
|
|
14
14
|
const explicit = process.env.COHUB_RELAY_URL?.trim();
|
|
15
15
|
if (explicit)
|
|
@@ -27,7 +27,7 @@ const confirm = async (question) => {
|
|
|
27
27
|
rl.close();
|
|
28
28
|
}
|
|
29
29
|
};
|
|
30
|
-
const webBaseUrl = () => resolveCohubEnvironment() === "prod" ? "https://cohub.
|
|
30
|
+
const webBaseUrl = () => resolveCohubEnvironment() === "prod" ? "https://cohub.live" : "https://dev.cohub.live";
|
|
31
31
|
export const resolveLocalSpaceName = (rootDir, requestedName) => requestedName?.trim() || basename(rootDir) || "local-space";
|
|
32
32
|
// Consent copy is deliberately explicit: a local sandbox runs agent-issued
|
|
33
33
|
// shell commands as the current OS user. File RPCs are fenced to the folder,
|
package/dist/commands/search.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
+
import { isUuid } from "@neta-art/cohub";
|
|
1
2
|
import { createClient } from "../client.js";
|
|
2
3
|
import { table, json as outJson, jsonRequested, error, handleHttp } from "../output.js";
|
|
3
4
|
const DEFAULT_LIMIT = 20;
|
|
4
5
|
const MAX_TITLE_LENGTH = 72;
|
|
5
6
|
const MAX_CONTEXT_LENGTH = 42;
|
|
6
7
|
const SEARCH_TYPES = new Set(["turn", "session", "space", "label"]);
|
|
7
|
-
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
8
8
|
function clampLimit(value) {
|
|
9
9
|
const parsed = Number(value ?? DEFAULT_LIMIT);
|
|
10
10
|
if (!Number.isFinite(parsed))
|
|
@@ -40,7 +40,7 @@ function parseTypes(value) {
|
|
|
40
40
|
function parseSearchInput(opts) {
|
|
41
41
|
const types = parseTypes(opts.types);
|
|
42
42
|
const spaceId = opts.spaceId?.trim();
|
|
43
|
-
if (spaceId && !
|
|
43
|
+
if (spaceId && !isUuid(spaceId))
|
|
44
44
|
throw new Error("Invalid space id");
|
|
45
45
|
return { types, spaceId: spaceId || undefined, labelRef: opts.labelRef?.trim() || undefined };
|
|
46
46
|
}
|
|
@@ -34,7 +34,7 @@ export function parseSpaceInvitationCreateOptions(options) {
|
|
|
34
34
|
return { role, ttlSeconds: days * 24 * 60 * 60, maxUses };
|
|
35
35
|
}
|
|
36
36
|
function invitationUrl(invitation) {
|
|
37
|
-
const origin = process.env.COHUB_WEB_URL?.replace(/\/+$/, "") ?? "https://cohub.
|
|
37
|
+
const origin = process.env.COHUB_WEB_URL?.replace(/\/+$/, "") ?? "https://cohub.live";
|
|
38
38
|
return `${origin}${buildSpaceInvitePath({
|
|
39
39
|
spaceId: invitation.spaceId,
|
|
40
40
|
ownerUsername: invitation.ownerUsername,
|
package/dist/commands/ui.js
CHANGED
|
@@ -72,7 +72,7 @@ resolved from request provenance. Nothing else can be targeted.
|
|
|
72
72
|
Examples:
|
|
73
73
|
cohub ui preview <work-id>
|
|
74
74
|
cohub ui preview alice/studio/launch
|
|
75
|
-
cohub ui preview https://cohub.
|
|
75
|
+
cohub ui preview https://cohub.live/alice/studio/w/launch?view=timeline
|
|
76
76
|
cohub ui preview <work-id> --call selection.get
|
|
77
77
|
cohub ui preview <work-id> --call board.focus --data '{"nodeId":"n1"}'
|
|
78
78
|
`);
|
package/dist/commands/works.js
CHANGED
|
@@ -87,6 +87,15 @@ function printWorkUrls(result) {
|
|
|
87
87
|
if (lines.length)
|
|
88
88
|
console.log(`\n${lines.join("\n")}`);
|
|
89
89
|
}
|
|
90
|
+
function promotionUrl(publicUrl, promotion) {
|
|
91
|
+
if (!publicUrl)
|
|
92
|
+
return null;
|
|
93
|
+
const url = new URL(publicUrl);
|
|
94
|
+
url.searchParams.set("cohub_campaign", promotion.id);
|
|
95
|
+
for (const [key, value] of Object.entries(promotion.parameters))
|
|
96
|
+
url.searchParams.set(key, value);
|
|
97
|
+
return url.toString();
|
|
98
|
+
}
|
|
90
99
|
function printWorkStats(stats) {
|
|
91
100
|
table([stats.summary], [
|
|
92
101
|
{ key: "totalViews", label: "Total" },
|
|
@@ -424,6 +433,111 @@ export function registerWorks(program) {
|
|
|
424
433
|
handleHttp(e);
|
|
425
434
|
}
|
|
426
435
|
});
|
|
436
|
+
const promotionsCmd = worksCmd.command("promotions").description("Work promotion links and analytics");
|
|
437
|
+
promotionsCmd
|
|
438
|
+
.command("list <work>")
|
|
439
|
+
.alias("ls")
|
|
440
|
+
.description("List promotion links")
|
|
441
|
+
.option("--json", "Output as JSON")
|
|
442
|
+
.action(async (workRef, opts) => {
|
|
443
|
+
const client = createClient();
|
|
444
|
+
try {
|
|
445
|
+
const detail = await getWorkByRef(client, workRef);
|
|
446
|
+
const result = await client.works.listPromotions(detail.work.id);
|
|
447
|
+
const promotions = result.promotions.map((promotion) => ({
|
|
448
|
+
...promotion,
|
|
449
|
+
url: promotionUrl(detail.publicUrl, promotion),
|
|
450
|
+
}));
|
|
451
|
+
if (jsonRequested(opts))
|
|
452
|
+
return outJson({ ...result, promotions });
|
|
453
|
+
table(promotions, [
|
|
454
|
+
{ key: "name", label: "Name" },
|
|
455
|
+
{ key: "provider", label: "Provider" },
|
|
456
|
+
{ key: "id", label: "ID" },
|
|
457
|
+
{ key: "url", label: "URL" },
|
|
458
|
+
]);
|
|
459
|
+
}
|
|
460
|
+
catch (e) {
|
|
461
|
+
handleHttp(e);
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
promotionsCmd
|
|
465
|
+
.command("create <work>")
|
|
466
|
+
.description("Create an immutable promotion link")
|
|
467
|
+
.requiredOption("--name <name>", "Promotion name")
|
|
468
|
+
.option("--provider <provider>", "Promotion provider", "generic")
|
|
469
|
+
.option("--utm-id <value>", "UTM campaign ID")
|
|
470
|
+
.option("--utm-source <value>", "UTM source")
|
|
471
|
+
.option("--utm-medium <value>", "UTM medium")
|
|
472
|
+
.option("--utm-campaign <value>", "UTM campaign")
|
|
473
|
+
.option("--utm-term <value>", "UTM term")
|
|
474
|
+
.option("--utm-content <value>", "UTM content")
|
|
475
|
+
.option("--json", "Output as JSON")
|
|
476
|
+
.action(async (workRef, opts) => {
|
|
477
|
+
const client = createClient();
|
|
478
|
+
try {
|
|
479
|
+
const detail = await getWorkByRef(client, workRef);
|
|
480
|
+
const parameters = Object.fromEntries(Object.entries({
|
|
481
|
+
utm_id: opts.utmId,
|
|
482
|
+
utm_source: opts.utmSource,
|
|
483
|
+
utm_medium: opts.utmMedium,
|
|
484
|
+
utm_campaign: opts.utmCampaign,
|
|
485
|
+
utm_term: opts.utmTerm,
|
|
486
|
+
utm_content: opts.utmContent,
|
|
487
|
+
}).filter((entry) => typeof entry[1] === "string"));
|
|
488
|
+
const result = await client.works.createPromotion(detail.work.id, {
|
|
489
|
+
name: opts.name,
|
|
490
|
+
provider: opts.provider,
|
|
491
|
+
parameters,
|
|
492
|
+
});
|
|
493
|
+
const url = promotionUrl(detail.publicUrl, result.promotion);
|
|
494
|
+
if (jsonRequested(opts))
|
|
495
|
+
return outJson({ ...result, url });
|
|
496
|
+
ok("Promotion created");
|
|
497
|
+
table([{ ...result.promotion, url }], [
|
|
498
|
+
{ key: "name", label: "Name" },
|
|
499
|
+
{ key: "provider", label: "Provider" },
|
|
500
|
+
{ key: "id", label: "ID" },
|
|
501
|
+
{ key: "url", label: "URL" },
|
|
502
|
+
]);
|
|
503
|
+
}
|
|
504
|
+
catch (e) {
|
|
505
|
+
handleHttp(e);
|
|
506
|
+
}
|
|
507
|
+
});
|
|
508
|
+
promotionsCmd
|
|
509
|
+
.command("stats <work> <promotionId>")
|
|
510
|
+
.description("Show promotion analytics")
|
|
511
|
+
.option("--json", "Output as JSON")
|
|
512
|
+
.action(async (workRef, promotionId, opts) => {
|
|
513
|
+
const client = createClient();
|
|
514
|
+
try {
|
|
515
|
+
const detail = await getWorkByRef(client, workRef);
|
|
516
|
+
const result = await client.works.getPromotionStats(detail.work.id, promotionId);
|
|
517
|
+
if (jsonRequested(opts))
|
|
518
|
+
return outJson(result);
|
|
519
|
+
table([{
|
|
520
|
+
name: result.promotion.name,
|
|
521
|
+
landing: result.summary.landing,
|
|
522
|
+
ready: result.summary.ready,
|
|
523
|
+
registered: result.summary.registrationCompleted,
|
|
524
|
+
paywall: result.summary.paywallViewed,
|
|
525
|
+
checkout: result.summary.checkoutStarted,
|
|
526
|
+
readyRate: `${(result.summary.readyRate * 100).toFixed(1)}%`,
|
|
527
|
+
}], [
|
|
528
|
+
{ key: "name", label: "Name" },
|
|
529
|
+
{ key: "landing", label: "Landing" },
|
|
530
|
+
{ key: "ready", label: "Ready" },
|
|
531
|
+
{ key: "registered", label: "Registered" },
|
|
532
|
+
{ key: "paywall", label: "Paywall" },
|
|
533
|
+
{ key: "checkout", label: "Checkout" },
|
|
534
|
+
{ key: "readyRate", label: "Ready rate" },
|
|
535
|
+
]);
|
|
536
|
+
}
|
|
537
|
+
catch (e) {
|
|
538
|
+
handleHttp(e);
|
|
539
|
+
}
|
|
540
|
+
});
|
|
427
541
|
registerWorkCommerce(worksCmd);
|
|
428
542
|
worksCmd
|
|
429
543
|
.command("rm <id>")
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,7 @@ import { registerMe } from "./commands/me.js";
|
|
|
10
10
|
import { registerModels } from "./commands/models.js";
|
|
11
11
|
import { registerProfile } from "./commands/profile.js";
|
|
12
12
|
import { registerPrompts } from "./commands/prompts.js";
|
|
13
|
+
import { registerPublic } from "./commands/public.js";
|
|
13
14
|
import { registerSkills } from "./commands/skills.js";
|
|
14
15
|
import { registerSearch } from "./commands/search.js";
|
|
15
16
|
import { registerReferences } from "./commands/references.js";
|
|
@@ -33,9 +34,9 @@ const program = new Command("cohub");
|
|
|
33
34
|
program
|
|
34
35
|
.name("cohub")
|
|
35
36
|
.summary("Work with Cohub from your terminal")
|
|
36
|
-
.description("Send prompts,
|
|
37
|
+
.description("Send prompts, manage Space files, and publish public output.")
|
|
37
38
|
.version(VERSION, "-v, --version", "Show version")
|
|
38
|
-
.option("-s, --space <id>", "Target
|
|
39
|
+
.option("-s, --space <id>", "Target Space ID")
|
|
39
40
|
.option("--json", "Print machine-readable JSON when supported")
|
|
40
41
|
.helpOption("-h, --help", "Show help")
|
|
41
42
|
.addHelpText("after", `
|
|
@@ -53,6 +54,7 @@ Common commands:
|
|
|
53
54
|
cohub -s <space-id> spaces turns ls --author others
|
|
54
55
|
cohub -s <space-id> spaces sessions turns ls <session-id>
|
|
55
56
|
cohub -s <space-id> spaces files ls
|
|
57
|
+
cohub -s <space-id> public upload ./dist demo
|
|
56
58
|
cohub -s <space-id> works publish demo --file dist/index.html
|
|
57
59
|
cohub ui preview <work-id> --call selection.get
|
|
58
60
|
cohub -s <space-id> spaces commerce products list
|
|
@@ -75,6 +77,7 @@ registerChannels(program);
|
|
|
75
77
|
registerGenerations(program);
|
|
76
78
|
registerModels(program);
|
|
77
79
|
registerPrompts(program);
|
|
80
|
+
registerPublic(program);
|
|
78
81
|
registerSkills(program);
|
|
79
82
|
registerSearch(program);
|
|
80
83
|
registerReferences(program);
|
package/dist/output.js
CHANGED
|
@@ -112,11 +112,22 @@ function errorPresentationFromHttpError(e) {
|
|
|
112
112
|
return null;
|
|
113
113
|
}
|
|
114
114
|
export function handleHttp(e) {
|
|
115
|
+
const status = e.status;
|
|
116
|
+
const body = e.body;
|
|
117
|
+
if (jsonRequested()) {
|
|
118
|
+
const payload = body && typeof body === "object"
|
|
119
|
+
? body
|
|
120
|
+
: {
|
|
121
|
+
code: e.code ?? "CLI_ERROR",
|
|
122
|
+
message: e instanceof Error ? e.message : String(e),
|
|
123
|
+
...(status ? { status } : {}),
|
|
124
|
+
};
|
|
125
|
+
process.stderr.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
115
128
|
if (e instanceof Error && e.name === "AuthRequiredError") {
|
|
116
129
|
return error("not authenticated", "run `cohub auth login`");
|
|
117
130
|
}
|
|
118
|
-
const status = e.status;
|
|
119
|
-
const body = e.body;
|
|
120
131
|
if (status === 402) {
|
|
121
132
|
const conversion = extractBillingPayload(body)?.conversion;
|
|
122
133
|
if (conversion) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neta-art/cohub-cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.10.1",
|
|
4
4
|
"description": "CLI for Cohub — spaces, sessions, and agent collaboration.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"commander": "^15.0.0",
|
|
20
20
|
"pixi.js": "^8.19.0",
|
|
21
21
|
"sharp": "^0.35.3",
|
|
22
|
-
"@neta-art/cohub": "5.
|
|
22
|
+
"@neta-art/cohub": "5.8.1"
|
|
23
23
|
},
|
|
24
24
|
"publishConfig": {
|
|
25
25
|
"access": "public"
|