@neta-art/cohub-cli 1.1.4 → 1.3.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 CHANGED
@@ -1,38 +1,208 @@
1
1
  # @neta-art/cohub-cli
2
2
 
3
- CLI for [Cohub](https://cohub.run) — spaces, sessions, and agent collaboration.
3
+ CLI for [Cohub](https://cohub.run) — work with Spaces, Chats, files, Saves, Tasks, scheduled prompts, search, and multimodal generation from your terminal.
4
4
 
5
5
  ## Installation
6
6
 
7
7
  ```bash
8
8
  npm install -g @neta-art/cohub-cli
9
+ cohub --help
9
10
  ```
10
11
 
11
- ## Usage
12
+ ## Sign in
13
+
14
+ For normal use outside Cohub Sandbox, sign in once:
12
15
 
13
16
  ```bash
14
- cohub --help
17
+ cohub auth login
18
+ cohub auth whoami
15
19
  ```
16
20
 
17
- ## Environment
21
+ The CLI will keep you signed in and refresh your session automatically.
18
22
 
19
- The CLI connects to production by default:
23
+ Inside Cohub Sandbox or CI, `COHUB_EXECUTION_TOKEN` can be used as an ephemeral auth override.
20
24
 
21
- - API: `https://api.cohub.run`
22
- - WebSocket: `wss://gateway.cohub.run/ws`
25
+ ## Environment
26
+
27
+ The CLI uses production by default.
23
28
 
24
29
  Use the development environment with `ENV=dev`:
25
30
 
26
31
  ```bash
32
+ ENV=dev cohub auth login
27
33
  ENV=dev cohub spaces ls
28
34
  ```
29
35
 
30
- Development uses:
36
+ ## Usage
37
+
38
+ Use `--json` when reading output for decisions, extracting IDs, or chaining commands.
39
+
40
+ For command details:
41
+
42
+ ```bash
43
+ cohub -h
44
+ cohub spaces -h
45
+ cohub spaces prompt -h
46
+ ```
47
+
48
+ ## Terminology
49
+
50
+ | Product UI | CLI / API |
51
+ |---|---|
52
+ | Chat | Session |
53
+ | Save | Checkpoint |
54
+ | Tasks | Task runs |
55
+ | Scheduled prompt | `spaces prompt` schedule |
56
+ | Recurring scheduled prompt | Cron job |
57
+
58
+ ## Spaces
59
+
60
+ ```bash
61
+ cohub spaces ls --json
62
+ cohub spaces get <spaceId> --json
63
+ cohub spaces create --name "<name>" --description "<description>" --json
64
+ cohub spaces rename <spaceId> "<new name>"
65
+ ```
66
+
67
+ Many space-scoped commands need a target Space:
68
+
69
+ ```bash
70
+ cohub -s <spaceId> spaces prompt "message" --json
71
+ ```
72
+
73
+ ## Chats and prompts
74
+
75
+ Use `spaces prompt` for immediate sends, delayed sends, one-time schedules, recurring schedules, new Chats, and existing Chats.
76
+
77
+ ```bash
78
+ # Send now
79
+ cohub -s <spaceId> spaces prompt "message" --json
80
+
81
+ # Send long content from stdin
82
+ cat prompt.md | cohub -s <spaceId> spaces prompt --json
83
+
84
+ # Send to an existing Chat
85
+ cohub -s <spaceId> spaces prompt --session <sessionId> "message" --json
86
+
87
+ # Create a new Chat and send
88
+ cohub -s <spaceId> spaces prompt --title "<chat title>" "message" --json
89
+
90
+ # Schedule once
91
+ cohub -s <spaceId> spaces prompt --at "2026-05-12T09:00:00+08:00" "message" --json
92
+
93
+ # Schedule recurring
94
+ cohub -s <spaceId> spaces prompt \
95
+ --cron "0 9 * * 1-5" \
96
+ --timezone "Asia/Shanghai" \
97
+ --title "Daily reminder" \
98
+ "message" \
99
+ --json
100
+ ```
101
+
102
+ Scheduling rules:
103
+
104
+ - Use only one of `--delay-ms`, `--at`, or `--cron`.
105
+ - `--cron` requires `--timezone`.
106
+
107
+ ## Chats / Sessions
108
+
109
+ ```bash
110
+ cohub -s <spaceId> spaces sessions ls --json
111
+ cohub -s <spaceId> spaces sessions create "<title>" --json
112
+ cohub -s <spaceId> spaces sessions get <sessionId> --json
113
+ cohub -s <spaceId> spaces sessions rename <sessionId> "<new title>"
114
+ ```
115
+
116
+ Use `spaces prompt --session <sessionId>` to send to a Chat.
117
+
118
+ ## Search
119
+
120
+ Search Spaces, Chats, and prior turns:
121
+
122
+ ```bash
123
+ cohub search "query"
124
+ cohub search "query" --limit 20 --json
125
+ ```
126
+
127
+ ## Models and multimodal generation
128
+
129
+ ```bash
130
+ cohub models ls --json
131
+ cohub models ls --model-type multimodal --json
132
+
133
+ cohub generate "a calm lake at sunrise" \
134
+ --model <model> \
135
+ --output output.png \
136
+ --json
137
+
138
+ cohub generate "restyle this image" \
139
+ --model <model> \
140
+ --image ./input.png \
141
+ --param size=1024x1024 \
142
+ --json
143
+ ```
144
+
145
+ Supported inputs:
146
+
147
+ ```bash
148
+ --image <path-or-url>
149
+ --video <path-or-url>
150
+ --audio <path-or-url>
151
+ ```
152
+
153
+ Pass generation parameters with `--param key=value` or `--parameters '<json>'`.
154
+
155
+ ## Files
156
+
157
+ ```bash
158
+ cohub -s <spaceId> spaces files ls [path] --json
159
+ cohub -s <spaceId> spaces files cat <path>
160
+ cohub -s <spaceId> spaces files write <path> -c "<content>"
161
+ cohub -s <spaceId> spaces files upload <files...> --dir <dir>
162
+ cohub -s <spaceId> spaces files mv <from> <to>
163
+ cohub -s <spaceId> spaces files rm <path>
164
+ ```
165
+
166
+ Confirm before deleting files or directories.
167
+
168
+ ## Saves
169
+
170
+ ```bash
171
+ cohub -s <spaceId> spaces checkpoints ls --json
172
+ cohub -s <spaceId> spaces checkpoints get <checkpointId> --json
173
+ cohub -s <spaceId> spaces checkpoints create "<description>" --json
174
+ ```
175
+
176
+ ## Tasks
177
+
178
+ ```bash
179
+ cohub tasks ls --space <spaceId> --json
180
+ cohub tasks get <taskRunId> --json
181
+ ```
182
+
183
+ Create scheduled sends with `spaces prompt` scheduling flags, not task commands.
184
+
185
+ ## Recurring scheduled prompts
186
+
187
+ Create recurring scheduled prompts with `spaces prompt --cron ... --timezone ...`.
188
+
189
+ Manage them with `cron-jobs`:
190
+
191
+ ```bash
192
+ cohub cron-jobs ls <spaceId> --json
193
+ cohub cron-jobs runs <cronJobId> --json
194
+ cohub cron-jobs toggle <cronJobId> on
195
+ cohub cron-jobs toggle <cronJobId> off
196
+ cohub cron-jobs delete <cronJobId>
197
+ ```
198
+
199
+ Confirm before enabling, disabling, or deleting recurring scheduled prompts.
31
200
 
32
- - API: `https://api-dev.cohub.run`
33
- - WebSocket: `wss://gateway-dev.cohub.run/ws`
201
+ ## Safety
34
202
 
35
- Auth tokens are stored per environment:
203
+ Confirm before:
36
204
 
37
- - prod: `~/.config/cohub/token`
38
- - dev: `~/.config/cohub/token.dev`
205
+ - deleting files or directories
206
+ - creating scheduled or recurring prompts with side effects
207
+ - enabling, disabling, or deleting recurring scheduled prompts
208
+ - changing access policies, member roles, or membership
package/dist/auth.d.ts CHANGED
@@ -1,12 +1,41 @@
1
- export declare const getTokenPath: () => string;
2
- /**
3
- * Resolve auth token with priority:
4
- * 1. COHUB_EXECUTION_TOKEN environment variable
5
- * 2. current environment token file
6
- * - prod: ~/.config/cohub/token
7
- * - dev: ~/.config/cohub/token.dev
8
- */
9
- export declare function resolveToken(): string | null;
10
- export declare function saveToken(token: string): void;
11
- export declare function clearToken(): void;
12
- export declare function tokenSource(): "env" | "file" | null;
1
+ import { type CohubEnvironment } from "@neta-art/cohub";
2
+ export type AuthSource = "execution-token" | "logto" | null;
3
+ type DeviceCode = {
4
+ deviceCode: string;
5
+ userCode: string;
6
+ verificationUri: string;
7
+ verificationUriComplete: string;
8
+ expiresAt: number;
9
+ interval: number;
10
+ createdAt: number;
11
+ };
12
+ type AuthSession = {
13
+ schemaVersion: 1;
14
+ env: CohubEnvironment;
15
+ issuer: string;
16
+ clientId: string;
17
+ resource: string;
18
+ scope: string;
19
+ tokenType: "Bearer";
20
+ accessToken: string;
21
+ refreshToken: string;
22
+ idToken?: string;
23
+ accessTokenExpiresAt: number;
24
+ createdAt: number;
25
+ updatedAt: number;
26
+ };
27
+ export declare class AuthRequiredError extends Error {
28
+ constructor(message?: string);
29
+ }
30
+ export declare const readAuthSession: () => AuthSession | null;
31
+ export declare const clearDeviceCode: () => void;
32
+ export declare const clearAuthSession: () => void;
33
+ export declare const authSource: () => AuthSource;
34
+ export declare function resolveAccessToken(): Promise<string | null>;
35
+ export declare function requireAccessToken(): Promise<string>;
36
+ export declare function refreshAccessToken(session?: AuthSession | null): Promise<string | null>;
37
+ export declare function requestDeviceCode(): Promise<DeviceCode>;
38
+ export declare function verifyDeviceCode(): Promise<AuthSession>;
39
+ export declare function loginWithDeviceFlow(onCode: (code: DeviceCode) => void, onPoll?: () => void): Promise<AuthSession>;
40
+ export declare function revokeAndClearAuthSession(): Promise<void>;
41
+ export {};
package/dist/auth.js CHANGED
@@ -1,43 +1,259 @@
1
1
  import { resolveCohubEnvironment } from "@neta-art/cohub";
2
- import { readFileSync, existsSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
2
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import { join } from "node:path";
5
- const TOKEN_DIR = join(homedir(), ".config", "cohub");
6
- export const getTokenPath = () => join(TOKEN_DIR, resolveCohubEnvironment() === "dev" ? "token.dev" : "token");
7
- /**
8
- * Resolve auth token with priority:
9
- * 1. COHUB_EXECUTION_TOKEN environment variable
10
- * 2. current environment token file
11
- * - prod: ~/.config/cohub/token
12
- * - dev: ~/.config/cohub/token.dev
13
- */
14
- export function resolveToken() {
15
- if (process.env.COHUB_EXECUTION_TOKEN) {
16
- return process.env.COHUB_EXECUTION_TOKEN.trim();
17
- }
18
- const path = getTokenPath();
19
- if (existsSync(path)) {
20
- return readFileSync(path, "utf-8").trim();
5
+ import { setTimeout as delay } from "node:timers/promises";
6
+ const CONFIG_DIR = join(homedir(), ".config", "cohub");
7
+ const EXPIRY_SKEW_MS = 5 * 60 * 1000;
8
+ const DEFAULT_SCOPE = "openid profile email offline_access";
9
+ const DEFAULT_RESOURCE = "https://api.talesofai";
10
+ export class AuthRequiredError extends Error {
11
+ constructor(message = "Not authenticated") {
12
+ super(message);
13
+ this.name = "AuthRequiredError";
21
14
  }
15
+ }
16
+ const currentEnv = () => resolveCohubEnvironment();
17
+ const normalizeUrl = (url) => url.trim().replace(/\/+$/, "");
18
+ const authConfig = (env = currentEnv()) => {
19
+ const prefix = env === "dev" ? "COHUB_DEV" : "COHUB";
20
+ const defaultIssuer = env === "dev" ? "https://dev-auth.neta.art" : "https://auth.neta.art";
21
+ const defaultClientId = env === "dev" ? "u2fnfgvf8f16dnt8si2bv" : "f8d26cdlwx85b0e5l3om2";
22
+ return {
23
+ issuer: normalizeUrl(process.env[`${prefix}_AUTH_ISSUER`] ?? process.env.COHUB_AUTH_ISSUER ?? defaultIssuer),
24
+ clientId: process.env[`${prefix}_AUTH_CLIENT_ID`] ?? process.env.COHUB_AUTH_CLIENT_ID ?? defaultClientId,
25
+ resource: process.env[`${prefix}_AUTH_RESOURCE`] ?? process.env.COHUB_AUTH_RESOURCE ?? DEFAULT_RESOURCE,
26
+ scope: process.env[`${prefix}_AUTH_SCOPE`] ?? process.env.COHUB_AUTH_SCOPE ?? DEFAULT_SCOPE,
27
+ };
28
+ };
29
+ const sessionPath = (env = currentEnv()) => join(CONFIG_DIR, env === "dev" ? "auth.dev.json" : "auth.json");
30
+ const deviceCodePath = (env = currentEnv()) => join(CONFIG_DIR, env === "dev" ? "device-code.dev.json" : "device-code.json");
31
+ const legacyTokenPath = (env = currentEnv()) => join(CONFIG_DIR, env === "dev" ? "token.dev" : "token");
32
+ const writePrivateJson = (path, value) => {
33
+ mkdirSync(CONFIG_DIR, { recursive: true });
34
+ writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, { encoding: "utf-8", mode: 0o600 });
35
+ };
36
+ const readJson = (path) => {
37
+ if (!existsSync(path))
38
+ return null;
39
+ try {
40
+ return JSON.parse(readFileSync(path, "utf-8"));
41
+ }
42
+ catch {
43
+ return null;
44
+ }
45
+ };
46
+ const removeIfExists = (path) => {
47
+ if (existsSync(path))
48
+ rmSync(path);
49
+ };
50
+ const tokenEndpoint = (issuer) => `${issuer}/oidc/token`;
51
+ const deviceEndpoint = (issuer) => `${issuer}/oidc/device/auth`;
52
+ const revocationEndpoint = (issuer) => `${issuer}/oidc/token/revocation`;
53
+ const formPost = async (url, body) => {
54
+ const response = await fetch(url, {
55
+ method: "POST",
56
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
57
+ body,
58
+ });
59
+ const contentType = response.headers.get("content-type") ?? "";
60
+ const data = contentType.includes("application/json")
61
+ ? await response.json().catch(() => null)
62
+ : await response.text().catch(() => null);
63
+ return { response, data };
64
+ };
65
+ const toNonEmptyString = (value, field) => {
66
+ if (typeof value !== "string" || !value.trim())
67
+ throw new Error(`Invalid auth response: missing ${field}`);
68
+ return value;
69
+ };
70
+ const toPositiveNumber = (value, field) => {
71
+ const number = typeof value === "number" ? value : Number(value);
72
+ if (!Number.isFinite(number) || number <= 0)
73
+ throw new Error(`Invalid auth response: invalid ${field}`);
74
+ return number;
75
+ };
76
+ const authErrorCode = (data) => {
77
+ if (typeof data !== "object" || data === null)
78
+ return null;
79
+ const error = data.error;
80
+ return typeof error === "string" ? error : null;
81
+ };
82
+ const isUnrecoverableRefreshError = (data) => {
83
+ const code = authErrorCode(data);
84
+ return code === "invalid_grant" || code === "invalid_token";
85
+ };
86
+ const toSession = (token, config, env, previous) => {
87
+ const accessToken = toNonEmptyString(token.access_token, "access_token");
88
+ const expiresIn = toPositiveNumber(token.expires_in, "expires_in");
89
+ if (token.token_type !== "Bearer")
90
+ throw new Error(`Unsupported token type: ${token.token_type}`);
91
+ if (token.refresh_token !== undefined && !token.refresh_token.trim())
92
+ throw new Error("Invalid auth response: invalid refresh_token");
93
+ if (!token.refresh_token && !previous?.refreshToken)
94
+ throw new Error("Logto did not return a refresh token");
95
+ const now = Date.now();
96
+ return {
97
+ schemaVersion: 1,
98
+ env,
99
+ issuer: config.issuer,
100
+ clientId: config.clientId,
101
+ resource: config.resource,
102
+ scope: token.scope ?? config.scope,
103
+ tokenType: "Bearer",
104
+ accessToken,
105
+ refreshToken: token.refresh_token ?? previous?.refreshToken ?? "",
106
+ idToken: token.id_token ?? previous?.idToken,
107
+ accessTokenExpiresAt: now + expiresIn * 1000,
108
+ createdAt: previous?.createdAt ?? now,
109
+ updatedAt: now,
110
+ };
111
+ };
112
+ export const readAuthSession = () => readJson(sessionPath());
113
+ export const clearDeviceCode = () => {
114
+ removeIfExists(deviceCodePath());
115
+ };
116
+ export const clearAuthSession = () => {
117
+ removeIfExists(sessionPath());
118
+ clearDeviceCode();
119
+ removeIfExists(legacyTokenPath());
120
+ };
121
+ export const authSource = () => {
122
+ if (process.env.COHUB_EXECUTION_TOKEN?.trim())
123
+ return "execution-token";
124
+ if (readAuthSession())
125
+ return "logto";
22
126
  return null;
127
+ };
128
+ export async function resolveAccessToken() {
129
+ const executionToken = process.env.COHUB_EXECUTION_TOKEN?.trim();
130
+ if (executionToken)
131
+ return executionToken;
132
+ const session = readAuthSession();
133
+ if (!session)
134
+ throw new AuthRequiredError();
135
+ if (session.accessTokenExpiresAt - Date.now() > EXPIRY_SKEW_MS)
136
+ return session.accessToken;
137
+ return refreshAccessToken(session);
23
138
  }
24
- export function saveToken(token) {
25
- const trimmed = token.trim();
26
- if (!trimmed)
27
- throw new Error("Token cannot be empty");
28
- mkdirSync(TOKEN_DIR, { recursive: true });
29
- writeFileSync(getTokenPath(), trimmed);
139
+ export async function requireAccessToken() {
140
+ const token = await resolveAccessToken();
141
+ if (!token)
142
+ throw new AuthRequiredError();
143
+ return token;
30
144
  }
31
- export function clearToken() {
32
- const path = getTokenPath();
33
- if (existsSync(path)) {
34
- rmSync(path);
145
+ export async function refreshAccessToken(session = readAuthSession()) {
146
+ if (!session?.refreshToken)
147
+ throw new AuthRequiredError();
148
+ const config = authConfig(session.env);
149
+ const { response, data } = await formPost(tokenEndpoint(session.issuer), new URLSearchParams({
150
+ client_id: session.clientId,
151
+ grant_type: "refresh_token",
152
+ refresh_token: session.refreshToken,
153
+ scope: session.scope,
154
+ resource: session.resource,
155
+ }));
156
+ if (!response.ok) {
157
+ if (isUnrecoverableRefreshError(data)) {
158
+ clearAuthSession();
159
+ return null;
160
+ }
161
+ throw new Error(formatAuthError(data, response.status));
35
162
  }
163
+ const next = toSession(data, config, session.env, session);
164
+ writePrivateJson(sessionPath(session.env), next);
165
+ return next.accessToken;
36
166
  }
37
- export function tokenSource() {
38
- if (process.env.COHUB_EXECUTION_TOKEN)
39
- return "env";
40
- if (existsSync(getTokenPath()))
41
- return "file";
42
- return null;
167
+ export async function requestDeviceCode() {
168
+ const env = currentEnv();
169
+ const config = authConfig(env);
170
+ const { response, data } = await formPost(deviceEndpoint(config.issuer), new URLSearchParams({
171
+ client_id: config.clientId,
172
+ scope: config.scope,
173
+ resource: config.resource,
174
+ }));
175
+ if (!response.ok || typeof data !== "object" || data === null) {
176
+ throw new Error(typeof data === "string" ? data : `Failed to request device code: HTTP ${response.status}`);
177
+ }
178
+ const body = data;
179
+ const now = Date.now();
180
+ const expiresIn = toPositiveNumber(body.expires_in, "expires_in");
181
+ const interval = body.interval === undefined ? 5 : toPositiveNumber(body.interval, "interval");
182
+ const verificationUri = toNonEmptyString(body.verification_uri, "verification_uri");
183
+ const deviceCode = {
184
+ deviceCode: toNonEmptyString(body.device_code, "device_code"),
185
+ userCode: toNonEmptyString(body.user_code, "user_code"),
186
+ verificationUri,
187
+ verificationUriComplete: body.verification_uri_complete === undefined
188
+ ? verificationUri
189
+ : toNonEmptyString(body.verification_uri_complete, "verification_uri_complete"),
190
+ expiresAt: now + expiresIn * 1000,
191
+ interval,
192
+ createdAt: now,
193
+ };
194
+ writePrivateJson(deviceCodePath(env), deviceCode);
195
+ return deviceCode;
196
+ }
197
+ export async function verifyDeviceCode() {
198
+ const env = currentEnv();
199
+ const config = authConfig(env);
200
+ const deviceCode = readJson(deviceCodePath(env));
201
+ if (!deviceCode)
202
+ throw new Error("Device code not found. Run `cohub auth login --request-code` first.");
203
+ if (deviceCode.expiresAt <= Date.now())
204
+ throw new Error("Device code expired. Run `cohub auth login` again.");
205
+ const { response, data } = await formPost(tokenEndpoint(config.issuer), new URLSearchParams({
206
+ client_id: config.clientId,
207
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
208
+ device_code: deviceCode.deviceCode,
209
+ resource: config.resource,
210
+ }));
211
+ if (!response.ok)
212
+ throw new Error(formatAuthError(data, response.status));
213
+ const session = toSession(data, config, env);
214
+ writePrivateJson(sessionPath(env), session);
215
+ removeIfExists(deviceCodePath(env));
216
+ return session;
217
+ }
218
+ export async function loginWithDeviceFlow(onCode, onPoll) {
219
+ const code = await requestDeviceCode();
220
+ onCode(code);
221
+ while (Date.now() < code.expiresAt) {
222
+ await delay(code.interval * 1000);
223
+ onPoll?.();
224
+ try {
225
+ return await verifyDeviceCode();
226
+ }
227
+ catch (e) {
228
+ const message = e instanceof Error ? e.message : String(e);
229
+ if (message.includes("authorization_pending"))
230
+ continue;
231
+ if (message.includes("slow_down")) {
232
+ await delay(code.interval * 1000);
233
+ continue;
234
+ }
235
+ throw e;
236
+ }
237
+ }
238
+ throw new Error("Login timed out. Run `cohub auth login` again.");
239
+ }
240
+ export async function revokeAndClearAuthSession() {
241
+ const session = readAuthSession();
242
+ if (session?.refreshToken) {
243
+ await formPost(revocationEndpoint(session.issuer), new URLSearchParams({
244
+ client_id: session.clientId,
245
+ token: session.refreshToken,
246
+ token_type_hint: "refresh_token",
247
+ })).catch(() => null);
248
+ }
249
+ clearAuthSession();
250
+ }
251
+ function formatAuthError(data, status) {
252
+ if (typeof data === "object" && data !== null) {
253
+ const record = data;
254
+ const code = String(record.error ?? "");
255
+ const description = String(record.error_description ?? "");
256
+ return [code, description].filter(Boolean).join(": ") || `Authentication failed: HTTP ${status}`;
257
+ }
258
+ return typeof data === "string" && data ? data : `Authentication failed: HTTP ${status}`;
43
259
  }
package/dist/client.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  import { CohubHttpClient } from "@neta-art/cohub";
2
- export declare function createClient(token: string): CohubHttpClient;
2
+ export declare function createClient(): CohubHttpClient;
package/dist/client.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import { CohubHttpClient } from "@neta-art/cohub";
2
- export function createClient(token) {
2
+ import { clearDeviceCode, resolveAccessToken } from "./auth.js";
3
+ export function createClient() {
3
4
  return new CohubHttpClient({
4
- getAccessToken: () => token,
5
+ getAccessToken: resolveAccessToken,
6
+ onUnauthorized: clearDeviceCode,
5
7
  });
6
8
  }