@context-engine-bridge/context-engine-mcp-bridge 0.0.28 → 0.0.29
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/.omc/state/hud-state.json +6 -0
- package/AGENTS.md +69 -0
- package/bin/AGENTS.md +34 -0
- package/docs/AGENTS.md +22 -0
- package/package.json +1 -1
- package/src/AGENTS.md +59 -0
- package/src/connectCli.js +42 -13
- package/src/mcpServer.js +7 -1
- package/src/uploader.js +65 -27
- package/.claude/settings.local.json +0 -11
package/AGENTS.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
<!-- Parent: ../AGENTS.md -->
|
|
2
|
+
<!-- Generated: 2026-02-19 | Updated: 2026-02-19 -->
|
|
3
|
+
|
|
4
|
+
# ctx-mcp-bridge
|
|
5
|
+
|
|
6
|
+
## Purpose
|
|
7
|
+
|
|
8
|
+
The MCP bridge (`ctxce` CLI) is a Model Context Protocol server that aggregates the Context Engine indexer and memory servers into a single unified MCP server. It supports both stdio and HTTP transport modes, making it compatible with MCP clients like Claude Code, Windsurf, Augment, and others. The bridge is primarily launched by the VS Code extension but can run standalone.
|
|
9
|
+
|
|
10
|
+
## Key Files
|
|
11
|
+
|
|
12
|
+
| File | Description |
|
|
13
|
+
|------|-------------|
|
|
14
|
+
| `bin/ctxce.js` | CLI entry point and executable (chmod +x 755) |
|
|
15
|
+
| `src/cli.js` | Command routing and argument parsing for `mcp-serve`, `mcp-http-serve`, `auth`, `connect` |
|
|
16
|
+
| `src/mcpServer.js` | MCP server implementation with stdio/HTTP transport, tool deduping, and auth handling |
|
|
17
|
+
| `src/authCli.js` | Auth command handlers: `login`, `logout`, `status` with token and password flows |
|
|
18
|
+
| `src/authConfig.js` | Session storage and management in `~/.ctxce/auth.json` |
|
|
19
|
+
| `src/oauthHandler.js` | OAuth protocol support for remote deployments |
|
|
20
|
+
| `src/uploader.js` | Standalone code uploader integration |
|
|
21
|
+
| `src/connectCli.js` | Connection validation and setup helpers |
|
|
22
|
+
| `src/resultPathMapping.js` | Path remapping for tool results (container/host paths) |
|
|
23
|
+
| `package.json` | Node.js package manifest (requires Node >= 18) |
|
|
24
|
+
|
|
25
|
+
## Subdirectories
|
|
26
|
+
|
|
27
|
+
| Directory | Purpose |
|
|
28
|
+
|-----------|---------|
|
|
29
|
+
| `bin/` | Executable CLI entry point |
|
|
30
|
+
| `docs/` | Debugging guides and documentation |
|
|
31
|
+
| `src/` | Core MCP server and auth logic (see `src/AGENTS.md`) |
|
|
32
|
+
|
|
33
|
+
## For AI Agents
|
|
34
|
+
|
|
35
|
+
### Working In This Directory
|
|
36
|
+
|
|
37
|
+
This is a Node.js MCP bridge package. Changes to MCP routing, tool forwarding, or auth handling require updates to `src/cli.js` and `src/mcpServer.js`. The bridge proxies requests between MCP clients and remote indexer/memory HTTP servers, so test both stdio and HTTP modes.
|
|
38
|
+
|
|
39
|
+
### Testing Requirements
|
|
40
|
+
|
|
41
|
+
- Run with `npm start` or `node bin/ctxce.js --help`
|
|
42
|
+
- Test MCP stdio mode: `ctxce mcp-serve --workspace /tmp/test`
|
|
43
|
+
- Test MCP HTTP mode: `ctxce mcp-http-serve --workspace /tmp/test --port 30810`
|
|
44
|
+
- Test auth commands: `ctxce auth login --backend-url http://localhost:8004 --token TEST_TOKEN`
|
|
45
|
+
- Verify auth state: `ctxce auth status --backend-url http://localhost:8004 --json`
|
|
46
|
+
- E2E tests: `npm run test:e2e`
|
|
47
|
+
|
|
48
|
+
### Common Patterns
|
|
49
|
+
|
|
50
|
+
- Environment variables: `CTXCE_INDEXER_URL`, `CTXCE_MEMORY_URL`, `CTXCE_HTTP_PORT`, `CTXCE_AUTH_*`
|
|
51
|
+
- Auth sessions stored in `~/.ctxce/auth.json` keyed by backend URL
|
|
52
|
+
- MCP tools are deduplicated and forwarded from indexer and memory servers
|
|
53
|
+
- Path remapping (host paths <-> container paths) handled transparently
|
|
54
|
+
- All MCP requests are logged to stderr or `CTXCE_DEBUG_LOG` if set
|
|
55
|
+
|
|
56
|
+
## Dependencies
|
|
57
|
+
|
|
58
|
+
### Internal
|
|
59
|
+
- Context Engine indexer (HTTP endpoint at `CTXCE_INDEXER_URL` or `http://localhost:8003/mcp`)
|
|
60
|
+
- Context Engine memory server (HTTP endpoint at `CTXCE_MEMORY_URL` or `http://localhost:8002/mcp`)
|
|
61
|
+
- Auth backend (optional, at `CTXCE_AUTH_BACKEND_URL` or `http://localhost:8004`)
|
|
62
|
+
|
|
63
|
+
### External
|
|
64
|
+
- `@modelcontextprotocol/sdk` (^1.24.3) – MCP protocol implementation
|
|
65
|
+
- `zod` (^3.25.0) – Runtime type validation
|
|
66
|
+
- `tar` (^7.5.9) – Archive support for uploads
|
|
67
|
+
- `ignore` (^7.0.5) – .gitignore-style file filtering
|
|
68
|
+
|
|
69
|
+
<!-- MANUAL: Any manually added notes below this line are preserved on regeneration -->
|
package/bin/AGENTS.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<!-- Parent: ../AGENTS.md -->
|
|
2
|
+
<!-- Generated: 2026-02-19 | Updated: 2026-02-19 -->
|
|
3
|
+
|
|
4
|
+
# bin
|
|
5
|
+
|
|
6
|
+
## Purpose
|
|
7
|
+
|
|
8
|
+
Executable CLI entry point for the `ctxce` command (also aliased as `ctxce-bridge`). This directory contains the shebang-wrapped Node.js script that is installed globally or via npm when the package is installed.
|
|
9
|
+
|
|
10
|
+
## Key Files
|
|
11
|
+
|
|
12
|
+
| File | Description |
|
|
13
|
+
|------|-------------|
|
|
14
|
+
| `ctxce.js` | CLI executable entry point (Node.js script, chmod +x 755) |
|
|
15
|
+
|
|
16
|
+
## For AI Agents
|
|
17
|
+
|
|
18
|
+
### Working In This Directory
|
|
19
|
+
|
|
20
|
+
Do not modify `ctxce.js` directly unless changing the CLI bootstrap. The actual CLI logic is in `src/cli.js`. The executable must have a shebang (`#!/usr/bin/env node`) and be marked executable on Unix systems.
|
|
21
|
+
|
|
22
|
+
### Testing Requirements
|
|
23
|
+
|
|
24
|
+
- Verify executable permission: `ls -l bin/ctxce.js` should show `-rwxr-xr-x`
|
|
25
|
+
- Test global install: `npm install -g` and run `ctxce --help`
|
|
26
|
+
- Test npx mode: `npx @context-engine-bridge/context-engine-mcp-bridge ctxce --help`
|
|
27
|
+
- Postinstall script auto-fixes permissions on non-Windows systems
|
|
28
|
+
|
|
29
|
+
## Dependencies
|
|
30
|
+
|
|
31
|
+
### Internal
|
|
32
|
+
- `src/cli.js` – Main CLI router and handler
|
|
33
|
+
|
|
34
|
+
<!-- MANUAL: Any manually added notes below this line are preserved on regeneration -->
|
package/docs/AGENTS.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<!-- Parent: ../AGENTS.md -->
|
|
2
|
+
<!-- Generated: 2026-02-19 | Updated: 2026-02-19 -->
|
|
3
|
+
|
|
4
|
+
# docs
|
|
5
|
+
|
|
6
|
+
## Purpose
|
|
7
|
+
|
|
8
|
+
Debugging guides and developer documentation for the MCP bridge. Currently contains minimal documentation; most usage is covered in the main `README.md`.
|
|
9
|
+
|
|
10
|
+
## Key Files
|
|
11
|
+
|
|
12
|
+
| File | Description |
|
|
13
|
+
|------|-------------|
|
|
14
|
+
| `debugging.md` | Debug logging and troubleshooting tips |
|
|
15
|
+
|
|
16
|
+
## For AI Agents
|
|
17
|
+
|
|
18
|
+
### Working In This Directory
|
|
19
|
+
|
|
20
|
+
This directory is for developer guides, not API documentation (which belongs in the main README). When adding debugging tips or advanced usage patterns, place them here.
|
|
21
|
+
|
|
22
|
+
<!-- MANUAL: Any manually added notes below this line are preserved on regeneration -->
|
package/package.json
CHANGED
package/src/AGENTS.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<!-- Parent: ../AGENTS.md -->
|
|
2
|
+
<!-- Generated: 2026-02-19 | Updated: 2026-02-19 -->
|
|
3
|
+
|
|
4
|
+
# src
|
|
5
|
+
|
|
6
|
+
## Purpose
|
|
7
|
+
|
|
8
|
+
Core MCP server implementation, auth handling, and CLI routing for the `ctxce` bridge. This module aggregates the Context Engine indexer and memory servers, manages authentication sessions, and handles both stdio and HTTP transport modes.
|
|
9
|
+
|
|
10
|
+
## Key Files
|
|
11
|
+
|
|
12
|
+
| File | Description |
|
|
13
|
+
|------|-------------|
|
|
14
|
+
| `cli.js` | Main command router for `mcp-serve`, `mcp-http-serve`, `auth`, `connect` subcommands; parses CLI flags and environment variables |
|
|
15
|
+
| `mcpServer.js` | MCP server implementation; proxies requests to indexer/memory servers, dedupes tools, handles auth, supports stdio and HTTP transports |
|
|
16
|
+
| `authCli.js` | Auth command handlers for `login`, `logout`, `status` with token and password flows; manages session lifecycle |
|
|
17
|
+
| `authConfig.js` | Persistent auth state in `~/.ctxce/auth.json`; session loading, saving, TTL handling, OAuth support |
|
|
18
|
+
| `oauthHandler.js` | OAuth protocol implementation for token refresh and remote auth flows |
|
|
19
|
+
| `uploader.js` | Integration with the standalone code uploader (tar archive support, progress tracking) |
|
|
20
|
+
| `connectCli.js` | Connection validation, workspace discovery, and setup helpers |
|
|
21
|
+
| `resultPathMapping.js` | Path remapping for tool results (host paths <-> container paths); handles path translation for Docker environments |
|
|
22
|
+
|
|
23
|
+
## For AI Agents
|
|
24
|
+
|
|
25
|
+
### Working In This Directory
|
|
26
|
+
|
|
27
|
+
This is the core business logic. Changes to MCP command handling, tool routing, or auth flows affect both the CLI and the VS Code extension. The server proxies all tool requests to remote indexer/memory servers and dedupes tool lists to prevent duplicates. Auth is optional but handles session TTL, token refresh, and fallback to dev tokens.
|
|
28
|
+
|
|
29
|
+
### Testing Requirements
|
|
30
|
+
|
|
31
|
+
- Test MCP stdio transport: `node src/cli.js mcp-serve --workspace /tmp/test`
|
|
32
|
+
- Test MCP HTTP transport: `node src/cli.js mcp-http-serve --workspace /tmp/test --port 30810`
|
|
33
|
+
- Test auth login: `node src/cli.js auth login --backend-url http://localhost:8004 --token TOKEN`
|
|
34
|
+
- Test auth status: `node src/cli.js auth status --backend-url http://localhost:8004 --json`
|
|
35
|
+
- Verify tool deduping: Check that tools from indexer and memory are merged without duplicates
|
|
36
|
+
- Test path remapping: Verify that container paths are correctly mapped for Docker environments
|
|
37
|
+
- Run E2E tests: `npm run test:e2e`, `npm run test:e2e:auth`, `npm run test:e2e:happy`, `npm run test:e2e:edge`
|
|
38
|
+
|
|
39
|
+
### Common Patterns
|
|
40
|
+
|
|
41
|
+
- Environment variables guide server initialization: `CTXCE_INDEXER_URL`, `CTXCE_MEMORY_URL`, `CTXCE_HTTP_PORT`, `CTXCE_AUTH_BACKEND_URL`, `CTXCE_AUTH_ENABLED`, `CTXCE_DEBUG_LOG`
|
|
42
|
+
- Sessions are stored per backend URL in `~/.ctxce/auth.json` with TTL tracking
|
|
43
|
+
- All MCP tools from indexer and memory are merged and deduplicated before listing
|
|
44
|
+
- Path remapping is applied to tool arguments and results for Docker host/container path translation
|
|
45
|
+
- Debug logging goes to stderr and optionally to a file (set `CTXCE_DEBUG_LOG` env var)
|
|
46
|
+
- HTTP transport uses Node.js `createServer` with MCP's `StreamableHTTPServerTransport`
|
|
47
|
+
|
|
48
|
+
## Dependencies
|
|
49
|
+
|
|
50
|
+
### Internal
|
|
51
|
+
- `bin/ctxce.js` – CLI entry point that imports and runs these modules
|
|
52
|
+
|
|
53
|
+
### External
|
|
54
|
+
- `@modelcontextprotocol/sdk` – MCP protocol (Server, StdioServerTransport, StreamableHTTPServerTransport, Client, StreamableHTTPClientTransport)
|
|
55
|
+
- `zod` – Runtime type validation (used in config parsing)
|
|
56
|
+
- `tar` – Archive handling for uploader
|
|
57
|
+
- `ignore` – File filtering for .gitignore patterns
|
|
58
|
+
|
|
59
|
+
<!-- MANUAL: Any manually added notes below this line are preserved on regeneration -->
|
package/src/connectCli.js
CHANGED
|
@@ -2,7 +2,7 @@ import process from "node:process";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import { saveAuthEntry } from "./authConfig.js";
|
|
5
|
-
import { indexWorkspace } from "./uploader.js";
|
|
5
|
+
import { indexWorkspace, loadGitignore, isCodeFile } from "./uploader.js";
|
|
6
6
|
|
|
7
7
|
const SAAS_ENDPOINTS = {
|
|
8
8
|
uploadEndpoint: "https://dev.context-engine.ai/upload",
|
|
@@ -160,12 +160,35 @@ async function triggerIndexing(workspace, sessionId, authEntry) {
|
|
|
160
160
|
return result.success;
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
-
function startWatcher(workspace,
|
|
163
|
+
function startWatcher(workspace, initialSessionId, authEntry, intervalMs) {
|
|
164
164
|
console.error(`[ctxce] Starting file watcher (sync every ${intervalMs / 1000}s)...`);
|
|
165
165
|
console.error("[ctxce] Press Ctrl+C to stop.");
|
|
166
166
|
|
|
167
167
|
let isRunning = false;
|
|
168
168
|
let pendingSync = false;
|
|
169
|
+
let sessionId = initialSessionId;
|
|
170
|
+
|
|
171
|
+
async function refreshSessionIfNeeded() {
|
|
172
|
+
// If the auth entry has an expiry and we're within 5 minutes of it,
|
|
173
|
+
// re-authenticate using the stored API key.
|
|
174
|
+
if (!authEntry || !authEntry.apiKey) return;
|
|
175
|
+
const expiresAt = authEntry.expiresAt;
|
|
176
|
+
if (typeof expiresAt !== "number" || !Number.isFinite(expiresAt) || expiresAt <= 0) return;
|
|
177
|
+
const nowSecs = Math.floor(Date.now() / 1000);
|
|
178
|
+
const remainingSecs = expiresAt - nowSecs;
|
|
179
|
+
if (remainingSecs > 300) return; // still valid for > 5 min
|
|
180
|
+
console.error("[ctxce] Session approaching expiry, refreshing...");
|
|
181
|
+
try {
|
|
182
|
+
const refreshed = await authenticateWithApiKey(authEntry.apiKey);
|
|
183
|
+
if (refreshed && refreshed.sessionId) {
|
|
184
|
+
sessionId = refreshed.sessionId;
|
|
185
|
+
authEntry = refreshed;
|
|
186
|
+
console.error("[ctxce] Session refreshed successfully.");
|
|
187
|
+
}
|
|
188
|
+
} catch (err) {
|
|
189
|
+
console.error(`[ctxce] Session refresh failed: ${err}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
169
192
|
|
|
170
193
|
const fileHashes = new Map();
|
|
171
194
|
|
|
@@ -178,26 +201,29 @@ function startWatcher(workspace, sessionId, authEntry, intervalMs) {
|
|
|
178
201
|
}
|
|
179
202
|
}
|
|
180
203
|
|
|
204
|
+
// Use gitignore from uploader.js so the watcher ignores the same files as
|
|
205
|
+
// the bundle creator -- prevents redundant uploads for generated/ignored files.
|
|
206
|
+
const _ig = loadGitignore(workspace);
|
|
207
|
+
|
|
181
208
|
function scanDirectory(dir, files = []) {
|
|
182
209
|
try {
|
|
183
210
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
184
211
|
for (const entry of entries) {
|
|
212
|
+
if (entry.isSymbolicLink()) continue;
|
|
185
213
|
const fullPath = path.join(dir, entry.name);
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
entry.name === "node_modules" ||
|
|
189
|
-
entry.name === "__pycache__" ||
|
|
190
|
-
entry.name === "venv" ||
|
|
191
|
-
entry.name === ".venv" ||
|
|
192
|
-
entry.name === "dist" ||
|
|
193
|
-
entry.name === "build") {
|
|
194
|
-
continue;
|
|
195
|
-
}
|
|
214
|
+
// Normalize to forward slashes for the `ignore` library (expects POSIX paths)
|
|
215
|
+
const relPath = path.relative(workspace, fullPath).split(path.sep).join('/');
|
|
196
216
|
|
|
217
|
+
// Use the same ignore rules as the bundle creator
|
|
218
|
+
// Directories need a trailing slash for gitignore pattern matching
|
|
197
219
|
if (entry.isDirectory()) {
|
|
220
|
+
if (_ig.ignores(relPath + '/')) continue;
|
|
198
221
|
scanDirectory(fullPath, files);
|
|
199
222
|
} else if (entry.isFile()) {
|
|
200
|
-
|
|
223
|
+
if (_ig.ignores(relPath)) continue;
|
|
224
|
+
if (isCodeFile(fullPath)) {
|
|
225
|
+
files.push(fullPath);
|
|
226
|
+
}
|
|
201
227
|
}
|
|
202
228
|
}
|
|
203
229
|
} catch {
|
|
@@ -242,6 +268,9 @@ function startWatcher(workspace, sessionId, authEntry, intervalMs) {
|
|
|
242
268
|
console.error(`[ctxce] [${now}] Syncing changes...`);
|
|
243
269
|
|
|
244
270
|
try {
|
|
271
|
+
// Refresh session before upload if approaching expiry
|
|
272
|
+
await refreshSessionIfNeeded();
|
|
273
|
+
|
|
245
274
|
const result = await indexWorkspace(
|
|
246
275
|
workspace,
|
|
247
276
|
SAAS_ENDPOINTS.uploadEndpoint,
|
package/src/mcpServer.js
CHANGED
|
@@ -501,7 +501,13 @@ async function createBridgeServer(options) {
|
|
|
501
501
|
expiresAt > 0 &&
|
|
502
502
|
expiresAt < Math.floor(Date.now() / 1000)
|
|
503
503
|
) {
|
|
504
|
-
|
|
504
|
+
// Allow 5-minute grace period for clock skew; beyond that, reject
|
|
505
|
+
const expiredSecs = Math.floor(Date.now() / 1000) - expiresAt;
|
|
506
|
+
if (expiredSecs > 300) {
|
|
507
|
+
debugLog(`[ctxce] Session expired ${expiredSecs}s ago (beyond 5min grace), triggering re-auth.`);
|
|
508
|
+
return "";
|
|
509
|
+
}
|
|
510
|
+
debugLog("[ctxce] Session expired but within 5min grace period; using it (server will validate).");
|
|
505
511
|
return entry.sessionId;
|
|
506
512
|
}
|
|
507
513
|
return entry.sessionId;
|
package/src/uploader.js
CHANGED
|
@@ -36,7 +36,7 @@ const DEFAULT_IGNORES = [
|
|
|
36
36
|
|
|
37
37
|
const MAX_FILE_SIZE = 10 * 1024 * 1024;
|
|
38
38
|
|
|
39
|
-
function loadGitignore(workspacePath) {
|
|
39
|
+
export function loadGitignore(workspacePath) {
|
|
40
40
|
const ig = ignore();
|
|
41
41
|
ig.add(DEFAULT_IGNORES);
|
|
42
42
|
|
|
@@ -52,7 +52,7 @@ function loadGitignore(workspacePath) {
|
|
|
52
52
|
return ig;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
function isCodeFile(filePath) {
|
|
55
|
+
export function isCodeFile(filePath) {
|
|
56
56
|
const ext = path.extname(filePath).toLowerCase();
|
|
57
57
|
if (CODE_EXTS.has(ext)) return true;
|
|
58
58
|
|
|
@@ -224,19 +224,10 @@ export async function createBundle(workspacePath, options = {}) {
|
|
|
224
224
|
export async function uploadBundle(bundlePath, manifest, uploadEndpoint, sessionId, options = {}) {
|
|
225
225
|
const { log = console.error, orgId, orgSlug } = options;
|
|
226
226
|
|
|
227
|
-
const
|
|
227
|
+
const bundleSize = fs.statSync(bundlePath).size;
|
|
228
228
|
const boundary = `----ctxce${Date.now()}${Math.random().toString(36).slice(2)}`;
|
|
229
229
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
parts.push(
|
|
233
|
-
`--${boundary}\r\n`,
|
|
234
|
-
`Content-Disposition: form-data; name="bundle"; filename="bundle.tar.gz"\r\n`,
|
|
235
|
-
`Content-Type: application/gzip\r\n\r\n`,
|
|
236
|
-
);
|
|
237
|
-
parts.push(bundleData);
|
|
238
|
-
parts.push(`\r\n`);
|
|
239
|
-
|
|
230
|
+
// Build form fields (small metadata -- kept in memory)
|
|
240
231
|
const logicalRepoId = computeLogicalRepoId(manifest.workspace_path);
|
|
241
232
|
const fields = {
|
|
242
233
|
workspace_path: manifest.workspace_path,
|
|
@@ -246,35 +237,67 @@ export async function uploadBundle(bundlePath, manifest, uploadEndpoint, session
|
|
|
246
237
|
logical_repo_id: logicalRepoId,
|
|
247
238
|
session: sessionId,
|
|
248
239
|
};
|
|
249
|
-
|
|
250
240
|
if (orgId) fields.org_id = orgId;
|
|
251
241
|
if (orgSlug) fields.org_slug = orgSlug;
|
|
252
242
|
|
|
243
|
+
// Build multipart preamble (file header) and epilogue (fields + close)
|
|
244
|
+
const filePreamble = Buffer.from(
|
|
245
|
+
`--${boundary}\r\n` +
|
|
246
|
+
`Content-Disposition: form-data; name="bundle"; filename="bundle.tar.gz"\r\n` +
|
|
247
|
+
`Content-Type: application/gzip\r\n\r\n`
|
|
248
|
+
);
|
|
249
|
+
const fileEpilogue = Buffer.from(`\r\n`);
|
|
250
|
+
|
|
251
|
+
let fieldsPart = '';
|
|
253
252
|
for (const [key, value] of Object.entries(fields)) {
|
|
254
253
|
if (value) {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
`${value}\r\n`,
|
|
259
|
-
);
|
|
254
|
+
fieldsPart += `--${boundary}\r\n` +
|
|
255
|
+
`Content-Disposition: form-data; name="${key}"\r\n\r\n` +
|
|
256
|
+
`${value}\r\n`;
|
|
260
257
|
}
|
|
261
258
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
259
|
+
fieldsPart += `--${boundary}--\r\n`;
|
|
260
|
+
const fieldsBuffer = Buffer.from(fieldsPart);
|
|
261
|
+
|
|
262
|
+
// Stream the bundle file instead of loading it entirely into memory.
|
|
263
|
+
// This prevents OOM for large repositories (hundreds of MB bundles).
|
|
264
|
+
const totalLength = filePreamble.length + bundleSize + fileEpilogue.length + fieldsBuffer.length;
|
|
265
|
+
|
|
266
|
+
const { Readable } = await import('node:stream');
|
|
267
|
+
const bodyStream = new Readable({
|
|
268
|
+
read() {
|
|
269
|
+
// Push preamble
|
|
270
|
+
this.push(filePreamble);
|
|
271
|
+
// Push file data in chunks via sync read to keep it simple
|
|
272
|
+
const CHUNK = 256 * 1024; // 256KB chunks
|
|
273
|
+
const fd = fs.openSync(bundlePath, 'r');
|
|
274
|
+
try {
|
|
275
|
+
const buf = Buffer.allocUnsafe(CHUNK);
|
|
276
|
+
let bytesRead;
|
|
277
|
+
while ((bytesRead = fs.readSync(fd, buf, 0, CHUNK)) > 0) {
|
|
278
|
+
this.push(bytesRead === CHUNK ? buf : buf.subarray(0, bytesRead));
|
|
279
|
+
}
|
|
280
|
+
} finally {
|
|
281
|
+
fs.closeSync(fd);
|
|
282
|
+
}
|
|
283
|
+
// Push epilogue + fields + close
|
|
284
|
+
this.push(fileEpilogue);
|
|
285
|
+
this.push(fieldsBuffer);
|
|
286
|
+
this.push(null); // EOF
|
|
287
|
+
}
|
|
288
|
+
});
|
|
267
289
|
|
|
268
290
|
const url = `${uploadEndpoint}/api/v1/delta/upload`;
|
|
269
|
-
log(`[uploader] Uploading to ${url}...`);
|
|
291
|
+
log(`[uploader] Uploading to ${url} (${(bundleSize / 1024).toFixed(0)}KB bundle, streaming)...`);
|
|
270
292
|
|
|
271
293
|
const resp = await fetch(url, {
|
|
272
294
|
method: "POST",
|
|
273
295
|
headers: {
|
|
274
296
|
"Content-Type": `multipart/form-data; boundary=${boundary}`,
|
|
275
|
-
"Content-Length": String(
|
|
297
|
+
"Content-Length": String(totalLength),
|
|
276
298
|
},
|
|
277
|
-
body,
|
|
299
|
+
body: bodyStream,
|
|
300
|
+
duplex: "half", // required for streaming request bodies in Node.js fetch
|
|
278
301
|
});
|
|
279
302
|
|
|
280
303
|
let result;
|
|
@@ -298,8 +321,23 @@ export async function uploadBundle(bundlePath, manifest, uploadEndpoint, session
|
|
|
298
321
|
return { success: true, result };
|
|
299
322
|
}
|
|
300
323
|
|
|
324
|
+
let _indexInFlight = false;
|
|
301
325
|
export async function indexWorkspace(workspacePath, uploadEndpoint, sessionId, options = {}) {
|
|
302
326
|
const { log = console.error, orgId, orgSlug } = options;
|
|
327
|
+
if (_indexInFlight) {
|
|
328
|
+
log("[uploader] indexWorkspace already in progress, skipping concurrent call");
|
|
329
|
+
return { success: false, error: "already_in_progress" };
|
|
330
|
+
}
|
|
331
|
+
_indexInFlight = true;
|
|
332
|
+
try {
|
|
333
|
+
return await _indexWorkspaceInner(workspacePath, uploadEndpoint, sessionId, options);
|
|
334
|
+
} finally {
|
|
335
|
+
_indexInFlight = false;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async function _indexWorkspaceInner(workspacePath, uploadEndpoint, sessionId, options = {}) {
|
|
340
|
+
const { log = console.error, orgId, orgSlug } = options;
|
|
303
341
|
|
|
304
342
|
log(`[uploader] Scanning workspace: ${workspacePath}`);
|
|
305
343
|
|