@genrupt/cli 0.1.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 +100 -0
- package/dist/auth.js +206 -0
- package/dist/cli.js +213 -0
- package/dist/config.js +53 -0
- package/dist/constants.js +7 -0
- package/dist/http.js +53 -0
- package/dist/localMcpServer.js +143 -0
- package/dist/mcpClient.js +121 -0
- package/dist/setup.js +44 -0
- package/dist/upload.js +122 -0
- package/package.json +47 -0
package/README.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# @genrupt/cli
|
|
2
|
+
|
|
3
|
+
Genrupt CLI for local-file workflows and local MCP clients.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @genrupt/cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Login
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
genrupt auth login
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The CLI uses Genrupt's OAuth device flow. It opens a browser approval page and stores refreshable credentials on the local machine.
|
|
18
|
+
|
|
19
|
+
To disconnect the machine, revoke the server-side OAuth grant and remove local credentials:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
genrupt auth logout
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
If Genrupt is unreachable and you only want to remove the local file, run:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
genrupt auth logout --local
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Claude Code
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
genrupt setup claude-code
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
This registers a local stdio MCP server:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
genrupt mcp serve
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Claude Code can then call Genrupt from chat while the local process can read user-approved local file paths.
|
|
44
|
+
|
|
45
|
+
## Upload Product Reference Images
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
genrupt upload image "./product-photo.jpg" --asin B07QF3GNS2
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Multiple files are supported:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
genrupt upload image "./front.jpg" "./detail.png" --asin B07QF3GNS2 --json
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The command creates Genrupt product reference assets and returns `sourceAssetIds` that can be used by product reference sheet workflows.
|
|
58
|
+
|
|
59
|
+
Remote MCP calls are audited by Genrupt and rate-limited per connected auth source. Product-reference upload capabilities are executed through Genrupt's capability registry, so hidden workflow tools are not directly exposed to stale MCP clients.
|
|
60
|
+
|
|
61
|
+
## Remote MCP Calls
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
genrupt mcp call get_current_account_context '{}'
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Windows-friendly flag form:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
genrupt mcp call search_capabilities --query "product reference upload" --includeUnavailable --limit 10
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
For nested arguments, write an args file and pass it directly:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
genrupt mcp call search_capabilities --json-file ./args.json
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Use this for smoke tests and low-level debugging.
|
|
80
|
+
|
|
81
|
+
## Publishing
|
|
82
|
+
|
|
83
|
+
The package is published publicly under the `@genrupt` npm scope.
|
|
84
|
+
|
|
85
|
+
Local verification:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
pnpm --filter @genrupt/cli build
|
|
89
|
+
node scripts/verify-genrupt-cli-release.cjs --check-registry
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Manual publish, from a machine logged into an npm account with `@genrupt` scope access:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
pnpm --filter @genrupt/cli publish --access public --provenance=false --otp 123456
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
If npm returns a 403 that says two-factor authentication or a granular access token is required, either pass the current npm 2FA one-time code with `--otp`, or publish with a granular npm token that has package write access and bypass 2FA enabled.
|
|
99
|
+
|
|
100
|
+
Preferred publish path is the `Publish Genrupt CLI` GitHub Actions workflow, using the `NPM_TOKEN` repository secret. The workflow publishes with provenance disabled because this repository is private and npm provenance currently requires a public GitHub source repository.
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { ACCESS_TOKEN_REFRESH_SKEW_MS, CLI_CLIENT_NAME, DEFAULT_ORIGIN, OAUTH_DEVICE_GRANT_TYPE, OAUTH_SCOPE, } from "./constants.js";
|
|
3
|
+
import { clearConfig, getConfigPath, readConfig, writeConfig, } from "./config.js";
|
|
4
|
+
import { HttpError, getErrorCode, postJson } from "./http.js";
|
|
5
|
+
function buildEndpoint(origin, path) {
|
|
6
|
+
return `${origin.replace(/\/$/, "")}${path}`;
|
|
7
|
+
}
|
|
8
|
+
function getAuthEndpoints(origin) {
|
|
9
|
+
return {
|
|
10
|
+
register: buildEndpoint(origin, "/api/agent/oauth/register"),
|
|
11
|
+
deviceAuthorization: buildEndpoint(origin, "/api/agent/oauth/device_authorization"),
|
|
12
|
+
token: buildEndpoint(origin, "/api/agent/oauth/token"),
|
|
13
|
+
revoke: buildEndpoint(origin, "/api/agent/oauth/revoke"),
|
|
14
|
+
mcpServerUrl: buildEndpoint(origin, "/api/agent/mcp"),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function toExpiresAt(expiresInSeconds) {
|
|
18
|
+
return new Date(Date.now() + Math.max(0, expiresInSeconds) * 1000).toISOString();
|
|
19
|
+
}
|
|
20
|
+
function isTokenFresh(config) {
|
|
21
|
+
return (new Date(config.accessTokenExpiresAt).getTime() - ACCESS_TOKEN_REFRESH_SKEW_MS > Date.now());
|
|
22
|
+
}
|
|
23
|
+
function openBrowser(url) {
|
|
24
|
+
const command = process.platform === "win32" ? "cmd" : process.platform === "darwin" ? "open" : "xdg-open";
|
|
25
|
+
const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
26
|
+
try {
|
|
27
|
+
const child = spawn(command, args, {
|
|
28
|
+
detached: true,
|
|
29
|
+
stdio: "ignore",
|
|
30
|
+
shell: false,
|
|
31
|
+
});
|
|
32
|
+
child.unref();
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// The printed URL is the source of truth; opening the browser is best effort.
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async function registerClient(origin) {
|
|
39
|
+
const endpoints = getAuthEndpoints(origin);
|
|
40
|
+
return postJson(endpoints.register, {
|
|
41
|
+
client_name: CLI_CLIENT_NAME,
|
|
42
|
+
grant_types: [OAUTH_DEVICE_GRANT_TYPE, "refresh_token"],
|
|
43
|
+
response_types: ["code"],
|
|
44
|
+
token_endpoint_auth_method: "none",
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
async function startDeviceAuthorization(origin, clientId) {
|
|
48
|
+
const endpoints = getAuthEndpoints(origin);
|
|
49
|
+
return postJson(endpoints.deviceAuthorization, {
|
|
50
|
+
client_id: clientId,
|
|
51
|
+
scope: OAUTH_SCOPE,
|
|
52
|
+
resource: endpoints.mcpServerUrl,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
async function exchangeDeviceCode(origin, clientId, deviceCode) {
|
|
56
|
+
const endpoints = getAuthEndpoints(origin);
|
|
57
|
+
return postJson(endpoints.token, {
|
|
58
|
+
grant_type: OAUTH_DEVICE_GRANT_TYPE,
|
|
59
|
+
client_id: clientId,
|
|
60
|
+
device_code: deviceCode,
|
|
61
|
+
resource: endpoints.mcpServerUrl,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
async function waitForDeviceApproval(params) {
|
|
65
|
+
const startedAt = Date.now();
|
|
66
|
+
const expiresAt = startedAt + params.expiresInSeconds * 1000;
|
|
67
|
+
let intervalMs = Math.max(1, params.intervalSeconds) * 1000;
|
|
68
|
+
while (Date.now() < expiresAt) {
|
|
69
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
70
|
+
try {
|
|
71
|
+
return await exchangeDeviceCode(params.origin, params.clientId, params.deviceCode);
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
if (!(error instanceof HttpError)) {
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
const code = getErrorCode(error.payload);
|
|
78
|
+
if (code === "authorization_pending") {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (code === "slow_down") {
|
|
82
|
+
intervalMs += 5_000;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
throw new Error("Device authorization expired. Run `genrupt auth login` again.");
|
|
89
|
+
}
|
|
90
|
+
export async function login(options = {}) {
|
|
91
|
+
const origin = (options.origin ?? DEFAULT_ORIGIN).replace(/\/$/, "");
|
|
92
|
+
const endpoints = getAuthEndpoints(origin);
|
|
93
|
+
const client = await registerClient(origin);
|
|
94
|
+
const device = await startDeviceAuthorization(origin, client.client_id);
|
|
95
|
+
console.log();
|
|
96
|
+
console.log("Genrupt CLI login");
|
|
97
|
+
console.log();
|
|
98
|
+
console.log(`Open this URL to approve access:\n${device.verification_uri_complete}`);
|
|
99
|
+
console.log();
|
|
100
|
+
console.log(`Code: ${device.user_code}`);
|
|
101
|
+
console.log();
|
|
102
|
+
if (!options.noOpen) {
|
|
103
|
+
openBrowser(device.verification_uri_complete);
|
|
104
|
+
}
|
|
105
|
+
const token = await waitForDeviceApproval({
|
|
106
|
+
origin,
|
|
107
|
+
clientId: client.client_id,
|
|
108
|
+
deviceCode: device.device_code,
|
|
109
|
+
intervalSeconds: device.interval,
|
|
110
|
+
expiresInSeconds: device.expires_in,
|
|
111
|
+
});
|
|
112
|
+
if (!token.refresh_token) {
|
|
113
|
+
throw new Error("OAuth response did not include a refresh token.");
|
|
114
|
+
}
|
|
115
|
+
const config = {
|
|
116
|
+
origin,
|
|
117
|
+
mcpServerUrl: endpoints.mcpServerUrl,
|
|
118
|
+
clientId: client.client_id,
|
|
119
|
+
accessToken: token.access_token,
|
|
120
|
+
accessTokenExpiresAt: toExpiresAt(token.expires_in),
|
|
121
|
+
refreshToken: token.refresh_token,
|
|
122
|
+
scope: token.scope,
|
|
123
|
+
};
|
|
124
|
+
await writeConfig(config);
|
|
125
|
+
console.log(`Logged in. Credentials saved to ${getConfigPath()}`);
|
|
126
|
+
}
|
|
127
|
+
export async function refreshAccessToken(config) {
|
|
128
|
+
const endpoints = getAuthEndpoints(config.origin);
|
|
129
|
+
const token = await postJson(endpoints.token, {
|
|
130
|
+
grant_type: "refresh_token",
|
|
131
|
+
client_id: config.clientId,
|
|
132
|
+
refresh_token: config.refreshToken,
|
|
133
|
+
resource: config.mcpServerUrl,
|
|
134
|
+
});
|
|
135
|
+
const nextConfig = {
|
|
136
|
+
...config,
|
|
137
|
+
accessToken: token.access_token,
|
|
138
|
+
accessTokenExpiresAt: toExpiresAt(token.expires_in),
|
|
139
|
+
refreshToken: token.refresh_token ?? config.refreshToken,
|
|
140
|
+
scope: token.scope,
|
|
141
|
+
};
|
|
142
|
+
await writeConfig(nextConfig);
|
|
143
|
+
return nextConfig;
|
|
144
|
+
}
|
|
145
|
+
export async function requireConfig() {
|
|
146
|
+
const config = await readConfig();
|
|
147
|
+
if (!config) {
|
|
148
|
+
throw new Error("Not logged in. Run `genrupt auth login` first.");
|
|
149
|
+
}
|
|
150
|
+
return config;
|
|
151
|
+
}
|
|
152
|
+
export async function getValidAccessToken() {
|
|
153
|
+
const config = await requireConfig();
|
|
154
|
+
if (isTokenFresh(config)) {
|
|
155
|
+
return config.accessToken;
|
|
156
|
+
}
|
|
157
|
+
const refreshed = await refreshAccessToken(config);
|
|
158
|
+
return refreshed.accessToken;
|
|
159
|
+
}
|
|
160
|
+
export async function getValidConfig() {
|
|
161
|
+
const config = await requireConfig();
|
|
162
|
+
if (isTokenFresh(config)) {
|
|
163
|
+
return config;
|
|
164
|
+
}
|
|
165
|
+
return refreshAccessToken(config);
|
|
166
|
+
}
|
|
167
|
+
export async function printAuthStatus() {
|
|
168
|
+
const config = await readConfig();
|
|
169
|
+
if (!config) {
|
|
170
|
+
console.log("Not logged in.");
|
|
171
|
+
console.log("Run: genrupt auth login");
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
console.log("Logged in.");
|
|
175
|
+
console.log(`Origin: ${config.origin}`);
|
|
176
|
+
console.log(`MCP server: ${config.mcpServerUrl}`);
|
|
177
|
+
console.log(`Access token expires: ${config.accessTokenExpiresAt}`);
|
|
178
|
+
console.log(`Config: ${getConfigPath()}`);
|
|
179
|
+
}
|
|
180
|
+
async function revokeCurrentCredentials(config) {
|
|
181
|
+
const endpoints = getAuthEndpoints(config.origin);
|
|
182
|
+
await postJson(endpoints.revoke, {
|
|
183
|
+
client_id: config.clientId,
|
|
184
|
+
token: config.refreshToken,
|
|
185
|
+
token_type_hint: "refresh_token",
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
export async function logout(options = {}) {
|
|
189
|
+
const config = await readConfig();
|
|
190
|
+
if (config && !options.localOnly) {
|
|
191
|
+
try {
|
|
192
|
+
await revokeCurrentCredentials(config);
|
|
193
|
+
console.log("Server credentials revoked.");
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
197
|
+
console.warn(`Could not revoke server credentials: ${message}`);
|
|
198
|
+
console.warn("Local credentials will still be removed.");
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
await clearConfig();
|
|
202
|
+
console.log("Logged out.");
|
|
203
|
+
}
|
|
204
|
+
export async function assertLoggedIn() {
|
|
205
|
+
await getValidAccessToken();
|
|
206
|
+
}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { CLI_VERSION } from "./constants.js";
|
|
4
|
+
import { login, logout, printAuthStatus } from "./auth.js";
|
|
5
|
+
import { callRemoteMcpTool, assertSuccessfulToolPayload } from "./mcpClient.js";
|
|
6
|
+
import { serveLocalMcp } from "./localMcpServer.js";
|
|
7
|
+
import { setupClaudeCode } from "./setup.js";
|
|
8
|
+
import { uploadProductReferenceImages } from "./upload.js";
|
|
9
|
+
function parseFlags(args) {
|
|
10
|
+
const flags = {};
|
|
11
|
+
const positionals = [];
|
|
12
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
13
|
+
const arg = args[index];
|
|
14
|
+
if (!arg.startsWith("--")) {
|
|
15
|
+
positionals.push(arg);
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
const [rawKey, inlineValue] = arg.slice(2).split("=", 2);
|
|
19
|
+
if (inlineValue !== undefined) {
|
|
20
|
+
flags[rawKey] = inlineValue;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
const next = args[index + 1];
|
|
24
|
+
if (next && !next.startsWith("--")) {
|
|
25
|
+
flags[rawKey] = next;
|
|
26
|
+
index += 1;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
flags[rawKey] = true;
|
|
30
|
+
}
|
|
31
|
+
return { flags, positionals };
|
|
32
|
+
}
|
|
33
|
+
function getStringFlag(flags, key) {
|
|
34
|
+
const value = flags[key];
|
|
35
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
36
|
+
}
|
|
37
|
+
function hasFlag(flags, key) {
|
|
38
|
+
return flags[key] === true;
|
|
39
|
+
}
|
|
40
|
+
function printHelp() {
|
|
41
|
+
console.log(`
|
|
42
|
+
Genrupt CLI ${CLI_VERSION}
|
|
43
|
+
|
|
44
|
+
Usage:
|
|
45
|
+
genrupt auth login [--origin https://genrupt.com] [--no-open]
|
|
46
|
+
genrupt auth status
|
|
47
|
+
genrupt auth logout [--local]
|
|
48
|
+
|
|
49
|
+
genrupt setup claude-code [--replace] [--scope user|project|local]
|
|
50
|
+
|
|
51
|
+
genrupt upload image <path...> [--asin ASIN] [--project-id ID] [--product-profile-id ID] [--domain amazon.com] [--json]
|
|
52
|
+
|
|
53
|
+
genrupt mcp serve
|
|
54
|
+
genrupt mcp call <tool_name> '<json_args>'
|
|
55
|
+
genrupt mcp call <tool_name> --query "..." --limit 10
|
|
56
|
+
genrupt mcp call <tool_name> --json-file ./args.json
|
|
57
|
+
|
|
58
|
+
Examples:
|
|
59
|
+
genrupt auth login
|
|
60
|
+
genrupt setup claude-code
|
|
61
|
+
genrupt upload image "G:\\My Drive\\product.jpg" --asin B07QF3GNS2
|
|
62
|
+
genrupt mcp call search_capabilities --query "product reference upload" --includeUnavailable --limit 10
|
|
63
|
+
`);
|
|
64
|
+
}
|
|
65
|
+
async function handleAuth(args) {
|
|
66
|
+
const subcommand = args[0];
|
|
67
|
+
const { flags } = parseFlags(args.slice(1));
|
|
68
|
+
if (subcommand === "login") {
|
|
69
|
+
await login({
|
|
70
|
+
origin: getStringFlag(flags, "origin"),
|
|
71
|
+
noOpen: hasFlag(flags, "no-open"),
|
|
72
|
+
});
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (subcommand === "status") {
|
|
76
|
+
await printAuthStatus();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (subcommand === "logout") {
|
|
80
|
+
await logout({
|
|
81
|
+
localOnly: hasFlag(flags, "local"),
|
|
82
|
+
});
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
throw new Error("Unknown auth command. Run `genrupt --help`.");
|
|
86
|
+
}
|
|
87
|
+
async function handleSetup(args) {
|
|
88
|
+
const subcommand = args[0];
|
|
89
|
+
const { flags } = parseFlags(args.slice(1));
|
|
90
|
+
if (subcommand === "claude-code") {
|
|
91
|
+
const scope = getStringFlag(flags, "scope") ?? "user";
|
|
92
|
+
if (!["local", "project", "user"].includes(scope)) {
|
|
93
|
+
throw new Error("--scope must be local, project, or user.");
|
|
94
|
+
}
|
|
95
|
+
await setupClaudeCode({
|
|
96
|
+
replace: hasFlag(flags, "replace"),
|
|
97
|
+
scope: scope,
|
|
98
|
+
});
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
throw new Error("Unknown setup command. Run `genrupt --help`.");
|
|
102
|
+
}
|
|
103
|
+
async function handleUpload(args) {
|
|
104
|
+
const type = args[0];
|
|
105
|
+
if (type !== "image") {
|
|
106
|
+
throw new Error("Only `genrupt upload image` is supported.");
|
|
107
|
+
}
|
|
108
|
+
const { flags, positionals } = parseFlags(args.slice(1));
|
|
109
|
+
const result = await uploadProductReferenceImages({
|
|
110
|
+
paths: positionals,
|
|
111
|
+
asin: getStringFlag(flags, "asin"),
|
|
112
|
+
projectId: getStringFlag(flags, "project-id"),
|
|
113
|
+
productProfileId: getStringFlag(flags, "product-profile-id"),
|
|
114
|
+
domain: getStringFlag(flags, "domain"),
|
|
115
|
+
});
|
|
116
|
+
if (hasFlag(flags, "json")) {
|
|
117
|
+
console.log(JSON.stringify(result, null, 2));
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
console.log();
|
|
121
|
+
console.log(`Uploaded ${result.uploads.length} image${result.uploads.length === 1 ? "" : "s"}.`);
|
|
122
|
+
console.log(`sourceAssetIds: ${result.sourceAssetIds.join(", ") || "(none returned)"}`);
|
|
123
|
+
for (const upload of result.uploads) {
|
|
124
|
+
console.log(`- ${upload.fileName}: ${upload.assetId ?? "no asset id"} ${upload.imageUrl}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function coerceMcpFlagValue(value) {
|
|
128
|
+
if (typeof value === "boolean") {
|
|
129
|
+
return value;
|
|
130
|
+
}
|
|
131
|
+
const trimmed = value.trim();
|
|
132
|
+
if (trimmed === "true")
|
|
133
|
+
return true;
|
|
134
|
+
if (trimmed === "false")
|
|
135
|
+
return false;
|
|
136
|
+
if (trimmed === "null")
|
|
137
|
+
return null;
|
|
138
|
+
const numeric = Number(trimmed);
|
|
139
|
+
if (trimmed && Number.isFinite(numeric) && String(numeric) === trimmed) {
|
|
140
|
+
return numeric;
|
|
141
|
+
}
|
|
142
|
+
if ((trimmed.startsWith("{") && trimmed.endsWith("}")) ||
|
|
143
|
+
(trimmed.startsWith("[") && trimmed.endsWith("]"))) {
|
|
144
|
+
return JSON.parse(trimmed);
|
|
145
|
+
}
|
|
146
|
+
return value;
|
|
147
|
+
}
|
|
148
|
+
async function readMcpCallArguments(args) {
|
|
149
|
+
const { flags, positionals } = parseFlags(args);
|
|
150
|
+
const jsonFile = getStringFlag(flags, "json-file");
|
|
151
|
+
if (jsonFile) {
|
|
152
|
+
return JSON.parse(await readFile(jsonFile, "utf8"));
|
|
153
|
+
}
|
|
154
|
+
if (positionals.length > 0) {
|
|
155
|
+
return JSON.parse(positionals.join(" "));
|
|
156
|
+
}
|
|
157
|
+
const parsed = Object.fromEntries(Object.entries(flags)
|
|
158
|
+
.filter(([key]) => key !== "json-file")
|
|
159
|
+
.map(([key, value]) => [key, coerceMcpFlagValue(value)]));
|
|
160
|
+
return parsed;
|
|
161
|
+
}
|
|
162
|
+
async function handleMcp(args) {
|
|
163
|
+
const subcommand = args[0];
|
|
164
|
+
if (subcommand === "serve") {
|
|
165
|
+
await serveLocalMcp();
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (subcommand === "call") {
|
|
169
|
+
const toolName = args[1];
|
|
170
|
+
if (!toolName) {
|
|
171
|
+
throw new Error("Tool name is required.");
|
|
172
|
+
}
|
|
173
|
+
const parsedArgs = await readMcpCallArguments(args.slice(2));
|
|
174
|
+
const result = await callRemoteMcpTool(toolName, parsedArgs);
|
|
175
|
+
const payload = assertSuccessfulToolPayload(result);
|
|
176
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
throw new Error("Unknown mcp command. Run `genrupt --help`.");
|
|
180
|
+
}
|
|
181
|
+
async function main() {
|
|
182
|
+
const args = process.argv.slice(2);
|
|
183
|
+
const command = args[0];
|
|
184
|
+
if (!command || command === "--help" || command === "-h") {
|
|
185
|
+
printHelp();
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (command === "--version" || command === "-v") {
|
|
189
|
+
console.log(CLI_VERSION);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (command === "auth") {
|
|
193
|
+
await handleAuth(args.slice(1));
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (command === "setup") {
|
|
197
|
+
await handleSetup(args.slice(1));
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (command === "upload") {
|
|
201
|
+
await handleUpload(args.slice(1));
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (command === "mcp") {
|
|
205
|
+
await handleMcp(args.slice(1));
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
throw new Error(`Unknown command: ${command}. Run \`genrupt --help\`.`);
|
|
209
|
+
}
|
|
210
|
+
main().catch((error) => {
|
|
211
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
212
|
+
process.exit(1);
|
|
213
|
+
});
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { DEFAULT_MCP_SERVER_URL, DEFAULT_ORIGIN } from "./constants.js";
|
|
5
|
+
export function getConfigDir() {
|
|
6
|
+
if (process.env.GENRUPT_CONFIG_DIR?.trim()) {
|
|
7
|
+
return process.env.GENRUPT_CONFIG_DIR.trim();
|
|
8
|
+
}
|
|
9
|
+
if (process.platform === "win32") {
|
|
10
|
+
return path.join(process.env.APPDATA ?? os.homedir(), "Genrupt");
|
|
11
|
+
}
|
|
12
|
+
return path.join(process.env.XDG_CONFIG_HOME ?? path.join(os.homedir(), ".config"), "genrupt");
|
|
13
|
+
}
|
|
14
|
+
export function getConfigPath() {
|
|
15
|
+
return path.join(getConfigDir(), "config.json");
|
|
16
|
+
}
|
|
17
|
+
export async function readConfig() {
|
|
18
|
+
try {
|
|
19
|
+
const raw = await readFile(getConfigPath(), "utf8");
|
|
20
|
+
const parsed = JSON.parse(raw);
|
|
21
|
+
if (!parsed.clientId ||
|
|
22
|
+
!parsed.accessToken ||
|
|
23
|
+
!parsed.accessTokenExpiresAt ||
|
|
24
|
+
!parsed.refreshToken) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
origin: parsed.origin ?? DEFAULT_ORIGIN,
|
|
29
|
+
mcpServerUrl: parsed.mcpServerUrl ?? DEFAULT_MCP_SERVER_URL,
|
|
30
|
+
clientId: parsed.clientId,
|
|
31
|
+
accessToken: parsed.accessToken,
|
|
32
|
+
accessTokenExpiresAt: parsed.accessTokenExpiresAt,
|
|
33
|
+
refreshToken: parsed.refreshToken,
|
|
34
|
+
scope: parsed.scope ?? "mcp",
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
if (error.code === "ENOENT") {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export async function writeConfig(config) {
|
|
45
|
+
await mkdir(getConfigDir(), { recursive: true });
|
|
46
|
+
await writeFile(getConfigPath(), `${JSON.stringify(config, null, 2)}\n`, {
|
|
47
|
+
encoding: "utf8",
|
|
48
|
+
mode: 0o600,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
export async function clearConfig() {
|
|
52
|
+
await rm(getConfigPath(), { force: true });
|
|
53
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const DEFAULT_ORIGIN = process.env.GENRUPT_ORIGIN?.replace(/\/$/, "") ?? "https://genrupt.com";
|
|
2
|
+
export const DEFAULT_MCP_SERVER_URL = `${DEFAULT_ORIGIN}/api/agent/mcp`;
|
|
3
|
+
export const OAUTH_DEVICE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
|
|
4
|
+
export const OAUTH_SCOPE = "mcp";
|
|
5
|
+
export const CLI_CLIENT_NAME = "Genrupt CLI";
|
|
6
|
+
export const CLI_VERSION = "0.1.0";
|
|
7
|
+
export const ACCESS_TOKEN_REFRESH_SKEW_MS = 60_000;
|
package/dist/http.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export class HttpError extends Error {
|
|
2
|
+
status;
|
|
3
|
+
payload;
|
|
4
|
+
constructor(message, status, payload) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "HttpError";
|
|
7
|
+
this.status = status;
|
|
8
|
+
this.payload = payload;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export function getErrorDescription(payload) {
|
|
12
|
+
if (!payload || typeof payload !== "object") {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
const record = payload;
|
|
16
|
+
const description = record.error_description ?? record.message;
|
|
17
|
+
return typeof description === "string" && description.trim() ? description : null;
|
|
18
|
+
}
|
|
19
|
+
export function getErrorCode(payload) {
|
|
20
|
+
if (!payload || typeof payload !== "object") {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
const code = payload.error;
|
|
24
|
+
return typeof code === "string" && code.trim() ? code : null;
|
|
25
|
+
}
|
|
26
|
+
async function readResponsePayload(response) {
|
|
27
|
+
const text = await response.text();
|
|
28
|
+
if (!text.trim()) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse(text);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return text;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export async function postJson(url, body, headers = {}) {
|
|
39
|
+
const response = await fetch(url, {
|
|
40
|
+
method: "POST",
|
|
41
|
+
headers: {
|
|
42
|
+
"Content-Type": "application/json",
|
|
43
|
+
Accept: "application/json",
|
|
44
|
+
...headers,
|
|
45
|
+
},
|
|
46
|
+
body: JSON.stringify(body),
|
|
47
|
+
});
|
|
48
|
+
const payload = await readResponsePayload(response);
|
|
49
|
+
if (!response.ok) {
|
|
50
|
+
throw new HttpError(getErrorDescription(payload) ?? `Request failed with HTTP ${response.status}.`, response.status, payload);
|
|
51
|
+
}
|
|
52
|
+
return payload;
|
|
53
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
4
|
+
import { CLI_VERSION } from "./constants.js";
|
|
5
|
+
import { listRemoteMcpTools, callRemoteMcpTool } from "./mcpClient.js";
|
|
6
|
+
import { uploadProductReferenceImages } from "./upload.js";
|
|
7
|
+
const LOCAL_UPLOAD_TOOL = {
|
|
8
|
+
name: "upload_product_reference_images",
|
|
9
|
+
title: "Upload local product reference images",
|
|
10
|
+
description: "Use when the user provides local image file paths from this computer and wants them uploaded to Genrupt as product reference assets. This local CLI MCP tool can read local files, uploads them through Genrupt's presigned upload flow, completes registration, and returns sourceAssetIds for create_product_reference_sheet. Supports JPEG, PNG, and WebP images up to 50 MB each.",
|
|
11
|
+
inputSchema: {
|
|
12
|
+
type: "object",
|
|
13
|
+
properties: {
|
|
14
|
+
paths: {
|
|
15
|
+
type: "array",
|
|
16
|
+
minItems: 1,
|
|
17
|
+
items: { type: "string", minLength: 1 },
|
|
18
|
+
description: "Absolute or working-directory-relative local image file paths.",
|
|
19
|
+
},
|
|
20
|
+
asin: {
|
|
21
|
+
type: "string",
|
|
22
|
+
minLength: 1,
|
|
23
|
+
description: "Optional Amazon ASIN to associate with the product reference assets.",
|
|
24
|
+
},
|
|
25
|
+
projectId: {
|
|
26
|
+
type: "string",
|
|
27
|
+
minLength: 1,
|
|
28
|
+
description: "Optional Genrupt project ID to associate with the uploaded assets.",
|
|
29
|
+
},
|
|
30
|
+
productProfileId: {
|
|
31
|
+
type: "string",
|
|
32
|
+
minLength: 1,
|
|
33
|
+
description: "Optional Genrupt product profile ID to associate with the uploaded assets.",
|
|
34
|
+
},
|
|
35
|
+
domain: {
|
|
36
|
+
type: "string",
|
|
37
|
+
minLength: 1,
|
|
38
|
+
description: "Optional Amazon domain, such as amazon.com.",
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
required: ["paths"],
|
|
42
|
+
additionalProperties: false,
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
function buildToolResult(payload) {
|
|
46
|
+
return {
|
|
47
|
+
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
|
|
48
|
+
structuredContent: payload,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function buildToolError(error) {
|
|
52
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
53
|
+
return {
|
|
54
|
+
isError: true,
|
|
55
|
+
content: [
|
|
56
|
+
{
|
|
57
|
+
type: "text",
|
|
58
|
+
text: JSON.stringify({
|
|
59
|
+
error: {
|
|
60
|
+
code: "GENRUPT_CLI_TOOL_ERROR",
|
|
61
|
+
message,
|
|
62
|
+
retryable: false,
|
|
63
|
+
},
|
|
64
|
+
}, null, 2),
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
structuredContent: {
|
|
68
|
+
error: {
|
|
69
|
+
code: "GENRUPT_CLI_TOOL_ERROR",
|
|
70
|
+
message,
|
|
71
|
+
retryable: false,
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function normalizeStringArray(value) {
|
|
77
|
+
if (!Array.isArray(value)) {
|
|
78
|
+
throw new Error("paths must be an array of image file paths.");
|
|
79
|
+
}
|
|
80
|
+
const paths = value.filter((entry) => typeof entry === "string" && entry.trim().length > 0);
|
|
81
|
+
if (paths.length === 0) {
|
|
82
|
+
throw new Error("paths must include at least one image file path.");
|
|
83
|
+
}
|
|
84
|
+
return paths;
|
|
85
|
+
}
|
|
86
|
+
function getOptionalString(args, key) {
|
|
87
|
+
const value = args[key];
|
|
88
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
89
|
+
}
|
|
90
|
+
async function handleLocalUploadTool(args) {
|
|
91
|
+
const payload = await uploadProductReferenceImages({
|
|
92
|
+
paths: normalizeStringArray(args.paths),
|
|
93
|
+
asin: getOptionalString(args, "asin"),
|
|
94
|
+
projectId: getOptionalString(args, "projectId"),
|
|
95
|
+
productProfileId: getOptionalString(args, "productProfileId"),
|
|
96
|
+
domain: getOptionalString(args, "domain"),
|
|
97
|
+
});
|
|
98
|
+
return buildToolResult(payload);
|
|
99
|
+
}
|
|
100
|
+
export async function serveLocalMcp() {
|
|
101
|
+
const server = new Server({
|
|
102
|
+
name: "genrupt-cli",
|
|
103
|
+
version: CLI_VERSION,
|
|
104
|
+
}, {
|
|
105
|
+
capabilities: {
|
|
106
|
+
tools: {},
|
|
107
|
+
},
|
|
108
|
+
instructions: "Genrupt local bridge. Use upload_product_reference_images for local file paths, then use Genrupt remote MCP tools for market analysis, listing images, A+ content, product reference sheets, and video workflows.",
|
|
109
|
+
});
|
|
110
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
111
|
+
try {
|
|
112
|
+
const remote = await listRemoteMcpTools();
|
|
113
|
+
return {
|
|
114
|
+
tools: [
|
|
115
|
+
LOCAL_UPLOAD_TOOL,
|
|
116
|
+
...remote.tools.filter((tool) => tool.name !== LOCAL_UPLOAD_TOOL.name),
|
|
117
|
+
],
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
console.error(`[genrupt] Failed to list remote MCP tools: ${error instanceof Error ? error.message : String(error)}`);
|
|
122
|
+
return {
|
|
123
|
+
tools: [LOCAL_UPLOAD_TOOL],
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
128
|
+
const args = request.params.arguments && typeof request.params.arguments === "object"
|
|
129
|
+
? request.params.arguments
|
|
130
|
+
: {};
|
|
131
|
+
try {
|
|
132
|
+
if (request.params.name === LOCAL_UPLOAD_TOOL.name) {
|
|
133
|
+
return await handleLocalUploadTool(args);
|
|
134
|
+
}
|
|
135
|
+
return await callRemoteMcpTool(request.params.name, args);
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
return buildToolError(error);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
const transport = new StdioServerTransport();
|
|
142
|
+
await server.connect(transport);
|
|
143
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { getValidAccessToken, getValidConfig, refreshAccessToken } from "./auth.js";
|
|
2
|
+
import { HttpError, getErrorDescription } from "./http.js";
|
|
3
|
+
let nextJsonRpcId = 1;
|
|
4
|
+
function parseEventStreamPayload(text) {
|
|
5
|
+
const dataLines = text
|
|
6
|
+
.split(/\r?\n/)
|
|
7
|
+
.filter((line) => line.startsWith("data:"))
|
|
8
|
+
.map((line) => line.slice(5).trim())
|
|
9
|
+
.filter(Boolean);
|
|
10
|
+
if (dataLines.length === 0) {
|
|
11
|
+
throw new Error("MCP server returned an empty event stream.");
|
|
12
|
+
}
|
|
13
|
+
return JSON.parse(dataLines[dataLines.length - 1]);
|
|
14
|
+
}
|
|
15
|
+
async function readJsonRpcResponse(response) {
|
|
16
|
+
const text = await response.text();
|
|
17
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
18
|
+
const payload = contentType.includes("text/event-stream")
|
|
19
|
+
? parseEventStreamPayload(text)
|
|
20
|
+
: JSON.parse(text);
|
|
21
|
+
if (!response.ok) {
|
|
22
|
+
throw new HttpError(getErrorDescription(payload) ?? `MCP request failed with HTTP ${response.status}.`, response.status, payload);
|
|
23
|
+
}
|
|
24
|
+
if ("error" in payload) {
|
|
25
|
+
throw new Error(payload.error.message);
|
|
26
|
+
}
|
|
27
|
+
return payload.result;
|
|
28
|
+
}
|
|
29
|
+
async function postMcpJsonRpc(params) {
|
|
30
|
+
const response = await fetch(params.mcpServerUrl, {
|
|
31
|
+
method: "POST",
|
|
32
|
+
headers: {
|
|
33
|
+
Authorization: `Bearer ${params.accessToken}`,
|
|
34
|
+
"Content-Type": "application/json",
|
|
35
|
+
Accept: "application/json, text/event-stream",
|
|
36
|
+
},
|
|
37
|
+
body: JSON.stringify({
|
|
38
|
+
jsonrpc: "2.0",
|
|
39
|
+
id: nextJsonRpcId++,
|
|
40
|
+
method: params.method,
|
|
41
|
+
params: params.rpcParams ?? {},
|
|
42
|
+
}),
|
|
43
|
+
});
|
|
44
|
+
return readJsonRpcResponse(response);
|
|
45
|
+
}
|
|
46
|
+
async function withAuthRetry(call) {
|
|
47
|
+
const config = await getValidConfig();
|
|
48
|
+
try {
|
|
49
|
+
return await call(config.accessToken, config.mcpServerUrl);
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
if (!(error instanceof HttpError) || error.status !== 401) {
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
const refreshed = await refreshAccessToken(config);
|
|
56
|
+
return call(refreshed.accessToken, refreshed.mcpServerUrl);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export async function listRemoteMcpTools() {
|
|
60
|
+
return withAuthRetry(async (accessToken, mcpServerUrl) => postMcpJsonRpc({
|
|
61
|
+
method: "tools/list",
|
|
62
|
+
accessToken,
|
|
63
|
+
mcpServerUrl,
|
|
64
|
+
}));
|
|
65
|
+
}
|
|
66
|
+
export async function callRemoteMcpTool(name, args) {
|
|
67
|
+
return withAuthRetry(async (accessToken, mcpServerUrl) => postMcpJsonRpc({
|
|
68
|
+
method: "tools/call",
|
|
69
|
+
rpcParams: {
|
|
70
|
+
name,
|
|
71
|
+
arguments: args,
|
|
72
|
+
},
|
|
73
|
+
accessToken,
|
|
74
|
+
mcpServerUrl,
|
|
75
|
+
}));
|
|
76
|
+
}
|
|
77
|
+
export async function executeRemoteCapability(capabilityId, args) {
|
|
78
|
+
return callRemoteMcpTool("execute_capability", {
|
|
79
|
+
capabilityId,
|
|
80
|
+
arguments: args,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
export async function callRemoteMcpToolWithFreshToken(name, args) {
|
|
84
|
+
const accessToken = await getValidAccessToken();
|
|
85
|
+
const config = await getValidConfig();
|
|
86
|
+
return postMcpJsonRpc({
|
|
87
|
+
method: "tools/call",
|
|
88
|
+
rpcParams: {
|
|
89
|
+
name,
|
|
90
|
+
arguments: args,
|
|
91
|
+
},
|
|
92
|
+
accessToken,
|
|
93
|
+
mcpServerUrl: config.mcpServerUrl,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
export function getStructuredContent(result) {
|
|
97
|
+
if (result.structuredContent !== undefined) {
|
|
98
|
+
return result.structuredContent;
|
|
99
|
+
}
|
|
100
|
+
const textBlock = result.content?.find((block) => block.type === "text" && typeof block.text === "string");
|
|
101
|
+
if (!textBlock?.text) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
return JSON.parse(textBlock.text);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return { text: textBlock.text };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
export function assertSuccessfulToolPayload(result) {
|
|
112
|
+
const payload = getStructuredContent(result);
|
|
113
|
+
if (result.isError) {
|
|
114
|
+
const textBlock = result.content?.find((block) => block.type === "text" && typeof block.text === "string" && block.text.trim());
|
|
115
|
+
const message = payload && typeof payload === "object" && "error" in payload
|
|
116
|
+
? JSON.stringify(payload.error)
|
|
117
|
+
: textBlock?.text?.trim() || "Remote Genrupt tool call failed.";
|
|
118
|
+
throw new Error(message);
|
|
119
|
+
}
|
|
120
|
+
return payload;
|
|
121
|
+
}
|
package/dist/setup.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { assertLoggedIn } from "./auth.js";
|
|
3
|
+
function runClaude(args) {
|
|
4
|
+
return spawnSync("claude", args, {
|
|
5
|
+
stdio: "inherit",
|
|
6
|
+
shell: process.platform === "win32",
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
export async function setupClaudeCode(options = {}) {
|
|
10
|
+
await assertLoggedIn();
|
|
11
|
+
const scope = options.scope ?? "user";
|
|
12
|
+
if (options.replace) {
|
|
13
|
+
runClaude(["mcp", "remove", "genrupt"]);
|
|
14
|
+
}
|
|
15
|
+
const args = [
|
|
16
|
+
"mcp",
|
|
17
|
+
"add",
|
|
18
|
+
"--transport",
|
|
19
|
+
"stdio",
|
|
20
|
+
"--scope",
|
|
21
|
+
scope,
|
|
22
|
+
"genrupt",
|
|
23
|
+
"--",
|
|
24
|
+
"genrupt",
|
|
25
|
+
"mcp",
|
|
26
|
+
"serve",
|
|
27
|
+
];
|
|
28
|
+
const result = runClaude(args);
|
|
29
|
+
if (result.error || result.status !== 0) {
|
|
30
|
+
console.log();
|
|
31
|
+
console.log("Could not register Genrupt with Claude Code automatically.");
|
|
32
|
+
console.log("Run this command manually:");
|
|
33
|
+
console.log();
|
|
34
|
+
console.log(`claude ${args.join(" ")}`);
|
|
35
|
+
console.log();
|
|
36
|
+
if (result.error) {
|
|
37
|
+
throw result.error;
|
|
38
|
+
}
|
|
39
|
+
process.exit(result.status ?? 1);
|
|
40
|
+
}
|
|
41
|
+
console.log();
|
|
42
|
+
console.log("Genrupt local MCP bridge is installed for Claude Code.");
|
|
43
|
+
console.log("Start a new Claude Code chat, then ask Genrupt to upload local product files or run a workflow.");
|
|
44
|
+
}
|
package/dist/upload.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { readFile, stat } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { assertSuccessfulToolPayload, executeRemoteCapability } from "./mcpClient.js";
|
|
4
|
+
const MAX_PRODUCT_REFERENCE_BYTES = 50 * 1024 * 1024;
|
|
5
|
+
function detectContentType(filePath) {
|
|
6
|
+
const extension = path.extname(filePath).toLowerCase();
|
|
7
|
+
if (extension === ".jpg" || extension === ".jpeg") {
|
|
8
|
+
return "image/jpeg";
|
|
9
|
+
}
|
|
10
|
+
if (extension === ".png") {
|
|
11
|
+
return "image/png";
|
|
12
|
+
}
|
|
13
|
+
if (extension === ".webp") {
|
|
14
|
+
return "image/webp";
|
|
15
|
+
}
|
|
16
|
+
throw new Error(`Unsupported image type for ${filePath}. Use JPEG, PNG, or WebP.`);
|
|
17
|
+
}
|
|
18
|
+
function asRecord(value) {
|
|
19
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
20
|
+
? value
|
|
21
|
+
: null;
|
|
22
|
+
}
|
|
23
|
+
function requireCreateUploadPayload(payload) {
|
|
24
|
+
const record = asRecord(payload);
|
|
25
|
+
if (!record ||
|
|
26
|
+
typeof record.uploadUrl !== "string" ||
|
|
27
|
+
typeof record.assetUrl !== "string" ||
|
|
28
|
+
typeof record.objectKey !== "string") {
|
|
29
|
+
throw new Error("Genrupt did not return a valid upload URL payload.");
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
uploadUrl: record.uploadUrl,
|
|
33
|
+
assetUrl: record.assetUrl,
|
|
34
|
+
objectKey: record.objectKey,
|
|
35
|
+
headers: asRecord(record.headers),
|
|
36
|
+
method: typeof record.method === "string" ? record.method : "PUT",
|
|
37
|
+
maxBytes: typeof record.maxBytes === "number" ? record.maxBytes : undefined,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function requireCompleteUploadPayload(payload) {
|
|
41
|
+
const record = asRecord(payload);
|
|
42
|
+
if (!record) {
|
|
43
|
+
throw new Error("Genrupt did not return a valid completed upload payload.");
|
|
44
|
+
}
|
|
45
|
+
return record;
|
|
46
|
+
}
|
|
47
|
+
async function uploadBytes(params) {
|
|
48
|
+
const body = new Blob([new Uint8Array(params.bytes)]);
|
|
49
|
+
const response = await fetch(params.uploadUrl, {
|
|
50
|
+
method: params.method,
|
|
51
|
+
headers: params.headers,
|
|
52
|
+
body,
|
|
53
|
+
});
|
|
54
|
+
if (!response.ok) {
|
|
55
|
+
const body = await response.text().catch(() => "");
|
|
56
|
+
throw new Error(`Storage upload failed with HTTP ${response.status}${body ? `: ${body.slice(0, 500)}` : ""}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export async function uploadProductReferenceImages(options) {
|
|
60
|
+
if (options.paths.length === 0) {
|
|
61
|
+
throw new Error("At least one image path is required.");
|
|
62
|
+
}
|
|
63
|
+
const uploads = [];
|
|
64
|
+
for (const filePath of options.paths) {
|
|
65
|
+
const fileStat = await stat(filePath);
|
|
66
|
+
if (!fileStat.isFile()) {
|
|
67
|
+
throw new Error(`${filePath} is not a file.`);
|
|
68
|
+
}
|
|
69
|
+
if (fileStat.size > MAX_PRODUCT_REFERENCE_BYTES) {
|
|
70
|
+
throw new Error(`${filePath} is larger than 50 MB.`);
|
|
71
|
+
}
|
|
72
|
+
const fileName = path.basename(filePath);
|
|
73
|
+
const contentType = detectContentType(filePath);
|
|
74
|
+
const createResult = await executeRemoteCapability("create_product_reference_upload", {
|
|
75
|
+
fileName,
|
|
76
|
+
contentType,
|
|
77
|
+
byteLength: fileStat.size,
|
|
78
|
+
...(options.asin ? { asin: options.asin } : {}),
|
|
79
|
+
...(options.projectId ? { projectId: options.projectId } : {}),
|
|
80
|
+
...(options.productProfileId ? { productProfileId: options.productProfileId } : {}),
|
|
81
|
+
...(options.domain ? { domain: options.domain } : {}),
|
|
82
|
+
});
|
|
83
|
+
const createPayload = requireCreateUploadPayload(assertSuccessfulToolPayload(createResult));
|
|
84
|
+
const bytes = await readFile(filePath);
|
|
85
|
+
await uploadBytes({
|
|
86
|
+
uploadUrl: createPayload.uploadUrl,
|
|
87
|
+
method: createPayload.method ?? "PUT",
|
|
88
|
+
headers: createPayload.headers ?? { "Content-Type": contentType },
|
|
89
|
+
bytes,
|
|
90
|
+
});
|
|
91
|
+
const completeResult = await executeRemoteCapability("complete_product_reference_upload", {
|
|
92
|
+
objectKey: createPayload.objectKey,
|
|
93
|
+
assetUrl: createPayload.assetUrl,
|
|
94
|
+
contentType,
|
|
95
|
+
byteLength: fileStat.size,
|
|
96
|
+
fileName,
|
|
97
|
+
...(options.asin ? { asin: options.asin } : {}),
|
|
98
|
+
...(options.projectId ? { projectId: options.projectId } : {}),
|
|
99
|
+
...(options.productProfileId ? { productProfileId: options.productProfileId } : {}),
|
|
100
|
+
...(options.domain ? { domain: options.domain } : {}),
|
|
101
|
+
});
|
|
102
|
+
const completePayload = requireCompleteUploadPayload(assertSuccessfulToolPayload(completeResult));
|
|
103
|
+
uploads.push({
|
|
104
|
+
path: filePath,
|
|
105
|
+
fileName,
|
|
106
|
+
objectKey: createPayload.objectKey,
|
|
107
|
+
assetUrl: createPayload.assetUrl,
|
|
108
|
+
assetId: completePayload.asset?.id ?? null,
|
|
109
|
+
imageUrl: completePayload.asset?.imageUrl ?? createPayload.assetUrl,
|
|
110
|
+
contentType,
|
|
111
|
+
byteLength: fileStat.size,
|
|
112
|
+
alreadyCompleted: completePayload.alreadyCompleted ?? false,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
uploads,
|
|
117
|
+
sourceAssetIds: uploads
|
|
118
|
+
.map((upload) => upload.assetId)
|
|
119
|
+
.filter((assetId) => Boolean(assetId)),
|
|
120
|
+
nextSuggestedTool: "create_product_reference_sheet",
|
|
121
|
+
};
|
|
122
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@genrupt/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Genrupt CLI for OAuth login, local file uploads, and local MCP bridge setup.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/Shackelfordian/Genrupt.git",
|
|
9
|
+
"directory": "packages/genrupt-cli"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"genrupt",
|
|
13
|
+
"mcp",
|
|
14
|
+
"claude-code",
|
|
15
|
+
"amazon",
|
|
16
|
+
"upload"
|
|
17
|
+
],
|
|
18
|
+
"type": "module",
|
|
19
|
+
"bin": {
|
|
20
|
+
"genrupt": "dist/cli.js"
|
|
21
|
+
},
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public",
|
|
24
|
+
"provenance": false
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist/",
|
|
28
|
+
"README.md"
|
|
29
|
+
],
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsc",
|
|
32
|
+
"pack:dry-run": "npm pack --dry-run",
|
|
33
|
+
"release:check": "npm run build && node ../../scripts/verify-genrupt-cli-release.cjs",
|
|
34
|
+
"prepublishOnly": "npm run build && node ../../scripts/verify-genrupt-cli-release.cjs"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@modelcontextprotocol/sdk": "^1.18.0",
|
|
38
|
+
"zod": "^3.25.76"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/node": "^20.8.7",
|
|
42
|
+
"typescript": "^5.2.2"
|
|
43
|
+
},
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=20"
|
|
46
|
+
}
|
|
47
|
+
}
|