@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 CHANGED
@@ -3,5 +3,8 @@
3
3
  ## Unreleased
4
4
  - Initial public packaging for @mcoda/integrations.
5
5
 
6
+ ## 0.1.9
7
+ - Release v0.1.9.
8
+
6
9
  ## 0.1.8
7
10
  - Initial release.
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 local or remote docdex queries.
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
- private getStorePath;
38
+ disable(reason?: string): void;
39
+ isAvailable(): boolean;
35
40
  private normalizePath;
36
- private loadStore;
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 saveStore;
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":"AAIA,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;AAyCD,qBAAa,YAAY;IAErB,OAAO,CAAC,OAAO;gBAAP,OAAO,GAAE;QACf,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,SAAS,CAAC,EAAE,MAAM,CAAC;KACf;IAGR,OAAO,CAAC,YAAY;IAOpB,OAAO,CAAC,aAAa;YAYP,SAAS;YAaT,WAAW;YAYX,SAAS;IAOjB,iBAAiB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IAoBtD,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC;IAU1F,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;IA0BtH,gBAAgB,CAAC,KAAK,EAAE,qBAAqB,GAAG,OAAO,CAAC,cAAc,CAAC;IA6DvE,wBAAwB,CAC5B,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACjC,OAAO,CAAC,cAAc,CAAC;CAO3B"}
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
- getStorePath() {
41
- const base = this.options.storePath
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
- async loadStore() {
59
- const storePath = this.getStorePath();
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 raw = await fs.readFile(storePath, "utf8");
62
- const parsed = JSON.parse(raw);
63
- parsed.documents = parsed.documents ?? [];
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 { updatedAt: nowIso(), documents: [], segments: [] };
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 (!this.options.baseUrl)
73
- throw new Error("Docdex baseUrl not configured");
74
- const url = new URL(pathname, this.options.baseUrl);
75
- const headers = { "Content-Type": "application/json" };
76
- if (this.options.authToken)
77
- headers.authorization = `Bearer ${this.options.authToken}`;
78
- const response = await fetch(url, { ...init, headers: { ...headers, ...init?.headers } });
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
- throw new Error(`Docdex request failed (${response.status}): ${await response.text()}`);
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 (await response.json());
202
+ return response;
83
203
  }
84
- async saveStore(store) {
85
- const storePath = this.getStorePath();
86
- await fs.mkdir(path.dirname(storePath), { recursive: true });
87
- const payload = { ...store, updatedAt: nowIso() };
88
- await fs.writeFile(storePath, JSON.stringify(payload, null, 2), "utf8");
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
- if (this.options.baseUrl) {
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 doc = await this.fetchRemote(`/documents/${id}`);
94
- return doc;
277
+ const parsed = JSON.parse(body);
278
+ content =
279
+ parsed.text ??
280
+ parsed.content ??
281
+ parsed.snippet ??
282
+ body;
95
283
  }
96
- catch (error) {
97
- // fall through to local if remote fails
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 store = await this.loadStore();
103
- const doc = store.documents.find((d) => d.id === id);
104
- if (!doc) {
105
- throw new Error(`Docdex document not found: ${id}`);
106
- }
107
- const segments = store.segments.filter((s) => s.docId === id);
108
- return { ...doc, segments };
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 store = await this.loadStore();
113
- const doc = store.documents.find((d) => d.path === normalized && (!docType || d.docType.toLowerCase() === docType.toLowerCase()));
114
- if (!doc)
115
- return undefined;
116
- return { ...doc, segments: store.segments.filter((s) => s.docId === doc.id) };
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
- if (this.options.baseUrl) {
120
- try {
121
- const params = new URLSearchParams();
122
- if (filter.docType)
123
- params.set("doc_type", filter.docType);
124
- if (filter.projectKey)
125
- params.set("project_key", filter.projectKey);
126
- if (filter.query)
127
- params.set("q", filter.query);
128
- if (filter.profile)
129
- params.set("profile", filter.profile);
130
- const path = `/documents?${params.toString()}`;
131
- const docs = await this.fetchRemote(path);
132
- return docs;
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 store = await this.loadStore();
140
- return store.documents
141
- .filter((doc) => {
142
- if (filter.docType && doc.docType.toLowerCase() !== filter.docType.toLowerCase())
143
- return false;
144
- if (filter.projectKey && doc.metadata && doc.metadata.projectKey !== filter.projectKey)
145
- return false;
146
- return true;
147
- })
148
- .map((doc) => ({ ...doc, segments: store.segments.filter((s) => s.docId === doc.id) }));
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
- if (this.options.baseUrl) {
152
- try {
153
- const registered = await this.fetchRemote(`/documents`, {
154
- method: "POST",
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
- const store = await this.loadStore();
171
- const normalizedPath = this.normalizePath(input.path);
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
- const doc = {
193
- id: randomUUID(),
194
- docType: input.docType,
195
- path: normalizedPath,
196
- content: input.content,
197
- metadata: input.metadata,
198
- title: input.title,
199
- createdAt: now,
200
- updatedAt: now,
201
- };
202
- const segments = segmentize(doc.id, input.content);
203
- store.documents.push(doc);
204
- store.segments.push(...segments);
205
- await this.saveStore(store);
206
- return { ...doc, segments };
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
- const existing = await this.findDocumentByPath(normalizedPath, docType);
211
- if (existing)
212
- return existing;
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
- return this.registerDocument({ docType, path: docPath, content, metadata });
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
  }