@neta-art/cohub-cli 1.3.0 → 1.4.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/dist/commands/search.js +35 -2
- package/dist/commands/spaces.js +96 -0
- package/package.json +4 -4
package/dist/commands/search.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { createClient } from "../client.js";
|
|
2
|
-
import { table, json as outJson, handleHttp } from "../output.js";
|
|
2
|
+
import { table, json as outJson, error, handleHttp } from "../output.js";
|
|
3
3
|
const DEFAULT_LIMIT = 20;
|
|
4
4
|
const MAX_TITLE_LENGTH = 72;
|
|
5
5
|
const MAX_CONTEXT_LENGTH = 42;
|
|
6
|
+
const SEARCH_TYPES = new Set(["turn", "session", "space"]);
|
|
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;
|
|
6
8
|
function clampLimit(value) {
|
|
7
9
|
const parsed = Number(value ?? DEFAULT_LIMIT);
|
|
8
10
|
if (!Number.isFinite(parsed))
|
|
@@ -22,6 +24,25 @@ function contextFor(item) {
|
|
|
22
24
|
return item.spaceName ?? "";
|
|
23
25
|
return item.sessionTitle || item.spaceName || "";
|
|
24
26
|
}
|
|
27
|
+
function parseTypes(value) {
|
|
28
|
+
const types = value
|
|
29
|
+
?.split(",")
|
|
30
|
+
.map((type) => type.trim())
|
|
31
|
+
.filter(Boolean);
|
|
32
|
+
if (!types?.length)
|
|
33
|
+
return undefined;
|
|
34
|
+
const invalidType = types.find((type) => !SEARCH_TYPES.has(type));
|
|
35
|
+
if (invalidType)
|
|
36
|
+
throw new Error(`Invalid search type: ${invalidType}`);
|
|
37
|
+
return [...new Set(types)];
|
|
38
|
+
}
|
|
39
|
+
function parseSearchInput(opts) {
|
|
40
|
+
const types = parseTypes(opts.types);
|
|
41
|
+
const spaceId = opts.spaceId?.trim();
|
|
42
|
+
if (spaceId && !UUID_PATTERN.test(spaceId))
|
|
43
|
+
throw new Error("Invalid space id");
|
|
44
|
+
return { types, spaceId: spaceId || undefined };
|
|
45
|
+
}
|
|
25
46
|
function rowsFor(items) {
|
|
26
47
|
return items.map((item) => ({
|
|
27
48
|
type: item.type,
|
|
@@ -38,18 +59,27 @@ export function registerSearch(program) {
|
|
|
38
59
|
.description("Search spaces, chats, and turns")
|
|
39
60
|
.argument("<query>", "Search query")
|
|
40
61
|
.option("--limit <n>", "Maximum results, 1-50", String(DEFAULT_LIMIT))
|
|
62
|
+
.option("--types <types>", "Comma-separated result types: turn,session,space")
|
|
63
|
+
.option("--space-id <id>", "Limit search to a space")
|
|
41
64
|
.option("--json", "Output as JSON")
|
|
42
65
|
.addHelpText("after", `
|
|
43
66
|
|
|
44
67
|
Examples:
|
|
45
68
|
cohub search "release notes"
|
|
46
69
|
cohub search "failing tests" --limit 10
|
|
70
|
+
cohub search "bug" --types turn,session --space-id <spaceId>
|
|
47
71
|
cohub search "design review" --json
|
|
48
72
|
`)
|
|
49
73
|
.action(async (query, opts) => {
|
|
50
74
|
const client = createClient();
|
|
51
75
|
try {
|
|
52
|
-
const
|
|
76
|
+
const input = parseSearchInput(opts);
|
|
77
|
+
const result = await client.search.query({
|
|
78
|
+
q: query,
|
|
79
|
+
limit: clampLimit(opts.limit),
|
|
80
|
+
types: input.types,
|
|
81
|
+
spaceId: input.spaceId,
|
|
82
|
+
});
|
|
53
83
|
if (opts.json)
|
|
54
84
|
return outJson(result);
|
|
55
85
|
if (result.degraded) {
|
|
@@ -65,6 +95,9 @@ Examples:
|
|
|
65
95
|
]);
|
|
66
96
|
}
|
|
67
97
|
catch (e) {
|
|
98
|
+
if (e instanceof Error && (e.message === "Invalid space id" || e.message.startsWith("Invalid search type:"))) {
|
|
99
|
+
return error(e.message);
|
|
100
|
+
}
|
|
68
101
|
handleHttp(e);
|
|
69
102
|
}
|
|
70
103
|
});
|
package/dist/commands/spaces.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { createReadStream } from "node:fs";
|
|
3
|
+
import { readdir, stat } from "node:fs/promises";
|
|
4
|
+
import { basename, dirname, relative, resolve, sep } from "node:path";
|
|
1
5
|
import { createClient } from "../client.js";
|
|
2
6
|
import { table, json as outJson, ok, error, handleHttp } from "../output.js";
|
|
3
7
|
function requireSpace(program) {
|
|
@@ -10,6 +14,87 @@ function requireSpace(program) {
|
|
|
10
14
|
}
|
|
11
15
|
return error("Missing required option", "Add -s, --space <id> to target a space");
|
|
12
16
|
}
|
|
17
|
+
const slashPath = (value) => value.split(sep).join("/");
|
|
18
|
+
const walkUploadPath = async (input, root, prefix = "") => {
|
|
19
|
+
const localPath = resolve(input);
|
|
20
|
+
const info = await stat(localPath);
|
|
21
|
+
const name = basename(localPath);
|
|
22
|
+
const relativePath = slashPath(prefix ? `${prefix}/${name}` : relative(root, localPath) || name);
|
|
23
|
+
if (info.isDirectory()) {
|
|
24
|
+
const children = await readdir(localPath);
|
|
25
|
+
const nested = await Promise.all(children.map((child) => walkUploadPath(resolve(localPath, child), root, relativePath)));
|
|
26
|
+
return nested.flat();
|
|
27
|
+
}
|
|
28
|
+
if (!info.isFile())
|
|
29
|
+
return [];
|
|
30
|
+
return [{
|
|
31
|
+
id: randomUploadEntryId(),
|
|
32
|
+
localPath,
|
|
33
|
+
relativePath,
|
|
34
|
+
name,
|
|
35
|
+
size: info.size,
|
|
36
|
+
mimeType: null,
|
|
37
|
+
}];
|
|
38
|
+
};
|
|
39
|
+
const randomUploadEntryId = () => randomUUID();
|
|
40
|
+
async function collectUploadFiles(paths) {
|
|
41
|
+
if (paths.length === 0)
|
|
42
|
+
return error("No files provided", "Pass one or more local files or directories.");
|
|
43
|
+
const roots = paths.map((path) => {
|
|
44
|
+
const resolved = resolve(path);
|
|
45
|
+
return dirname(resolved);
|
|
46
|
+
});
|
|
47
|
+
const nested = await Promise.all(paths.map((path, index) => walkUploadPath(path, roots[index] ?? process.cwd())));
|
|
48
|
+
const files = nested.flat();
|
|
49
|
+
if (files.length === 0)
|
|
50
|
+
return error("No regular files found");
|
|
51
|
+
return files;
|
|
52
|
+
}
|
|
53
|
+
async function putUploadEntry(entry, uploadUrl, headers) {
|
|
54
|
+
const response = await fetch(uploadUrl, {
|
|
55
|
+
method: "PUT",
|
|
56
|
+
headers,
|
|
57
|
+
body: createReadStream(entry.localPath),
|
|
58
|
+
duplex: "half",
|
|
59
|
+
});
|
|
60
|
+
if (!response.ok) {
|
|
61
|
+
const detail = await response.text().catch(() => "");
|
|
62
|
+
throw new Error(`Failed to upload ${entry.relativePath}: HTTP ${response.status}${detail ? ` — ${detail}` : ""}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
async function uploadFiles(command, paths, opts) {
|
|
66
|
+
const spaceId = requireSpace(command);
|
|
67
|
+
const client = createClient();
|
|
68
|
+
try {
|
|
69
|
+
const files = await collectUploadFiles(paths);
|
|
70
|
+
const plan = await client.space(spaceId).files.createUpload({
|
|
71
|
+
targetDir: opts.dir,
|
|
72
|
+
entries: files.map((file) => ({
|
|
73
|
+
id: file.id,
|
|
74
|
+
name: file.name,
|
|
75
|
+
relativePath: file.relativePath,
|
|
76
|
+
size: file.size,
|
|
77
|
+
mimeType: file.mimeType,
|
|
78
|
+
})),
|
|
79
|
+
});
|
|
80
|
+
const byId = new Map(files.map((file) => [file.id, file]));
|
|
81
|
+
for (const entry of plan.entries) {
|
|
82
|
+
const file = byId.get(entry.id);
|
|
83
|
+
if (!file)
|
|
84
|
+
throw new Error(`Missing upload entry: ${entry.id}`);
|
|
85
|
+
await putUploadEntry(file, entry.uploadUrl, entry.headers);
|
|
86
|
+
}
|
|
87
|
+
const result = await client.space(spaceId).files.completeUpload(plan.uploadId, {
|
|
88
|
+
entries: plan.entries.map((entry) => ({ id: entry.id })),
|
|
89
|
+
});
|
|
90
|
+
if (opts.json)
|
|
91
|
+
return outJson({ ...result, uploadId: plan.uploadId, files: files.length });
|
|
92
|
+
ok(`Uploaded ${files.length} file${files.length === 1 ? "" : "s"} — taskRunId: ${result.taskRunId}`);
|
|
93
|
+
}
|
|
94
|
+
catch (e) {
|
|
95
|
+
handleHttp(e);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
13
98
|
async function readPromptContent(words) {
|
|
14
99
|
let content = words.join(" ");
|
|
15
100
|
if (!content && !process.stdin.isTTY) {
|
|
@@ -254,6 +339,11 @@ function registerFiles(spacesCmd) {
|
|
|
254
339
|
const client = createClient();
|
|
255
340
|
try {
|
|
256
341
|
const file = await client.space(spaceId).files.read(path);
|
|
342
|
+
if (!("content" in file))
|
|
343
|
+
return error("File is being prepared. Please retry shortly.");
|
|
344
|
+
if (file.delivery === "url" && file.url) {
|
|
345
|
+
console.log(`[CDN] ${file.url}`);
|
|
346
|
+
}
|
|
257
347
|
console.log(file.content);
|
|
258
348
|
}
|
|
259
349
|
catch (e) {
|
|
@@ -289,6 +379,12 @@ function registerFiles(spacesCmd) {
|
|
|
289
379
|
handleHttp(e);
|
|
290
380
|
}
|
|
291
381
|
});
|
|
382
|
+
filesCmd
|
|
383
|
+
.command("upload <paths...>")
|
|
384
|
+
.description("Upload local files or directories")
|
|
385
|
+
.option("--dir <path>", "Target directory in the space")
|
|
386
|
+
.option("--json", "Output as JSON")
|
|
387
|
+
.action((paths, opts) => uploadFiles(spacesCmd, paths, opts));
|
|
292
388
|
filesCmd
|
|
293
389
|
.command("mkdir <path>")
|
|
294
390
|
.description("Create a directory")
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neta-art/cohub-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.1",
|
|
4
4
|
"description": "CLI for Cohub — spaces, sessions, and agent collaboration.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
],
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"commander": "^13.1.0",
|
|
17
|
-
"@neta-art/cohub": "1.
|
|
17
|
+
"@neta-art/cohub": "1.10.1"
|
|
18
18
|
},
|
|
19
19
|
"publishConfig": {
|
|
20
20
|
"access": "public"
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"typescript": "^6.0.3"
|
|
25
25
|
},
|
|
26
26
|
"scripts": {
|
|
27
|
-
"build": "
|
|
28
|
-
"typecheck": "
|
|
27
|
+
"build": "tsgo -p tsconfig.build.json",
|
|
28
|
+
"typecheck": "tsgo -p tsconfig.json --noEmit"
|
|
29
29
|
}
|
|
30
30
|
}
|