@cachly-dev/mcp-server 0.10.69 → 0.10.71

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/README.md CHANGED
@@ -22,7 +22,7 @@
22
22
  <img src="https://img.shields.io/badge/GDPR-EU%20servers-green" alt="GDPR: EU servers" />
23
23
  </a>
24
24
  &nbsp;
25
- <img src="https://img.shields.io/badge/101%20MCP%20tools-violet" alt="101 MCP tools" />
25
+ <img src="https://img.shields.io/badge/102%20MCP%20tools-violet" alt="102 MCP tools" />
26
26
  &nbsp;
27
27
  <img src="https://img.shields.io/badge/License-Apache--2.0-yellow" alt="License: Apache-2.0" />
28
28
  </p>
@@ -1,3 +1,29 @@
1
1
  export declare function jwtExpiryMs(token: string): number | null;
2
+ /** Long-lived API keys cannot expire silently; raw access tokens can. */
3
+ export declare const API_KEY_PREFIX = "cky_";
4
+ export type AuthState = 'healthy' | 'no_jwt' | 'expired' | 'near_expiry' | 'long_lived';
5
+ export interface AuthDiagnosis {
6
+ state: AuthState;
7
+ /** Safe to make API calls right now? (healthy, near_expiry and long_lived all are.) */
8
+ usable: boolean;
9
+ /** ms until expiry, or null for tokens with no exp claim (long-lived keys). */
10
+ expiresInMs: number | null;
11
+ /** Human-facing message with the single fix, when action is needed. */
12
+ message: string;
13
+ }
14
+ /** A long-lived API key (cky_live_… / cky_…) has no exp and never expires silently. */
15
+ export declare function isLongLivedApiKey(token: string): boolean;
16
+ export declare const NEAR_EXPIRY_MS: number;
17
+ /**
18
+ * Diagnose the current credential without making any network call. Pure +
19
+ * deterministic (pass `now` in tests).
20
+ */
21
+ export declare function diagnoseAuth(jwt: string, now?: number): AuthDiagnosis;
22
+ export type AuthHealAction = 'none' | 'refresh' | 'reauth';
23
+ /**
24
+ * Decide what self-healing step (if any) to take for a diagnosis. Pure so the
25
+ * decision is unit-testable independently of the network-bound executor.
26
+ */
27
+ export declare function planAuthHeal(d: AuthDiagnosis): AuthHealAction;
2
28
  export declare function checkJwt(jwt: string): void;
3
29
  export declare function handleApiError(status: number, detail: string): never;
package/dist/src/auth.js CHANGED
@@ -11,18 +11,90 @@ export function jwtExpiryMs(token) {
11
11
  return null;
12
12
  }
13
13
  }
14
- export function checkJwt(jwt) {
14
+ // ── Self-healing auth ─────────────────────────────────────────────────────────
15
+ // The silent-failure mode we guard against: a token that is missing, expired, or
16
+ // about to expire degrades the brain into "0 recalls" without the user ever being
17
+ // told why. Instead of failing silently (or only at the moment a network call
18
+ // finally 401s), we diagnose the credential up front and either heal it
19
+ // automatically (mint a fresh long-lived key while the current one is still valid)
20
+ // or surface a single, actionable instruction.
21
+ /** Long-lived API keys cannot expire silently; raw access tokens can. */
22
+ export const API_KEY_PREFIX = 'cky_';
23
+ /** A long-lived API key (cky_live_… / cky_…) has no exp and never expires silently. */
24
+ export function isLongLivedApiKey(token) {
25
+ return token.startsWith(API_KEY_PREFIX);
26
+ }
27
+ // Refresh a token this close to expiry (or already expired) the moment we touch it.
28
+ export const NEAR_EXPIRY_MS = 10 * 60 * 1000; // 10 minutes
29
+ const SETUP_URL = 'https://cachly.dev/setup-ai';
30
+ /**
31
+ * Diagnose the current credential without making any network call. Pure +
32
+ * deterministic (pass `now` in tests).
33
+ */
34
+ export function diagnoseAuth(jwt, now = Date.now()) {
15
35
  if (!jwt) {
16
- throw new McpError(ErrorCode.InvalidRequest, 'CACHLY_JWT env var not set.\n\nGet your token at https://cachly.dev/setup-ai and add it to your MCP config:\n CACHLY_JWT=<your-token>');
36
+ return {
37
+ state: 'no_jwt',
38
+ usable: false,
39
+ expiresInMs: null,
40
+ message: `No CACHLY_JWT set — the brain can't persist or recall anything (you'd silently get 0 recalls).\n\nGet your token at ${SETUP_URL} and add it to your MCP config, or just call any tool to start the one-time device-flow sign-in.`,
41
+ };
17
42
  }
18
43
  const expMs = jwtExpiryMs(jwt);
19
- if (expMs !== null && expMs < Date.now()) {
20
- throw new McpError(ErrorCode.InvalidRequest, `CACHLY_JWT expired at ${new Date(expMs).toISOString()}.\n\nGet a fresh token at https://cachly.dev/setup-ai and update CACHLY_JWT in your MCP config.`);
44
+ // No exp claim → long-lived API key. These are the healthy steady state.
45
+ if (expMs === null) {
46
+ return {
47
+ state: isLongLivedApiKey(jwt) ? 'long_lived' : 'healthy',
48
+ usable: true,
49
+ expiresInMs: null,
50
+ message: '',
51
+ };
52
+ }
53
+ const remaining = expMs - now;
54
+ if (remaining <= 0) {
55
+ return {
56
+ state: 'expired',
57
+ usable: false,
58
+ expiresInMs: remaining,
59
+ message: `CACHLY_JWT expired at ${new Date(expMs).toISOString()} — recalls will silently return nothing until you re-authenticate.\n\nGet a fresh token at ${SETUP_URL}, or call any tool to re-run the device-flow sign-in.`,
60
+ };
61
+ }
62
+ if (remaining <= NEAR_EXPIRY_MS) {
63
+ const mins = Math.max(1, Math.round(remaining / 60_000));
64
+ return {
65
+ state: 'near_expiry',
66
+ usable: true,
67
+ expiresInMs: remaining,
68
+ message: `CACHLY_JWT expires in ~${mins} min. cachly will refresh it automatically into a long-lived key while it's still valid.`,
69
+ };
70
+ }
71
+ return { state: 'healthy', usable: true, expiresInMs: remaining, message: '' };
72
+ }
73
+ /**
74
+ * Decide what self-healing step (if any) to take for a diagnosis. Pure so the
75
+ * decision is unit-testable independently of the network-bound executor.
76
+ */
77
+ export function planAuthHeal(d) {
78
+ switch (d.state) {
79
+ case 'no_jwt':
80
+ case 'expired':
81
+ return 'reauth';
82
+ case 'near_expiry':
83
+ return 'refresh';
84
+ case 'healthy':
85
+ case 'long_lived':
86
+ return 'none';
87
+ }
88
+ }
89
+ export function checkJwt(jwt) {
90
+ const d = diagnoseAuth(jwt);
91
+ if (!d.usable) {
92
+ throw new McpError(ErrorCode.InvalidRequest, d.message);
21
93
  }
22
94
  }
23
95
  export function handleApiError(status, detail) {
24
96
  if (status === 401) {
25
- throw new McpError(ErrorCode.InvalidRequest, `Authentication failed (401): ${detail}\n\nYour CACHLY_JWT may be expired or invalid. Get a fresh token at https://cachly.dev/setup-ai`);
97
+ throw new McpError(ErrorCode.InvalidRequest, `Authentication failed (401): ${detail}\n\nYour CACHLY_JWT may be expired or invalid. Get a fresh token at ${SETUP_URL}`);
26
98
  }
27
99
  if (status === 403) {
28
100
  throw new McpError(ErrorCode.InvalidRequest, `Access denied (403): ${detail}\n\nCheck that your CACHLY_JWT belongs to an account with access to this resource.`);
@@ -1 +1 @@
1
- {"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,oCAAoC,CAAC;AAEzE,MAAM,UAAU,WAAW,CAAC,KAAa;IACvC,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC/B,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;QACpC,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CACxB,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CACjC,CAAC;QACtB,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;IACjD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,GAAW;IAClC,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,MAAM,IAAI,QAAQ,CAChB,SAAS,CAAC,cAAc,EACxB,wIAAwI,CACzI,CAAC;IACJ,CAAC;IACD,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC;IAC/B,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;QACzC,MAAM,IAAI,QAAQ,CAChB,SAAS,CAAC,cAAc,EACxB,yBAAyB,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,WAAW,EAAE,iGAAiG,CACxJ,CAAC;IACJ,CAAC;AACH,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,MAAc,EAAE,MAAc;IAC3D,IAAI,MAAM,KAAK,GAAG,EAAE,CAAC;QACnB,MAAM,IAAI,QAAQ,CAChB,SAAS,CAAC,cAAc,EACxB,gCAAgC,MAAM,iGAAiG,CACxI,CAAC;IACJ,CAAC;IACD,IAAI,MAAM,KAAK,GAAG,EAAE,CAAC;QACnB,MAAM,IAAI,QAAQ,CAChB,SAAS,CAAC,cAAc,EACxB,wBAAwB,MAAM,oFAAoF,CACnH,CAAC;IACJ,CAAC;IACD,MAAM,IAAI,QAAQ,CAAC,SAAS,CAAC,aAAa,EAAE,oBAAoB,MAAM,KAAK,MAAM,EAAE,CAAC,CAAC;AACvF,CAAC"}
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/auth.ts"],"names":[],"mappings":"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"}
package/dist/src/ckg.d.ts CHANGED
@@ -31,6 +31,15 @@ export type FileNode = {
31
31
  count: number;
32
32
  ts: string;
33
33
  };
34
+ export type ServiceNode = {
35
+ id: string;
36
+ name: string;
37
+ kind: 'service' | 'system';
38
+ domain: string;
39
+ type: 'service';
40
+ count: number;
41
+ last_active: string;
42
+ };
34
43
  export declare function ckgSlug(text: string): string;
35
44
  /** Extract 1-3 significant keywords from free text for a problem concept */
36
45
  export declare function extractProblemConcept(text: string): string | null;
@@ -39,5 +48,7 @@ export declare function ckgUpsertNode(redis: Redis, id: string, domain: string,
39
48
  export declare function ckgUpsertPersonNode(redis: Redis, handle: string, domain: string): Promise<string>;
40
49
  /** Upsert a File node; returns the node id ("file:{slug}"). */
41
50
  export declare function ckgUpsertFileNode(redis: Redis, filePath: string): Promise<string>;
51
+ /** Upsert a Service/System node; returns the node id ("service:{slug}"). */
52
+ export declare function ckgUpsertServiceNode(redis: Redis, name: string, domain: string, kind?: 'service' | 'system'): Promise<string>;
42
53
  export declare function ckgUpdateEdge(redis: Redis, from: string, edgeType: string, to: string, success: boolean, partial?: boolean): Promise<void>;
43
54
  export declare function ckgRecordCollaboration(redis: Redis, fileId: string, personId: string): Promise<void>;
package/dist/src/ckg.js CHANGED
@@ -49,6 +49,25 @@ export async function ckgUpsertFileNode(redis, filePath) {
49
49
  await redis.set(key, JSON.stringify(node));
50
50
  return id;
51
51
  }
52
+ /** Upsert a Service/System node; returns the node id ("service:{slug}"). */
53
+ export async function ckgUpsertServiceNode(redis, name, domain, kind = 'service') {
54
+ const id = `service:${ckgSlug(name)}`;
55
+ const key = `cachly:ckg:node:${id}`;
56
+ const raw = await redis.get(key);
57
+ const node = raw
58
+ ? JSON.parse(raw)
59
+ : { id, name, kind, domain, type: 'service', count: 0, last_active: new Date().toISOString() };
60
+ node.count = (node.count || 0) + 1;
61
+ node.last_active = new Date().toISOString();
62
+ if (!node.domain)
63
+ node.domain = domain;
64
+ // A node may start life as a 'service' and later be classified as 'system' (or vice
65
+ // versa) by an explicit caller — honour the latest non-default classification.
66
+ if (kind === 'system')
67
+ node.kind = 'system';
68
+ await redis.set(key, JSON.stringify(node));
69
+ return id;
70
+ }
52
71
  export async function ckgUpdateEdge(redis, from, edgeType, to, success, partial = false) {
53
72
  const key = `cachly:ckg:edge:${from}:${edgeType}:${to}`;
54
73
  const raw = await redis.get(key);
@@ -1 +1 @@
1
- {"version":3,"file":"ckg.js","sourceRoot":"","sources":["../../src/ckg.ts"],"names":[],"mappings":"AAiCA,MAAM,aAAa,GAAG,IAAI,GAAG,CAAC,CAAC,MAAM,EAAC,MAAM,EAAC,MAAM,EAAC,MAAM,EAAC,MAAM,EAAC,MAAM,EAAC,MAAM,EAAC,MAAM,EAAC,MAAM,EAAC,MAAM,EAAC,MAAM,EAAC,MAAM,EAAC,MAAM,EAAC,MAAM,EAAC,MAAM,EAAC,MAAM,EAAC,MAAM,EAAC,MAAM,EAAC,MAAM,EAAC,MAAM,EAAC,OAAO,EAAC,OAAO,EAAC,OAAO,EAAC,OAAO,EAAC,OAAO,EAAC,OAAO,EAAC,OAAO,EAAC,QAAQ,EAAC,OAAO,EAAC,QAAQ,EAAC,OAAO,CAAC,CAAC,CAAC;AAEvQ,MAAM,UAAU,OAAO,CAAC,IAAY;IAClC,OAAO,IAAI,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,eAAe,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AACjH,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,qBAAqB,CAAC,IAAY;IAChD,MAAM,KAAK,GAAG,IAAI,CAAC,WAAW,EAAE;SAC7B,OAAO,CAAC,iBAAiB,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC;SAC5C,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;SAClD,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACf,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACpC,OAAO,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACrC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,KAAY,EAAE,EAAU,EAAE,MAAc,EAAE,IAAY;IACxF,MAAM,GAAG,GAAG,mBAAmB,EAAE,EAAE,CAAC;IACpC,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACjC,MAAM,IAAI,GAAY,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,CAAC;IAC3G,IAAI,CAAC,KAAK,GAAG,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;IACnC,IAAI,CAAC,EAAE,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IACnC,MAAM,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC;AAC7C,CAAC;AAED,mEAAmE;AACnE,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,KAAY,EAAE,MAAc,EAAE,MAAc;IACpF,MAAM,EAAE,GAAG,UAAU,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;IACvC,MAAM,GAAG,GAAG,mBAAmB,EAAE,EAAE,CAAC;IACpC,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACjC,MAAM,IAAI,GAAe,GAAG;QAC1B,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC;QACjB,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,EAAE,WAAW,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,CAAC;IAC5F,IAAI,CAAC,KAAK,GAAG,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;IACnC,IAAI,CAAC,WAAW,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAC5C,IAAI,CAAC,IAAI,CAAC,MAAM;QAAE,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvC,MAAM,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC;IAC3C,OAAO,EAAE,CAAC;AACZ,CAAC;AAED,+DAA+D;AAC/D,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,KAAY,EAAE,QAAgB;IACpE,MAAM,GAAG,GAAG,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC;IAC7C,MAAM,EAAE,GAAG,QAAQ,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;IACvC,MAAM,GAAG,GAAG,mBAAmB,EAAE,EAAE,CAAC;IACpC,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACjC,MAAM,IAAI,GAAa,GAAG;QACxB,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC;QACjB,CAAC,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,CAAC;IAC9F,IAAI,CAAC,KAAK,GAAG,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;IACnC,IAAI,CAAC,EAAE,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IACnC,MAAM,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC;IAC3C,OAAO,EAAE,CAAC;AACZ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,KAAY,EAAE,IAAY,EAAE,QAAgB,EAAE,EAAU,EAAE,OAAgB,EAAE,OAAO,GAAG,KAAK;IAC7H,MAAM,GAAG,GAAG,mBAAmB,IAAI,IAAI,QAAQ,IAAI,EAAE,EAAE,CAAC;IACxD,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACjC,MAAM,IAAI,GAAY,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,YAAY,EAAE,EAAE,EAAE,CAAC;IAC/H,IAAI,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;IACrC,IAAI,OAAO;QAAE,IAAI,CAAC,SAAS,GAAG,CAAC,IAAI,CAAC,SAAS,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;SACnD,IAAI,OAAO;QAAE,IAAI,CAAC,SAAS,GAAG,CAAC,IAAI,CAAC,SAAS,IAAI,CAAC,CAAC,GAAG,GAAG,CAAC;IAC/D,oEAAoE;IACpE,IAAI,CAAC,UAAU,GAAG,CAAC,IAAI,CAAC,SAAS,GAAG,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAC3D,IAAI,CAAC,YAAY,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAC7C,MAAM,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC;IAC3C,+DAA+D;IAC/D,MAAM,KAAK,CAAC,IAAI,CAAC,uBAAuB,IAAI,EAAE,EAAE,GAAG,CAAC,CAAC;IACrD,MAAM,KAAK,CAAC,IAAI,CAAC,qBAAqB,EAAE,EAAE,EAAE,GAAG,CAAC,CAAC;AACnD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,eAAe,GAAG,EAAE,CAAC;AAC3B,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAAC,KAAY,EAAE,MAAc,EAAE,QAAgB;IACzF,MAAM,WAAW,GAAG,4BAA4B,MAAM,EAAE,CAAC;IACzD,MAAM,aAAa,GAAG,MAAM,KAAK,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;IACxD,KAAK,MAAM,KAAK,IAAI,aAAa,CAAC,KAAK,CAAC,CAAC,EAAE,eAAe,CAAC,EAAE,CAAC;QAC5D,IAAI,KAAK,KAAK,QAAQ;YAAE,SAAS;QACjC,uDAAuD;QACvD,MAAM,aAAa,CAAC,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;QAClE,MAAM,aAAa,CAAC,KAAK,EAAE,KAAK,EAAE,cAAc,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAC;IACpE,CAAC;IACD,MAAM,KAAK,CAAC,IAAI,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;AAC1C,CAAC"}
1
+ {"version":3,"file":"ckg.js","sourceRoot":"","sources":["../../src/ckg.ts"],"names":[],"mappings":"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"}
@@ -1,13 +1,26 @@
1
1
  import { execSync } from 'node:child_process';
2
2
  import { calculateConfidence, confidenceBadge, STRUCTURED_TEMPLATES, CONFIDENCE_WARN_VALUE, CONFIDENCE_STALE_VALUE, CONFIDENCE_WARN_DAYS } from '../confidence.js';
3
- import { ckgSlug, extractProblemConcept, ckgUpsertNode, ckgUpdateEdge, ckgUpsertPersonNode, ckgUpsertFileNode, ckgRecordCollaboration } from '../ckg.js';
3
+ import { ckgSlug, extractProblemConcept, ckgUpsertNode, ckgUpdateEdge, ckgUpsertPersonNode, ckgUpsertFileNode, ckgRecordCollaboration, ckgUpsertServiceNode } from '../ckg.js';
4
4
  import { safeJsonParse, scanKeys } from '../utils.js';
5
5
  import { keywordSearch, tokenize, splitMultiQuery, levenshtein, indexVocab as _indexVocab } from '../search.js';
6
6
  import { rerankByQuality } from '../rerank.js';
7
7
  import { computeEmbedding, hasEmbedProvider } from '../embeddings.js';
8
8
  // ── Changelog (shown once per version in session_start) ──────────────────────
9
- const MCP_VERSION = '0.10.69';
9
+ const MCP_VERSION = '0.10.71';
10
10
  const WHATS_NEW = {
11
+ '0.10.71': [
12
+ `🛡️ **Self-healing auth** — no more silent "0 recalls because the token quietly died"`,
13
+ ` ♻️ Near-expiry tokens auto-refresh into a long-lived key while still valid (zero interaction)`,
14
+ ` 🔁 A rejected (401) call self-heals once and retries before surfacing an error`,
15
+ ` 📣 If the credential is truly dead, session_start + get_api_status say so plainly + how to fix`,
16
+ ],
17
+ '0.10.70': [
18
+ `🛰️ **Service/System nodes in the graph (Phase 3)** — the brain now models running systems`,
19
+ ` 🆕 \`brain_service_map(service="prometheus")\` — who owns it + every failure/fix known`,
20
+ ` 🏷️ Tag lessons with \`service="..."\` (\`service_kind="system"\` for infra) — person→operates, file→runs_in edges`,
21
+ ` 🚨 Incident triage: a restarting pod → instant "who knows this and what's broken before"`,
22
+ ` 📊 102 MCP tools`,
23
+ ],
11
24
  '0.10.69': [
12
25
  `📁 **Personalized context-aware recall (Phase 3)** — pass \`context_files\` to \`smart_recall\``,
13
26
  ` 🎯 Lessons learned on your current files bubble up — even when the query doesn't name the file`,
@@ -87,12 +100,12 @@ export const BRAIN_TOOL_NAMES = new Set([
87
100
  'learn_from_attempts', 'recall_best_solution', 'smart_recall',
88
101
  'session_start', 'session_end', 'session_ping', 'session_handoff', 'auto_learn_session',
89
102
  'brain_who_knows', 'brain_file_map', 'team_expertise_map',
90
- 'skill_gaps', 'brain_coverage', 'brain_metrics',
103
+ 'skill_gaps', 'brain_coverage', 'brain_metrics', 'brain_service_map',
91
104
  ]);
92
105
  export async function handleBrainTool(name, args, getConnection, apiFetch) {
93
106
  switch (name) {
94
107
  case 'learn_from_attempts': {
95
- const { instance_id, topic, outcome, what_worked, what_failed = '', context: ctx = '', severity = 'major', file_paths = [], commands = [], tags = [], depends_on = [], author = '', visibility = 'team', } = args;
108
+ const { instance_id, topic, outcome, what_worked, what_failed = '', context: ctx = '', severity = 'major', file_paths = [], commands = [], tags = [], depends_on = [], author = '', visibility = 'team', service = '', service_kind = 'service', } = args;
96
109
  const redis = await getConnection(instance_id);
97
110
  const ts = new Date().toISOString();
98
111
  // ── Structured template hints ──────────────────────────────────────────
@@ -210,6 +223,7 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
210
223
  tags,
211
224
  depends_on,
212
225
  ...(author ? { author } : {}),
226
+ ...(service ? { service } : {}),
213
227
  visibility,
214
228
  recall_count: recallCount,
215
229
  ts,
@@ -378,6 +392,30 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
378
392
  }
379
393
  catch { /* non-critical */ }
380
394
  }
395
+ // ── Phase 3: Service/System nodes ─────────────────────────────────────────
396
+ // When a lesson names the service/system it concerns (e.g. "prometheus",
397
+ // "cachly-web"), wire it into the graph: person→operates→service,
398
+ // concept→affects→service, file→runs_in→service. This is what lets
399
+ // brain_service_map answer "who owns X and what's gone wrong with X before".
400
+ if (service && typeof service === 'string' && service.trim()) {
401
+ try {
402
+ const domain = topic.split(':')[0] ?? 'unknown';
403
+ const kind = service_kind === 'system' ? 'system' : 'service';
404
+ const serviceId = await ckgUpsertServiceNode(redis, service.trim(), domain, kind);
405
+ const conceptId = ckgSlug(topic);
406
+ // concept → affects → service (outcome decides edge confidence)
407
+ await ckgUpdateEdge(redis, conceptId, 'affects', serviceId, outcome === 'success', outcome === 'partial');
408
+ if (author) {
409
+ const personId = `person:${ckgSlug(author)}`;
410
+ await ckgUpdateEdge(redis, personId, 'operates', serviceId, true);
411
+ }
412
+ for (const fp of file_paths.slice(0, 8)) {
413
+ const fileId = `file:${ckgSlug(fp)}`;
414
+ await ckgUpdateEdge(redis, fileId, 'runs_in', serviceId, true);
415
+ }
416
+ }
417
+ catch { /* non-critical */ }
418
+ }
381
419
  // ── Ambient team propagation: if author is set + outcome is success,
382
420
  // auto-store as a team lesson so knowledge is shared without a separate
383
421
  // team_learn call. Tags gain 'team'; the lesson is attributed to the author.
@@ -2417,6 +2455,120 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
2417
2455
  ].filter(Boolean);
2418
2456
  return lines.join('\n');
2419
2457
  }
2458
+ // ── brain_service_map ───────────────────────────────────────────────────────
2459
+ // Phase 3: "Who owns this service, and what's gone wrong with it before?"
2460
+ // Bridges a running system (e.g. a restarting prometheus pod) to the people who
2461
+ // operate it, the files that run in it, and the lessons learned operating it.
2462
+ case 'brain_service_map': {
2463
+ const { instance_id, service, } = args;
2464
+ if (!service || typeof service !== 'string' || !service.trim()) {
2465
+ return '⚠️ `brain_service_map` requires a non-empty `service`. Example: `brain_service_map(service="prometheus")`.';
2466
+ }
2467
+ const redis = await getConnection(instance_id);
2468
+ const serviceId = `service:${ckgSlug(service.trim())}`;
2469
+ const nodeRaw = await redis.get(`cachly:ckg:node:${serviceId}`).catch(() => null);
2470
+ const node = nodeRaw ? safeJsonParse(nodeRaw, null) : null;
2471
+ // Inbound edges: person→operates, concept→affects, file→runs_in
2472
+ const inbound = await redis.smembers(`cachly:ckg:idx:to:${serviceId}`).catch(() => []);
2473
+ const operators = [];
2474
+ const files = [];
2475
+ const concepts = [];
2476
+ for (const ek of inbound) {
2477
+ const er = await redis.get(ek).catch(() => null);
2478
+ if (!er)
2479
+ continue;
2480
+ const edge = safeJsonParse(er, null);
2481
+ if (!edge)
2482
+ continue;
2483
+ if (edge.edgeType === 'operates') {
2484
+ const pr = await redis.get(`cachly:ckg:node:${edge.from}`).catch(() => null);
2485
+ const pn = pr ? safeJsonParse(pr, null) : null;
2486
+ if (pn?.handle)
2487
+ operators.push({ handle: pn.handle, trials: edge.trials });
2488
+ }
2489
+ else if (edge.edgeType === 'runs_in') {
2490
+ const fr = await redis.get(`cachly:ckg:node:${edge.from}`).catch(() => null);
2491
+ const fn = fr ? safeJsonParse(fr, null) : null;
2492
+ if (fn?.path)
2493
+ files.push({ path: fn.path, trials: edge.trials });
2494
+ }
2495
+ else if (edge.edgeType === 'affects') {
2496
+ concepts.push({ id: edge.from, confidence: edge.confidence });
2497
+ }
2498
+ }
2499
+ operators.sort((a, b) => b.trials - a.trials);
2500
+ files.sort((a, b) => b.trials - a.trials);
2501
+ concepts.sort((a, b) => b.confidence - a.confidence);
2502
+ const lessons = [];
2503
+ const lessonKeys = await scanKeys(redis, 'cachly:lesson:best:*', { max: 1500 });
2504
+ const svcLower = service.trim().toLowerCase();
2505
+ for (const k of lessonKeys.slice(0, 400)) {
2506
+ const raw = await redis.get(k).catch(() => null);
2507
+ if (!raw)
2508
+ continue;
2509
+ const ld = safeJsonParse(raw, {});
2510
+ if (ld.visibility === 'private')
2511
+ continue;
2512
+ if ((ld.service ?? '').toLowerCase() !== svcLower)
2513
+ continue;
2514
+ lessons.push({ ...ld, topic: k.replace('cachly:lesson:best:', '') });
2515
+ }
2516
+ if (!node && lessons.length === 0 && operators.length === 0) {
2517
+ return [
2518
+ `## 🛰️ Service Map — \`${service}\``,
2519
+ ``,
2520
+ `Nothing known about this service yet.`,
2521
+ ``,
2522
+ `Tag lessons with the service they concern so the brain can build this map:`,
2523
+ `\`learn_from_attempts(topic="...", service="${service}", author="you", file_paths=[...])\``,
2524
+ ].join('\n');
2525
+ }
2526
+ const kindLabel = node?.kind === 'system' ? '🖥️ system' : '🛰️ service';
2527
+ const lines = [
2528
+ `## ${node?.kind === 'system' ? '🖥️' : '🛰️'} Service Map — \`${service}\``,
2529
+ ``,
2530
+ node ? `_${kindLabel} · ${node.count} lesson${node.count !== 1 ? 's' : ''} referenced · domain: ${node.domain}_` : `_No graph node yet — built from tagged lessons below._`,
2531
+ ``,
2532
+ ];
2533
+ // Operators (who owns it)
2534
+ if (operators.length > 0) {
2535
+ const owners = operators.slice(0, 5).map((o, i) => {
2536
+ const medal = i === 0 ? '🥇' : i === 1 ? '🥈' : i === 2 ? '🥉' : `${i + 1}.`;
2537
+ return `${medal} **${o.handle}** (${o.trials}×)`;
2538
+ }).join(' · ');
2539
+ lines.push(`**Operators:** ${owners}`, ``);
2540
+ }
2541
+ else {
2542
+ lines.push(`**Operators:** _None attributed — add \`author\` when learning about this service_`, ``);
2543
+ }
2544
+ // Recent failures first — the triage gold for an incident
2545
+ const failures = lessons.filter(l => l.outcome === 'failure');
2546
+ const successes = lessons.filter(l => l.outcome !== 'failure');
2547
+ if (failures.length > 0) {
2548
+ lines.push(`**⚠️ Known failures (${failures.length}):**`);
2549
+ for (const l of failures.slice(0, 6)) {
2550
+ const sev = l.severity === 'critical' ? ' 🔴' : l.severity === 'major' ? ' 🟡' : '';
2551
+ const by = l.author ? ` · 👤 ${l.author}` : '';
2552
+ lines.push(` - ❌ \`${l.topic}\`${sev}${by} — ${(l.what_worked ?? '').slice(0, 100)}`);
2553
+ }
2554
+ lines.push(``);
2555
+ }
2556
+ if (successes.length > 0) {
2557
+ lines.push(`**✅ Proven fixes (${successes.length}):**`);
2558
+ for (const l of successes.slice(0, 6)) {
2559
+ const emoji = l.outcome === 'partial' ? '⚠️' : '✅';
2560
+ const by = l.author ? ` · 👤 ${l.author}` : '';
2561
+ lines.push(` - ${emoji} \`${l.topic}\`${by} — ${(l.what_worked ?? '').slice(0, 100)}`);
2562
+ }
2563
+ lines.push(``);
2564
+ }
2565
+ // Files that run in this service
2566
+ if (files.length > 0) {
2567
+ lines.push(`**Files in this service:** ${files.slice(0, 8).map(f => `\`${f.path}\``).join(', ')}`, ``);
2568
+ }
2569
+ lines.push(`---`, `_Tag lessons with \`service="${service}"\` to keep this map current. For infra, pass \`service_kind="system"\`._`);
2570
+ return lines.join('\n');
2571
+ }
2420
2572
  // ── sync_file_changes ─────────────────────────────────────────────────────
2421
2573
  default:
2422
2574
  return null;