@lehaotech/walmart-mcp 0.5.4

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.
Files changed (87) hide show
  1. package/.env.example +25 -0
  2. package/CHANGELOG.md +247 -0
  3. package/LICENSE +21 -0
  4. package/README.md +344 -0
  5. package/build/api/advertising/ad-client.d.ts +24 -0
  6. package/build/api/advertising/ad-client.js +174 -0
  7. package/build/api/advertising/advertising-api.d.ts +50 -0
  8. package/build/api/advertising/advertising-api.js +89 -0
  9. package/build/api/client.d.ts +19 -0
  10. package/build/api/client.js +150 -0
  11. package/build/api/feeds/feeds-api.d.ts +15 -0
  12. package/build/api/feeds/feeds-api.js +53 -0
  13. package/build/api/fulfillment/fulfillment-api.d.ts +37 -0
  14. package/build/api/fulfillment/fulfillment-api.js +81 -0
  15. package/build/api/index.d.ts +44 -0
  16. package/build/api/index.js +56 -0
  17. package/build/api/inventory/inventory-api.d.ts +27 -0
  18. package/build/api/inventory/inventory-api.js +49 -0
  19. package/build/api/items/items-api.d.ts +33 -0
  20. package/build/api/items/items-api.js +67 -0
  21. package/build/api/notifications/notifications-api.d.ts +14 -0
  22. package/build/api/notifications/notifications-api.js +33 -0
  23. package/build/api/orders/orders-api.d.ts +32 -0
  24. package/build/api/orders/orders-api.js +47 -0
  25. package/build/api/pricing/pricing-api.d.ts +32 -0
  26. package/build/api/pricing/pricing-api.js +60 -0
  27. package/build/api/reports/reports-api.d.ts +37 -0
  28. package/build/api/reports/reports-api.js +51 -0
  29. package/build/api/returns/returns-api.d.ts +26 -0
  30. package/build/api/returns/returns-api.js +37 -0
  31. package/build/api/settings/settings-api.d.ts +9 -0
  32. package/build/api/settings/settings-api.js +21 -0
  33. package/build/auth/oauth.d.ts +16 -0
  34. package/build/auth/oauth.js +125 -0
  35. package/build/config/environment.d.ts +22 -0
  36. package/build/config/environment.js +50 -0
  37. package/build/index.d.ts +2 -0
  38. package/build/index.js +180 -0
  39. package/build/scripts/client-configs.d.ts +36 -0
  40. package/build/scripts/client-configs.js +132 -0
  41. package/build/scripts/diagnose.d.ts +15 -0
  42. package/build/scripts/diagnose.js +320 -0
  43. package/build/scripts/setup.d.ts +17 -0
  44. package/build/scripts/setup.js +276 -0
  45. package/build/tools/definitions/advertising.d.ts +664 -0
  46. package/build/tools/definitions/advertising.js +315 -0
  47. package/build/tools/definitions/discovery.d.ts +24 -0
  48. package/build/tools/definitions/discovery.js +65 -0
  49. package/build/tools/definitions/feeds.d.ts +46 -0
  50. package/build/tools/definitions/feeds.js +42 -0
  51. package/build/tools/definitions/fulfillment.d.ts +1127 -0
  52. package/build/tools/definitions/fulfillment.js +272 -0
  53. package/build/tools/definitions/inventory.d.ts +392 -0
  54. package/build/tools/definitions/inventory.js +182 -0
  55. package/build/tools/definitions/items.d.ts +447 -0
  56. package/build/tools/definitions/items.js +223 -0
  57. package/build/tools/definitions/notifications.d.ts +84 -0
  58. package/build/tools/definitions/notifications.js +73 -0
  59. package/build/tools/definitions/orders.d.ts +2659 -0
  60. package/build/tools/definitions/orders.js +298 -0
  61. package/build/tools/definitions/pricing.d.ts +724 -0
  62. package/build/tools/definitions/pricing.js +254 -0
  63. package/build/tools/definitions/reports.d.ts +223 -0
  64. package/build/tools/definitions/reports.js +144 -0
  65. package/build/tools/definitions/returns.d.ts +441 -0
  66. package/build/tools/definitions/returns.js +126 -0
  67. package/build/tools/definitions/settings.d.ts +100 -0
  68. package/build/tools/definitions/settings.js +52 -0
  69. package/build/tools/definitions/shared-schemas.d.ts +40 -0
  70. package/build/tools/definitions/shared-schemas.js +47 -0
  71. package/build/tools/definitions/token-management.d.ts +16 -0
  72. package/build/tools/definitions/token-management.js +41 -0
  73. package/build/tools/index.d.ts +6924 -0
  74. package/build/tools/index.js +379 -0
  75. package/build/utils/api-error.d.ts +41 -0
  76. package/build/utils/api-error.js +97 -0
  77. package/build/utils/endpoint-catalog.d.ts +22 -0
  78. package/build/utils/endpoint-catalog.js +89 -0
  79. package/build/utils/env-file.d.ts +12 -0
  80. package/build/utils/env-file.js +27 -0
  81. package/build/utils/known-issues.d.ts +29 -0
  82. package/build/utils/known-issues.js +122 -0
  83. package/build/utils/logger.d.ts +15 -0
  84. package/build/utils/logger.js +56 -0
  85. package/build/utils/rate-limiter.d.ts +51 -0
  86. package/build/utils/rate-limiter.js +109 -0
  87. package/package.json +1 -0
@@ -0,0 +1,320 @@
1
+ /**
2
+ * walmart-mcp diagnose
3
+ * ------------------------------------------------------------
4
+ * Self-check script. Run via `npm run diagnose`.
5
+ *
6
+ * Reports environment readiness, credential validity, and MCP-client config
7
+ * presence without revealing credential values. Use `--export` to dump a
8
+ * JSON report suitable for attaching to bug reports.
9
+ *
10
+ * Exit codes:
11
+ * 0 all checks passed (or only warnings)
12
+ * 1 one or more errors — configuration must be fixed before the MCP works
13
+ * 2 fatal (script itself crashed)
14
+ */
15
+ import { readFileSync, existsSync, writeFileSync } from 'node:fs';
16
+ import { join } from 'node:path';
17
+ import { homedir, platform } from 'node:os';
18
+ import * as dotenv from 'dotenv';
19
+ import { getBaseUrl } from '../config/environment.js';
20
+ // ---------- Tiny tty helpers — no chalk dep on purpose ----------
21
+ const isTty = process.stdout.isTTY === true;
22
+ const c = {
23
+ red: (s) => (isTty ? `\x1b[31m${s}\x1b[0m` : s),
24
+ green: (s) => (isTty ? `\x1b[32m${s}\x1b[0m` : s),
25
+ yellow: (s) => (isTty ? `\x1b[33m${s}\x1b[0m` : s),
26
+ dim: (s) => (isTty ? `\x1b[2m${s}\x1b[0m` : s),
27
+ bold: (s) => (isTty ? `\x1b[1m${s}\x1b[0m` : s),
28
+ };
29
+ const results = [];
30
+ function record(section, severity, message) {
31
+ results.push({ section, severity, message });
32
+ const icon = severity === 'ok' ? c.green('OK ') :
33
+ severity === 'warn' ? c.yellow('WARN') :
34
+ severity === 'error' ? c.red('ERR ') :
35
+ c.dim('.. ');
36
+ console.log(` ${icon} ${message}`);
37
+ }
38
+ function section(num, total, title) {
39
+ console.log('');
40
+ console.log(c.bold(`[${num}/${total}] ${title}`));
41
+ }
42
+ // ---------- Placeholder & secret-safe rendering ----------
43
+ const PLACEHOLDER_PATTERNS = [
44
+ /^REPLACE_WITH_/i,
45
+ /^YOUR_/i,
46
+ /^xxx+$/i,
47
+ /^<.*>$/,
48
+ /^\$\{.*\}$/,
49
+ ];
50
+ function isPlaceholder(value) {
51
+ if (!value)
52
+ return false;
53
+ return PLACEHOLDER_PATTERNS.some((re) => re.test(value));
54
+ }
55
+ function safeRender(value, label) {
56
+ if (!value)
57
+ return `${label} not set`;
58
+ if (isPlaceholder(value))
59
+ return `${label} is a placeholder ("${value}")`;
60
+ const masked = value.length > 12
61
+ ? `${value.slice(0, 4)}...${value.slice(-4)}`
62
+ : '***';
63
+ return `${label} = ${masked} (${value.length} chars)`;
64
+ }
65
+ // ===================================================================
66
+ // Step 1: Node runtime
67
+ // ===================================================================
68
+ function checkNode() {
69
+ section('1', '7', 'Node runtime');
70
+ const v = process.versions.node;
71
+ const major = parseInt(v.split('.')[0] ?? '0', 10);
72
+ if (major >= 22) {
73
+ record('node', 'ok', `Node v${v} (>= 22 required)`);
74
+ }
75
+ else if (major >= 20) {
76
+ record('node', 'warn', `Node v${v} works but >= 22 recommended`);
77
+ }
78
+ else {
79
+ record('node', 'error', `Node v${v} too old — install Node 22+ from https://nodejs.org/`);
80
+ }
81
+ }
82
+ // ===================================================================
83
+ // Step 2: env file presence
84
+ // ===================================================================
85
+ function checkEnvFile() {
86
+ section('2', '7', 'Environment file');
87
+ const cwd = process.cwd();
88
+ const envPath = join(cwd, '.env');
89
+ if (!existsSync(envPath)) {
90
+ record('env-file', 'warn', `No .env at ${envPath} — relying on env vars from MCP client config`);
91
+ return;
92
+ }
93
+ dotenv.config({ path: envPath });
94
+ record('env-file', 'ok', `Found .env at ${envPath}`);
95
+ }
96
+ // ===================================================================
97
+ // Step 3: Required env vars
98
+ // ===================================================================
99
+ function checkRequiredEnv() {
100
+ section('3', '7', 'Required env vars');
101
+ const clientId = process.env.WALMART_CLIENT_ID;
102
+ if (!clientId) {
103
+ record('env', 'error', 'WALMART_CLIENT_ID not set');
104
+ }
105
+ else if (isPlaceholder(clientId)) {
106
+ record('env', 'error', `WALMART_CLIENT_ID is a placeholder ("${clientId}")`);
107
+ }
108
+ else {
109
+ record('env', 'ok', safeRender(clientId, 'WALMART_CLIENT_ID'));
110
+ }
111
+ const secret = process.env.WALMART_CLIENT_SECRET;
112
+ if (!secret) {
113
+ record('env', 'error', 'WALMART_CLIENT_SECRET not set');
114
+ }
115
+ else if (isPlaceholder(secret)) {
116
+ record('env', 'error', 'WALMART_CLIENT_SECRET is a placeholder');
117
+ }
118
+ else {
119
+ record('env', 'ok', safeRender(secret, 'WALMART_CLIENT_SECRET'));
120
+ }
121
+ const env = process.env.WALMART_ENVIRONMENT || 'sandbox';
122
+ if (env !== 'sandbox' && env !== 'production') {
123
+ record('env', 'error', `WALMART_ENVIRONMENT="${env}" — must be "sandbox" or "production"`);
124
+ }
125
+ else {
126
+ record('env', 'ok', `WALMART_ENVIRONMENT = "${env}"`);
127
+ }
128
+ const market = (process.env.WALMART_MARKET || 'us').toLowerCase();
129
+ if (!['us', 'mx', 'ca', 'cl'].includes(market)) {
130
+ record('env', 'error', `WALMART_MARKET="${market}" — must be us|mx|ca|cl`);
131
+ }
132
+ else {
133
+ record('env', 'ok', `WALMART_MARKET = "${market}"`);
134
+ }
135
+ }
136
+ // ===================================================================
137
+ // Step 4: Optional env vars (advertising)
138
+ // ===================================================================
139
+ function checkOptionalEnv() {
140
+ section('4', '7', 'Optional env vars (Walmart Connect advertising)');
141
+ const adId = process.env.WALMART_AD_CONSUMER_ID;
142
+ const adKey = process.env.WALMART_AD_PRIVATE_KEY;
143
+ if (!adId && !adKey) {
144
+ record('ads', 'info', 'WALMART_AD_CONSUMER_ID + WALMART_AD_PRIVATE_KEY not set');
145
+ record('ads', 'info', '25 walmart_ad_* tools will return a friendly "credentials not configured" error');
146
+ }
147
+ else if (adId && adKey) {
148
+ record('ads', 'ok', safeRender(adId, 'WALMART_AD_CONSUMER_ID'));
149
+ record('ads', 'ok', safeRender(adKey, 'WALMART_AD_PRIVATE_KEY'));
150
+ }
151
+ else {
152
+ record('ads', 'warn', 'Only one of AD_CONSUMER_ID / AD_PRIVATE_KEY is set — both required');
153
+ }
154
+ }
155
+ // ===================================================================
156
+ // Step 5: Walmart token exchange
157
+ // ===================================================================
158
+ async function checkTokenExchange() {
159
+ section('5', '7', 'Walmart token exchange');
160
+ const id = process.env.WALMART_CLIENT_ID;
161
+ const secret = process.env.WALMART_CLIENT_SECRET;
162
+ if (!id || !secret || isPlaceholder(id) || isPlaceholder(secret)) {
163
+ record('token', 'info', 'Skipped (credentials missing or placeholder — fix step [3] first)');
164
+ return false;
165
+ }
166
+ const env = (process.env.WALMART_ENVIRONMENT === 'production' ? 'production' : 'sandbox');
167
+ const tokenUrl = `${getBaseUrl(env)}/v3/token`;
168
+ try {
169
+ const basicAuth = Buffer.from(`${id}:${secret}`).toString('base64');
170
+ const resp = await fetch(tokenUrl, {
171
+ method: 'POST',
172
+ headers: {
173
+ Authorization: `Basic ${basicAuth}`,
174
+ 'WM_QOS.CORRELATION_ID': 'diagnose-' + Date.now(),
175
+ 'WM_SVC.NAME': process.env.WALMART_SVC_NAME || 'Walmart Marketplace',
176
+ Accept: 'application/json',
177
+ 'Content-Type': 'application/x-www-form-urlencoded',
178
+ },
179
+ body: 'grant_type=client_credentials',
180
+ });
181
+ if (resp.ok) {
182
+ const body = (await resp.json());
183
+ record('token', 'ok', `Token exchange OK (type=${body.token_type}, expires_in=${body.expires_in}s)`);
184
+ return true;
185
+ }
186
+ const bodyText = await resp.text();
187
+ record('token', 'error', `Token exchange failed: ${resp.status} ${resp.statusText}`);
188
+ if (bodyText) {
189
+ record('token', 'error', ` Body: ${bodyText.slice(0, 300)}`);
190
+ }
191
+ if (resp.status === 400) {
192
+ record('token', 'info', ' Hint: 400 usually means wrong Client ID/Secret, or sandbox creds against production endpoint (or vice versa)');
193
+ }
194
+ return false;
195
+ }
196
+ catch (err) {
197
+ record('token', 'error', `Network error: ${err instanceof Error ? err.message : String(err)}`);
198
+ record('token', 'info', ' Hint: check HTTPS_PROXY / firewall / DNS to marketplace.walmartapis.com');
199
+ return false;
200
+ }
201
+ }
202
+ // ===================================================================
203
+ // Step 6: Walmart API connectivity sanity
204
+ // ===================================================================
205
+ function checkApiCall(hadToken) {
206
+ section('6', '7', 'Walmart API connectivity');
207
+ if (!hadToken) {
208
+ record('api', 'info', 'Skipped (no token from step [5])');
209
+ return;
210
+ }
211
+ record('api', 'ok', 'Token endpoint reachable — same host as all other APIs');
212
+ }
213
+ // ===================================================================
214
+ // Step 7: MCP client config (best effort, read-only)
215
+ // ===================================================================
216
+ function checkMcpClientConfig() {
217
+ section('7', '7', 'MCP client config (best effort)');
218
+ const home = homedir();
219
+ const plat = platform();
220
+ const candidates = [];
221
+ if (plat === 'darwin') {
222
+ candidates.push({
223
+ name: 'Claude Desktop',
224
+ path: join(home, 'Library/Application Support/Claude/claude_desktop_config.json'),
225
+ });
226
+ }
227
+ else if (plat === 'win32') {
228
+ const appdata = process.env.APPDATA || join(home, 'AppData', 'Roaming');
229
+ candidates.push({
230
+ name: 'Claude Desktop',
231
+ path: join(appdata, 'Claude', 'claude_desktop_config.json'),
232
+ });
233
+ }
234
+ else {
235
+ candidates.push({
236
+ name: 'Claude Desktop',
237
+ path: join(home, '.config/Claude/claude_desktop_config.json'),
238
+ });
239
+ }
240
+ candidates.push({ name: 'Claude Code CLI', path: join(home, '.claude.json') });
241
+ candidates.push({ name: 'Cursor', path: join(home, '.cursor', 'mcp.json') });
242
+ let foundAny = false;
243
+ for (const cand of candidates) {
244
+ if (!existsSync(cand.path))
245
+ continue;
246
+ foundAny = true;
247
+ record('mcp-config', 'ok', `${cand.name} config found at ${cand.path}`);
248
+ try {
249
+ const raw = readFileSync(cand.path, 'utf8');
250
+ const json = JSON.parse(raw);
251
+ const servers = json.mcpServers ?? json.servers;
252
+ if (!servers) {
253
+ record('mcp-config', 'warn', ` No mcpServers section in ${cand.name}`);
254
+ continue;
255
+ }
256
+ const walmartKey = Object.keys(servers).find((k) => k.toLowerCase().startsWith('walmart'));
257
+ if (!walmartKey) {
258
+ record('mcp-config', 'warn', ' No "walmart*" entry in mcpServers — server not registered');
259
+ continue;
260
+ }
261
+ record('mcp-config', 'ok', ` "${walmartKey}" entry present in mcpServers`);
262
+ const entry = servers[walmartKey];
263
+ const envBlock = entry?.env ?? {};
264
+ const idInBlock = envBlock.WALMART_CLIENT_ID;
265
+ const secretInBlock = envBlock.WALMART_CLIENT_SECRET;
266
+ if (isPlaceholder(idInBlock) || isPlaceholder(secretInBlock)) {
267
+ record('mcp-config', 'warn', ` env block in ${cand.name} still has placeholder credentials`);
268
+ }
269
+ }
270
+ catch (err) {
271
+ record('mcp-config', 'error', ` Failed to parse: ${err instanceof Error ? err.message : String(err)}`);
272
+ }
273
+ }
274
+ if (!foundAny) {
275
+ record('mcp-config', 'warn', 'No supported MCP client config found in expected paths');
276
+ record('mcp-config', 'info', ' walmart-mcp will still work via direct stdio, but no client knows about it yet');
277
+ }
278
+ }
279
+ // ===================================================================
280
+ // Overall + export
281
+ // ===================================================================
282
+ async function main() {
283
+ console.log(c.bold('=== walmart-mcp diagnose ==='));
284
+ checkNode();
285
+ checkEnvFile();
286
+ checkRequiredEnv();
287
+ checkOptionalEnv();
288
+ const hadToken = await checkTokenExchange();
289
+ checkApiCall(hadToken);
290
+ checkMcpClientConfig();
291
+ console.log('');
292
+ const errors = results.filter((r) => r.severity === 'error').length;
293
+ const warnings = results.filter((r) => r.severity === 'warn').length;
294
+ const summary = errors > 0
295
+ ? c.red(`Overall: ${errors} errors, ${warnings} warnings`)
296
+ : warnings > 0
297
+ ? c.yellow(`Overall: ${warnings} warnings`)
298
+ : c.green('Overall: all checks passed');
299
+ console.log(summary);
300
+ if (process.argv.includes('--export')) {
301
+ const reportPath = join(process.cwd(), 'walmart-mcp-diagnose.json');
302
+ writeFileSync(reportPath, JSON.stringify({
303
+ timestamp: new Date().toISOString(),
304
+ node: process.versions.node,
305
+ platform: process.platform,
306
+ cwd: process.cwd(),
307
+ environment: process.env.WALMART_ENVIRONMENT || 'sandbox',
308
+ market: process.env.WALMART_MARKET || 'us',
309
+ results,
310
+ summary: { errors, warnings },
311
+ }, null, 2));
312
+ console.log(c.dim(`Report exported to ${reportPath}`));
313
+ }
314
+ if (errors > 0)
315
+ process.exit(1);
316
+ }
317
+ main().catch((err) => {
318
+ console.error(c.red('Fatal:'), err);
319
+ process.exit(2);
320
+ });
@@ -0,0 +1,17 @@
1
+ /**
2
+ * walmart-mcp setup
3
+ * ------------------------------------------------------------
4
+ * Interactive wizard. Run via `npm run setup` (or `npx walmart-mcp setup`).
5
+ *
6
+ * What it does:
7
+ * 1. Ask for environment + market (sandbox/production, us/mx/ca/cl)
8
+ * 2. Ask for WALMART_CLIENT_ID and WALMART_CLIENT_SECRET (masked input)
9
+ * 3. Live-validate credentials via OAuth token exchange
10
+ * 4. Optionally ask for Walmart Connect advertising credentials
11
+ * 5. Detect installed MCP clients (Claude Desktop, Claude Code CLI,
12
+ * Cursor, Cline, Continue, Windsurf, Zed) and offer multi-select
13
+ * write — backs up each client's config before modifying it.
14
+ *
15
+ * Zero new runtime deps. Uses node:readline/promises.
16
+ */
17
+ export {};
@@ -0,0 +1,276 @@
1
+ /**
2
+ * walmart-mcp setup
3
+ * ------------------------------------------------------------
4
+ * Interactive wizard. Run via `npm run setup` (or `npx walmart-mcp setup`).
5
+ *
6
+ * What it does:
7
+ * 1. Ask for environment + market (sandbox/production, us/mx/ca/cl)
8
+ * 2. Ask for WALMART_CLIENT_ID and WALMART_CLIENT_SECRET (masked input)
9
+ * 3. Live-validate credentials via OAuth token exchange
10
+ * 4. Optionally ask for Walmart Connect advertising credentials
11
+ * 5. Detect installed MCP clients (Claude Desktop, Claude Code CLI,
12
+ * Cursor, Cline, Continue, Windsurf, Zed) and offer multi-select
13
+ * write — backs up each client's config before modifying it.
14
+ *
15
+ * Zero new runtime deps. Uses node:readline/promises.
16
+ */
17
+ import { existsSync } from 'node:fs';
18
+ import { join } from 'node:path';
19
+ import * as readline from 'node:readline/promises';
20
+ import { stdin, stdout } from 'node:process';
21
+ import { getBaseUrl } from '../config/environment.js';
22
+ import { CLIENT_SPECS, writeWalmartEntry, } from './client-configs.js';
23
+ // ---------- Tty helpers — no chalk dep ----------
24
+ const isTty = stdout.isTTY === true;
25
+ const c = {
26
+ red: (s) => (isTty ? `\x1b[31m${s}\x1b[0m` : s),
27
+ green: (s) => (isTty ? `\x1b[32m${s}\x1b[0m` : s),
28
+ yellow: (s) => (isTty ? `\x1b[33m${s}\x1b[0m` : s),
29
+ cyan: (s) => (isTty ? `\x1b[36m${s}\x1b[0m` : s),
30
+ dim: (s) => (isTty ? `\x1b[2m${s}\x1b[0m` : s),
31
+ bold: (s) => (isTty ? `\x1b[1m${s}\x1b[0m` : s),
32
+ };
33
+ const rl = readline.createInterface({ input: stdin, output: stdout });
34
+ function logBanner() {
35
+ console.log('');
36
+ console.log(c.bold('======================================'));
37
+ console.log(c.bold(' walmart-mcp setup wizard'));
38
+ console.log(c.bold('======================================'));
39
+ console.log(c.dim(' Press Ctrl+C to abort at any time'));
40
+ console.log('');
41
+ }
42
+ function logStep(label) {
43
+ console.log('');
44
+ console.log(c.cyan(`>> ${label}`));
45
+ }
46
+ // ---------- Masked input ----------
47
+ async function askMasked(prompt) {
48
+ process.stdout.write(prompt);
49
+ let value = '';
50
+ await new Promise((resolve) => {
51
+ const onData = (chunk) => {
52
+ const s = chunk.toString('utf8');
53
+ for (const ch of s) {
54
+ if (ch === '\n' || ch === '\r') {
55
+ stdin.removeListener('data', onData);
56
+ stdin.setRawMode(false);
57
+ process.stdout.write('\n');
58
+ resolve();
59
+ return;
60
+ }
61
+ if (ch === '') {
62
+ process.stdout.write('\n');
63
+ process.exit(130);
64
+ }
65
+ if (ch === '' || ch === '\b') {
66
+ if (value.length > 0) {
67
+ value = value.slice(0, -1);
68
+ process.stdout.write('\b \b');
69
+ }
70
+ continue;
71
+ }
72
+ value += ch;
73
+ process.stdout.write('*');
74
+ }
75
+ };
76
+ stdin.setRawMode(true);
77
+ stdin.resume();
78
+ stdin.on('data', onData);
79
+ });
80
+ return value.trim();
81
+ }
82
+ async function chooseFromList(label, options, defaultIdx = 0) {
83
+ console.log(label);
84
+ options.forEach((opt, i) => {
85
+ const marker = i === defaultIdx ? c.green(' (default)') : '';
86
+ console.log(` ${i + 1}) ${opt.value}${opt.description ? c.dim(' - ' + opt.description) : ''}${marker}`);
87
+ });
88
+ const answer = (await rl.question(`Pick [1-${options.length}] (default ${defaultIdx + 1}): `)).trim();
89
+ if (!answer)
90
+ return options[defaultIdx].value;
91
+ const idx = parseInt(answer, 10) - 1;
92
+ if (Number.isNaN(idx) || idx < 0 || idx >= options.length) {
93
+ console.log(c.yellow(` Not a valid choice, using default ${options[defaultIdx].value}`));
94
+ return options[defaultIdx].value;
95
+ }
96
+ return options[idx].value;
97
+ }
98
+ /**
99
+ * Parse a comma-separated list of 1-based indices into a deduped set.
100
+ * "1,3,4" -> Set{0,2,3}. Out-of-range entries are dropped silently.
101
+ */
102
+ function parseMultiSelect(answer, maxIdx) {
103
+ const out = new Set();
104
+ for (const part of answer.split(/[,\s]+/)) {
105
+ if (!part)
106
+ continue;
107
+ const n = parseInt(part, 10);
108
+ if (!Number.isNaN(n) && n >= 1 && n <= maxIdx)
109
+ out.add(n - 1);
110
+ }
111
+ return out;
112
+ }
113
+ async function validateCredentials(clientId, clientSecret, env) {
114
+ const url = `${getBaseUrl(env)}/v3/token`;
115
+ try {
116
+ const auth = Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
117
+ const resp = await fetch(url, {
118
+ method: 'POST',
119
+ headers: {
120
+ Authorization: `Basic ${auth}`,
121
+ 'WM_QOS.CORRELATION_ID': 'setup-' + Date.now(),
122
+ 'WM_SVC.NAME': 'Walmart Marketplace',
123
+ Accept: 'application/json',
124
+ 'Content-Type': 'application/x-www-form-urlencoded',
125
+ },
126
+ body: 'grant_type=client_credentials',
127
+ });
128
+ if (resp.ok) {
129
+ const body = (await resp.json());
130
+ return { ok: true, expiresIn: body.expires_in ?? 0 };
131
+ }
132
+ const bodyText = await resp.text();
133
+ return {
134
+ ok: false,
135
+ reason: `HTTP ${resp.status} ${resp.statusText}${bodyText ? ': ' + bodyText.slice(0, 200) : ''}`,
136
+ };
137
+ }
138
+ catch (err) {
139
+ return { ok: false, reason: err instanceof Error ? err.message : String(err) };
140
+ }
141
+ }
142
+ async function main() {
143
+ logBanner();
144
+ logStep('1/5 Choose Walmart environment');
145
+ const environment = await chooseFromList(' Which environment will you use?', [
146
+ { value: 'sandbox', description: 'testing, mock data, no real listings' },
147
+ { value: 'production', description: 'real store, real money' },
148
+ ], 0);
149
+ logStep('2/5 Choose marketplace');
150
+ const market = await chooseFromList(' Walmart marketplace:', [
151
+ { value: 'us', description: 'United States' },
152
+ { value: 'mx', description: 'Mexico' },
153
+ { value: 'ca', description: 'Canada' },
154
+ { value: 'cl', description: 'Chile' },
155
+ ], 0);
156
+ logStep('3/5 Walmart credentials');
157
+ let clientId = '';
158
+ let clientSecret = '';
159
+ while (true) {
160
+ clientId = (await rl.question(' WALMART_CLIENT_ID: ')).trim();
161
+ clientSecret = await askMasked(' WALMART_CLIENT_SECRET (hidden): ');
162
+ if (!clientId || !clientSecret) {
163
+ console.log(c.red(' Both fields required.'));
164
+ continue;
165
+ }
166
+ process.stdout.write(c.dim(' Validating against Walmart... '));
167
+ const result = await validateCredentials(clientId, clientSecret, environment);
168
+ if (result.ok) {
169
+ console.log(c.green(`OK (token expires in ${result.expiresIn}s)`));
170
+ break;
171
+ }
172
+ console.log(c.red('FAIL'));
173
+ console.log(c.red(' ' + result.reason));
174
+ const retry = (await rl.question(' Try again? [Y/n]: ')).trim().toLowerCase();
175
+ if (retry === 'n') {
176
+ console.log(c.yellow(' Aborting. Fix credentials at https://developer.walmart.com/ and re-run.'));
177
+ process.exit(1);
178
+ }
179
+ }
180
+ logStep('4/5 Walmart Connect advertising (optional)');
181
+ const wantsAds = (await rl.question(' Configure now? [y/N]: ')).trim().toLowerCase() === 'y';
182
+ let adConsumerId;
183
+ let adPrivateKey;
184
+ if (wantsAds) {
185
+ adConsumerId = (await rl.question(' WALMART_AD_CONSUMER_ID: ')).trim() || undefined;
186
+ if (adConsumerId)
187
+ adPrivateKey = (await askMasked(' WALMART_AD_PRIVATE_KEY (hidden): ')) || undefined;
188
+ }
189
+ else {
190
+ console.log(c.dim(' Skipped. 25 walmart_ad_* tools will return a friendly error until configured.'));
191
+ }
192
+ logStep('5/5 Register MCP server in your AI clients');
193
+ const buildPath = join(process.cwd(), 'build', 'index.js');
194
+ if (!existsSync(buildPath)) {
195
+ console.log(c.yellow(` WARN: ${buildPath} does not exist yet. Run 'npm run build' first.`));
196
+ console.log(c.yellow(' Continuing anyway — config will point to that path once you build.'));
197
+ }
198
+ // Detect installed clients.
199
+ const installed = [];
200
+ const notInstalled = [];
201
+ for (const spec of CLIENT_SPECS) {
202
+ const p = spec.resolvePath();
203
+ if (p && existsSync(p))
204
+ installed.push({ ...spec, path: p });
205
+ else
206
+ notInstalled.push(spec);
207
+ }
208
+ if (installed.length === 0) {
209
+ console.log(c.yellow(' No supported MCP client config files found in expected paths.'));
210
+ console.log(c.dim(' Looked for:'));
211
+ for (const spec of CLIENT_SPECS)
212
+ console.log(c.dim(` - ${spec.name}: ${spec.resolvePath()}`));
213
+ console.log(c.yellow(' Skipping write — install at least one MCP client and re-run.'));
214
+ process.exit(0);
215
+ }
216
+ console.log(' Detected MCP clients:');
217
+ installed.forEach((spec, i) => console.log(` ${i + 1}) ${spec.name} ${c.dim('(' + spec.path + ')')}`));
218
+ console.log('');
219
+ const answer = (await rl.question(' Select which to configure (comma-separated, e.g. "1,3"; empty = all): ')).trim();
220
+ const picked = answer === ''
221
+ ? new Set(installed.map((_, i) => i))
222
+ : parseMultiSelect(answer, installed.length);
223
+ if (picked.size === 0) {
224
+ console.log(c.yellow(' Nothing selected, aborting.'));
225
+ process.exit(0);
226
+ }
227
+ const walmartEntry = {
228
+ type: 'stdio',
229
+ command: 'node',
230
+ args: [buildPath],
231
+ env: {
232
+ WALMART_CLIENT_ID: clientId,
233
+ WALMART_CLIENT_SECRET: clientSecret,
234
+ WALMART_ENVIRONMENT: environment,
235
+ WALMART_MARKET: market,
236
+ ...(adConsumerId ? { WALMART_AD_CONSUMER_ID: adConsumerId } : {}),
237
+ ...(adPrivateKey ? { WALMART_AD_PRIVATE_KEY: adPrivateKey } : {}),
238
+ },
239
+ };
240
+ let okCount = 0;
241
+ let failCount = 0;
242
+ for (const idx of picked) {
243
+ const target = installed[idx];
244
+ process.stdout.write(c.dim(` Writing ${target.name}... `));
245
+ try {
246
+ const r = writeWalmartEntry(target, target.path, walmartEntry);
247
+ const tag = r.overwrote ? c.yellow('updated') : c.green('added');
248
+ const backupTag = r.backedUp ? c.dim(' (backup at .before-walmart-setup.bak)') : '';
249
+ console.log(`${tag}${backupTag}`);
250
+ okCount += 1;
251
+ }
252
+ catch (err) {
253
+ console.log(c.red(`FAIL: ${err instanceof Error ? err.message : String(err)}`));
254
+ failCount += 1;
255
+ }
256
+ }
257
+ console.log('');
258
+ console.log(c.bold('================= Done ================='));
259
+ if (failCount === 0)
260
+ console.log(c.green(`Configured ${okCount} client(s).`));
261
+ else
262
+ console.log(c.yellow(`Configured ${okCount} client(s), ${failCount} failed.`));
263
+ console.log('');
264
+ console.log('Next:');
265
+ console.log(' 1. If you have not built yet: ' + c.cyan('npm run build'));
266
+ console.log(' 2. Restart each configured AI client');
267
+ console.log(' 3. Try in chat: ' + c.cyan('"Show me my recent Walmart orders"'));
268
+ console.log('');
269
+ console.log(c.dim('If anything fails, run ' + c.cyan('npm run diagnose') + c.dim(' for a self-check.')));
270
+ rl.close();
271
+ }
272
+ main().catch((err) => {
273
+ console.error(c.red('Setup failed:'), err);
274
+ rl.close();
275
+ process.exit(2);
276
+ });