@danielblomma/cortex-mcp 1.7.2 → 2.0.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.
- package/bin/cortex.mjs +679 -32
- package/bin/style.mjs +349 -0
- package/package.json +4 -3
- package/scaffold/mcp/src/cli/enterprise-setup.ts +124 -0
- package/scaffold/mcp/src/cli/govern.ts +987 -0
- package/scaffold/mcp/src/cli/run.ts +306 -0
- package/scaffold/mcp/src/cli/telemetry-test.ts +158 -0
- package/scaffold/mcp/src/cli/ungoverned-detector.ts +168 -0
- package/scaffold/mcp/src/core/audit/query.ts +81 -0
- package/scaffold/mcp/src/core/audit/writer.ts +68 -0
- package/scaffold/mcp/src/core/config.ts +329 -0
- package/scaffold/mcp/src/core/index.ts +34 -0
- package/scaffold/mcp/src/core/license.ts +202 -0
- package/scaffold/mcp/src/core/policy/enforce.ts +98 -0
- package/scaffold/mcp/src/core/policy/injection.ts +229 -0
- package/scaffold/mcp/src/core/policy/store.ts +197 -0
- package/scaffold/mcp/src/core/rbac/check.ts +40 -0
- package/scaffold/mcp/src/core/telemetry/collector.ts +234 -0
- package/scaffold/mcp/src/core/validators/builtins.ts +711 -0
- package/scaffold/mcp/src/core/validators/config.ts +47 -0
- package/scaffold/mcp/src/core/validators/engine.ts +199 -0
- package/scaffold/mcp/src/core/validators/evaluators/code_comments.ts +294 -0
- package/scaffold/mcp/src/core/validators/evaluators/regex.ts +144 -0
- package/scaffold/mcp/src/daemon/client.ts +155 -0
- package/scaffold/mcp/src/daemon/egress-proxy.ts +331 -0
- package/scaffold/mcp/src/daemon/heartbeat-pusher.ts +147 -0
- package/scaffold/mcp/src/daemon/heartbeat-tracker.ts +223 -0
- package/scaffold/mcp/src/daemon/host-events-pusher.ts +285 -0
- package/scaffold/mcp/src/daemon/main.ts +300 -0
- package/scaffold/mcp/src/daemon/paths.ts +41 -0
- package/scaffold/mcp/src/daemon/protocol.ts +101 -0
- package/scaffold/mcp/src/daemon/server.ts +227 -0
- package/scaffold/mcp/src/daemon/sync-checker.ts +213 -0
- package/scaffold/mcp/src/daemon/ungoverned-scanner.ts +149 -0
- package/scaffold/mcp/src/enterprise/audit/push.ts +84 -0
- package/scaffold/mcp/src/enterprise/index.ts +415 -0
- package/scaffold/mcp/src/enterprise/model/deploy.ts +33 -0
- package/scaffold/mcp/src/enterprise/policy/sync.ts +146 -0
- package/scaffold/mcp/src/enterprise/privacy/boundary.ts +212 -0
- package/scaffold/mcp/src/enterprise/reviews/push.ts +79 -0
- package/scaffold/mcp/src/enterprise/telemetry/sync.ts +72 -0
- package/scaffold/mcp/src/enterprise/tools/enterprise.ts +1031 -0
- package/scaffold/mcp/src/enterprise/tools/walk.ts +79 -0
- package/scaffold/mcp/src/enterprise/violations/push.ts +102 -0
- package/scaffold/mcp/src/enterprise/workflow/push.ts +60 -0
- package/scaffold/mcp/src/enterprise/workflow/state.ts +535 -0
- package/scaffold/mcp/src/hooks/pre-compact.ts +54 -0
- package/scaffold/mcp/src/hooks/pre-tool-use.ts +96 -0
- package/scaffold/mcp/src/hooks/session-end.ts +73 -0
- package/scaffold/mcp/src/hooks/session-start.ts +78 -0
- package/scaffold/mcp/src/hooks/shared.ts +134 -0
- package/scaffold/mcp/src/hooks/stop.ts +60 -0
- package/scaffold/mcp/src/hooks/user-prompt-submit.ts +64 -0
- package/scaffold/mcp/src/plugin.ts +150 -0
- package/scaffold/mcp/src/server.ts +218 -7
- package/scaffold/mcp/tests/copilot-shim.test.mjs +146 -0
- package/scaffold/mcp/tests/daemon-client.test.mjs +32 -0
- package/scaffold/mcp/tests/egress-proxy.test.mjs +239 -0
- package/scaffold/mcp/tests/enterprise-config.test.mjs +154 -0
- package/scaffold/mcp/tests/govern-install.test.mjs +320 -0
- package/scaffold/mcp/tests/govern-repair.test.mjs +157 -0
- package/scaffold/mcp/tests/govern-status.test.mjs +538 -0
- package/scaffold/mcp/tests/govern.test.mjs +74 -0
- package/scaffold/mcp/tests/heartbeat-pusher.test.mjs +154 -0
- package/scaffold/mcp/tests/heartbeat-tracker.test.mjs +237 -0
- package/scaffold/mcp/tests/host-events-pusher.test.mjs +347 -0
- package/scaffold/mcp/tests/policy-check.test.mjs +220 -0
- package/scaffold/mcp/tests/repo-name.test.mjs +134 -0
- package/scaffold/mcp/tests/run.test.mjs +109 -0
- package/scaffold/mcp/tests/sync-checker.test.mjs +188 -0
- package/scaffold/mcp/tests/ungoverned-detector.test.mjs +191 -0
- package/scaffold/mcp/tests/ungoverned-scanner.test.mjs +198 -0
- package/scaffold/scripts/bootstrap.sh +0 -11
- package/scaffold/scripts/doctor.sh +24 -4
- package/types.js +5 -0
package/bin/style.mjs
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
// Cortex CLI style helpers.
|
|
2
|
+
//
|
|
3
|
+
// Pure ANSI / Unicode utilities — no external deps. The brand language is:
|
|
4
|
+
// GitHub CLI meets cyberpunk compliance system.
|
|
5
|
+
// Dark background, neon gradient (purple → indigo → blue → cyan), thin lines,
|
|
6
|
+
// subtle motion. "Cortex is in control."
|
|
7
|
+
//
|
|
8
|
+
// Every helper degrades to plain text when:
|
|
9
|
+
// - process.stdout.isTTY is false (piped, CI, file redirect), or
|
|
10
|
+
// - NO_COLOR is set (https://no-color.org/), or
|
|
11
|
+
// - CORTEX_NO_COLOR is set (escape hatch).
|
|
12
|
+
//
|
|
13
|
+
// Unicode glyphs degrade to ASCII when CORTEX_NO_UNICODE is set or LANG/LC_ALL
|
|
14
|
+
// look non-UTF-8.
|
|
15
|
+
|
|
16
|
+
const NO_COLOR =
|
|
17
|
+
typeof process.env.NO_COLOR === "string" && process.env.NO_COLOR.length > 0;
|
|
18
|
+
const CORTEX_NO_COLOR =
|
|
19
|
+
typeof process.env.CORTEX_NO_COLOR === "string" &&
|
|
20
|
+
process.env.CORTEX_NO_COLOR.length > 0;
|
|
21
|
+
|
|
22
|
+
function streamSupportsColor(stream) {
|
|
23
|
+
if (NO_COLOR || CORTEX_NO_COLOR) return false;
|
|
24
|
+
if (!stream) return false;
|
|
25
|
+
if (stream.isTTY === false) return false;
|
|
26
|
+
if (process.env.TERM === "dumb") return false;
|
|
27
|
+
return Boolean(stream.isTTY);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function supportsColor(stream = process.stdout) {
|
|
31
|
+
return streamSupportsColor(stream);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function unicodeOk() {
|
|
35
|
+
if (process.env.CORTEX_NO_UNICODE) return false;
|
|
36
|
+
const locale =
|
|
37
|
+
process.env.LC_ALL || process.env.LC_CTYPE || process.env.LANG || "";
|
|
38
|
+
if (!locale) return process.platform !== "win32";
|
|
39
|
+
return /UTF-?8/i.test(locale);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const ESC = "\x1b[";
|
|
43
|
+
const RESET = `${ESC}0m`;
|
|
44
|
+
const DIM = `${ESC}2m`;
|
|
45
|
+
const BOLD = `${ESC}1m`;
|
|
46
|
+
|
|
47
|
+
// Cortex neon palette (256-color). Picked from xterm-256 to read as
|
|
48
|
+
// purple → indigo → blue → cyan on a dark terminal.
|
|
49
|
+
const GRADIENT_COLORS = [
|
|
50
|
+
141, // light purple
|
|
51
|
+
99, // indigo
|
|
52
|
+
63, // deep indigo / blue
|
|
53
|
+
69, // mid blue
|
|
54
|
+
75, // bright blue
|
|
55
|
+
81, // cyan-blue
|
|
56
|
+
45, // cyan
|
|
57
|
+
51 // bright cyan
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
const SEMANTIC = {
|
|
61
|
+
ok: 84, // green
|
|
62
|
+
warn: 215, // amber
|
|
63
|
+
fail: 203, // red
|
|
64
|
+
info: 81, // cyan
|
|
65
|
+
alert: 197, // hot pink-red (cyberpunk alert)
|
|
66
|
+
muted: 244 // soft grey
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
function fg256(code) {
|
|
70
|
+
return `${ESC}38;5;${code}m`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function colorize(text, code, stream = process.stdout) {
|
|
74
|
+
if (!supportsColor(stream)) return text;
|
|
75
|
+
return `${fg256(code)}${text}${RESET}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function dim(text, stream = process.stdout) {
|
|
79
|
+
if (!supportsColor(stream)) return text;
|
|
80
|
+
return `${DIM}${text}${RESET}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function bold(text, stream = process.stdout) {
|
|
84
|
+
if (!supportsColor(stream)) return text;
|
|
85
|
+
return `${BOLD}${text}${RESET}`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function muted(text, stream = process.stdout) {
|
|
89
|
+
return colorize(text, SEMANTIC.muted, stream);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function accent(text, stream = process.stdout) {
|
|
93
|
+
return colorize(text, SEMANTIC.info, stream);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Render text letter-by-letter through the neon gradient. Whitespace is
|
|
97
|
+
// preserved verbatim so layout stays aligned.
|
|
98
|
+
export function gradient(text, stream = process.stdout) {
|
|
99
|
+
if (!supportsColor(stream)) return text;
|
|
100
|
+
const chars = [...text];
|
|
101
|
+
const visible = chars.filter((ch) => ch.trim().length > 0).length;
|
|
102
|
+
if (visible === 0) return text;
|
|
103
|
+
|
|
104
|
+
let visibleIndex = 0;
|
|
105
|
+
let out = "";
|
|
106
|
+
for (const ch of chars) {
|
|
107
|
+
if (ch.trim().length === 0) {
|
|
108
|
+
out += ch;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const t = visible === 1 ? 0 : visibleIndex / (visible - 1);
|
|
112
|
+
const slot = Math.min(
|
|
113
|
+
GRADIENT_COLORS.length - 1,
|
|
114
|
+
Math.round(t * (GRADIENT_COLORS.length - 1))
|
|
115
|
+
);
|
|
116
|
+
out += `${fg256(GRADIENT_COLORS[slot])}${ch}${RESET}`;
|
|
117
|
+
visibleIndex += 1;
|
|
118
|
+
}
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const BULLET_GLYPHS_UTF8 = {
|
|
123
|
+
ok: "✔",
|
|
124
|
+
warn: "!",
|
|
125
|
+
fail: "✗",
|
|
126
|
+
info: "▸",
|
|
127
|
+
alert: "●"
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const BULLET_GLYPHS_ASCII = {
|
|
131
|
+
ok: "v",
|
|
132
|
+
warn: "!",
|
|
133
|
+
fail: "x",
|
|
134
|
+
info: ">",
|
|
135
|
+
alert: "*"
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
function bulletGlyph(state) {
|
|
139
|
+
const map = unicodeOk() ? BULLET_GLYPHS_UTF8 : BULLET_GLYPHS_ASCII;
|
|
140
|
+
return map[state] ?? map.info;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function bullet(state, text, stream = process.stdout) {
|
|
144
|
+
const glyph = bulletGlyph(state);
|
|
145
|
+
const code = SEMANTIC[state] ?? SEMANTIC.info;
|
|
146
|
+
if (!supportsColor(stream)) {
|
|
147
|
+
return `${glyph} ${text}`;
|
|
148
|
+
}
|
|
149
|
+
return `${fg256(code)}${glyph}${RESET} ${text}`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function printBullet(state, text, stream = process.stdout) {
|
|
153
|
+
stream.write(`${bullet(state, text, stream)}\n`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Spinner: Braille pattern in UTF-8 mode, ASCII clock fallback otherwise.
|
|
157
|
+
const SPINNER_FRAMES_UTF8 = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
158
|
+
const SPINNER_FRAMES_ASCII = ["|", "/", "-", "\\"];
|
|
159
|
+
|
|
160
|
+
function spinnerFrames() {
|
|
161
|
+
return unicodeOk() ? SPINNER_FRAMES_UTF8 : SPINNER_FRAMES_ASCII;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Returns a controller: { stop(finalState, finalText), update(text) }.
|
|
165
|
+
// In non-TTY mode the spinner just writes the static label as a "▸ label"
|
|
166
|
+
// info bullet and returns a no-op stop().
|
|
167
|
+
export function spinner(label, stream = process.stdout) {
|
|
168
|
+
const useTTY = supportsColor(stream) && Boolean(stream.isTTY);
|
|
169
|
+
const frames = spinnerFrames();
|
|
170
|
+
let i = 0;
|
|
171
|
+
let currentLabel = label;
|
|
172
|
+
|
|
173
|
+
if (!useTTY) {
|
|
174
|
+
stream.write(`${bullet("info", currentLabel, stream)}\n`);
|
|
175
|
+
return {
|
|
176
|
+
update(next) {
|
|
177
|
+
currentLabel = next;
|
|
178
|
+
},
|
|
179
|
+
stop(finalState = "ok", finalText) {
|
|
180
|
+
const text = finalText ?? currentLabel;
|
|
181
|
+
stream.write(`${bullet(finalState, text, stream)}\n`);
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const hideCursor = `${ESC}?25l`;
|
|
187
|
+
const showCursor = `${ESC}?25h`;
|
|
188
|
+
const clearLine = `\r${ESC}2K`;
|
|
189
|
+
|
|
190
|
+
stream.write(hideCursor);
|
|
191
|
+
const render = () => {
|
|
192
|
+
const frame = frames[i % frames.length];
|
|
193
|
+
i += 1;
|
|
194
|
+
stream.write(`${clearLine}${fg256(SEMANTIC.info)}${frame}${RESET} ${currentLabel}`);
|
|
195
|
+
};
|
|
196
|
+
render();
|
|
197
|
+
const interval = setInterval(render, 80);
|
|
198
|
+
if (typeof interval.unref === "function") {
|
|
199
|
+
interval.unref();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
let stopped = false;
|
|
203
|
+
return {
|
|
204
|
+
update(next) {
|
|
205
|
+
currentLabel = next;
|
|
206
|
+
},
|
|
207
|
+
stop(finalState = "ok", finalText) {
|
|
208
|
+
if (stopped) return;
|
|
209
|
+
stopped = true;
|
|
210
|
+
clearInterval(interval);
|
|
211
|
+
const text = finalText ?? currentLabel;
|
|
212
|
+
stream.write(`${clearLine}${bullet(finalState, text, stream)}\n${showCursor}`);
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Box-drawing wrapper. UTF-8: rounded corners + thin lines. ASCII fallback uses
|
|
218
|
+
// + - | corners. Optional title and accent color (256-color code).
|
|
219
|
+
const BOX_UTF8 = {
|
|
220
|
+
tl: "╭",
|
|
221
|
+
tr: "╮",
|
|
222
|
+
bl: "╰",
|
|
223
|
+
br: "╯",
|
|
224
|
+
h: "─",
|
|
225
|
+
v: "│"
|
|
226
|
+
};
|
|
227
|
+
const BOX_ASCII = {
|
|
228
|
+
tl: "+",
|
|
229
|
+
tr: "+",
|
|
230
|
+
bl: "+",
|
|
231
|
+
br: "+",
|
|
232
|
+
h: "-",
|
|
233
|
+
v: "|"
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
function visibleLength(line) {
|
|
237
|
+
// strip ANSI escapes for width calculation
|
|
238
|
+
// eslint-disable-next-line no-control-regex
|
|
239
|
+
return line.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function padRight(line, width) {
|
|
243
|
+
const len = visibleLength(line);
|
|
244
|
+
if (len >= width) return line;
|
|
245
|
+
return line + " ".repeat(width - len);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function box(content, opts = {}) {
|
|
249
|
+
const stream = opts.stream ?? process.stdout;
|
|
250
|
+
const useColor = supportsColor(stream);
|
|
251
|
+
const glyphs = unicodeOk() ? BOX_UTF8 : BOX_ASCII;
|
|
252
|
+
const accentCode = opts.accent ?? GRADIENT_COLORS[2];
|
|
253
|
+
const tint = (text) => (useColor ? `${fg256(accentCode)}${text}${RESET}` : text);
|
|
254
|
+
|
|
255
|
+
const lines = String(content).split("\n");
|
|
256
|
+
const titleRaw = opts.title ?? "";
|
|
257
|
+
const padding = opts.padding ?? 1;
|
|
258
|
+
|
|
259
|
+
const inner = Math.max(
|
|
260
|
+
visibleLength(titleRaw) + 4,
|
|
261
|
+
...lines.map(visibleLength)
|
|
262
|
+
);
|
|
263
|
+
const width = inner + padding * 2;
|
|
264
|
+
|
|
265
|
+
let topMid = glyphs.h.repeat(width);
|
|
266
|
+
if (titleRaw) {
|
|
267
|
+
const titleText = useColor ? bold(titleRaw, stream) : titleRaw;
|
|
268
|
+
const lhs = glyphs.h.repeat(2);
|
|
269
|
+
const rhs = glyphs.h.repeat(Math.max(1, width - visibleLength(titleRaw) - 4));
|
|
270
|
+
topMid = `${lhs} ${titleText} ${rhs}`;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const top = tint(`${glyphs.tl}${topMid}${glyphs.tr}`);
|
|
274
|
+
const bottom = tint(`${glyphs.bl}${glyphs.h.repeat(width)}${glyphs.br}`);
|
|
275
|
+
const pad = " ".repeat(padding);
|
|
276
|
+
|
|
277
|
+
const body = lines.map(
|
|
278
|
+
(line) => `${tint(glyphs.v)}${pad}${padRight(line, inner)}${pad}${tint(glyphs.v)}`
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
return [top, ...body, bottom].join("\n");
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Header banner: stylized CORTEX wordmark + a "node network" motif.
|
|
285
|
+
// The motif is a thin row of dot-and-line glyphs framing a central core dot —
|
|
286
|
+
// the same idea as the orbiting nodes in the brand brief, reduced to one line.
|
|
287
|
+
//
|
|
288
|
+
// Wordmark uses block characters that fit ~6 lines tall, kept narrow so it
|
|
289
|
+
// doesn't wrap on 80-col terminals.
|
|
290
|
+
const CORTEX_WORDMARK = [
|
|
291
|
+
" ██████╗ ██████╗ ██████╗ ████████╗███████╗██╗ ██╗",
|
|
292
|
+
" ██╔════╝██╔═══██╗██╔══██╗╚══██╔══╝██╔════╝╚██╗██╔╝",
|
|
293
|
+
" ██║ ██║ ██║██████╔╝ ██║ █████╗ ╚███╔╝ ",
|
|
294
|
+
" ██║ ██║ ██║██╔══██╗ ██║ ██╔══╝ ██╔██╗ ",
|
|
295
|
+
" ╚██████╗╚██████╔╝██║ ██║ ██║ ███████╗██╔╝ ██╗",
|
|
296
|
+
" ╚═════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝"
|
|
297
|
+
];
|
|
298
|
+
|
|
299
|
+
const CORTEX_WORDMARK_ASCII = [
|
|
300
|
+
" CCCCC OOO RRRR TTTTT EEEEE X X",
|
|
301
|
+
" C O O R R T E X X ",
|
|
302
|
+
" C O O RRRR T EEEE X ",
|
|
303
|
+
" C O O R R T E X X ",
|
|
304
|
+
" CCCCC OOO R R T EEEEE X X"
|
|
305
|
+
];
|
|
306
|
+
|
|
307
|
+
function networkMotif(width) {
|
|
308
|
+
if (!unicodeOk()) {
|
|
309
|
+
const dots = ".".repeat(Math.max(0, Math.floor((width - 7) / 2)));
|
|
310
|
+
return `${dots} [ core ] ${dots}`;
|
|
311
|
+
}
|
|
312
|
+
const dots = "·".repeat(Math.max(0, Math.floor((width - 7) / 2)));
|
|
313
|
+
// ⟢ / ⟣ are subtle "incoming/outgoing" arrows around a core node ◉.
|
|
314
|
+
return `${dots} ⟢ ◉ ⟣ ${dots}`;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export function headerBanner(opts = {}) {
|
|
318
|
+
const stream = opts.stream ?? process.stdout;
|
|
319
|
+
const tagline = opts.tagline ?? "";
|
|
320
|
+
const wordmark = unicodeOk() ? CORTEX_WORDMARK : CORTEX_WORDMARK_ASCII;
|
|
321
|
+
const width = Math.max(...wordmark.map((line) => line.length));
|
|
322
|
+
|
|
323
|
+
const lines = [];
|
|
324
|
+
lines.push("");
|
|
325
|
+
for (const line of wordmark) {
|
|
326
|
+
lines.push(gradient(line, stream));
|
|
327
|
+
}
|
|
328
|
+
const motif = networkMotif(width);
|
|
329
|
+
lines.push(muted(motif, stream));
|
|
330
|
+
if (tagline) {
|
|
331
|
+
lines.push(muted(tagline, stream));
|
|
332
|
+
}
|
|
333
|
+
lines.push("");
|
|
334
|
+
return lines.join("\n");
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export function printHeaderBanner(opts = {}) {
|
|
338
|
+
const stream = opts.stream ?? process.stdout;
|
|
339
|
+
stream.write(headerBanner(opts));
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Convenience: compose a labelled status line (used by `cortex run` header).
|
|
343
|
+
export function runHeader(label, stream = process.stdout) {
|
|
344
|
+
const glyph = unicodeOk() ? "▸" : ">";
|
|
345
|
+
if (!supportsColor(stream)) {
|
|
346
|
+
return `${glyph} ${label}`;
|
|
347
|
+
}
|
|
348
|
+
return `${fg256(SEMANTIC.info)}${glyph}${RESET} ${label}`;
|
|
349
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danielblomma/cortex-mcp",
|
|
3
3
|
"mcpName": "io.github.DanielBlomma/cortex",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "2.0.2",
|
|
5
5
|
"description": "Local, repo-scoped context platform for coding assistants. Semantic search, graph relationships, and architectural rule context.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"author": "Daniel Blomma",
|
|
@@ -26,13 +26,14 @@
|
|
|
26
26
|
],
|
|
27
27
|
"exports": {
|
|
28
28
|
".": "./bin/cortex.mjs",
|
|
29
|
-
"./types": "./
|
|
29
|
+
"./types": "./types.js"
|
|
30
30
|
},
|
|
31
31
|
"bin": {
|
|
32
32
|
"cortex": "bin/cortex.mjs"
|
|
33
33
|
},
|
|
34
34
|
"files": [
|
|
35
35
|
"bin",
|
|
36
|
+
"types.js",
|
|
36
37
|
"scaffold/.context",
|
|
37
38
|
"scaffold/.githooks",
|
|
38
39
|
"scaffold/docs",
|
|
@@ -52,6 +53,6 @@
|
|
|
52
53
|
"prepublishOnly": "echo 'Ready to publish to npm'"
|
|
53
54
|
},
|
|
54
55
|
"engines": {
|
|
55
|
-
"node": ">=
|
|
56
|
+
"node": ">=20"
|
|
56
57
|
}
|
|
57
58
|
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { verifyLicense } from "../core/license.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* One-liner enterprise onboarding.
|
|
7
|
+
*
|
|
8
|
+
* $ cortex enterprise <api-key> [--endpoint <url>]
|
|
9
|
+
*
|
|
10
|
+
* Replaces the manual `.context/enterprise.yml` editing flow that's been
|
|
11
|
+
* the friction point for new users. Validates the key against the
|
|
12
|
+
* license endpoint before writing config — so a typo'd key fails fast
|
|
13
|
+
* with a clear error rather than going silently into community-mode.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const DEFAULT_ENDPOINT = "https://cortex-web-rho.vercel.app";
|
|
17
|
+
|
|
18
|
+
export type EnterpriseSetupOptions = {
|
|
19
|
+
apiKey: string;
|
|
20
|
+
endpoint?: string;
|
|
21
|
+
cwd?: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type EnterpriseSetupResult = {
|
|
25
|
+
ok: boolean;
|
|
26
|
+
message: string;
|
|
27
|
+
configPath?: string;
|
|
28
|
+
edition?: string;
|
|
29
|
+
expiresAt?: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const API_KEY_RE = /^(?:ctx|ent)_[A-Za-z0-9._-]{8,}$/;
|
|
33
|
+
|
|
34
|
+
function buildEnterpriseYaml(baseUrl: string, apiKey: string): string {
|
|
35
|
+
const lines = [
|
|
36
|
+
"# Cortex enterprise configuration. Generated by `cortex enterprise <key>`.",
|
|
37
|
+
"# Single api_key is used for telemetry, policy, audit and govern services.",
|
|
38
|
+
"enterprise:",
|
|
39
|
+
` api_key: ${apiKey}`,
|
|
40
|
+
` base_url: ${baseUrl}`,
|
|
41
|
+
"",
|
|
42
|
+
"telemetry:",
|
|
43
|
+
" enabled: true",
|
|
44
|
+
" interval_minutes: 1",
|
|
45
|
+
"",
|
|
46
|
+
"compliance:",
|
|
47
|
+
" frameworks: [iso27001, iso42001, soc2]",
|
|
48
|
+
" eu_addons: false",
|
|
49
|
+
"",
|
|
50
|
+
"govern:",
|
|
51
|
+
" mode: off",
|
|
52
|
+
" sync_on_startup: true",
|
|
53
|
+
" sync_interval_minutes: 60",
|
|
54
|
+
" tier_claude: prevent",
|
|
55
|
+
" tier_codex: prevent",
|
|
56
|
+
" tier_copilot: wrap",
|
|
57
|
+
" detect_ungoverned: true",
|
|
58
|
+
"",
|
|
59
|
+
];
|
|
60
|
+
return lines.join("\n");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function runEnterpriseSetup(
|
|
64
|
+
options: EnterpriseSetupOptions,
|
|
65
|
+
): Promise<EnterpriseSetupResult> {
|
|
66
|
+
const cwd = options.cwd ?? process.cwd();
|
|
67
|
+
const endpoint = (options.endpoint ?? DEFAULT_ENDPOINT).replace(/\/$/, "");
|
|
68
|
+
const apiKey = options.apiKey.trim();
|
|
69
|
+
|
|
70
|
+
if (!API_KEY_RE.test(apiKey)) {
|
|
71
|
+
return {
|
|
72
|
+
ok: false,
|
|
73
|
+
message:
|
|
74
|
+
"API key must start with 'ctx_' or 'ent_' followed by at least 8 alphanumeric/._- chars.",
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!/^https?:\/\//.test(endpoint)) {
|
|
79
|
+
return {
|
|
80
|
+
ok: false,
|
|
81
|
+
message: `Endpoint must be http(s) URL: ${endpoint}`,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const contextDir = join(cwd, ".context");
|
|
86
|
+
if (!existsSync(contextDir)) {
|
|
87
|
+
return {
|
|
88
|
+
ok: false,
|
|
89
|
+
message: `No .context/ at ${cwd}. Run 'cortex init --bootstrap' first.`,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Validate key BEFORE writing config — fail fast, no half-configured state.
|
|
94
|
+
const license = await verifyLicense(contextDir, endpoint, apiKey, {
|
|
95
|
+
client_version: process.env.CORTEX_VERSION,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
if (!license.valid) {
|
|
99
|
+
return {
|
|
100
|
+
ok: false,
|
|
101
|
+
message: `License rejected: ${license.reason} (source=${license.source}). Verify the API key and endpoint are correct.`,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Write enterprise.yml.
|
|
106
|
+
const configPath = join(contextDir, "enterprise.yml");
|
|
107
|
+
try {
|
|
108
|
+
mkdirSync(contextDir, { recursive: true });
|
|
109
|
+
writeFileSync(configPath, buildEnterpriseYaml(endpoint, apiKey), "utf8");
|
|
110
|
+
} catch (err) {
|
|
111
|
+
return {
|
|
112
|
+
ok: false,
|
|
113
|
+
message: `Failed to write ${configPath}: ${err instanceof Error ? err.message : String(err)}`,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
ok: true,
|
|
119
|
+
message: "Enterprise configuration written.",
|
|
120
|
+
configPath,
|
|
121
|
+
edition: license.edition,
|
|
122
|
+
expiresAt: license.expires_at,
|
|
123
|
+
};
|
|
124
|
+
}
|