@connxio/cli 0.1.4 → 0.1.5

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
@@ -40,8 +40,6 @@ You will be prompted for:
40
40
  - OAuth scope, defaulting to `api://connxio/.default`
41
41
  - OAuth client secret
42
42
 
43
- The OAuth token URL is always `https://api.connxio.com/oauth/token`.
44
-
45
43
  Check status:
46
44
 
47
45
  ```bash
@@ -65,8 +63,6 @@ CONNXIO_OAUTH_SCOPE="api://connxio/.default"
65
63
 
66
64
  `CONNXIO_OAUTH_TOKEN_URL` and `CONNXIO_OAUTH_SCOPE` are optional overrides.
67
65
 
68
- Do not commit OAuth client secrets or put them in MCP client config unless you explicitly accept that local setup tradeoff.
69
-
70
66
  ## Configure Contexts
71
67
 
72
68
  A context represents one Connxio subscription. Connxio API keys are subscription-scoped, so add one context for each subscription you want to use.
@@ -101,17 +97,19 @@ VS Code 1.99+ supports MCP servers natively via GitHub Copilot agent mode.
101
97
 
102
98
  **Option 1 — user settings (all projects)**
103
99
 
104
- Open `settings.json` (`Cmd+Shift+P` _Preferences: Open User Settings (JSON)_) and add:
100
+ Open the command palette (`Ctrl+Shift+P`/`Cmd+Shift+P`) and select `MCP: Add server`.
101
+
102
+ Select `stdio` transport, `connxio mcp serve` as the command, and set the name to `connxio`. This will create a user-level MCP server registration that is available in all projects.
103
+
104
+ Alternatively, edit the user level mcp.json config file directly. Open the command palette and select `MCP: Open User Configuration`, then add:
105
105
 
106
106
  ```json
107
107
  {
108
- "mcp": {
109
- "servers": {
110
- "connxio": {
111
- "type": "stdio",
112
- "command": "connxio",
113
- "args": ["mcp", "serve"]
114
- }
108
+ "servers": {
109
+ "connxio": {
110
+ "type": "stdio",
111
+ "command": "connxio",
112
+ "args": ["mcp", "serve"]
115
113
  }
116
114
  }
117
115
  }
@@ -133,7 +131,7 @@ Create `.vscode/mcp.json` in your project root:
133
131
  }
134
132
  ```
135
133
 
136
- After saving, open a Copilot chat in agent mode (`@workspace` or the **Agent** dropdown) and the Connxio tools will be available.
134
+ The Connxio MCP server will now be available when you open this project in VS Code.
137
135
 
138
136
  If you need to pass environment variables (e.g. for OAuth or a custom API base URL), add an `env` block:
139
137
 
@@ -159,6 +157,12 @@ Do not commit files containing OAuth client secrets to source control.
159
157
 
160
158
  Register the installed MCP server:
161
159
 
160
+ ```bash
161
+ claude mcp add --transport stdio connxio -- connxio mcp serve
162
+ ```
163
+
164
+ To register the MCP server at user level (available in all projects), add `--scope user`:
165
+
162
166
  ```bash
163
167
  claude mcp add --scope user --transport stdio connxio -- connxio mcp serve
164
168
  ```
@@ -173,7 +177,7 @@ If you need to replace an existing registration:
173
177
 
174
178
  ```bash
175
179
  claude mcp remove connxio
176
- claude mcp add --scope user --transport stdio connxio -- connxio mcp serve
180
+ claude mcp add --transport stdio connxio -- connxio mcp serve
177
181
  ```
178
182
 
179
183
  ## API Base URL
@@ -271,4 +275,6 @@ Non-secret config is stored in:
271
275
  - macOS/Linux: `${XDG_CONFIG_HOME:-~/.config}/connxio/config.json`
272
276
  - Windows: `%APPDATA%\connxio\config.json`
273
277
 
274
- Secrets are stored through the CLI credential abstraction. The current implementation uses a local credential store at the same config root so it can later move to OS keychain storage without changing context consumers.
278
+ Secrets are stored through the CLI credential abstraction. The CLI now uses the OS keyring when available and falls back to `${XDG_CONFIG_HOME:-~/.config}/connxio/credentials.json` on macOS/Linux or `%APPDATA%\connxio\credentials.json` on Windows when secure storage is unavailable.
279
+
280
+ If you already have secrets in `credentials.json`, the CLI migrates them into the OS keyring lazily as they are used and removes the legacy file entries after a successful migration.
package/dist/index.mjs CHANGED
@@ -11222,7 +11222,7 @@ function updateNotifier(options) {
11222
11222
  //#endregion
11223
11223
  //#region packages/cli/package.json
11224
11224
  var name = "@connxio/cli";
11225
- var version$1 = "0.1.4";
11225
+ var version$1 = "0.1.5";
11226
11226
  var package_default = {
11227
11227
  name,
11228
11228
  version: version$1,
@@ -11252,6 +11252,7 @@ var package_default = {
11252
11252
  },
11253
11253
  dependencies: {
11254
11254
  "@modelcontextprotocol/sdk": "^1.29.0",
11255
+ "@napi-rs/keyring": "^1.3.0",
11255
11256
  "commander": "^14.0.3",
11256
11257
  "update-notifier": "^7.3.1",
11257
11258
  "yazl": "^3.3.1",
@@ -11337,51 +11338,157 @@ function isNodeError$1(error) {
11337
11338
  }
11338
11339
  //#endregion
11339
11340
  //#region packages/cli/src/connxio/credentials.ts
11340
- function getCredentialStoreDescription() {
11341
- return `local file (${getCredentialPath()})`;
11341
+ const KEYRING_SERVICE_NAME = "com.connxio.cli";
11342
+ let credentialBackendPromise;
11343
+ let fileFallbackWarningEmitted = false;
11344
+ async function getCredentialStoreDescription() {
11345
+ return (await getCredentialBackend()).description;
11342
11346
  }
11343
11347
  async function deleteApiKey(ref) {
11344
- const store = await readCredentialFile();
11345
- delete store.apiKeys[ref];
11346
- await writeCredentialFile(store);
11348
+ await deleteCredential("apiKey", ref);
11347
11349
  }
11348
11350
  async function deleteOAuthClientSecret(ref) {
11349
- const store = await readCredentialFile();
11350
- delete store.oauthClientSecrets[ref];
11351
- await writeCredentialFile(store);
11351
+ await deleteCredential("oauthClientSecret", ref);
11352
11352
  }
11353
11353
  async function getApiKey(ref) {
11354
- const apiKey = (await readCredentialFile()).apiKeys[ref];
11355
- if (!apiKey) throw new Error(`Missing credential for context ${ref}. Run \`connxio context add ${ref}\`.`);
11356
- return apiKey;
11354
+ return getCredential("apiKey", ref);
11357
11355
  }
11358
11356
  async function getOAuthClientSecret(ref) {
11359
- const clientSecret = (await readCredentialFile()).oauthClientSecrets[ref];
11360
- if (!clientSecret) throw new Error("Missing OAuth client secret. Run `connxio auth configure`.");
11361
- return clientSecret;
11357
+ return getCredential("oauthClientSecret", ref);
11362
11358
  }
11363
11359
  async function hasApiKey(ref) {
11364
- const store = await readCredentialFile();
11365
- return typeof store.apiKeys[ref] === "string" && store.apiKeys[ref].length > 0;
11360
+ return hasCredential("apiKey", ref);
11366
11361
  }
11367
11362
  async function hasOAuthClientSecret(ref) {
11368
- const store = await readCredentialFile();
11369
- return typeof store.oauthClientSecrets[ref] === "string" && store.oauthClientSecrets[ref].length > 0;
11363
+ return hasCredential("oauthClientSecret", ref);
11370
11364
  }
11371
11365
  async function setApiKey(ref, apiKey) {
11372
- const trimmed = apiKey.trim();
11373
- if (!trimmed) throw new Error("API key cannot be empty.");
11374
- const store = await readCredentialFile();
11375
- store.apiKeys[ref] = trimmed;
11376
- await writeCredentialFile(store);
11366
+ await setCredential("apiKey", ref, apiKey);
11377
11367
  }
11378
11368
  async function setOAuthClientSecret(ref, clientSecret) {
11379
- const trimmed = clientSecret.trim();
11380
- if (!trimmed) throw new Error("OAuth client secret cannot be empty.");
11381
- const store = await readCredentialFile();
11382
- store.oauthClientSecrets[ref] = trimmed;
11383
- await writeCredentialFile(store);
11369
+ await setCredential("oauthClientSecret", ref, clientSecret);
11370
+ }
11371
+ async function deleteCredential(kind, ref) {
11372
+ const backend = await getCredentialBackend();
11373
+ await backend.delete(kind, ref);
11374
+ if (backend.type === "keyring") await deleteLegacyCredentialQuietly(kind, ref);
11375
+ }
11376
+ async function getCredential(kind, ref) {
11377
+ const backend = await getCredentialBackend();
11378
+ const credential = await backend.get(kind, ref);
11379
+ if (credential) return credential;
11380
+ if (backend.type === "keyring") {
11381
+ const legacyCredential = await fileCredentialBackend.get(kind, ref);
11382
+ if (legacyCredential) {
11383
+ await backend.set(kind, ref, legacyCredential);
11384
+ await deleteLegacyCredentialQuietly(kind, ref);
11385
+ return legacyCredential;
11386
+ }
11387
+ }
11388
+ throw new Error(getMissingCredentialMessage(kind, ref));
11389
+ }
11390
+ async function getCredentialBackend() {
11391
+ credentialBackendPromise ??= resolveCredentialBackend();
11392
+ return credentialBackendPromise;
11393
+ }
11394
+ function getCredentialFileKey(kind) {
11395
+ return kind === "apiKey" ? "apiKeys" : "oauthClientSecrets";
11396
+ }
11397
+ function getKeyringAccountName(kind, ref) {
11398
+ return kind === "apiKey" ? `api-key:${ref}` : `oauth-client-secret:${ref}`;
11399
+ }
11400
+ function getMissingCredentialMessage(kind, ref) {
11401
+ return kind === "apiKey" ? `Missing credential for context ${ref}. Run \`connxio context add ${ref}\`.` : "Missing OAuth client secret. Run `connxio auth configure`.";
11402
+ }
11403
+ async function hasCredential(kind, ref) {
11404
+ const backend = await getCredentialBackend();
11405
+ if (await backend.has(kind, ref)) return true;
11406
+ return backend.type === "keyring" ? fileCredentialBackend.has(kind, ref) : false;
11407
+ }
11408
+ function normalizeKeyringModule(value) {
11409
+ const candidate = isRecord$3(value) ? value : void 0;
11410
+ const defaultCandidate = candidate && Object.prototype.hasOwnProperty.call(candidate, "default") && isRecord$3(candidate["default"]) ? candidate["default"] : void 0;
11411
+ const normalizedCandidate = defaultCandidate ? {
11412
+ ...defaultCandidate,
11413
+ ...candidate
11414
+ } : candidate;
11415
+ if (!normalizedCandidate || typeof normalizedCandidate.AsyncEntry !== "function" || typeof normalizedCandidate.findCredentialsAsync !== "function") throw new Error("@napi-rs/keyring did not expose the expected API.");
11416
+ return {
11417
+ AsyncEntry: normalizedCandidate.AsyncEntry,
11418
+ findCredentialsAsync: normalizedCandidate.findCredentialsAsync
11419
+ };
11420
+ }
11421
+ async function resolveCredentialBackend() {
11422
+ try {
11423
+ const keyring = normalizeKeyringModule(await import("@napi-rs/keyring"));
11424
+ await keyring.findCredentialsAsync(KEYRING_SERVICE_NAME);
11425
+ return createKeyringCredentialBackend(keyring);
11426
+ } catch (error) {
11427
+ warnFileFallback(error);
11428
+ return fileCredentialBackend;
11429
+ }
11430
+ }
11431
+ async function setCredential(kind, ref, value) {
11432
+ const trimmed = value.trim();
11433
+ if (!trimmed) throw new Error(kind === "apiKey" ? "API key cannot be empty." : "OAuth client secret cannot be empty.");
11434
+ const backend = await getCredentialBackend();
11435
+ await backend.set(kind, ref, trimmed);
11436
+ if (backend.type === "keyring") await deleteLegacyCredentialQuietly(kind, ref);
11437
+ }
11438
+ function warnFileFallback(error) {
11439
+ if (fileFallbackWarningEmitted) return;
11440
+ fileFallbackWarningEmitted = true;
11441
+ console.warn(`Connxio secure credential storage unavailable; falling back to local file storage at ${getCredentialPath()}: ${formatError$4(error)}`);
11442
+ }
11443
+ async function deleteLegacyCredentialQuietly(kind, ref) {
11444
+ try {
11445
+ await fileCredentialBackend.delete(kind, ref);
11446
+ } catch {}
11447
+ }
11448
+ function createKeyringCredentialBackend(keyring) {
11449
+ const get = async (kind, ref) => {
11450
+ const credential = await new keyring.AsyncEntry(KEYRING_SERVICE_NAME, getKeyringAccountName(kind, ref)).getPassword();
11451
+ return typeof credential === "string" && credential.length > 0 ? credential : void 0;
11452
+ };
11453
+ return {
11454
+ description: "OS keyring",
11455
+ type: "keyring",
11456
+ async delete(kind, ref) {
11457
+ const entry = new keyring.AsyncEntry(KEYRING_SERVICE_NAME, getKeyringAccountName(kind, ref));
11458
+ if (await get(kind, ref) === void 0) return;
11459
+ await entry.deleteCredential();
11460
+ },
11461
+ get,
11462
+ async has(kind, ref) {
11463
+ return await get(kind, ref) !== void 0;
11464
+ },
11465
+ async set(kind, ref, value) {
11466
+ await new keyring.AsyncEntry(KEYRING_SERVICE_NAME, getKeyringAccountName(kind, ref)).setPassword(value);
11467
+ }
11468
+ };
11384
11469
  }
11470
+ const fileCredentialBackend = {
11471
+ description: `local file (${getCredentialPath()})`,
11472
+ type: "file",
11473
+ async delete(kind, ref) {
11474
+ const store = await readCredentialFile();
11475
+ delete store[getCredentialFileKey(kind)][ref];
11476
+ await writeCredentialFile(store);
11477
+ },
11478
+ async get(kind, ref) {
11479
+ const credential = (await readCredentialFile())[getCredentialFileKey(kind)][ref];
11480
+ return typeof credential === "string" && credential.length > 0 ? credential : void 0;
11481
+ },
11482
+ async has(kind, ref) {
11483
+ const credential = (await readCredentialFile())[getCredentialFileKey(kind)][ref];
11484
+ return typeof credential === "string" && credential.length > 0;
11485
+ },
11486
+ async set(kind, ref, value) {
11487
+ const store = await readCredentialFile();
11488
+ store[getCredentialFileKey(kind)][ref] = value;
11489
+ await writeCredentialFile(store);
11490
+ }
11491
+ };
11385
11492
  function getCredentialPath() {
11386
11493
  return path.join(getConfigDir(), "credentials.json");
11387
11494
  }
@@ -11433,6 +11540,9 @@ function isStringRecord(value) {
11433
11540
  function isNodeError(error) {
11434
11541
  return error instanceof Error && "code" in error;
11435
11542
  }
11543
+ function formatError$4(error) {
11544
+ return error instanceof Error ? error.message : String(error);
11545
+ }
11436
11546
  //#endregion
11437
11547
  //#region packages/cli/src/connxio/http.ts
11438
11548
  function allowInsecureTlsIfLocal(url) {
@@ -33187,7 +33297,7 @@ function registerMcpCommands(program) {
33187
33297
  const oauth = await getOAuthStatus();
33188
33298
  const problems = [];
33189
33299
  console.log(`Config: ${getConfigPath()}`);
33190
- console.log(`Credential store: ${getCredentialStoreDescription()}`);
33300
+ console.log(`Credential store: ${await getCredentialStoreDescription()}`);
33191
33301
  if (!oauth.configured) problems.push("OAuth is not configured. Run `connxio auth configure`.");
33192
33302
  else if (!oauth.hasClientSecret) problems.push("OAuth client secret is missing. Run `connxio auth configure`.");
33193
33303
  if (contexts.length === 0) problems.push("No contexts configured. Run `connxio context add`.");