@neta-art/cohub-cli 6.6.0 → 6.8.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/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.6.0",
3
+ "version": "6.8.0",
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.9.0"
22
+ "@neta-art/cohub": "8.10.1"
23
23
  },
24
24
  "publishConfig": {
25
25
  "access": "public"