@cachly-dev/mcp-server 0.10.101 β 0.10.103
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 +15 -3
- package/dist/src/cls-hook.d.ts +15 -0
- package/dist/src/cls-hook.js +94 -0
- package/dist/src/cls-hook.js.map +1 -0
- package/dist/src/handlers/brain.js +66 -6
- package/dist/src/handlers/brain.js.map +1 -1
- package/dist/src/handlers/fedbrain.js +108 -14
- package/dist/src/handlers/fedbrain.js.map +1 -1
- package/dist/src/handlers/share.js +1 -1
- package/dist/src/handlers/share.js.map +1 -1
- package/dist/src/handlers/syndicate.js +193 -0
- package/dist/src/handlers/syndicate.js.map +1 -1
- package/dist/src/handlers/team.js +180 -1
- package/dist/src/handlers/team.js.map +1 -1
- package/dist/src/index.js +121 -70
- package/dist/src/index.js.map +1 -1
- package/dist/src/telemetry-types.d.ts +1 -1
- package/dist/src/tools.d.ts +147 -0
- package/dist/src/tools.js +122 -0
- package/dist/src/tools.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -494,6 +494,18 @@ keep talking to your backend on every editor launch. All data stays in your infr
|
|
|
494
494
|
|
|
495
495
|
---
|
|
496
496
|
|
|
497
|
+
## π§ Brain v3 β what's new
|
|
498
|
+
|
|
499
|
+
| Feature | Tool | What it does |
|
|
500
|
+
|---|---|---|
|
|
501
|
+
| Autonomous hygiene | `brain_hygiene` | Sweeps stale lessons, flags provisional, archives orphans |
|
|
502
|
+
| PR risk scan | `brain_plan` + `cachly-action scan` | Predicts CI failures before they run, posts PR comment |
|
|
503
|
+
| Multi-agent arbitration | `brain_conflicts` Β· `brain_resolve_conflict` | Detects + resolves conflicting lessons across agents |
|
|
504
|
+
| Plans dashboard | `brain_plan` | Persistent plans in the UI with step tracking and brain-viz overlay |
|
|
505
|
+
| Privacy federation | `brain_contribute_signal` Β· `brain_import_meta` | Share patterns without sharing data β k-anonymous global commons |
|
|
506
|
+
|
|
507
|
+
---
|
|
508
|
+
|
|
497
509
|
## π οΈ Ecosystem & Docs
|
|
498
510
|
|
|
499
511
|
**One brain, wherever you work.** Start with the MCP server, or drop the same memory
|
|
@@ -503,13 +515,13 @@ straight into your editor β your lessons follow you across all of them.
|
|
|
503
515
|
|---------|-------------|
|
|
504
516
|
| **[`@cachly-dev/mcp-server`](https://www.npmjs.com/package/@cachly-dev/mcp-server)** | β you are here Β· works with Claude, Cursor, Copilot, Windsurf, Cline, Zed |
|
|
505
517
|
| **[Cachly Brain for VS Code](https://marketplace.visualstudio.com/items?itemName=cachly-dev.cachly-brain)** | One-click memory in the editor β status bar, lessons view, ambient learning. No terminal needed. |
|
|
506
|
-
| **Cachly Brain for JetBrains** | Same brain for IntelliJ / PyCharm / GoLand / WebStorm β
|
|
518
|
+
| **[Cachly Brain for JetBrains](https://plugins.jetbrains.com/plugin/32059-cachly-brain)** | Same brain for IntelliJ / PyCharm / GoLand / WebStorm / Rider β status bar, brain health, lessons view. |
|
|
507
519
|
| **[`@cachly-dev/openclaw`](https://www.npmjs.com/package/@cachly-dev/openclaw)** | Cut LLM costs 60β90% in JS/TS apps |
|
|
520
|
+
| **[cachly-dev/cachly-action](https://github.com/cachly-dev/cachly-action)** | GitHub Action: auto-setup, PR risk scan, auto-learn from merged PRs, weekly hygiene |
|
|
508
521
|
|
|
509
522
|
- π [cachly.dev](https://cachly.dev) β Dashboard & free signup
|
|
510
523
|
- π [Docs](https://cachly.dev/docs/ai-memory) β Full documentation
|
|
511
|
-
-
|
|
512
|
-
- π§ [STRATEGY.md](./STRATEGY.md) Β· [VISION_10X.md](./VISION_10X.md) Β· [PROGRESS.md](./PROGRESS.md)
|
|
524
|
+
- πΊοΈ [Public Roadmap](https://cachly.dev/roadmap) β what's coming next
|
|
513
525
|
- π¬ [GitHub Issues](https://github.com/cachly-dev/cachly-mcp/issues) β Bugs & feature requests
|
|
514
526
|
|
|
515
527
|
---
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Bumped whenever the hook script changes so installers can upgrade old hooks. */
|
|
2
|
+
export declare const CLS_HOOK_VERSION = "v2";
|
|
3
|
+
export type ClsHookResult = 'written' | 'upgraded' | 'appended' | 'unchanged' | 'skipped-no-git';
|
|
4
|
+
/**
|
|
5
|
+
* Idempotently install/upgrade the CLS post-commit hook in `projectDir`.
|
|
6
|
+
* Never throws β returns a status the caller can log. Shared by `init`, `setup`
|
|
7
|
+
* and `autopilot` so all paths stay in lock-step.
|
|
8
|
+
*/
|
|
9
|
+
export declare function installClsPostCommitHook(projectDir: string, instanceId: string, apiKey?: string): Promise<ClsHookResult>;
|
|
10
|
+
/**
|
|
11
|
+
* Build the `.git/hooks/post-commit` shell script.
|
|
12
|
+
* @param instanceId Cachly Brain instance to ingest into.
|
|
13
|
+
* @param apiKey Optional cky_β¦ key embedded for authentication.
|
|
14
|
+
*/
|
|
15
|
+
export declare function buildClsPostCommitHook(instanceId: string, apiKey?: string): string;
|
|
@@ -0,0 +1,94 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cls-hook.js","sourceRoot":"","sources":["../../src/cls-hook.ts"],"names":[],"mappings":"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"}
|
|
@@ -301,6 +301,33 @@ export const BRAIN_TOOL_NAMES = new Set([
|
|
|
301
301
|
'skill_gaps', 'brain_coverage', 'brain_metrics', 'brain_service_map',
|
|
302
302
|
'brain_collab_pairs', 'brain_portability',
|
|
303
303
|
]);
|
|
304
|
+
// ββ Free-tier Teaser-Gate ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
305
|
+
// When a Free-tier Brain crosses its recall limit, smart_recall still delivers
|
|
306
|
+
// the top hits β the magic moment is sacred β but withholds the long tail
|
|
307
|
+
// behind an upgrade teaser ("π N more relevant lessons available"). This is a
|
|
308
|
+
// DEPTH gate (hide the tail), never an access wall: the #1 result is always
|
|
309
|
+
// returned. Usage/limit come from the same authoritative numbers as the Brain
|
|
310
|
+
// Health bar (GET /instances/:id/memory), cached briefly to avoid per-call latency.
|
|
311
|
+
const FREE_TIER_VISIBLE_RECALLS = 3;
|
|
312
|
+
const UPGRADE_URL = 'https://cachly.dev/billing';
|
|
313
|
+
const _recallGateCache = new Map();
|
|
314
|
+
async function getRecallGate(instanceId, apiFetch) {
|
|
315
|
+
const cached = _recallGateCache.get(instanceId);
|
|
316
|
+
if (cached && cached.expiresAt > Date.now())
|
|
317
|
+
return cached.gate;
|
|
318
|
+
try {
|
|
319
|
+
const mem = await apiFetch(`/api/v1/instances/${instanceId}/memory`);
|
|
320
|
+
const limit = mem?.recall_limit ?? -1;
|
|
321
|
+
const used = mem?.total_recall_count ?? 0;
|
|
322
|
+
const gate = { reached: limit > 0 && used >= limit, limit, used };
|
|
323
|
+
_recallGateCache.set(instanceId, { gate, expiresAt: Date.now() + 60_000 });
|
|
324
|
+
return gate;
|
|
325
|
+
}
|
|
326
|
+
catch {
|
|
327
|
+
// Stats unavailable β never gate (fail open: value delivery beats monetization).
|
|
328
|
+
return { reached: false, limit: -1, used: 0 };
|
|
329
|
+
}
|
|
330
|
+
}
|
|
304
331
|
export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
305
332
|
switch (name) {
|
|
306
333
|
case 'learn_from_attempts': {
|
|
@@ -509,10 +536,21 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
509
536
|
if (!existEdge)
|
|
510
537
|
continue;
|
|
511
538
|
if (existEdge.edgeType === 'fixes' && existEdge.confidence > 0.7 && existEdge.trials >= 3) {
|
|
512
|
-
beliefConflict = `β οΈ **belief_conflict** on \`${topic}\`: previously confirmed fix (confidence ${existEdge.confidence.toFixed(2)}, n=${existEdge.trials}) now reports failure. Both beliefs retained as \`contested\`. Use \`ckg_inspect(concept="${conceptId}")\` to review.`;
|
|
513
|
-
// Store conflict marker
|
|
539
|
+
beliefConflict = `β οΈ **belief_conflict** on \`${topic}\`: previously confirmed fix (confidence ${existEdge.confidence.toFixed(2)}, n=${existEdge.trials}) now reports failure. Both beliefs retained as \`contested\`. Use \`brain_resolve_conflict(instance_id="...", topic="${topic}", winner="success"|"failure")\` to arbitrate, or \`ckg_inspect(concept="${conceptId}")\` to review.`;
|
|
540
|
+
// Store conflict marker β records WHICH agent reported the contradiction
|
|
541
|
+
// so multi-agent arbitration can show both sides (Move 4).
|
|
514
542
|
const conflictKey = `cachly:ckg:conflict:${conceptId}`;
|
|
515
|
-
await redis.set(conflictKey, JSON.stringify({
|
|
543
|
+
await redis.set(conflictKey, JSON.stringify({
|
|
544
|
+
topic,
|
|
545
|
+
concept_id: conceptId,
|
|
546
|
+
detected_at: new Date().toISOString(),
|
|
547
|
+
fix_confidence: existEdge.confidence,
|
|
548
|
+
fix_trials: existEdge.trials,
|
|
549
|
+
failure_outcome: outcome,
|
|
550
|
+
reported_by: author || 'unknown',
|
|
551
|
+
what_failed: what_failed || '',
|
|
552
|
+
resolved: false,
|
|
553
|
+
}), 'EX', 60 * 60 * 24 * 90);
|
|
516
554
|
// Auto-trigger MADC deliberation in background β never blocks learn_from_attempts
|
|
517
555
|
// Note: fire-and-forget; madc_deliberate is handled elsewhere in the switch
|
|
518
556
|
// (conflict marker stored above β no warm-up call needed)
|
|
@@ -578,6 +616,10 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
578
616
|
// Builds the "who knows what" map automatically from author + file_paths.
|
|
579
617
|
// Person β authored β Concept edges power brain_who_knows queries.
|
|
580
618
|
if (author) {
|
|
619
|
+
// Agent activity registry (Move 4): track which agents are actively
|
|
620
|
+
// writing to this Brain, keyed by author, with a 1h TTL. Powers
|
|
621
|
+
// brain_conflicts' "who is live" view and multi-agent arbitration.
|
|
622
|
+
redis.set(`cachly:agents:active:${author}`, JSON.stringify({ author, last_topic: topic, last_outcome: outcome, ts: new Date().toISOString() }), 'EX', 60 * 60).catch(() => { });
|
|
581
623
|
try {
|
|
582
624
|
const domain = topic.split(':')[0] ?? 'unknown';
|
|
583
625
|
const personId = await ckgUpsertPersonNode(redis, author, domain);
|
|
@@ -791,12 +833,16 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
791
833
|
// text-similar failed attempts (the moat; see src/rerank.ts + Cachly-Bench). ββ
|
|
792
834
|
const kwMatches = rerankByQuality(rawMatches);
|
|
793
835
|
const savedHere = [];
|
|
836
|
+
// Exclude archived lessons β they are kept for audit but must not surface in recall.
|
|
794
837
|
const lessonMatches = kwMatches.filter(m => m.key.startsWith('cachly:lesson:best:'));
|
|
795
838
|
let crossAuthorThisCall = 0;
|
|
796
839
|
for (const m of lessonMatches.slice(0, 5)) {
|
|
797
840
|
const existing = await redis.get(m.key).catch(() => null);
|
|
798
841
|
if (existing) {
|
|
799
842
|
const lesson = safeJsonParse(existing, null);
|
|
843
|
+
// Skip archived lessons β brain_hygiene moves them here; they should not resurface.
|
|
844
|
+
if (lesson?.state === 'archived')
|
|
845
|
+
continue;
|
|
800
846
|
if (lesson) {
|
|
801
847
|
const updated = { ...lesson, recall_count: (lesson.recall_count ?? 0) + 1, verified_at: new Date().toISOString() };
|
|
802
848
|
redis.set(m.key, JSON.stringify(updated)).catch(() => { });
|
|
@@ -838,9 +884,12 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
838
884
|
redis.set(`cachly:stats:first_recall_at:${instance_id}`, new Date().toISOString(), 'EX', 365 * 86400, 'NX').catch(() => { });
|
|
839
885
|
}
|
|
840
886
|
// ββ Layer 2: Semantic search (parallel, optional) ββββββββββββββββββββββββ
|
|
887
|
+
// Semantic recall is a Premium depth layer: free tier keeps full keyword +
|
|
888
|
+
// CKG recall (the magic moment), paid tiers add embedding-based retrieval.
|
|
841
889
|
const inst = await apiFetch(`/api/v1/instances/${instance_id}`).catch(() => null);
|
|
890
|
+
const tierIsFree = !inst?.tier || inst.tier.toLowerCase() === 'free';
|
|
842
891
|
const semHits = [];
|
|
843
|
-
if (inst?.vector_token && hasEmbedProvider()) {
|
|
892
|
+
if (!tierIsFree && inst?.vector_token && hasEmbedProvider()) {
|
|
844
893
|
try {
|
|
845
894
|
const embedding = await computeEmbedding(query);
|
|
846
895
|
const vectorUrl = `https://api.cachly.dev/v1/sem/${inst.vector_token}`;
|
|
@@ -980,6 +1029,10 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
980
1029
|
return true;
|
|
981
1030
|
})
|
|
982
1031
|
.sort((a, b) => b.hybridScore - a.hybridScore);
|
|
1032
|
+
// ββ Teaser-Gate: free tier over its recall limit hides the long tail ββββββ
|
|
1033
|
+
const recallGate = await getRecallGate(instance_id, apiFetch);
|
|
1034
|
+
const gateActive = recallGate.reached && hybridResults.length > FREE_TIER_VISIBLE_RECALLS;
|
|
1035
|
+
const visibleCount = gateActive ? FREE_TIER_VISIBLE_RECALLS : 8;
|
|
983
1036
|
// ββ Build output ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
984
1037
|
const lines = [`π§ **Smart Recall** for: _"${query}"_\n`];
|
|
985
1038
|
// "Brain saved you here" banner β surfaces value inline for proven recalled lessons
|
|
@@ -1029,7 +1082,7 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
1029
1082
|
// Group by sub-query for multi-topic queries
|
|
1030
1083
|
if (subQueries.length > 1) {
|
|
1031
1084
|
const grouped = new Map();
|
|
1032
|
-
for (const r of hybridResults.slice(0, 12)) {
|
|
1085
|
+
for (const r of hybridResults.slice(0, gateActive ? FREE_TIER_VISIBLE_RECALLS : 12)) {
|
|
1033
1086
|
const sq = r.subQuery ?? query;
|
|
1034
1087
|
if (!grouped.has(sq))
|
|
1035
1088
|
grouped.set(sq, []);
|
|
@@ -1060,7 +1113,7 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
1060
1113
|
}
|
|
1061
1114
|
}
|
|
1062
1115
|
else {
|
|
1063
|
-
for (const r of hybridResults.slice(0,
|
|
1116
|
+
for (const r of hybridResults.slice(0, visibleCount)) {
|
|
1064
1117
|
const label = r.key.replace('cachly:ctx:', 'π ').replace('cachly:lesson:best:', 'π‘ ').replace('cachly:idx:', 'π ');
|
|
1065
1118
|
let authorBadge = '';
|
|
1066
1119
|
if (r.key.startsWith('cachly:lesson:best:')) {
|
|
@@ -1079,6 +1132,13 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
1079
1132
|
lines.push(`> ${preview}${r.content.length > 400 ? 'β¦' : ''}\n`);
|
|
1080
1133
|
}
|
|
1081
1134
|
}
|
|
1135
|
+
// ββ Teaser-Gate footer: sell the withheld tail at the moment of need ββ
|
|
1136
|
+
if (gateActive) {
|
|
1137
|
+
const withheld = Math.max(0, hybridResults.length - visibleCount);
|
|
1138
|
+
if (withheld > 0) {
|
|
1139
|
+
lines.push(`π **${withheld} more relevant lesson${withheld !== 1 ? 's' : ''} available** β you've reached your Free recall limit (${recallGate.used}/${recallGate.limit}). Unlock your full Brain β ${UPGRADE_URL}\n`);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1082
1142
|
}
|
|
1083
1143
|
else {
|
|
1084
1144
|
lines.push(`β οΈ No matches found for: "${query}"`);
|