@danielblomma/cortex-mcp 2.2.5 → 2.4.1

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 (196) hide show
  1. package/CHANGELOG.md +201 -0
  2. package/README.md +73 -3
  3. package/bin/cortex.mjs +339 -242
  4. package/bin/daemon-control.mjs +162 -0
  5. package/package.json +7 -3
  6. package/scaffold/mcp/dist/.cortex-build-hash +1 -0
  7. package/scaffold/mcp/dist/cli/enterprise-setup.js +134 -0
  8. package/scaffold/mcp/dist/cli/govern.js +937 -0
  9. package/scaffold/mcp/dist/cli/query.js +409 -0
  10. package/scaffold/mcp/dist/cli/run.js +295 -0
  11. package/scaffold/mcp/dist/cli/stage.js +308 -0
  12. package/scaffold/mcp/dist/cli/telemetry-test.js +141 -0
  13. package/scaffold/mcp/dist/cli/ungoverned-detector.js +133 -0
  14. package/scaffold/mcp/dist/contextEntities.js +282 -0
  15. package/scaffold/mcp/dist/core/audit/query.js +72 -0
  16. package/scaffold/mcp/dist/core/audit/writer.js +28 -0
  17. package/scaffold/mcp/dist/core/config.js +235 -0
  18. package/scaffold/mcp/dist/core/enterprise-host-identity.js +193 -0
  19. package/scaffold/mcp/dist/core/enterprise-identity.js +18 -0
  20. package/scaffold/mcp/dist/core/enterprise-rotation.js +158 -0
  21. package/scaffold/mcp/dist/core/govern-paths.js +21 -0
  22. package/scaffold/mcp/dist/core/index.js +14 -0
  23. package/scaffold/mcp/dist/core/license.js +278 -0
  24. package/scaffold/mcp/dist/core/policy/enforce.js +66 -0
  25. package/scaffold/mcp/dist/core/policy/injection.js +172 -0
  26. package/scaffold/mcp/dist/core/policy/store.js +179 -0
  27. package/scaffold/mcp/dist/core/rbac/check.js +30 -0
  28. package/scaffold/mcp/dist/core/secure-endpoint.js +25 -0
  29. package/scaffold/mcp/dist/core/telemetry/collector.js +285 -0
  30. package/scaffold/mcp/dist/core/telemetry/state-dir.js +31 -0
  31. package/scaffold/mcp/dist/core/validators/builtins.js +632 -0
  32. package/scaffold/mcp/dist/core/validators/config.js +44 -0
  33. package/scaffold/mcp/dist/core/validators/engine.js +120 -0
  34. package/scaffold/mcp/dist/core/validators/evaluators/code_comments.js +236 -0
  35. package/scaffold/mcp/dist/core/validators/evaluators/regex.js +116 -0
  36. package/scaffold/mcp/dist/core/workflow/artifact-io.js +103 -0
  37. package/scaffold/mcp/dist/core/workflow/capabilities.js +88 -0
  38. package/scaffold/mcp/dist/core/workflow/default-workflows.js +102 -0
  39. package/scaffold/mcp/dist/core/workflow/enforcement.js +164 -0
  40. package/scaffold/mcp/dist/core/workflow/envelope.js +132 -0
  41. package/scaffold/mcp/dist/core/workflow/index.js +11 -0
  42. package/scaffold/mcp/dist/core/workflow/mcp-tools.js +175 -0
  43. package/scaffold/mcp/dist/core/workflow/resolution.js +69 -0
  44. package/scaffold/mcp/dist/core/workflow/run-lifecycle.js +123 -0
  45. package/scaffold/mcp/dist/core/workflow/schemas.js +141 -0
  46. package/scaffold/mcp/dist/core/workflow/synced-capability-registry.js +60 -0
  47. package/scaffold/mcp/dist/core/workflow/synced-registry.js +60 -0
  48. package/scaffold/mcp/dist/daemon/capability-sync-checker.js +242 -0
  49. package/scaffold/mcp/dist/daemon/client.js +130 -0
  50. package/scaffold/mcp/dist/daemon/egress-proxy.js +288 -0
  51. package/scaffold/mcp/dist/daemon/global-host-events.js +222 -0
  52. package/scaffold/mcp/dist/daemon/heartbeat-pusher.js +116 -0
  53. package/scaffold/mcp/dist/daemon/heartbeat-tracker.js +165 -0
  54. package/scaffold/mcp/dist/daemon/host-events-pusher.js +249 -0
  55. package/scaffold/mcp/dist/daemon/main.js +449 -0
  56. package/scaffold/mcp/dist/daemon/paths.js +36 -0
  57. package/scaffold/mcp/dist/daemon/project-service-registry.js +78 -0
  58. package/scaffold/mcp/dist/daemon/protocol.js +8 -0
  59. package/scaffold/mcp/dist/daemon/server.js +180 -0
  60. package/scaffold/mcp/dist/daemon/skill-sync-checker.js +557 -0
  61. package/scaffold/mcp/dist/daemon/sync-checker.js +165 -0
  62. package/scaffold/mcp/dist/daemon/ungoverned-scanner.js +136 -0
  63. package/scaffold/mcp/dist/daemon/workflow-sync-checker.js +249 -0
  64. package/scaffold/mcp/dist/defaults.js +6 -0
  65. package/scaffold/mcp/dist/embed.js +627 -0
  66. package/scaffold/mcp/dist/embedScheduler.js +479 -0
  67. package/scaffold/mcp/dist/embeddings.js +167 -0
  68. package/scaffold/mcp/dist/enterprise/audit/push.js +66 -0
  69. package/scaffold/mcp/dist/enterprise/index.js +327 -0
  70. package/scaffold/mcp/dist/enterprise/model/deploy.js +27 -0
  71. package/scaffold/mcp/dist/enterprise/policy/sync.js +129 -0
  72. package/scaffold/mcp/dist/enterprise/privacy/boundary.js +184 -0
  73. package/scaffold/mcp/dist/enterprise/reviews/changed-files.js +33 -0
  74. package/scaffold/mcp/dist/enterprise/reviews/pattern-context.js +202 -0
  75. package/scaffold/mcp/dist/enterprise/reviews/policy-selection.js +46 -0
  76. package/scaffold/mcp/dist/enterprise/reviews/push.js +102 -0
  77. package/scaffold/mcp/dist/enterprise/reviews/trust-state.js +186 -0
  78. package/scaffold/mcp/dist/enterprise/telemetry/sync.js +57 -0
  79. package/scaffold/mcp/dist/enterprise/tools/enterprise.js +826 -0
  80. package/scaffold/mcp/dist/enterprise/tools/harness.js +40 -0
  81. package/scaffold/mcp/dist/enterprise/tools/walk.js +73 -0
  82. package/scaffold/mcp/dist/enterprise/violations/push.js +77 -0
  83. package/scaffold/mcp/dist/enterprise/workflow/push.js +44 -0
  84. package/scaffold/mcp/dist/enterprise/workflow/state.js +329 -0
  85. package/scaffold/mcp/dist/frontmatter.js +33 -0
  86. package/scaffold/mcp/dist/graph.js +769 -0
  87. package/scaffold/mcp/dist/graphCsv.js +55 -0
  88. package/scaffold/mcp/dist/graphMetrics.js +8 -0
  89. package/scaffold/mcp/dist/hooks/permission-request.js +89 -0
  90. package/scaffold/mcp/dist/hooks/post-tool-use.js +105 -0
  91. package/scaffold/mcp/dist/hooks/pre-compact.js +29 -0
  92. package/scaffold/mcp/dist/hooks/pre-tool-use.js +78 -0
  93. package/scaffold/mcp/dist/hooks/session-end.js +43 -0
  94. package/scaffold/mcp/dist/hooks/session-start.js +41 -0
  95. package/scaffold/mcp/dist/hooks/shared.js +194 -0
  96. package/scaffold/mcp/dist/hooks/stop.js +28 -0
  97. package/scaffold/mcp/dist/hooks/user-prompt-submit.js +33 -0
  98. package/scaffold/mcp/dist/impactPresentation.js +137 -0
  99. package/scaffold/mcp/dist/impactRanking.js +191 -0
  100. package/scaffold/mcp/dist/impactResponse.js +30 -0
  101. package/scaffold/mcp/dist/impactResults.js +105 -0
  102. package/scaffold/mcp/dist/impactSeed.js +20 -0
  103. package/scaffold/mcp/dist/impactTraversal.js +64 -0
  104. package/scaffold/mcp/dist/jsonl.js +77 -0
  105. package/scaffold/mcp/dist/loadGraph.js +759 -0
  106. package/scaffold/mcp/dist/lruCache.js +38 -0
  107. package/scaffold/mcp/dist/paths.js +97 -0
  108. package/scaffold/mcp/dist/patternEvidence.js +272 -0
  109. package/scaffold/mcp/dist/plugin.js +81 -0
  110. package/scaffold/mcp/dist/presets.js +78 -0
  111. package/scaffold/mcp/dist/relatedResponse.js +18 -0
  112. package/scaffold/mcp/dist/relatedTraversal.js +78 -0
  113. package/scaffold/mcp/dist/rules.js +23 -0
  114. package/scaffold/mcp/dist/search.js +212 -0
  115. package/scaffold/mcp/dist/searchCore.js +457 -0
  116. package/scaffold/mcp/dist/searchResults.js +230 -0
  117. package/scaffold/mcp/dist/server.js +317 -0
  118. package/scaffold/mcp/dist/types.js +1 -0
  119. package/scaffold/mcp/package-lock.json +336 -212
  120. package/scaffold/mcp/package.json +15 -6
  121. package/scaffold/mcp/src/cli/enterprise-setup.ts +82 -8
  122. package/scaffold/mcp/src/cli/govern.ts +137 -52
  123. package/scaffold/mcp/src/cli/query.ts +36 -2
  124. package/scaffold/mcp/src/cli/run.ts +53 -19
  125. package/scaffold/mcp/src/cli/stage.ts +8 -3
  126. package/scaffold/mcp/src/core/config.ts +9 -2
  127. package/scaffold/mcp/src/core/enterprise-host-identity.ts +259 -0
  128. package/scaffold/mcp/src/core/enterprise-identity.ts +20 -0
  129. package/scaffold/mcp/src/core/enterprise-rotation.ts +185 -0
  130. package/scaffold/mcp/src/core/govern-paths.ts +30 -0
  131. package/scaffold/mcp/src/core/license.ts +186 -17
  132. package/scaffold/mcp/src/core/secure-endpoint.ts +23 -0
  133. package/scaffold/mcp/src/core/workflow/enforcement.ts +8 -1
  134. package/scaffold/mcp/src/core/workflow/mcp-tools.ts +3 -0
  135. package/scaffold/mcp/src/core/workflow/resolution.ts +9 -1
  136. package/scaffold/mcp/src/core/workflow/synced-capability-registry.ts +15 -0
  137. package/scaffold/mcp/src/core/workflow/synced-registry.ts +15 -0
  138. package/scaffold/mcp/src/daemon/capability-sync-checker.ts +38 -2
  139. package/scaffold/mcp/src/daemon/egress-proxy.ts +14 -8
  140. package/scaffold/mcp/src/daemon/global-host-events.ts +288 -0
  141. package/scaffold/mcp/src/daemon/heartbeat-pusher.ts +4 -0
  142. package/scaffold/mcp/src/daemon/heartbeat-tracker.ts +2 -0
  143. package/scaffold/mcp/src/daemon/host-events-pusher.ts +5 -0
  144. package/scaffold/mcp/src/daemon/main.ts +99 -25
  145. package/scaffold/mcp/src/daemon/project-service-registry.ts +107 -0
  146. package/scaffold/mcp/src/daemon/skill-sync-checker.ts +368 -31
  147. package/scaffold/mcp/src/daemon/sync-checker.ts +4 -0
  148. package/scaffold/mcp/src/daemon/ungoverned-scanner.ts +57 -25
  149. package/scaffold/mcp/src/daemon/workflow-sync-checker.ts +41 -2
  150. package/scaffold/mcp/src/embed.ts +211 -10
  151. package/scaffold/mcp/src/enterprise/audit/push.ts +8 -0
  152. package/scaffold/mcp/src/enterprise/index.ts +1 -1
  153. package/scaffold/mcp/src/enterprise/policy/sync.ts +12 -0
  154. package/scaffold/mcp/src/enterprise/reviews/changed-files.ts +34 -0
  155. package/scaffold/mcp/src/enterprise/reviews/pattern-context.ts +231 -0
  156. package/scaffold/mcp/src/enterprise/reviews/push.ts +9 -0
  157. package/scaffold/mcp/src/enterprise/reviews/trust-state.ts +3 -1
  158. package/scaffold/mcp/src/enterprise/telemetry/sync.ts +9 -0
  159. package/scaffold/mcp/src/enterprise/tools/enterprise.ts +49 -36
  160. package/scaffold/mcp/src/enterprise/violations/push.ts +9 -0
  161. package/scaffold/mcp/src/enterprise/workflow/push.ts +7 -0
  162. package/scaffold/mcp/src/paths.ts +3 -5
  163. package/scaffold/mcp/src/patternEvidence.ts +347 -0
  164. package/scaffold/mcp/src/plugin.ts +20 -0
  165. package/scaffold/mcp/src/search.ts +35 -9
  166. package/scaffold/mcp/src/searchCore.ts +254 -12
  167. package/scaffold/mcp/src/searchResults.ts +96 -9
  168. package/scaffold/mcp/src/types.ts +7 -0
  169. package/scaffold/mcp/tests/changed-files.test.mjs +41 -0
  170. package/scaffold/mcp/tests/copilot-shim.test.mjs +19 -1
  171. package/scaffold/mcp/tests/egress-proxy.test.mjs +37 -0
  172. package/scaffold/mcp/tests/embed-entities.test.mjs +109 -1
  173. package/scaffold/mcp/tests/enterprise-identity-sync.test.mjs +401 -0
  174. package/scaffold/mcp/tests/enterprise-pattern-context.test.mjs +322 -0
  175. package/scaffold/mcp/tests/enterprise-setup.test.mjs +189 -0
  176. package/scaffold/mcp/tests/global-host-events.test.mjs +81 -0
  177. package/scaffold/mcp/tests/govern-install.test.mjs +95 -2
  178. package/scaffold/mcp/tests/govern-repair.test.mjs +20 -3
  179. package/scaffold/mcp/tests/heartbeat-tracker.test.mjs +26 -0
  180. package/scaffold/mcp/tests/license.test.mjs +367 -0
  181. package/scaffold/mcp/tests/paths.test.mjs +11 -3
  182. package/scaffold/mcp/tests/pattern-evidence.test.mjs +321 -0
  183. package/scaffold/mcp/tests/project-service-registry.test.mjs +172 -0
  184. package/scaffold/mcp/tests/query-cli.test.mjs +73 -1
  185. package/scaffold/mcp/tests/review-trust-contract.test.mjs +18 -0
  186. package/scaffold/mcp/tests/search-graph-score.test.mjs +167 -0
  187. package/scaffold/mcp/tests/secure-enterprise-endpoint.test.mjs +46 -0
  188. package/scaffold/mcp/tests/skill-sync-checker.test.mjs +446 -2
  189. package/scaffold/mcp/tests/ungoverned-scanner.test.mjs +104 -22
  190. package/scaffold/mcp/tests/workflow-cli.test.mjs +14 -1
  191. package/scaffold/mcp/tests/workflow-synced-capabilities.test.mjs +37 -0
  192. package/scaffold/mcp/tests/workflow-synced-registry.test.mjs +40 -1
  193. package/scaffold/scripts/dashboard.mjs +29 -18
  194. package/scaffold/scripts/doctor.sh +26 -8
  195. package/scaffold/scripts/ingest.mjs +37 -4
  196. package/scaffold/scripts/status.sh +13 -6
@@ -6,6 +6,11 @@ import {
6
6
  import { hostname } from "node:os";
7
7
  import { join } from "node:path";
8
8
  import { loadEnterpriseConfig } from "../core/config.js";
9
+ import { matchesEnterpriseHostIdentity } from "../core/enterprise-host-identity.js";
10
+ import {
11
+ enterpriseCredentialId,
12
+ isAllowedLicenseEndpoint,
13
+ } from "../core/license.js";
9
14
  import { workflowDefinitionSchema, type WorkflowDefinition } from "../core/workflow/schemas.js";
10
15
  import { writeHostAuditEvent } from "./ungoverned-scanner.js";
11
16
  import { daemonDir } from "./paths.js";
@@ -55,6 +60,7 @@ type LocalWorkflowRecord = {
55
60
  };
56
61
 
57
62
  type LocalWorkflowsState = {
63
+ credential_id?: string;
58
64
  workflows: Record<string, LocalWorkflowRecord>;
59
65
  last_synced_at?: string;
60
66
  };
@@ -79,6 +85,10 @@ export function readSyncedWorkflowsState(): LocalWorkflowsState {
79
85
  try {
80
86
  const parsed = JSON.parse(readFileSync(path, "utf8")) as LocalWorkflowsState;
81
87
  return {
88
+ credential_id:
89
+ typeof parsed.credential_id === "string"
90
+ ? parsed.credential_id
91
+ : undefined,
82
92
  workflows: parsed.workflows ?? {},
83
93
  last_synced_at: parsed.last_synced_at,
84
94
  };
@@ -149,6 +159,28 @@ export async function runWorkflowSyncOnce(
149
159
  await writeAudit(cwd, outcome);
150
160
  return outcome;
151
161
  }
162
+ if (!isAllowedLicenseEndpoint(baseUrl)) {
163
+ const outcome: WorkflowSyncOutcome = {
164
+ kind: "failed",
165
+ error: "insecure or invalid enterprise endpoint",
166
+ };
167
+ await writeAudit(cwd, outcome);
168
+ return outcome;
169
+ }
170
+
171
+ const credentialId = enterpriseCredentialId(baseUrl, apiKey);
172
+ if (!matchesEnterpriseHostIdentity(credentialId)) {
173
+ const outcome: WorkflowSyncOutcome = {
174
+ kind: "failed",
175
+ error:
176
+ "enterprise identity conflict: this user profile is already enrolled to another endpoint or API key",
177
+ };
178
+ await writeAudit(cwd, outcome);
179
+ return outcome;
180
+ }
181
+ const state = readSyncedWorkflowsState();
182
+ const identityChanged = state.credential_id !== credentialId;
183
+ state.credential_id = credentialId;
152
184
 
153
185
  let manifest: ManifestEntry[];
154
186
  try {
@@ -162,7 +194,6 @@ export async function runWorkflowSyncOnce(
162
194
  return outcome;
163
195
  }
164
196
 
165
- const state = readSyncedWorkflowsState();
166
197
  const remoteByName = new Map(manifest.map((e) => [e.workflow_id, e]));
167
198
 
168
199
  const added: string[] = [];
@@ -174,7 +205,11 @@ export async function runWorkflowSyncOnce(
174
205
  const isNew = !local;
175
206
  const isChanged =
176
207
  Boolean(local) &&
177
- (local.updated_at !== entry.updated_at || local.version !== entry.version);
208
+ (
209
+ identityChanged ||
210
+ local.updated_at !== entry.updated_at ||
211
+ local.version !== entry.version
212
+ );
178
213
  if (!isNew && !isChanged) continue;
179
214
 
180
215
  let fetched: FetchedWorkflow;
@@ -224,6 +259,10 @@ export async function runWorkflowSyncOnce(
224
259
 
225
260
  const totalChanged = added.length + changed.length + removed.length;
226
261
  if (totalChanged === 0) {
262
+ if (identityChanged) {
263
+ state.last_synced_at = new Date().toISOString();
264
+ writeSyncedWorkflowsState(state);
265
+ }
227
266
  const outcome: WorkflowSyncOutcome = {
228
267
  kind: "unchanged",
229
268
  count: manifest.length,
@@ -31,6 +31,31 @@ const MODEL_CACHE_DIR = PATHS.embeddingsModelCache;
31
31
  const EMBEDDINGS_DIR = path.dirname(EMBEDDINGS_PATH);
32
32
 
33
33
  export const DEFAULT_MODEL_ID = "jinaai/jina-embeddings-v2-base-code";
34
+ export const COMPACT_FILE_TEXT_STRATEGY = "compact_files_v1";
35
+ export const COMPACT_FILE_TEXT_THRESHOLD_CHARS = 32768;
36
+ export const COMPACT_FILE_TEXT_TARGET_CHARS = 16000;
37
+
38
+ const COMPACT_FILE_SIGNAL_BUDGET_CHARS = 4096;
39
+ const COMPACT_FILE_SIGNAL_MAX_LINE_CHARS = 512;
40
+ const COMPACT_FILE_MIN_HEAD_CHARS = 4096;
41
+ const COMPACT_FILE_MIN_TAIL_CHARS = 2048;
42
+
43
+ export type EmbedTextProfile = "full" | "compact-files";
44
+
45
+ type FileEmbeddingTextResult = {
46
+ text: string;
47
+ profile: EmbedTextProfile;
48
+ compacted: boolean;
49
+ original_chars: number;
50
+ text_chars: number;
51
+ omitted_chars: number;
52
+ };
53
+
54
+ type ParseFileEntitiesOptions = {
55
+ textProfile?: EmbedTextProfile;
56
+ };
57
+
58
+ type SignatureEntityType = "File" | "Rule" | "ADR" | "Module" | "Project" | "Chunk";
34
59
 
35
60
  export function resolveModelId(): string {
36
61
  return (process.env.CORTEX_EMBED_MODEL ?? DEFAULT_MODEL_ID).trim() || DEFAULT_MODEL_ID;
@@ -48,6 +73,11 @@ type FileEntity = {
48
73
  updated_at: string;
49
74
  text: string;
50
75
  signature: string;
76
+ text_profile: EmbedTextProfile;
77
+ text_compacted: boolean;
78
+ text_original_chars: number;
79
+ text_chars: number;
80
+ text_omitted_chars: number;
51
81
  };
52
82
 
53
83
  type RuleEntity = {
@@ -155,6 +185,129 @@ function normalizeText(value: string): string {
155
185
  return value.replace(/\s+/g, " ").trim();
156
186
  }
157
187
 
188
+ export function resolveEmbedTextProfile(raw = process.env.CORTEX_EMBED_TEXT_PROFILE): EmbedTextProfile {
189
+ const value = (raw ?? "").trim().toLowerCase();
190
+ if (!value || value === "full") {
191
+ return "full";
192
+ }
193
+ if (value === "compact-files") {
194
+ return "compact-files";
195
+ }
196
+ throw new Error(
197
+ `Unsupported CORTEX_EMBED_TEXT_PROFILE=${JSON.stringify(raw)}; expected "full" or "compact-files"`
198
+ );
199
+ }
200
+
201
+ function isSignalLine(line: string): boolean {
202
+ const trimmed = line.trim();
203
+ if (!trimmed) {
204
+ return false;
205
+ }
206
+ return (
207
+ /^(import|export)\b/.test(trimmed) ||
208
+ /^(abstract\s+|async\s+|public\s+|private\s+|protected\s+|static\s+|readonly\s+|override\s+)*(class|interface|type|enum|function)\b/.test(trimmed) ||
209
+ /^(const|let|var)\s+[$A-Z_a-z][$\w]*\s*=/.test(trimmed) ||
210
+ /^(describe|it|test)\s*\(/.test(trimmed) ||
211
+ /^(@[A-Z_a-z][$\w]*|#[#\s])/.test(trimmed) ||
212
+ /^```[A-Za-z0-9_-]+/.test(trimmed) ||
213
+ /\b(route|router|endpoint|controller|handler|middleware|permission|auth|token|secret|security|todo|fixme)\b/i.test(trimmed)
214
+ );
215
+ }
216
+
217
+ function collectSignalLines(content: string, budgetChars: number): string {
218
+ const lines: string[] = [];
219
+ const seen = new Set<string>();
220
+ let used = 0;
221
+
222
+ for (const line of content.split(/\r?\n/)) {
223
+ if (!isSignalLine(line)) {
224
+ continue;
225
+ }
226
+ const normalized = line.trimEnd();
227
+ const signalLine = normalized.length > COMPACT_FILE_SIGNAL_MAX_LINE_CHARS
228
+ ? `${normalized.slice(0, COMPACT_FILE_SIGNAL_MAX_LINE_CHARS)} [cortex ${COMPACT_FILE_TEXT_STRATEGY} signal_line_truncated_chars=${normalized.length - COMPACT_FILE_SIGNAL_MAX_LINE_CHARS}]`
229
+ : normalized;
230
+ if (seen.has(signalLine)) {
231
+ continue;
232
+ }
233
+ const next = used + signalLine.length + 1;
234
+ if (next > budgetChars) {
235
+ continue;
236
+ }
237
+ lines.push(signalLine);
238
+ seen.add(signalLine);
239
+ used = next;
240
+ }
241
+
242
+ return lines.join("\n");
243
+ }
244
+
245
+ export function buildFileEmbeddingText(
246
+ filePath: string,
247
+ excerpt: string,
248
+ content: string,
249
+ profile: EmbedTextProfile = "full"
250
+ ): FileEmbeddingTextResult {
251
+ const fullText = `${filePath}\n${excerpt}\n${content}`;
252
+ if (profile === "full" || fullText.length <= COMPACT_FILE_TEXT_THRESHOLD_CHARS) {
253
+ return {
254
+ text: fullText,
255
+ profile,
256
+ compacted: false,
257
+ original_chars: fullText.length,
258
+ text_chars: fullText.length,
259
+ omitted_chars: 0
260
+ };
261
+ }
262
+
263
+ const signalText = collectSignalLines(content, COMPACT_FILE_SIGNAL_BUDGET_CHARS);
264
+ const markerPrefix = `[cortex ${COMPACT_FILE_TEXT_STRATEGY} omitted_chars=`;
265
+ const staticChars =
266
+ filePath.length +
267
+ excerpt.length +
268
+ signalText.length +
269
+ markerPrefix.length +
270
+ 64;
271
+ const available = Math.max(
272
+ COMPACT_FILE_MIN_HEAD_CHARS + COMPACT_FILE_MIN_TAIL_CHARS,
273
+ COMPACT_FILE_TEXT_TARGET_CHARS - staticChars
274
+ );
275
+ const headChars = Math.max(COMPACT_FILE_MIN_HEAD_CHARS, Math.floor(available * 0.62));
276
+ const tailChars = Math.max(COMPACT_FILE_MIN_TAIL_CHARS, available - headChars);
277
+ const head = content.slice(0, headChars);
278
+ const tail = content.slice(-tailChars);
279
+ const omittedChars = Math.max(0, content.length - head.length - tail.length);
280
+ const marker = `${markerPrefix}${omittedChars}]`;
281
+ const compactText = [
282
+ filePath,
283
+ excerpt,
284
+ head,
285
+ marker,
286
+ signalText ? `[cortex ${COMPACT_FILE_TEXT_STRATEGY} signal_lines]\n${signalText}` : "",
287
+ tail
288
+ ].filter((part) => part.length > 0).join("\n");
289
+
290
+ if (compactText.length >= fullText.length) {
291
+ return {
292
+ text: fullText,
293
+ profile,
294
+ compacted: false,
295
+ original_chars: fullText.length,
296
+ text_chars: fullText.length,
297
+ omitted_chars: 0
298
+ };
299
+ }
300
+
301
+ return {
302
+ text: compactText,
303
+ profile,
304
+ compacted: true,
305
+ original_chars: fullText.length,
306
+ text_chars: compactText.length,
307
+ omitted_chars: fullText.length - compactText.length
308
+ };
309
+ }
310
+
158
311
  function ensureRequiredFiles(): void {
159
312
  const required = [
160
313
  path.join(CACHE_DIR, "documents.jsonl"),
@@ -169,7 +322,8 @@ function ensureRequiredFiles(): void {
169
322
  }
170
323
  }
171
324
 
172
- export function parseFileEntities(raw: JsonObject[]): FileEntity[] {
325
+ export function parseFileEntities(raw: JsonObject[], options: ParseFileEntitiesOptions = {}): FileEntity[] {
326
+ const textProfile = options.textProfile ?? "full";
173
327
  return raw
174
328
  .map((item) => {
175
329
  const id = asString(item.id);
@@ -182,7 +336,7 @@ export function parseFileEntities(raw: JsonObject[]): FileEntity[] {
182
336
  const excerpt = asString(item.excerpt);
183
337
  const updatedAt = asString(item.updated_at);
184
338
  const checksum = asString(item.checksum, hashText(content));
185
- const text = `${filePath}\n${excerpt}\n${content}`;
339
+ const embeddingText = buildFileEmbeddingText(filePath, excerpt, content, textProfile);
186
340
 
187
341
  return {
188
342
  id,
@@ -194,8 +348,13 @@ export function parseFileEntities(raw: JsonObject[]): FileEntity[] {
194
348
  source_of_truth: asBoolean(item.source_of_truth, false),
195
349
  trust_level: asNumber(item.trust_level, 50),
196
350
  updated_at: updatedAt,
197
- text,
198
- signature: hashText(`file|${checksum}|${updatedAt}|${hashText(text)}`)
351
+ text: embeddingText.text,
352
+ signature: hashText(`file|${checksum}|${updatedAt}|${hashText(embeddingText.text)}`),
353
+ text_profile: embeddingText.profile,
354
+ text_compacted: embeddingText.compacted,
355
+ text_original_chars: embeddingText.original_chars,
356
+ text_chars: embeddingText.text_chars,
357
+ text_omitted_chars: embeddingText.omitted_chars
199
358
  };
200
359
  })
201
360
  .filter((value): value is FileEntity => value !== null);
@@ -408,8 +567,24 @@ function roundVector(values: number[]): number[] {
408
567
  return values.map((value) => Number(value.toFixed(6)));
409
568
  }
410
569
 
411
- function resolveSignatureProfile(maxTokenCap: number | null): string {
412
- return maxTokenCap ? `embed|max_tokens=${maxTokenCap}` : "";
570
+ export function resolveSignatureProfile(
571
+ maxTokenCap: number | null,
572
+ textProfile: EmbedTextProfile = "full",
573
+ entityType?: SignatureEntityType
574
+ ): string {
575
+ const parts: string[] = [];
576
+ if (maxTokenCap) {
577
+ parts.push(`max_tokens=${maxTokenCap}`);
578
+ }
579
+ if (textProfile === "compact-files" && (entityType === undefined || entityType === "File")) {
580
+ parts.push(
581
+ "text_profile=compact-files",
582
+ COMPACT_FILE_TEXT_STRATEGY,
583
+ `threshold_chars=${COMPACT_FILE_TEXT_THRESHOLD_CHARS}`,
584
+ `target_chars=${COMPACT_FILE_TEXT_TARGET_CHARS}`
585
+ );
586
+ }
587
+ return parts.length ? `embed|${parts.join("|")}` : "";
413
588
  }
414
589
 
415
590
  function embeddingSignature(entitySignature: string, profile: string): string {
@@ -434,8 +609,19 @@ async function main(): Promise<void> {
434
609
  fs.mkdirSync(MODEL_CACHE_DIR, { recursive: true });
435
610
 
436
611
  const modelId = resolveModelId();
437
-
438
- const documents = parseFileEntities(readJsonl(path.join(CACHE_DIR, "documents.jsonl")));
612
+ const textProfile = resolveEmbedTextProfile();
613
+
614
+ const documents = parseFileEntities(readJsonl(path.join(CACHE_DIR, "documents.jsonl")), { textProfile });
615
+ const textProfileStats = {
616
+ strategy: textProfile === "compact-files" ? COMPACT_FILE_TEXT_STRATEGY : null,
617
+ threshold_chars: textProfile === "compact-files" ? COMPACT_FILE_TEXT_THRESHOLD_CHARS : null,
618
+ target_chars: textProfile === "compact-files" ? COMPACT_FILE_TEXT_TARGET_CHARS : null,
619
+ file_entities: documents.length,
620
+ compacted_files: documents.filter((doc) => doc.text_compacted).length,
621
+ original_chars: documents.reduce((total, doc) => total + doc.text_original_chars, 0),
622
+ text_chars: documents.reduce((total, doc) => total + doc.text_chars, 0),
623
+ saved_chars: documents.reduce((total, doc) => total + doc.text_omitted_chars, 0)
624
+ };
439
625
  const rules = parseRuleEntities(readJsonl(path.join(CACHE_DIR, "entities.rule.jsonl")));
440
626
  const adrs = parseAdrEntities(readJsonl(path.join(CACHE_DIR, "entities.adr.jsonl")));
441
627
  const modules = parseModuleEntities(readJsonl(path.join(CACHE_DIR, "entities.module.jsonl")));
@@ -483,7 +669,10 @@ async function main(): Promise<void> {
483
669
  memoryBytes: memoryHeadroom,
484
670
  sessions: previewPoolConfig.sessions
485
671
  });
486
- const signatureProfile = resolveSignatureProfile(tokenBudget.cap);
672
+ const defaultSignatureProfile = resolveSignatureProfile(tokenBudget.cap, "full");
673
+ const fileSignatureProfile = resolveSignatureProfile(tokenBudget.cap, textProfile, "File");
674
+ const signatureProfileForEntity = (entity: SearchEntity) =>
675
+ entity.type === "File" ? fileSignatureProfile : defaultSignatureProfile;
487
676
 
488
677
  const existing = parseExistingEmbeddings(readJsonlRecords(EMBEDDINGS_PATH), modelId);
489
678
 
@@ -494,6 +683,7 @@ async function main(): Promise<void> {
494
683
  let dimensions = 0;
495
684
 
496
685
  entities.forEach((entity, index) => {
686
+ const signatureProfile = signatureProfileForEntity(entity);
497
687
  const signature = embeddingSignature(entity.signature, signatureProfile);
498
688
  const previous = existing.get(entity.id);
499
689
  if (previous && previous.signature === signature && previous.vector.length > 0) {
@@ -628,6 +818,7 @@ async function main(): Promise<void> {
628
818
  maxInFlightTokens,
629
819
  onVector(index, rawVector) {
630
820
  const entity = entities[index];
821
+ const signatureProfile = signatureProfileForEntity(entity);
631
822
  const vector = roundVector(rawVector);
632
823
  embedded += 1;
633
824
  dimensions = dimensions || vector.length;
@@ -662,6 +853,13 @@ async function main(): Promise<void> {
662
853
  mode,
663
854
  model: modelId,
664
855
  dimensions,
856
+ text_profile: textProfile,
857
+ signature_profile: fileSignatureProfile === defaultSignatureProfile ? defaultSignatureProfile : "per_entity",
858
+ signature_profiles: {
859
+ default: defaultSignatureProfile,
860
+ file: fileSignatureProfile
861
+ },
862
+ text_profile_stats: textProfileStats,
665
863
  counts: {
666
864
  entities: entities.length,
667
865
  output: outputCount,
@@ -675,7 +873,10 @@ async function main(): Promise<void> {
675
873
  fs.writeFileSync(EMBEDDINGS_MANIFEST_PATH, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
676
874
 
677
875
  console.log(
678
- `[embed] mode=${mode} model=${modelId} dim=${dimensions} pool=${poolConfig.sessions}x${poolConfig.threadsPerSession} batch<=${schedulerOptions.batchMaxItems} max_tokens<=${modelMaxTokensUsed || tokenBudget.cap || "model"} token_budget=${tokenBudget.mode} reason=${tokenBudget.reason}`
876
+ `[embed] mode=${mode} model=${modelId} dim=${dimensions} pool=${poolConfig.sessions}x${poolConfig.threadsPerSession} batch<=${schedulerOptions.batchMaxItems} max_tokens<=${modelMaxTokensUsed || tokenBudget.cap || "model"} token_budget=${tokenBudget.mode} reason=${tokenBudget.reason} text_profile=${textProfile}`
877
+ );
878
+ console.log(
879
+ `[embed] text_profile=${textProfile} compacted_files=${textProfileStats.compacted_files}/${textProfileStats.file_entities} saved_chars=${textProfileStats.saved_chars}`
679
880
  );
680
881
  console.log(
681
882
  `[embed] entities=${entities.length} embedded=${embedded} reused=${reused} failed=${failed}`
@@ -1,4 +1,5 @@
1
1
  import type { AuditEntry } from "../../core/audit/writer.js";
2
+ import { isAllowedEnterpriseEndpoint } from "../../core/secure-endpoint.js";
2
3
  import { sanitizeAuditEntryForPush } from "../privacy/boundary.js";
3
4
 
4
5
  export type AuditPushContext = {
@@ -40,6 +41,13 @@ export async function pushAuditEvents(
40
41
  if (pending.length === 0) {
41
42
  return { success: true, count: 0 };
42
43
  }
44
+ if (!isAllowedEnterpriseEndpoint(baseUrl)) {
45
+ return {
46
+ success: false,
47
+ count: 0,
48
+ error: "insecure or invalid enterprise endpoint",
49
+ };
50
+ }
43
51
 
44
52
  const auditUrl = `${baseUrl.replace(/\/$/, "")}/api/v1/audit/push`;
45
53
  let pushedCount = 0;
@@ -21,7 +21,7 @@ import { pushWorkflowSnapshot, setWorkflowPushContext } from "./workflow/push.js
21
21
  import { hasWorkflowState, loadWorkflowState } from "./workflow/state.js";
22
22
 
23
23
  const require = createRequire(import.meta.url);
24
- const pkg = require("../package.json") as { version: string };
24
+ const pkg = require("../../package.json") as { version: string };
25
25
 
26
26
  export const name = "cortex-enterprise";
27
27
  export const version: string = pkg.version;
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import type { OrgPolicy, PolicyStore } from "../../core/policy/store.js";
3
+ import { isAllowedEnterpriseEndpoint } from "../../core/secure-endpoint.js";
3
4
 
4
5
  const CloudPolicySchema = z.object({
5
6
  id: z.string().min(1).max(200),
@@ -60,6 +61,17 @@ export async function syncFromCloud(
60
61
  lastSync = result;
61
62
  return result;
62
63
  }
64
+ if (!isAllowedEnterpriseEndpoint(endpoint)) {
65
+ const result: SyncResult = {
66
+ success: false,
67
+ synced: 0,
68
+ source: "cloud",
69
+ timestamp: new Date().toISOString(),
70
+ error: "insecure or invalid enterprise endpoint",
71
+ };
72
+ lastSync = result;
73
+ return result;
74
+ }
63
75
 
64
76
  try {
65
77
  const response = await fetch(endpoint, {
@@ -0,0 +1,34 @@
1
+ import { execFileSync } from "node:child_process";
2
+
3
+ function gitLines(projectRoot: string, args: string[]): string[] {
4
+ const output = execFileSync("git", args, {
5
+ cwd: projectRoot,
6
+ encoding: "utf8",
7
+ timeout: 5000,
8
+ stdio: ["ignore", "pipe", "ignore"],
9
+ });
10
+ return output.split("\n").map((value) => value.trim()).filter(Boolean);
11
+ }
12
+
13
+ function trackedChangedFiles(projectRoot: string): string[] {
14
+ try {
15
+ return gitLines(projectRoot, ["diff", "--name-only", "HEAD"]);
16
+ } catch {
17
+ // No HEAD yet (repo without commits): every tracked file is new.
18
+ return [
19
+ ...gitLines(projectRoot, ["diff", "--name-only"]),
20
+ ...gitLines(projectRoot, ["ls-files"]),
21
+ ];
22
+ }
23
+ }
24
+
25
+ export function resolveChangedReviewFiles(projectRoot: string): string[] | null {
26
+ try {
27
+ gitLines(projectRoot, ["rev-parse", "--is-inside-work-tree"]);
28
+ const tracked = trackedChangedFiles(projectRoot);
29
+ const untracked = gitLines(projectRoot, ["ls-files", "--others", "--exclude-standard"]);
30
+ return [...new Set([...tracked, ...untracked])].sort();
31
+ } catch {
32
+ return null;
33
+ }
34
+ }