@cachly-dev/mcp-server 0.10.129 → 0.10.130

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 (223) hide show
  1. package/README.md +2 -2
  2. package/dist/index.js +1 -7
  3. package/dist/packages/telegram-notify/client.js +2 -75
  4. package/dist/src/ambient-cli.js +7 -253
  5. package/dist/src/ambient-deps.js +1 -55
  6. package/dist/src/ambient-hooks.js +5 -278
  7. package/dist/src/ambient-ledger.js +6 -95
  8. package/dist/src/ambient-memory.js +1 -67
  9. package/dist/src/ambient-recall.js +1 -221
  10. package/dist/src/aussetzer.js +1 -225
  11. package/dist/src/auth.js +9 -140
  12. package/dist/src/autor-abzeichen.js +1 -56
  13. package/dist/src/autor.js +1 -136
  14. package/dist/src/bauabdruck.js +1 -93
  15. package/dist/src/bedeutung.js +2 -323
  16. package/dist/src/bench/cachly-bench.js +4 -249
  17. package/dist/src/bench/external-corpus.js +4 -135
  18. package/dist/src/bench/fixtures.js +1 -225
  19. package/dist/src/brain-export.js +16 -119
  20. package/dist/src/brain-watch-hook.js +8 -86
  21. package/dist/src/cachly-url.js +1 -21
  22. package/dist/src/ckg.js +1 -108
  23. package/dist/src/cls-hook.js +8 -94
  24. package/dist/src/confidence.js +1 -50
  25. package/dist/src/credentials.js +1 -85
  26. package/dist/src/deutsch.js +1 -223
  27. package/dist/src/doctor.js +2 -248
  28. package/dist/src/editor.js +1 -63
  29. package/dist/src/eingaenge-nachruesten.js +1 -290
  30. package/dist/src/eingaenge.js +2 -260
  31. package/dist/src/embeddings.js +13 -223
  32. package/dist/src/feldreparatur.js +3 -113
  33. package/dist/src/first-contact.js +4 -108
  34. package/dist/src/first-recall-proof.js +4 -41
  35. package/dist/src/funnel-milestones.js +2 -48
  36. package/dist/src/geliefert.js +1 -114
  37. package/dist/src/handlers/advanced.js +38 -537
  38. package/dist/src/handlers/brain.js +69 -4397
  39. package/dist/src/handlers/cache.js +59 -638
  40. package/dist/src/handlers/context.js +17 -182
  41. package/dist/src/handlers/fedbrain.js +40 -1436
  42. package/dist/src/handlers/instances.js +16 -201
  43. package/dist/src/handlers/roadmap.js +13 -175
  44. package/dist/src/handlers/share.js +19 -530
  45. package/dist/src/handlers/syndicate.js +38 -904
  46. package/dist/src/handlers/team.js +89 -1865
  47. package/dist/src/handlers/viz.js +3 -148
  48. package/dist/src/index.js +321 -4281
  49. package/dist/src/interferenz.js +1 -141
  50. package/dist/src/kostprobe.js +7 -125
  51. package/dist/src/lesson-preview.js +2 -133
  52. package/dist/src/namespace.js +1 -128
  53. package/dist/src/notifier.js +1 -6
  54. package/dist/src/rangfolge-stellschrauben.js +1 -0
  55. package/dist/src/rangfolge.js +1 -249
  56. package/dist/src/raumkorrektur.js +1 -197
  57. package/dist/src/recall-tiefe.js +1 -42
  58. package/dist/src/rerank.js +1 -154
  59. package/dist/src/resonanz.js +1 -140
  60. package/dist/src/search.js +1 -2104
  61. package/dist/src/seltenheitsbestand.js +1 -92
  62. package/dist/src/speicherlage.js +1 -84
  63. package/dist/src/spuren.js +1 -150
  64. package/dist/src/starter-corpus.js +1 -165
  65. package/dist/src/telemetry-types.js +0 -13
  66. package/dist/src/tools.js +10 -2465
  67. package/dist/src/toolspecs.js +1 -121
  68. package/dist/src/upgrade-nudge.js +1 -29
  69. package/dist/src/utils.js +1 -89
  70. package/dist/src/versuch.js +1 -264
  71. package/dist/src/vorspann.js +1 -146
  72. package/dist/src/wartung/vektoren-verdichten.js +1 -200
  73. package/dist/src/werkzeug-nutzung.js +1 -72
  74. package/dist/src/wertbeitrag.js +1 -203
  75. package/dist/src/wirkungsspur.js +1 -0
  76. package/glama.json +1 -1
  77. package/package.json +8 -7
  78. package/server.json +3 -3
  79. package/dist/index.d.ts +0 -1
  80. package/dist/index.js.map +0 -1
  81. package/dist/packages/telegram-notify/client.d.ts +0 -19
  82. package/dist/packages/telegram-notify/client.js.map +0 -1
  83. package/dist/src/ambient-cli.d.ts +0 -101
  84. package/dist/src/ambient-cli.js.map +0 -1
  85. package/dist/src/ambient-deps.d.ts +0 -26
  86. package/dist/src/ambient-deps.js.map +0 -1
  87. package/dist/src/ambient-hooks.d.ts +0 -137
  88. package/dist/src/ambient-hooks.js.map +0 -1
  89. package/dist/src/ambient-ledger.d.ts +0 -19
  90. package/dist/src/ambient-ledger.js.map +0 -1
  91. package/dist/src/ambient-memory.d.ts +0 -12
  92. package/dist/src/ambient-memory.js.map +0 -1
  93. package/dist/src/ambient-recall.d.ts +0 -127
  94. package/dist/src/ambient-recall.js.map +0 -1
  95. package/dist/src/aussetzer.d.ts +0 -156
  96. package/dist/src/aussetzer.js.map +0 -1
  97. package/dist/src/auth.d.ts +0 -43
  98. package/dist/src/auth.js.map +0 -1
  99. package/dist/src/autor-abzeichen.d.ts +0 -41
  100. package/dist/src/autor-abzeichen.js.map +0 -1
  101. package/dist/src/autor.d.ts +0 -101
  102. package/dist/src/autor.js.map +0 -1
  103. package/dist/src/bauabdruck-daten.generiert.json +0 -5
  104. package/dist/src/bauabdruck.d.ts +0 -38
  105. package/dist/src/bauabdruck.js.map +0 -1
  106. package/dist/src/bedeutung.d.ts +0 -168
  107. package/dist/src/bedeutung.js.map +0 -1
  108. package/dist/src/bench/cachly-bench.d.ts +0 -61
  109. package/dist/src/bench/cachly-bench.js.map +0 -1
  110. package/dist/src/bench/external-corpus.d.ts +0 -44
  111. package/dist/src/bench/external-corpus.js.map +0 -1
  112. package/dist/src/bench/fixtures.d.ts +0 -37
  113. package/dist/src/bench/fixtures.js.map +0 -1
  114. package/dist/src/brain-export.d.ts +0 -64
  115. package/dist/src/brain-export.js.map +0 -1
  116. package/dist/src/brain-watch-hook.d.ts +0 -21
  117. package/dist/src/brain-watch-hook.js.map +0 -1
  118. package/dist/src/cachly-url.d.ts +0 -68
  119. package/dist/src/cachly-url.js.map +0 -1
  120. package/dist/src/ckg.d.ts +0 -54
  121. package/dist/src/ckg.js.map +0 -1
  122. package/dist/src/cls-hook.d.ts +0 -15
  123. package/dist/src/cls-hook.js.map +0 -1
  124. package/dist/src/confidence.d.ts +0 -19
  125. package/dist/src/confidence.js.map +0 -1
  126. package/dist/src/credentials.d.ts +0 -37
  127. package/dist/src/credentials.js.map +0 -1
  128. package/dist/src/deutsch.d.ts +0 -98
  129. package/dist/src/deutsch.js.map +0 -1
  130. package/dist/src/doctor.d.ts +0 -47
  131. package/dist/src/doctor.js.map +0 -1
  132. package/dist/src/editor.d.ts +0 -9
  133. package/dist/src/editor.js.map +0 -1
  134. package/dist/src/eingaenge-nachruesten.d.ts +0 -41
  135. package/dist/src/eingaenge-nachruesten.js.map +0 -1
  136. package/dist/src/eingaenge.d.ts +0 -159
  137. package/dist/src/eingaenge.js.map +0 -1
  138. package/dist/src/embeddings.d.ts +0 -48
  139. package/dist/src/embeddings.js.map +0 -1
  140. package/dist/src/feldreparatur.d.ts +0 -58
  141. package/dist/src/feldreparatur.js.map +0 -1
  142. package/dist/src/first-contact.d.ts +0 -56
  143. package/dist/src/first-contact.js.map +0 -1
  144. package/dist/src/first-recall-proof.d.ts +0 -19
  145. package/dist/src/first-recall-proof.js.map +0 -1
  146. package/dist/src/funnel-milestones.d.ts +0 -5
  147. package/dist/src/funnel-milestones.js.map +0 -1
  148. package/dist/src/geliefert.d.ts +0 -63
  149. package/dist/src/geliefert.js.map +0 -1
  150. package/dist/src/handlers/advanced.d.ts +0 -6
  151. package/dist/src/handlers/advanced.js.map +0 -1
  152. package/dist/src/handlers/brain.d.ts +0 -41
  153. package/dist/src/handlers/brain.js.map +0 -1
  154. package/dist/src/handlers/cache.d.ts +0 -6
  155. package/dist/src/handlers/cache.js.map +0 -1
  156. package/dist/src/handlers/context.d.ts +0 -6
  157. package/dist/src/handlers/context.js.map +0 -1
  158. package/dist/src/handlers/fedbrain.d.ts +0 -18
  159. package/dist/src/handlers/fedbrain.js.map +0 -1
  160. package/dist/src/handlers/instances.d.ts +0 -33
  161. package/dist/src/handlers/instances.js.map +0 -1
  162. package/dist/src/handlers/roadmap.d.ts +0 -6
  163. package/dist/src/handlers/roadmap.js.map +0 -1
  164. package/dist/src/handlers/share.d.ts +0 -8
  165. package/dist/src/handlers/share.js.map +0 -1
  166. package/dist/src/handlers/syndicate.d.ts +0 -6
  167. package/dist/src/handlers/syndicate.js.map +0 -1
  168. package/dist/src/handlers/team.d.ts +0 -45
  169. package/dist/src/handlers/team.js.map +0 -1
  170. package/dist/src/handlers/viz.d.ts +0 -6
  171. package/dist/src/handlers/viz.js.map +0 -1
  172. package/dist/src/index.d.ts +0 -93
  173. package/dist/src/index.js.map +0 -1
  174. package/dist/src/interferenz.d.ts +0 -111
  175. package/dist/src/interferenz.js.map +0 -1
  176. package/dist/src/kostprobe.d.ts +0 -25
  177. package/dist/src/kostprobe.js.map +0 -1
  178. package/dist/src/lesson-preview.d.ts +0 -81
  179. package/dist/src/lesson-preview.js.map +0 -1
  180. package/dist/src/namespace.d.ts +0 -20
  181. package/dist/src/namespace.js.map +0 -1
  182. package/dist/src/notifier.d.ts +0 -5
  183. package/dist/src/notifier.js.map +0 -1
  184. package/dist/src/rangfolge.d.ts +0 -205
  185. package/dist/src/rangfolge.js.map +0 -1
  186. package/dist/src/raumkorrektur.d.ts +0 -100
  187. package/dist/src/raumkorrektur.js.map +0 -1
  188. package/dist/src/recall-tiefe.d.ts +0 -48
  189. package/dist/src/recall-tiefe.js.map +0 -1
  190. package/dist/src/rerank.d.ts +0 -48
  191. package/dist/src/rerank.js.map +0 -1
  192. package/dist/src/resonanz.d.ts +0 -85
  193. package/dist/src/resonanz.js.map +0 -1
  194. package/dist/src/search.d.ts +0 -153
  195. package/dist/src/search.js.map +0 -1
  196. package/dist/src/seltenheitsbestand.d.ts +0 -53
  197. package/dist/src/seltenheitsbestand.js.map +0 -1
  198. package/dist/src/speicherlage.d.ts +0 -51
  199. package/dist/src/speicherlage.js.map +0 -1
  200. package/dist/src/spuren.d.ts +0 -97
  201. package/dist/src/spuren.js.map +0 -1
  202. package/dist/src/starter-corpus.d.ts +0 -28
  203. package/dist/src/starter-corpus.js.map +0 -1
  204. package/dist/src/telemetry-types.d.ts +0 -87
  205. package/dist/src/telemetry-types.js.map +0 -1
  206. package/dist/src/tools.d.ts +0 -2933
  207. package/dist/src/tools.js.map +0 -1
  208. package/dist/src/toolspecs.d.ts +0 -43
  209. package/dist/src/toolspecs.js.map +0 -1
  210. package/dist/src/upgrade-nudge.d.ts +0 -15
  211. package/dist/src/upgrade-nudge.js.map +0 -1
  212. package/dist/src/utils.d.ts +0 -34
  213. package/dist/src/utils.js.map +0 -1
  214. package/dist/src/versuch.d.ts +0 -187
  215. package/dist/src/versuch.js.map +0 -1
  216. package/dist/src/vorspann.d.ts +0 -73
  217. package/dist/src/vorspann.js.map +0 -1
  218. package/dist/src/wartung/vektoren-verdichten.d.ts +0 -50
  219. package/dist/src/wartung/vektoren-verdichten.js.map +0 -1
  220. package/dist/src/werkzeug-nutzung.d.ts +0 -62
  221. package/dist/src/werkzeug-nutzung.js.map +0 -1
  222. package/dist/src/wertbeitrag.d.ts +0 -148
  223. package/dist/src/wertbeitrag.js.map +0 -1
@@ -1,119 +1,16 @@
1
- /**
2
- * Turns raw Brain lessons into the two files `cachly export` writes: a
3
- * human-readable Markdown dump and a machine-readable JSONL dump. Pure by
4
- * design — no network, no filesystem. The `export` CLI command in index.ts
5
- * fetches the lessons and writes the files these functions build.
6
- */
7
- /**
8
- * Macht aus einem gespeicherten Lektions-Datensatz eine ExportLesson.
9
- *
10
- * Die bekannten Felder werden benannt, damit das Markdown sie rendern kann.
11
- * Der ganze Datensatz bleibt zusaetzlich unter `raw` erhalten, damit die
12
- * JSONL-Zeile nichts verliert.
13
- */
14
- export function vonRohLektion(roh) {
15
- if (typeof roh !== 'object' || roh === null)
16
- return null;
17
- const r = roh;
18
- const topic = typeof r.topic === 'string' ? r.topic : '';
19
- if (!topic)
20
- return null;
21
- const text = (v) => (typeof v === 'string' && v ? v : undefined);
22
- const liste = (v) => Array.isArray(v) && v.length > 0 ? v.filter((x) => typeof x === 'string') : undefined;
23
- return {
24
- topic,
25
- outcome: text(r.outcome),
26
- severity: text(r.severity),
27
- whatWorked: text(r.what_worked),
28
- whatFailed: text(r.what_failed),
29
- filePaths: liste(r.file_paths),
30
- tags: liste(r.tags),
31
- ts: text(r.ts),
32
- author: text(r.author),
33
- recallCount: typeof r.recall_count === 'number' ? r.recall_count : undefined,
34
- raw: r,
35
- };
36
- }
37
- const UNGROUPED_HEADING = 'Ungrouped';
38
- function groupByCategory(lessons) {
39
- const groups = new Map();
40
- for (const lesson of lessons) {
41
- const colonIdx = lesson.topic.indexOf(':');
42
- const category = colonIdx > 0 ? lesson.topic.slice(0, colonIdx) : UNGROUPED_HEADING;
43
- const bucket = groups.get(category);
44
- if (bucket) {
45
- bucket.push(lesson);
46
- }
47
- else {
48
- groups.set(category, [lesson]);
49
- }
50
- }
51
- return groups;
52
- }
53
- function renderLesson(lesson) {
54
- const fields = [];
55
- if (lesson.outcome)
56
- fields.push(`- **Outcome:** ${lesson.outcome}`);
57
- if (lesson.severity)
58
- fields.push(`- **Severity:** ${lesson.severity}`);
59
- if (lesson.ts)
60
- fields.push(`- **Date:** ${lesson.ts}`);
61
- if (lesson.filePaths && lesson.filePaths.length > 0)
62
- fields.push(`- **Files:** ${lesson.filePaths.join(', ')}`);
63
- if (lesson.author)
64
- fields.push(`- **Author:** ${lesson.author}`);
65
- if (lesson.tags && lesson.tags.length > 0)
66
- fields.push(`- **Tags:** ${lesson.tags.join(', ')}`);
67
- const body = [];
68
- if (lesson.whatWorked)
69
- body.push(lesson.whatWorked);
70
- if (lesson.whatFailed)
71
- body.push(`**What failed:** ${lesson.whatFailed}`);
72
- return [`### ${lesson.topic}`, '', ...fields, '', ...body].join('\n').trimEnd();
73
- }
74
- /**
75
- * Builds the human-readable Markdown dump: one `##` section per topic
76
- * category (the part of `topic` before the first `:`, or "Ungrouped" when
77
- * there is none), one `###` subsection per lesson listing outcome, severity,
78
- * date and files. Never throws — an empty list renders a plain "no lessons
79
- * yet" line instead of an empty document, and any text in a lesson (however
80
- * unusual) is written as-is without breaking the surrounding structure.
81
- */
82
- export function buildLessonsMarkdown(lessons) {
83
- if (lessons.length === 0) {
84
- return '# Your Cachly Brain\n\nNo lessons yet — nothing to export.\n';
85
- }
86
- const groups = groupByCategory(lessons);
87
- const categories = [...groups.keys()].sort((a, b) => a.localeCompare(b));
88
- const sections = categories.map((category) => {
89
- const entries = groups.get(category).map(renderLesson).join('\n\n');
90
- return `## ${category}\n\n${entries}`;
91
- });
92
- const count = lessons.length;
93
- const heading = `# Your Cachly Brain\n\n${count} lesson${count === 1 ? '' : 's'}, grouped by topic.`;
94
- return [heading, ...sections].join('\n\n') + '\n';
95
- }
96
- /**
97
- * Builds the machine-readable JSONL dump: one JSON object per line, one
98
- * line per lesson, snake_case keys matching `learn_from_attempts` so the
99
- * file can be grepped, jq'd or re-imported without a schema lookup. Fields
100
- * that are absent on a lesson are simply omitted from its line. An empty
101
- * list returns an empty string (a zero-line file), never throws.
102
- */
103
- export function buildLessonsJsonl(lessons) {
104
- if (lessons.length === 0)
105
- return '';
106
- return lessons.map((lesson) => JSON.stringify(lesson.raw ?? {
107
- topic: lesson.topic,
108
- outcome: lesson.outcome,
109
- severity: lesson.severity,
110
- what_worked: lesson.whatWorked,
111
- what_failed: lesson.whatFailed,
112
- file_paths: lesson.filePaths,
113
- tags: lesson.tags,
114
- ts: lesson.ts,
115
- author: lesson.author,
116
- recall_count: lesson.recallCount,
117
- })).join('\n') + '\n';
118
- }
119
- //# sourceMappingURL=brain-export.js.map
1
+ function p(e){if(typeof e!="object"||e===null)return null;const t=e,i=typeof t.topic=="string"?t.topic:"";if(!i)return null;const o=r=>typeof r=="string"&&r?r:void 0,n=r=>Array.isArray(r)&&r.length>0?r.filter(a=>typeof a=="string"):void 0;return{topic:i,outcome:o(t.outcome),severity:o(t.severity),whatWorked:o(t.what_worked),whatFailed:o(t.what_failed),filePaths:n(t.file_paths),tags:n(t.tags),ts:o(t.ts),author:o(t.author),recallCount:typeof t.recall_count=="number"?t.recall_count:void 0,raw:t}}const c="Ungrouped";function h(e){const t=new Map;for(const i of e){const o=i.topic.indexOf(":"),n=o>0?i.topic.slice(0,o):c,r=t.get(n);r?r.push(i):t.set(n,[i])}return t}function f(e){const t=[];e.outcome&&t.push(`- **Outcome:** ${e.outcome}`),e.severity&&t.push(`- **Severity:** ${e.severity}`),e.ts&&t.push(`- **Date:** ${e.ts}`),e.filePaths&&e.filePaths.length>0&&t.push(`- **Files:** ${e.filePaths.join(", ")}`),e.author&&t.push(`- **Author:** ${e.author}`),e.tags&&e.tags.length>0&&t.push(`- **Tags:** ${e.tags.join(", ")}`);const i=[];return e.whatWorked&&i.push(e.whatWorked),e.whatFailed&&i.push(`**What failed:** ${e.whatFailed}`),[`### ${e.topic}`,"",...t,"",...i].join(`
2
+ `).trimEnd()}function d(e){if(e.length===0)return`# Your Cachly Brain
3
+
4
+ No lessons yet \u2014 nothing to export.
5
+ `;const t=h(e),o=[...t.keys()].sort((a,u)=>a.localeCompare(u)).map(a=>{const u=t.get(a).map(f).join(`
6
+
7
+ `);return`## ${a}
8
+
9
+ ${u}`}),n=e.length;return[`# Your Cachly Brain
10
+
11
+ ${n} lesson${n===1?"":"s"}, grouped by topic.`,...o].join(`
12
+
13
+ `)+`
14
+ `}function g(e){return e.length===0?"":e.map(t=>JSON.stringify(t.raw??{topic:t.topic,outcome:t.outcome,severity:t.severity,what_worked:t.whatWorked,what_failed:t.whatFailed,file_paths:t.filePaths,tags:t.tags,ts:t.ts,author:t.author,recall_count:t.recallCount})).join(`
15
+ `)+`
16
+ `}export{g as buildLessonsJsonl,d as buildLessonsMarkdown,p as vonRohLektion};
@@ -1,86 +1,8 @@
1
- // Ambient git post-commit hook builder for Brain auto-learning.
2
- //
3
- // Installs a .git/hooks/post-commit script that POSTs the commit data
4
- // directly to the cachly API via curl — no Node required at hook runtime,
5
- // so it works even without npx or the MCP server installed locally.
6
- //
7
- // Safety contract:
8
- // • exit 0 is ALWAYS the last line — the hook never blocks a commit.
9
- // • All network calls run in the background (&) with 2>/dev/null.
10
- // • Commit message / sha / files are never interpolated into JS source;
11
- // they're passed as shell variables in the JSON body (curl --data-raw).
12
- import { existsSync } from 'node:fs';
13
- import { readFile, writeFile, chmod, mkdir } from 'node:fs/promises';
14
- import { resolve } from 'node:path';
15
- /** Bumped whenever the hook script changes so installers can upgrade old hooks. */
16
- export const BRAIN_WATCH_HOOK_VERSION = 'v1';
17
- /**
18
- * Matches any previously-installed cachly brain_watch block (from its marker
19
- * comment down to the trailing `exit 0`), optionally including a shebang.
20
- * Used to upgrade old hooks in place rather than duplicating them.
21
- */
22
- const BRAIN_WATCH_BLOCK_RE = /(?:#!\/bin\/sh\n)?# cachly brain_watch[\s\S]*?\nexit 0\n?/;
23
- /**
24
- * Idempotently install/upgrade the brain_watch post-commit hook in `projectDir`.
25
- * Never throws — returns a status string the caller can report. Safe to run
26
- * repeatedly; a second call with the same version returns 'unchanged'.
27
- */
28
- export async function installBrainWatchHook(projectDir, instanceId, apiKey) {
29
- if (!existsSync(resolve(projectDir, '.git'))) {
30
- return { result: 'skipped-no-git', hookPath: '' };
31
- }
32
- const hookDir = resolve(projectDir, '.git', 'hooks');
33
- const hookPath = resolve(hookDir, 'post-commit');
34
- await mkdir(hookDir, { recursive: true });
35
- const script = buildBrainWatchHook(instanceId, apiKey);
36
- let existing = '';
37
- try {
38
- existing = await readFile(hookPath, 'utf-8');
39
- }
40
- catch { /* no existing hook */ }
41
- if (!existing) {
42
- await writeFile(hookPath, script + '\n', 'utf-8');
43
- await chmod(hookPath, 0o755).catch(() => { });
44
- return { result: 'written', hookPath };
45
- }
46
- if (existing.includes(`cachly brain_watch — Auto-Learn ${BRAIN_WATCH_HOOK_VERSION}`)) {
47
- return { result: 'unchanged', hookPath };
48
- }
49
- if (BRAIN_WATCH_BLOCK_RE.test(existing)) {
50
- const replaced = existing.replace(BRAIN_WATCH_BLOCK_RE, script);
51
- await writeFile(hookPath, replaced.endsWith('\n') ? replaced : replaced + '\n', 'utf-8');
52
- await chmod(hookPath, 0o755).catch(() => { });
53
- return { result: 'upgraded', hookPath };
54
- }
55
- // Foreign hook with no brain_watch block → append ours.
56
- await writeFile(hookPath, existing.trimEnd() + '\n\n' + script + '\n', 'utf-8');
57
- await chmod(hookPath, 0o755).catch(() => { });
58
- return { result: 'appended', hookPath };
59
- }
60
- /**
61
- * Build the `.git/hooks/post-commit` shell script for brain_watch.
62
- * Uses curl (not Node) so it works without npx. Runs in the background and
63
- * always exits 0 — never blocks a commit.
64
- *
65
- * @param instanceId Cachly Brain instance to learn into.
66
- * @param apiKey Optional cky_… key; if absent, the script reads $CACHLY_JWT from env.
67
- */
68
- export function buildBrainWatchHook(instanceId, apiKey) {
69
- return [
70
- `#!/bin/sh`,
71
- `# cachly brain_watch — Auto-Learn ${BRAIN_WATCH_HOOK_VERSION}`,
72
- `# Silently teaches your Brain after every commit — no manual brain_from_git needed.`,
73
- ...(apiKey ? [`CACHLY_JWT="${apiKey}"`] : []),
74
- `_BW_SHA=$(git rev-parse HEAD 2>/dev/null || echo "")`,
75
- `_BW_MSG=$(git log -1 --pretty=%B 2>/dev/null | head -1 | cut -c1-200)`,
76
- `_BW_FILES=$(git diff-tree --no-commit-id -r --name-only HEAD 2>/dev/null | tr '\\n' '\\t' | sed 's/\\t$//')`,
77
- `(curl -s -o /dev/null -m 8 \\`,
78
- ` -X POST "https://api.cachly.dev/api/v1/instances/${instanceId}/learn" \\`,
79
- ` -H "Authorization: Bearer \${CACHLY_JWT}" \\`,
80
- ` -H "Content-Type: application/json" \\`,
81
- ` --data-raw "{\\"message\\":\\"$(echo "$_BW_MSG" | sed 's/"/\\\\"/g')\\",\\"sha\\":\\"$_BW_SHA\\",\\"files\\":[$(echo "$_BW_FILES" | tr '\\t' '\\n' | grep -v '^$' | sed 's/.*/\\"&\\"/' | tr '\\n' ',' | sed 's/,$//')],\\"source\\":\\"brain_watch\\"}" \\`,
82
- `) 2>/dev/null &`,
83
- `exit 0`,
84
- ].join('\n');
85
- }
86
- //# sourceMappingURL=brain-watch-hook.js.map
1
+ import{existsSync as _}from"node:fs";import{readFile as p,writeFile as o,chmod as c,mkdir as m}from"node:fs/promises";import{resolve as s}from"node:path";const u="v1",h=/(?:#!\/bin\/sh\n)?# cachly brain_watch[\s\S]*?\nexit 0\n?/;async function B(n,a,d){if(!_(s(n,".git")))return{result:"skipped-no-git",hookPath:""};const l=s(n,".git","hooks"),t=s(l,"post-commit");await m(l,{recursive:!0});const r=f(a,d);let e="";try{e=await p(t,"utf-8")}catch{}if(!e)return await o(t,r+`
2
+ `,"utf-8"),await c(t,493).catch(()=>{}),{result:"written",hookPath:t};if(e.includes(`cachly brain_watch \u2014 Auto-Learn ${u}`))return{result:"unchanged",hookPath:t};if(h.test(e)){const i=e.replace(h,r);return await o(t,i.endsWith(`
3
+ `)?i:i+`
4
+ `,"utf-8"),await c(t,493).catch(()=>{}),{result:"upgraded",hookPath:t}}return await o(t,e.trimEnd()+`
5
+
6
+ `+r+`
7
+ `,"utf-8"),await c(t,493).catch(()=>{}),{result:"appended",hookPath:t}}function f(n,a){return["#!/bin/sh",`# cachly brain_watch \u2014 Auto-Learn ${u}`,"# Silently teaches your Brain after every commit \u2014 no manual brain_from_git needed.",...a?[`CACHLY_JWT="${a}"`]:[],'_BW_SHA=$(git rev-parse HEAD 2>/dev/null || echo "")',"_BW_MSG=$(git log -1 --pretty=%B 2>/dev/null | head -1 | cut -c1-200)","_BW_FILES=$(git diff-tree --no-commit-id -r --name-only HEAD 2>/dev/null | tr '\\n' '\\t' | sed 's/\\t$//')","(curl -s -o /dev/null -m 8 \\",` -X POST "https://api.cachly.dev/api/v1/instances/${n}/learn" \\`,' -H "Authorization: Bearer ${CACHLY_JWT}" \\',' -H "Content-Type: application/json" \\',` --data-raw "{\\"message\\":\\"$(echo "$_BW_MSG" | sed 's/"/\\\\"/g')\\",\\"sha\\":\\"$_BW_SHA\\",\\"files\\":[$(echo "$_BW_FILES" | tr '\\t' '\\n' | grep -v '^$' | sed 's/.*/\\"&\\"/' | tr '\\n' ',' | sed 's/,$//')],\\"source\\":\\"brain_watch\\"}" \\`,") 2>/dev/null &","exit 0"].join(`
8
+ `)}export{u as BRAIN_WATCH_HOOK_VERSION,f as buildBrainWatchHook,B as installBrainWatchHook};
@@ -1,21 +1 @@
1
- const CACHLY_ORIGIN = 'https://cachly.dev';
2
- /**
3
- * Builds a cachly.dev URL that carries a `utm_source` tag naming the place
4
- * the click came from, so Plausible can group clicks instead of showing
5
- * every visit as "direct".
6
- *
7
- * Pure: no network, no filesystem, no environment access. Existing query
8
- * parts in `path` are preserved; an existing `utm_source` is replaced, never
9
- * duplicated. `path` may include a path segment such as an instance ID —
10
- * `source` must never be anything but a value from {@link UrlQuelle}.
11
- */
12
- export function cachlyUrl(path, source) {
13
- const withSlash = path.startsWith('/') ? path : `/${path}`;
14
- const questionMark = withSlash.indexOf('?');
15
- const pathPart = questionMark === -1 ? withSlash : withSlash.slice(0, questionMark);
16
- const queryPart = questionMark === -1 ? '' : withSlash.slice(questionMark + 1);
17
- const params = new URLSearchParams(queryPart);
18
- params.set('utm_source', source);
19
- return `${CACHLY_ORIGIN}${pathPart}?${params.toString()}`;
20
- }
21
- //# sourceMappingURL=cachly-url.js.map
1
+ const o="https://cachly.dev";function i(c,n){const t=c.startsWith("/")?c:`/${c}`,s=t.indexOf("?"),e=s===-1?t:t.slice(0,s),a=s===-1?"":t.slice(s+1),r=new URLSearchParams(a);return r.set("utm_source",n),`${o}${e}?${r.toString()}`}export{i as cachlyUrl};
package/dist/src/ckg.js CHANGED
@@ -1,108 +1 @@
1
- const STOPWORDS_CKG = new Set(['that', 'this', 'with', 'from', 'when', 'then', 'also', 'have', 'been', 'will', 'were', 'they', 'them', 'than', 'more', 'some', 'into', 'over', 'only', 'just', 'where', 'while', 'which', 'there', 'their', 'would', 'could', 'should', 'after', 'before', 'about']);
2
- export function ckgSlug(text) {
3
- return text.toLowerCase().replace(/[^a-z0-9\-:]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '').slice(0, 80);
4
- }
5
- /** Extract 1-3 significant keywords from free text for a problem concept */
6
- export function extractProblemConcept(text) {
7
- const words = text.toLowerCase()
8
- .replace(/[^a-z0-9\s\-_]/g, ' ').split(/\s+/)
9
- .filter(w => w.length > 3 && !STOPWORDS_CKG.has(w))
10
- .slice(0, 5);
11
- if (words.length === 0)
12
- return null;
13
- return words.slice(0, 2).join('-');
14
- }
15
- export async function ckgUpsertNode(redis, id, domain, type) {
16
- const key = `cachly:ckg:node:${id}`;
17
- const raw = await redis.get(key);
18
- const node = raw ? JSON.parse(raw) : { id, domain, type, count: 0, ts: new Date().toISOString() };
19
- node.count = (node.count || 0) + 1;
20
- node.ts = new Date().toISOString();
21
- await redis.set(key, JSON.stringify(node));
22
- }
23
- /** Upsert a Person node; returns the node id ("person:{slug}"). */
24
- export async function ckgUpsertPersonNode(redis, handle, domain) {
25
- const id = `person:${ckgSlug(handle)}`;
26
- const key = `cachly:ckg:node:${id}`;
27
- const raw = await redis.get(key);
28
- const node = raw
29
- ? JSON.parse(raw)
30
- : { id, handle, domain, type: 'person', count: 0, last_active: new Date().toISOString() };
31
- node.count = (node.count || 0) + 1;
32
- node.last_active = new Date().toISOString();
33
- if (!node.domain)
34
- node.domain = domain;
35
- await redis.set(key, JSON.stringify(node));
36
- return id;
37
- }
38
- /** Upsert a File node; returns the node id ("file:{slug}"). */
39
- export async function ckgUpsertFileNode(redis, filePath) {
40
- const dir = filePath.split('/')[0] ?? 'root';
41
- const id = `file:${ckgSlug(filePath)}`;
42
- const key = `cachly:ckg:node:${id}`;
43
- const raw = await redis.get(key);
44
- const node = raw
45
- ? JSON.parse(raw)
46
- : { id, path: filePath, domain: dir, type: 'file', count: 0, ts: new Date().toISOString() };
47
- node.count = (node.count || 0) + 1;
48
- node.ts = new Date().toISOString();
49
- await redis.set(key, JSON.stringify(node));
50
- return id;
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
- }
71
- export async function ckgUpdateEdge(redis, from, edgeType, to, success, partial = false) {
72
- const key = `cachly:ckg:edge:${from}:${edgeType}:${to}`;
73
- const raw = await redis.get(key);
74
- const edge = raw ? JSON.parse(raw) : { from, to, edgeType, successes: 0, trials: 0, confidence: 0, last_updated: '' };
75
- edge.trials = (edge.trials || 0) + 1;
76
- if (success)
77
- edge.successes = (edge.successes || 0) + 1;
78
- else if (partial)
79
- edge.successes = (edge.successes || 0) + 0.5;
80
- // Beta distribution smoothed confidence: (successes+1) / (trials+2)
81
- edge.confidence = (edge.successes + 1) / (edge.trials + 2);
82
- edge.last_updated = new Date().toISOString();
83
- await redis.set(key, JSON.stringify(edge));
84
- // Index: set of edge keys per source node (for fast traversal)
85
- await redis.sadd(`cachly:ckg:idx:from:${from}`, key);
86
- await redis.sadd(`cachly:ckg:idx:to:${to}`, key);
87
- }
88
- /**
89
- * Phase 3: Collaboration graph. When `personId` touches `fileId`, record a
90
- * bidirectional `collaborates` edge to every *other* person who has touched the
91
- * same file. Builds the person↔person graph organically — "who works with whom".
92
- * Bounded: only the first MAX_CO_TOUCHERS prior touchers are linked, so a hot file
93
- * touched by hundreds of people can't blow up the write path.
94
- */
95
- const MAX_CO_TOUCHERS = 25;
96
- export async function ckgRecordCollaboration(redis, fileId, personId) {
97
- const touchersKey = `cachly:ckg:file:touchers:${fileId}`;
98
- const priorTouchers = await redis.smembers(touchersKey);
99
- for (const other of priorTouchers.slice(0, MAX_CO_TOUCHERS)) {
100
- if (other === personId)
101
- continue;
102
- // Symmetric edges so traversal works from either node.
103
- await ckgUpdateEdge(redis, personId, 'collaborates', other, true);
104
- await ckgUpdateEdge(redis, other, 'collaborates', personId, true);
105
- }
106
- await redis.sadd(touchersKey, personId);
107
- }
108
- //# sourceMappingURL=ckg.js.map
1
+ const g=new Set(["that","this","with","from","when","then","also","have","been","will","were","they","them","than","more","some","into","over","only","just","where","while","which","there","their","would","could","should","after","before","about"]);function l(t){return t.toLowerCase().replace(/[^a-z0-9\-:]/g,"-").replace(/-+/g,"-").replace(/^-|-$/g,"").slice(0,80)}function S(t){const c=t.toLowerCase().replace(/[^a-z0-9\s\-_]/g," ").split(/\s+/).filter(n=>n.length>3&&!g.has(n)).slice(0,5);return c.length===0?null:c.slice(0,2).join("-")}async function y(t,c,n,o){const a=`cachly:ckg:node:${c}`,s=await t.get(a),e=s?JSON.parse(s):{id:c,domain:n,type:o,count:0,ts:new Date().toISOString()};e.count=(e.count||0)+1,e.ts=new Date().toISOString(),await t.set(a,JSON.stringify(e))}async function p(t,c,n){const o=`person:${l(c)}`,a=`cachly:ckg:node:${o}`,s=await t.get(a),e=s?JSON.parse(s):{id:o,handle:c,domain:n,type:"person",count:0,last_active:new Date().toISOString()};return e.count=(e.count||0)+1,e.last_active=new Date().toISOString(),e.domain||(e.domain=n),await t.set(a,JSON.stringify(e)),o}async function f(t,c){const n=c.split("/")[0]??"root",o=`file:${l(c)}`,a=`cachly:ckg:node:${o}`,s=await t.get(a),e=s?JSON.parse(s):{id:o,path:c,domain:n,type:"file",count:0,ts:new Date().toISOString()};return e.count=(e.count||0)+1,e.ts=new Date().toISOString(),await t.set(a,JSON.stringify(e)),o}async function h(t,c,n,o="service"){const a=`service:${l(c)}`,s=`cachly:ckg:node:${a}`,e=await t.get(s),r=e?JSON.parse(e):{id:a,name:c,kind:o,domain:n,type:"service",count:0,last_active:new Date().toISOString()};return r.count=(r.count||0)+1,r.last_active=new Date().toISOString(),r.domain||(r.domain=n),o==="system"&&(r.kind="system"),await t.set(s,JSON.stringify(r)),a}async function u(t,c,n,o,a,s=!1){const e=`cachly:ckg:edge:${c}:${n}:${o}`,r=await t.get(e),i=r?JSON.parse(r):{from:c,to:o,edgeType:n,successes:0,trials:0,confidence:0,last_updated:""};i.trials=(i.trials||0)+1,a?i.successes=(i.successes||0)+1:s&&(i.successes=(i.successes||0)+.5),i.confidence=(i.successes+1)/(i.trials+2),i.last_updated=new Date().toISOString(),await t.set(e,JSON.stringify(i)),await t.sadd(`cachly:ckg:idx:from:${c}`,e),await t.sadd(`cachly:ckg:idx:to:${o}`,e)}const w=25;async function d(t,c,n){const o=`cachly:ckg:file:touchers:${c}`,a=await t.smembers(o);for(const s of a.slice(0,w))s!==n&&(await u(t,n,"collaborates",s,!0),await u(t,s,"collaborates",n,!0));await t.sadd(o,n)}export{d as ckgRecordCollaboration,l as ckgSlug,u as ckgUpdateEdge,f as ckgUpsertFileNode,y as ckgUpsertNode,p as ckgUpsertPersonNode,h as ckgUpsertServiceNode,S as extractProblemConcept};
@@ -1,94 +1,8 @@
1
- // Shared builder for the CLS (Continuous Learning Stream) git post-commit hook.
2
- //
3
- // History / why this is centralised:
4
- // The previous inline versions interpolated the commit message directly into a
5
- // `node -e "... message:'$MSG' ..."` JS string. Any commit message containing an
6
- // apostrophe (e.g. "don't crash") produced invalid JavaScript, so the hook
7
- // silently ingested nothing — and a crafted message could inject shell/JS.
8
- //
9
- // This version is robust:
10
- // • Commit message / sha / files are passed to node via ENVIRONMENT VARIABLES,
11
- // never interpolated into JS source — no quoting or injection breakage.
12
- // • The JSON payload is handed to the CLI via execFileSync (no shell), so the
13
- // shell never re-parses quotes, `$`, or backticks from the message.
14
- // • CACHLY_JWT is embedded only when provided so the `cls-ingest` CLI command
15
- // can authenticate; absent it, that command exits 0 silently.
16
- import { existsSync } from 'node:fs';
17
- import { readFile, writeFile, chmod, mkdir } from 'node:fs/promises';
18
- import { resolve } from 'node:path';
19
- /** Bumped whenever the hook script changes so installers can upgrade old hooks. */
20
- export const CLS_HOOK_VERSION = 'v2';
21
- /**
22
- * Matches any previously-installed cachly CLS block (from its marker comment
23
- * down to the trailing `exit 0`), optionally including an immediately-preceding
24
- * standalone shebang. Used to upgrade old hooks in place rather than skipping
25
- * them (the v1 block was a silent no-op).
26
- */
27
- const CLS_BLOCK_RE = /(?:#!\/bin\/sh\n)?# cachly CLS[\s\S]*?\nexit 0\n?/;
28
- /**
29
- * Idempotently install/upgrade the CLS post-commit hook in `projectDir`.
30
- * Never throws — returns a status the caller can log. Shared by `init`, `setup`
31
- * and `autopilot` so all paths stay in lock-step.
32
- */
33
- export async function installClsPostCommitHook(projectDir, instanceId, apiKey) {
34
- if (!existsSync(resolve(projectDir, '.git')))
35
- return 'skipped-no-git';
36
- const hookDir = resolve(projectDir, '.git', 'hooks');
37
- const hookPath = resolve(hookDir, 'post-commit');
38
- await mkdir(hookDir, { recursive: true });
39
- const script = buildClsPostCommitHook(instanceId, apiKey);
40
- let existing = '';
41
- try {
42
- existing = await readFile(hookPath, 'utf-8');
43
- }
44
- catch { /* no existing hook */ }
45
- if (!existing) {
46
- await writeFile(hookPath, script + '\n', 'utf-8');
47
- await chmod(hookPath, 0o755).catch(() => { });
48
- return 'written';
49
- }
50
- if (existing.includes(`cachly CLS — Continuous Learning Stream ${CLS_HOOK_VERSION}`)) {
51
- return 'unchanged';
52
- }
53
- if (CLS_BLOCK_RE.test(existing)) {
54
- // Upgrade an older cachly block in place, preserving any surrounding hook.
55
- const replaced = existing.replace(CLS_BLOCK_RE, script);
56
- await writeFile(hookPath, replaced.endsWith('\n') ? replaced : replaced + '\n', 'utf-8');
57
- await chmod(hookPath, 0o755).catch(() => { });
58
- return 'upgraded';
59
- }
60
- // Foreign hook with no cachly block → append ours.
61
- await writeFile(hookPath, existing.trimEnd() + '\n\n' + script + '\n', 'utf-8');
62
- await chmod(hookPath, 0o755).catch(() => { });
63
- return 'appended';
64
- }
65
- /** The node `-e` program — reads commit data from env, ingests via the CLI. */
66
- const CLS_NODE_PROGRAM = "try{" +
67
- "var p={instance_id:process.env.CACHLY_BRAIN_INSTANCE_ID,source:'git_commit'," +
68
- "payload:{message:process.env.CLS_MSG||'',sha:process.env.CLS_SHA||''," +
69
- "files:(process.env.CLS_FILES||'').split(',').filter(Boolean)}};" +
70
- "require('child_process').execFileSync('npx',['@cachly-dev/mcp-server@latest','cls-ingest',JSON.stringify(p)]," +
71
- "{stdio:'ignore',timeout:8000});" +
72
- "}catch(e){}";
73
- /**
74
- * Build the `.git/hooks/post-commit` shell script.
75
- * @param instanceId Cachly Brain instance to ingest into.
76
- * @param apiKey Optional cky_… key embedded for authentication.
77
- */
78
- export function buildClsPostCommitHook(instanceId, apiKey) {
79
- return [
80
- `#!/bin/sh`,
81
- `# cachly CLS — Continuous Learning Stream ${CLS_HOOK_VERSION}`,
82
- `# Runs silently on every commit to keep your brain up to date.`,
83
- `export CACHLY_BRAIN_INSTANCE_ID="${instanceId}"`,
84
- ...(apiKey ? [`export CACHLY_JWT="${apiKey}"`] : []),
85
- `CLS_SHA=$(git rev-parse HEAD 2>/dev/null || echo "")`,
86
- // No `tr`/quote-mangling — the message is passed verbatim via env.
87
- `CLS_MSG=$(git log -1 --pretty=%B 2>/dev/null | head -1 | cut -c1-200)`,
88
- `CLS_FILES=$(git diff-tree --no-commit-id -r --name-only HEAD 2>/dev/null | tr '\\n' ',' | sed 's/,$//')`,
89
- `export CLS_SHA CLS_MSG CLS_FILES`,
90
- `node -e "${CLS_NODE_PROGRAM}" 2>/dev/null &`,
91
- `exit 0`,
92
- ].join('\n');
93
- }
94
- //# sourceMappingURL=cls-hook.js.map
1
+ import{existsSync as C}from"node:fs";import{readFile as d,writeFile as s,chmod as c,mkdir as m}from"node:fs/promises";import{resolve as a}from"node:path";const u="v2",S=/(?:#!\/bin\/sh\n)?# cachly CLS[\s\S]*?\nexit 0\n?/;async function v(n,i,p){if(!C(a(n,".git")))return"skipped-no-git";const l=a(n,".git","hooks"),t=a(l,"post-commit");await m(l,{recursive:!0});const o=L(i,p);let e="";try{e=await d(t,"utf-8")}catch{}if(!e)return await s(t,o+`
2
+ `,"utf-8"),await c(t,493).catch(()=>{}),"written";if(e.includes(`cachly CLS \u2014 Continuous Learning Stream ${u}`))return"unchanged";if(S.test(e)){const r=e.replace(S,o);return await s(t,r.endsWith(`
3
+ `)?r:r+`
4
+ `,"utf-8"),await c(t,493).catch(()=>{}),"upgraded"}return await s(t,e.trimEnd()+`
5
+
6
+ `+o+`
7
+ `,"utf-8"),await c(t,493).catch(()=>{}),"appended"}const _="try{var p={instance_id:process.env.CACHLY_BRAIN_INSTANCE_ID,source:'git_commit',payload:{message:process.env.CLS_MSG||'',sha:process.env.CLS_SHA||'',files:(process.env.CLS_FILES||'').split(',').filter(Boolean)}};require('child_process').execFileSync('npx',['@cachly-dev/mcp-server@latest','cls-ingest',JSON.stringify(p)],{stdio:'ignore',timeout:8000});}catch(e){}";function L(n,i){return["#!/bin/sh",`# cachly CLS \u2014 Continuous Learning Stream ${u}`,"# Runs silently on every commit to keep your brain up to date.",`export CACHLY_BRAIN_INSTANCE_ID="${n}"`,...i?[`export CACHLY_JWT="${i}"`]:[],'CLS_SHA=$(git rev-parse HEAD 2>/dev/null || echo "")',"CLS_MSG=$(git log -1 --pretty=%B 2>/dev/null | head -1 | cut -c1-200)","CLS_FILES=$(git diff-tree --no-commit-id -r --name-only HEAD 2>/dev/null | tr '\\n' ',' | sed 's/,$//')","export CLS_SHA CLS_MSG CLS_FILES",`node -e "${_}" 2>/dev/null &`,"exit 0"].join(`
8
+ `)}export{u as CLS_HOOK_VERSION,L as buildClsPostCommitHook,v as installClsPostCommitHook};
@@ -1,50 +1 @@
1
- import { createHash } from 'node:crypto';
2
- // ── Confidence Decay Config ───────────────────────────────────────────────────
3
- export const CONFIDENCE_WARN_DAYS = Number(process.env.CACHLY_CONFIDENCE_WARN_DAYS ?? 5);
4
- export const CONFIDENCE_STALE_DAYS = Number(process.env.CACHLY_CONFIDENCE_STALE_DAYS ?? 10);
5
- export const CONFIDENCE_WARN_VALUE = 0.7;
6
- export const CONFIDENCE_STALE_VALUE = 0.5;
7
- // ── One of THREE deliberately-different age/decay curves. Do NOT unify them. ──
8
- // This one (SHARP: 0.7@5d, 0.5@10d) is a user-facing *staleness warning* — it
9
- // drives the recall_best_solution freshness badge and brain_predict, where the
10
- // point is to shout "verify before applying" once a lesson is old.
11
- // • Ranking uses a SEPARATE, gentle curve — search.ts `recencyBoost`
12
- // (0.5^(age/7)+0.5, range [0.5,1.5]). It is bench-tuned. Feeding THIS sharp
13
- // curve into the recall rerank regressed every Cachly-Bench floor (home P@1
14
- // 92→77%) and was reverted — see PR #228. Relevant lessons are often days
15
- // /weeks old; penalising age hard in ranking buries the right answer.
16
- // • The knowledge_decay *report* uses a THIRD, points-based curve
17
- // (advanced.ts) that also folds in recall_count + outcome.
18
- // Three purposes, three curves — that divergence is intentional, not a bug.
19
- /** Calculate current confidence for a lesson based on how long since last verified. */
20
- export function calculateConfidence(lesson) {
21
- const ref = lesson.verified_at ?? lesson.ts;
22
- const ageMs = Date.now() - new Date(ref).getTime();
23
- const ageDays = ageMs / 86400000;
24
- if (ageDays >= CONFIDENCE_STALE_DAYS)
25
- return CONFIDENCE_STALE_VALUE;
26
- if (ageDays >= CONFIDENCE_WARN_DAYS)
27
- return CONFIDENCE_WARN_VALUE;
28
- return 1.0 - (ageDays / CONFIDENCE_WARN_DAYS) * (1.0 - CONFIDENCE_WARN_VALUE);
29
- }
30
- /** Render a confidence badge string. */
31
- export function confidenceBadge(confidence, ageDays) {
32
- if (confidence >= 0.9)
33
- return '✅';
34
- if (confidence >= 0.7)
35
- return `⚠️ (${Math.round(ageDays)}d old, confidence ${(confidence * 100).toFixed(0)}% — verify before applying)`;
36
- return `🔴 STALE (${Math.round(ageDays)}d old, confidence ${(confidence * 100).toFixed(0)}% — likely outdated!)`;
37
- }
38
- /** Category-specific required fields for structured lessons. */
39
- export const STRUCTURED_TEMPLATES = {
40
- 'deploy': { required: ['commands'], hint: 'deploy:* needs commands[]' },
41
- 'bash': { required: ['commands'], hint: 'bash:* needs commands[]' },
42
- 'infra': { required: ['commands'], hint: 'infra:* needs commands[] + verified on real system' },
43
- 'pricing': { required: [], hint: 'pricing:* — add context with source (e.g. "Stripe Dashboard Apr 2026")' },
44
- 'stripe': { required: [], hint: 'stripe:* — add context with Stripe API version' },
45
- };
46
- /** Fast content hash for index invalidation (not cryptographic, just change detection) */
47
- export function simpleHash(text) {
48
- return createHash('md5').update(text).digest('hex').slice(0, 12);
49
- }
50
- //# sourceMappingURL=confidence.js.map
1
+ import{createHash as i}from"node:crypto";const n=Number(process.env.CACHLY_CONFIDENCE_WARN_DAYS??5),d=Number(process.env.CACHLY_CONFIDENCE_STALE_DAYS??10),o=.7,s=.5;function p(e){const r=e.verified_at??e.ts,t=(Date.now()-new Date(r).getTime())/864e5;return t>=d?s:t>=n?o:1-t/n*(1-o)}function u(e,r){return e>=.9?"\u2705":e>=.7?`\u26A0\uFE0F (${Math.round(r)}d old, confidence ${(e*100).toFixed(0)}% \u2014 verify before applying)`:`\u{1F534} STALE (${Math.round(r)}d old, confidence ${(e*100).toFixed(0)}% \u2014 likely outdated!)`}const E={deploy:{required:["commands"],hint:"deploy:* needs commands[]"},bash:{required:["commands"],hint:"bash:* needs commands[]"},infra:{required:["commands"],hint:"infra:* needs commands[] + verified on real system"},pricing:{required:[],hint:'pricing:* \u2014 add context with source (e.g. "Stripe Dashboard Apr 2026")'},stripe:{required:[],hint:"stripe:* \u2014 add context with Stripe API version"}};function m(e){return i("md5").update(e).digest("hex").slice(0,12)}export{d as CONFIDENCE_STALE_DAYS,s as CONFIDENCE_STALE_VALUE,n as CONFIDENCE_WARN_DAYS,o as CONFIDENCE_WARN_VALUE,E as STRUCTURED_TEMPLATES,p as calculateConfidence,u as confidenceBadge,m as simpleHash};
@@ -1,85 +1 @@
1
- // Credentials — where the ambient hooks and CLI resolve the API key from
2
- // (GROW-015): never a literal in a generated/committed file, always the
3
- // environment or a file in the user's home directory.
4
- import { existsSync, mkdirSync, writeFileSync, renameSync, chmodSync, readFileSync } from 'node:fs';
5
- import { resolve, dirname } from 'node:path';
6
- function defaultHome() {
7
- return process.env.HOME ?? process.env.USERPROFILE ?? process.env.APPDATA ?? '';
8
- }
9
- function nonEmpty(value) {
10
- return typeof value === 'string' && value.length > 0 ? value : undefined;
11
- }
12
- /**
13
- * Path of the local credentials file in the user's home directory
14
- * (`<home>/.cachly/credentials.json`) — never inside a project checkout.
15
- * Mirrors the HOME/USERPROFILE/APPDATA fallback `persistApiKeyToConfig`
16
- * already uses for `~/.claude/mcp.json`.
17
- */
18
- export function credentialsPath(opts = {}) {
19
- const home = opts.home ?? defaultHome();
20
- return resolve(home, '.cachly', 'credentials.json');
21
- }
22
- /**
23
- * Persists the API key to {@link credentialsPath} so ambient hooks (which run
24
- * as bare OS processes and never see the MCP config's env) can find it at
25
- * run time without it ever being embedded in a generated script. Writes via
26
- * a temp file + rename so a crash mid-write can never leave a truncated
27
- * file, and restricts the file to the owner (0600 — a no-op on Windows).
28
- * Best-effort: a failure here never throws, matching every other ambient-path
29
- * write (setup and CLI auth still succeed without a persisted copy).
30
- */
31
- export function saveApiKey(key, opts = {}) {
32
- try {
33
- const target = credentialsPath(opts);
34
- mkdirSync(dirname(target), { recursive: true });
35
- const tmp = `${target}.${process.pid}-${Date.now()}.tmp`;
36
- writeFileSync(tmp, JSON.stringify({ apiKey: key }, null, 2), 'utf8');
37
- try {
38
- chmodSync(tmp, 0o600);
39
- }
40
- catch { /* e.g. Windows — best-effort */ }
41
- renameSync(tmp, target);
42
- }
43
- catch { /* best-effort — a failed persist must not break the caller */ }
44
- }
45
- function readHomeCredential(opts) {
46
- try {
47
- const path = credentialsPath(opts);
48
- if (!existsSync(path))
49
- return undefined;
50
- const parsed = JSON.parse(readFileSync(path, 'utf8'));
51
- return nonEmpty(parsed.apiKey);
52
- }
53
- catch {
54
- return undefined;
55
- }
56
- }
57
- function readProjectConfigKey(opts) {
58
- try {
59
- const path = resolve(opts.cwd ?? process.cwd(), '.mcp.json');
60
- if (!existsSync(path))
61
- return undefined;
62
- const parsed = JSON.parse(readFileSync(path, 'utf8'));
63
- return nonEmpty(parsed.mcpServers?.cachly?.env?.CACHLY_JWT);
64
- }
65
- catch {
66
- return undefined;
67
- }
68
- }
69
- /**
70
- * Resolves the API key the same way for every ambient/CLI caller, in order:
71
- * `CACHLY_JWT` env -> `CACHLY_API_KEY` env -> {@link credentialsPath} in the
72
- * user's home -> (legacy fallback) `mcpServers.cachly.env.CACHLY_JWT` in a
73
- * project's `.mcp.json`, for installs from before this file existed. A
74
- * corrupt file at any source counts as empty rather than throwing. Returns
75
- * `undefined` — never an empty string — when nothing is found, so callers
76
- * can tell "no key" apart from "empty key" with a plain truthiness check.
77
- */
78
- export function resolveApiKey(opts = {}) {
79
- const env = opts.env ?? process.env;
80
- return (nonEmpty(env.CACHLY_JWT) ??
81
- nonEmpty(env.CACHLY_API_KEY) ??
82
- readHomeCredential(opts) ??
83
- readProjectConfigKey(opts));
84
- }
85
- //# sourceMappingURL=credentials.js.map
1
+ import{existsSync as o,mkdirSync as p,writeFileSync as a,renameSync as d,chmodSync as f,readFileSync as i}from"node:fs";import{resolve as s,dirname as y}from"node:path";function m(){return process.env.HOME??process.env.USERPROFILE??process.env.APPDATA??""}function t(e){return typeof e=="string"&&e.length>0?e:void 0}function u(e={}){const n=e.home??m();return s(n,".cachly","credentials.json")}function A(e,n={}){try{const r=u(n);p(y(r),{recursive:!0});const c=`${r}.${process.pid}-${Date.now()}.tmp`;a(c,JSON.stringify({apiKey:e},null,2),"utf8");try{f(c,384)}catch{}d(c,r)}catch{}}function h(e){try{const n=u(e);if(!o(n))return;const r=JSON.parse(i(n,"utf8"));return t(r.apiKey)}catch{return}}function l(e){try{const n=s(e.cwd??process.cwd(),".mcp.json");if(!o(n))return;const r=JSON.parse(i(n,"utf8"));return t(r.mcpServers?.cachly?.env?.CACHLY_JWT)}catch{return}}function C(e={}){const n=e.env??process.env;return t(n.CACHLY_JWT)??t(n.CACHLY_API_KEY)??h(e)??l(e)}export{u as credentialsPath,C as resolveApiKey,A as saveApiKey};