@levr-one/cli 0.1.1 → 0.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/dist/cli.js CHANGED
@@ -52,6 +52,68 @@ function buildContext(process$1) {
52
52
  };
53
53
  }
54
54
 
55
+ //#endregion
56
+ //#region src/commands/mcp/add.ts
57
+ const mcpAddCommand = buildCommand({
58
+ docs: {
59
+ brief: "Add the Levr MCP server to installed AI clients",
60
+ fullDescription: `Detect MCP-capable clients on this machine (Claude Desktop,
61
+ Claude Code, Cursor, Windsurf, Zed) and write the Levr MCP server into each
62
+ one's config. The entry is credential-free — the client opens a browser to
63
+ authorize with Levr the first time it connects.
64
+
65
+ Interactive by default; non-interactive when --all/--client/--yes is passed
66
+ or when not running in a terminal (CI). Config edits preserve existing
67
+ servers and comments, and re-running is a no-op.
68
+
69
+ Examples:
70
+ npx @levr-one/cli mcp add # detect clients and pick interactively
71
+ levr mcp add --all # set up every detected client
72
+ levr mcp add --client cursor --yes
73
+ levr mcp add --dry-run # preview without writing
74
+ levr mcp add --url <mcp-url> # target a non-default MCP server`
75
+ },
76
+ parameters: {
77
+ flags: {
78
+ client: {
79
+ kind: "parsed",
80
+ parse: String,
81
+ brief: "Set up these client ids (comma-separated or repeated)",
82
+ placeholder: "id[,id]",
83
+ variadic: true,
84
+ optional: true
85
+ },
86
+ all: {
87
+ kind: "boolean",
88
+ default: false,
89
+ brief: "Set up every detected, installable client"
90
+ },
91
+ yes: {
92
+ kind: "boolean",
93
+ default: false,
94
+ brief: "Non-interactive; auto-select detected clients"
95
+ },
96
+ "dry-run": {
97
+ kind: "boolean",
98
+ default: false,
99
+ brief: "Show changes without writing"
100
+ },
101
+ url: {
102
+ kind: "parsed",
103
+ parse: String,
104
+ brief: "MCP server URL (default derived from the API server)",
105
+ placeholder: "url",
106
+ optional: true
107
+ }
108
+ },
109
+ aliases: { y: "yes" }
110
+ },
111
+ loader: async () => {
112
+ const { mcpAddHandler } = await import("./addHandler-boWTd9yY.js");
113
+ return mcpAddHandler;
114
+ }
115
+ });
116
+
55
117
  //#endregion
56
118
  //#region src/commands/auth/login.ts
57
119
  const loginCommand = buildCommand({
@@ -68,15 +130,24 @@ Examples:
68
130
  levr auth login --device-code # Device flow for SSH/headless`
69
131
  },
70
132
  parameters: {
71
- flags: { "device-code": {
72
- kind: "boolean",
73
- default: false,
74
- brief: "Use device code flow (for SSH/headless environments)"
75
- } },
133
+ flags: {
134
+ "device-code": {
135
+ kind: "boolean",
136
+ default: false,
137
+ brief: "Use device code flow (for SSH/headless environments)"
138
+ },
139
+ url: {
140
+ kind: "parsed",
141
+ parse: String,
142
+ brief: "API base URL (default: https://api.levr.one)",
143
+ placeholder: "url",
144
+ optional: true
145
+ }
146
+ },
76
147
  aliases: { d: "device-code" }
77
148
  },
78
149
  loader: async () => {
79
- const { loginHandler } = await import("./loginHandler-BnokItPo.js");
150
+ const { loginHandler } = await import("./loginHandler-BfIUQJrC.js");
80
151
  return loginHandler;
81
152
  }
82
153
  });
@@ -95,7 +166,7 @@ Examples:
95
166
  },
96
167
  parameters: {},
97
168
  loader: async () => {
98
- const { logoutHandler } = await import("./logoutHandler-C3X9rqLP.js");
169
+ const { logoutHandler } = await import("./logoutHandler-BC8laIAB.js");
99
170
  return logoutHandler;
100
171
  }
101
172
  });
@@ -115,7 +186,7 @@ Examples:
115
186
  },
116
187
  parameters: {},
117
188
  loader: async () => {
118
- const { statusHandler } = await import("./statusHandler-LfrSN4uh.js");
189
+ const { statusHandler } = await import("./statusHandler-BaQB40t7.js");
119
190
  return statusHandler;
120
191
  }
121
192
  });
@@ -226,7 +297,7 @@ Examples:
226
297
  }
227
298
  },
228
299
  loader: async () => {
229
- const { pushHandler } = await import("./pushHandler-C3DteESD.js");
300
+ const { pushHandler } = await import("./pushHandler-DBBa6hKz.js");
230
301
  return pushHandler;
231
302
  }
232
303
  });
@@ -247,7 +318,7 @@ Examples:
247
318
  },
248
319
  parameters: {},
249
320
  loader: async () => {
250
- const { listHandler } = await import("./listHandler-gfnJbj60.js");
321
+ const { listHandler } = await import("./listHandler-EsB3AeOB.js");
251
322
  return listHandler;
252
323
  }
253
324
  });
@@ -280,7 +351,7 @@ Examples:
280
351
  flags: {}
281
352
  },
282
353
  loader: async () => {
283
- const { selectHandler } = await import("./selectHandler-BjM4LKYW.js");
354
+ const { selectHandler } = await import("./selectHandler-XOUHdyea.js");
284
355
  return selectHandler;
285
356
  }
286
357
  });
@@ -297,35 +368,41 @@ Examples:
297
368
  },
298
369
  parameters: {},
299
370
  loader: async () => {
300
- const { currentHandler } = await import("./currentHandler-DpWIqCBm.js");
371
+ const { currentHandler } = await import("./currentHandler-Bm7GFMkj.js");
301
372
  return currentHandler;
302
373
  }
303
374
  });
304
375
 
305
376
  //#endregion
306
377
  //#region package.json
307
- var version = "0.1.1";
378
+ var version = "0.3.0";
308
379
 
309
380
  //#endregion
310
381
  //#region src/app.ts
382
+ const authRoutes = buildRouteMap({
383
+ routes: {
384
+ login: loginCommand,
385
+ logout: logoutCommand,
386
+ status: statusCommand
387
+ },
388
+ docs: { brief: "Manage authentication" }
389
+ });
390
+ const workspaceRoutes = buildRouteMap({
391
+ routes: {
392
+ list: listCommand,
393
+ select: selectCommand,
394
+ current: currentCommand
395
+ },
396
+ docs: { brief: "Manage workspace selection" }
397
+ });
311
398
  const routes = buildRouteMap({
312
399
  routes: {
313
- auth: buildRouteMap({
314
- routes: {
315
- login: loginCommand,
316
- logout: logoutCommand,
317
- status: statusCommand
318
- },
319
- docs: { brief: "Manage authentication" }
320
- }),
321
- workspace: buildRouteMap({
322
- routes: {
323
- list: listCommand,
324
- select: selectCommand,
325
- current: currentCommand
326
- },
327
- docs: { brief: "Manage workspace selection" }
400
+ mcp: buildRouteMap({
401
+ routes: { add: mcpAddCommand },
402
+ docs: { brief: "Wire the Levr MCP server into installed AI clients" }
328
403
  }),
404
+ auth: authRoutes,
405
+ workspace: workspaceRoutes,
329
406
  push: pushCommand,
330
407
  install: buildInstallCommand("levr", { bash: "levr __complete" }),
331
408
  uninstall: buildUninstallCommand("levr", { bash: true })
@@ -0,0 +1,102 @@
1
+ import { mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join } from "node:path";
4
+
5
+ //#region src/auth/credentials.ts
6
+ const CREDENTIALS_PATH = join(join(homedir(), ".config", "levr"), "credentials.json");
7
+ function readCredentials() {
8
+ try {
9
+ const raw = readFileSync(CREDENTIALS_PATH, "utf8");
10
+ return JSON.parse(raw);
11
+ } catch {
12
+ return null;
13
+ }
14
+ }
15
+ function writeCredentials(creds) {
16
+ mkdirSync(dirname(CREDENTIALS_PATH), { recursive: true });
17
+ writeFileSync(CREDENTIALS_PATH, JSON.stringify(creds, null, 2), { mode: 384 });
18
+ }
19
+ function deleteCredentials() {
20
+ try {
21
+ unlinkSync(CREDENTIALS_PATH);
22
+ return true;
23
+ } catch {
24
+ return false;
25
+ }
26
+ }
27
+
28
+ //#endregion
29
+ //#region src/utils/env.ts
30
+ const DEFAULT_API_URL = "https://api.levr.one";
31
+ const KNOWN_AUTH_HOSTS = {
32
+ "api.levr.one": "auth.levr.one",
33
+ "api.levr.now": "auth.levr.now"
34
+ };
35
+ /** OAuth client ID for the CLI (seeded in oauth_clients table) */
36
+ const CLI_CLIENT_ID = "3";
37
+ let sessionApiUrl;
38
+ function setSessionApiUrl(url) {
39
+ sessionApiUrl = normalizeUrl(url);
40
+ }
41
+ function normalizeUrl(url) {
42
+ return url.replace(/\/+$/, "");
43
+ }
44
+ /**
45
+ * Resolve the API base URL: `--url` flag > `LEVR_URL` env var > the
46
+ * `api_url` stored in credentials at login > production default.
47
+ *
48
+ * The stored-credentials fallback keeps every later command (push, refresh,
49
+ * workspace list) pointed at the environment the user actually logged into,
50
+ * instead of silently falling back to production when the env var is no
51
+ * longer exported in the current shell.
52
+ */
53
+ function getApiUrl() {
54
+ if (sessionApiUrl) return sessionApiUrl;
55
+ const env = process.env["LEVR_URL"];
56
+ if (env) return normalizeUrl(env);
57
+ if (!getPatToken()) {
58
+ const stored = readCredentials()?.api_url;
59
+ if (stored) return normalizeUrl(stored);
60
+ }
61
+ return DEFAULT_API_URL;
62
+ }
63
+ /**
64
+ * Resolve the auth-server base URL (browser PKCE consent page only):
65
+ * `LEVR_AUTH_URL` env var > derived from the resolved API URL for known
66
+ * Levr hosts. Unrecognized hosts (e.g. localhost dev stacks) require the
67
+ * explicit env var — throwing beats silently opening the production login
68
+ * page for a token exchange that can never succeed.
69
+ */
70
+ function getAuthUrl() {
71
+ const env = process.env["LEVR_AUTH_URL"];
72
+ if (env) return normalizeUrl(env);
73
+ const apiUrl = getApiUrl();
74
+ const derived = deriveAuthUrl(apiUrl);
75
+ if (derived) return derived;
76
+ throw new Error(`Cannot derive the auth server URL from API URL "${apiUrl}". Set LEVR_AUTH_URL to the auth server base URL (e.g. https://auth.levr.one).`);
77
+ }
78
+ function deriveAuthUrl(apiUrl) {
79
+ let parsed;
80
+ try {
81
+ parsed = new URL(apiUrl);
82
+ } catch {
83
+ return;
84
+ }
85
+ const authHost = KNOWN_AUTH_HOSTS[parsed.host];
86
+ return authHost ? `${parsed.protocol}//${authHost}` : void 0;
87
+ }
88
+ function getTeamId(flagValue) {
89
+ return flagValue || process.env["LEVR_TEAM_ID"] || void 0;
90
+ }
91
+ function getPatToken() {
92
+ return process.env["LEVR_TOKEN"];
93
+ }
94
+ function getSourceOverride() {
95
+ return process.env["LEVR_SOURCE"];
96
+ }
97
+ function getAutomationSourceIdOverride() {
98
+ return process.env["LEVR_AUTOMATION_SOURCE_ID"];
99
+ }
100
+
101
+ //#endregion
102
+ export { CLI_CLIENT_ID, deleteCredentials, getApiUrl, getAuthUrl, getAutomationSourceIdOverride, getPatToken, getSourceOverride, getTeamId, readCredentials, setSessionApiUrl, writeCredentials };
@@ -0,0 +1,51 @@
1
+ import "./env-NxtzJJPk.js";
2
+ import { authGetSitesV1, configureClient } from "./sdk-client-BeE6mXns.js";
3
+ import { loadWorkspace } from "./workspace-store-4hfvsEHS.js";
4
+ import "./token-refresh-Cz-FqDtC.js";
5
+ import { resolveToken } from "./resolve-token-BRPrqdG-.js";
6
+
7
+ //#region src/commands/workspace/listHandler.ts
8
+ async function listHandler() {
9
+ let auth;
10
+ try {
11
+ auth = await resolveToken();
12
+ } catch (err) {
13
+ this.logger.error(err instanceof Error ? err.message : "Not authenticated. Run 'levr auth login' first.");
14
+ this.process.exitCode = 1;
15
+ return;
16
+ }
17
+ if (auth.type === "pat") {
18
+ this.logger.error("Workspace listing requires JWT auth. Run 'levr auth login'.");
19
+ this.process.exitCode = 1;
20
+ return;
21
+ }
22
+ configureClient(auth);
23
+ const result = await authGetSitesV1();
24
+ if (result.error) {
25
+ this.logger.error("Failed to list workspaces.");
26
+ this.process.exitCode = 1;
27
+ return;
28
+ }
29
+ const data = result.data;
30
+ printSites(this, data.sites);
31
+ }
32
+ /**
33
+ * Print the workspace list with the current-workspace indicator. Shared
34
+ * with `levr init` (ENG-2361).
35
+ */
36
+ function printSites(ctx, sites) {
37
+ if (sites.length === 0) {
38
+ ctx.logger.info("No workspaces available.");
39
+ return;
40
+ }
41
+ const currentWs = loadWorkspace();
42
+ ctx.process.stdout.write("\nWorkspaces:\n\n");
43
+ for (const site of sites) {
44
+ const indicator = site.workspace_id === currentWs ? " *" : "";
45
+ ctx.process.stdout.write(` ${site.workspace_name} (${site.workspace_id}) [${site.role}]${indicator}\n`);
46
+ }
47
+ ctx.process.stdout.write("\n");
48
+ }
49
+
50
+ //#endregion
51
+ export { listHandler };
@@ -1,7 +1,7 @@
1
- import { CLI_CLIENT_ID, getApiUrl, getAuthUrl, writeCredentials } from "./credentials-Ciq9mA7N.js";
2
- import { configureClient } from "./sdk-client-rpUEykDw.js";
1
+ import { CLI_CLIENT_ID, getApiUrl, getAuthUrl, setSessionApiUrl, writeCredentials } from "./env-NxtzJJPk.js";
2
+ import { configureClient } from "./sdk-client-BeE6mXns.js";
3
3
  import "./workspace-store-4hfvsEHS.js";
4
- import { autoSelectWorkspace } from "./resolve-workspace-BPMkU_SK.js";
4
+ import { autoSelectWorkspace } from "./resolve-workspace-Dj7jgtE3.js";
5
5
  import chalk from "chalk";
6
6
  import { createHash, randomBytes } from "node:crypto";
7
7
  import { createServer } from "node:http";
@@ -166,14 +166,19 @@ function sleep(ms) {
166
166
  */
167
167
  async function requestDeviceCode() {
168
168
  const apiUrl = getApiUrl();
169
- const res = await fetch(`${apiUrl}/v1/oauth/device/authorize`, {
170
- method: "POST",
171
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
172
- body: new URLSearchParams({
173
- client_id: CLI_CLIENT_ID,
174
- scope: "read:own write:own"
175
- })
176
- });
169
+ let res;
170
+ try {
171
+ res = await fetch(`${apiUrl}/v1/oauth/device/authorize`, {
172
+ method: "POST",
173
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
174
+ body: new URLSearchParams({
175
+ client_id: CLI_CLIENT_ID,
176
+ scope: "read:own write:own"
177
+ })
178
+ });
179
+ } catch {
180
+ throw new Error(`Could not reach ${apiUrl} — check the URL and your network connection.`);
181
+ }
177
182
  if (!res.ok) {
178
183
  const body = await res.text();
179
184
  throw new Error(`Failed to request device code (${res.status}): ${body}`);
@@ -223,15 +228,31 @@ function formatCountdown(ms) {
223
228
  return `${Math.floor(totalSec / 60)}:${(totalSec % 60).toString().padStart(2, "0")}`;
224
229
  }
225
230
  async function loginHandler(flags) {
226
- if (flags["device-code"]) {
227
- await deviceCodeLogin(this);
228
- return;
229
- }
230
- await pkceLogin(this);
231
+ await performLogin(this, {
232
+ deviceCode: flags["device-code"],
233
+ url: flags.url
234
+ });
235
+ }
236
+ /**
237
+ * Run the login flow (PKCE or device code). Reusable outside the Stricli
238
+ * handler (levr init composes it). Returns true on success; on failure the
239
+ * error has been printed and `exitCode` set.
240
+ */
241
+ async function performLogin(ctx, options) {
242
+ if (options.url) setSessionApiUrl(options.url);
243
+ if (options.deviceCode) return deviceCodeLogin(ctx);
244
+ return pkceLogin(ctx);
231
245
  }
232
246
  async function pkceLogin(ctx) {
233
247
  const apiUrl = getApiUrl();
234
- const authUrl = getAuthUrl();
248
+ let authUrl;
249
+ try {
250
+ authUrl = getAuthUrl();
251
+ } catch (err) {
252
+ ctx.logger.error(err instanceof Error ? err.message : "Invalid auth server configuration.");
253
+ ctx.process.exitCode = 1;
254
+ return false;
255
+ }
235
256
  const codeVerifier = generateCodeVerifier();
236
257
  const codeChallenge = generateCodeChallenge(codeVerifier);
237
258
  const state = randomBytes(16).toString("hex");
@@ -240,7 +261,7 @@ async function pkceLogin(ctx) {
240
261
  expectedState: state
241
262
  });
242
263
  const redirectUri = `http://127.0.0.1:${await portPromise}/callback`;
243
- const authorizeUrl = `${authUrl}/auth/oauth/authorize?${new URLSearchParams({
264
+ const params = new URLSearchParams({
244
265
  response_type: "code",
245
266
  client_id: CLI_CLIENT_ID,
246
267
  redirect_uri: redirectUri,
@@ -248,7 +269,8 @@ async function pkceLogin(ctx) {
248
269
  code_challenge_method: "S256",
249
270
  scope: "read:own write:own",
250
271
  state
251
- }).toString()}`;
272
+ });
273
+ const authorizeUrl = `${authUrl}/auth/oauth/authorize?${params.toString()}`;
252
274
  ctx.process.stdout.write("Opening browser to authenticate...\n\n");
253
275
  ctx.process.stdout.write(` If the browser doesn't open, visit:\n ${chalk.cyan(authorizeUrl)}\n\n`);
254
276
  try {
@@ -270,7 +292,7 @@ async function pkceLogin(ctx) {
270
292
  ctx.process.stdout.write("\n");
271
293
  ctx.logger.error(err instanceof Error ? err.message : "Authentication failed.");
272
294
  ctx.process.exitCode = 1;
273
- return;
295
+ return false;
274
296
  }
275
297
  clearInterval(countdownTimer);
276
298
  try {
@@ -289,11 +311,12 @@ async function pkceLogin(ctx) {
289
311
  const errBody = await tokenRes.text();
290
312
  throw new Error(`Token exchange failed (${tokenRes.status}): ${errBody}`);
291
313
  }
292
- await saveTokensAndFinish(ctx, await tokenRes.json());
314
+ return await saveTokensAndFinish(ctx, await tokenRes.json());
293
315
  } catch (err) {
294
316
  ctx.process.stdout.write("\n");
295
317
  ctx.logger.error(err instanceof Error ? err.message : "Token exchange failed.");
296
318
  ctx.process.exitCode = 1;
319
+ return false;
297
320
  }
298
321
  }
299
322
  async function deviceCodeLogin(ctx) {
@@ -319,11 +342,12 @@ async function deviceCodeLogin(ctx) {
319
342
  throw err;
320
343
  }
321
344
  clearInterval(deviceTimer);
322
- await saveTokensAndFinish(ctx, tokenData);
345
+ return await saveTokensAndFinish(ctx, tokenData);
323
346
  } catch (err) {
324
347
  ctx.process.stdout.write("\n");
325
348
  ctx.logger.error(err instanceof Error ? err.message : "Device flow failed.");
326
349
  ctx.process.exitCode = 1;
350
+ return false;
327
351
  }
328
352
  }
329
353
  async function saveTokensAndFinish(ctx, tokenData) {
@@ -337,7 +361,7 @@ async function saveTokensAndFinish(ctx, tokenData) {
337
361
  ctx.process.stdout.write("\n");
338
362
  ctx.logger.error("Token response missing user data.");
339
363
  ctx.process.exitCode = 1;
340
- return;
364
+ return false;
341
365
  }
342
366
  writeCredentials({
343
367
  version: 1,
@@ -357,6 +381,7 @@ async function saveTokensAndFinish(ctx, tokenData) {
357
381
  const wsResult = await autoSelectWorkspace();
358
382
  if (wsResult.kind === "single") ctx.process.stdout.write(`Workspace: ${chalk.bold(wsResult.workspaceName)}\n`);
359
383
  else if (wsResult.kind === "multiple") ctx.process.stdout.write(`\n${wsResult.count} workspaces available. Run ${chalk.cyan("'levr workspace list'")} to see them.\n`);
384
+ return true;
360
385
  }
361
386
 
362
387
  //#endregion
@@ -1,4 +1,4 @@
1
- import { deleteCredentials, getPatToken } from "./credentials-Ciq9mA7N.js";
1
+ import { deleteCredentials, getPatToken } from "./env-NxtzJJPk.js";
2
2
  import { clearWorkspace } from "./workspace-store-4hfvsEHS.js";
3
3
 
4
4
  //#region src/commands/auth/logoutHandler.ts
@@ -1,9 +1,9 @@
1
- import { getApiUrl, getAutomationSourceIdOverride, getSourceOverride, getTeamId } from "./credentials-Ciq9mA7N.js";
2
- import { client, configureClient, uploadAutomationIngest, uploadImport } from "./sdk-client-rpUEykDw.js";
1
+ import { getApiUrl, getAutomationSourceIdOverride, getSourceOverride, getTeamId } from "./env-NxtzJJPk.js";
2
+ import { client, configureClient, uploadAutomationIngest, uploadImport } from "./sdk-client-BeE6mXns.js";
3
3
  import "./workspace-store-4hfvsEHS.js";
4
- import { resolveWorkspace } from "./resolve-workspace-BPMkU_SK.js";
5
- import "./token-refresh-Pl6Adngb.js";
6
- import { resolveToken } from "./resolve-token-CQU55U6J.js";
4
+ import { resolveWorkspace } from "./resolve-workspace-Dj7jgtE3.js";
5
+ import "./token-refresh-Cz-FqDtC.js";
6
+ import { resolveToken } from "./resolve-token-BRPrqdG-.js";
7
7
  import { readFileSync, statSync } from "node:fs";
8
8
  import { basename } from "node:path";
9
9
  import ora from "ora";
@@ -0,0 +1,48 @@
1
+ import { getApiUrl, getPatToken, readCredentials } from "./env-NxtzJJPk.js";
2
+ import { isTokenExpired, refreshToken } from "./token-refresh-Cz-FqDtC.js";
3
+
4
+ //#region src/auth/resolve-token.ts
5
+ /**
6
+ * Stored credentials exist but were issued by a different server than the
7
+ * one this command targets (`--url`/`LEVR_URL`). Callers that can recover
8
+ * (e.g. `levr init` re-logging-in against the new target) match on this
9
+ * class instead of string-sniffing the message.
10
+ */
11
+ var CredentialsMismatchError = class extends Error {
12
+ constructor(storedUrl, activeUrl) {
13
+ super(`Stored credentials are for ${storedUrl}, but this command targets ${activeUrl}. Run 'levr auth login' to authenticate against this server.`);
14
+ this.name = "CredentialsMismatchError";
15
+ }
16
+ };
17
+ /**
18
+ * Resolve auth token in priority order:
19
+ * 1. LEVR_TOKEN env var (PAT) — long-lived, no refresh
20
+ * 2. Stored credentials (JWT) — auto-refresh if expired
21
+ * 3. Error — not authenticated
22
+ */
23
+ async function resolveToken() {
24
+ const pat = getPatToken();
25
+ if (pat) return {
26
+ token: pat,
27
+ type: "pat"
28
+ };
29
+ let creds = readCredentials();
30
+ if (creds) {
31
+ const activeUrl = getApiUrl();
32
+ const storedUrl = creds.api_url.replace(/\/+$/, "");
33
+ if (storedUrl !== activeUrl) throw new CredentialsMismatchError(storedUrl, activeUrl);
34
+ if (isTokenExpired(creds)) {
35
+ const refreshed = await refreshToken(creds);
36
+ if (!refreshed) throw new Error("Token expired and refresh failed. Run 'levr auth login' to re-authenticate.");
37
+ creds = refreshed;
38
+ }
39
+ return {
40
+ token: creds.access_token,
41
+ type: "jwt"
42
+ };
43
+ }
44
+ throw new Error("Not authenticated. Run 'levr auth login' or set LEVR_TOKEN environment variable.");
45
+ }
46
+
47
+ //#endregion
48
+ export { resolveToken };
@@ -1,4 +1,4 @@
1
- import { authGetSitesV1 } from "./sdk-client-rpUEykDw.js";
1
+ import { authGetSitesV1 } from "./sdk-client-BeE6mXns.js";
2
2
  import { clearWorkspace, loadWorkspace, saveWorkspace } from "./workspace-store-4hfvsEHS.js";
3
3
 
4
4
  //#region src/workspace/resolve-workspace.ts