@danielblomma/cortex-mcp 2.4.1 → 2.4.2

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.
Files changed (45) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +5 -0
  3. package/bin/cli/arguments.mjs +60 -0
  4. package/bin/cli/context-passthrough.mjs +129 -0
  5. package/bin/cli/daemon.mjs +157 -0
  6. package/bin/cli/enterprise.mjs +516 -0
  7. package/bin/cli/help.mjs +88 -0
  8. package/bin/cli/hooks.mjs +199 -0
  9. package/bin/cli/mcp-command.mjs +87 -0
  10. package/bin/cli/paths.mjs +14 -0
  11. package/bin/cli/process.mjs +49 -0
  12. package/bin/cli/project-commands.mjs +237 -0
  13. package/bin/cli/project-runtime.mjs +34 -0
  14. package/bin/cli/query-command.mjs +18 -0
  15. package/bin/cli/router.mjs +66 -0
  16. package/bin/cli/run-command.mjs +44 -0
  17. package/bin/cli/scaffold-ownership.mjs +936 -0
  18. package/bin/cli/scaffold.mjs +687 -0
  19. package/bin/cli/stage-command.mjs +9 -0
  20. package/bin/cli/telemetry-command.mjs +18 -0
  21. package/bin/cli/trusted-runtime.mjs +18 -0
  22. package/bin/cortex.mjs +6 -1834
  23. package/mcp-registry-submission.json +1 -1
  24. package/package.json +3 -2
  25. package/scaffold/ownership/baseline-v2.4.1.json +22 -0
  26. package/scaffold/ownership/current.json +4 -0
  27. package/scaffold/ownership/v1.json +456 -0
  28. package/scaffold/scripts/ingest-parsers.mjs +1 -387
  29. package/scaffold/scripts/ingest-worker.mjs +4 -1
  30. package/scaffold/scripts/ingest.mjs +5 -3899
  31. package/scaffold/scripts/lib/ingest/arguments.mjs +78 -0
  32. package/scaffold/scripts/lib/ingest/chunks.mjs +212 -0
  33. package/scaffold/scripts/lib/ingest/config.mjs +120 -0
  34. package/scaffold/scripts/lib/ingest/constants.mjs +222 -0
  35. package/scaffold/scripts/lib/ingest/files.mjs +387 -0
  36. package/scaffold/scripts/lib/ingest/incremental-state.mjs +131 -0
  37. package/scaffold/scripts/lib/ingest/io.mjs +78 -0
  38. package/scaffold/scripts/lib/ingest/main.mjs +76 -0
  39. package/scaffold/scripts/lib/ingest/parser-composition.mjs +140 -0
  40. package/scaffold/scripts/lib/ingest/parser-registry.mjs +387 -0
  41. package/scaffold/scripts/lib/ingest/pipeline-stages.mjs +1625 -0
  42. package/scaffold/scripts/lib/ingest/projects.mjs +272 -0
  43. package/scaffold/scripts/lib/ingest/relations.mjs +860 -0
  44. package/scaffold/scripts/lib/ingest/runtime-paths.mjs +11 -0
  45. package/scaffold/scripts/lib/ingest/workers.mjs +264 -0
@@ -0,0 +1,11 @@
1
+ import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+
4
+ const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
5
+ export const SCRIPT_DIR = path.resolve(MODULE_DIR, "..", "..");
6
+ export const REPO_ROOT = process.env.CORTEX_PROJECT_ROOT
7
+ ? path.resolve(process.env.CORTEX_PROJECT_ROOT)
8
+ : path.resolve(SCRIPT_DIR, "..", "..");
9
+ export const CONTEXT_DIR = path.join(REPO_ROOT, ".context");
10
+ export const CACHE_DIR = path.join(CONTEXT_DIR, "cache");
11
+ export const DB_IMPORT_DIR = path.join(CONTEXT_DIR, "db", "import");
@@ -0,0 +1,264 @@
1
+ import os from "node:os";
2
+ import { Worker } from "node:worker_threads";
3
+
4
+ function resolveIngestWorkerCount(taskCount) {
5
+ const raw = process.env.CORTEX_INGEST_WORKERS;
6
+ const configured = raw !== undefined ? Number.parseInt(raw, 10) : Number.NaN;
7
+ const cpuBudget = Math.max(1, (os.availableParallelism?.() ?? os.cpus().length) - 1);
8
+ const defaultWorkerLimit = taskCount >= 1000 ? 4 : 8;
9
+ const desired =
10
+ Number.isFinite(configured) && configured >= 0
11
+ ? configured
12
+ : Math.min(cpuBudget, defaultWorkerLimit);
13
+ if (desired <= 1) return 1;
14
+ // Worker spin-up plus per-worker WASM grammar init dominates on small or
15
+ // incremental runs; stay sequential until there is enough work to amortize.
16
+ if (taskCount < 50) return 1;
17
+ return Math.min(desired, taskCount);
18
+ }
19
+
20
+ function createEmptyWorkerParseStream(tasks, workerCount) {
21
+ const stats = () => ({
22
+ worker_tasks: tasks.length,
23
+ worker_count: Number.isFinite(workerCount) ? workerCount : 0,
24
+ worker_tasks_assigned: 0,
25
+ worker_tasks_settled: 0,
26
+ worker_tasks_unsettled_fallback: tasks.length,
27
+ worker_results: 0,
28
+ worker_results_consumed: 0,
29
+ worker_results_retained: 0,
30
+ worker_results_retained_peak: 0,
31
+ worker_results_pending: 0,
32
+ worker_results_missing: tasks.length,
33
+ worker_waiters: 0
34
+ });
35
+ return {
36
+ hasTask: () => false,
37
+ stats,
38
+ take: async () => undefined,
39
+ drain: async () => stats()
40
+ };
41
+ }
42
+
43
+ function startWorkerParseStream(tasks, { workerCount, verbose, workerUrl } = {}) {
44
+ if (tasks.length === 0) {
45
+ return createEmptyWorkerParseStream(tasks, workerCount);
46
+ }
47
+
48
+ const resolvedWorkerUrl = workerUrl ?? new URL("../../ingest-worker.mjs", import.meta.url);
49
+ const poolSize = Math.min(workerCount, tasks.length);
50
+ // Defensive: an undefined/0/negative/NaN workerCount yields poolSize < 1, in
51
+ // which case no workers are ever spawned and the pool would never resolve.
52
+ // Return empty so the caller parses everything inline. (Math.min(undefined, n)
53
+ // is NaN, which is why this is `!(>= 1)` rather than `< 1`.)
54
+ if (!(poolSize >= 1)) {
55
+ return createEmptyWorkerParseStream(tasks, workerCount);
56
+ }
57
+
58
+ const taskIds = new Set(tasks.map((task) => task.id));
59
+ const results = new Map();
60
+ const missingResults = new Set();
61
+ const waiters = new Map();
62
+ const workers = [];
63
+ const inflight = new Map(); // worker -> taskId being parsed, or null when idle
64
+ let nextTask = 0;
65
+ let alive = poolSize;
66
+ let finished = false;
67
+ let resolveDone;
68
+ const done = new Promise((resolve) => {
69
+ resolveDone = resolve;
70
+ });
71
+ const state = {
72
+ assigned: 0,
73
+ settled: 0,
74
+ successful: 0,
75
+ consumed: 0,
76
+ retainedPeak: 0
77
+ };
78
+
79
+ const stats = () => ({
80
+ worker_tasks: tasks.length,
81
+ worker_count: poolSize,
82
+ worker_tasks_assigned: state.assigned,
83
+ worker_tasks_settled: state.settled,
84
+ worker_tasks_unsettled_fallback: finished ? Math.max(0, tasks.length - state.settled) : 0,
85
+ worker_results: state.successful,
86
+ worker_results_consumed: state.consumed,
87
+ worker_results_retained: results.size,
88
+ worker_results_retained_peak: state.retainedPeak,
89
+ worker_results_pending: finished ? 0 : Math.max(0, tasks.length - state.settled),
90
+ worker_results_missing: tasks.length - state.successful,
91
+ worker_waiters: waiters.size
92
+ });
93
+
94
+ const finish = () => {
95
+ if (finished) return;
96
+ finished = true;
97
+ for (const resolve of waiters.values()) {
98
+ resolve(undefined);
99
+ }
100
+ waiters.clear();
101
+ resolveDone();
102
+ };
103
+
104
+ // A task is "settled" once it has a result, was skipped, or its worker
105
+ // died holding it. Finish when every task is settled, or when no worker is
106
+ // left alive to make progress (any still-queued tasks then parse inline).
107
+ const maybeFinish = () => {
108
+ if (state.settled >= tasks.length || alive <= 0) {
109
+ finish();
110
+ }
111
+ };
112
+
113
+ const resolveWaiter = (taskId, result) => {
114
+ const resolve = waiters.get(taskId);
115
+ if (!resolve) {
116
+ return false;
117
+ }
118
+ waiters.delete(taskId);
119
+ if (result !== undefined) {
120
+ state.consumed += 1;
121
+ }
122
+ resolve(result);
123
+ return true;
124
+ };
125
+
126
+ const settleTask = (taskId, result) => {
127
+ state.settled += 1;
128
+ if (result !== undefined) {
129
+ state.successful += 1;
130
+ if (!resolveWaiter(taskId, result)) {
131
+ results.set(taskId, result);
132
+ state.retainedPeak = Math.max(state.retainedPeak, results.size);
133
+ }
134
+ } else {
135
+ missingResults.add(taskId);
136
+ resolveWaiter(taskId, undefined);
137
+ }
138
+ maybeFinish();
139
+ };
140
+
141
+ const assign = (worker) => {
142
+ if (nextTask >= tasks.length) {
143
+ inflight.set(worker, null);
144
+ worker.postMessage({ type: "shutdown" });
145
+ return;
146
+ }
147
+ const task = tasks[nextTask++];
148
+ state.assigned += 1;
149
+ inflight.set(worker, task.id);
150
+ worker.postMessage({
151
+ taskId: task.id,
152
+ ext: task.ext,
153
+ content: task.content,
154
+ absolutePath: task.absolutePath,
155
+ contentLimit: task.contentLimit,
156
+ filePath: task.path
157
+ });
158
+ };
159
+
160
+ const onMessage = (worker, message) => {
161
+ if (finished) return;
162
+ inflight.set(worker, null);
163
+ if (message.ok) {
164
+ settleTask(message.taskId, message.result);
165
+ } else {
166
+ if (verbose) {
167
+ console.log(`[ingest] worker skipped ${message.taskId}: ${message.reason}`);
168
+ }
169
+ settleTask(message.taskId, undefined);
170
+ }
171
+ if (!finished) {
172
+ assign(worker);
173
+ }
174
+ };
175
+
176
+ const onExit = (worker) => {
177
+ if (finished) return;
178
+ alive -= 1;
179
+ const taskId = inflight.get(worker);
180
+ if (taskId != null) {
181
+ // Worker exited mid-parse without posting a result (OOM, native abort,
182
+ // process.exit) and without an 'error' event. Count its in-flight task
183
+ // as settled so it falls back to inline parsing rather than leaving the
184
+ // pool waiting on a dead worker forever.
185
+ if (verbose) {
186
+ console.log(`[ingest] worker exited mid-task ${taskId}; will parse inline`);
187
+ }
188
+ inflight.set(worker, null);
189
+ settleTask(taskId, undefined);
190
+ return;
191
+ }
192
+ maybeFinish();
193
+ };
194
+
195
+ for (let i = 0; i < poolSize; i += 1) {
196
+ const worker = new Worker(resolvedWorkerUrl);
197
+ workers.push(worker);
198
+ worker.on("message", (message) => onMessage(worker, message));
199
+ worker.on("error", (error) => {
200
+ // Uncaught exception in the worker. The 'exit' event that always
201
+ // follows does the task accounting (idempotent via inflight), so we
202
+ // only log here to avoid double-counting.
203
+ if (verbose) {
204
+ console.log(`[ingest] worker error: ${error.message}`);
205
+ }
206
+ });
207
+ worker.on("exit", () => onExit(worker));
208
+ }
209
+
210
+ for (const worker of workers) {
211
+ assign(worker);
212
+ }
213
+
214
+ return {
215
+ hasTask(taskId) {
216
+ return taskIds.has(taskId);
217
+ },
218
+ stats,
219
+ async take(taskId) {
220
+ if (!taskIds.has(taskId)) {
221
+ return undefined;
222
+ }
223
+ if (results.has(taskId)) {
224
+ const result = results.get(taskId);
225
+ results.delete(taskId);
226
+ state.consumed += 1;
227
+ return result;
228
+ }
229
+ if (missingResults.has(taskId) || finished) {
230
+ return undefined;
231
+ }
232
+ return new Promise((resolve) => {
233
+ waiters.set(taskId, resolve);
234
+ });
235
+ },
236
+ async drain() {
237
+ await done;
238
+ await Promise.all(workers.map((worker) => worker.terminate().catch(() => {})));
239
+ return stats();
240
+ }
241
+ };
242
+ }
243
+
244
+ // Compatibility helper for direct worker-pool tests. The ingest pipeline
245
+ // consumes the stream in sorted file-record order and does not retain this map.
246
+ async function parseFilesInWorkers(tasks, options = {}) {
247
+ const stream = startWorkerParseStream(tasks, options);
248
+ const results = new Map();
249
+ for (const task of tasks) {
250
+ const result = await stream.take(task.id);
251
+ if (result) {
252
+ results.set(task.id, result);
253
+ }
254
+ }
255
+
256
+ await stream.drain();
257
+ return results;
258
+ }
259
+
260
+ export {
261
+ parseFilesInWorkers,
262
+ resolveIngestWorkerCount,
263
+ startWorkerParseStream
264
+ };