pi-mega-compact 0.20.49 → 0.20.51

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 (85) hide show
  1. package/dist/config/vector-cortex-enc1a.js +39 -0
  2. package/dist/config/vector-cortex-enc1b.js +46 -0
  3. package/dist/config/vector-cortex.js +2 -0
  4. package/dist/config.js +1 -1
  5. package/dist/extensions/dashboard-server/routes-rag-settings-vector-cortex.js +2 -0
  6. package/dist/extensions/dashboard-server/routes-setup-enc1a.js +157 -0
  7. package/dist/extensions/dashboard-server/routes-setup-enc1b.js +297 -0
  8. package/dist/extensions/dashboard-server/routes-setup-env-upsert.js +79 -0
  9. package/dist/extensions/dashboard-server/routes-setup.js +47 -25
  10. package/dist/src/config/vector-cortex-enc1a.js +39 -0
  11. package/dist/src/config/vector-cortex-enc1b.js +46 -0
  12. package/dist/src/config/vector-cortex.js +2 -0
  13. package/dist/src/config.js +1 -1
  14. package/extensions/dashboard-client/dist/assets/{AreaChart-COnj8FHi.js → AreaChart-BFTUTmmW.js} +2 -2
  15. package/extensions/dashboard-client/dist/assets/{AreaChart-COnj8FHi.js.map → AreaChart-BFTUTmmW.js.map} +1 -1
  16. package/extensions/dashboard-client/dist/assets/{BarChart-BYqt8c5-.js → BarChart-DfFurESR.js} +2 -2
  17. package/extensions/dashboard-client/dist/assets/{BarChart-BYqt8c5-.js.map → BarChart-DfFurESR.js.map} +1 -1
  18. package/extensions/dashboard-client/dist/assets/{CacheTab-PLRjMrZV.js → CacheTab-FpCJSmy8.js} +2 -2
  19. package/extensions/dashboard-client/dist/assets/{CacheTab-PLRjMrZV.js.map → CacheTab-FpCJSmy8.js.map} +1 -1
  20. package/extensions/dashboard-client/dist/assets/{EventsTab-DXKSft6w.js → EventsTab--9lJFJtc.js} +2 -2
  21. package/extensions/dashboard-client/dist/assets/{EventsTab-DXKSft6w.js.map → EventsTab--9lJFJtc.js.map} +1 -1
  22. package/extensions/dashboard-client/dist/assets/{HealthTab-BgVIsrde.js → HealthTab-CkgtF1qa.js} +2 -2
  23. package/extensions/dashboard-client/dist/assets/{HealthTab-BgVIsrde.js.map → HealthTab-CkgtF1qa.js.map} +1 -1
  24. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-DNgFC8KZ.js → MaintenanceTab-ChXGq1or.js} +2 -2
  25. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-DNgFC8KZ.js.map → MaintenanceTab-ChXGq1or.js.map} +1 -1
  26. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-B6yY47sz.js → MemoryMapTab-CPpRR4MV.js} +2 -2
  27. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-B6yY47sz.js.map → MemoryMapTab-CPpRR4MV.js.map} +1 -1
  28. package/extensions/dashboard-client/dist/assets/{MetricsTab-BrhJ6rkx.js → MetricsTab-Db1wkykf.js} +2 -2
  29. package/extensions/dashboard-client/dist/assets/{MetricsTab-BrhJ6rkx.js.map → MetricsTab-Db1wkykf.js.map} +1 -1
  30. package/extensions/dashboard-client/dist/assets/{OverviewTab-RFHU9L4x.js → OverviewTab-LanDECrz.js} +2 -2
  31. package/extensions/dashboard-client/dist/assets/{OverviewTab-RFHU9L4x.js.map → OverviewTab-LanDECrz.js.map} +1 -1
  32. package/extensions/dashboard-client/dist/assets/{ReposTab-UFIWKkJM.js → ReposTab-I2umKduk.js} +2 -2
  33. package/extensions/dashboard-client/dist/assets/{ReposTab-UFIWKkJM.js.map → ReposTab-I2umKduk.js.map} +1 -1
  34. package/extensions/dashboard-client/dist/assets/{SessionsTab-BHFYsmK8.js → SessionsTab-D_IxuEt5.js} +2 -2
  35. package/extensions/dashboard-client/dist/assets/{SessionsTab-BHFYsmK8.js.map → SessionsTab-D_IxuEt5.js.map} +1 -1
  36. package/extensions/dashboard-client/dist/assets/SetupTab-BZ7Jn5MR.js +3 -0
  37. package/extensions/dashboard-client/dist/assets/SetupTab-BZ7Jn5MR.js.map +1 -0
  38. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-aPG5Knqn.js → TimeSavedCard-BTdcO1E2.js} +2 -2
  39. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-aPG5Knqn.js.map → TimeSavedCard-BTdcO1E2.js.map} +1 -1
  40. package/extensions/dashboard-client/dist/assets/{TurnsTab-fGm9n-Tk.js → TurnsTab-BXC8IL_B.js} +2 -2
  41. package/extensions/dashboard-client/dist/assets/{TurnsTab-fGm9n-Tk.js.map → TurnsTab-BXC8IL_B.js.map} +1 -1
  42. package/extensions/dashboard-client/dist/assets/{VcStatusBadge-BtmSNH8X.js → VcStatusBadge-CNJnFtJo.js} +2 -2
  43. package/extensions/dashboard-client/dist/assets/{VcStatusBadge-BtmSNH8X.js.map → VcStatusBadge-CNJnFtJo.js.map} +1 -1
  44. package/extensions/dashboard-client/dist/assets/{VectorCortexTab-meALMMOZ.js → VectorCortexTab-fsuWgA6e.js} +2 -2
  45. package/extensions/dashboard-client/dist/assets/{VectorCortexTab-meALMMOZ.js.map → VectorCortexTab-fsuWgA6e.js.map} +1 -1
  46. package/extensions/dashboard-client/dist/assets/{WikiTab-0TK6q6g1.js → WikiTab-Cn1BScDU.js} +2 -2
  47. package/extensions/dashboard-client/dist/assets/{WikiTab-0TK6q6g1.js.map → WikiTab-Cn1BScDU.js.map} +1 -1
  48. package/extensions/dashboard-client/dist/assets/{button-CKEnMZ98.js → button-B4_9rsul.js} +2 -2
  49. package/extensions/dashboard-client/dist/assets/{button-CKEnMZ98.js.map → button-B4_9rsul.js.map} +1 -1
  50. package/extensions/dashboard-client/dist/assets/{card-BVVhm3sL.js → card-DUvC_geI.js} +2 -2
  51. package/extensions/dashboard-client/dist/assets/{card-BVVhm3sL.js.map → card-DUvC_geI.js.map} +1 -1
  52. package/extensions/dashboard-client/dist/assets/{client-extra-BvGxKZpm.js → client-extra-HaH97bzp.js} +2 -2
  53. package/extensions/dashboard-client/dist/assets/{client-extra-BvGxKZpm.js.map → client-extra-HaH97bzp.js.map} +1 -1
  54. package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-B1PeCct1.js → generateCategoricalChart-BxFWU2Mt.js} +2 -2
  55. package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-B1PeCct1.js.map → generateCategoricalChart-BxFWU2Mt.js.map} +1 -1
  56. package/extensions/dashboard-client/dist/assets/index-C_aDd9kk.css +1 -0
  57. package/extensions/dashboard-client/dist/assets/{index-DuThOA-d.js → index-xPdbZ73U.js} +3 -3
  58. package/extensions/dashboard-client/dist/assets/{index-DuThOA-d.js.map → index-xPdbZ73U.js.map} +1 -1
  59. package/extensions/dashboard-client/dist/assets/{switch-7qLS7ajc.js → switch-Bv3_1R_j.js} +2 -2
  60. package/extensions/dashboard-client/dist/assets/{switch-7qLS7ajc.js.map → switch-Bv3_1R_j.js.map} +1 -1
  61. package/extensions/dashboard-client/dist/assets/{toggle-D7mgOVxz.js → toggle-DHXrB3Aw.js} +2 -2
  62. package/extensions/dashboard-client/dist/assets/{toggle-D7mgOVxz.js.map → toggle-DHXrB3Aw.js.map} +1 -1
  63. package/extensions/dashboard-client/dist/assets/{useSSE-ChpyZVeX.js → useSSE-NigHeFyT.js} +2 -2
  64. package/extensions/dashboard-client/dist/assets/{useSSE-ChpyZVeX.js.map → useSSE-NigHeFyT.js.map} +1 -1
  65. package/extensions/dashboard-client/dist/index.html +2 -2
  66. package/extensions/dashboard-client/src/tabs/SetupTab/CortexRuntimeCard.tsx +103 -0
  67. package/extensions/dashboard-client/src/tabs/SetupTab/CortexRuntimeCardStyles.ts +49 -0
  68. package/extensions/dashboard-client/src/tabs/SetupTab/CortexSetup.tsx +2 -0
  69. package/extensions/dashboard-client/src/tabs/SetupTab/CustomEndpointSection.tsx +178 -2
  70. package/extensions/dashboard-client/src/tabs/SetupTab/EmbedderSetup.tsx +61 -113
  71. package/extensions/dashboard-client/src/tabs/SetupTab/EmbedderSetupHelpers.tsx +125 -0
  72. package/extensions/dashboard-server/api-contracts/setup.ts +47 -0
  73. package/extensions/dashboard-server/routes-rag-settings-vector-cortex.ts +12 -0
  74. package/extensions/dashboard-server/routes-setup-enc1a.ts +184 -0
  75. package/extensions/dashboard-server/routes-setup-enc1b.ts +336 -0
  76. package/extensions/dashboard-server/routes-setup-env-upsert.ts +93 -0
  77. package/extensions/dashboard-server/routes-setup.ts +46 -23
  78. package/package.json +1 -1
  79. package/src/config/vector-cortex-enc1a.ts +41 -0
  80. package/src/config/vector-cortex-enc1b.ts +50 -0
  81. package/src/config/vector-cortex.ts +9 -0
  82. package/src/config.ts +2 -0
  83. package/extensions/dashboard-client/dist/assets/SetupTab-C3dnKa8y.js +0 -3
  84. package/extensions/dashboard-client/dist/assets/SetupTab-C3dnKa8y.js.map +0 -1
  85. package/extensions/dashboard-client/dist/assets/index-DbuGwYD7.css +0 -1
@@ -0,0 +1,184 @@
1
+ /**
2
+ * dashboard-server/routes-setup-enc1a.ts — ENC-1a external-embedder Settings
3
+ * read/write + the additive writer branch.
4
+ *
5
+ * Extracted out of routes-setup.ts (which hovers at the 300-line source soft
6
+ * cap) so the ENC-1a additive read/write of the per-repo `.mega-compact.env`
7
+ * keys AND the pure external-embedder configure branch live in a sibling impl
8
+ * file. The two keys are `MEGACOMPACT_EMBEDDING_URL` and
9
+ * `MEGACOMPACT_EMBEDDING_KEY` — the exact names `embeddingConfigFromEnv`
10
+ * (src/httpEmbedder.ts) reads at runtime.
11
+ *
12
+ * The write is create-or-append: it upserts the two keys into the existing
13
+ * per-repo `.mega-compact.env`, preserving every unrelated line and never
14
+ * deleting other keys. The read reports the endpoint URL (echoed) and an
15
+ * `apiKeySet` boolean ONLY — the raw API key is never returned, logged, or
16
+ * emitted (redaction invariant).
17
+ *
18
+ * Guardrails: PREVENT-PI-004 (local filesystem writes only, zero network),
19
+ * PREVENT-001 (null-safe JSON), PREVENT-011 (no `any`).
20
+ */
21
+
22
+ import { writeFileSync, mkdirSync, readFileSync, existsSync } from "node:fs";
23
+ import { join } from "node:path";
24
+ import type { ServerResponse } from "node:http";
25
+ import type { RouteContext } from "./routes-core.js";
26
+ import { ENC_1A_ENABLED } from "../../src/config.js";
27
+ import type {
28
+ SetupConfigureRequest,
29
+ SetupConfigureResponse,
30
+ } from "./api-contracts/setup.js";
31
+
32
+ const URL_KEY = "MEGACOMPACT_EMBEDDING_URL";
33
+ const API_KEY = "MEGACOMPACT_EMBEDDING_KEY";
34
+
35
+ function envPath(stateDir: string): string {
36
+ return join(stateDir, ".mega-compact.env");
37
+ }
38
+
39
+ /** Extract the value of `export MEGACOMPACT_X="..."` (or unquoted) from a line. */
40
+ function lineValue(line: string, key: string): string | null {
41
+ const m = line.match(new RegExp(`^export\\s+${key}=(.*)$`));
42
+ if (!m) return null;
43
+ const rest = m[1].trim();
44
+ const q = rest.match(/^"([^"]*)"$/);
45
+ return q ? q[1] : rest;
46
+ }
47
+
48
+ /** True when the payload carries the ENC-1a keys (flag-gated). */
49
+ export function wantsEnc1a(body: SetupConfigureRequest): boolean {
50
+ return (
51
+ ENC_1A_ENABLED() &&
52
+ (typeof body.embeddingEndpointUrl === "string" ||
53
+ typeof body.embeddingApiKey === "string")
54
+ );
55
+ }
56
+
57
+ /** Additive GET status fields for ENC-1a (both omitted when flag-off): the
58
+ * endpoint URL (echoed) + an apiKeySet boolean (true/false) ONLY — never the
59
+ * raw key. The boolean is always present when the flag is on so an absent key
60
+ * is reported as `embeddingApiKeySet:false` (non-fatal, triad B). */
61
+ export function enc1aStatusFields(stateDir: string): {
62
+ embeddingEndpointUrl?: string;
63
+ embeddingApiKeySet?: boolean;
64
+ } {
65
+ if (!ENC_1A_ENABLED()) return {};
66
+ const enc1a = readEnc1aEnv(stateDir);
67
+ return {
68
+ ...(enc1a.endpointUrl !== null ? { embeddingEndpointUrl: enc1a.endpointUrl } : {}),
69
+ embeddingApiKeySet: enc1a.apiKeySet,
70
+ };
71
+ }
72
+
73
+ /** Read the ENC-1a keys from the per-repo `.mega-compact.env` (never the key
74
+ * itself to callers — only its presence). */
75
+ export function readEnc1aEnv(stateDir: string): {
76
+ endpointUrl: string | null;
77
+ apiKeySet: boolean;
78
+ } {
79
+ try {
80
+ const p = envPath(stateDir);
81
+ if (!existsSync(p)) return { endpointUrl: null, apiKeySet: false };
82
+ const content = readFileSync(p, "utf8");
83
+ const lines = content.split(/\r?\n/);
84
+ let endpointUrl: string | null = null;
85
+ let apiKeySet = false;
86
+ for (const line of lines) {
87
+ const url = lineValue(line, URL_KEY);
88
+ if (url !== null) endpointUrl = url;
89
+ const key = lineValue(line, API_KEY);
90
+ if (key !== null && key.length > 0) apiKeySet = true;
91
+ }
92
+ return { endpointUrl, apiKeySet };
93
+ } catch {
94
+ return { endpointUrl: null, apiKeySet: false };
95
+ }
96
+ }
97
+
98
+ /** Upsert the ENC-1a keys into the per-repo `.mega-compact.env`. Creates the
99
+ * file if absent; preserves every unrelated line; never deletes other keys.
100
+ * Passing `null` for a key leaves that line untouched. */
101
+ export function writeEnc1aEnv(
102
+ stateDir: string,
103
+ entries: { endpointUrl?: string | null; apiKey?: string | null },
104
+ ): string {
105
+ const p = envPath(stateDir);
106
+ const existingLines: string[] = existsSync(p)
107
+ ? readFileSync(p, "utf8").split(/\r?\n/)
108
+ : [];
109
+ const out: string[] = [];
110
+ let urlWritten = false;
111
+ let keyWritten = false;
112
+ for (const line of existingLines) {
113
+ if (entries.endpointUrl !== null && lineValue(line, URL_KEY) !== null) {
114
+ out.push(`export ${URL_KEY}="${entries.endpointUrl ?? ""}"`);
115
+ urlWritten = true;
116
+ continue;
117
+ }
118
+ if (entries.apiKey !== null && lineValue(line, API_KEY) !== null) {
119
+ out.push(`export ${API_KEY}="${entries.apiKey ?? ""}"`);
120
+ keyWritten = true;
121
+ continue;
122
+ }
123
+ out.push(line);
124
+ }
125
+ if (entries.endpointUrl !== null && !urlWritten) {
126
+ out.push(`export ${URL_KEY}="${entries.endpointUrl}"`);
127
+ }
128
+ if (entries.apiKey !== null && !keyWritten) {
129
+ out.push(`export ${API_KEY}="${entries.apiKey ?? ""}"`);
130
+ }
131
+ mkdirSync(stateDir, { recursive: true });
132
+ writeFileSync(p, out.join("\n"), "utf-8");
133
+ return p;
134
+ }
135
+
136
+ /** If the payload is a pure ENC-1a configure (new keys, no valid embedder
137
+ * selection), write them additively to the per-repo env and reply. Returns
138
+ * true when it fully handled the request. */
139
+ export function tryEnc1aConfigure(
140
+ body: SetupConfigureRequest,
141
+ res: ServerResponse,
142
+ ctx: RouteContext,
143
+ ): boolean {
144
+ if (!wantsEnc1a(body)) return false;
145
+ const embedder = body.embedder;
146
+ const embedderValid =
147
+ !!embedder &&
148
+ (embedder === "ollama" || embedder === "llama" || embedder === "trigram" ||
149
+ embedder === "custom" || embedder === "onnx");
150
+ if (embedderValid) return false; // combined payload — integrated after the embedder write
151
+ try {
152
+ const envPath = writeEnc1aEnv(ctx.stateDir, {
153
+ endpointUrl:
154
+ typeof body.embeddingEndpointUrl === "string" && body.embeddingEndpointUrl
155
+ ? body.embeddingEndpointUrl
156
+ : null,
157
+ apiKey: typeof body.embeddingApiKey === "string" ? body.embeddingApiKey : null,
158
+ });
159
+ const currentUrl = process.env["MEGACOMPACT_EMBEDDING_URL"];
160
+ const alreadyActive =
161
+ typeof body.embeddingEndpointUrl === "string" &&
162
+ currentUrl === body.embeddingEndpointUrl;
163
+ const resp: SetupConfigureResponse = {
164
+ embedder: "custom",
165
+ url:
166
+ typeof body.embeddingEndpointUrl === "string"
167
+ ? body.embeddingEndpointUrl
168
+ : null,
169
+ envPath,
170
+ restartRequired: !alreadyActive,
171
+ alreadyActive,
172
+ };
173
+ // guardrails-allow PREVENT-PI-004: loopback dashboard response (local)
174
+ res.writeHead(200, { "Content-Type": "application/json" });
175
+ res.end(JSON.stringify(resp));
176
+ return true;
177
+ } catch (e) {
178
+ res.writeHead(500, { "Content-Type": "application/json" });
179
+ res.end(
180
+ JSON.stringify({ error: `write_failed: ${e instanceof Error ? e.message : String(e)}` }),
181
+ );
182
+ return true;
183
+ }
184
+ }
@@ -0,0 +1,336 @@
1
+ /**
2
+ * dashboard-server/routes-setup-enc1b.ts — ENC-1b embedder API completion +
3
+ * ONNX runtime selection Settings read/write + the additive writer branch.
4
+ *
5
+ * Extracted out of routes-setup.ts (which hovers at the 300-line source soft
6
+ * cap, mirroring the ENC-1a sibling extract) so the ENC-1b additive read/write
7
+ * of the per-repo `.mega-compact.env` keys AND the runtime-backend computation
8
+ * live in a sibling impl file. Four keys are managed here:
9
+ * `MEGACOMPACT_EMBEDDING_DIM`, `MEGACOMPACT_EMBEDDING_HEADERS`,
10
+ * `MEGACOMPACT_ALLOW_REMOTE_EMBEDDER`, `MEGACOMPACT_ENCODER_NATIVE` — the exact
11
+ * names `embeddingConfigFromEnv` (src/httpEmbedder.ts) and the runtime selector
12
+ * read at runtime.
13
+ *
14
+ * The write is create-or-append: it upserts the four keys into the existing
15
+ * per-repo `.mega-compact.env`, preserving every unrelated line and never
16
+ * deleting another key. The read reports the persisted dim, booleans for
17
+ * headers-set / allow-remote / native-opt-in, and computes the current
18
+ * effective backend + demotion reason through the EXISTING `selectRuntimeBackend`
19
+ * (reader-only — the runtime's own selection, never a reimplemented literal,
20
+ * never install logic). The raw headers JSON is NEVER returned, logged or
21
+ * emitted (redaction invariant — it may carry secrets such as an Authorization
22
+ * header for a TEI/ONNX server).
23
+ *
24
+ * Guardrails: PREVENT-PI-004 (local filesystem reads/writes + in-memory
25
+ * runtime selection only, zero network), PREVENT-001 (null-safe JSON),
26
+ * PREVENT-011 (no `any`).
27
+ */
28
+
29
+ import { writeFileSync, mkdirSync, readFileSync, existsSync } from "node:fs";
30
+ import { join } from "node:path";
31
+ import type { ServerResponse } from "node:http";
32
+ import type { RouteContext } from "./routes-core.js";
33
+ import {
34
+ ENC_1B_ENABLED,
35
+ ENC_1B_MAX_EMBEDDING_DIM,
36
+ ENC_1B_EMBEDDING_DIM_ENV,
37
+ ENC_1B_EMBEDDING_HEADERS_ENV,
38
+ ENC_1B_ALLOW_REMOTE_EMBEDDER_ENV,
39
+ ENC_1B_ENCODER_NATIVE_ENV,
40
+ } from "../../src/config/vector-cortex.js";
41
+ import type { SetupConfigureRequest, SetupConfigureResponse } from "./api-contracts/setup.js";
42
+ import { selectRuntimeBackend } from "../../src/vector-cortex/encoder/runtime-select.js";
43
+ import { detectPlatform } from "../../src/vector-cortex/encoder/asset.js";
44
+
45
+ function envPath(stateDir: string): string {
46
+ return join(stateDir, ".mega-compact.env");
47
+ }
48
+
49
+ /** Extract the value of `export MEGACOMPACT_X="..."` (or unquoted) from a line. */
50
+ function lineValue(line: string, key: string): string | null {
51
+ const m = line.match(new RegExp(`^export\\s+${key}=(.*)$`));
52
+ if (!m) return null;
53
+ const rest = m[1].trim();
54
+ if (rest.length === 0) return null;
55
+ const q = rest.match(/^"([^"]*)"$/);
56
+ return q ? q[1] : rest;
57
+ }
58
+
59
+ /** True when the payload carries any ENC-1b key (flag-gated). */
60
+ export function wantsEnc1b(body: SetupConfigureRequest): boolean {
61
+ return (
62
+ ENC_1B_ENABLED() &&
63
+ (typeof body.embeddingDim === "string" ||
64
+ typeof body.embeddingHeaders === "string" ||
65
+ typeof body.allowRemoteEmbedder === "boolean" ||
66
+ typeof body.encoderNativeOptIn === "boolean")
67
+ );
68
+ }
69
+
70
+ /** Read the ENC-1b keys from the per-repo `.mega-compact.env`. The headers
71
+ * value is NEVER surfaced to callers — only its presence boolean. */
72
+ export function readEnc1bEnv(stateDir: string): {
73
+ dim: string | null;
74
+ headersSet: boolean;
75
+ allowRemote: boolean;
76
+ nativeOptIn: boolean;
77
+ } {
78
+ try {
79
+ const p = envPath(stateDir);
80
+ if (!existsSync(p)) return { dim: null, headersSet: false, allowRemote: false, nativeOptIn: false };
81
+ const lines = readFileSync(p, "utf8").split(/\r?\n/);
82
+ let dim: string | null = null;
83
+ let headersSet = false;
84
+ let allowRemote = false;
85
+ let nativeOptIn = false;
86
+ for (const line of lines) {
87
+ const d = lineValue(line, ENC_1B_EMBEDDING_DIM_ENV);
88
+ if (d !== null && d.length > 0) dim = d;
89
+ const h = lineValue(line, ENC_1B_EMBEDDING_HEADERS_ENV);
90
+ if (h !== null && h.length > 0) headersSet = true;
91
+ const a = lineValue(line, ENC_1B_ALLOW_REMOTE_EMBEDDER_ENV);
92
+ if (a !== null && ["1", "true", "yes"].includes(a.trim().toLowerCase())) allowRemote = true;
93
+ const n = lineValue(line, ENC_1B_ENCODER_NATIVE_ENV);
94
+ if (n !== null && ["1", "true", "yes"].includes(n.trim().toLowerCase())) nativeOptIn = true;
95
+ }
96
+ return { dim, headersSet, allowRemote, nativeOptIn };
97
+ } catch {
98
+ return { dim: null, headersSet: false, allowRemote: false, nativeOptIn: false };
99
+ }
100
+ }
101
+
102
+ /**
103
+ * ENC-1b additive GET status fields. All omitted when flag-off. The Cortex
104
+ * trio (native opt-in + effective backend + demotion reason) is computed here
105
+ * (reader-only): the stored opt-in + current platform drive the EXISTING
106
+ * `selectRuntimeBackend`, whose own demotion reason is surfaced verbatim when
107
+ * the runtime falls back. `modeB` (flag-off selection) is projected to `wasm`
108
+ * for display. Returns `{}` when the flag is off (byte-identical predecessor).
109
+ */
110
+ export function enc1bStatusFields(stateDir: string): {
111
+ embeddingDim?: string;
112
+ embeddingHeadersSet?: boolean;
113
+ allowRemoteEmbedder?: boolean;
114
+ encoderNativeOptIn?: boolean;
115
+ encoderBackend?: "wasm" | "native";
116
+ encoderDemotionReason?: string | null;
117
+ } {
118
+ if (!ENC_1B_ENABLED()) return {};
119
+ const env = readEnc1bEnv(stateDir);
120
+ const platform = detectPlatform();
121
+ const chosen = selectRuntimeBackend({
122
+ platform: platform ?? "unsupported",
123
+ benchRecord: null,
124
+ nativeOptIn: env.nativeOptIn,
125
+ });
126
+ const backend: "wasm" | "native" =
127
+ chosen.backend === "native" ? "native" : "wasm";
128
+ return {
129
+ ...(env.dim !== null ? { embeddingDim: env.dim } : {}),
130
+ embeddingHeadersSet: env.headersSet,
131
+ allowRemoteEmbedder: env.allowRemote,
132
+ encoderNativeOptIn: env.nativeOptIn,
133
+ encoderBackend: backend,
134
+ encoderDemotionReason: chosen.demotionReason,
135
+ };
136
+ }
137
+
138
+ /** Upsert the ENC-1b keys into the per-repo `.mega-compact.env`. Creates the
139
+ * file if absent; preserves every unrelated line; never deletes other keys.
140
+ * A `null` entry leaves that key's line untouched. The headers value is
141
+ * wrapped in single quotes so the inner JSON double quotes survive the env
142
+ * loader's outer-quote strip (shell-sourceable + loader-compatible). */
143
+ export function writeEnc1bEnv(
144
+ stateDir: string,
145
+ entries: {
146
+ dim?: string | null;
147
+ headers?: string | null;
148
+ allowRemote?: boolean | null;
149
+ nativeOptIn?: boolean | null;
150
+ },
151
+ ): string {
152
+ const p = envPath(stateDir);
153
+ const existingLines: string[] = existsSync(p) ? readFileSync(p, "utf8").split(/\r?\n/) : [];
154
+ const out: string[] = [];
155
+ const keys = new Set<{ key: string; present: () => boolean; push: () => void }>([
156
+ {
157
+ key: ENC_1B_EMBEDDING_DIM_ENV,
158
+ present: () => entries.dim !== null,
159
+ push: () => out.push(`export ${ENC_1B_EMBEDDING_DIM_ENV}="${entries.dim ?? ""}"`),
160
+ },
161
+ {
162
+ key: ENC_1B_EMBEDDING_HEADERS_ENV,
163
+ present: () => entries.headers !== null,
164
+ push: () => out.push(`export ${ENC_1B_EMBEDDING_HEADERS_ENV}='${entries.headers ?? ""}'`),
165
+ },
166
+ {
167
+ key: ENC_1B_ALLOW_REMOTE_EMBEDDER_ENV,
168
+ present: () => entries.allowRemote !== null,
169
+ push: () =>
170
+ out.push(
171
+ entries.allowRemote
172
+ ? `export ${ENC_1B_ALLOW_REMOTE_EMBEDDER_ENV}="1"`
173
+ : `export ${ENC_1B_ALLOW_REMOTE_EMBEDDER_ENV}="0"`,
174
+ ),
175
+ },
176
+ {
177
+ key: ENC_1B_ENCODER_NATIVE_ENV,
178
+ present: () => entries.nativeOptIn !== null,
179
+ push: () =>
180
+ out.push(
181
+ entries.nativeOptIn
182
+ ? `export ${ENC_1B_ENCODER_NATIVE_ENV}="1"`
183
+ : `export ${ENC_1B_ENCODER_NATIVE_ENV}="0"`,
184
+ ),
185
+ },
186
+ ]);
187
+ const written = new Set<string>();
188
+ for (const line of existingLines) {
189
+ let replaced = false;
190
+ for (const k of keys) {
191
+ if (k.present() && lineValue(line, k.key) !== null) {
192
+ k.push();
193
+ written.add(k.key);
194
+ replaced = true;
195
+ break;
196
+ }
197
+ }
198
+ if (!replaced) out.push(line);
199
+ }
200
+ for (const k of keys) {
201
+ if (k.present() && !written.has(k.key)) k.push();
202
+ }
203
+ mkdirSync(stateDir, { recursive: true });
204
+ writeFileSync(p, out.join("\n"), "utf-8");
205
+ return p;
206
+ }
207
+
208
+ /** Validate `embeddingDim` as a positive integer string within the ENC-1b cap.
209
+ * Returns `null` when valid, else the error code. Exported so the routes host
210
+ * (combined-payload path) and the aggregator share the same rule. */
211
+ export function validateDim(value: unknown): string | null {
212
+ if (typeof value !== "string" || value.trim().length === 0) return "invalid_embedding_dim";
213
+ if (!/^\d+$/.test(value)) return "invalid_embedding_dim";
214
+ const n = Number(value);
215
+ if (!Number.isSafeInteger(n) || n < 1 || n > ENC_1B_MAX_EMBEDDING_DIM) {
216
+ return "invalid_embedding_dim";
217
+ }
218
+ return null;
219
+ }
220
+
221
+ /** Validate `embeddingHeaders` as a string that parses to a JSON object.
222
+ * Returns `null` when valid, else the error code. Exported so the routes host
223
+ * (combined-payload path) and the aggregator share the same rule. */
224
+ export function validateHeaders(value: unknown): string | null {
225
+ if (typeof value !== "string" || value.trim().length === 0) return "invalid_embedding_headers";
226
+ let parsed: unknown;
227
+ try {
228
+ parsed = JSON.parse(value);
229
+ } catch {
230
+ return "invalid_embedding_headers";
231
+ }
232
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
233
+ return "invalid_embedding_headers";
234
+ }
235
+ return null;
236
+ }
237
+
238
+ /**
239
+ * Combined-payload upsert: after the main embedder write has freshly written
240
+ * the per-repo env, append any ENC-1b keys carried alongside a valid embedder
241
+ * selection (flag-gated, additive). No-op when flag-off or no ENC-1b key.
242
+ */
243
+ export function tryEnc1bInto(stateDir: string, body: SetupConfigureRequest): void {
244
+ if (!wantsEnc1b(body)) return;
245
+ writeEnc1bEnv(stateDir, {
246
+ dim: typeof body.embeddingDim === "string" ? body.embeddingDim : null,
247
+ headers: typeof body.embeddingHeaders === "string" ? body.embeddingHeaders : null,
248
+ allowRemote: typeof body.allowRemoteEmbedder === "boolean" ? body.allowRemoteEmbedder : null,
249
+ nativeOptIn:
250
+ typeof body.encoderNativeOptIn === "boolean" ? body.encoderNativeOptIn : null,
251
+ });
252
+ }
253
+
254
+ /**
255
+ * If the payload is a pure ENC-1b configure (new keys, no embedder selection),
256
+ * validate + write them additively to the per-repo env and reply. Returns true
257
+ * when it fully handled the request. On a validation failure it replies 400
258
+ * with the code and leaves the file byte-unchanged. Flag-off = keys are not
259
+ * recognized and this returns false (byte-identical ENC-1a predecessor).
260
+ */
261
+ /**
262
+ * Combined-path validation: the standalone `tryEnc1bConfigure` handles the
263
+ * pure-ENC-1b shape; when a payload ALSO carries a valid embedder the sibling
264
+ * writer returns false and the routes host is responsible for running the same
265
+ * dim/headers validation before the combined upsert — returned as the error
266
+ * code string, or `null` if everything passes (or the flag is off).
267
+ */
268
+ export function enc1bValidateCombined(body: SetupConfigureRequest): string | null {
269
+ if (!ENC_1B_ENABLED()) return null;
270
+ if (body.embeddingDim !== undefined) {
271
+ const e = validateDim(body.embeddingDim);
272
+ if (e !== null) return e;
273
+ }
274
+ if (body.embeddingHeaders !== undefined) {
275
+ const e = validateHeaders(body.embeddingHeaders);
276
+ if (e !== null) return e;
277
+ }
278
+ return null;
279
+ }
280
+
281
+ export function tryEnc1bConfigure(
282
+ body: SetupConfigureRequest,
283
+ res: ServerResponse,
284
+ ctx: RouteContext,
285
+ ): boolean {
286
+ if (!wantsEnc1b(body)) return false;
287
+ const embedder = body.embedder;
288
+ const embedderValid =
289
+ !!embedder &&
290
+ (embedder === "ollama" || embedder === "llama" || embedder === "trigram" ||
291
+ embedder === "custom" || embedder === "onnx");
292
+ if (embedderValid) return false; // combined payload — integrated after the embedder write
293
+ // Each key is optional: only validate the ones actually carried in the
294
+ // payload (absent keys are left untouched, never rejected).
295
+ const dimError = body.embeddingDim === undefined ? null : validateDim(body.embeddingDim);
296
+ if (dimError !== null) {
297
+ // guardrails-allow PREVENT-PI-004: loopback dashboard response (local)
298
+ res.writeHead(400, { "Content-Type": "application/json" });
299
+ res.end(JSON.stringify({ error: dimError }));
300
+ return true;
301
+ }
302
+ const headersError =
303
+ body.embeddingHeaders === undefined ? null : validateHeaders(body.embeddingHeaders);
304
+ if (headersError !== null) {
305
+ // guardrails-allow PREVENT-PI-004: loopback dashboard response (local)
306
+ res.writeHead(400, { "Content-Type": "application/json" });
307
+ res.end(JSON.stringify({ error: headersError }));
308
+ return true;
309
+ }
310
+ try {
311
+ const envPath = writeEnc1bEnv(ctx.stateDir, {
312
+ dim: typeof body.embeddingDim === "string" ? body.embeddingDim : null,
313
+ headers: typeof body.embeddingHeaders === "string" ? body.embeddingHeaders : null,
314
+ allowRemote: typeof body.allowRemoteEmbedder === "boolean" ? body.allowRemoteEmbedder : null,
315
+ nativeOptIn:
316
+ typeof body.encoderNativeOptIn === "boolean" ? body.encoderNativeOptIn : null,
317
+ });
318
+ const resp: SetupConfigureResponse = {
319
+ embedder: "custom",
320
+ url: null,
321
+ envPath,
322
+ restartRequired: true,
323
+ alreadyActive: false,
324
+ };
325
+ // guardrails-allow PREVENT-PI-004: loopback dashboard response (local)
326
+ res.writeHead(200, { "Content-Type": "application/json" });
327
+ res.end(JSON.stringify(resp));
328
+ return true;
329
+ } catch (e) {
330
+ res.writeHead(500, { "Content-Type": "application/json" });
331
+ res.end(
332
+ JSON.stringify({ error: `write_failed: ${e instanceof Error ? e.message : String(e)}` }),
333
+ );
334
+ return true;
335
+ }
336
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * routes-setup-env-upsert.ts — upsert-style write of the per-repo
3
+ * `.mega-compact.env` for the primary embedder selection.
4
+ *
5
+ * Extracted from routes-setup.ts so the route stays under the 300-line soft
6
+ * cap. The writer preserves every unrelated line (ENC-1a's endpoint/key,
7
+ * ENC-1b's dim/headers, operator comments) and replaces only the three keys
8
+ * this primary write owns — MEGACOMPACT_EMBEDDING_URL,
9
+ * MEGACOMPACT_ALLOW_REMOTE_EMBEDDER, MEGACOMPACT_MINILM. URLs already persisted
10
+ * by enc1a/enc1b helpers are NOT clobbered across a later native-opt-in POST
11
+ * (the ENC-1b defect class caught in live review).
12
+ */
13
+
14
+ import { writeFileSync, mkdirSync, readFileSync, existsSync } from "node:fs";
15
+ import { join } from "node:path";
16
+
17
+ /** Line-prefix recognizer for an owned key (covers active, commented, and prose forms). */
18
+ function ownsKey(line: string, key: string): boolean {
19
+ return (
20
+ line.startsWith(`export ${key}=`) ||
21
+ line.startsWith(`# export ${key}=`) ||
22
+ line.startsWith(`# ${key} `)
23
+ );
24
+ }
25
+
26
+ const OWNED_PREFIXES = [
27
+ "MEGACOMPACT_EMBEDDING_URL",
28
+ "MEGACOMPACT_ALLOW_REMOTE_EMBEDDER",
29
+ "MEGACOMPACT_MINILM",
30
+ ];
31
+
32
+ const SCAFFOLD_COMMENTS = new Set([
33
+ "# Mega-Compact Embedder Configuration",
34
+ "# trigram: built-in embedder, no URL needed",
35
+ "# unset MEGACOMPACT_EMBEDDING_URL (commented to override any shell-set value)",
36
+ "# MEGACOMPACT_ALLOW_REMOTE_EMBEDDER not set (loopback-only)",
37
+ ]);
38
+
39
+ function isScaffoldComment(line: string): boolean {
40
+ return (
41
+ SCAFFOLD_COMMENTS.has(line) ||
42
+ line.startsWith("# Configured via dashboard Setup tab at ")
43
+ );
44
+ }
45
+
46
+ /**
47
+ * Write the embedder selection upsert-style into `<stateDir>/.mega-compact.env`.
48
+ * `resolvedUrl === null` selects the trigram (built-in) path — the emitted
49
+ * lines carry only the scaffold comments for that case (the URL/remote lines
50
+ * are intentionally absent, matching the pre-refactor byte shape).
51
+ */
52
+ export function writeEmbedderEnv(
53
+ stateDir: string,
54
+ resolvedUrl: string | null,
55
+ allowRemote: boolean,
56
+ ): string {
57
+ const envPath = join(stateDir, ".mega-compact.env");
58
+ const existingLines: string[] = (() => {
59
+ try {
60
+ return existsSync(envPath) ? readFileSync(envPath, "utf-8").split("\n") : [];
61
+ } catch {
62
+ return [];
63
+ }
64
+ })();
65
+ const preserved: string[] = existingLines.filter(
66
+ (l) =>
67
+ l.trim().length > 0 &&
68
+ !isScaffoldComment(l) &&
69
+ !OWNED_PREFIXES.some((p) => ownsKey(l, p)),
70
+ );
71
+ const next: string[] = [
72
+ "# Mega-Compact Embedder Configuration",
73
+ `# Configured via dashboard Setup tab at ${new Date().toISOString()}`,
74
+ ...preserved,
75
+ ];
76
+ if (resolvedUrl !== null) {
77
+ next.push(`export MEGACOMPACT_EMBEDDING_URL="${resolvedUrl}"`);
78
+ if (allowRemote) {
79
+ next.push(`export MEGACOMPACT_ALLOW_REMOTE_EMBEDDER="1"`);
80
+ } else {
81
+ next.push(`# MEGACOMPACT_ALLOW_REMOTE_EMBEDDER not set (loopback-only)`);
82
+ }
83
+ } else {
84
+ next.push("# trigram: built-in embedder, no URL needed");
85
+ next.push("# unset MEGACOMPACT_EMBEDDING_URL (commented to override any shell-set value)");
86
+ next.push("# export MEGACOMPACT_EMBEDDING_URL=");
87
+ next.push("# export MEGACOMPACT_ALLOW_REMOTE_EMBEDDER=");
88
+ }
89
+ next.push("");
90
+ mkdirSync(stateDir, { recursive: true });
91
+ writeFileSync(envPath, next.join("\n"), "utf-8");
92
+ return envPath;
93
+ }