@mcoda/integrations 0.1.8 → 0.1.10
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/CHANGELOG.md +3 -0
- package/README.md +3 -1
- package/dist/docdex/DocdexClient.d.ts +19 -4
- package/dist/docdex/DocdexClient.d.ts.map +1 -1
- package/dist/docdex/DocdexClient.js +359 -133
- package/dist/docdex/DocdexRuntime.d.ts +59 -0
- package/dist/docdex/DocdexRuntime.d.ts.map +1 -0
- package/dist/docdex/DocdexRuntime.js +219 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/qa/ChromiumQaAdapter.d.ts +59 -1
- package/dist/qa/ChromiumQaAdapter.d.ts.map +1 -1
- package/dist/qa/ChromiumQaAdapter.js +1634 -42
- package/dist/qa/CliQaAdapter.d.ts +2 -0
- package/dist/qa/CliQaAdapter.d.ts.map +1 -1
- package/dist/qa/CliQaAdapter.js +88 -33
- package/dist/qa/QaTypes.d.ts +4 -0
- package/dist/qa/QaTypes.d.ts.map +1 -1
- package/dist/telemetry/TelemetryClient.d.ts +13 -0
- package/dist/telemetry/TelemetryClient.d.ts.map +1 -1
- package/dist/vcs/VcsClient.d.ts +20 -1
- package/dist/vcs/VcsClient.d.ts.map +1 -1
- package/dist/vcs/VcsClient.js +123 -10
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ External integrations for mcoda (docdex, telemetry, VCS, QA runners, update chec
|
|
|
7
7
|
- Install: `npm i @mcoda/integrations`
|
|
8
8
|
|
|
9
9
|
## What it provides
|
|
10
|
-
- DocdexClient for
|
|
10
|
+
- DocdexClient for docdex daemon queries and CLI-backed ingestion.
|
|
11
11
|
- TelemetryClient for token usage reporting.
|
|
12
12
|
- VcsClient for Git operations.
|
|
13
13
|
- SystemClient for update checks.
|
|
@@ -26,6 +26,8 @@ const docs = await client.search({ docType: "rfp", query: "payments" });
|
|
|
26
26
|
```
|
|
27
27
|
|
|
28
28
|
## Notes
|
|
29
|
+
- Docdex state lives under `~/.docdex` (managed by the `docdex` CLI); mcoda does not create repo-local `.docdex`.
|
|
30
|
+
- Chromium QA expects Docdex-installed Chromium (`docdex setup` or `MCODA_QA_CHROMIUM_PATH`).
|
|
29
31
|
- Some integrations call external services; configure base URLs and tokens as needed.
|
|
30
32
|
- Primarily used by the mcoda CLI; APIs may evolve.
|
|
31
33
|
|
|
@@ -25,17 +25,28 @@ export interface RegisterDocumentInput {
|
|
|
25
25
|
}
|
|
26
26
|
export declare class DocdexClient {
|
|
27
27
|
private options;
|
|
28
|
+
private resolvedBaseUrl?;
|
|
29
|
+
private repoId?;
|
|
30
|
+
private initializing;
|
|
31
|
+
private disabledReason?;
|
|
28
32
|
constructor(options?: {
|
|
29
33
|
workspaceRoot?: string;
|
|
30
|
-
storePath?: string;
|
|
31
34
|
baseUrl?: string;
|
|
32
35
|
authToken?: string;
|
|
36
|
+
repoId?: string;
|
|
33
37
|
});
|
|
34
|
-
|
|
38
|
+
disable(reason?: string): void;
|
|
39
|
+
isAvailable(): boolean;
|
|
35
40
|
private normalizePath;
|
|
36
|
-
private
|
|
41
|
+
private resolveLocalDocPath;
|
|
42
|
+
private loadLocalDoc;
|
|
43
|
+
private resolveBaseUrl;
|
|
44
|
+
private buildMissingRepoMessage;
|
|
45
|
+
ensureRepoScope(): Promise<void>;
|
|
46
|
+
private ensureRepoInitialized;
|
|
37
47
|
private fetchRemote;
|
|
38
|
-
private
|
|
48
|
+
private buildLocalDoc;
|
|
49
|
+
private coerceSearchResults;
|
|
39
50
|
fetchDocumentById(id: string): Promise<DocdexDocument>;
|
|
40
51
|
findDocumentByPath(docPath: string, docType?: string): Promise<DocdexDocument | undefined>;
|
|
41
52
|
search(filter: {
|
|
@@ -44,7 +55,11 @@ export declare class DocdexClient {
|
|
|
44
55
|
query?: string;
|
|
45
56
|
profile?: string;
|
|
46
57
|
}): Promise<DocdexDocument[]>;
|
|
58
|
+
reindex(): Promise<void>;
|
|
47
59
|
registerDocument(input: RegisterDocumentInput): Promise<DocdexDocument>;
|
|
48
60
|
ensureRegisteredFromFile(docPath: string, docType: string, metadata?: Record<string, unknown>): Promise<DocdexDocument>;
|
|
61
|
+
private callMcp;
|
|
62
|
+
memorySave(text: string): Promise<void>;
|
|
63
|
+
savePreference(agentId: string, category: string, content: string): Promise<void>;
|
|
49
64
|
}
|
|
50
65
|
//# sourceMappingURL=DocdexClient.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DocdexClient.d.ts","sourceRoot":"","sources":["../../src/docdex/DocdexClient.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"DocdexClient.d.ts","sourceRoot":"","sources":["../../src/docdex/DocdexClient.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,aAAa,EAAE,CAAC;CAC5B;AAED,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAkDD,qBAAa,YAAY;IAOrB,OAAO,CAAC,OAAO;IANjB,OAAO,CAAC,eAAe,CAAC,CAAS;IACjC,OAAO,CAAC,MAAM,CAAC,CAAS;IACxB,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,cAAc,CAAC,CAAS;gBAGtB,OAAO,GAAE;QACf,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,MAAM,CAAC,EAAE,MAAM,CAAC;KACZ;IAKR,OAAO,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI;IAI9B,WAAW,IAAI,OAAO;IAItB,OAAO,CAAC,aAAa;IAYrB,OAAO,CAAC,mBAAmB;YASb,YAAY;YAYZ,cAAc;IAY5B,OAAO,CAAC,uBAAuB;IAKzB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC;YAWxB,qBAAqB;YA0BrB,WAAW;IAwCzB,OAAO,CAAC,aAAa;IAgBrB,OAAO,CAAC,mBAAmB;IAmDrB,iBAAiB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IA4BtD,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC;IAkB1F,MAAM,CAAC,MAAM,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IAkBtH,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAcxB,gBAAgB,CAAC,KAAK,EAAE,qBAAqB,GAAG,OAAO,CAAC,cAAc,CAAC;IA0BvE,wBAAwB,CAC5B,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACjC,OAAO,CAAC,cAAc,CAAC;YAqBZ,OAAO;IAyBf,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQvC,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAQxF"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { promises as fs } from "node:fs";
|
|
4
|
+
import { resolveDocdexBaseUrl, runDocdex } from "./DocdexRuntime.js";
|
|
4
5
|
const nowIso = () => new Date().toISOString();
|
|
5
6
|
const segmentize = (docId, content) => {
|
|
6
7
|
const lines = content.split(/\r?\n/);
|
|
@@ -33,15 +34,36 @@ const segmentize = (docId, content) => {
|
|
|
33
34
|
flush();
|
|
34
35
|
return segments;
|
|
35
36
|
};
|
|
37
|
+
const inferDocType = (docPath, fallback = "DOC") => {
|
|
38
|
+
if (!docPath)
|
|
39
|
+
return fallback;
|
|
40
|
+
const name = path.basename(docPath).toLowerCase();
|
|
41
|
+
if (name.includes("openapi") || name.includes("swagger"))
|
|
42
|
+
return "OPENAPI";
|
|
43
|
+
if (name.includes("sds"))
|
|
44
|
+
return "SDS";
|
|
45
|
+
if (name.includes("pdr"))
|
|
46
|
+
return "PDR";
|
|
47
|
+
if (name.includes("rfp"))
|
|
48
|
+
return "RFP";
|
|
49
|
+
return fallback;
|
|
50
|
+
};
|
|
51
|
+
const normalizeBaseUrl = (value) => {
|
|
52
|
+
if (!value)
|
|
53
|
+
return value;
|
|
54
|
+
return value.endsWith("/") ? value.slice(0, -1) : value;
|
|
55
|
+
};
|
|
36
56
|
export class DocdexClient {
|
|
37
57
|
constructor(options = {}) {
|
|
38
58
|
this.options = options;
|
|
59
|
+
this.initializing = false;
|
|
60
|
+
this.repoId = options.repoId;
|
|
61
|
+
}
|
|
62
|
+
disable(reason) {
|
|
63
|
+
this.disabledReason = reason?.trim() || "docdex unavailable";
|
|
39
64
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
? path.resolve(this.options.storePath)
|
|
43
|
-
: path.join(this.options.workspaceRoot ?? process.cwd(), ".mcoda", "docdex", "documents.json");
|
|
44
|
-
return base;
|
|
65
|
+
isAvailable() {
|
|
66
|
+
return !this.disabledReason;
|
|
45
67
|
}
|
|
46
68
|
normalizePath(inputPath) {
|
|
47
69
|
if (!inputPath)
|
|
@@ -55,162 +77,366 @@ export class DocdexClient {
|
|
|
55
77
|
}
|
|
56
78
|
return absolute;
|
|
57
79
|
}
|
|
58
|
-
|
|
59
|
-
|
|
80
|
+
resolveLocalDocPath(docPath) {
|
|
81
|
+
if (!this.options.workspaceRoot)
|
|
82
|
+
return undefined;
|
|
83
|
+
const root = path.resolve(this.options.workspaceRoot);
|
|
84
|
+
const absolute = path.resolve(path.isAbsolute(docPath) ? docPath : path.join(root, docPath));
|
|
85
|
+
if (!absolute.startsWith(root))
|
|
86
|
+
return undefined;
|
|
87
|
+
const relative = path.relative(root, absolute);
|
|
88
|
+
return { absolute, relative: relative || undefined };
|
|
89
|
+
}
|
|
90
|
+
async loadLocalDoc(docPath, docType) {
|
|
91
|
+
const resolved = this.resolveLocalDocPath(docPath);
|
|
92
|
+
if (!resolved)
|
|
93
|
+
return undefined;
|
|
60
94
|
try {
|
|
61
|
-
const
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
parsed.segments = parsed.segments ?? [];
|
|
65
|
-
return parsed;
|
|
95
|
+
const content = await fs.readFile(resolved.absolute, "utf8");
|
|
96
|
+
const inferred = docType || inferDocType(resolved.relative ?? docPath);
|
|
97
|
+
return this.buildLocalDoc(inferred, resolved.relative ?? docPath, content);
|
|
66
98
|
}
|
|
67
99
|
catch {
|
|
68
|
-
return
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async resolveBaseUrl() {
|
|
104
|
+
if (this.disabledReason)
|
|
105
|
+
return undefined;
|
|
106
|
+
if (this.options.baseUrl !== undefined) {
|
|
107
|
+
const trimmed = this.options.baseUrl.trim();
|
|
108
|
+
return trimmed ? normalizeBaseUrl(trimmed) : undefined;
|
|
109
|
+
}
|
|
110
|
+
if (this.resolvedBaseUrl !== undefined)
|
|
111
|
+
return this.resolvedBaseUrl;
|
|
112
|
+
const resolved = await resolveDocdexBaseUrl({ cwd: this.options.workspaceRoot });
|
|
113
|
+
this.resolvedBaseUrl = resolved ? normalizeBaseUrl(resolved) : undefined;
|
|
114
|
+
return this.resolvedBaseUrl;
|
|
115
|
+
}
|
|
116
|
+
buildMissingRepoMessage() {
|
|
117
|
+
const root = this.options.workspaceRoot ? path.resolve(this.options.workspaceRoot) : "unknown workspace";
|
|
118
|
+
return `Docdex repo scope missing for ${root}. Ensure docdexd is running and initialized for this repo, or set MCODA_DOCDEX_URL and MCODA_DOCDEX_REPO_ID.`;
|
|
119
|
+
}
|
|
120
|
+
async ensureRepoScope() {
|
|
121
|
+
const baseUrl = await this.resolveBaseUrl();
|
|
122
|
+
if (!baseUrl)
|
|
123
|
+
return;
|
|
124
|
+
if (!this.repoId) {
|
|
125
|
+
await this.ensureRepoInitialized(baseUrl, true);
|
|
126
|
+
}
|
|
127
|
+
if (!this.repoId) {
|
|
128
|
+
throw new Error(this.buildMissingRepoMessage());
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
async ensureRepoInitialized(baseUrl, force = false) {
|
|
132
|
+
if ((this.repoId && !force) || this.initializing)
|
|
133
|
+
return;
|
|
134
|
+
if (!this.options.workspaceRoot)
|
|
135
|
+
return;
|
|
136
|
+
this.initializing = true;
|
|
137
|
+
try {
|
|
138
|
+
const rootUri = `file://${path.resolve(this.options.workspaceRoot)}`;
|
|
139
|
+
const response = await fetch(`${baseUrl}/v1/initialize`, {
|
|
140
|
+
method: "POST",
|
|
141
|
+
headers: { "Content-Type": "application/json" },
|
|
142
|
+
body: JSON.stringify({ rootUri }),
|
|
143
|
+
});
|
|
144
|
+
if (!response.ok)
|
|
145
|
+
return;
|
|
146
|
+
const payload = (await response.json().catch(() => undefined));
|
|
147
|
+
const repoId = payload?.repoId ??
|
|
148
|
+
payload?.repo_id ??
|
|
149
|
+
payload?.repo ??
|
|
150
|
+
payload?.id;
|
|
151
|
+
if (repoId)
|
|
152
|
+
this.repoId = String(repoId);
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
// ignore initialize errors; assume single-repo daemon
|
|
156
|
+
}
|
|
157
|
+
finally {
|
|
158
|
+
this.initializing = false;
|
|
69
159
|
}
|
|
70
160
|
}
|
|
71
161
|
async fetchRemote(pathname, init) {
|
|
72
|
-
if (
|
|
73
|
-
throw new Error(
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
if (
|
|
77
|
-
|
|
78
|
-
|
|
162
|
+
if (this.disabledReason) {
|
|
163
|
+
throw new Error(`Docdex unavailable: ${this.disabledReason}`);
|
|
164
|
+
}
|
|
165
|
+
const baseUrl = await this.resolveBaseUrl();
|
|
166
|
+
if (!baseUrl) {
|
|
167
|
+
throw new Error("Docdex baseUrl not configured. Run docdex setup or set MCODA_DOCDEX_URL.");
|
|
168
|
+
}
|
|
169
|
+
await this.ensureRepoScope();
|
|
170
|
+
const url = new URL(pathname, baseUrl);
|
|
171
|
+
if (this.repoId && !url.searchParams.has("repo_id")) {
|
|
172
|
+
url.searchParams.set("repo_id", this.repoId);
|
|
173
|
+
}
|
|
174
|
+
if (this.options.workspaceRoot && !url.searchParams.has("repo_root")) {
|
|
175
|
+
url.searchParams.set("repo_root", path.resolve(this.options.workspaceRoot));
|
|
176
|
+
}
|
|
177
|
+
const buildHeaders = () => {
|
|
178
|
+
const headers = { "Content-Type": "application/json" };
|
|
179
|
+
if (this.options.authToken)
|
|
180
|
+
headers.authorization = `Bearer ${this.options.authToken}`;
|
|
181
|
+
if (this.repoId)
|
|
182
|
+
headers["x-docdex-repo-id"] = this.repoId;
|
|
183
|
+
if (this.options.workspaceRoot)
|
|
184
|
+
headers["x-docdex-repo-root"] = path.resolve(this.options.workspaceRoot);
|
|
185
|
+
return { ...headers, ...init?.headers };
|
|
186
|
+
};
|
|
187
|
+
const response = await fetch(url, { ...init, headers: buildHeaders() });
|
|
79
188
|
if (!response.ok) {
|
|
80
|
-
|
|
189
|
+
const message = await response.text();
|
|
190
|
+
if (message.includes("missing_repo")) {
|
|
191
|
+
await this.ensureRepoInitialized(baseUrl, true);
|
|
192
|
+
if (this.repoId) {
|
|
193
|
+
const retry = await fetch(url, { ...init, headers: buildHeaders() });
|
|
194
|
+
if (retry.ok)
|
|
195
|
+
return retry;
|
|
196
|
+
const retryMessage = await retry.text();
|
|
197
|
+
throw new Error(`Docdex request failed (${retry.status}): ${retryMessage}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
throw new Error(`Docdex request failed (${response.status}): ${message}`);
|
|
81
201
|
}
|
|
82
|
-
return
|
|
202
|
+
return response;
|
|
83
203
|
}
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
204
|
+
buildLocalDoc(docType, docPath, content, metadata) {
|
|
205
|
+
const now = nowIso();
|
|
206
|
+
const id = `local-${randomUUID()}`;
|
|
207
|
+
return {
|
|
208
|
+
id,
|
|
209
|
+
docType,
|
|
210
|
+
path: docPath,
|
|
211
|
+
title: docPath ? path.basename(docPath) : undefined,
|
|
212
|
+
content,
|
|
213
|
+
metadata,
|
|
214
|
+
createdAt: now,
|
|
215
|
+
updatedAt: now,
|
|
216
|
+
segments: segmentize(id, content),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
coerceSearchResults(raw, fallbackDocType) {
|
|
220
|
+
const items = Array.isArray(raw)
|
|
221
|
+
? raw
|
|
222
|
+
: Array.isArray(raw?.results)
|
|
223
|
+
? raw.results
|
|
224
|
+
: Array.isArray(raw?.hits)
|
|
225
|
+
? raw.hits
|
|
226
|
+
: [];
|
|
227
|
+
const now = nowIso();
|
|
228
|
+
return items
|
|
229
|
+
.map((item, idx) => {
|
|
230
|
+
if (!item || typeof item !== "object")
|
|
231
|
+
return undefined;
|
|
232
|
+
const id = (item.doc_id ?? item.docId ?? item.id ?? `doc-${idx + 1}`);
|
|
233
|
+
const pathValue = (item.path ?? item.file ?? item.rel_path ?? item.file_path);
|
|
234
|
+
const title = (item.title ?? item.name ?? item.file_name);
|
|
235
|
+
const docType = (item.doc_type ?? item.docType ?? item.type ?? inferDocType(pathValue, fallbackDocType ?? "DOC"));
|
|
236
|
+
const snippet = (item.snippet ?? item.summary ?? item.excerpt);
|
|
237
|
+
const content = (item.content ?? snippet);
|
|
238
|
+
const segments = Array.isArray(item.segments)
|
|
239
|
+
? item.segments.map((seg, segIdx) => ({
|
|
240
|
+
id: seg.id ?? `${id}-seg-${segIdx + 1}`,
|
|
241
|
+
docId: id,
|
|
242
|
+
index: segIdx,
|
|
243
|
+
content: seg.content ?? seg.text ?? "",
|
|
244
|
+
heading: seg.heading ?? seg.title ?? undefined,
|
|
245
|
+
}))
|
|
246
|
+
: snippet
|
|
247
|
+
? [
|
|
248
|
+
{
|
|
249
|
+
id: `${id}-seg-1`,
|
|
250
|
+
docId: id,
|
|
251
|
+
index: 0,
|
|
252
|
+
content: snippet,
|
|
253
|
+
heading: undefined,
|
|
254
|
+
},
|
|
255
|
+
]
|
|
256
|
+
: undefined;
|
|
257
|
+
return {
|
|
258
|
+
id,
|
|
259
|
+
docType,
|
|
260
|
+
path: pathValue,
|
|
261
|
+
title,
|
|
262
|
+
content,
|
|
263
|
+
createdAt: item.created_at ?? item.createdAt ?? now,
|
|
264
|
+
updatedAt: item.updated_at ?? item.updatedAt ?? now,
|
|
265
|
+
segments,
|
|
266
|
+
};
|
|
267
|
+
})
|
|
268
|
+
.filter(Boolean);
|
|
89
269
|
}
|
|
90
270
|
async fetchDocumentById(id) {
|
|
91
|
-
|
|
271
|
+
const response = await this.fetchRemote(`/snippet/${encodeURIComponent(id)}?text_only=true`);
|
|
272
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
273
|
+
const body = await response.text();
|
|
274
|
+
let content = body;
|
|
275
|
+
if (contentType.includes("application/json")) {
|
|
92
276
|
try {
|
|
93
|
-
const
|
|
94
|
-
|
|
277
|
+
const parsed = JSON.parse(body);
|
|
278
|
+
content =
|
|
279
|
+
parsed.text ??
|
|
280
|
+
parsed.content ??
|
|
281
|
+
parsed.snippet ??
|
|
282
|
+
body;
|
|
95
283
|
}
|
|
96
|
-
catch
|
|
97
|
-
|
|
98
|
-
// eslint-disable-next-line no-console
|
|
99
|
-
console.warn(`Docdex remote fetch failed, falling back to local: ${error.message}`);
|
|
284
|
+
catch {
|
|
285
|
+
content = body;
|
|
100
286
|
}
|
|
101
287
|
}
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
288
|
+
const now = nowIso();
|
|
289
|
+
return {
|
|
290
|
+
id,
|
|
291
|
+
docType: "DOC",
|
|
292
|
+
content,
|
|
293
|
+
createdAt: now,
|
|
294
|
+
updatedAt: now,
|
|
295
|
+
segments: content ? segmentize(id, content) : undefined,
|
|
296
|
+
};
|
|
109
297
|
}
|
|
110
298
|
async findDocumentByPath(docPath, docType) {
|
|
111
299
|
const normalized = this.normalizePath(docPath);
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
300
|
+
const query = normalized ?? docPath;
|
|
301
|
+
let docs = [];
|
|
302
|
+
try {
|
|
303
|
+
docs = await this.search({ query, docType });
|
|
304
|
+
}
|
|
305
|
+
catch (error) {
|
|
306
|
+
const local = await this.loadLocalDoc(docPath, docType);
|
|
307
|
+
if (local)
|
|
308
|
+
return local;
|
|
309
|
+
throw error;
|
|
310
|
+
}
|
|
311
|
+
if (!docs.length) {
|
|
312
|
+
return await this.loadLocalDoc(docPath, docType);
|
|
313
|
+
}
|
|
314
|
+
if (!normalized)
|
|
315
|
+
return docs[0];
|
|
316
|
+
return docs.find((doc) => doc.path === normalized) ?? docs[0];
|
|
117
317
|
}
|
|
118
318
|
async search(filter) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
catch (error) {
|
|
135
|
-
// eslint-disable-next-line no-console
|
|
136
|
-
console.warn(`Docdex remote search failed, falling back to local: ${error.message}`);
|
|
137
|
-
}
|
|
319
|
+
const params = new URLSearchParams();
|
|
320
|
+
const queryParts = [filter.query, filter.docType, filter.projectKey].filter(Boolean);
|
|
321
|
+
const query = queryParts.join(" ").trim();
|
|
322
|
+
if (query)
|
|
323
|
+
params.set("q", query);
|
|
324
|
+
if (filter.profile)
|
|
325
|
+
params.set("profile", filter.profile);
|
|
326
|
+
if (filter.docType)
|
|
327
|
+
params.set("doc_type", filter.docType);
|
|
328
|
+
if (filter.projectKey)
|
|
329
|
+
params.set("project_key", filter.projectKey);
|
|
330
|
+
params.set("limit", "8");
|
|
331
|
+
const baseUrl = await this.resolveBaseUrl();
|
|
332
|
+
if (!baseUrl) {
|
|
333
|
+
return [];
|
|
138
334
|
}
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
335
|
+
const response = await this.fetchRemote(`/search?${params.toString()}`);
|
|
336
|
+
const payload = (await response.json());
|
|
337
|
+
return this.coerceSearchResults(payload, filter.docType);
|
|
338
|
+
}
|
|
339
|
+
async reindex() {
|
|
340
|
+
const repoRoot = this.options.workspaceRoot ?? process.cwd();
|
|
341
|
+
const baseUrl = await this.resolveBaseUrl();
|
|
342
|
+
await runDocdex(["index", "--repo", repoRoot], {
|
|
343
|
+
cwd: repoRoot,
|
|
344
|
+
env: baseUrl
|
|
345
|
+
? {
|
|
346
|
+
DOCDEX_URL: baseUrl,
|
|
347
|
+
MCODA_DOCDEX_URL: baseUrl,
|
|
348
|
+
}
|
|
349
|
+
: undefined,
|
|
350
|
+
});
|
|
149
351
|
}
|
|
150
352
|
async registerDocument(input) {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
body: JSON.stringify({
|
|
156
|
-
doc_type: input.docType,
|
|
157
|
-
path: input.path,
|
|
158
|
-
title: input.title,
|
|
159
|
-
content: input.content,
|
|
160
|
-
metadata: input.metadata,
|
|
161
|
-
}),
|
|
162
|
-
});
|
|
163
|
-
return registered;
|
|
164
|
-
}
|
|
165
|
-
catch (error) {
|
|
166
|
-
// eslint-disable-next-line no-console
|
|
167
|
-
console.warn(`Docdex remote register failed, falling back to local: ${error.message}`);
|
|
168
|
-
}
|
|
353
|
+
const baseUrl = await this.resolveBaseUrl();
|
|
354
|
+
if (!baseUrl) {
|
|
355
|
+
const normalized = input.path ? this.normalizePath(input.path) ?? input.path : undefined;
|
|
356
|
+
return this.buildLocalDoc(input.docType, normalized, input.content, input.metadata);
|
|
169
357
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const existingByPath = normalizedPath
|
|
173
|
-
? store.documents.find((d) => d.path === normalizedPath &&
|
|
174
|
-
d.docType.toLowerCase() === input.docType.toLowerCase() &&
|
|
175
|
-
(input.metadata?.projectKey ? d.metadata?.projectKey === input.metadata.projectKey : true))
|
|
176
|
-
: undefined;
|
|
177
|
-
const now = nowIso();
|
|
178
|
-
if (existingByPath) {
|
|
179
|
-
const updated = {
|
|
180
|
-
...existingByPath,
|
|
181
|
-
content: input.content ?? existingByPath.content,
|
|
182
|
-
metadata: { ...(existingByPath.metadata ?? {}), ...(input.metadata ?? {}) },
|
|
183
|
-
title: input.title ?? existingByPath.title,
|
|
184
|
-
updatedAt: now,
|
|
185
|
-
};
|
|
186
|
-
const segments = segmentize(updated.id, input.content);
|
|
187
|
-
store.documents[store.documents.findIndex((d) => d.id === existingByPath.id)] = updated;
|
|
188
|
-
store.segments = store.segments.filter((s) => s.docId !== updated.id).concat(segments);
|
|
189
|
-
await this.saveStore(store);
|
|
190
|
-
return { ...updated, segments };
|
|
358
|
+
if (!input.path) {
|
|
359
|
+
throw new Error("Docdex register requires a file path to ingest.");
|
|
191
360
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
361
|
+
await this.ensureRepoInitialized(baseUrl);
|
|
362
|
+
const resolvedPath = path.isAbsolute(input.path)
|
|
363
|
+
? input.path
|
|
364
|
+
: path.join(this.options.workspaceRoot ?? process.cwd(), input.path);
|
|
365
|
+
const repoRoot = this.options.workspaceRoot ?? process.cwd();
|
|
366
|
+
await runDocdex(["ingest", "--repo", repoRoot, "--file", resolvedPath], {
|
|
367
|
+
cwd: repoRoot,
|
|
368
|
+
env: {
|
|
369
|
+
DOCDEX_URL: baseUrl,
|
|
370
|
+
MCODA_DOCDEX_URL: baseUrl,
|
|
371
|
+
},
|
|
372
|
+
});
|
|
373
|
+
const registered = await this.findDocumentByPath(resolvedPath, input.docType).catch(() => undefined);
|
|
374
|
+
if (registered)
|
|
375
|
+
return registered;
|
|
376
|
+
return this.buildLocalDoc(input.docType, resolvedPath, input.content, input.metadata);
|
|
207
377
|
}
|
|
208
378
|
async ensureRegisteredFromFile(docPath, docType, metadata) {
|
|
209
379
|
const normalizedPath = this.normalizePath(docPath) ?? docPath;
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
380
|
+
try {
|
|
381
|
+
const existing = await this.findDocumentByPath(normalizedPath, docType);
|
|
382
|
+
if (existing)
|
|
383
|
+
return existing;
|
|
384
|
+
}
|
|
385
|
+
catch {
|
|
386
|
+
// ignore docdex lookup failures; fall back to local
|
|
387
|
+
}
|
|
213
388
|
const content = await fs.readFile(docPath, "utf8");
|
|
214
|
-
|
|
389
|
+
const inferredType = docType || inferDocType(docPath);
|
|
390
|
+
const baseUrl = await this.resolveBaseUrl();
|
|
391
|
+
if (!baseUrl) {
|
|
392
|
+
return this.buildLocalDoc(inferredType, normalizedPath, content, metadata);
|
|
393
|
+
}
|
|
394
|
+
try {
|
|
395
|
+
return await this.registerDocument({ docType: inferredType, path: docPath, content, metadata });
|
|
396
|
+
}
|
|
397
|
+
catch {
|
|
398
|
+
return this.buildLocalDoc(inferredType, normalizedPath, content, metadata);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
async callMcp(toolName, args) {
|
|
402
|
+
const payload = {
|
|
403
|
+
jsonrpc: "2.0",
|
|
404
|
+
id: randomUUID(),
|
|
405
|
+
method: "tools/call",
|
|
406
|
+
params: {
|
|
407
|
+
name: toolName,
|
|
408
|
+
arguments: args,
|
|
409
|
+
},
|
|
410
|
+
};
|
|
411
|
+
const response = await this.fetchRemote("/v1/mcp", {
|
|
412
|
+
method: "POST",
|
|
413
|
+
body: JSON.stringify(payload),
|
|
414
|
+
});
|
|
415
|
+
const raw = (await response.json());
|
|
416
|
+
if (raw.error) {
|
|
417
|
+
throw new Error(raw.error.message ?? "Docdex MCP call failed");
|
|
418
|
+
}
|
|
419
|
+
const result = raw.result;
|
|
420
|
+
if (result && typeof result === "object" && result.structuredContent !== undefined) {
|
|
421
|
+
return result.structuredContent;
|
|
422
|
+
}
|
|
423
|
+
return result;
|
|
424
|
+
}
|
|
425
|
+
async memorySave(text) {
|
|
426
|
+
if (!text.trim())
|
|
427
|
+
return;
|
|
428
|
+
await this.callMcp("docdex_memory_save", {
|
|
429
|
+
project_root: this.options.workspaceRoot,
|
|
430
|
+
text,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
async savePreference(agentId, category, content) {
|
|
434
|
+
if (!agentId.trim() || !category.trim() || !content.trim())
|
|
435
|
+
return;
|
|
436
|
+
await this.callMcp("docdex_save_preference", {
|
|
437
|
+
agent_id: agentId,
|
|
438
|
+
category,
|
|
439
|
+
content,
|
|
440
|
+
});
|
|
215
441
|
}
|
|
216
442
|
}
|