@velaro/cli 0.7.0 → 1.4.8
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 +161 -138
- package/bin/velaro.js +177 -62
- package/lib/api.js +91 -52
- package/lib/api.test.js +46 -0
- package/lib/banner.js +76 -0
- package/lib/commands/activity.js +133 -0
- package/lib/commands/acuity.js +66 -0
- package/lib/commands/agent.js +204 -50
- package/lib/commands/ai-config.js +193 -0
- package/lib/commands/ai-models.js +159 -0
- package/lib/commands/appointments.js +198 -0
- package/lib/commands/article.js +668 -388
- package/lib/commands/automation-draft.js +134 -0
- package/lib/commands/avatar.js +75 -0
- package/lib/commands/bigcommerce.js +50 -0
- package/lib/commands/billing-contacts.js +62 -0
- package/lib/commands/billing-email-preference.js +64 -0
- package/lib/commands/billing-subscription.js +265 -0
- package/lib/commands/billing.js +138 -0
- package/lib/commands/bot.js +141 -137
- package/lib/commands/bundle.js +168 -0
- package/lib/commands/calendly.js +62 -0
- package/lib/commands/callback.js +125 -0
- package/lib/commands/callrail.js +88 -0
- package/lib/commands/campaigns.js +44 -0
- package/lib/commands/case.js +102 -0
- package/lib/commands/check.js +163 -163
- package/lib/commands/compliance.js +229 -0
- package/lib/commands/conversation-efficiency.js +178 -0
- package/lib/commands/copilotstudio.js +114 -0
- package/lib/commands/coupon-grant.js +192 -0
- package/lib/commands/db.js +101 -0
- package/lib/commands/deployment.js +107 -107
- package/lib/commands/diagnostics.js +298 -0
- package/lib/commands/email-campaign.js +47 -0
- package/lib/commands/email-inbox.js +88 -0
- package/lib/commands/entitlement.js +176 -0
- package/lib/commands/env.js +45 -45
- package/lib/commands/feature-discovery.js +40 -0
- package/lib/commands/focus.js +278 -0
- package/lib/commands/index.js +38 -5
- package/lib/commands/ingest.js +31 -31
- package/lib/commands/inline-widget-config.js +126 -0
- package/lib/commands/integration.js +93 -0
- package/lib/commands/kb.js +450 -309
- package/lib/commands/login.js +86 -86
- package/lib/commands/logs.js +680 -0
- package/lib/commands/magento.js +210 -0
- package/lib/commands/mcp-key.js +188 -159
- package/lib/commands/migrate.js +134 -0
- package/lib/commands/migration-status.js +66 -0
- package/lib/commands/monday.js +137 -0
- package/lib/commands/netsuite.js +87 -0
- package/lib/commands/notifications.js +63 -0
- package/lib/commands/notion.js +70 -0
- package/lib/commands/ops.js +267 -173
- package/lib/commands/payment-recovery.js +170 -0
- package/lib/commands/pickup.js +172 -0
- package/lib/commands/pricing.js +132 -0
- package/lib/commands/product.js +55 -0
- package/lib/commands/recruiting.js +374 -0
- package/lib/commands/report.js +462 -0
- package/lib/commands/routing.js +304 -0
- package/lib/commands/rule.js +85 -85
- package/lib/commands/sharepoint.js +167 -0
- package/lib/commands/site-provision.js +68 -0
- package/lib/commands/site.js +62 -62
- package/lib/commands/sitesync.js +158 -0
- package/lib/commands/slack.js +64 -0
- package/lib/commands/squarespace.js +108 -0
- package/lib/commands/status.js +24 -24
- package/lib/commands/subscription.js +43 -0
- package/lib/commands/support.js +128 -0
- package/lib/commands/survey.js +216 -0
- package/lib/commands/team.js +144 -144
- package/lib/commands/teams-phone.js +131 -0
- package/lib/commands/teams.js +106 -0
- package/lib/commands/telephony.js +99 -0
- package/lib/commands/update.js +47 -47
- package/lib/commands/webflow.js +128 -0
- package/lib/commands/whoami.js +25 -22
- package/lib/commands/widget-container.js +152 -0
- package/lib/commands/woocommerce.js +240 -0
- package/lib/commands/workflow.js +233 -98
- package/lib/config.js +85 -83
- package/lib/kb-screenshot.js +320 -0
- package/lib/migrations/amscro.json +72 -0
- package/lib/migrations/azenta.json +68 -0
- package/lib/migrations/bluefire.json +49 -0
- package/lib/migrations/donaldson.json +75 -0
- package/lib/oauth.js +149 -135
- package/lib/run.js +21 -16
- package/lib/sharepoint-auth.js +138 -0
- package/lib/subscription.js +41 -39
- package/lib/track.js +35 -35
- package/lib/update-check.js +64 -64
- package/package.json +34 -19
- package/scripts/postinstall.js +12 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* velaro magento — Magento / Adobe Commerce store management
|
|
3
|
+
*
|
|
4
|
+
* velaro magento list List connected Magento stores
|
|
5
|
+
* velaro magento test Test a store connection
|
|
6
|
+
* velaro magento customers Search customers by email
|
|
7
|
+
* velaro magento orders Search orders by email or order ID
|
|
8
|
+
* velaro magento products Search the product catalog
|
|
9
|
+
* velaro magento logs View integration activity logs
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { request } from '../api.js';
|
|
13
|
+
|
|
14
|
+
const GRN = '\x1b[32m';
|
|
15
|
+
const RED = '\x1b[31m';
|
|
16
|
+
const CYN = '\x1b[36m';
|
|
17
|
+
const DIM = '\x1b[2m';
|
|
18
|
+
const BOLD = '\x1b[1m';
|
|
19
|
+
const RST = '\x1b[0m';
|
|
20
|
+
|
|
21
|
+
// ── list ──────────────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
async function listHandler() {
|
|
24
|
+
const stores = await request('GET', '/Magento/Configs');
|
|
25
|
+
if (!Array.isArray(stores) || stores.length === 0) {
|
|
26
|
+
console.log('\nNo Magento stores connected. Set one up in the admin UI under Integrations → Magento.\n');
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
console.log(`\n${BOLD}${stores.length} Magento store(s)${RST}\n`);
|
|
30
|
+
for (const s of stores) {
|
|
31
|
+
const status = s.isActive ? `${GRN}active${RST}` : `${DIM}inactive${RST}`;
|
|
32
|
+
const edition = [s.edition, s.magentoVersion].filter(Boolean).join(' ') || 'CE';
|
|
33
|
+
console.log(` [${s.id}] ${BOLD}${s.displayName ?? s.storeName}${RST} ${DIM}${s.storeUrl}${RST}`);
|
|
34
|
+
console.log(` ${edition} | ${status}`);
|
|
35
|
+
}
|
|
36
|
+
console.log('');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ── test ──────────────────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
async function testHandler(argv) {
|
|
42
|
+
if (!argv.url || !argv.token) {
|
|
43
|
+
console.error(`${RED}✗ --url and --token are required${RST}`);
|
|
44
|
+
process.exitCode = 1;
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
const r = await request('POST', '/Magento/Configs/Test', {
|
|
49
|
+
storeUrl: argv.url,
|
|
50
|
+
accessToken: argv.token,
|
|
51
|
+
});
|
|
52
|
+
console.log(`\n${GRN}✓ Connected${RST} to "${r.storeName}" (${r.edition} ${r.version})\n`);
|
|
53
|
+
} catch (e) {
|
|
54
|
+
console.error(`\n${RED}✗ Connection failed: ${e.message}${RST}\n`);
|
|
55
|
+
process.exitCode = 1;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ── customers ─────────────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
async function customersHandler(argv) {
|
|
62
|
+
if (!argv.email) {
|
|
63
|
+
console.error(`${RED}✗ --email is required${RST}`);
|
|
64
|
+
process.exitCode = 1;
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const r = await request('GET', `/Magento/Customers/Search?q=${encodeURIComponent(argv.email)}`);
|
|
68
|
+
if (!r || !r.id) {
|
|
69
|
+
console.log(`\nNo customer found for ${argv.email}.\n`);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
console.log(`\n${BOLD}Magento Customer${RST}\n`);
|
|
73
|
+
console.log(` ID: ${r.id}`);
|
|
74
|
+
console.log(` Name: ${r.name ?? '(none)'}`);
|
|
75
|
+
console.log(` Email: ${CYN}${r.email}${RST}`);
|
|
76
|
+
console.log(` Group: ${r.group ?? r.groupId ?? 'General'}`);
|
|
77
|
+
console.log('');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── orders ────────────────────────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
async function ordersHandler(argv) {
|
|
83
|
+
if (!argv.email && !argv.id) {
|
|
84
|
+
console.error(`${RED}✗ --email or --id is required${RST}`);
|
|
85
|
+
process.exitCode = 1;
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const q = argv.id ?? argv.email;
|
|
89
|
+
const limit = Math.min(50, argv.limit ?? 10);
|
|
90
|
+
const orders = await request('GET', `/Magento/Orders/Search?q=${encodeURIComponent(q)}&limit=${limit}`);
|
|
91
|
+
if (!Array.isArray(orders) || orders.length === 0) {
|
|
92
|
+
console.log(`\nNo orders found for "${q}".\n`);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
console.log(`\n${BOLD}${orders.length} order(s)${RST}\n`);
|
|
96
|
+
for (const o of orders) {
|
|
97
|
+
const date = (o.createdAt ?? '').slice(0, 10);
|
|
98
|
+
console.log(` [${o.incrementId ?? o.id}] ${o.status?.padEnd(12)} $${String(o.grandTotal ?? 0).padStart(9)} ${DIM}${date}${RST}`);
|
|
99
|
+
if (argv.verbose && o.items?.length) {
|
|
100
|
+
console.log(` ${DIM}${o.items.join(' · ')}${RST}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
console.log('');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ── products ──────────────────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
async function productsHandler(argv) {
|
|
109
|
+
if (!argv.query) {
|
|
110
|
+
console.error(`${RED}✗ --query is required${RST}`);
|
|
111
|
+
process.exitCode = 1;
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const limit = Math.min(50, argv.limit ?? 10);
|
|
115
|
+
const products = await request('GET', `/Magento/Products/Search?q=${encodeURIComponent(argv.query)}&limit=${limit}`);
|
|
116
|
+
if (!Array.isArray(products) || products.length === 0) {
|
|
117
|
+
console.log(`\nNo products found for "${argv.query}".\n`);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
console.log(`\n${BOLD}${products.length} product(s)${RST}\n`);
|
|
121
|
+
for (const p of products) {
|
|
122
|
+
const status = p.status === 'Enabled' ? `${GRN}enabled${RST}` : `${DIM}disabled${RST}`;
|
|
123
|
+
console.log(` [${p.sku}] ${BOLD}${p.name}${RST} $${p.price} ${status}`);
|
|
124
|
+
if (p.url) console.log(` ${DIM}${p.url}${RST}`);
|
|
125
|
+
}
|
|
126
|
+
console.log('');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ── logs ──────────────────────────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
async function logsHandler(argv) {
|
|
132
|
+
const params = new URLSearchParams();
|
|
133
|
+
if (argv.severity) params.set('severity', argv.severity);
|
|
134
|
+
if (argv.search) params.set('search', argv.search);
|
|
135
|
+
params.set('pageSize', String(Math.min(100, argv.pageSize ?? 25)));
|
|
136
|
+
const data = await request('GET', `/Integration/Logs/Magento?${params}`);
|
|
137
|
+
const items = data?.items ?? [];
|
|
138
|
+
if (items.length === 0) {
|
|
139
|
+
console.log('\nNo Magento activity log entries found.\n');
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
console.log(`\n${BOLD}${items.length} of ${data.total} Magento activity entries${RST}\n`);
|
|
143
|
+
for (const item of items) {
|
|
144
|
+
const ts = new Date(item.createdAt).toISOString().replace('T', ' ').slice(0, 19);
|
|
145
|
+
const sev = item.severity === 'Error' ? `${RED}${item.severity}${RST} ` :
|
|
146
|
+
item.severity === 'Warning' ? `${'\x1b[33m'}${item.severity}${RST}` :
|
|
147
|
+
`${DIM}${item.severity}${RST} `;
|
|
148
|
+
console.log(` ${DIM}${ts}${RST} ${sev} [${item.category ?? '?'}] ${item.message ?? ''}`);
|
|
149
|
+
if (item.additionalInfo) console.log(` ${DIM}${item.additionalInfo}${RST}`);
|
|
150
|
+
}
|
|
151
|
+
if (data.total > items.length) console.log(`\n ${DIM}${data.total - items.length} more — use --page-size to see more${RST}`);
|
|
152
|
+
console.log('');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ── command export ────────────────────────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
export const magentoCommand = {
|
|
158
|
+
command: 'magento <subcommand>',
|
|
159
|
+
describe: 'Magento / Adobe Commerce store management',
|
|
160
|
+
builder: y => y
|
|
161
|
+
.command({
|
|
162
|
+
command: 'list',
|
|
163
|
+
describe: 'List connected Magento stores',
|
|
164
|
+
handler: listHandler,
|
|
165
|
+
})
|
|
166
|
+
.command({
|
|
167
|
+
command: 'test',
|
|
168
|
+
describe: 'Test a Magento store connection',
|
|
169
|
+
builder: yy => yy
|
|
170
|
+
.option('url', { describe: 'Magento store URL', type: 'string' })
|
|
171
|
+
.option('token', { describe: 'Admin API bearer token', type: 'string' }),
|
|
172
|
+
handler: testHandler,
|
|
173
|
+
})
|
|
174
|
+
.command({
|
|
175
|
+
command: 'customers',
|
|
176
|
+
describe: 'Search Magento customers by email',
|
|
177
|
+
builder: yy => yy
|
|
178
|
+
.option('email', { describe: 'Customer email address', type: 'string' }),
|
|
179
|
+
handler: customersHandler,
|
|
180
|
+
})
|
|
181
|
+
.command({
|
|
182
|
+
command: 'orders',
|
|
183
|
+
describe: 'Search Magento orders by email or order ID',
|
|
184
|
+
builder: yy => yy
|
|
185
|
+
.option('email', { describe: 'Customer email address', type: 'string' })
|
|
186
|
+
.option('id', { describe: 'Order increment ID (e.g. 000001234)', type: 'string' })
|
|
187
|
+
.option('limit', { describe: 'Max results (default: 10)', type: 'number', default: 10 })
|
|
188
|
+
.option('verbose', { describe: 'Show order line items', type: 'boolean', default: false }),
|
|
189
|
+
handler: ordersHandler,
|
|
190
|
+
})
|
|
191
|
+
.command({
|
|
192
|
+
command: 'products',
|
|
193
|
+
describe: 'Search the Magento product catalog',
|
|
194
|
+
builder: yy => yy
|
|
195
|
+
.option('query', { describe: 'Product name search query', type: 'string' })
|
|
196
|
+
.option('limit', { describe: 'Max results (default: 10)', type: 'number', default: 10 }),
|
|
197
|
+
handler: productsHandler,
|
|
198
|
+
})
|
|
199
|
+
.command({
|
|
200
|
+
command: 'logs',
|
|
201
|
+
describe: 'View Magento integration activity logs',
|
|
202
|
+
builder: yy => yy
|
|
203
|
+
.option('severity', { describe: 'Filter: Info, Warning, Error', type: 'string' })
|
|
204
|
+
.option('search', { describe: 'Text to search for in messages', type: 'string' })
|
|
205
|
+
.option('page-size', { describe: 'Results per page (default: 25)', type: 'number', default: 25 }),
|
|
206
|
+
handler: logsHandler,
|
|
207
|
+
})
|
|
208
|
+
.demandCommand(1, 'Specify a subcommand. Try: velaro magento --help'),
|
|
209
|
+
handler: () => {},
|
|
210
|
+
};
|
package/lib/commands/mcp-key.js
CHANGED
|
@@ -1,159 +1,188 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import os from 'os';
|
|
4
|
-
import { get, post, del } from '../api.js';
|
|
5
|
-
import { getActiveEnv, ENVS } from '../config.js';
|
|
6
|
-
import { runCommand } from '../run.js';
|
|
7
|
-
|
|
8
|
-
export const mcpKeyCommand = {
|
|
9
|
-
command: 'mcp-key <subcommand>',
|
|
10
|
-
describe: 'Manage MCP API keys (vel_live_* keys for AI skill access)',
|
|
11
|
-
builder: (yargs) =>
|
|
12
|
-
yargs
|
|
13
|
-
.command(mcpKeyListCommand)
|
|
14
|
-
.command(mcpKeyCreateCommand)
|
|
15
|
-
.command(mcpKeyRevokeCommand)
|
|
16
|
-
.command(mcpKeyRotateCommand)
|
|
17
|
-
.command(mcpKeyInstallCommand)
|
|
18
|
-
.demandCommand(1, 'Specify a subcommand: list, create, revoke, rotate, install'),
|
|
19
|
-
handler: () => {},
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
const mcpKeyListCommand = {
|
|
23
|
-
command: 'list',
|
|
24
|
-
describe: 'List MCP API keys for your site',
|
|
25
|
-
handler: runCommand(async () => {
|
|
26
|
-
const keys = await get('/McpApiKeys');
|
|
27
|
-
if (!keys?.length) { console.log('No MCP keys found.'); return; }
|
|
28
|
-
console.log(`Found ${keys.length} key(s):\n`);
|
|
29
|
-
for (const k of keys) {
|
|
30
|
-
const used = k.lastUsedAt ? new Date(k.lastUsedAt).toLocaleDateString() : 'never';
|
|
31
|
-
const expires = k.expiresAt ? ` expires ${new Date(k.expiresAt).toLocaleDateString()}` : '';
|
|
32
|
-
console.log(` [${k.id}] ${k.label} prefix=${k.keyPrefix} ${k.isActive ? 'active' : 'revoked'} last used=${used}${expires}`);
|
|
33
|
-
}
|
|
34
|
-
}),
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
const mcpKeyCreateCommand = {
|
|
38
|
-
command: 'create',
|
|
39
|
-
describe: 'Create a new MCP API key',
|
|
40
|
-
builder: (y) =>
|
|
41
|
-
y
|
|
42
|
-
.option('label', {
|
|
43
|
-
describe: 'Human-readable label (e.g. "Claude Code")',
|
|
44
|
-
type: 'string',
|
|
45
|
-
demandOption: true,
|
|
46
|
-
})
|
|
47
|
-
.option('expires', {
|
|
48
|
-
describe: 'Expiry date in ISO 8601 format (e.g. 2027-01-01)',
|
|
49
|
-
type: 'string',
|
|
50
|
-
}),
|
|
51
|
-
|
|
52
|
-
handler: runCommand(async (argv) => {
|
|
53
|
-
const payload = { label: argv.label };
|
|
54
|
-
if (argv.expires) payload.expiresAt = new Date(argv.expires).toISOString();
|
|
55
|
-
|
|
56
|
-
const result = await post('/McpApiKeys', payload);
|
|
57
|
-
console.log(`MCP key created: ${result.label}`);
|
|
58
|
-
console.log(`\n Key: ${result.rawKey}`);
|
|
59
|
-
console.log(`\n ${result.warning ?? 'Copy this key now — it will not be shown again.'}`);
|
|
60
|
-
}),
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
const mcpKeyRevokeCommand = {
|
|
64
|
-
command: 'revoke <id>',
|
|
65
|
-
describe: 'Revoke an MCP API key by ID',
|
|
66
|
-
handler: runCommand(async (argv) => {
|
|
67
|
-
await del(`/McpApiKeys/${argv.id}`);
|
|
68
|
-
console.log(`Key ${argv.id} revoked.`);
|
|
69
|
-
}),
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
const mcpKeyInstallCommand = {
|
|
73
|
-
command: 'install',
|
|
74
|
-
describe: 'Create an MCP key and write it to ~/.claude
|
|
75
|
-
builder: (y) =>
|
|
76
|
-
y
|
|
77
|
-
.option('label', {
|
|
78
|
-
describe: 'Label for the key',
|
|
79
|
-
type: 'string',
|
|
80
|
-
})
|
|
81
|
-
.option('staging', {
|
|
82
|
-
describe: 'Install key for the staging environment',
|
|
83
|
-
type: 'boolean',
|
|
84
|
-
default: false,
|
|
85
|
-
}),
|
|
86
|
-
|
|
87
|
-
handler: runCommand(async (argv) => {
|
|
88
|
-
const env = argv.staging ? 'staging' : getActiveEnv();
|
|
89
|
-
const apiBase = ENVS[env].adminApiBase;
|
|
90
|
-
const label = argv.label || (env === 'staging' ? 'Claude Code (Staging)' : 'Claude Code');
|
|
91
|
-
|
|
92
|
-
const payload = { label };
|
|
93
|
-
const result = await post('/McpApiKeys', payload);
|
|
94
|
-
const rawKey = result.rawKey;
|
|
95
|
-
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import { get, post, del } from '../api.js';
|
|
5
|
+
import { getActiveEnv, ENVS } from '../config.js';
|
|
6
|
+
import { runCommand } from '../run.js';
|
|
7
|
+
|
|
8
|
+
export const mcpKeyCommand = {
|
|
9
|
+
command: 'mcp-key <subcommand>',
|
|
10
|
+
describe: 'Manage MCP API keys (vel_live_* keys for AI skill access)',
|
|
11
|
+
builder: (yargs) =>
|
|
12
|
+
yargs
|
|
13
|
+
.command(mcpKeyListCommand)
|
|
14
|
+
.command(mcpKeyCreateCommand)
|
|
15
|
+
.command(mcpKeyRevokeCommand)
|
|
16
|
+
.command(mcpKeyRotateCommand)
|
|
17
|
+
.command(mcpKeyInstallCommand)
|
|
18
|
+
.demandCommand(1, 'Specify a subcommand: list, create, revoke, rotate, install'),
|
|
19
|
+
handler: () => {},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const mcpKeyListCommand = {
|
|
23
|
+
command: 'list',
|
|
24
|
+
describe: 'List MCP API keys for your site',
|
|
25
|
+
handler: runCommand(async () => {
|
|
26
|
+
const keys = await get('/McpApiKeys');
|
|
27
|
+
if (!keys?.length) { console.log('No MCP keys found.'); return; }
|
|
28
|
+
console.log(`Found ${keys.length} key(s):\n`);
|
|
29
|
+
for (const k of keys) {
|
|
30
|
+
const used = k.lastUsedAt ? new Date(k.lastUsedAt).toLocaleDateString() : 'never';
|
|
31
|
+
const expires = k.expiresAt ? ` expires ${new Date(k.expiresAt).toLocaleDateString()}` : '';
|
|
32
|
+
console.log(` [${k.id}] ${k.label} prefix=${k.keyPrefix} ${k.isActive ? 'active' : 'revoked'} last used=${used}${expires}`);
|
|
33
|
+
}
|
|
34
|
+
}),
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const mcpKeyCreateCommand = {
|
|
38
|
+
command: 'create',
|
|
39
|
+
describe: 'Create a new MCP API key',
|
|
40
|
+
builder: (y) =>
|
|
41
|
+
y
|
|
42
|
+
.option('label', {
|
|
43
|
+
describe: 'Human-readable label (e.g. "Claude Code")',
|
|
44
|
+
type: 'string',
|
|
45
|
+
demandOption: true,
|
|
46
|
+
})
|
|
47
|
+
.option('expires', {
|
|
48
|
+
describe: 'Expiry date in ISO 8601 format (e.g. 2027-01-01)',
|
|
49
|
+
type: 'string',
|
|
50
|
+
}),
|
|
51
|
+
|
|
52
|
+
handler: runCommand(async (argv) => {
|
|
53
|
+
const payload = { label: argv.label };
|
|
54
|
+
if (argv.expires) payload.expiresAt = new Date(argv.expires).toISOString();
|
|
55
|
+
|
|
56
|
+
const result = await post('/McpApiKeys', payload);
|
|
57
|
+
console.log(`MCP key created: ${result.label}`);
|
|
58
|
+
console.log(`\n Key: ${result.rawKey}`);
|
|
59
|
+
console.log(`\n ${result.warning ?? 'Copy this key now — it will not be shown again.'}`);
|
|
60
|
+
}),
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const mcpKeyRevokeCommand = {
|
|
64
|
+
command: 'revoke <id>',
|
|
65
|
+
describe: 'Revoke an MCP API key by ID',
|
|
66
|
+
handler: runCommand(async (argv) => {
|
|
67
|
+
await del(`/McpApiKeys/${argv.id}`);
|
|
68
|
+
console.log(`Key ${argv.id} revoked.`);
|
|
69
|
+
}),
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const mcpKeyInstallCommand = {
|
|
73
|
+
command: 'install',
|
|
74
|
+
describe: 'Create an MCP key and write it to ~/.claude.json automatically',
|
|
75
|
+
builder: (y) =>
|
|
76
|
+
y
|
|
77
|
+
.option('label', {
|
|
78
|
+
describe: 'Label for the key',
|
|
79
|
+
type: 'string',
|
|
80
|
+
})
|
|
81
|
+
.option('staging', {
|
|
82
|
+
describe: 'Install key for the staging environment',
|
|
83
|
+
type: 'boolean',
|
|
84
|
+
default: false,
|
|
85
|
+
}),
|
|
86
|
+
|
|
87
|
+
handler: runCommand(async (argv) => {
|
|
88
|
+
const env = argv.staging ? 'staging' : getActiveEnv();
|
|
89
|
+
const apiBase = ENVS[env].adminApiBase;
|
|
90
|
+
const label = argv.label || (env === 'staging' ? 'Claude Code (Staging)' : 'Claude Code');
|
|
91
|
+
|
|
92
|
+
const payload = { label };
|
|
93
|
+
const result = await post('/McpApiKeys', payload);
|
|
94
|
+
const rawKey = result.rawKey;
|
|
95
|
+
|
|
96
|
+
// Claude Code reads MCP server definitions from the top-level "mcpServers" key
|
|
97
|
+
// in ~/.claude.json (NOT ~/.claude/settings.json — that file has no effect here).
|
|
98
|
+
// This is the same shape `claude mcp add` itself writes.
|
|
99
|
+
// prod → entry named "velaro"
|
|
100
|
+
// staging → entry named "velaro-staging"
|
|
101
|
+
const serverName = env === 'prod' ? 'velaro' : `velaro-${env}`;
|
|
102
|
+
const configPath = path.join(os.homedir(), '.claude.json');
|
|
103
|
+
let config = {};
|
|
104
|
+
if (fs.existsSync(configPath)) {
|
|
105
|
+
const raw = fs.readFileSync(configPath, 'utf8');
|
|
106
|
+
try {
|
|
107
|
+
config = JSON.parse(raw);
|
|
108
|
+
} catch (err) {
|
|
109
|
+
throw new Error(
|
|
110
|
+
`~/.claude.json exists but isn't valid JSON (${err.message}) — refusing to overwrite it. ` +
|
|
111
|
+
`Fix or remove the file, then re-run 'velaro mcp-key install'.`
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
config.mcpServers = config.mcpServers || {};
|
|
117
|
+
config.mcpServers[serverName] = {
|
|
118
|
+
type: 'stdio',
|
|
119
|
+
command: 'npx',
|
|
120
|
+
args: ['-y', '@velaro/mcp-server'],
|
|
121
|
+
env: {
|
|
122
|
+
VELARO_MCP_KEY: rawKey,
|
|
123
|
+
VELARO_ADMIN_API: apiBase,
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// Claude Code itself writes ~/.claude.json continuously (history, project state),
|
|
128
|
+
// so this must never be a plain in-place write: write to a temp file and rename,
|
|
129
|
+
// which is atomic on the same filesystem — a crash or concurrent write mid-save
|
|
130
|
+
// can't leave the file truncated or silently clobber a write that landed between
|
|
131
|
+
// our read and our write.
|
|
132
|
+
const tmpPath = `${configPath}.tmp-${process.pid}`;
|
|
133
|
+
fs.writeFileSync(tmpPath, JSON.stringify(config, null, 2));
|
|
134
|
+
fs.renameSync(tmpPath, configPath);
|
|
135
|
+
|
|
136
|
+
console.log(`✅ MCP key created: ${label} (${result.keyPrefix}...)`);
|
|
137
|
+
console.log(`✅ Written to ~/.claude.json as "${serverName}" → ${apiBase}`);
|
|
138
|
+
console.log(`\nRestart Claude Code to pick up the new MCP server.`);
|
|
139
|
+
console.log(`\nTo use in Claude Code: say "use ${serverName}" to target ${env}.`);
|
|
140
|
+
}),
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const mcpKeyRotateCommand = {
|
|
144
|
+
command: 'rotate <id|label>',
|
|
145
|
+
describe: 'Revoke an existing key and issue a replacement in one step',
|
|
146
|
+
builder: (y) =>
|
|
147
|
+
y
|
|
148
|
+
.positional('id|label', { type: 'string', describe: 'Numeric ID or partial label of the key to rotate (e.g. "jb" or "42")' })
|
|
149
|
+
.option('label', {
|
|
150
|
+
describe: 'Label for the replacement key (defaults to the old key\'s label)',
|
|
151
|
+
type: 'string',
|
|
152
|
+
})
|
|
153
|
+
.option('expires', {
|
|
154
|
+
describe: 'Expiry date for the new key in ISO 8601 format (e.g. 2027-01-01)',
|
|
155
|
+
type: 'string',
|
|
156
|
+
}),
|
|
157
|
+
|
|
158
|
+
handler: runCommand(async (argv) => {
|
|
159
|
+
const keys = await get('/McpApiKeys');
|
|
160
|
+
const search = String(argv['id|label'] ?? argv.id ?? '');
|
|
161
|
+
const numId = parseInt(search, 10);
|
|
162
|
+
const matches = isNaN(numId)
|
|
163
|
+
? keys?.filter(k => k.label.toLowerCase().includes(search.toLowerCase()))
|
|
164
|
+
: keys?.filter(k => k.id === numId);
|
|
165
|
+
|
|
166
|
+
if (!matches?.length)
|
|
167
|
+
throw new Error(`No key found matching "${search}". Run 'velaro mcp-key list' to see your keys.`);
|
|
168
|
+
if (matches.length > 1) {
|
|
169
|
+
console.log(`Multiple keys match "${search}":\n`);
|
|
170
|
+
matches.forEach(k => console.log(` [${k.id}] ${k.label} (${k.keyPrefix}...)`));
|
|
171
|
+
throw new Error('Be more specific — provide the numeric ID or a more unique label fragment.');
|
|
172
|
+
}
|
|
173
|
+
const old = matches[0];
|
|
174
|
+
|
|
175
|
+
const label = argv.label || old.label;
|
|
176
|
+
|
|
177
|
+
await del(`/McpApiKeys/${old.id}`);
|
|
178
|
+
console.log(`Revoked: [${old.id}] ${old.label} (${old.keyPrefix}...)`);
|
|
179
|
+
|
|
180
|
+
const payload = { label };
|
|
181
|
+
if (argv.expires) payload.expiresAt = new Date(argv.expires).toISOString();
|
|
182
|
+
const result = await post('/McpApiKeys', payload);
|
|
183
|
+
|
|
184
|
+
console.log(`Replaced: [${result.id}] ${result.label}`);
|
|
185
|
+
console.log(`\n New key: ${result.rawKey}`);
|
|
186
|
+
console.log(`\n ${result.warning ?? 'Copy this key now — it will not be shown again.'}`);
|
|
187
|
+
}),
|
|
188
|
+
};
|