@context-engine-bridge/context-engine-mcp-bridge 0.0.78 → 0.0.82

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@context-engine-bridge/context-engine-mcp-bridge",
3
- "version": "0.0.78",
3
+ "version": "0.0.82",
4
4
  "description": "Context Engine MCP bridge (http/stdio proxy combining indexer + memory servers)",
5
5
  "bin": {
6
6
  "ctxce": "bin/ctxce.js",
package/src/authCli.js CHANGED
@@ -154,11 +154,19 @@ async function doLogin(args) {
154
154
  const sessionId = data.session_id || data.sessionId || null;
155
155
  const userId = data.user_id || data.userId || null;
156
156
  const expiresAt = data.expires_at || data.expiresAt || null;
157
+ const orgId = data.org_id || data.orgId || null;
158
+ const orgSlug = data.org_slug || data.orgSlug || null;
157
159
  if (!sessionId) {
158
160
  console.error("[ctxce] Auth login response missing session id.");
159
161
  process.exit(1);
160
162
  }
161
- saveAuthEntry(url, { sessionId, userId, expiresAt });
163
+ saveAuthEntry(url, {
164
+ sessionId,
165
+ userId,
166
+ expiresAt,
167
+ org_id: orgId,
168
+ org_slug: orgSlug,
169
+ });
162
170
  console.error("[ctxce] Auth login successful for", url);
163
171
  }
164
172
 
package/src/authConfig.js CHANGED
@@ -4,6 +4,7 @@ import path from "node:path";
4
4
 
5
5
  const CONFIG_DIR_NAME = ".ctxce";
6
6
  const CONFIG_BASENAME = "auth.json";
7
+ const RESOLVED_COLLECTIONS_KEY = "resolved_collections_v1";
7
8
 
8
9
  function getConfigPath() {
9
10
  const home = os.homedir() || process.cwd();
@@ -36,6 +37,38 @@ function writeConfig(data) {
36
37
  }
37
38
  }
38
39
 
40
+ function normalizeScopeValue(value) {
41
+ return typeof value === "string" ? value.trim() : "";
42
+ }
43
+
44
+ function buildResolvedCollectionScopeKey({ orgId, orgSlug, logicalRepoId } = {}) {
45
+ const repo = normalizeScopeValue(logicalRepoId);
46
+ if (!repo) {
47
+ return "";
48
+ }
49
+ const normalizedOrgId = normalizeScopeValue(orgId);
50
+ const normalizedOrgSlug = normalizeScopeValue(orgSlug);
51
+ const org = normalizedOrgId || (normalizedOrgSlug ? `slug:${normalizedOrgSlug}` : "org:none");
52
+ return `${org}::${repo}`;
53
+ }
54
+
55
+ function getResolvedCollections(entry) {
56
+ const map = entry && typeof entry === "object" ? entry[RESOLVED_COLLECTIONS_KEY] : null;
57
+ return map && typeof map === "object" ? map : {};
58
+ }
59
+
60
+ function preserveInternalEntryState(existingEntry, nextEntry) {
61
+ const preserved = {};
62
+ const existingCollections = getResolvedCollections(existingEntry);
63
+ if (Object.keys(existingCollections).length > 0) {
64
+ preserved[RESOLVED_COLLECTIONS_KEY] = existingCollections;
65
+ }
66
+ return {
67
+ ...preserved,
68
+ ...nextEntry,
69
+ };
70
+ }
71
+
39
72
  export function loadAuthEntry(backendUrl) {
40
73
  if (!backendUrl) {
41
74
  return null;
@@ -55,7 +88,8 @@ export function saveAuthEntry(backendUrl, entry) {
55
88
  }
56
89
  const all = readConfig();
57
90
  const key = String(backendUrl);
58
- all[key] = entry;
91
+ const existingEntry = all[key];
92
+ all[key] = preserveInternalEntryState(existingEntry, entry);
59
93
  writeConfig(all);
60
94
  }
61
95
 
@@ -82,3 +116,39 @@ export function loadAnyAuthEntry() {
82
116
  }
83
117
  return null;
84
118
  }
119
+
120
+ export function loadResolvedCollection(backendUrl, scope) {
121
+ if (!backendUrl) {
122
+ return null;
123
+ }
124
+ const scopeKey = buildResolvedCollectionScopeKey(scope);
125
+ if (!scopeKey) {
126
+ return null;
127
+ }
128
+ const entry = loadAuthEntry(backendUrl);
129
+ const resolved = getResolvedCollections(entry)[scopeKey];
130
+ return typeof resolved === "string" && resolved.trim() ? resolved.trim() : null;
131
+ }
132
+
133
+ export function saveResolvedCollection(backendUrl, scope, collection) {
134
+ if (!backendUrl) {
135
+ return;
136
+ }
137
+ const scopeKey = buildResolvedCollectionScopeKey(scope);
138
+ const nextCollection = typeof collection === "string" ? collection.trim() : "";
139
+ if (!scopeKey || !nextCollection) {
140
+ return;
141
+ }
142
+ const all = readConfig();
143
+ const key = String(backendUrl);
144
+ const existingEntry = all[key] && typeof all[key] === "object" ? all[key] : {};
145
+ const resolvedCollections = {
146
+ ...getResolvedCollections(existingEntry),
147
+ [scopeKey]: nextCollection,
148
+ };
149
+ all[key] = {
150
+ ...existingEntry,
151
+ [RESOLVED_COLLECTIONS_KEY]: resolvedCollections,
152
+ };
153
+ writeConfig(all);
154
+ }
package/src/mcpServer.js CHANGED
@@ -11,9 +11,17 @@ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/
11
11
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
12
12
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
13
13
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
14
- import { loadAnyAuthEntry, loadAuthEntry, readConfig, saveAuthEntry } from "./authConfig.js";
14
+ import {
15
+ loadAnyAuthEntry,
16
+ loadAuthEntry,
17
+ loadResolvedCollection,
18
+ readConfig,
19
+ saveAuthEntry,
20
+ saveResolvedCollection,
21
+ } from "./authConfig.js";
15
22
  import { maybeRemapToolArgs, maybeRemapToolResult } from "./resultPathMapping.js";
16
23
  import { startSyncDaemon } from "./syncDaemon.js";
24
+ import { computeLogicalRepoIdentity } from "./uploader.js";
17
25
  import * as oauthHandler from "./oauthHandler.js";
18
26
 
19
27
  const LSP_CONN_CACHE_TTL = 5000;
@@ -593,6 +601,7 @@ async function fetchBridgeCollectionState({
593
601
  collection,
594
602
  sessionId,
595
603
  repoName,
604
+ logicalRepoId,
596
605
  bridgeStateToken,
597
606
  backendHint,
598
607
  uploadServiceUrl,
@@ -612,6 +621,9 @@ async function fetchBridgeCollectionState({
612
621
  if (repoName && repoName.trim()) {
613
622
  url.searchParams.set("repo_name", repoName.trim());
614
623
  }
624
+ if (logicalRepoId && logicalRepoId.trim()) {
625
+ url.searchParams.set("logical_repo_id", logicalRepoId.trim());
626
+ }
615
627
 
616
628
  const headers = {
617
629
  Accept: "application/json",
@@ -646,6 +658,131 @@ async function fetchBridgeCollectionState({
646
658
  }
647
659
  }
648
660
 
661
+ export async function buildDefaultsPayload({
662
+ workspace,
663
+ sessionId,
664
+ explicitCollection,
665
+ defaultCollection,
666
+ defaultMode,
667
+ defaultUnder,
668
+ config,
669
+ backendHint,
670
+ uploadServiceUrl,
671
+ bridgeStateToken = BRIDGE_STATE_TOKEN,
672
+ authEntry = undefined,
673
+ fetchState = fetchBridgeCollectionState,
674
+ getLogicalRepoIdentity = computeLogicalRepoIdentity,
675
+ loadResolvedCollectionHint = loadResolvedCollection,
676
+ saveResolvedCollectionHint = saveResolvedCollection,
677
+ log = debugLog,
678
+ } = {}) {
679
+ const defaultsPayload = { session: sessionId };
680
+ const pinnedCollection =
681
+ typeof explicitCollection === "string" ? explicitCollection.trim() : "";
682
+ const configuredCollection =
683
+ !pinnedCollection && typeof defaultCollection === "string"
684
+ ? defaultCollection.trim()
685
+ : "";
686
+
687
+ if (pinnedCollection) {
688
+ defaultsPayload.collection = pinnedCollection;
689
+ } else if (configuredCollection) {
690
+ defaultsPayload.collection = configuredCollection;
691
+ }
692
+
693
+ const resolvedAuthEntry = authEntry === undefined
694
+ ? (backendHint ? loadAuthEntry(backendHint) : null)
695
+ : authEntry;
696
+
697
+ if (resolvedAuthEntry && (resolvedAuthEntry.org_id || resolvedAuthEntry.org_slug)) {
698
+ if (resolvedAuthEntry.org_id) {
699
+ defaultsPayload.org_id = resolvedAuthEntry.org_id;
700
+ }
701
+ if (resolvedAuthEntry.org_slug) {
702
+ defaultsPayload.org_slug = resolvedAuthEntry.org_slug;
703
+ }
704
+ }
705
+
706
+ const repoName = detectRepoName(workspace, config);
707
+ let logicalRepoId = "";
708
+ try {
709
+ logicalRepoId = getLogicalRepoIdentity(workspace)?.id || "";
710
+ } catch {
711
+ logicalRepoId = "";
712
+ }
713
+
714
+ const exactWorkspaceCollection = !pinnedCollection
715
+ ? _readExactWorkspaceCachedCollection(workspace)
716
+ : null;
717
+
718
+ if (!defaultsPayload.collection && exactWorkspaceCollection) {
719
+ defaultsPayload.collection = exactWorkspaceCollection;
720
+ log(`[ctxce] Using exact workspace cached collection: ${exactWorkspaceCollection}`);
721
+ }
722
+
723
+ if (!defaultsPayload.collection && backendHint && logicalRepoId) {
724
+ const cachedCollection = loadResolvedCollectionHint(backendHint, {
725
+ orgId: resolvedAuthEntry?.org_id,
726
+ orgSlug: resolvedAuthEntry?.org_slug,
727
+ logicalRepoId,
728
+ });
729
+ if (cachedCollection) {
730
+ defaultsPayload.collection = cachedCollection;
731
+ log(`[ctxce] Using cached resolved collection from auth store: ${cachedCollection}`);
732
+ }
733
+ }
734
+
735
+ if (!pinnedCollection) {
736
+ try {
737
+ const state = await fetchState({
738
+ workspace,
739
+ collection: configuredCollection,
740
+ sessionId,
741
+ repoName,
742
+ logicalRepoId,
743
+ bridgeStateToken,
744
+ backendHint,
745
+ uploadServiceUrl,
746
+ });
747
+ if (state) {
748
+ const servingCollection = typeof state.serving_collection === "string"
749
+ ? state.serving_collection.trim()
750
+ : "";
751
+ const activeCollection = typeof state.active_collection === "string"
752
+ ? state.active_collection.trim()
753
+ : "";
754
+ if (servingCollection) {
755
+ defaultsPayload.collection = servingCollection;
756
+ if (!configuredCollection || configuredCollection !== servingCollection) {
757
+ log(`[ctxce] Using serving collection from /bridge/state: ${servingCollection}`);
758
+ }
759
+ if (backendHint && logicalRepoId) {
760
+ saveResolvedCollectionHint(backendHint, {
761
+ orgId: resolvedAuthEntry?.org_id,
762
+ orgSlug: resolvedAuthEntry?.org_slug,
763
+ logicalRepoId,
764
+ }, servingCollection);
765
+ }
766
+ } else if (!defaultsPayload.collection && activeCollection) {
767
+ defaultsPayload.collection = activeCollection;
768
+ log(`[ctxce] Using active collection from /bridge/state fallback: ${activeCollection}`);
769
+ }
770
+ }
771
+ } catch (err) {
772
+ log("[ctxce] bridge/state lookup failed: " + String(err));
773
+ }
774
+ }
775
+
776
+ if (defaultMode) {
777
+ defaultsPayload.mode = defaultMode;
778
+ }
779
+ if (defaultUnder) {
780
+ defaultsPayload.under = defaultUnder;
781
+ }
782
+
783
+ return defaultsPayload;
784
+ }
785
+
649
786
  function _validateWorkspacePath(raw) {
650
787
  if (typeof raw !== "string" || raw.length === 0) return null;
651
788
  const resolved = path.resolve(raw);
@@ -659,6 +796,42 @@ function _validateWorkspacePath(raw) {
659
796
  return resolved;
660
797
  }
661
798
 
799
+ function _readExactWorkspaceCachedCollection(workspacePath) {
800
+ const resolvedWorkspace = _validateWorkspacePath(workspacePath);
801
+ if (!resolvedWorkspace) return null;
802
+
803
+ const wsDir = _computeWorkspaceDir(resolvedWorkspace);
804
+ const configPath = path.join(wsDir, "ctx_config.json");
805
+ const metaPath = path.join(wsDir, "meta.json");
806
+
807
+ if (!fs.existsSync(configPath)) {
808
+ return null;
809
+ }
810
+
811
+ if (fs.existsSync(metaPath)) {
812
+ try {
813
+ const meta = JSON.parse(fs.readFileSync(metaPath, "utf8"));
814
+ const metaWorkspace = _validateWorkspacePath(meta && meta.workspace_path);
815
+ if (!metaWorkspace || metaWorkspace !== resolvedWorkspace) {
816
+ return null;
817
+ }
818
+ } catch (_) {
819
+ return null;
820
+ }
821
+ }
822
+
823
+ try {
824
+ const parsed = JSON.parse(fs.readFileSync(configPath, "utf8"));
825
+ const collection =
826
+ parsed && typeof parsed.default_collection === "string"
827
+ ? parsed.default_collection.trim()
828
+ : "";
829
+ return collection || null;
830
+ } catch (_) {
831
+ return null;
832
+ }
833
+ }
834
+
662
835
  const MAX_WS_SCAN = 50;
663
836
 
664
837
  function _resolveWorkspace(providedWorkspace) {
@@ -877,55 +1050,17 @@ async function createBridgeServer(options) {
877
1050
 
878
1051
  // Best-effort: inform the indexer of default collection and session.
879
1052
  // If this fails we still proceed, falling back to per-call injection.
880
- const defaultsPayload = { session: sessionId };
881
- if (defaultCollection) {
882
- defaultsPayload.collection = defaultCollection;
883
- }
884
-
885
- // Include org context from auth entry if available (for org-scoped collection isolation)
886
- try {
887
- const authEntry = backendHint ? loadAuthEntry(backendHint) : null;
888
- if (authEntry && authEntry.org_id) {
889
- defaultsPayload.org_id = authEntry.org_id;
890
- defaultsPayload.org_slug = authEntry.org_slug;
891
- }
892
- } catch {
893
- // ignore auth entry lookup failures
894
- }
895
-
896
- const repoName = detectRepoName(workspace, config);
897
-
898
- try {
899
- const state = await fetchBridgeCollectionState({
900
- workspace,
901
- collection: defaultCollection,
902
- sessionId,
903
- repoName,
904
- bridgeStateToken: BRIDGE_STATE_TOKEN,
905
- backendHint,
906
- uploadServiceUrl,
907
- });
908
- if (state) {
909
- const serving = state.serving_collection || state.active_collection;
910
- if (serving) {
911
- defaultsPayload.collection = serving;
912
- if (!defaultCollection || defaultCollection !== serving) {
913
- debugLog(
914
- `[ctxce] Using serving collection from /bridge/state: ${serving}`,
915
- );
916
- }
917
- }
918
- }
919
- } catch (err) {
920
- debugLog("[ctxce] bridge/state lookup failed: " + String(err));
921
- }
922
-
923
- if (defaultMode) {
924
- defaultsPayload.mode = defaultMode;
925
- }
926
- if (defaultUnder) {
927
- defaultsPayload.under = defaultUnder;
928
- }
1053
+ const defaultsPayload = await buildDefaultsPayload({
1054
+ workspace,
1055
+ sessionId,
1056
+ explicitCollection,
1057
+ defaultCollection,
1058
+ defaultMode,
1059
+ defaultUnder,
1060
+ config,
1061
+ backendHint,
1062
+ uploadServiceUrl,
1063
+ });
929
1064
 
930
1065
  async function initializeRemoteClients(forceRecreate = false) {
931
1066
  if (!forceRecreate && indexerClient) {
@@ -247,6 +247,8 @@ export function getLoginPage(redirectUri, clientId, state, codeChallenge, codeCh
247
247
 
248
248
  const data = await resp.json();
249
249
  const sessionId = data.session_id || data.sessionId;
250
+ const orgId = data.org_id || data.orgId || null;
251
+ const orgSlug = data.org_slug || data.orgSlug || null;
250
252
  if (!sessionId) {
251
253
  throw new Error('No session in response');
252
254
  }
@@ -258,6 +260,8 @@ export function getLoginPage(redirectUri, clientId, state, codeChallenge, codeCh
258
260
  body: JSON.stringify({
259
261
  session_id: sessionId,
260
262
  backend_url: backendUrl,
263
+ org_id: orgId,
264
+ org_slug: orgSlug,
261
265
  redirect_uri: params.redirect_uri,
262
266
  state: params.state,
263
267
  code_challenge: params.code_challenge,
@@ -443,7 +447,17 @@ export function handleOAuthStoreSession(req, res) {
443
447
  res.setHeader("Content-Type", "application/json");
444
448
  try {
445
449
  const data = JSON.parse(body);
446
- const { session_id, backend_url, redirect_uri, state, code_challenge, code_challenge_method, client_id } = data;
450
+ const {
451
+ session_id,
452
+ backend_url,
453
+ org_id,
454
+ org_slug,
455
+ redirect_uri,
456
+ state,
457
+ code_challenge,
458
+ code_challenge_method,
459
+ client_id,
460
+ } = data;
447
461
 
448
462
  if (!session_id || !backend_url) {
449
463
  res.statusCode = 400;
@@ -503,6 +517,8 @@ export function handleOAuthStoreSession(req, res) {
503
517
  sessionId: session_id,
504
518
  userId: "oauth-user",
505
519
  expiresAt: null,
520
+ org_id: org_id || null,
521
+ org_slug: org_slug || null,
506
522
  });
507
523
 
508
524
  // Generate auth code
package/src/syncDaemon.js CHANGED
@@ -26,7 +26,7 @@ const noop = () => {};
26
26
 
27
27
  // ---------------------------------------------------------------------------
28
28
  // Process-level singleton registry
29
- // keyed by path.resolve(workspace) -> { intervalId, cleanup }
29
+ // keyed by path.resolve(workspace) -> { intervalId, cleanup, updateRuntimeState }
30
30
  // ---------------------------------------------------------------------------
31
31
 
32
32
  const _activeDaemons = new Map();
@@ -153,17 +153,24 @@ export function startSyncDaemon(options) {
153
153
  workspace,
154
154
  sessionId: initialSessionId,
155
155
  authEntry: initialAuthEntry,
156
- uploadEndpoint,
156
+ uploadEndpoint: initialUploadEndpoint,
157
157
  intervalMs = DEFAULT_WATCH_INTERVAL_MS,
158
158
  log = noop,
159
159
  } = options;
160
160
 
161
161
  const resolvedWorkspace = path.resolve(workspace);
162
162
 
163
- // Singleton guard: return existing daemon if already running.
163
+ // Singleton guard: return existing daemon if already running, but refresh
164
+ // any mutable runtime state supplied by the new caller.
164
165
  if (_activeDaemons.has(resolvedWorkspace)) {
166
+ const existingHandle = _activeDaemons.get(resolvedWorkspace);
167
+ existingHandle?.updateRuntimeState?.({
168
+ sessionId: initialSessionId,
169
+ authEntry: initialAuthEntry,
170
+ uploadEndpoint: initialUploadEndpoint,
171
+ });
165
172
  log(`[syncDaemon] Daemon already running for ${resolvedWorkspace}, reusing.`);
166
- return _activeDaemons.get(resolvedWorkspace);
173
+ return existingHandle;
167
174
  }
168
175
 
169
176
  log(`[syncDaemon] Starting file watcher for ${resolvedWorkspace} (interval: ${intervalMs / 1000}s)`);
@@ -174,9 +181,28 @@ export function startSyncDaemon(options) {
174
181
  let pendingHistoryOnly = true;
175
182
  let sessionId = initialSessionId;
176
183
  let authEntry = initialAuthEntry;
184
+ let uploadEndpoint = initialUploadEndpoint;
177
185
  let lastKnownHead = "";
178
186
 
179
- const authBackendUrl = deriveAuthBackendUrl(uploadEndpoint);
187
+ let authBackendUrl = deriveAuthBackendUrl(uploadEndpoint);
188
+
189
+ function updateRuntimeState(nextState = {}) {
190
+ if (typeof nextState.sessionId === "string" && nextState.sessionId) {
191
+ sessionId = nextState.sessionId;
192
+ }
193
+
194
+ if (nextState.authEntry && typeof nextState.authEntry === "object") {
195
+ authEntry = nextState.authEntry;
196
+ if (typeof nextState.authEntry.sessionId === "string" && nextState.authEntry.sessionId) {
197
+ sessionId = nextState.authEntry.sessionId;
198
+ }
199
+ }
200
+
201
+ if (typeof nextState.uploadEndpoint === "string" && nextState.uploadEndpoint) {
202
+ uploadEndpoint = nextState.uploadEndpoint;
203
+ authBackendUrl = deriveAuthBackendUrl(uploadEndpoint);
204
+ }
205
+ }
180
206
 
181
207
  // -------------------------------------------------------------------------
182
208
  // Session refresh
@@ -407,7 +433,7 @@ export function startSyncDaemon(options) {
407
433
  };
408
434
 
409
435
  // Register in singleton map BEFORE returning so concurrent callers see it.
410
- const handle = { intervalId, cleanup };
436
+ const handle = { intervalId, cleanup, updateRuntimeState };
411
437
  _activeDaemons.set(resolvedWorkspace, handle);
412
438
 
413
439
  return handle;
package/src/uploader.js CHANGED
@@ -37,6 +37,7 @@ const DEFAULT_IGNORES = [
37
37
  ];
38
38
 
39
39
  const MAX_FILE_SIZE = 10 * 1024 * 1024;
40
+ const ADMIN_SESSION_COOKIE_NAME = "ctxce_session";
40
41
 
41
42
  function sanitizeRepoName(repoName) {
42
43
  return String(repoName || "")
@@ -61,6 +62,116 @@ function getCollectionName(repoName) {
61
62
  return `${sanitized}-${hash}`;
62
63
  }
63
64
 
65
+ function normalizeBackendUrl(raw) {
66
+ const value = String(raw || "").trim();
67
+ if (!value) return "";
68
+ try {
69
+ const parsed = new URL(value);
70
+ return `${parsed.protocol}//${parsed.host}`;
71
+ } catch {
72
+ return value.replace(/\/+$/, "");
73
+ }
74
+ }
75
+
76
+ function buildBridgeStateHeaders(sessionId) {
77
+ const headers = {
78
+ Accept: "application/json",
79
+ };
80
+ if (sessionId) {
81
+ headers.Authorization = `Bearer ${sessionId}`;
82
+ headers["X-Session-Id"] = sessionId;
83
+ headers.Cookie = `${ADMIN_SESSION_COOKIE_NAME}=${sessionId}`;
84
+ }
85
+ return headers;
86
+ }
87
+
88
+ async function fetchBridgeCollectionState({
89
+ workspacePath,
90
+ uploadEndpoint,
91
+ sessionId,
92
+ repoName,
93
+ logicalRepoId,
94
+ log = console.error,
95
+ }) {
96
+ const backendUrl = normalizeBackendUrl(uploadEndpoint);
97
+ if (!backendUrl) {
98
+ return { kind: "skipped" };
99
+ }
100
+
101
+ try {
102
+ const url = new URL("/bridge/state", backendUrl);
103
+ if (workspacePath) {
104
+ url.searchParams.set("workspace", workspacePath);
105
+ }
106
+ if (repoName) {
107
+ url.searchParams.set("repo_name", repoName);
108
+ }
109
+ if (logicalRepoId) {
110
+ url.searchParams.set("logical_repo_id", logicalRepoId);
111
+ }
112
+
113
+ const resp = await fetch(url, {
114
+ method: "GET",
115
+ headers: buildBridgeStateHeaders(sessionId),
116
+ });
117
+ if (resp.status === 404) {
118
+ return { kind: "missing" };
119
+ }
120
+ if (resp.status === 401 || resp.status === 403) {
121
+ return { kind: "unavailable" };
122
+ }
123
+ if (!resp.ok) {
124
+ log(`[uploader] bridge/state lookup failed: HTTP ${resp.status}`);
125
+ return { kind: "unavailable" };
126
+ }
127
+
128
+ const data = await resp.json();
129
+ return {
130
+ kind: "ok",
131
+ state: data && typeof data === "object" ? data : null,
132
+ };
133
+ } catch (err) {
134
+ const message = err instanceof Error ? err.message : String(err);
135
+ log(`[uploader] bridge/state lookup failed: ${message}`);
136
+ return { kind: "unavailable" };
137
+ }
138
+ }
139
+
140
+ async function resolveRepoScopedCollection(workspacePath, uploadEndpoint, sessionId, options = {}) {
141
+ const { log = console.error } = options;
142
+ const repoName = extractRepoNameFromPath(workspacePath);
143
+ const logicalRepoIdentity = computeLogicalRepoIdentity(workspacePath);
144
+
145
+ const stateResult = await fetchBridgeCollectionState({
146
+ workspacePath,
147
+ uploadEndpoint,
148
+ sessionId,
149
+ repoName,
150
+ logicalRepoId: logicalRepoIdentity.id,
151
+ log,
152
+ });
153
+
154
+ if (stateResult.kind !== "ok") {
155
+ return null;
156
+ }
157
+
158
+ const state = stateResult.state || {};
159
+ const servingCollection = typeof state.serving_collection === "string"
160
+ ? state.serving_collection.trim()
161
+ : "";
162
+ const activeCollection = typeof state.active_collection === "string"
163
+ ? state.active_collection.trim()
164
+ : "";
165
+ const resolvedCollection = servingCollection || activeCollection;
166
+
167
+ if (resolvedCollection) {
168
+ log(`[uploader] Resuming repo-scoped collection ${resolvedCollection} for ${logicalRepoIdentity.id}`);
169
+ return resolvedCollection;
170
+ }
171
+
172
+ return null;
173
+ }
174
+
64
175
  export function findGitRoot(startPath) {
65
176
  let current = path.resolve(startPath);
66
177
  while (current !== path.dirname(current)) {
@@ -451,7 +562,13 @@ function scanWorkspace(workspacePath, ig) {
451
562
  }
452
563
 
453
564
  export async function createBundle(workspacePath, options = {}) {
454
- const { log = console.error, gitHistory = null, gitState = null, allowEmpty = false } = options;
565
+ const {
566
+ log = console.error,
567
+ gitHistory = null,
568
+ gitState = null,
569
+ allowEmpty = false,
570
+ collectionName = null,
571
+ } = options;
455
572
 
456
573
  const ig = loadGitignore(workspacePath);
457
574
  const files = scanWorkspace(workspacePath, ig);
@@ -508,7 +625,7 @@ export async function createBundle(workspacePath, options = {}) {
508
625
  version: "1.0",
509
626
  bundle_id: bundleId,
510
627
  workspace_path: workspacePath,
511
- collection_name: getCollectionName(repoName),
628
+ collection_name: collectionName || getCollectionName(repoName),
512
629
  created_at: createdAt,
513
630
  sequence_number: null,
514
631
  parent_sequence: null,
@@ -583,6 +700,12 @@ export async function uploadGitHistoryOnly(workspacePath, uploadEndpoint, sessio
583
700
  const { log = console.error, orgId, orgSlug, quietNoop = false } = options;
584
701
 
585
702
  try {
703
+ const resolvedCollection = await resolveRepoScopedCollection(
704
+ workspacePath,
705
+ uploadEndpoint,
706
+ sessionId,
707
+ { log, orgId, orgSlug }
708
+ );
586
709
  const gitHistory = collectGitHistory(workspacePath);
587
710
  if (!gitHistory) {
588
711
  if (!quietNoop) {
@@ -597,6 +720,7 @@ export async function uploadGitHistoryOnly(workspacePath, uploadEndpoint, sessio
597
720
  gitHistory,
598
721
  gitState,
599
722
  allowEmpty: true,
723
+ collectionName: resolvedCollection,
600
724
  });
601
725
  if (!bundle) {
602
726
  return { success: false, error: "failed_to_create_history_bundle" };
@@ -740,7 +864,16 @@ async function _indexWorkspaceInner(workspacePath, uploadEndpoint, sessionId, op
740
864
  });
741
865
  }
742
866
 
743
- const bundle = await createBundle(workspacePath, { log });
867
+ const resolvedCollection = await resolveRepoScopedCollection(
868
+ workspacePath,
869
+ uploadEndpoint,
870
+ sessionId,
871
+ { log, orgId, orgSlug }
872
+ );
873
+ const bundle = await createBundle(workspacePath, {
874
+ log,
875
+ collectionName: resolvedCollection,
876
+ });
744
877
  if (!bundle) {
745
878
  return await uploadGitHistoryOnly(workspacePath, uploadEndpoint, sessionId, {
746
879
  log,