@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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@danielblomma/cortex-mcp",
3
3
  "mcpName": "io.github.DanielBlomma/cortex",
4
- "version": "2.0.15",
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 = join(contextDir, "telemetry", "machine_id");
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 join(contextDir, "telemetry", CACHE_FILE);
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(join(contextDir, "telemetry"), { recursive: true });
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 idPath = join(contextDir, "telemetry", "machine_id");
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(join(contextDir, "telemetry"), { recursive: true });
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
- const telemetryDir = join(contextDir, "telemetry");
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 = join(contextDir, "telemetry", "metrics.json");
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 join(contextDir, "telemetry", "pending-push.json");
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(join(contextDir, "telemetry"), { recursive: true });
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
+ });