@rankwrangler/cli 0.6.0 → 0.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/README.md CHANGED
@@ -1,75 +1,24 @@
1
1
  # @rankwrangler/cli
2
2
 
3
- Official CLI for RankWrangler.
4
-
5
- ## Install
3
+ Official agent-oriented CLI for RankWrangler.
6
4
 
7
5
  ```bash
8
6
  npm install -g @rankwrangler/cli
9
- ```
10
-
11
- ## Usage
12
-
13
- ```bash
14
- rw --version
15
- rw changelog
16
- rw auth set rrk_...
17
- printf %s "$RR_LICENSE_KEY" | rw auth set --stdin
18
- rw config set storage-dir ~/.config/rankwrangler
19
- rw config get marketplace
20
- rw config unset marketplace
21
- rw products get B0DV53VS61
22
- rw products history B0DV53VS61 --metrics bsr,price --bucket week
23
- RR_LICENSE_KEY=rrk_... rw license status
24
- # `rankwrangler` is also supported as an alias
25
- ```
26
-
27
- `rw auth set [licenseKey]` stores the license key in the platform secure store
28
- (`macOS Keychain` on macOS). `rw auth status` reports whether the CLI will use an env override,
29
- stored auth, or no auth. `rw auth set --stdin` reads the key from standard input for agents and
30
- scripts. `rw auth clear` removes the stored key.
31
-
32
- `rw config set storage-dir <path>` saves the active storage directory globally. After that, every
33
- CLI command reads and writes its non-secret config/data from that directory, while preserving
34
- existing config values when switching. `rw config get <key>`, `rw config unset <key>`, and
35
- `rw config reset` inspect or remove non-secret config without touching stored auth.
36
- `RR_LICENSE_KEY`, `RR_STORAGE_DIR`, `RR_API_URL`, and `RR_MARKETPLACE_ID` override saved CLI state
37
- when set for CI, automation, or agent runtimes.
38
- `rw products history` ensures product/history data before returning bucketed history (`auto`,
39
- `day`, `week`, or `month`), not raw point series. `rw --version` prints the installed CLI version.
40
- `rw changelog` prints the latest bundled release notes shipped with the package.
41
7
 
42
- ## Development
43
-
44
- ```bash
45
- bun run cli:build
46
- bun run cli:test:e2e
8
+ rw auth set ak_...
9
+ rw product get B0DV53VS61
10
+ rw product search "retro gardening shirt"
11
+ rw product history B0DV53VS61 --bucket week
12
+ rw keyword get "retro gardening shirt"
13
+ rw keyword search "retro gardening"
14
+ rw keyword history "retro gardening shirt" --rangeDays 90
47
15
  ```
48
16
 
49
- ## Release (Maintainers)
50
-
51
- From repo root:
52
-
53
- ```bash
54
- bun run release:bump patch
55
- bun install
56
- bun run release:collect-changelog-context
57
- # draft CHANGELOG.md entry from commit context
58
- bun run release:check
59
- bun run cli:build
60
- bun run release:check-cli-pack
61
- ```
62
-
63
- Then publish:
64
-
65
- Publish `packages/http-client` first. `packages/cli` now fails fast on `npm publish` until the
66
- matching `@rankwrangler/http-client` version is already available on npm.
67
-
68
- ```bash
69
- cd packages/cli
70
- node ../../scripts/release/with-npm-token.mjs npm whoami --userconfig ../../.npmrc
71
- node ../../scripts/release/with-npm-token.mjs npm publish --access public --userconfig ../../.npmrc
72
- ```
17
+ The public command surface is Product `get/search/history`, keyword `get/search/history`, auth,
18
+ config, version, and changelog. Product `get/history` and keyword reads wait for policy-current
19
+ data and expose no refresh control; Product search retains its separate search contract. Search
20
+ data contains `keyword`, `searchedAt`, and compact results with `organicSearchPlacement`. Output is
21
+ a JSON `{ ok, data }` envelope; failures use `{ ok: false, error }` on stderr.
22
+ The CLI does not expose Catalog, Operation, provider status, or polling commands.
73
23
 
74
- On macOS, the helper reads `NPM_TOKEN` from the environment first and otherwise falls back to the
75
- Keychain item `rankwrangler-npm-token` for account `$USER`.
24
+ See the [CLI reference](../../docs/reference/cli.md) for options and release workflow.
package/dist/CHANGELOG.md CHANGED
@@ -2,6 +2,42 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## v0.8.0 - 2026-08-09
6
+
7
+ ### Added
8
+
9
+ - Add synchronous basic Product lookup for up to 200 marketplace/ASIN pairs through the public
10
+ API and hosted MCP, with smart single- or multi-ASIN routing in `rw product get`.
11
+ - Add complete Product Search, source provenance, and nullable merch-listing classification to the
12
+ public Product intelligence surface.
13
+
14
+ ### Changed
15
+
16
+ - Resolve public Product reads to policy-current, source-attributed results while batching cold
17
+ SP-API catalog work by marketplace and persisting every requested Product identity.
18
+
19
+ ### Fixed
20
+
21
+ - Preserve unavailable listing data, stop repeatedly polling unavailable listings, and keep those
22
+ Products accessible from the dashboard drawer.
23
+ - Expose the hosted MCP ingress correctly in production.
24
+
25
+ ## v0.7.0 - 2026-08-06
26
+
27
+ ### Added
28
+
29
+ - Add typed Product and keyword `get`, `search`, and `history` contracts for agents and integrations.
30
+ - Add Merchbase OAuth bearer authentication and centralized RankWrangler access metering across
31
+ public clients.
32
+
33
+ ### Changed
34
+
35
+ - Breaking: replace the previous public client surface with the grouped `product.get`,
36
+ `product.search`, `product.history`, `keyword.get`, `keyword.search`, and `keyword.history`
37
+ procedures.
38
+ - Return final source-attributed data and freshness through the public API while keeping Catalog,
39
+ Operation, polling, and provider orchestration internal.
40
+
5
41
  ## v0.6.0 - 2026-06-15
6
42
 
7
43
  ### Added
@@ -220,4 +256,5 @@ All notable changes to this project will be documented in this file.
220
256
 
221
257
  - Rename and standardize the typed npm client package to `@merchbase/rankwrangler-http-client`.
222
258
  - Set initial HTTP client package release to `0.1.0` in `packages/http-client/package.json`.
223
- - Document npm publish and versioning policy for the typed HTTP client in `docs/http-client-spec.md`.
259
+ - Document npm publish and versioning policy for the typed HTTP client; the current owner is
260
+ `docs/reference/http-client.md`.
@@ -0,0 +1,5 @@
1
+ export declare class CliAuthError extends Error {
2
+ readonly code: string;
3
+ readonly details?: unknown;
4
+ constructor(code: string, message: string, details?: unknown);
5
+ }
@@ -0,0 +1,10 @@
1
+ export class CliAuthError extends Error {
2
+ code;
3
+ details;
4
+ constructor(code, message, details) {
5
+ super(message);
6
+ this.name = 'CliAuthError';
7
+ this.code = code;
8
+ this.details = details;
9
+ }
10
+ }
@@ -1,6 +1,6 @@
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";
1
+ export declare const API_KEY_ENV_VAR = "MERCHBASE_API_KEY";
2
+ export declare const AUTH_SERVICE_NAME = "co.merchbase.cli";
3
+ export declare const AUTH_ACCOUNT_NAME = "api-key";
4
4
  type CliFail = (code: string, message: string, details?: unknown) => never;
5
5
  type AuthCommand = {
6
6
  verb: string;
@@ -9,11 +9,6 @@ type AuthCommand = {
9
9
  type AuthCommandOptions = {
10
10
  stdin?: boolean;
11
11
  };
12
- export declare class CliAuthError extends Error {
13
- readonly code: string;
14
- readonly details?: unknown;
15
- constructor(code: string, message: string, details?: unknown);
16
- }
17
12
  export declare const resolveApiKey: () => Promise<string | undefined>;
18
13
  export declare const runAuthCommand: (command: AuthCommand, fail: CliFail, options?: AuthCommandOptions) => Promise<{
19
14
  source: string;
@@ -23,7 +18,7 @@ export declare const runAuthCommand: (command: AuthCommand, fail: CliFail, optio
23
18
  serviceName: string;
24
19
  accountName: string;
25
20
  available: boolean;
26
- hasStoredLicenseKey: boolean;
21
+ hasStoredApiKey: boolean;
27
22
  error?: undefined;
28
23
  };
29
24
  } | {
@@ -34,7 +29,7 @@ export declare const runAuthCommand: (command: AuthCommand, fail: CliFail, optio
34
29
  serviceName: string;
35
30
  accountName: string;
36
31
  available: boolean;
37
- hasStoredLicenseKey: null;
32
+ hasStoredApiKey: null;
38
33
  error: string;
39
34
  };
40
35
  } | {
@@ -46,7 +41,7 @@ export declare const runAuthCommand: (command: AuthCommand, fail: CliFail, optio
46
41
  serviceName: string;
47
42
  accountName: string;
48
43
  available: boolean;
49
- hasStoredLicenseKey: boolean;
44
+ hasStoredApiKey: boolean;
50
45
  error?: undefined;
51
46
  };
52
47
  } | {
@@ -58,7 +53,7 @@ export declare const runAuthCommand: (command: AuthCommand, fail: CliFail, optio
58
53
  serviceName: string;
59
54
  accountName: string;
60
55
  available: boolean;
61
- hasStoredLicenseKey: null;
56
+ hasStoredApiKey: null;
62
57
  error: string;
63
58
  };
64
59
  } | {
@@ -70,7 +65,7 @@ export declare const runAuthCommand: (command: AuthCommand, fail: CliFail, optio
70
65
  serviceName: string;
71
66
  accountName: string;
72
67
  available: boolean;
73
- hasStoredLicenseKey: boolean;
68
+ hasStoredApiKey: boolean;
74
69
  error?: undefined;
75
70
  };
76
71
  } | {
@@ -82,7 +77,7 @@ export declare const runAuthCommand: (command: AuthCommand, fail: CliFail, optio
82
77
  serviceName: string;
83
78
  accountName: string;
84
79
  available: boolean;
85
- hasStoredLicenseKey: null;
80
+ hasStoredApiKey: null;
86
81
  error: string;
87
82
  };
88
83
  }>;
@@ -94,7 +89,7 @@ export declare const buildAuthStatus: () => Promise<{
94
89
  serviceName: string;
95
90
  accountName: string;
96
91
  available: boolean;
97
- hasStoredLicenseKey: boolean;
92
+ hasStoredApiKey: boolean;
98
93
  error?: undefined;
99
94
  };
100
95
  } | {
@@ -105,7 +100,7 @@ export declare const buildAuthStatus: () => Promise<{
105
100
  serviceName: string;
106
101
  accountName: string;
107
102
  available: boolean;
108
- hasStoredLicenseKey: null;
103
+ hasStoredApiKey: null;
109
104
  error: string;
110
105
  };
111
106
  }>;
package/dist/cli-auth.js CHANGED
@@ -2,24 +2,16 @@ import { spawnSync } from 'node:child_process';
2
2
  import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
  import readline from 'node:readline';
5
- export const LICENSE_KEY_ENV_VAR = 'RR_LICENSE_KEY';
6
- export const AUTH_SERVICE_NAME = 'RankWrangler CLI';
7
- export const AUTH_ACCOUNT_NAME = 'license-key';
5
+ import { MERCHBASE_API_KEY_ENV, MERCHBASE_API_KEY_KEYCHAIN_ACCOUNT, MERCHBASE_API_KEY_KEYCHAIN_SERVICE, } from '@merchbaseco/access';
6
+ import { CliAuthError } from './cli-auth-error.js';
7
+ export const API_KEY_ENV_VAR = MERCHBASE_API_KEY_ENV;
8
+ export const AUTH_SERVICE_NAME = MERCHBASE_API_KEY_KEYCHAIN_SERVICE;
9
+ export const AUTH_ACCOUNT_NAME = MERCHBASE_API_KEY_KEYCHAIN_ACCOUNT;
8
10
  const INTERNAL_SECRET_STORE_DIR_ENV_VAR = 'RW_INTERNAL_CLI_SECRET_STORE_DIR';
9
- export class CliAuthError extends Error {
10
- code;
11
- details;
12
- constructor(code, message, details) {
13
- super(message);
14
- this.name = 'CliAuthError';
15
- this.code = code;
16
- this.details = details;
17
- }
18
- }
19
11
  export const resolveApiKey = async () => {
20
- const envLicenseKey = resolveEnvLicenseKey();
21
- if (envLicenseKey) {
22
- return envLicenseKey;
12
+ const envApiKey = resolveEnvApiKey();
13
+ if (envApiKey) {
14
+ return envApiKey;
23
15
  }
24
16
  const secureStore = await createSecureStore();
25
17
  return (await secureStore.get()) ?? undefined;
@@ -48,32 +40,32 @@ export const runAuthCommand = async (command, fail, options = {}) => {
48
40
  fail('UNKNOWN_COMMAND', 'Unknown auth command', { verb: command.verb });
49
41
  };
50
42
  export const buildAuthStatus = async () => {
51
- const envLicenseKey = resolveEnvLicenseKey();
43
+ const envApiKey = resolveEnvApiKey();
52
44
  try {
53
45
  const secureStore = await createSecureStore();
54
- const storedLicenseKey = await secureStore.get();
46
+ const storedApiKey = await secureStore.get();
55
47
  return {
56
- source: envLicenseKey ? 'env' : storedLicenseKey ? 'secure-store' : 'none',
57
- envOverride: Boolean(envLicenseKey),
48
+ source: envApiKey ? 'env' : storedApiKey ? 'secure-store' : 'none',
49
+ envOverride: Boolean(envApiKey),
58
50
  secureStore: {
59
51
  backend: secureStore.backend,
60
52
  serviceName: secureStore.serviceName,
61
53
  accountName: secureStore.accountName,
62
54
  available: true,
63
- hasStoredLicenseKey: Boolean(storedLicenseKey),
55
+ hasStoredApiKey: Boolean(storedApiKey),
64
56
  },
65
57
  };
66
58
  }
67
59
  catch (error) {
68
60
  return {
69
- source: envLicenseKey ? 'env' : 'none',
70
- envOverride: Boolean(envLicenseKey),
61
+ source: envApiKey ? 'env' : 'none',
62
+ envOverride: Boolean(envApiKey),
71
63
  secureStore: {
72
64
  backend: getSecureStoreBackendLabel(),
73
65
  serviceName: AUTH_SERVICE_NAME,
74
66
  accountName: AUTH_ACCOUNT_NAME,
75
67
  available: false,
76
- hasStoredLicenseKey: null,
68
+ hasStoredApiKey: null,
77
69
  error: error instanceof Error ? error.message : 'Unknown secure-store error',
78
70
  },
79
71
  };
@@ -89,20 +81,20 @@ const resolveAuthSetValue = async (args, fail, options) => {
89
81
  if (stdinValue) {
90
82
  return stdinValue;
91
83
  }
92
- fail('INVALID_INPUT', 'auth set --stdin received an empty license key');
84
+ fail('INVALID_INPUT', 'auth set --stdin received an empty API key');
93
85
  }
94
- const envLicenseKey = resolveEnvLicenseKey();
95
- if (envLicenseKey) {
96
- return envLicenseKey;
86
+ const envApiKey = resolveEnvApiKey();
87
+ if (envApiKey) {
88
+ return envApiKey;
97
89
  }
98
90
  if (process.stdin.isTTY && process.stderr.isTTY) {
99
- const promptValue = await readSecretFromPrompt('RankWrangler license key: ');
91
+ const promptValue = await readSecretFromPrompt('Merchbase API key: ');
100
92
  if (promptValue) {
101
93
  return promptValue;
102
94
  }
103
- fail('INVALID_INPUT', 'auth set received an empty license key');
95
+ fail('INVALID_INPUT', 'auth set received an empty API key');
104
96
  }
105
- fail('INVALID_INPUT', `auth set requires <licenseKey>, --stdin, or ${LICENSE_KEY_ENV_VAR}`);
97
+ fail('INVALID_INPUT', `auth set requires <apiKey>, --stdin, or ${API_KEY_ENV_VAR}`);
106
98
  };
107
99
  const readSecretFromStdin = async () => {
108
100
  const chunks = [];
@@ -139,8 +131,8 @@ const setTerminalEcho = (enabled) => {
139
131
  stdio: ['inherit', 'ignore', 'ignore'],
140
132
  });
141
133
  };
142
- const resolveEnvLicenseKey = () => {
143
- const value = process.env[LICENSE_KEY_ENV_VAR]?.trim();
134
+ const resolveEnvApiKey = () => {
135
+ const value = process.env[API_KEY_ENV_VAR]?.trim();
144
136
  return value ? value : undefined;
145
137
  };
146
138
  const createSecureStore = async () => {
@@ -178,7 +170,7 @@ const getSecureStoreBackendLabel = () => {
178
170
  }
179
171
  };
180
172
  const createFileSecretStore = (secretStoreDir) => {
181
- const secretPath = path.join(secretStoreDir, 'license-key.json');
173
+ const secretPath = path.join(secretStoreDir, 'api-key.json');
182
174
  return {
183
175
  backend: 'test-file-store',
184
176
  serviceName: AUTH_SERVICE_NAME,
@@ -187,7 +179,7 @@ const createFileSecretStore = (secretStoreDir) => {
187
179
  try {
188
180
  const raw = await readFile(secretPath, 'utf8');
189
181
  const parsed = JSON.parse(raw);
190
- return typeof parsed.licenseKey === 'string' ? parsed.licenseKey : null;
182
+ return typeof parsed.apiKey === 'string' ? parsed.apiKey : null;
191
183
  }
192
184
  catch (error) {
193
185
  if (error &&
@@ -201,7 +193,7 @@ const createFileSecretStore = (secretStoreDir) => {
201
193
  },
202
194
  set: async (value) => {
203
195
  await mkdir(secretStoreDir, { recursive: true });
204
- await writeFile(secretPath, `${JSON.stringify({ licenseKey: value }, null, 2)}\n`, 'utf8');
196
+ await writeFile(secretPath, `${JSON.stringify({ apiKey: value }, null, 2)}\n`, 'utf8');
205
197
  },
206
198
  clear: async () => {
207
199
  try {
@@ -17,7 +17,7 @@ export declare const runConfigCommand: (command: ConfigCommand, config: CliConfi
17
17
  serviceName: string;
18
18
  accountName: string;
19
19
  available: boolean;
20
- hasStoredLicenseKey: boolean;
20
+ hasStoredApiKey: boolean;
21
21
  error?: undefined;
22
22
  };
23
23
  } | {
@@ -28,7 +28,7 @@ export declare const runConfigCommand: (command: ConfigCommand, config: CliConfi
28
28
  serviceName: string;
29
29
  accountName: string;
30
30
  available: boolean;
31
- hasStoredLicenseKey: null;
31
+ hasStoredApiKey: null;
32
32
  error: string;
33
33
  };
34
34
  };
@@ -49,7 +49,7 @@ export declare const runConfigCommand: (command: ConfigCommand, config: CliConfi
49
49
  serviceName: string;
50
50
  accountName: string;
51
51
  available: boolean;
52
- hasStoredLicenseKey: boolean;
52
+ hasStoredApiKey: boolean;
53
53
  error?: undefined;
54
54
  };
55
55
  } | {
@@ -60,7 +60,7 @@ export declare const runConfigCommand: (command: ConfigCommand, config: CliConfi
60
60
  serviceName: string;
61
61
  accountName: string;
62
62
  available: boolean;
63
- hasStoredLicenseKey: null;
63
+ hasStoredApiKey: null;
64
64
  error: string;
65
65
  };
66
66
  };
@@ -80,7 +80,7 @@ export declare const runConfigCommand: (command: ConfigCommand, config: CliConfi
80
80
  serviceName: string;
81
81
  accountName: string;
82
82
  available: boolean;
83
- hasStoredLicenseKey: boolean;
83
+ hasStoredApiKey: boolean;
84
84
  error?: undefined;
85
85
  };
86
86
  } | {
@@ -91,7 +91,7 @@ export declare const runConfigCommand: (command: ConfigCommand, config: CliConfi
91
91
  serviceName: string;
92
92
  accountName: string;
93
93
  available: boolean;
94
- hasStoredLicenseKey: null;
94
+ hasStoredApiKey: null;
95
95
  error: string;
96
96
  };
97
97
  };
@@ -0,0 +1,9 @@
1
+ import type { createRankWranglerClient } from '@rankwrangler/http-client';
2
+ import { type CliOptionValues } from './cli-options';
3
+ type CliFail = (code: string, message: string, details?: unknown) => never;
4
+ type CliClient = ReturnType<typeof createRankWranglerClient>;
5
+ export declare const runKeywordCommand: (command: {
6
+ verb: string;
7
+ args: string[];
8
+ }, client: CliClient, options: CliOptionValues, fail: CliFail) => Promise<unknown>;
9
+ export {};
@@ -0,0 +1,69 @@
1
+ import { parseIntegerOption } from './cli-options.js';
2
+ const KEYWORD_MARKETPLACE_ID = 'ATVPDKIKX0DER';
3
+ export const runKeywordCommand = async (command, client, options, fail) => {
4
+ if (options.refresh) {
5
+ fail('INVALID_INPUT', '--refresh is not supported for keyword reads');
6
+ }
7
+ if (command.verb === 'get') {
8
+ return await runKeywordGet(command.args, client, fail);
9
+ }
10
+ if (command.verb === 'search') {
11
+ return await runKeywordSearch(command.args, client, options, fail);
12
+ }
13
+ if (command.verb === 'history') {
14
+ return await runKeywordHistory(command.args, client, options, fail);
15
+ }
16
+ fail('UNKNOWN_COMMAND', 'Unknown Keyword command', {
17
+ command: `keyword ${command.verb}`,
18
+ });
19
+ };
20
+ const runKeywordGet = async (args, client, fail) => {
21
+ const keyword = requireKeyword(args, fail, 'keyword get');
22
+ return await client.keyword.get.query({
23
+ keyword,
24
+ marketplaceId: KEYWORD_MARKETPLACE_ID,
25
+ });
26
+ };
27
+ const runKeywordSearch = async (args, client, options, fail) => {
28
+ const text = requireKeyword(args, fail, 'keyword search');
29
+ return await client.keyword.search.query({
30
+ text,
31
+ marketplaceId: KEYWORD_MARKETPLACE_ID,
32
+ cursor: parseIntegerOption({
33
+ value: options.cursor,
34
+ optionName: 'cursor',
35
+ min: 0,
36
+ max: 1_000_000,
37
+ defaultValue: 0,
38
+ }, fail),
39
+ limit: parseIntegerOption({
40
+ value: options.limit,
41
+ optionName: 'limit',
42
+ min: 1,
43
+ max: 100,
44
+ defaultValue: 25,
45
+ }, fail),
46
+ merchOnly: true,
47
+ });
48
+ };
49
+ const runKeywordHistory = async (args, client, options, fail) => {
50
+ const keyword = requireKeyword(args, fail, 'keyword history');
51
+ return await client.keyword.history.query({
52
+ keyword,
53
+ marketplaceId: KEYWORD_MARKETPLACE_ID,
54
+ rangeDays: parseIntegerOption({
55
+ value: options.rangeDays ?? options.days,
56
+ optionName: 'rangeDays',
57
+ min: 7,
58
+ max: 365,
59
+ defaultValue: 90,
60
+ }, fail),
61
+ });
62
+ };
63
+ const requireKeyword = (args, fail, commandName) => {
64
+ const keyword = args.join(' ').trim();
65
+ if (!keyword) {
66
+ fail('INVALID_INPUT', `${commandName} requires text`);
67
+ }
68
+ return keyword;
69
+ };
@@ -1,22 +1,24 @@
1
1
  import type { CliConfig } from './cli-config';
2
- import { type HistoryBucket, type HistoryMetricAlias } from './history-response';
2
+ import { type HistoryBucket, type HistoryMetricAlias } from './history-options';
3
3
  type CliFail = (code: string, message: string, details?: unknown) => never;
4
- export type CliOptionValues = {
4
+ export interface CliOptionValues {
5
5
  baseUrl?: string;
6
6
  marketplace?: string;
7
- asin?: string[];
8
7
  metrics?: string;
9
8
  startAt?: string;
10
9
  endAt?: string;
11
10
  days?: string;
11
+ rangeDays?: string;
12
12
  limit?: string;
13
13
  bucket?: string;
14
+ cursor?: string;
15
+ refresh?: boolean;
14
16
  help?: boolean;
15
17
  version?: boolean;
16
- };
18
+ }
17
19
  export declare const requireMarketplaceId: (values: CliOptionValues, config: CliConfig) => string;
18
- export declare const requireAsins: (commandArgs: string[], values: CliOptionValues, fail: CliFail) => string[];
19
- export declare const requireSingleAsin: (commandArgs: string[], values: CliOptionValues, fail: CliFail) => string;
20
+ export declare const requireSingleAsin: (commandArgs: string[], fail: CliFail, commandName?: string) => string;
21
+ export declare const requireAsins: (commandArgs: string[], fail: CliFail, commandName?: string) => string[];
20
22
  export declare const resolveHistoryMetrics: (values: CliOptionValues, fail: CliFail) => HistoryMetricAlias[];
21
23
  export declare const resolveHistoryBucket: (values: CliOptionValues, fail: CliFail) => HistoryBucket;
22
24
  export declare const resolveHistoryWindow: (values: CliOptionValues, fail: CliFail) => {
@@ -1,31 +1,34 @@
1
- import { HISTORY_BUCKETS, HISTORY_METRIC_ALIASES, } from './history-response.js';
1
+ import { HISTORY_BUCKETS, HISTORY_METRIC_ALIASES, } from './history-options.js';
2
2
  const ASIN_REGEX = /^[A-Z0-9]{10}$/i;
3
3
  const TRAILING_SLASHES_REGEX = /\/+$/;
4
4
  const API_SUFFIX_REGEX = /\/api$/i;
5
5
  const DEFAULT_MARKETPLACE_ID = 'ATVPDKIKX0DER';
6
6
  export const requireMarketplaceId = (values, config) => {
7
- return values.marketplace ?? config.marketplaceId ?? process.env.RR_MARKETPLACE_ID ?? DEFAULT_MARKETPLACE_ID;
7
+ return (values.marketplace ??
8
+ config.marketplaceId ??
9
+ process.env.RR_MARKETPLACE_ID ??
10
+ DEFAULT_MARKETPLACE_ID);
8
11
  };
9
- export const requireAsins = (commandArgs, values, fail) => {
10
- const candidates = [...commandArgs, ...(values.asin ?? []), ...resolveEnvAsins()]
11
- .map(value => value.trim())
12
- .filter(Boolean);
13
- if (candidates.length === 0) {
14
- fail('INVALID_INPUT', 'at least one asin is required');
12
+ export const requireSingleAsin = (commandArgs, fail, commandName = 'product history') => {
13
+ if (commandArgs.length !== 1) {
14
+ fail('INVALID_INPUT', `${commandName} requires exactly one ASIN`);
15
15
  }
16
- return Array.from(new Set(candidates.map(value => normalizeAsin(value, fail))));
16
+ return normalizeAsin(commandArgs[0] ?? '', fail);
17
17
  };
18
- export const requireSingleAsin = (commandArgs, values, fail) => {
19
- const asins = requireAsins(commandArgs, values, fail);
20
- if (asins.length !== 1) {
21
- fail('INVALID_INPUT', 'products history requires exactly one asin');
18
+ export const requireAsins = (commandArgs, fail, commandName = 'product get') => {
19
+ if (commandArgs.length < 1 || commandArgs.length > 200) {
20
+ fail('INVALID_INPUT', `${commandName} requires between 1 and 200 ASINs`);
21
+ }
22
+ const asins = commandArgs.map(value => normalizeAsin(value, fail));
23
+ if (new Set(asins).size !== asins.length) {
24
+ fail('INVALID_INPUT', `${commandName} requires unique ASINs`);
22
25
  }
23
- return asins[0];
26
+ return asins;
24
27
  };
25
28
  export const resolveHistoryMetrics = (values, fail) => {
26
- const requested = (values.metrics ?? process.env.RR_HISTORY_METRICS ?? 'bsr,price')
29
+ const requested = (values.metrics ?? process.env.RR_HISTORY_METRICS ?? 'salesRank,price')
27
30
  .split(',')
28
- .map(value => value.trim().toLowerCase())
31
+ .map(value => normalizeHistoryMetric(value.trim()))
29
32
  .filter(Boolean);
30
33
  if (requested.length === 0) {
31
34
  fail('INVALID_INPUT', 'metrics cannot be empty');
@@ -48,7 +51,9 @@ export const resolveHistoryBucket = (values, fail) => {
48
51
  return requested;
49
52
  };
50
53
  export const resolveHistoryWindow = (values, fail) => {
51
- const startAt = values.startAt ? normalizeDateOption('startAt', values.startAt, fail) : undefined;
54
+ const startAt = values.startAt
55
+ ? normalizeDateOption('startAt', values.startAt, fail)
56
+ : undefined;
52
57
  const endAt = values.endAt ? normalizeDateOption('endAt', values.endAt, fail) : undefined;
53
58
  if (values.days && (startAt || endAt)) {
54
59
  fail('INVALID_INPUT', 'use --days or --startAt/--endAt, not both');
@@ -69,7 +74,10 @@ export const resolveBaseUrl = (values, config, defaultBaseUrl, fail) => {
69
74
  return normalizeBaseUrl(values.baseUrl ?? config.baseUrl ?? process.env.RR_API_URL ?? defaultBaseUrl, fail);
70
75
  };
71
76
  export const normalizeBaseUrl = (value, fail) => {
72
- const normalized = value.trim().replace(TRAILING_SLASHES_REGEX, '').replace(API_SUFFIX_REGEX, '');
77
+ const normalized = value
78
+ .trim()
79
+ .replace(TRAILING_SLASHES_REGEX, '')
80
+ .replace(API_SUFFIX_REGEX, '');
73
81
  if (!normalized) {
74
82
  fail('INVALID_INPUT', 'base url cannot be empty');
75
83
  }
@@ -99,9 +107,12 @@ const normalizeAsin = (value, fail) => {
99
107
  }
100
108
  return normalized;
101
109
  };
102
- const resolveEnvAsins = () => {
103
- return [
104
- ...(process.env.RR_ASINS ? process.env.RR_ASINS.split(',') : []),
105
- ...(process.env.RR_ASIN ? [process.env.RR_ASIN] : []),
106
- ];
110
+ const normalizeHistoryMetric = (value) => {
111
+ if (value.toLowerCase() === 'salesrank') {
112
+ return 'salesRank';
113
+ }
114
+ if (value.toLowerCase() === 'price') {
115
+ return 'price';
116
+ }
117
+ return value;
107
118
  };
@@ -1,4 +1,4 @@
1
- import { CliAuthError } from './cli-auth.js';
1
+ import { CliAuthError } from './cli-auth-error.js';
2
2
  import { CliStorageError } from './cli-config.js';
3
3
  const OUTPUT_PRETTY = true;
4
4
  export const printSuccess = (data) => {
@@ -0,0 +1,10 @@
1
+ import type { createRankWranglerClient } from '@rankwrangler/http-client';
2
+ import type { CliConfig } from './cli-config';
3
+ import { type CliOptionValues } from './cli-options';
4
+ type CliFail = (code: string, message: string, details?: unknown) => never;
5
+ type CliClient = ReturnType<typeof createRankWranglerClient>;
6
+ export declare const runProductCommand: (command: {
7
+ verb: string;
8
+ args: string[];
9
+ }, client: CliClient, config: CliConfig, options: CliOptionValues, fail: CliFail) => Promise<unknown>;
10
+ export {};
@@ -0,0 +1,62 @@
1
+ import { parseIntegerOption, requireAsins, requireMarketplaceId, requireSingleAsin, resolveHistoryBucket, resolveHistoryMetrics, resolveHistoryWindow, } from './cli-options.js';
2
+ export const runProductCommand = async (command, client, config, options, fail) => {
3
+ if (command.verb !== 'search' && options.refresh) {
4
+ fail('INVALID_INPUT', '--refresh is only supported for product search');
5
+ }
6
+ if (command.verb === 'get') {
7
+ return await runProductGet(command.args, client, config, options, fail);
8
+ }
9
+ if (command.verb === 'search') {
10
+ return await runProductSearch(command.args, client, options, fail);
11
+ }
12
+ if (command.verb === 'history') {
13
+ return await runProductHistory(command.args, client, config, options, fail);
14
+ }
15
+ fail('UNKNOWN_COMMAND', 'Unknown Product command', {
16
+ command: `product ${command.verb}`,
17
+ });
18
+ };
19
+ const runProductGet = async (args, client, config, options, fail) => {
20
+ const marketplaceId = requireMarketplaceId(options, config);
21
+ const asins = requireAsins(args, fail);
22
+ if (asins.length === 1) {
23
+ return await client.product.get.mutate({
24
+ marketplaceId,
25
+ asin: asins[0],
26
+ });
27
+ }
28
+ return await client.product.getMany.mutate({
29
+ products: asins.map(asin => ({ marketplaceId, asin })),
30
+ });
31
+ };
32
+ const runProductSearch = async (args, client, options, fail) => {
33
+ const term = args.join(' ').trim();
34
+ if (!term) {
35
+ fail('INVALID_INPUT', 'product search requires a keyword');
36
+ }
37
+ return await client.product.search.mutate({
38
+ term,
39
+ refresh: Boolean(options.refresh),
40
+ });
41
+ };
42
+ const runProductHistory = async (args, client, config, options, fail) => {
43
+ const asin = requireSingleAsin(args, fail, 'product history');
44
+ const historyOptions = resolveHistoryOptions(options, fail);
45
+ return await client.product.history.mutate({
46
+ marketplaceId: requireMarketplaceId(options, config),
47
+ asin,
48
+ ...historyOptions,
49
+ });
50
+ };
51
+ const resolveHistoryOptions = (options, fail) => ({
52
+ metrics: resolveHistoryMetrics(options, fail),
53
+ bucket: resolveHistoryBucket(options, fail),
54
+ limit: parseIntegerOption({
55
+ value: options.limit,
56
+ optionName: 'limit',
57
+ min: 1,
58
+ max: 10_000,
59
+ defaultValue: 5000,
60
+ }, fail),
61
+ ...resolveHistoryWindow(options, fail),
62
+ });
@@ -0,0 +1,4 @@
1
+ export declare const HISTORY_METRIC_ALIASES: readonly ["salesRank", "price"];
2
+ export declare const HISTORY_BUCKETS: readonly ["auto", "day", "week", "month"];
3
+ export type HistoryMetricAlias = (typeof HISTORY_METRIC_ALIASES)[number];
4
+ export type HistoryBucket = (typeof HISTORY_BUCKETS)[number];
@@ -0,0 +1,2 @@
1
+ export const HISTORY_METRIC_ALIASES = ['salesRank', 'price'];
2
+ export const HISTORY_BUCKETS = ['auto', 'day', 'week', 'month'];
@@ -1,17 +1,6 @@
1
1
  export declare const HISTORY_METRIC_ALIASES: readonly ["bsr", "price"];
2
- export declare const HISTORY_BUCKETS: readonly ["auto", "day", "week", "month"];
3
2
  export type HistoryMetricAlias = (typeof HISTORY_METRIC_ALIASES)[number];
4
- export type HistoryBucket = (typeof HISTORY_BUCKETS)[number];
5
- type HistoryBucketTuple = [string, number | null];
6
- type HistoryBucketSummary = {
7
- first: number | null;
8
- latest: number | null;
9
- min: number | null;
10
- max: number | null;
11
- count: number;
12
- firstBucketAt: string | null;
13
- latestBucketAt: string | null;
14
- };
3
+ type HistoryPointTuple = [string, number | null] | [string, null, 1];
15
4
  export type AgentHistorySeries = {
16
5
  bsr?: {
17
6
  unit: 'rank';
@@ -19,15 +8,13 @@ export type AgentHistorySeries = {
19
8
  id: number;
20
9
  name: string | null;
21
10
  } | null;
22
- buckets: HistoryBucketTuple[];
23
- summary: HistoryBucketSummary;
11
+ points: HistoryPointTuple[];
24
12
  };
25
13
  price?: {
26
14
  unit: 'minorCurrency';
27
15
  currencyCode: 'USD';
28
16
  valueScale: 100;
29
- buckets: HistoryBucketTuple[];
30
- summary: HistoryBucketSummary;
17
+ points: HistoryPointTuple[];
31
18
  };
32
19
  };
33
20
  export type AgentHistoryResponse = {
@@ -35,11 +22,6 @@ export type AgentHistoryResponse = {
35
22
  status?: string;
36
23
  syncTriggered: boolean;
37
24
  latestImportAt: string | null;
38
- range?: {
39
- startAt: string;
40
- endAt: string;
41
- bucket: Exclude<HistoryBucket, 'auto'>;
42
- };
43
25
  series?: AgentHistorySeries;
44
26
  };
45
27
  export declare const buildCliHistoryResponse: ({ asin, marketplaceId, metrics, response, }: {
@@ -48,17 +30,12 @@ export declare const buildCliHistoryResponse: ({ asin, marketplaceId, metrics, r
48
30
  metrics: HistoryMetricAlias[];
49
31
  response: AgentHistoryResponse;
50
32
  }) => {
51
- series: AgentHistorySeries;
52
- range?: {
53
- startAt: string;
54
- endAt: string;
55
- bucket: Exclude<HistoryBucket, "auto">;
56
- } | undefined;
57
33
  schemaVersion: number;
58
34
  asin: string;
59
35
  marketplaceId: string;
60
36
  status: string;
61
37
  latestImportAt: string | null;
62
38
  syncTriggered: boolean;
39
+ series: AgentHistorySeries;
63
40
  };
64
41
  export {};
@@ -1,5 +1,4 @@
1
1
  export const HISTORY_METRIC_ALIASES = ['bsr', 'price'];
2
- export const HISTORY_BUCKETS = ['auto', 'day', 'week', 'month'];
3
2
  export const buildCliHistoryResponse = ({ asin, marketplaceId, metrics, response, }) => {
4
3
  const series = {
5
4
  ...(metrics.includes('bsr') && response.series?.bsr ? { bsr: response.series.bsr } : {}),
@@ -7,16 +6,15 @@ export const buildCliHistoryResponse = ({ asin, marketplaceId, metrics, response
7
6
  ? { price: response.series.price }
8
7
  : {}),
9
8
  };
10
- const hasAnyBuckets = Object.values(series).some(metricSeries => metricSeries.buckets.length > 0);
9
+ const hasAnyPoints = Object.values(series).some(metricSeries => metricSeries.points.length > 0);
11
10
  const status = normalizeHistoryStatus(response.status);
12
11
  return {
13
- schemaVersion: response.schemaVersion ?? 2,
12
+ schemaVersion: response.schemaVersion ?? 1,
14
13
  asin,
15
14
  marketplaceId,
16
- status: status ?? (response.syncTriggered ? 'collecting' : hasAnyBuckets ? 'ready' : 'empty'),
15
+ status: status ?? (response.syncTriggered ? 'collecting' : hasAnyPoints ? 'ready' : 'empty'),
17
16
  latestImportAt: response.latestImportAt,
18
17
  syncTriggered: response.syncTriggered,
19
- ...(response.range ? { range: response.range } : {}),
20
18
  series,
21
19
  };
22
20
  };
package/dist/index.js CHANGED
@@ -2,18 +2,21 @@
2
2
  import { parseArgs } from 'node:util';
3
3
  import { createRankWranglerClient, DEFAULT_API_BASE_URL } from '@rankwrangler/http-client';
4
4
  import { resolveApiKey, runAuthCommand } from './cli-auth.js';
5
- import { printBundledChangelog, printCliVersion } from './cli-metadata.js';
6
- import { runConfigCommand } from './cli-config-command.js';
7
5
  import { loadCliContext, loadCliPathsOrDefault } from './cli-config.js';
8
- import { parseIntegerOption, requireAsins, requireMarketplaceId, requireSingleAsin, resolveBaseUrl, resolveHistoryBucket, resolveHistoryMetrics, resolveHistoryWindow, } from './cli-options.js';
6
+ import { runConfigCommand } from './cli-config-command.js';
7
+ import { runKeywordCommand } from './cli-keyword-command.js';
8
+ import { printBundledChangelog, printCliVersion } from './cli-metadata.js';
9
+ import { resolveBaseUrl } from './cli-options.js';
9
10
  import { fail, printSuccess, resolveError } from './cli-output.js';
10
- import { buildCliHistoryResponse } from './history-response.js';
11
+ import { runProductCommand } from './cli-product-command.js';
11
12
  import { printUsage } from './usage.js';
12
13
  const SUPPORTED_COMMANDS = new Set([
13
- 'products:get',
14
- 'products:history',
15
- 'license:status',
16
- 'license:validate',
14
+ 'product:get',
15
+ 'product:search',
16
+ 'product:history',
17
+ 'keyword:get',
18
+ 'keyword:search',
19
+ 'keyword:history',
17
20
  'auth:status',
18
21
  'auth:set',
19
22
  'auth:clear',
@@ -30,13 +33,15 @@ const { positionals, values } = parseArgs({
30
33
  version: { type: 'boolean' },
31
34
  baseUrl: { type: 'string' },
32
35
  marketplace: { type: 'string', short: 'm' },
33
- asin: { type: 'string', multiple: true },
34
36
  metrics: { type: 'string' },
35
37
  startAt: { type: 'string' },
36
38
  endAt: { type: 'string' },
37
39
  days: { type: 'string' },
40
+ rangeDays: { type: 'string' },
38
41
  limit: { type: 'string' },
39
42
  bucket: { type: 'string' },
43
+ cursor: { type: 'string' },
44
+ refresh: { type: 'boolean' },
40
45
  stdin: { type: 'boolean' },
41
46
  },
42
47
  allowPositionals: true,
@@ -52,11 +57,9 @@ const main = async () => {
52
57
  process.exit(values.help ? 0 : 1);
53
58
  }
54
59
  const metaCommand = resolveMetaCommand(positionals);
55
- if (metaCommand) {
56
- if (metaCommand.name === 'changelog') {
57
- printBundledChangelog();
58
- return;
59
- }
60
+ if (metaCommand?.name === 'changelog') {
61
+ printBundledChangelog();
62
+ return;
60
63
  }
61
64
  const command = resolveCommandOrFail(positionals);
62
65
  if (!isSupportedCommand(command)) {
@@ -70,72 +73,32 @@ const main = async () => {
70
73
  return;
71
74
  }
72
75
  if (command.resource === 'auth') {
73
- printSuccess(await runAuthCommand(command, fail, { stdin: Boolean(values.stdin) }));
76
+ printSuccess(await runAuthCommand(command, fail, {
77
+ stdin: Boolean(values.stdin),
78
+ }));
74
79
  return;
75
80
  }
76
81
  const apiKey = await resolveApiKey();
77
82
  if (!apiKey) {
78
- fail('MISSING_CONFIG', 'license key is required. run `rw auth set`, `rw auth set --stdin`, or set RR_LICENSE_KEY');
83
+ fail('MISSING_CONFIG', 'Merchbase API key is required. run `rw auth set`, `rw auth set --stdin`, or set MERCHBASE_API_KEY');
79
84
  }
80
85
  const client = createRankWranglerClient({
81
86
  baseUrl: resolveBaseUrl(optionValues, config, DEFAULT_API_BASE_URL, fail),
82
87
  apiKey,
83
88
  });
84
- printSuccess(await runApiCommand(command, client, config));
89
+ printSuccess(await runApiCommand(command, client, config, optionValues));
85
90
  };
86
- const runApiCommand = async (command, client, config) => {
87
- if (command.resource === 'products' && command.verb === 'get') {
88
- const optionValues = values;
89
- const marketplaceId = requireMarketplaceId(optionValues, config);
90
- const asins = requireAsins(command.args, optionValues, fail);
91
- if (asins.length === 1) {
92
- return client.getProductInfo.mutate({ marketplaceId, asin: asins[0] });
93
- }
94
- return client.getProductInfoBatch.mutate({ marketplaceId, asins });
95
- }
96
- if (command.resource === 'products' && command.verb === 'history') {
97
- return runProductHistoryCommand(command, client, config);
91
+ const runApiCommand = async (command, client, config, options) => {
92
+ if (command.resource === 'product') {
93
+ return await runProductCommand(command, client, config, options, fail);
98
94
  }
99
- if (command.resource === 'license' && command.verb === 'status') {
100
- return client.license.status.mutate();
101
- }
102
- if (command.resource === 'license' && command.verb === 'validate') {
103
- return client.license.validate.mutate();
95
+ if (command.resource === 'keyword') {
96
+ return await runKeywordCommand(command, client, options, fail);
104
97
  }
105
98
  fail('UNKNOWN_COMMAND', 'Unknown command', {
106
99
  command: `${command.resource} ${command.verb}`,
107
100
  });
108
101
  };
109
- const runProductHistoryCommand = async (command, client, config) => {
110
- const optionValues = values;
111
- const asin = requireSingleAsin(command.args, optionValues, fail);
112
- const marketplaceId = requireMarketplaceId(optionValues, config);
113
- const metrics = resolveHistoryMetrics(optionValues, fail);
114
- const bucket = resolveHistoryBucket(optionValues, fail);
115
- const historyWindow = resolveHistoryWindow(optionValues, fail);
116
- const limit = parseIntegerOption({
117
- value: optionValues.limit,
118
- optionName: 'limit',
119
- min: 1,
120
- max: 10000,
121
- defaultValue: 5000,
122
- }, fail);
123
- const response = await client.getProductHistory.mutate({
124
- marketplaceId,
125
- asin,
126
- metrics,
127
- format: 'agent',
128
- bucket,
129
- limit,
130
- ...historyWindow,
131
- });
132
- return buildCliHistoryResponse({
133
- asin,
134
- marketplaceId,
135
- metrics,
136
- response: response,
137
- });
138
- };
139
102
  const resolveMetaCommand = (inputPositionals) => {
140
103
  if (inputPositionals.length === 1 && inputPositionals[0] === 'changelog') {
141
104
  return { name: 'changelog' };
@@ -144,7 +107,7 @@ const resolveMetaCommand = (inputPositionals) => {
144
107
  };
145
108
  const resolveCommand = (inputPositionals) => {
146
109
  const [first, second, ...rest] = inputPositionals;
147
- if (!first || !second) {
110
+ if (!(first && second)) {
148
111
  return null;
149
112
  }
150
113
  return {
@@ -161,9 +124,7 @@ const resolveCommandOrFail = (inputPositionals) => {
161
124
  fail('UNKNOWN_COMMAND', 'Unknown command', { command: inputPositionals.join(' ') });
162
125
  throw new Error('unreachable');
163
126
  };
164
- const isSupportedCommand = (command) => {
165
- return SUPPORTED_COMMANDS.has(`${command.resource}:${command.verb}`);
166
- };
127
+ const isSupportedCommand = (command) => SUPPORTED_COMMANDS.has(`${command.resource}:${command.verb}`);
167
128
  await main().catch(error => {
168
129
  const resolved = resolveError(error);
169
130
  fail(resolved.code, resolved.message, resolved.details);
package/dist/usage.js CHANGED
@@ -1,4 +1,4 @@
1
- import { LICENSE_KEY_ENV_VAR } from './cli-auth.js';
1
+ import { API_KEY_ENV_VAR } from './cli-auth.js';
2
2
  const DEFAULT_MARKETPLACE_ID = 'ATVPDKIKX0DER';
3
3
  export const printUsage = (paths) => {
4
4
  const usage = [
@@ -15,12 +15,14 @@ export const printUsage = (paths) => {
15
15
  '',
16
16
  'COMMANDS',
17
17
  ' changelog',
18
- ' products get <ASIN...>',
19
- ' products history <ASIN>',
20
- ' license status',
21
- ' license validate',
18
+ ' product get <ASIN...>',
19
+ ' product search <keyword>',
20
+ ' product history <ASIN>',
21
+ ' keyword get <keyword>',
22
+ ' keyword search <text>',
23
+ ' keyword history <keyword>',
22
24
  ' auth status',
23
- ' auth set [licenseKey]',
25
+ ' auth set [apiKey]',
24
26
  ' auth set --stdin',
25
27
  ' auth clear',
26
28
  ' config show',
@@ -36,13 +38,15 @@ export const printUsage = (paths) => {
36
38
  ' --version Print the CLI version',
37
39
  ' --baseUrl <origin> Override API origin',
38
40
  ` -m, --marketplace <id> Override marketplace (default: ${DEFAULT_MARKETPLACE_ID})`,
39
- ' --asin <ASIN> Add ASIN (repeatable)',
40
- ' --metrics <list> History metrics: bsr,price (default: bsr,price)',
41
- ' --bucket <unit> History buckets: auto,day,week,month (default: auto)',
42
- ' --days <N> History lookback window (30-3650, default: 365)',
43
- ' --startAt <ISO> History range start (ISO date/time)',
44
- ' --endAt <ISO> History range end (ISO date/time)',
45
- ' --limit <N> Internal point cap (1-10000, default: 5000)',
41
+ ' --refresh Product search refresh (separate Search contract)',
42
+ ' --metrics <list> Product history metrics: salesRank,price (default: salesRank,price)',
43
+ ' --bucket <unit> Product history buckets: auto,day,week,month (default: auto)',
44
+ ' --days <N> Product history lookback (30-3650, default: 365)',
45
+ ' --rangeDays <N> Keyword history lookback (7-365, default: 90)',
46
+ ' --startAt <ISO> Product history range start (ISO date/time)',
47
+ ' --endAt <ISO> Product history range end (ISO date/time)',
48
+ ' --limit <N> Result limit',
49
+ ' --cursor <N> Keyword search offset (default: 0)',
46
50
  ' --stdin Read auth secret from standard input',
47
51
  '',
48
52
  'FILES',
@@ -50,27 +54,26 @@ export const printUsage = (paths) => {
50
54
  ` ${paths.globalConfigPath} Global CLI settings`,
51
55
  '',
52
56
  'ENVIRONMENT',
53
- ` ${LICENSE_KEY_ENV_VAR} License key override for CI/automation`,
57
+ ` ${API_KEY_ENV_VAR} Merchbase API key override for CI/automation`,
54
58
  ' RR_API_URL API origin fallback',
55
59
  ' RR_MARKETPLACE_ID Marketplace fallback',
56
60
  ' RR_STORAGE_DIR Storage directory override',
57
- ' RR_ASIN Single ASIN fallback',
58
- ' RR_ASINS Comma-separated ASIN fallback',
59
- ' RR_HISTORY_METRICS Comma-separated history metrics fallback',
61
+ ' RR_HISTORY_METRICS Comma-separated product history metrics fallback',
60
62
  '',
61
63
  'EXAMPLES',
62
- ' rw auth set rrk_...',
63
- ' printf %s "$RR_LICENSE_KEY" | rw auth set --stdin',
64
- ` ${LICENSE_KEY_ENV_VAR}=rrk_... rw products get B0DV53VS61`,
64
+ ' rw auth set ak_...',
65
+ ' printf %s "$MERCHBASE_API_KEY" | rw auth set --stdin',
66
+ ` ${API_KEY_ENV_VAR}=ak_... rw product get B0DV53VS61`,
67
+ ' rw product get B0DV53VS61 B012345678',
68
+ ' rw product search "retro gardening shirt"',
69
+ ' rw product history B0DV53VS61 --bucket week',
70
+ ' rw keyword get "retro gardening shirt"',
71
+ ' rw keyword search "retro gardening" --limit 10',
72
+ ' rw keyword history "retro gardening shirt" --rangeDays 90',
65
73
  ' RR_STORAGE_DIR=~/.config/rankwrangler rw config show',
66
74
  ' rw --version',
67
75
  ' rw changelog',
68
- ' rw config get marketplace',
69
- ' rw config set storage-dir ~/.config/rankwrangler',
70
- ' rw config unset marketplace',
71
- ' rw products history B0DV53VS61 --metrics bsr,price --bucket week',
72
- ' rw products history B0DV53VS61 --startAt 2025-01-01 --endAt 2025-12-31 --bucket month',
73
- ' rankwrangler license status',
76
+ ' rankwrangler auth status',
74
77
  ];
75
78
  console.log(usage.join('\n'));
76
79
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rankwrangler/cli",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "Official RankWrangler command line interface",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,7 +16,7 @@
16
16
  "scripts": {
17
17
  "build": "tsc -p tsconfig.json && node scripts/fix-dist-imports.mjs && node scripts/sync-changelog.mjs",
18
18
  "prepack": "node scripts/sync-changelog.mjs",
19
- "test:e2e": "bun test test/index.e2e.test.ts",
19
+ "test:e2e": "bun test test/*.e2e.test.ts",
20
20
  "prepublishOnly": "node ../../scripts/release/ensure-cli-http-client-published.mjs"
21
21
  },
22
22
  "publishConfig": {
@@ -24,7 +24,8 @@
24
24
  "provenance": true
25
25
  },
26
26
  "dependencies": {
27
- "@rankwrangler/http-client": "^0.6.0",
27
+ "@merchbaseco/access": "0.3.0",
28
+ "@rankwrangler/http-client": "^0.8.0",
28
29
  "keytar": "7.9.0"
29
30
  },
30
31
  "devDependencies": {