@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 +1 -1
- package/src/authCli.js +9 -1
- package/src/authConfig.js +71 -1
- package/src/mcpServer.js +185 -50
- package/src/oauthHandler.js +17 -1
- package/src/syncDaemon.js +32 -6
- package/src/uploader.js +136 -3
package/package.json
CHANGED
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, {
|
|
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]
|
|
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 {
|
|
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 = {
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
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) {
|
package/src/oauthHandler.js
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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,
|