@neta-art/cohub-cli 6.7.0 → 6.8.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.
@@ -0,0 +1,223 @@
1
+ function isHelpFlag(token) {
2
+ return token === "-h" || token === "--help";
3
+ }
4
+ function findChild(command, name) {
5
+ return command.commands.find((child) => {
6
+ if (child.name() === "help")
7
+ return false;
8
+ return child.name() === name || child.aliases().includes(name);
9
+ });
10
+ }
11
+ function hasSubcommands(command) {
12
+ return command.commands.some((child) => child.name() !== "help");
13
+ }
14
+ function knownOptions(command) {
15
+ const options = [];
16
+ let current = command;
17
+ while (current) {
18
+ options.push(...current.options);
19
+ current = current.parent;
20
+ }
21
+ return options;
22
+ }
23
+ function matchOption(command, token) {
24
+ if (!token.startsWith("-") || token === "-")
25
+ return undefined;
26
+ const eq = token.indexOf("=");
27
+ const inline = eq >= 0;
28
+ const flag = inline ? token.slice(0, eq) : token;
29
+ for (const option of knownOptions(command)) {
30
+ if (option.short === flag || option.long === flag)
31
+ return { option, inline };
32
+ }
33
+ return undefined;
34
+ }
35
+ function skipOption(command, argv, index) {
36
+ const token = argv[index] ?? "";
37
+ const matched = matchOption(command, token);
38
+ const next = index + 1;
39
+ if (matched?.inline)
40
+ return next;
41
+ const value = argv[next];
42
+ if (!value)
43
+ return next;
44
+ if (matched?.option.required)
45
+ return next + 1;
46
+ if (matched?.option.optional && !value.startsWith("-"))
47
+ return next + 1;
48
+ return next;
49
+ }
50
+ function remainderRequestsHelp(command, argv) {
51
+ let index = 0;
52
+ while (index < argv.length) {
53
+ const token = argv[index] ?? "";
54
+ if (token === "--")
55
+ break;
56
+ if (isHelpFlag(token))
57
+ return true;
58
+ if (token.startsWith("-")) {
59
+ index = skipOption(command, argv, index);
60
+ continue;
61
+ }
62
+ index += 1;
63
+ }
64
+ return false;
65
+ }
66
+ /** Match `run.ts`: Cohub flags first, then the rest is the shell command. */
67
+ function runRemainderRequestsHelp(argv) {
68
+ for (let index = 0; index < argv.length; index += 1) {
69
+ const token = argv[index] ?? "";
70
+ if (token === "--")
71
+ return false;
72
+ if (isHelpFlag(token))
73
+ return true;
74
+ if (token === "--async" || token === "--json")
75
+ continue;
76
+ if (token === "-c" || token === "--command") {
77
+ index += 1;
78
+ continue;
79
+ }
80
+ if (token.startsWith("--command="))
81
+ continue;
82
+ return false;
83
+ }
84
+ return false;
85
+ }
86
+ function walkCommandPath(program, argv) {
87
+ let command = program;
88
+ const path = [];
89
+ let helpRequested = false;
90
+ let index = 0;
91
+ while (index < argv.length) {
92
+ const token = argv[index] ?? "";
93
+ if (token === "--")
94
+ break;
95
+ if (isHelpFlag(token)) {
96
+ helpRequested = true;
97
+ index += 1;
98
+ continue;
99
+ }
100
+ if (token.startsWith("-")) {
101
+ index = skipOption(command, argv, index);
102
+ continue;
103
+ }
104
+ if (token === "help" && hasSubcommands(command)) {
105
+ helpRequested = true;
106
+ index += 1;
107
+ continue;
108
+ }
109
+ const child = findChild(command, token);
110
+ if (child) {
111
+ command = child;
112
+ path.push(child.name());
113
+ index += 1;
114
+ continue;
115
+ }
116
+ if (hasSubcommands(command)) {
117
+ const remainder = argv.slice(index + 1);
118
+ const helpAfter = token === "run" && path.length === 0
119
+ ? runRemainderRequestsHelp(remainder)
120
+ : remainderRequestsHelp(command, remainder);
121
+ return {
122
+ command,
123
+ path,
124
+ helpRequested: helpRequested || helpAfter,
125
+ unknown: token,
126
+ };
127
+ }
128
+ index += 1;
129
+ }
130
+ return { command, path, helpRequested, unknown: null };
131
+ }
132
+ function stripRootCommand(program, argv, name) {
133
+ let index = 0;
134
+ while (index < argv.length) {
135
+ const token = argv[index] ?? "";
136
+ if (token === "--")
137
+ return undefined;
138
+ if (isHelpFlag(token) || token === "help") {
139
+ index += 1;
140
+ continue;
141
+ }
142
+ if (token.startsWith("-")) {
143
+ index = skipOption(program, argv, index);
144
+ continue;
145
+ }
146
+ if (token !== name)
147
+ return undefined;
148
+ return [...argv.slice(0, index), ...argv.slice(index + 1)];
149
+ }
150
+ return undefined;
151
+ }
152
+ function editDistance(a, b) {
153
+ if (a === b)
154
+ return 0;
155
+ if (Math.abs(a.length - b.length) > 3)
156
+ return Math.max(a.length, b.length);
157
+ const rows = a.length + 1;
158
+ const cols = b.length + 1;
159
+ const previous = Array.from({ length: cols }, (_, index) => index);
160
+ const current = new Array(cols);
161
+ for (let i = 1; i < rows; i += 1) {
162
+ current[0] = i;
163
+ for (let j = 1; j < cols; j += 1) {
164
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
165
+ current[j] = Math.min((previous[j] ?? 0) + 1, (current[j - 1] ?? 0) + 1, (previous[j - 1] ?? 0) + cost);
166
+ }
167
+ for (let j = 0; j < cols; j += 1)
168
+ previous[j] = current[j] ?? 0;
169
+ }
170
+ return previous[b.length] ?? Math.max(a.length, b.length);
171
+ }
172
+ function suggestCommand(command, token) {
173
+ const names = [...new Set(command.commands.flatMap((child) => {
174
+ if (child.name() === "help" || child.name().length <= 1)
175
+ return [];
176
+ return [child.name(), ...child.aliases().filter((alias) => alias.length > 1)];
177
+ }))];
178
+ let best;
179
+ let bestDistance = 3;
180
+ for (const name of names) {
181
+ const distance = editDistance(token, name);
182
+ const length = Math.max(token.length, name.length);
183
+ if (distance >= bestDistance || (length - distance) / length <= 0.4)
184
+ continue;
185
+ bestDistance = distance;
186
+ best = name;
187
+ }
188
+ return best;
189
+ }
190
+ export function resolveHelpPath(program, argv) {
191
+ const walk = walkCommandPath(program, argv);
192
+ const cliAtRoot = walk.unknown === "cli" && walk.path.length === 0;
193
+ if (cliAtRoot) {
194
+ const stripped = stripRootCommand(program, argv, "cli");
195
+ return {
196
+ kind: "unknown",
197
+ token: "cli",
198
+ tryCommand: stripped && stripped.length > 0 ? stripped.join(" ") : undefined,
199
+ };
200
+ }
201
+ if (!walk.helpRequested)
202
+ return { kind: "passthrough" };
203
+ if (walk.unknown === "run" && walk.path.length === 0)
204
+ return { kind: "run-help" };
205
+ if (walk.unknown) {
206
+ return {
207
+ kind: "unknown",
208
+ token: walk.unknown,
209
+ suggestion: suggestCommand(walk.command, walk.unknown),
210
+ };
211
+ }
212
+ return { kind: "help", command: walk.command };
213
+ }
214
+ export function formatUnknownCommandError(result) {
215
+ const lines = [`error: unknown command '${result.token}'`];
216
+ if (result.tryCommand)
217
+ lines.push(`Try: cohub ${result.tryCommand}`);
218
+ else if (result.token === "cli")
219
+ lines.push(`cohub has no "cli" subcommand. See cohub --help`);
220
+ else if (result.suggestion)
221
+ lines.push(`(Did you mean ${result.suggestion}?)`);
222
+ return `${lines.join("\n")}\n`;
223
+ }
@@ -0,0 +1,25 @@
1
+ export declare class HttpPutError extends Error {
2
+ readonly status: number;
3
+ readonly body: string;
4
+ constructor(message: string, status: number, body?: string);
5
+ }
6
+ export type PutRetryOptions = {
7
+ attempts?: number;
8
+ delayMs?: number;
9
+ sleep?: (ms: number) => Promise<void>;
10
+ fetch?: typeof fetch;
11
+ };
12
+ export declare function putLocalFile(input: PutRetryOptions & {
13
+ url: string;
14
+ filePath: string;
15
+ size: number;
16
+ headers?: HeadersInit;
17
+ label: string;
18
+ }): Promise<void>;
19
+ export declare function putBytes(input: PutRetryOptions & {
20
+ url: string;
21
+ body: Blob;
22
+ size?: number;
23
+ headers?: HeadersInit;
24
+ label: string;
25
+ }): Promise<void>;
@@ -0,0 +1,132 @@
1
+ import { createReadStream } from "node:fs";
2
+ const DEFAULT_ATTEMPTS = 3;
3
+ const DEFAULT_DELAY_MS = 200;
4
+ const RETRYABLE_STATUS = new Set([408, 411, 425, 429, 500, 502, 503, 504]);
5
+ const RETRYABLE_CODES = new Set([
6
+ "BodyTimeoutError",
7
+ "ConnectTimeoutError",
8
+ "EAI_AGAIN",
9
+ "ECONNREFUSED",
10
+ "ECONNRESET",
11
+ "EHOSTUNREACH",
12
+ "ENETUNREACH",
13
+ "ENOTFOUND",
14
+ "EPIPE",
15
+ "ETIMEDOUT",
16
+ "HeadersTimeoutError",
17
+ "UND_ERR_BODY_TIMEOUT",
18
+ "UND_ERR_CONNECT",
19
+ "UND_ERR_CONNECT_TIMEOUT",
20
+ "UND_ERR_HEADERS_TIMEOUT",
21
+ "UND_ERR_SOCKET",
22
+ ]);
23
+ export class HttpPutError extends Error {
24
+ status;
25
+ body;
26
+ constructor(message, status, body = "") {
27
+ super(message);
28
+ this.name = "HttpPutError";
29
+ this.status = status;
30
+ this.body = body;
31
+ }
32
+ }
33
+ function withContentLength(headers, contentLength) {
34
+ const result = new Headers(headers);
35
+ if (contentLength !== undefined && !result.has("content-length")) {
36
+ result.set("content-length", String(contentLength));
37
+ }
38
+ return result;
39
+ }
40
+ function errorCode(error) {
41
+ if (!error || typeof error !== "object")
42
+ return undefined;
43
+ const record = error;
44
+ if (typeof record.code === "string")
45
+ return record.code;
46
+ if (typeof record.cause?.code === "string")
47
+ return record.cause.code;
48
+ if (typeof record.cause?.name === "string")
49
+ return record.cause.name;
50
+ if (typeof record.name === "string")
51
+ return record.name;
52
+ return undefined;
53
+ }
54
+ function isRetryableError(error) {
55
+ const code = errorCode(error);
56
+ return Boolean(code && RETRYABLE_CODES.has(code));
57
+ }
58
+ function isRetryableStatus(status) {
59
+ return RETRYABLE_STATUS.has(status);
60
+ }
61
+ function defaultSleep(ms) {
62
+ return new Promise((resolve) => setTimeout(resolve, ms));
63
+ }
64
+ function failureMessage(label, status, detail) {
65
+ return `Failed to upload ${label}: HTTP ${status}${detail ? ` — ${detail}` : ""}`;
66
+ }
67
+ async function putWithRetry(input) {
68
+ const attempts = input.attempts ?? DEFAULT_ATTEMPTS;
69
+ const delayMs = input.delayMs ?? DEFAULT_DELAY_MS;
70
+ const sleep = input.sleep ?? defaultSleep;
71
+ const fetchImpl = input.fetch ?? fetch;
72
+ let lastError;
73
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
74
+ try {
75
+ const response = await fetchImpl(input.url, {
76
+ method: "PUT",
77
+ headers: withContentLength(input.headers, input.contentLength),
78
+ body: input.body(),
79
+ ...(input.duplex ? { duplex: "half" } : {}),
80
+ });
81
+ if (response.ok)
82
+ return response;
83
+ const detail = await response.text().catch(() => "");
84
+ const error = new HttpPutError(failureMessage(input.label, response.status, detail), response.status, detail);
85
+ if (attempt < attempts - 1 && isRetryableStatus(response.status)) {
86
+ lastError = error;
87
+ await sleep(delayMs * 2 ** attempt);
88
+ continue;
89
+ }
90
+ throw error;
91
+ }
92
+ catch (error) {
93
+ if (error instanceof HttpPutError)
94
+ throw error;
95
+ lastError = error;
96
+ if (attempt < attempts - 1 && isRetryableError(error)) {
97
+ await sleep(delayMs * 2 ** attempt);
98
+ continue;
99
+ }
100
+ throw error;
101
+ }
102
+ }
103
+ throw lastError;
104
+ }
105
+ export async function putLocalFile(input) {
106
+ await putWithRetry({
107
+ url: input.url,
108
+ body: () => createReadStream(input.filePath),
109
+ headers: input.headers,
110
+ contentLength: input.size,
111
+ duplex: true,
112
+ label: input.label,
113
+ attempts: input.attempts,
114
+ delayMs: input.delayMs,
115
+ sleep: input.sleep,
116
+ fetch: input.fetch,
117
+ });
118
+ }
119
+ export async function putBytes(input) {
120
+ const body = input.body;
121
+ await putWithRetry({
122
+ url: input.url,
123
+ body: () => body,
124
+ headers: input.headers,
125
+ contentLength: input.size,
126
+ label: input.label,
127
+ attempts: input.attempts,
128
+ delayMs: input.delayMs,
129
+ sleep: input.sleep,
130
+ fetch: input.fetch,
131
+ });
132
+ }
package/dist/index.js CHANGED
@@ -16,11 +16,12 @@ import { registerSearch } from "./commands/search.js";
16
16
  import { registerReferences } from "./commands/references.js";
17
17
  import { registerReferrals } from "./commands/referrals.js";
18
18
  import { registerPrompt, registerSpaces } from "./commands/spaces.js";
19
- import { maybeHandleRunCommand } from "./commands/run.js";
19
+ import { maybeHandleRunCommand, printRunHelp } from "./commands/run.js";
20
20
  import { registerSandbox } from "./commands/sandbox.js";
21
21
  import { registerTasks } from "./commands/tasks.js";
22
22
  import { registerDesktop, registerLegacyUi } from "./commands/desktop.js";
23
23
  import { registerApps } from "./commands/apps.js";
24
+ import { formatUnknownCommandError, resolveHelpPath } from "./help-path.js";
24
25
  const VERSION = (() => {
25
26
  try {
26
27
  const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
@@ -36,18 +37,22 @@ program
36
37
  .summary("Work with Cohub from your terminal")
37
38
  .description("Send prompts, manage Space files, and publish public output.")
38
39
  .version(VERSION, "-v, --version", "Show version")
39
- .option("-s, --space <id>", "Target Space ID")
40
+ .option("-s, --space <id>", "Target Space ID (defaults to your Home space)")
40
41
  .option("--json", "Print machine-readable JSON when supported")
41
42
  .helpOption("-h, --help", "Show help")
42
43
  .addHelpText("after", `
43
44
 
45
+ Help:
46
+ cohub apps publish --help
47
+ cohub help apps publish
48
+
44
49
  Common commands:
45
50
  cohub auth login
46
51
  cohub profile avatar ./avatar.png
47
52
  cohub spaces ls
48
- cohub -s <space-id> prompt "Fix the failing tests"
49
- cohub -s <space-id> completion "Summarize AGENTS.md" --system-prompt AGENTS.md --stream
50
- cohub -s <space-id> run -- git status
53
+ cohub prompt "Fix the failing tests"
54
+ cohub completion "Summarize AGENTS.md" --system-prompt AGENTS.md --stream
55
+ cohub run -- git status
51
56
  cohub sandbox up ./my-project
52
57
  cohub search "release notes"
53
58
  cohub -s <space-id> boards inspect <board-id>
@@ -62,9 +67,14 @@ Common commands:
62
67
  cohub models ls --model-type multimodal
63
68
  cohub generate "A calm lake at sunrise" --model <model> --output lake.png
64
69
 
70
+ Target space:
71
+ -s <space-id>, then COHUB_SPACE_ID, then your Home space
72
+
65
73
  Environment:
74
+ COHUB_SPACE_ID Target Space ID when -s is omitted
66
75
  COHUB_EXECUTION_TOKEN Use this token instead of the stored Logto session
67
76
  ENV=dev Use the development Cohub environment
77
+ HTTPS_PROXY Honored for API and uploads (also HTTP_PROXY, NO_PROXY)
68
78
  `);
69
79
  registerAuth(program);
70
80
  registerBoards(program);
@@ -88,7 +98,20 @@ registerApps(program);
88
98
  registerDesktop(program);
89
99
  registerLegacyUi(program);
90
100
  const argv = process.argv.slice(2);
91
- if (await maybeHandleRunCommand(argv)) {
101
+ const help = resolveHelpPath(program, argv);
102
+ if (help.kind === "help") {
103
+ help.command.outputHelp();
104
+ }
105
+ else if (help.kind === "run-help") {
106
+ printRunHelp();
107
+ }
108
+ else if (help.kind === "unknown") {
109
+ process.stderr.write(formatUnknownCommandError(help));
110
+ process.exit(1);
111
+ }
112
+ else if (await maybeHandleRunCommand(argv)) {
92
113
  process.exit();
93
114
  }
94
- program.parse();
115
+ else {
116
+ program.parse();
117
+ }
@@ -1,2 +1,4 @@
1
1
  export declare function exitCodeForChild(code: number | null, signal: NodeJS.Signals | null): number;
2
- export declare function relaunchCli(entrypoint: string, argv: string[]): Promise<number>;
2
+ export declare function relaunchCli(entrypoint: string, argv: string[], options?: {
3
+ execArgv?: readonly string[];
4
+ }): Promise<number>;
package/dist/launcher.js CHANGED
@@ -8,9 +8,9 @@ export function exitCodeForChild(code, signal) {
8
8
  return 1;
9
9
  return 128 + (constants.signals[signal] ?? 1);
10
10
  }
11
- export function relaunchCli(entrypoint, argv) {
11
+ export function relaunchCli(entrypoint, argv, options) {
12
12
  return new Promise((resolve, reject) => {
13
- const child = spawn(process.execPath, [entrypoint, ...argv], {
13
+ const child = spawn(process.execPath, [...(options?.execArgv ?? []), entrypoint, ...argv], {
14
14
  env: process.env,
15
15
  stdio: "inherit",
16
16
  });
package/dist/space.d.ts CHANGED
@@ -1,2 +1,32 @@
1
1
  import type { Command } from "commander";
2
- export declare function resolveSpace(program: Command): string;
2
+ /**
3
+ * Cache key aligned with auth: execution token is exclusive (same as
4
+ * `resolveAccessToken`) and never falls back to a local Logto session.
5
+ * Execution grants identify the actor as `actorUserId`, not `sub`.
6
+ */
7
+ export declare function identityKeyFrom(input: {
8
+ env: string;
9
+ executionToken?: string | null;
10
+ idToken?: string | null;
11
+ accessToken?: string | null;
12
+ }): string | null;
13
+ /** Exported for tests; production always uses `CACHE_PATH`. */
14
+ export declare function readDefaultSpaceCache(path: string, key: string, now?: number): string | null;
15
+ export declare function clearDefaultSpaceCache(): void;
16
+ /** Explicit target from `-s/--space` (any ancestor) or `COHUB_SPACE_ID`, else null. */
17
+ export declare function explicitSpace(program: Command): string | null;
18
+ /**
19
+ * Resolve the user's home space when no target is given. Cached locally per
20
+ * identity so repeated invocations skip the network entirely, and memoized
21
+ * in-process so preAction hooks and actions share a single lookup.
22
+ * Network and auth failures propagate so callers can report them faithfully.
23
+ */
24
+ export declare function resolveDefaultSpace(): Promise<string | null>;
25
+ /** Shared exit for commands that need a space but resolved none. */
26
+ export declare function missingSpaceError(): never;
27
+ /**
28
+ * Target space for a command: explicit `-s`/`COHUB_SPACE_ID` first, then the
29
+ * user's home space. Exits with guidance when neither is available; request
30
+ * failures go through the shared HTTP error handler.
31
+ */
32
+ export declare function resolveSpace(program: Command): Promise<string>;
package/dist/space.js CHANGED
@@ -1,5 +1,85 @@
1
- import { error } from "./output.js";
2
- export function resolveSpace(program) {
1
+ import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { resolveCohubEnvironment } from "@neta-art/cohub";
5
+ import { readAuthSession } from "./auth.js";
6
+ import { createClient } from "./client.js";
7
+ import { error, handleHttp } from "./output.js";
8
+ const CONFIG_DIR = join(homedir(), ".config", "cohub");
9
+ const CACHE_PATH = join(CONFIG_DIR, "default-space.json");
10
+ /** Home space is stable; a one-day TTL bounds how long a stale hit survives. */
11
+ const CACHE_TTL_MS = 86_400_000;
12
+ function jwtClaim(token, key) {
13
+ const payload = token?.split(".")[1];
14
+ if (!payload)
15
+ return null;
16
+ try {
17
+ const parsed = JSON.parse(Buffer.from(payload, "base64url").toString("utf-8"));
18
+ const value = parsed[key];
19
+ return typeof value === "string" && value ? value : null;
20
+ }
21
+ catch {
22
+ return null;
23
+ }
24
+ }
25
+ /**
26
+ * Cache key aligned with auth: execution token is exclusive (same as
27
+ * `resolveAccessToken`) and never falls back to a local Logto session.
28
+ * Execution grants identify the actor as `actorUserId`, not `sub`.
29
+ */
30
+ export function identityKeyFrom(input) {
31
+ if (input.executionToken) {
32
+ const actor = jwtClaim(input.executionToken, "actorUserId") ?? jwtClaim(input.executionToken, "sub");
33
+ return actor ? `${input.env}:${actor}` : null;
34
+ }
35
+ const sub = jwtClaim(input.idToken, "sub") ?? jwtClaim(input.accessToken, "sub");
36
+ return sub ? `${input.env}:${sub}` : null;
37
+ }
38
+ function identityKey() {
39
+ const session = readAuthSession();
40
+ return identityKeyFrom({
41
+ env: resolveCohubEnvironment(),
42
+ executionToken: process.env.COHUB_EXECUTION_TOKEN?.trim(),
43
+ idToken: session?.idToken,
44
+ accessToken: session?.accessToken,
45
+ });
46
+ }
47
+ /** Exported for tests; production always uses `CACHE_PATH`. */
48
+ export function readDefaultSpaceCache(path, key, now = Date.now()) {
49
+ try {
50
+ const cache = JSON.parse(readFileSync(path, "utf-8"));
51
+ if (cache.key !== key || typeof cache.spaceId !== "string" || typeof cache.cachedAt !== "number")
52
+ return null;
53
+ if (now - cache.cachedAt > CACHE_TTL_MS)
54
+ return null;
55
+ return cache.spaceId;
56
+ }
57
+ catch {
58
+ return null;
59
+ }
60
+ }
61
+ function writeCachedDefaultSpace(key, spaceId) {
62
+ try {
63
+ mkdirSync(CONFIG_DIR, { recursive: true });
64
+ const cache = { key, spaceId, cachedAt: Date.now() };
65
+ writeFileSync(CACHE_PATH, `${JSON.stringify(cache, null, 2)}\n`, { encoding: "utf-8", mode: 0o600 });
66
+ }
67
+ catch {
68
+ // Cache is best-effort; never fail the command over it.
69
+ }
70
+ }
71
+ let defaultSpacePromise = null;
72
+ export function clearDefaultSpaceCache() {
73
+ defaultSpacePromise = null;
74
+ try {
75
+ rmSync(CACHE_PATH, { force: true });
76
+ }
77
+ catch {
78
+ // Best-effort, same as writes.
79
+ }
80
+ }
81
+ /** Explicit target from `-s/--space` (any ancestor) or `COHUB_SPACE_ID`, else null. */
82
+ export function explicitSpace(program) {
3
83
  let current = program;
4
84
  while (current) {
5
85
  const opts = current.opts();
@@ -7,8 +87,39 @@ export function resolveSpace(program) {
7
87
  return opts.space.trim();
8
88
  current = current.parent ?? null;
9
89
  }
10
- const envSpace = process.env.COHUB_SPACE_ID?.trim();
11
- if (envSpace)
12
- return envSpace;
13
- return error("Missing required space", "Add -s, --space <id> or set COHUB_SPACE_ID.");
90
+ return process.env.COHUB_SPACE_ID?.trim() || null;
91
+ }
92
+ /**
93
+ * Resolve the user's home space when no target is given. Cached locally per
94
+ * identity so repeated invocations skip the network entirely, and memoized
95
+ * in-process so preAction hooks and actions share a single lookup.
96
+ * Network and auth failures propagate so callers can report them faithfully.
97
+ */
98
+ export function resolveDefaultSpace() {
99
+ defaultSpacePromise ??= (async () => {
100
+ const key = identityKey();
101
+ if (key) {
102
+ const cached = readDefaultSpaceCache(CACHE_PATH, key);
103
+ if (cached)
104
+ return cached;
105
+ }
106
+ const space = (await createClient().spaces.getDefault()).space ?? null;
107
+ // Recent-space fallback from getDefault() is not stable enough to cache.
108
+ if (space?.id && space.slug === "home" && key)
109
+ writeCachedDefaultSpace(key, space.id);
110
+ return space?.id ?? null;
111
+ })();
112
+ return defaultSpacePromise;
113
+ }
114
+ /** Shared exit for commands that need a space but resolved none. */
115
+ export function missingSpaceError() {
116
+ return error("No target space", "Add -s, --space <id> or set COHUB_SPACE_ID. Run `cohub auth login` to use your home space.");
117
+ }
118
+ /**
119
+ * Target space for a command: explicit `-s`/`COHUB_SPACE_ID` first, then the
120
+ * user's home space. Exits with guidance when neither is available; request
121
+ * failures go through the shared HTTP error handler.
122
+ */
123
+ export async function resolveSpace(program) {
124
+ return explicitSpace(program) ?? (await resolveDefaultSpace().catch(handleHttp)) ?? missingSpaceError();
14
125
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neta-art/cohub-cli",
3
- "version": "6.7.0",
3
+ "version": "6.8.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.20.1",
21
21
  "sharp": "^0.35.4",
22
- "@neta-art/cohub": "8.10.0"
22
+ "@neta-art/cohub": "8.10.2"
23
23
  },
24
24
  "publishConfig": {
25
25
  "access": "public"