@rankwrangler/cli 0.4.1 → 0.5.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
@@ -11,19 +11,24 @@ npm install -g @rankwrangler/cli
11
11
  ## Usage
12
12
 
13
13
  ```bash
14
- export RR_LICENSE_KEY=rrk_...
15
14
  rw --version
16
15
  rw changelog
16
+ rw auth set rrk_...
17
17
  rw config set storage-dir ~/.config/rankwrangler
18
18
  rw products get B0DV53VS61
19
19
  rw products history B0DV53VS61 --metrics bsr,price
20
+ RR_LICENSE_KEY=rrk_... rw license status
20
21
  # `rankwrangler` is also supported as an alias
21
22
  ```
22
23
 
23
- `RR_LICENSE_KEY` is the API-key source. `rw config set storage-dir <path>` saves the
24
- active storage directory globally. After that, every CLI command reads and writes its non-secret
25
- config/data from that directory, while preserving existing config values when switching.
26
- `RR_STORAGE_DIR`, `RR_API_URL`, and `RR_MARKETPLACE_ID` override saved CLI config when set.
24
+ `rw auth set <licenseKey>` stores the license key in the platform secure store
25
+ (`macOS Keychain` on macOS). `rw auth status` reports whether the CLI will use an env override,
26
+ stored auth, or no auth. `rw auth clear` removes the stored key.
27
+
28
+ `rw config set storage-dir <path>` saves the active storage directory globally. After that, every
29
+ CLI command reads and writes its non-secret config/data from that directory, while preserving
30
+ existing config values when switching. `RR_LICENSE_KEY`, `RR_STORAGE_DIR`, `RR_API_URL`, and
31
+ `RR_MARKETPLACE_ID` override saved CLI state when set for CI, automation, or agent runtimes.
27
32
  `rw --version` prints the installed CLI version. `rw changelog` prints the latest bundled release
28
33
  notes shipped with the package.
29
34
 
package/dist/CHANGELOG.md CHANGED
@@ -2,6 +2,24 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## v0.5.0 - 2026-03-18
6
+
7
+ ### Added
8
+
9
+ - Add CLI secure-store auth with `rw auth set`, `rw auth status`, and `rw auth clear`, storing
10
+ license keys in the platform secure store while keeping `RR_LICENSE_KEY` as an override for
11
+ CI, automation, and agent runtimes.
12
+
13
+ ### Changed
14
+
15
+ - Change local development defaults so the app server no longer starts the job runner unless you
16
+ use the explicit jobs-enabled dev commands.
17
+
18
+ ### Fixed
19
+
20
+ - Fix top search terms scheduler timestamp handling so due datasets are selected and refreshed
21
+ reliably.
22
+
5
23
  ## v0.4.1 - 2026-03-09
6
24
 
7
25
  ### Added
@@ -0,0 +1,86 @@
1
+ export declare const LICENSE_KEY_ENV_VAR = "RR_LICENSE_KEY";
2
+ export declare const AUTH_SERVICE_NAME = "RankWrangler CLI";
3
+ export declare const AUTH_ACCOUNT_NAME = "license-key";
4
+ type CliFail = (code: string, message: string, details?: unknown) => never;
5
+ type AuthCommand = {
6
+ verb: string;
7
+ args: string[];
8
+ };
9
+ export declare class CliAuthError extends Error {
10
+ readonly code: string;
11
+ readonly details?: unknown;
12
+ constructor(code: string, message: string, details?: unknown);
13
+ }
14
+ export declare const resolveApiKey: () => Promise<string | undefined>;
15
+ export declare const runAuthCommand: (command: AuthCommand, fail: CliFail) => Promise<{
16
+ source: string;
17
+ envOverride: boolean;
18
+ secureStore: {
19
+ backend: string;
20
+ serviceName: string;
21
+ accountName: string;
22
+ available: boolean;
23
+ hasStoredLicenseKey: boolean;
24
+ error?: undefined;
25
+ };
26
+ } | {
27
+ source: string;
28
+ envOverride: boolean;
29
+ secureStore: {
30
+ backend: string;
31
+ serviceName: string;
32
+ accountName: string;
33
+ available: boolean;
34
+ hasStoredLicenseKey: null;
35
+ error: string;
36
+ };
37
+ } | {
38
+ saved: boolean;
39
+ source: string;
40
+ envOverride: boolean;
41
+ secureStore: {
42
+ backend: string;
43
+ serviceName: string;
44
+ accountName: string;
45
+ available: boolean;
46
+ hasStoredLicenseKey: boolean;
47
+ error?: undefined;
48
+ };
49
+ } | {
50
+ saved: boolean;
51
+ source: string;
52
+ envOverride: boolean;
53
+ secureStore: {
54
+ backend: string;
55
+ serviceName: string;
56
+ accountName: string;
57
+ available: boolean;
58
+ hasStoredLicenseKey: null;
59
+ error: string;
60
+ };
61
+ } | {
62
+ cleared: boolean;
63
+ source: string;
64
+ envOverride: boolean;
65
+ secureStore: {
66
+ backend: string;
67
+ serviceName: string;
68
+ accountName: string;
69
+ available: boolean;
70
+ hasStoredLicenseKey: boolean;
71
+ error?: undefined;
72
+ };
73
+ } | {
74
+ cleared: boolean;
75
+ source: string;
76
+ envOverride: boolean;
77
+ secureStore: {
78
+ backend: string;
79
+ serviceName: string;
80
+ accountName: string;
81
+ available: boolean;
82
+ hasStoredLicenseKey: null;
83
+ error: string;
84
+ };
85
+ }>;
86
+ export {};
@@ -0,0 +1,171 @@
1
+ import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ export const LICENSE_KEY_ENV_VAR = 'RR_LICENSE_KEY';
4
+ export const AUTH_SERVICE_NAME = 'RankWrangler CLI';
5
+ export const AUTH_ACCOUNT_NAME = 'license-key';
6
+ const INTERNAL_SECRET_STORE_DIR_ENV_VAR = 'RW_INTERNAL_CLI_SECRET_STORE_DIR';
7
+ export class CliAuthError extends Error {
8
+ code;
9
+ details;
10
+ constructor(code, message, details) {
11
+ super(message);
12
+ this.name = 'CliAuthError';
13
+ this.code = code;
14
+ this.details = details;
15
+ }
16
+ }
17
+ export const resolveApiKey = async () => {
18
+ const envLicenseKey = resolveEnvLicenseKey();
19
+ if (envLicenseKey) {
20
+ return envLicenseKey;
21
+ }
22
+ const secureStore = await createSecureStore();
23
+ return (await secureStore.get()) ?? undefined;
24
+ };
25
+ export const runAuthCommand = async (command, fail) => {
26
+ if (command.verb === 'status') {
27
+ return buildAuthStatus();
28
+ }
29
+ if (command.verb === 'set') {
30
+ const value = resolveAuthSetValue(command.args, fail);
31
+ const secureStore = await createSecureStore();
32
+ await secureStore.set(value);
33
+ return {
34
+ ...(await buildAuthStatus()),
35
+ saved: true,
36
+ };
37
+ }
38
+ if (command.verb === 'clear') {
39
+ const secureStore = await createSecureStore();
40
+ const cleared = await secureStore.clear();
41
+ return {
42
+ ...(await buildAuthStatus()),
43
+ cleared,
44
+ };
45
+ }
46
+ fail('UNKNOWN_COMMAND', 'Unknown auth command', { verb: command.verb });
47
+ };
48
+ const buildAuthStatus = async () => {
49
+ const envLicenseKey = resolveEnvLicenseKey();
50
+ try {
51
+ const secureStore = await createSecureStore();
52
+ const storedLicenseKey = await secureStore.get();
53
+ return {
54
+ source: envLicenseKey ? 'env' : storedLicenseKey ? 'secure-store' : 'none',
55
+ envOverride: Boolean(envLicenseKey),
56
+ secureStore: {
57
+ backend: secureStore.backend,
58
+ serviceName: secureStore.serviceName,
59
+ accountName: secureStore.accountName,
60
+ available: true,
61
+ hasStoredLicenseKey: Boolean(storedLicenseKey),
62
+ },
63
+ };
64
+ }
65
+ catch (error) {
66
+ return {
67
+ source: envLicenseKey ? 'env' : 'none',
68
+ envOverride: Boolean(envLicenseKey),
69
+ secureStore: {
70
+ backend: getSecureStoreBackendLabel(),
71
+ serviceName: AUTH_SERVICE_NAME,
72
+ accountName: AUTH_ACCOUNT_NAME,
73
+ available: false,
74
+ hasStoredLicenseKey: null,
75
+ error: error instanceof Error ? error.message : 'Unknown secure-store error',
76
+ },
77
+ };
78
+ }
79
+ };
80
+ const resolveAuthSetValue = (args, fail) => {
81
+ const positionalValue = args.join(' ').trim();
82
+ if (positionalValue) {
83
+ return positionalValue;
84
+ }
85
+ const envLicenseKey = resolveEnvLicenseKey();
86
+ if (envLicenseKey) {
87
+ return envLicenseKey;
88
+ }
89
+ fail('INVALID_INPUT', `auth set requires <licenseKey> or ${LICENSE_KEY_ENV_VAR}`);
90
+ };
91
+ const resolveEnvLicenseKey = () => {
92
+ const value = process.env[LICENSE_KEY_ENV_VAR]?.trim();
93
+ return value ? value : undefined;
94
+ };
95
+ const createSecureStore = async () => {
96
+ const internalSecretStoreDir = process.env[INTERNAL_SECRET_STORE_DIR_ENV_VAR]?.trim();
97
+ if (internalSecretStoreDir) {
98
+ return createFileSecretStore(path.resolve(internalSecretStoreDir));
99
+ }
100
+ const keytar = await loadKeytar();
101
+ return {
102
+ backend: getSecureStoreBackendLabel(),
103
+ serviceName: AUTH_SERVICE_NAME,
104
+ accountName: AUTH_ACCOUNT_NAME,
105
+ get: () => keytar.getPassword(AUTH_SERVICE_NAME, AUTH_ACCOUNT_NAME),
106
+ set: value => keytar.setPassword(AUTH_SERVICE_NAME, AUTH_ACCOUNT_NAME, value),
107
+ clear: () => keytar.deletePassword(AUTH_SERVICE_NAME, AUTH_ACCOUNT_NAME),
108
+ };
109
+ };
110
+ const loadKeytar = async () => {
111
+ try {
112
+ const imported = (await import('keytar'));
113
+ return imported.default ?? imported;
114
+ }
115
+ catch (error) {
116
+ throw new CliAuthError('SECURE_STORE_UNAVAILABLE', 'platform secure store is unavailable', error instanceof Error ? { cause: error.message } : undefined);
117
+ }
118
+ };
119
+ const getSecureStoreBackendLabel = () => {
120
+ switch (process.platform) {
121
+ case 'darwin':
122
+ return 'macos-keychain';
123
+ case 'win32':
124
+ return 'windows-credential-manager';
125
+ default:
126
+ return 'platform-secure-store';
127
+ }
128
+ };
129
+ const createFileSecretStore = (secretStoreDir) => {
130
+ const secretPath = path.join(secretStoreDir, 'license-key.json');
131
+ return {
132
+ backend: 'test-file-store',
133
+ serviceName: AUTH_SERVICE_NAME,
134
+ accountName: AUTH_ACCOUNT_NAME,
135
+ get: async () => {
136
+ try {
137
+ const raw = await readFile(secretPath, 'utf8');
138
+ const parsed = JSON.parse(raw);
139
+ return typeof parsed.licenseKey === 'string' ? parsed.licenseKey : null;
140
+ }
141
+ catch (error) {
142
+ if (error &&
143
+ typeof error === 'object' &&
144
+ 'code' in error &&
145
+ error.code === 'ENOENT') {
146
+ return null;
147
+ }
148
+ throw new CliAuthError('INVALID_CONFIG', 'failed to read secure-store test data');
149
+ }
150
+ },
151
+ set: async (value) => {
152
+ await mkdir(secretStoreDir, { recursive: true });
153
+ await writeFile(secretPath, `${JSON.stringify({ licenseKey: value }, null, 2)}\n`, 'utf8');
154
+ },
155
+ clear: async () => {
156
+ try {
157
+ await rm(secretPath);
158
+ return true;
159
+ }
160
+ catch (error) {
161
+ if (error &&
162
+ typeof error === 'object' &&
163
+ 'code' in error &&
164
+ error.code === 'ENOENT') {
165
+ return false;
166
+ }
167
+ throw error;
168
+ }
169
+ },
170
+ };
171
+ };
@@ -0,0 +1,19 @@
1
+ import { type CliConfig, type CliPaths } from './cli-config';
2
+ type CliFail = (code: string, message: string, details?: unknown) => never;
3
+ type ConfigCommand = {
4
+ verb: string;
5
+ args: string[];
6
+ };
7
+ export declare const runConfigCommand: (command: ConfigCommand, config: CliConfig, paths: CliPaths, fail: CliFail) => Promise<{
8
+ storageDir: string;
9
+ path: string;
10
+ globalPath: string;
11
+ config: CliConfig;
12
+ } | {
13
+ cleared: boolean;
14
+ storageDir: string;
15
+ path: string;
16
+ globalPath: string;
17
+ config: CliConfig;
18
+ }>;
19
+ export {};
@@ -0,0 +1,56 @@
1
+ import { clearConfig, saveConfig, switchStorageDir } from './cli-config.js';
2
+ import { normalizeBaseUrl } from './cli-options.js';
3
+ export const runConfigCommand = async (command, config, paths, fail) => {
4
+ if (command.verb === 'show') {
5
+ return buildConfigResponse(paths, config);
6
+ }
7
+ if (command.verb === 'clear') {
8
+ await clearConfig(paths);
9
+ return {
10
+ ...buildConfigResponse(paths, {}),
11
+ cleared: true,
12
+ };
13
+ }
14
+ if (command.verb !== 'set') {
15
+ fail('UNKNOWN_COMMAND', 'Unknown config command', { verb: command.verb });
16
+ }
17
+ const [key, ...valueParts] = command.args;
18
+ if (!key || valueParts.length === 0) {
19
+ fail('INVALID_INPUT', 'config set requires <key> <value>');
20
+ }
21
+ const value = valueParts.join(' ').trim();
22
+ if (!value) {
23
+ fail('INVALID_INPUT', 'config set value cannot be empty');
24
+ }
25
+ if (key === 'storage-dir') {
26
+ const nextState = await switchStorageDir({
27
+ currentConfig: config,
28
+ currentPaths: paths,
29
+ requestedStorageDir: value,
30
+ });
31
+ return buildConfigResponse(nextState.paths, nextState.config);
32
+ }
33
+ const nextConfig = { ...config };
34
+ if (key === 'base-url') {
35
+ nextConfig.baseUrl = normalizeBaseUrl(value, fail);
36
+ }
37
+ else if (key === 'marketplace') {
38
+ nextConfig.marketplaceId = value;
39
+ }
40
+ else {
41
+ fail('INVALID_INPUT', 'unsupported config key', {
42
+ key,
43
+ supportedKeys: ['base-url', 'marketplace', 'storage-dir'],
44
+ });
45
+ }
46
+ await saveConfig(paths, nextConfig);
47
+ return buildConfigResponse(paths, nextConfig);
48
+ };
49
+ const buildConfigResponse = (paths, config) => {
50
+ return {
51
+ storageDir: paths.storageDir,
52
+ path: paths.configPath,
53
+ globalPath: paths.globalConfigPath,
54
+ config,
55
+ };
56
+ };
@@ -22,7 +22,6 @@ export declare const resolveHistoryWindow: (values: CliOptionValues, fail: CliFa
22
22
  endAt?: string | undefined;
23
23
  startAt?: string | undefined;
24
24
  };
25
- export declare const resolveApiKey: () => string | undefined;
26
25
  export declare const resolveBaseUrl: (values: CliOptionValues, config: CliConfig, defaultBaseUrl: string, fail: CliFail) => string;
27
26
  export declare const normalizeBaseUrl: (value: string, fail: CliFail) => string;
28
27
  export declare const parseIntegerOption: ({ value, optionName, min, max, defaultValue, }: {
@@ -56,9 +56,6 @@ export const resolveHistoryWindow = (values, fail) => {
56
56
  }, fail),
57
57
  };
58
58
  };
59
- export const resolveApiKey = () => {
60
- return process.env.RR_LICENSE_KEY;
61
- };
62
59
  export const resolveBaseUrl = (values, config, defaultBaseUrl, fail) => {
63
60
  return normalizeBaseUrl(values.baseUrl ?? config.baseUrl ?? process.env.RR_API_URL ?? defaultBaseUrl, fail);
64
61
  };
@@ -1,3 +1,4 @@
1
+ import { CliAuthError } from './cli-auth.js';
1
2
  import { CliStorageError } from './cli-config.js';
2
3
  const OUTPUT_PRETTY = true;
3
4
  export const printSuccess = (data) => {
@@ -27,6 +28,13 @@ export const resolveError = (error) => {
27
28
  details: error.details,
28
29
  };
29
30
  }
31
+ if (error instanceof CliAuthError) {
32
+ return {
33
+ code: error.code,
34
+ message: error.message,
35
+ details: error.details,
36
+ };
37
+ }
30
38
  if (error && typeof error === 'object' && 'data' in error) {
31
39
  const data = error.data;
32
40
  if (data?.code) {
package/dist/index.js CHANGED
@@ -1,9 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import { parseArgs } from 'node:util';
3
3
  import { createRankWranglerClient, DEFAULT_API_BASE_URL } from '@rankwrangler/http-client';
4
+ import { resolveApiKey, runAuthCommand } from './cli-auth.js';
4
5
  import { printBundledChangelog, printCliVersion } from './cli-metadata.js';
5
- import { clearConfig, loadCliContext, loadCliPathsOrDefault, saveConfig, switchStorageDir, } from './cli-config.js';
6
- import { normalizeBaseUrl, parseIntegerOption, requireAsins, requireMarketplaceId, requireSingleAsin, resolveApiKey, resolveBaseUrl, resolveHistoryMetrics, resolveHistoryWindow, } from './cli-options.js';
6
+ import { runConfigCommand } from './cli-config-command.js';
7
+ import { loadCliContext, loadCliPathsOrDefault } from './cli-config.js';
8
+ import { parseIntegerOption, requireAsins, requireMarketplaceId, requireSingleAsin, resolveBaseUrl, resolveHistoryMetrics, resolveHistoryWindow, } from './cli-options.js';
7
9
  import { fail, printSuccess, resolveError } from './cli-output.js';
8
10
  import { buildCliHistoryResponse } from './history-response.js';
9
11
  import { printUsage } from './usage.js';
@@ -12,6 +14,9 @@ const SUPPORTED_COMMANDS = new Set([
12
14
  'products:history',
13
15
  'license:status',
14
16
  'license:validate',
17
+ 'auth:status',
18
+ 'auth:set',
19
+ 'auth:clear',
15
20
  'config:show',
16
21
  'config:clear',
17
22
  'config:set',
@@ -57,12 +62,16 @@ const main = async () => {
57
62
  }
58
63
  const { config, paths } = await loadCliContext();
59
64
  if (command.resource === 'config') {
60
- printSuccess(await runConfigCommand(command, config, paths));
65
+ printSuccess(await runConfigCommand(command, config, paths, fail));
61
66
  return;
62
67
  }
63
- const apiKey = resolveApiKey();
68
+ if (command.resource === 'auth') {
69
+ printSuccess(await runAuthCommand(command, fail));
70
+ return;
71
+ }
72
+ const apiKey = await resolveApiKey();
64
73
  if (!apiKey) {
65
- fail('MISSING_CONFIG', 'api key is required. set RR_LICENSE_KEY');
74
+ fail('MISSING_CONFIG', 'license key is required. run `rw auth set <licenseKey>` or set RR_LICENSE_KEY');
66
75
  }
67
76
  const client = createRankWranglerClient({
68
77
  baseUrl: resolveBaseUrl(optionValues, config, DEFAULT_API_BASE_URL, fail),
@@ -121,60 +130,6 @@ const runProductHistoryCommand = async (command, client, config) => {
121
130
  response: response,
122
131
  });
123
132
  };
124
- const runConfigCommand = async (command, config, paths) => {
125
- if (command.verb === 'show') {
126
- return buildConfigResponse(paths, config);
127
- }
128
- if (command.verb === 'clear') {
129
- await clearConfig(paths);
130
- return {
131
- ...buildConfigResponse(paths, {}),
132
- cleared: true,
133
- };
134
- }
135
- if (command.verb !== 'set') {
136
- fail('UNKNOWN_COMMAND', 'Unknown config command', { verb: command.verb });
137
- }
138
- const [key, ...valueParts] = command.args;
139
- if (!key || valueParts.length === 0) {
140
- fail('INVALID_INPUT', 'config set requires <key> <value>');
141
- }
142
- const value = valueParts.join(' ').trim();
143
- if (!value) {
144
- fail('INVALID_INPUT', 'config set value cannot be empty');
145
- }
146
- if (key === 'storage-dir') {
147
- const nextState = await switchStorageDir({
148
- currentConfig: config,
149
- currentPaths: paths,
150
- requestedStorageDir: value,
151
- });
152
- return buildConfigResponse(nextState.paths, nextState.config);
153
- }
154
- const nextConfig = { ...config };
155
- if (key === 'base-url') {
156
- nextConfig.baseUrl = normalizeBaseUrl(value, fail);
157
- }
158
- else if (key === 'marketplace') {
159
- nextConfig.marketplaceId = value;
160
- }
161
- else {
162
- fail('INVALID_INPUT', 'unsupported config key', {
163
- key,
164
- supportedKeys: ['base-url', 'marketplace', 'storage-dir'],
165
- });
166
- }
167
- await saveConfig(paths, nextConfig);
168
- return buildConfigResponse(paths, nextConfig);
169
- };
170
- const buildConfigResponse = (paths, config) => {
171
- return {
172
- storageDir: paths.storageDir,
173
- path: paths.configPath,
174
- globalPath: paths.globalConfigPath,
175
- config,
176
- };
177
- };
178
133
  const resolveMetaCommand = (inputPositionals) => {
179
134
  if (inputPositionals.length === 1 && inputPositionals[0] === 'changelog') {
180
135
  return { name: 'changelog' };
package/dist/usage.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { LICENSE_KEY_ENV_VAR } from './cli-auth.js';
1
2
  const DEFAULT_MARKETPLACE_ID = 'ATVPDKIKX0DER';
2
3
  export const printUsage = (paths) => {
3
4
  const usage = [
@@ -18,6 +19,9 @@ export const printUsage = (paths) => {
18
19
  ' products history <ASIN>',
19
20
  ' license status',
20
21
  ' license validate',
22
+ ' auth status',
23
+ ' auth set <licenseKey>',
24
+ ' auth clear',
21
25
  ' config show',
22
26
  ' config clear',
23
27
  ' config set base-url <origin>',
@@ -41,7 +45,7 @@ export const printUsage = (paths) => {
41
45
  ` ${paths.globalConfigPath} Global CLI settings`,
42
46
  '',
43
47
  'ENVIRONMENT',
44
- ' RR_LICENSE_KEY API key source',
48
+ ` ${LICENSE_KEY_ENV_VAR} License key override for CI/automation`,
45
49
  ' RR_API_URL API origin fallback',
46
50
  ' RR_MARKETPLACE_ID Marketplace fallback',
47
51
  ' RR_STORAGE_DIR Storage directory override',
@@ -50,7 +54,8 @@ export const printUsage = (paths) => {
50
54
  ' RR_HISTORY_METRICS Comma-separated history metrics fallback',
51
55
  '',
52
56
  'EXAMPLES',
53
- ' RR_LICENSE_KEY=rrk_... rw products get B0DV53VS61',
57
+ ' rw auth set rrk_...',
58
+ ` ${LICENSE_KEY_ENV_VAR}=rrk_... rw products get B0DV53VS61`,
54
59
  ' RR_STORAGE_DIR=~/.config/rankwrangler rw config show',
55
60
  ' rw --version',
56
61
  ' rw changelog',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rankwrangler/cli",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "Official RankWrangler command line interface",
5
5
  "type": "module",
6
6
  "bin": {
@@ -23,7 +23,8 @@
23
23
  "access": "public"
24
24
  },
25
25
  "dependencies": {
26
- "@rankwrangler/http-client": "^0.4.1"
26
+ "@rankwrangler/http-client": "^0.5.0",
27
+ "keytar": "^7.9.0"
27
28
  },
28
29
  "devDependencies": {
29
30
  "@types/node": "^24.3.0",