@kite-copilot/cli 1.0.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 ADDED
@@ -0,0 +1,325 @@
1
+ # @kite/cli
2
+
3
+ Command-line tool for analyzing frontend codebases to discover API mappings. The CLI uses Claude AI to analyze your frontend code, identify routes, API calls, and generate mappings that connect your frontend to the Kite backend.
4
+
5
+ ## Prerequisites
6
+
7
+ ### Claude CLI
8
+
9
+ The Kite CLI requires Claude CLI to be installed for code analysis:
10
+
11
+ ```bash
12
+ npm install -g @anthropic-ai/claude-cli
13
+ ```
14
+
15
+ Verify installation:
16
+
17
+ ```bash
18
+ claude --version
19
+ ```
20
+
21
+ ### Node.js
22
+
23
+ Requires Node.js 18.0.0 or higher.
24
+
25
+ ## Installation
26
+
27
+ ### From npm (when published)
28
+
29
+ ```bash
30
+ npm install -g @kite/cli
31
+ ```
32
+
33
+ ### From source
34
+
35
+ ```bash
36
+ cd agent/cli-npm
37
+ npm install
38
+ npm run build
39
+ ```
40
+
41
+ Then run with:
42
+
43
+ ```bash
44
+ node dist/index.js <command>
45
+ ```
46
+
47
+ Or create a global link:
48
+
49
+ ```bash
50
+ npm link
51
+ kite <command>
52
+ ```
53
+
54
+ ## Commands
55
+
56
+ ### `kite login`
57
+
58
+ Authenticate with the Kite backend.
59
+
60
+ ```bash
61
+ kite login [--backend-url <url>]
62
+ ```
63
+
64
+ **Options:**
65
+
66
+ | Option | Description |
67
+ |--------|-------------|
68
+ | `--backend-url <url>` | Kite backend URL (default: https://api.kite.com) |
69
+
70
+ **Environment Variables:**
71
+
72
+ | Variable | Description |
73
+ |----------|-------------|
74
+ | `KITE_BACKEND_URL` | Override default backend URL |
75
+
76
+ **Example:**
77
+
78
+ ```bash
79
+ # Login to production
80
+ kite login
81
+
82
+ # Login to local development server
83
+ kite login --backend-url http://localhost:5002
84
+ ```
85
+
86
+ When prompted, enter your API token from the Kite dashboard.
87
+
88
+ ### `kite logout`
89
+
90
+ Clear stored credentials.
91
+
92
+ ```bash
93
+ kite logout
94
+ ```
95
+
96
+ ### `kite status`
97
+
98
+ Show current login status.
99
+
100
+ ```bash
101
+ kite status
102
+ ```
103
+
104
+ ### `kite map`
105
+
106
+ Analyze a frontend codebase to discover API mappings.
107
+
108
+ ```bash
109
+ kite map <path> [options]
110
+ ```
111
+
112
+ **Arguments:**
113
+
114
+ | Argument | Description |
115
+ |----------|-------------|
116
+ | `<path>` | Path to the frontend codebase directory |
117
+
118
+ **Options:**
119
+
120
+ | Option | Description |
121
+ |--------|-------------|
122
+ | `-o, --output <file>` | Output JSON file path (default: kite-mappings.json) |
123
+ | `-u, --upload` | Upload mappings to Kite backend after analysis |
124
+ | `-q, --quiet` | Suppress terminal visualization output |
125
+ | `-v, --verbose` | Show verbose debug output |
126
+ | `--timeout <seconds>` | Analysis timeout in seconds (default: 300) |
127
+ | `--no-output-file` | Don't generate output JSON file |
128
+
129
+ **Examples:**
130
+
131
+ ```bash
132
+ # Analyze and save to default file
133
+ kite map ./my-frontend-app
134
+
135
+ # Analyze with custom output file
136
+ kite map ./my-frontend-app -o custom-mappings.json
137
+
138
+ # Analyze and upload to backend
139
+ kite map ./my-frontend-app --upload
140
+
141
+ # Quiet mode - just generate the file
142
+ kite map ./my-frontend-app --quiet
143
+
144
+ # Verbose mode for debugging
145
+ kite map ./my-frontend-app --verbose
146
+
147
+ # Increase timeout for large codebases
148
+ kite map ./my-frontend-app --timeout 600
149
+ ```
150
+
151
+ ## Configuration
152
+
153
+ Credentials are stored in `~/.kite/config.json`:
154
+
155
+ ```json
156
+ {
157
+ "token": "your-api-token",
158
+ "org_id": "organization-uuid",
159
+ "api_config_id": "config-uuid",
160
+ "email": "your@email.com",
161
+ "org_name": "Your Organization",
162
+ "backend_url": "https://api.kite.com"
163
+ }
164
+ ```
165
+
166
+ ## Output Format
167
+
168
+ The `kite map` command generates a JSON file with the following structure:
169
+
170
+ ```json
171
+ {
172
+ "version": "1.0",
173
+ "generated_at": "2024-01-15T10:30:00Z",
174
+ "codebase_path": "/path/to/frontend",
175
+ "routes": [
176
+ {
177
+ "route_name": "customers",
178
+ "file_path": "pages/customers.tsx",
179
+ "component_name": "CustomersPage"
180
+ }
181
+ ],
182
+ "api_calls": [
183
+ {
184
+ "endpoint": "/api/customers",
185
+ "method": "GET",
186
+ "component_file": "pages/customers.tsx",
187
+ "action_form_type": null
188
+ }
189
+ ],
190
+ "mappings": [
191
+ {
192
+ "tool_name": "customers",
193
+ "api_endpoint": "/api/customers",
194
+ "navigation_page": "customers",
195
+ "action_form_type": "addCustomer",
196
+ "frontend_component": "CustomersPage",
197
+ "frontend_file_path": "pages/customers.tsx"
198
+ }
199
+ ]
200
+ }
201
+ ```
202
+
203
+ ## Workflow
204
+
205
+ ### First-time Setup
206
+
207
+ 1. Install the CLI and Claude CLI
208
+ 2. Get your API token from the Kite dashboard
209
+ 3. Login: `kite login`
210
+ 4. Verify: `kite status`
211
+
212
+ ### Analyzing a Codebase
213
+
214
+ 1. Navigate to your project or provide the path
215
+ 2. Run: `kite map ./your-frontend --upload`
216
+ 3. Review the generated mappings
217
+ 4. Mappings are now available in the Kite backend
218
+
219
+ ### Local Development
220
+
221
+ For local development without Supabase:
222
+
223
+ ```bash
224
+ # 1. Seed local test data
225
+ python agent/app/scripts/seed_local_dev.py
226
+
227
+ # 2. Start the backend
228
+ cd agent && python server.py
229
+
230
+ # 3. Login to local backend
231
+ kite login --backend-url http://localhost:5002
232
+
233
+ # 4. Map your codebase
234
+ kite map ./your-frontend --upload
235
+ ```
236
+
237
+ ## Troubleshooting
238
+
239
+ ### Claude CLI not found
240
+
241
+ ```
242
+ Error: Claude CLI not found
243
+ ```
244
+
245
+ **Solution:** Install Claude CLI:
246
+
247
+ ```bash
248
+ npm install -g @anthropic-ai/claude-cli
249
+ ```
250
+
251
+ ### Analysis timeout
252
+
253
+ ```
254
+ Error: Analysis timed out after 300 seconds
255
+ ```
256
+
257
+ **Solution:** Increase the timeout:
258
+
259
+ ```bash
260
+ kite map ./large-codebase --timeout 600
261
+ ```
262
+
263
+ ### Authentication failed
264
+
265
+ ```
266
+ Error: Invalid token
267
+ ```
268
+
269
+ **Solution:** Re-run login:
270
+
271
+ ```bash
272
+ kite logout
273
+ kite login
274
+ ```
275
+
276
+ ### Not logged in
277
+
278
+ ```
279
+ Error: Not logged in. Run 'kite login' first.
280
+ ```
281
+
282
+ **Solution:** Login before uploading:
283
+
284
+ ```bash
285
+ kite login
286
+ kite map ./frontend --upload
287
+ ```
288
+
289
+ ### Path is not a frontend project
290
+
291
+ ```
292
+ Error: Directory doesn't appear to be a frontend project
293
+ ```
294
+
295
+ **Solution:** Ensure your path contains one of:
296
+ - `package.json`
297
+ - `tsconfig.json`
298
+ - `src/` directory
299
+ - `app/` directory
300
+ - `components/` directory
301
+ - `pages/` directory
302
+
303
+ ## Development
304
+
305
+ ### Building
306
+
307
+ ```bash
308
+ npm run build
309
+ ```
310
+
311
+ ### Development mode
312
+
313
+ ```bash
314
+ npm run dev
315
+ ```
316
+
317
+ ### Running from source
318
+
319
+ ```bash
320
+ node dist/index.js map ./test-frontend
321
+ ```
322
+
323
+ ## License
324
+
325
+ MIT
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Claude Code Analyzer Module
3
+ *
4
+ * Invokes Claude Code CLI to analyze frontend codebases.
5
+ * Parses JSON responses into structured analysis results.
6
+ */
7
+ import type { FrontendAnalysisResult, FrontendRoute, FrontendApiCall, FrontendMapping, AnalyzeOptions } from "./types.js";
8
+ export declare class AnalyzerError extends Error {
9
+ constructor(message: string);
10
+ }
11
+ export declare class ClaudeNotFoundError extends AnalyzerError {
12
+ constructor(message: string);
13
+ }
14
+ export declare class ClaudeTimeoutError extends AnalyzerError {
15
+ constructor(message: string);
16
+ }
17
+ export declare class ClaudeParseError extends AnalyzerError {
18
+ constructor(message: string);
19
+ }
20
+ /**
21
+ * Validate that a path looks like a frontend project
22
+ */
23
+ export declare function validateCodebasePath(codebasePath: string): void;
24
+ /**
25
+ * Check if Claude CLI is available
26
+ */
27
+ export declare function checkClaudeAvailable(claudePath?: string): Promise<boolean>;
28
+ /**
29
+ * Parse Claude's response text into a structured object
30
+ */
31
+ export declare function parseClaudeResponse(responseText: string): {
32
+ routes: FrontendRoute[];
33
+ api_calls: FrontendApiCall[];
34
+ mappings: FrontendMapping[];
35
+ };
36
+ /**
37
+ * Build a complete analysis result
38
+ */
39
+ export declare function buildAnalysisResult(data: {
40
+ routes: FrontendRoute[];
41
+ api_calls: FrontendApiCall[];
42
+ mappings: FrontendMapping[];
43
+ }, codebasePath: string): FrontendAnalysisResult;
44
+ /**
45
+ * Analyze a frontend codebase using Claude Code
46
+ */
47
+ export declare function analyzeWithClaude(codebasePath: string, options?: AnalyzeOptions): Promise<FrontendAnalysisResult>;
48
+ /**
49
+ * Save analysis result to a JSON file
50
+ */
51
+ export declare function saveResultToFile(result: FrontendAnalysisResult, outputPath: string): void;
52
+ /**
53
+ * Upload mappings to the backend
54
+ */
55
+ export declare function uploadMappings(result: FrontendAnalysisResult, config: {
56
+ backendUrl: string;
57
+ token: string;
58
+ orgId: string;
59
+ apiConfigId: string;
60
+ }): Promise<{
61
+ success: boolean;
62
+ mappings_uploaded: number;
63
+ mappings_merged: number;
64
+ mappings_skipped: number;
65
+ message: string;
66
+ }>;
@@ -0,0 +1,333 @@
1
+ "use strict";
2
+ /**
3
+ * Claude Code Analyzer Module
4
+ *
5
+ * Invokes Claude Code CLI to analyze frontend codebases.
6
+ * Parses JSON responses into structured analysis results.
7
+ */
8
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
9
+ if (k2 === undefined) k2 = k;
10
+ var desc = Object.getOwnPropertyDescriptor(m, k);
11
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
12
+ desc = { enumerable: true, get: function() { return m[k]; } };
13
+ }
14
+ Object.defineProperty(o, k2, desc);
15
+ }) : (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ o[k2] = m[k];
18
+ }));
19
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
20
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
21
+ }) : function(o, v) {
22
+ o["default"] = v;
23
+ });
24
+ var __importStar = (this && this.__importStar) || (function () {
25
+ var ownKeys = function(o) {
26
+ ownKeys = Object.getOwnPropertyNames || function (o) {
27
+ var ar = [];
28
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
+ return ar;
30
+ };
31
+ return ownKeys(o);
32
+ };
33
+ return function (mod) {
34
+ if (mod && mod.__esModule) return mod;
35
+ var result = {};
36
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
+ __setModuleDefault(result, mod);
38
+ return result;
39
+ };
40
+ })();
41
+ Object.defineProperty(exports, "__esModule", { value: true });
42
+ exports.ClaudeParseError = exports.ClaudeTimeoutError = exports.ClaudeNotFoundError = exports.AnalyzerError = void 0;
43
+ exports.validateCodebasePath = validateCodebasePath;
44
+ exports.checkClaudeAvailable = checkClaudeAvailable;
45
+ exports.parseClaudeResponse = parseClaudeResponse;
46
+ exports.buildAnalysisResult = buildAnalysisResult;
47
+ exports.analyzeWithClaude = analyzeWithClaude;
48
+ exports.saveResultToFile = saveResultToFile;
49
+ exports.uploadMappings = uploadMappings;
50
+ const node_child_process_1 = require("node:child_process");
51
+ const fs = __importStar(require("node:fs"));
52
+ const path = __importStar(require("node:path"));
53
+ // Analysis prompt for Claude Code (same as Python version)
54
+ const ANALYSIS_PROMPT = `Analyze this frontend codebase and extract:
55
+
56
+ 1. All page routes and their component files
57
+ - Look for route definitions (React Router, Next.js pages, etc.)
58
+ - Map route names to their component files
59
+
60
+ 2. All API calls (fetch, axios, etc.) with endpoints and HTTP methods
61
+ - Find all HTTP requests to backend APIs
62
+ - Identify the HTTP method (GET, POST, PUT, DELETE, etc.)
63
+ - Note which component file makes each call
64
+
65
+ 3. Form action types (event names like "addCustomer", "updateSettings")
66
+ - Find custom event dispatches or action handlers
67
+ - Look for form submission handlers and their action types
68
+
69
+ 4. Map API endpoints to their likely tool names
70
+ - Extract the resource name from the endpoint (e.g., /api/customers -> customers)
71
+ - Associate with navigation pages and form types
72
+
73
+ Return a JSON object with this EXACT structure (no markdown, no explanation, just JSON):
74
+ {
75
+ "routes": [
76
+ {"route_name": "string", "file_path": "string", "component_name": "string"}
77
+ ],
78
+ "api_calls": [
79
+ {"endpoint": "string", "method": "string", "component_file": "string", "action_form_type": "string or null"}
80
+ ],
81
+ "mappings": [
82
+ {
83
+ "tool_name": "string",
84
+ "api_endpoint": "string",
85
+ "navigation_page": "string or null",
86
+ "action_form_type": "string or null",
87
+ "frontend_component": "string or null",
88
+ "frontend_file_path": "string or null"
89
+ }
90
+ ]
91
+ }
92
+
93
+ Important:
94
+ - tool_name should be the resource name extracted from the API endpoint (e.g., "/api/customers" -> "customers")
95
+ - navigation_page should match a route_name if the API is used on that page
96
+ - action_form_type should match form handlers or custom events
97
+ - Return ONLY valid JSON, no markdown code blocks or explanations`;
98
+ class AnalyzerError extends Error {
99
+ constructor(message) {
100
+ super(message);
101
+ this.name = "AnalyzerError";
102
+ }
103
+ }
104
+ exports.AnalyzerError = AnalyzerError;
105
+ class ClaudeNotFoundError extends AnalyzerError {
106
+ constructor(message) {
107
+ super(message);
108
+ this.name = "ClaudeNotFoundError";
109
+ }
110
+ }
111
+ exports.ClaudeNotFoundError = ClaudeNotFoundError;
112
+ class ClaudeTimeoutError extends AnalyzerError {
113
+ constructor(message) {
114
+ super(message);
115
+ this.name = "ClaudeTimeoutError";
116
+ }
117
+ }
118
+ exports.ClaudeTimeoutError = ClaudeTimeoutError;
119
+ class ClaudeParseError extends AnalyzerError {
120
+ constructor(message) {
121
+ super(message);
122
+ this.name = "ClaudeParseError";
123
+ }
124
+ }
125
+ exports.ClaudeParseError = ClaudeParseError;
126
+ /**
127
+ * Validate that a path looks like a frontend project
128
+ */
129
+ function validateCodebasePath(codebasePath) {
130
+ if (!fs.existsSync(codebasePath)) {
131
+ throw new AnalyzerError(`Codebase path does not exist: ${codebasePath}`);
132
+ }
133
+ const stat = fs.statSync(codebasePath);
134
+ if (!stat.isDirectory()) {
135
+ throw new AnalyzerError(`Codebase path is not a directory: ${codebasePath}`);
136
+ }
137
+ // Check for common frontend project indicators
138
+ const indicators = [
139
+ "package.json",
140
+ "tsconfig.json",
141
+ "src",
142
+ "app",
143
+ "components",
144
+ "pages",
145
+ ];
146
+ const foundIndicators = indicators.filter((ind) => fs.existsSync(path.join(codebasePath, ind)));
147
+ if (foundIndicators.length === 0) {
148
+ throw new AnalyzerError(`Directory doesn't appear to be a frontend project. Expected one of: ${indicators.join(", ")}`);
149
+ }
150
+ }
151
+ /**
152
+ * Check if Claude CLI is available
153
+ */
154
+ async function checkClaudeAvailable(claudePath = "claude") {
155
+ return new Promise((resolve) => {
156
+ const proc = (0, node_child_process_1.spawn)(claudePath, ["--version"], { stdio: "pipe" });
157
+ proc.on("close", (code) => {
158
+ resolve(code === 0);
159
+ });
160
+ proc.on("error", () => {
161
+ resolve(false);
162
+ });
163
+ // Timeout after 5 seconds
164
+ setTimeout(() => {
165
+ proc.kill();
166
+ resolve(false);
167
+ }, 5000);
168
+ });
169
+ }
170
+ /**
171
+ * Parse Claude's response text into a structured object
172
+ */
173
+ function parseClaudeResponse(responseText) {
174
+ if (!responseText || !responseText.trim()) {
175
+ throw new ClaudeParseError("Empty response from Claude");
176
+ }
177
+ let text = responseText.trim();
178
+ // Remove markdown code blocks if present
179
+ if (text.startsWith("```")) {
180
+ const lines = text.split("\n");
181
+ // Remove first line (```json or ```)
182
+ const contentLines = lines.slice(1);
183
+ // Find and remove closing ```
184
+ const endIndex = contentLines.findIndex((line) => line.trim() === "```");
185
+ if (endIndex !== -1) {
186
+ text = contentLines.slice(0, endIndex).join("\n");
187
+ }
188
+ else {
189
+ text = contentLines.join("\n");
190
+ }
191
+ }
192
+ // Try to find JSON object in the text
193
+ const jsonMatch = text.match(/\{[\s\S]*\}/);
194
+ if (jsonMatch) {
195
+ text = jsonMatch[0];
196
+ }
197
+ try {
198
+ const data = JSON.parse(text);
199
+ return {
200
+ routes: data.routes || [],
201
+ api_calls: data.api_calls || [],
202
+ mappings: data.mappings || [],
203
+ };
204
+ }
205
+ catch (e) {
206
+ throw new ClaudeParseError(`Failed to parse JSON response: ${e}\nResponse: ${text.substring(0, 500)}`);
207
+ }
208
+ }
209
+ /**
210
+ * Build a complete analysis result
211
+ */
212
+ function buildAnalysisResult(data, codebasePath) {
213
+ return {
214
+ version: "1.0",
215
+ generated_at: new Date().toISOString(),
216
+ codebase_path: codebasePath,
217
+ routes: data.routes,
218
+ api_calls: data.api_calls,
219
+ mappings: data.mappings,
220
+ };
221
+ }
222
+ /**
223
+ * Analyze a frontend codebase using Claude Code
224
+ */
225
+ async function analyzeWithClaude(codebasePath, options = {}) {
226
+ const { timeout = 300, claudePath = "claude" } = options;
227
+ // Validate path
228
+ validateCodebasePath(codebasePath);
229
+ // Get absolute path
230
+ const absolutePath = path.resolve(codebasePath);
231
+ // Check Claude is available
232
+ const claudeAvailable = await checkClaudeAvailable(claudePath);
233
+ if (!claudeAvailable) {
234
+ throw new ClaudeNotFoundError("Claude CLI not found. Install it with: npm install -g @anthropic-ai/claude-cli");
235
+ }
236
+ return new Promise((resolve, reject) => {
237
+ const args = ["--print", "-p", ANALYSIS_PROMPT];
238
+ const proc = (0, node_child_process_1.spawn)(claudePath, args, {
239
+ cwd: absolutePath,
240
+ stdio: ["ignore", "pipe", "pipe"],
241
+ });
242
+ let stdout = "";
243
+ let stderr = "";
244
+ proc.stdout.on("data", (data) => {
245
+ stdout += data.toString();
246
+ });
247
+ proc.stderr.on("data", (data) => {
248
+ stderr += data.toString();
249
+ });
250
+ // Set timeout
251
+ const timeoutId = setTimeout(() => {
252
+ proc.kill();
253
+ reject(new ClaudeTimeoutError(`Claude analysis timed out after ${timeout} seconds`));
254
+ }, timeout * 1000);
255
+ proc.on("close", (code) => {
256
+ clearTimeout(timeoutId);
257
+ if (code !== 0) {
258
+ reject(new AnalyzerError(`Claude exited with code ${code}: ${stderr}`));
259
+ return;
260
+ }
261
+ try {
262
+ const data = parseClaudeResponse(stdout);
263
+ const result = buildAnalysisResult(data, absolutePath);
264
+ resolve(result);
265
+ }
266
+ catch (e) {
267
+ reject(e);
268
+ }
269
+ });
270
+ proc.on("error", (err) => {
271
+ clearTimeout(timeoutId);
272
+ reject(new ClaudeNotFoundError(`Failed to spawn Claude: ${err.message}`));
273
+ });
274
+ });
275
+ }
276
+ /**
277
+ * Save analysis result to a JSON file
278
+ */
279
+ function saveResultToFile(result, outputPath) {
280
+ const json = JSON.stringify(result, null, 2);
281
+ fs.writeFileSync(outputPath, json, "utf-8");
282
+ }
283
+ /**
284
+ * Upload mappings to the backend
285
+ */
286
+ async function uploadMappings(result, config) {
287
+ const url = `${config.backendUrl}/api/mappings/frontend?org_id=${config.orgId}&api_config_id=${config.apiConfigId}`;
288
+ // Filter out mappings with null/empty api_endpoint (invalid data from Claude)
289
+ const validMappings = result.mappings.filter((m) => m.api_endpoint && m.tool_name);
290
+ const payload = {
291
+ mappings: validMappings.map((m) => ({
292
+ tool_name: m.tool_name,
293
+ api_endpoint: m.api_endpoint,
294
+ navigation_page: m.navigation_page,
295
+ action_form_type: m.action_form_type,
296
+ frontend_component: m.frontend_component,
297
+ frontend_file_path: m.frontend_file_path,
298
+ })),
299
+ metadata: {
300
+ codebase_path: result.codebase_path,
301
+ analyzed_at: result.generated_at,
302
+ version: result.version,
303
+ routes_count: result.routes.length,
304
+ api_calls_count: result.api_calls.length,
305
+ },
306
+ };
307
+ const response = await fetch(url, {
308
+ method: "POST",
309
+ headers: {
310
+ "Content-Type": "application/json",
311
+ Authorization: `Bearer ${config.token}`,
312
+ },
313
+ body: JSON.stringify(payload),
314
+ });
315
+ if (response.status === 401) {
316
+ throw new Error("Authentication failed. Please run 'kite login' again.");
317
+ }
318
+ if (response.status === 403) {
319
+ throw new Error("Access denied for this organization.");
320
+ }
321
+ if (!response.ok) {
322
+ const text = await response.text();
323
+ throw new Error(`Upload failed: ${response.status} ${text}`);
324
+ }
325
+ const data = await response.json();
326
+ return {
327
+ success: true,
328
+ mappings_uploaded: data.mappings_uploaded || validMappings.length,
329
+ mappings_merged: data.mappings_merged || 0,
330
+ mappings_skipped: data.mappings_skipped || (result.mappings.length - validMappings.length),
331
+ message: data.message || "Mappings uploaded successfully",
332
+ };
333
+ }