@danielblomma/cortex-mcp 2.0.15 → 2.0.16
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/package.json +1 -1
- package/scaffold/mcp/src/cli/telemetry-test.ts +2 -1
- package/scaffold/mcp/src/core/license.ts +3 -2
- package/scaffold/mcp/src/core/telemetry/collector.ts +5 -4
- package/scaffold/mcp/src/core/telemetry/state-dir.ts +40 -0
- package/scaffold/mcp/src/daemon/main.ts +4 -3
- package/scaffold/mcp/tests/telemetry-collector.test.mjs +31 -1
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": "2.0.
|
|
4
|
+
"version": "2.0.16",
|
|
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",
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
loadEnterpriseConfig,
|
|
7
7
|
resolveEnterpriseActivation,
|
|
8
8
|
} from "../core/config.js";
|
|
9
|
+
import { telemetryStatePath } from "../core/telemetry/state-dir.js";
|
|
9
10
|
import { pushMetrics } from "../enterprise/telemetry/sync.js";
|
|
10
11
|
import type { TelemetryMetrics } from "../core/telemetry/collector.js";
|
|
11
12
|
|
|
@@ -25,7 +26,7 @@ import type { TelemetryMetrics } from "../core/telemetry/collector.js";
|
|
|
25
26
|
*/
|
|
26
27
|
|
|
27
28
|
function readMachineId(contextDir: string): string {
|
|
28
|
-
const path =
|
|
29
|
+
const path = telemetryStatePath(contextDir, "machine_id");
|
|
29
30
|
if (existsSync(path)) {
|
|
30
31
|
try {
|
|
31
32
|
const id = readFileSync(path, "utf8").trim();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
import { resolveTelemetryStateDir, telemetryStatePath } from "./telemetry/state-dir.js";
|
|
3
4
|
|
|
4
5
|
export type LicenseVerification =
|
|
5
6
|
| {
|
|
@@ -30,7 +31,7 @@ const GRACE_PERIOD_MS = 7 * 24 * 60 * 60 * 1000; // 7d grace if endpoint unreach
|
|
|
30
31
|
const REQUEST_TIMEOUT_MS = 5000;
|
|
31
32
|
|
|
32
33
|
function cachePath(contextDir: string): string {
|
|
33
|
-
return
|
|
34
|
+
return telemetryStatePath(contextDir, CACHE_FILE);
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
function readCache(contextDir: string): CacheEntry | null {
|
|
@@ -47,7 +48,7 @@ function readCache(contextDir: string): CacheEntry | null {
|
|
|
47
48
|
function writeCache(contextDir: string, entry: CacheEntry): void {
|
|
48
49
|
const path = cachePath(contextDir);
|
|
49
50
|
try {
|
|
50
|
-
mkdirSync(
|
|
51
|
+
mkdirSync(resolveTelemetryStateDir(contextDir), { recursive: true });
|
|
51
52
|
writeFileSync(path, JSON.stringify(entry, null, 2), "utf8");
|
|
52
53
|
} catch {
|
|
53
54
|
// Cache failures are non-fatal — license check just won't be cached.
|
|
@@ -2,6 +2,7 @@ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { hostname, platform, arch } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
+
import { resolveTelemetryStateDir, telemetryStatePath } from "./state-dir.js";
|
|
5
6
|
|
|
6
7
|
export type TelemetryMetrics = {
|
|
7
8
|
period_start: string;
|
|
@@ -37,7 +38,8 @@ export type TelemetryMetrics = {
|
|
|
37
38
|
const AVG_TOKENS_PER_RESULT = 400;
|
|
38
39
|
|
|
39
40
|
function generateInstanceId(contextDir: string): string {
|
|
40
|
-
const
|
|
41
|
+
const telemetryDir = resolveTelemetryStateDir(contextDir);
|
|
42
|
+
const idPath = join(telemetryDir, "machine_id");
|
|
41
43
|
if (existsSync(idPath)) {
|
|
42
44
|
try {
|
|
43
45
|
const existing = readFileSync(idPath, "utf8").trim();
|
|
@@ -51,7 +53,7 @@ function generateInstanceId(contextDir: string): string {
|
|
|
51
53
|
const fingerprint = `${hostname()}|${platform()}|${arch()}`;
|
|
52
54
|
const id = createHash("sha256").update(fingerprint).digest("hex").slice(0, 16);
|
|
53
55
|
try {
|
|
54
|
-
mkdirSync(
|
|
56
|
+
mkdirSync(telemetryDir, { recursive: true });
|
|
55
57
|
writeFileSync(idPath, id, "utf8");
|
|
56
58
|
} catch (err) {
|
|
57
59
|
process.stderr.write(`[cortex-enterprise] Could not persist instance id: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
@@ -142,8 +144,7 @@ export class TelemetryCollector {
|
|
|
142
144
|
constructor(contextDir: string, clientVersion = "unknown") {
|
|
143
145
|
this.clientVersion = clientVersion;
|
|
144
146
|
this.instanceId = generateInstanceId(contextDir);
|
|
145
|
-
|
|
146
|
-
this.metricsPath = join(telemetryDir, "metrics.json");
|
|
147
|
+
this.metricsPath = telemetryStatePath(contextDir, "metrics.json");
|
|
147
148
|
|
|
148
149
|
// Load existing metrics or start fresh
|
|
149
150
|
try {
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { accessSync, constants, mkdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
const warnedFallbacks = new Set<string>();
|
|
5
|
+
|
|
6
|
+
function canUseDirectory(dir: string): boolean {
|
|
7
|
+
try {
|
|
8
|
+
mkdirSync(dir, { recursive: true });
|
|
9
|
+
accessSync(dir, constants.R_OK | constants.W_OK | constants.X_OK);
|
|
10
|
+
return true;
|
|
11
|
+
} catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function resolveTelemetryStateDir(contextDir: string): string {
|
|
17
|
+
const primary = join(contextDir, "telemetry");
|
|
18
|
+
if (canUseDirectory(primary)) return primary;
|
|
19
|
+
|
|
20
|
+
const fallback = join(contextDir, "cache", "telemetry");
|
|
21
|
+
if (canUseDirectory(fallback)) {
|
|
22
|
+
const warningKey = `${primary}->${fallback}`;
|
|
23
|
+
if (!warnedFallbacks.has(warningKey)) {
|
|
24
|
+
warnedFallbacks.add(warningKey);
|
|
25
|
+
process.stderr.write(
|
|
26
|
+
`[cortex-enterprise] telemetry dir not writable at ${primary}; using ${fallback}\n`,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
return fallback;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return primary;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function telemetryStatePath(
|
|
36
|
+
contextDir: string,
|
|
37
|
+
...parts: string[]
|
|
38
|
+
): string {
|
|
39
|
+
return join(resolveTelemetryStateDir(contextDir), ...parts);
|
|
40
|
+
}
|
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
import { loadEnterpriseConfig, resolveEnterpriseActivation } from "../core/config.js";
|
|
14
14
|
import { pushMetrics } from "../enterprise/telemetry/sync.js";
|
|
15
15
|
import { TelemetryCollector, type TelemetryMetrics } from "../core/telemetry/collector.js";
|
|
16
|
+
import { resolveTelemetryStateDir, telemetryStatePath } from "../core/telemetry/state-dir.js";
|
|
16
17
|
import { AuditWriter, type AuditEntry } from "../core/audit/writer.js";
|
|
17
18
|
import { PolicyStore } from "../core/policy/store.js";
|
|
18
19
|
import {
|
|
@@ -84,7 +85,7 @@ async function policyCheck(
|
|
|
84
85
|
}
|
|
85
86
|
|
|
86
87
|
function readMetrics(contextDir: string): TelemetryMetrics | null {
|
|
87
|
-
const path =
|
|
88
|
+
const path = telemetryStatePath(contextDir, "metrics.json");
|
|
88
89
|
if (!existsSync(path)) return null;
|
|
89
90
|
try {
|
|
90
91
|
return JSON.parse(readFileSync(path, "utf8")) as TelemetryMetrics;
|
|
@@ -103,7 +104,7 @@ type PendingPush = {
|
|
|
103
104
|
};
|
|
104
105
|
|
|
105
106
|
function pendingPushPath(contextDir: string): string {
|
|
106
|
-
return
|
|
107
|
+
return telemetryStatePath(contextDir, "pending-push.json");
|
|
107
108
|
}
|
|
108
109
|
|
|
109
110
|
function readPendingPush(contextDir: string): PendingPush | null {
|
|
@@ -118,7 +119,7 @@ function readPendingPush(contextDir: string): PendingPush | null {
|
|
|
118
119
|
|
|
119
120
|
function writePendingPush(contextDir: string, pending: PendingPush): void {
|
|
120
121
|
const path = pendingPushPath(contextDir);
|
|
121
|
-
mkdirSync(
|
|
122
|
+
mkdirSync(resolveTelemetryStateDir(contextDir), { recursive: true });
|
|
122
123
|
writeFileSync(path, JSON.stringify(pending, null, 2), "utf8");
|
|
123
124
|
}
|
|
124
125
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
-
import { mkdtempSync } from "node:fs";
|
|
3
|
+
import { chmodSync, existsSync, mkdtempSync, mkdirSync } from "node:fs";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
|
|
@@ -28,3 +28,33 @@ test("TelemetryCollector counts context.impact as an impact analysis", () => {
|
|
|
28
28
|
assert.equal(metrics.impact_analyses, 1);
|
|
29
29
|
assert.equal(metrics.tool_metrics["context.impact"].calls, 1);
|
|
30
30
|
});
|
|
31
|
+
|
|
32
|
+
test("TelemetryCollector falls back when .context/telemetry is not writable", () => {
|
|
33
|
+
const contextDir = createContextDir("cortex-telemetry-fallback-");
|
|
34
|
+
const primaryTelemetryDir = path.join(contextDir, "telemetry");
|
|
35
|
+
const fallbackMetricsPath = path.join(
|
|
36
|
+
contextDir,
|
|
37
|
+
"cache",
|
|
38
|
+
"telemetry",
|
|
39
|
+
"metrics.json",
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
mkdirSync(primaryTelemetryDir, { recursive: true });
|
|
43
|
+
chmodSync(primaryTelemetryDir, 0o555);
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const collector = new TelemetryCollector(contextDir, "test-version");
|
|
47
|
+
collector.recordEvent({
|
|
48
|
+
tool: "context.search",
|
|
49
|
+
phase: "success",
|
|
50
|
+
result_count: 1,
|
|
51
|
+
estimated_tokens_saved: 100,
|
|
52
|
+
duration_ms: 10,
|
|
53
|
+
});
|
|
54
|
+
collector.flush();
|
|
55
|
+
|
|
56
|
+
assert.equal(existsSync(fallbackMetricsPath), true);
|
|
57
|
+
} finally {
|
|
58
|
+
chmodSync(primaryTelemetryDir, 0o755);
|
|
59
|
+
}
|
|
60
|
+
});
|