@cachly-dev/mcp-server 0.10.117 → 0.10.119
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 +9 -5
- package/dist/src/ambient-cli.d.ts +85 -0
- package/dist/src/ambient-cli.js +199 -0
- package/dist/src/ambient-cli.js.map +1 -0
- package/dist/src/ambient-hooks.d.ts +128 -0
- package/dist/src/ambient-hooks.js +269 -0
- package/dist/src/ambient-hooks.js.map +1 -0
- package/dist/src/ambient-ledger.d.ts +19 -0
- package/dist/src/ambient-ledger.js +95 -0
- package/dist/src/ambient-ledger.js.map +1 -0
- package/dist/src/ambient-recall.d.ts +59 -0
- package/dist/src/ambient-recall.js +96 -0
- package/dist/src/ambient-recall.js.map +1 -0
- package/dist/src/doctor.d.ts +47 -0
- package/dist/src/doctor.js +247 -0
- package/dist/src/doctor.js.map +1 -0
- package/dist/src/editor.d.ts +9 -0
- package/dist/src/editor.js +63 -0
- package/dist/src/editor.js.map +1 -0
- package/dist/src/first-contact.d.ts +56 -0
- package/dist/src/first-contact.js +108 -0
- package/dist/src/first-contact.js.map +1 -0
- package/dist/src/funnel-milestones.d.ts +5 -0
- package/dist/src/funnel-milestones.js +48 -0
- package/dist/src/funnel-milestones.js.map +1 -0
- package/dist/src/handlers/brain.d.ts +1 -0
- package/dist/src/handlers/brain.js +45 -8
- package/dist/src/handlers/brain.js.map +1 -1
- package/dist/src/handlers/fedbrain.js +48 -16
- package/dist/src/handlers/fedbrain.js.map +1 -1
- package/dist/src/handlers/syndicate.js +91 -1
- package/dist/src/handlers/syndicate.js.map +1 -1
- package/dist/src/handlers/team.d.ts +10 -0
- package/dist/src/handlers/team.js +45 -1
- package/dist/src/handlers/team.js.map +1 -1
- package/dist/src/index.d.ts +27 -5
- package/dist/src/index.js +293 -28
- package/dist/src/index.js.map +1 -1
- package/dist/src/tools.d.ts +5 -0
- package/dist/src/tools.js +12 -1
- package/dist/src/tools.js.map +1 -1
- package/glama.json +1 -1
- package/package.json +2 -2
- package/server.json +4 -4
package/dist/src/index.js
CHANGED
|
@@ -524,6 +524,60 @@ async function pollDeviceFlow(flow) {
|
|
|
524
524
|
return 'pending';
|
|
525
525
|
}
|
|
526
526
|
}
|
|
527
|
+
/**
|
|
528
|
+
* Mutex wrapper so the background poller (below) and a user-triggered tool call
|
|
529
|
+
* never poll the same device code concurrently — a double poll can race the
|
|
530
|
+
* one-time JWT→API-key exchange.
|
|
531
|
+
*/
|
|
532
|
+
async function pollDeviceFlowGuarded(flow) {
|
|
533
|
+
if (flow.polling)
|
|
534
|
+
return 'pending'; // another poll owns this tick
|
|
535
|
+
flow.polling = true;
|
|
536
|
+
try {
|
|
537
|
+
return await pollDeviceFlow(flow);
|
|
538
|
+
}
|
|
539
|
+
finally {
|
|
540
|
+
flow.polling = false;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* Once the device code is issued, poll in the BACKGROUND on the flow's interval
|
|
545
|
+
* so browser sign-in self-completes. Without this, the flow only advanced when
|
|
546
|
+
* the user manually triggered ANOTHER tool call — the #1 onboarding drop-off
|
|
547
|
+
* (report: device flow 36→1 = 3%). Now the user signs in and their Brain
|
|
548
|
+
* activates on its own; their next natural request just works instead of
|
|
549
|
+
* showing "still waiting". Bounded by the flow deadline, timer unref'd so it
|
|
550
|
+
* never keeps the process alive, fully fail-safe (never throws). `poll` and
|
|
551
|
+
* `isCurrent` are injectable for testing.
|
|
552
|
+
*/
|
|
553
|
+
export function startBackgroundDevicePoll(flow, poll = pollDeviceFlowGuarded, isCurrent = (f) => _deviceFlow === f, schedule = defaultUnrefTimeout) {
|
|
554
|
+
const tick = async () => {
|
|
555
|
+
if (!isCurrent(flow))
|
|
556
|
+
return; // superseded or already completed → stop
|
|
557
|
+
let status = 'pending';
|
|
558
|
+
try {
|
|
559
|
+
status = await poll(flow);
|
|
560
|
+
}
|
|
561
|
+
catch {
|
|
562
|
+
/* keep polling — a transient network error must not abort sign-in */
|
|
563
|
+
}
|
|
564
|
+
if (status === 'done')
|
|
565
|
+
return; // poll already set JWT + persisted the key
|
|
566
|
+
if (status === 'expired') {
|
|
567
|
+
if (isCurrent(flow))
|
|
568
|
+
_deviceFlow = null;
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
if (isCurrent(flow))
|
|
572
|
+
schedule(tick, flow.pollInterval);
|
|
573
|
+
};
|
|
574
|
+
schedule(tick, flow.pollInterval);
|
|
575
|
+
}
|
|
576
|
+
function defaultUnrefTimeout(fn, ms) {
|
|
577
|
+
const t = setTimeout(fn, ms);
|
|
578
|
+
if (typeof t.unref === 'function')
|
|
579
|
+
t.unref();
|
|
580
|
+
}
|
|
527
581
|
// ── Embeddings (imported from embeddings.ts) ────────────────────────────────
|
|
528
582
|
import { setEmbedJwt } from './embeddings.js';
|
|
529
583
|
// ── Search Engine (imported from search.ts) ─────────────────────────────────
|
|
@@ -694,6 +748,12 @@ import { handleAdvancedTool } from './handlers/advanced.js';
|
|
|
694
748
|
import { handleSyndicateTool } from './handlers/syndicate.js';
|
|
695
749
|
import { handleFedbrainTool, _lastBrainFromGitCounts } from './handlers/fedbrain.js';
|
|
696
750
|
import { installClsPostCommitHook, CLS_HOOK_VERSION } from './cls-hook.js';
|
|
751
|
+
import { installAmbientHooks, AMBIENT_HOOK_VERSION } from './ambient-hooks.js';
|
|
752
|
+
import { runAmbient, truncateToTokens, parseHookPayload, stopObservation } from './ambient-cli.js';
|
|
753
|
+
import { appendLedgerEntry, readLedger, defaultLedgerPath } from './ambient-ledger.js';
|
|
754
|
+
import { detectEditor as detectEditorImpl } from './editor.js';
|
|
755
|
+
import { milestoneSent, markMilestoneSent } from './funnel-milestones.js';
|
|
756
|
+
import { netBalance, shouldBackoff } from './ambient-recall.js';
|
|
697
757
|
import { handleShareTool } from './handlers/share.js';
|
|
698
758
|
import { handleVizTool } from './handlers/viz.js';
|
|
699
759
|
// ── Tools (imported from tools.ts) ─────────────────────────────────────────
|
|
@@ -703,12 +763,10 @@ import { TOOLS } from './tools.js';
|
|
|
703
763
|
let _telemetryPingSent = false;
|
|
704
764
|
// Fires once per process on the first successful Brain tool call — key activation metric
|
|
705
765
|
let _firstCallSuccessSent = false;
|
|
766
|
+
// Host detection lives in editor.ts (pure + unit-tested). Kept as a thin
|
|
767
|
+
// wrapper so the many call sites here stay unchanged.
|
|
706
768
|
function detectEditor() {
|
|
707
|
-
return
|
|
708
|
-
: process.env.WINDSURF_SESSION_ID ? 'windsurf'
|
|
709
|
-
: process.env.GITHUB_COPILOT_WORKSPACE ? 'copilot'
|
|
710
|
-
: process.env.CLAUDE_CODE_ENTRYPOINT ? 'claude'
|
|
711
|
-
: 'unknown';
|
|
769
|
+
return detectEditorImpl();
|
|
712
770
|
}
|
|
713
771
|
/** Derive an anonymous, non-reversible fingerprint from the JWT sub claim. */
|
|
714
772
|
function _jwtUserFingerprint(jwt) {
|
|
@@ -748,6 +806,21 @@ function sendFunnelEvent(event, extra, metrics) {
|
|
|
748
806
|
signal: AbortSignal.timeout(3000),
|
|
749
807
|
}).catch(() => { });
|
|
750
808
|
}
|
|
809
|
+
// Mirrors one ambient net-token ledger entry to the API (Phase 4 v2: the
|
|
810
|
+
// org-wide dashboard aggregates these per team). Best-effort fire-and-forget —
|
|
811
|
+
// the local JSONL ledger stays authoritative; this never throws and never
|
|
812
|
+
// blocks a hook. Returns the fetch promise so short-lived CLI paths can flush
|
|
813
|
+
// (bounded) before process.exit kills the socket.
|
|
814
|
+
function reportAmbientLedgerEvent(instanceId, entry) {
|
|
815
|
+
if (!instanceId || !JWT || process.env.CACHLY_NO_TELEMETRY === '1')
|
|
816
|
+
return Promise.resolve();
|
|
817
|
+
return fetch(`${API_URL}/api/v1/instances/${instanceId}/ambient-events`, {
|
|
818
|
+
method: 'POST',
|
|
819
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${JWT}` },
|
|
820
|
+
body: JSON.stringify({ events: [entry] }),
|
|
821
|
+
signal: AbortSignal.timeout(3000),
|
|
822
|
+
}).then(() => undefined, () => undefined);
|
|
823
|
+
}
|
|
751
824
|
async function sendAnonymousTelemetry(toolName) {
|
|
752
825
|
if (_telemetryPingSent)
|
|
753
826
|
return;
|
|
@@ -833,7 +906,7 @@ async function handleTool(name, args) {
|
|
|
833
906
|
// 1st call: start device flow, return code + URL
|
|
834
907
|
// 2nd+ calls: poll for token; once authenticated, proceed transparently
|
|
835
908
|
if (_deviceFlow) {
|
|
836
|
-
const result = await
|
|
909
|
+
const result = await pollDeviceFlowGuarded(_deviceFlow);
|
|
837
910
|
if (result === 'done') {
|
|
838
911
|
// Auth complete — re-enter handleTool with now-valid JWT
|
|
839
912
|
return handleTool(name, args);
|
|
@@ -843,14 +916,14 @@ async function handleTool(name, args) {
|
|
|
843
916
|
_deviceFlow = null;
|
|
844
917
|
return '⌛ **Authentication timed out.** Please call any tool again to restart the sign-in flow.';
|
|
845
918
|
}
|
|
846
|
-
// Still pending
|
|
919
|
+
// Still pending — the background poller will complete sign-in on its own.
|
|
847
920
|
return [
|
|
848
|
-
'⏳ **
|
|
921
|
+
'⏳ **Signing you in…**',
|
|
849
922
|
'',
|
|
850
|
-
`
|
|
851
|
-
`
|
|
923
|
+
`Finish in your browser: **${_deviceFlow.verifyUrl}**`,
|
|
924
|
+
`Code: **${_deviceFlow.userCode}**`,
|
|
852
925
|
'',
|
|
853
|
-
'
|
|
926
|
+
'This completes automatically once you sign in — no need to come back here.',
|
|
854
927
|
].join('\n');
|
|
855
928
|
}
|
|
856
929
|
// No pending flow — start a new one
|
|
@@ -860,6 +933,9 @@ async function handleTool(name, args) {
|
|
|
860
933
|
sendFunnelEvent('device_flow_started', { tool: name });
|
|
861
934
|
// Try to open the browser automatically — fire-and-forget, never block
|
|
862
935
|
openInBrowser(flow.verifyUrl);
|
|
936
|
+
// Poll in the background so sign-in self-completes — the user no longer
|
|
937
|
+
// has to trigger another tool call to drive the flow (the #1 drop-off).
|
|
938
|
+
startBackgroundDevicePoll(flow);
|
|
863
939
|
return [
|
|
864
940
|
'🧠 **cachly AI Brain — sign in to activate** (browser opening...)',
|
|
865
941
|
'',
|
|
@@ -867,7 +943,8 @@ async function handleTool(name, args) {
|
|
|
867
943
|
'',
|
|
868
944
|
`Code: **${flow.userCode}** (pre-filled if browser opened automatically)`,
|
|
869
945
|
'',
|
|
870
|
-
'
|
|
946
|
+
'That’s it — your Brain activates automatically the moment you finish',
|
|
947
|
+
'signing in. Just keep working; your next request arrives brain-powered.',
|
|
871
948
|
'',
|
|
872
949
|
'✨ Free forever · No credit card · 126 MCP tools · GDPR · EU servers',
|
|
873
950
|
].join('\n');
|
|
@@ -904,7 +981,12 @@ async function handleTool(name, args) {
|
|
|
904
981
|
if (brainResult !== null) {
|
|
905
982
|
if (!_firstCallSuccessSent && JWT) {
|
|
906
983
|
_firstCallSuccessSent = true;
|
|
907
|
-
|
|
984
|
+
// Persistent guard: ambient hooks spawn a fresh process per prompt, so the
|
|
985
|
+
// in-memory flag alone re-sent "first call" on every hook invocation.
|
|
986
|
+
if (!milestoneSent('first_call_success')) {
|
|
987
|
+
markMilestoneSent('first_call_success');
|
|
988
|
+
sendFunnelEvent('first_call_success', { tool: name, instance_id: args.instance_id ?? _defaultInstanceId ?? '' });
|
|
989
|
+
}
|
|
908
990
|
}
|
|
909
991
|
// Per-tool telemetry — fires after every successful brain tool call.
|
|
910
992
|
// api_key carries the cky_live_... token so the backend can resolve tenant + increment counters.
|
|
@@ -1007,7 +1089,12 @@ async function handleTool(name, args) {
|
|
|
1007
1089
|
if (contextResult !== null) {
|
|
1008
1090
|
if (!_firstCallSuccessSent && JWT) {
|
|
1009
1091
|
_firstCallSuccessSent = true;
|
|
1010
|
-
|
|
1092
|
+
// Persistent guard: ambient hooks spawn a fresh process per prompt, so the
|
|
1093
|
+
// in-memory flag alone re-sent "first call" on every hook invocation.
|
|
1094
|
+
if (!milestoneSent('first_call_success')) {
|
|
1095
|
+
markMilestoneSent('first_call_success');
|
|
1096
|
+
sendFunnelEvent('first_call_success', { tool: name, instance_id: args.instance_id ?? _defaultInstanceId ?? '' });
|
|
1097
|
+
}
|
|
1011
1098
|
}
|
|
1012
1099
|
return contextResult;
|
|
1013
1100
|
}
|
|
@@ -1178,7 +1265,19 @@ async function handleTool(name, args) {
|
|
|
1178
1265
|
return `⚠️ Unknown tool: ${name}`;
|
|
1179
1266
|
}
|
|
1180
1267
|
}
|
|
1181
|
-
|
|
1268
|
+
// Pushed to every MCP host at connect time (initialize response). This is the
|
|
1269
|
+
// "SessionStart-equivalent" for harnesses without a per-prompt hook — Cursor,
|
|
1270
|
+
// Windsurf, Cline, Copilot agent-mode all surface `instructions`. It's a one-time
|
|
1271
|
+
// nudge (not per-prompt recall), so it stays short and behavior-focused.
|
|
1272
|
+
const CACHLY_MCP_INSTRUCTIONS = `This project has a Cachly AI Brain — persistent, cross-session memory of past fixes, decisions, and gotchas for THIS codebase. Use it proactively; it makes you faster and stops you repeating known mistakes.
|
|
1273
|
+
|
|
1274
|
+
- BEFORE a non-trivial task: call smart_recall with a short description of what you're about to do. If it returns lessons, apply them before researching from scratch.
|
|
1275
|
+
- WHEN debugging an error: call causal_trace with the exact error first — a known fix may already exist.
|
|
1276
|
+
- AFTER a fix, deploy, or discovery: call learn_from_attempts so the knowledge persists.
|
|
1277
|
+
- BEFORE a risky op (deploy, migration, dependency bump): call brain_predict.
|
|
1278
|
+
|
|
1279
|
+
The Brain instance id comes from the CACHLY_BRAIN_INSTANCE_ID environment variable.`;
|
|
1280
|
+
const server = new Server({ name: 'cachly-mcp', version: CURRENT_VERSION }, { capabilities: { tools: {} }, instructions: CACHLY_MCP_INSTRUCTIONS });
|
|
1182
1281
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
1183
1282
|
// ── Auto-session management ───────────────────────────────────────────────────
|
|
1184
1283
|
// Transparently starts a Brain session on the first tool call of a process
|
|
@@ -1467,31 +1566,49 @@ Report predicted failure risks to the user before proceeding.
|
|
|
1467
1566
|
Never commit code that does not compile. Run \`tsc --noEmit\` / \`go build ./...\` before every commit.
|
|
1468
1567
|
${CLAUDE_MD_MARKER_END}`;
|
|
1469
1568
|
}
|
|
1569
|
+
// Cursor loads rules from .cursor/rules/*.mdc with YAML frontmatter. alwaysApply
|
|
1570
|
+
// makes the block active in every request (the closest Cursor gets to a
|
|
1571
|
+
// SessionStart briefing). The frontmatter sits OUTSIDE the brain markers so
|
|
1572
|
+
// idempotent marker-replacement leaves it intact.
|
|
1573
|
+
const CURSOR_MDC_FRONTMATTER = '---\n' +
|
|
1574
|
+
'description: Cachly AI Brain — persistent memory protocol (recall before tasks, learn after fixes)\n' +
|
|
1575
|
+
'alwaysApply: true\n' +
|
|
1576
|
+
'---\n\n';
|
|
1470
1577
|
/**
|
|
1471
|
-
* Schreibt das Brain-Protokoll in alle relevanten Instruction-Dateien:
|
|
1472
|
-
* - CLAUDE.md
|
|
1473
|
-
* - AGENTS.md
|
|
1474
|
-
* - .github/copilot-instructions.md
|
|
1475
|
-
*
|
|
1578
|
+
* Schreibt das Brain-Protokoll in alle relevanten Instruction-/Rules-Dateien:
|
|
1579
|
+
* - CLAUDE.md (Claude Code)
|
|
1580
|
+
* - AGENTS.md (Codex / generic agents)
|
|
1581
|
+
* - .github/copilot-instructions.md (GitHub Copilot)
|
|
1582
|
+
* - .cursor/rules/cachly.mdc (Cursor — frontmatter + alwaysApply)
|
|
1583
|
+
* - .windsurfrules (Windsurf / Cascade)
|
|
1584
|
+
* - .clinerules (Cline)
|
|
1585
|
+
* Idempotent, Marker-basiert. Cross-Harness Tier A (siehe make_cachly_great_again.md §6.7).
|
|
1476
1586
|
*/
|
|
1477
1587
|
export async function writeInstructions(projectDir, instanceId) {
|
|
1478
1588
|
const { writeFile, appendFile, readFile, mkdir } = await import('node:fs/promises');
|
|
1479
1589
|
const { existsSync } = await import('node:fs');
|
|
1480
1590
|
const { resolve, dirname } = await import('node:path');
|
|
1591
|
+
// `prefix` is written only when the file is created fresh (Cursor .mdc needs it);
|
|
1592
|
+
// on marker-update/append the prefix is left untouched.
|
|
1481
1593
|
const files = [
|
|
1482
|
-
resolve(projectDir, 'CLAUDE.md'),
|
|
1483
|
-
resolve(projectDir, 'AGENTS.md'),
|
|
1484
|
-
resolve(projectDir, '.github', 'copilot-instructions.md'),
|
|
1594
|
+
{ path: resolve(projectDir, 'CLAUDE.md') },
|
|
1595
|
+
{ path: resolve(projectDir, 'AGENTS.md') },
|
|
1596
|
+
{ path: resolve(projectDir, '.github', 'copilot-instructions.md') },
|
|
1597
|
+
{ path: resolve(projectDir, '.cursor', 'rules', 'cachly.mdc'), prefix: CURSOR_MDC_FRONTMATTER },
|
|
1598
|
+
{ path: resolve(projectDir, '.windsurfrules') },
|
|
1599
|
+
{ path: resolve(projectDir, '.clinerules') },
|
|
1485
1600
|
];
|
|
1486
1601
|
const block = '\n' + buildClaudeMdBlock(instanceId) + '\n';
|
|
1487
1602
|
const results = {};
|
|
1488
|
-
for (const file of files) {
|
|
1603
|
+
for (const { path: file, prefix } of files) {
|
|
1489
1604
|
await mkdir(dirname(file), { recursive: true });
|
|
1490
1605
|
if (existsSync(file)) {
|
|
1491
1606
|
const existing = await readFile(file, 'utf-8');
|
|
1492
1607
|
if (existing.includes(CLAUDE_MD_MARKER_START)) {
|
|
1493
|
-
// Idempotent update: replace existing block
|
|
1494
|
-
|
|
1608
|
+
// Idempotent update: replace existing block ([\s\S] must keep its
|
|
1609
|
+
// backslashes so it matches any char — a bare [\s\S] in the template
|
|
1610
|
+
// literal collapses to [sS] and the replace silently no-ops).
|
|
1611
|
+
const updated = existing.replace(new RegExp(`${CLAUDE_MD_MARKER_START}[\\s\\S]*?${CLAUDE_MD_MARKER_END}`), buildClaudeMdBlock(instanceId));
|
|
1495
1612
|
await writeFile(file, updated, 'utf-8');
|
|
1496
1613
|
results[file] = 'updated';
|
|
1497
1614
|
continue;
|
|
@@ -1500,7 +1617,7 @@ export async function writeInstructions(projectDir, instanceId) {
|
|
|
1500
1617
|
results[file] = 'appended';
|
|
1501
1618
|
continue;
|
|
1502
1619
|
}
|
|
1503
|
-
await writeFile(file, block.trimStart(), 'utf-8');
|
|
1620
|
+
await writeFile(file, (prefix ?? '') + block.trimStart(), 'utf-8');
|
|
1504
1621
|
results[file] = 'written';
|
|
1505
1622
|
}
|
|
1506
1623
|
return results;
|
|
@@ -2408,6 +2525,22 @@ if (process.argv[2] === 'init') {
|
|
|
2408
2525
|
console.log(`✓ CLS hook already current in .git/hooks/post-commit`);
|
|
2409
2526
|
}
|
|
2410
2527
|
catch { /* non-critical — git hook is a best-effort feature */ }
|
|
2528
|
+
// ── Ambient Recall Phase 4: auto-install SessionStart + UserPromptSubmit hooks ─
|
|
2529
|
+
// Only for Claude Code (the hooks config lives in .claude/settings.json).
|
|
2530
|
+
if (editor === 'claude') {
|
|
2531
|
+
try {
|
|
2532
|
+
const a = await installAmbientHooks(projectDir, instanceId, JWT || undefined);
|
|
2533
|
+
const scriptVerb = a.scripts === 'written' ? '✅ Written' : a.scripts === 'upgraded' ? `✅ Upgraded (→ ${AMBIENT_HOOK_VERSION})` : '✓ Unchanged';
|
|
2534
|
+
console.log(`${scriptVerb}: .claude/hooks/ (Ambient Recall — session briefing, per-prompt recall, file briefing, auto-learn)`);
|
|
2535
|
+
if (a.settings === 'written')
|
|
2536
|
+
console.log(`✅ Written: .claude/settings.json (Ambient Recall hooks wired)`);
|
|
2537
|
+
else if (a.settings === 'merged')
|
|
2538
|
+
console.log(`✅ Merged: .claude/settings.json (Ambient Recall hooks wired)`);
|
|
2539
|
+
else
|
|
2540
|
+
console.log(`✓ Ambient Recall hooks already wired in .claude/settings.json`);
|
|
2541
|
+
}
|
|
2542
|
+
catch { /* non-critical — ambient hooks are a best-effort feature */ }
|
|
2543
|
+
}
|
|
2411
2544
|
const initSecs = ((Date.now() - initStart) / 1000).toFixed(1);
|
|
2412
2545
|
console.log(`\n🧠 Cachly AI Brain configured for ${editor === 'claude' ? 'Claude Code' : editor}! (${initSecs}s)`);
|
|
2413
2546
|
console.log(` Restart your editor — the \`cachly\` MCP tools will appear.`);
|
|
@@ -3395,6 +3528,138 @@ if (process.argv[2] === 'learn-git') {
|
|
|
3395
3528
|
}
|
|
3396
3529
|
process.exit(0);
|
|
3397
3530
|
}
|
|
3531
|
+
// ── ambient-recall: CLI entrypoint for the Claude Code hooks ──────────────────
|
|
3532
|
+
// Invoked as: <hook payload JSON on stdin> | cachly ambient-recall
|
|
3533
|
+
// SessionStart/UserPromptSubmit/PreToolUse payloads recall through the §6.3
|
|
3534
|
+
// relevance gate (ambient-cli.ts) and print the `hookSpecificOutput` JSON that
|
|
3535
|
+
// Claude Code injects as additionalContext — or nothing. Stop payloads instead
|
|
3536
|
+
// feed a fix-signal observation to auto_learn_session (the automatic
|
|
3537
|
+
// `learn_from_attempts`). Injections are booked into the net-token ledger
|
|
3538
|
+
// (§6.2) and auto-backoff kicks in when the recent window is net-negative.
|
|
3539
|
+
// Best-effort: no JWT, no stdin, a slow brain or any error → prints nothing and
|
|
3540
|
+
// exits 0 so a hook can NEVER block or corrupt the agent's turn.
|
|
3541
|
+
if (process.argv[2] === 'ambient-recall') {
|
|
3542
|
+
try {
|
|
3543
|
+
if (process.stdin.isTTY)
|
|
3544
|
+
process.exit(0); // no piped payload → nothing to do
|
|
3545
|
+
const chunks = [];
|
|
3546
|
+
for await (const chunk of process.stdin)
|
|
3547
|
+
chunks.push(chunk);
|
|
3548
|
+
const raw = Buffer.concat(chunks).toString('utf-8');
|
|
3549
|
+
if (!JWT)
|
|
3550
|
+
process.exit(0); // not authenticated → silent no-op
|
|
3551
|
+
const instanceId = process.env.CACHLY_BRAIN_INSTANCE_ID ?? _defaultInstanceId;
|
|
3552
|
+
// Stop event → auto-learn, never inject (roadmap §6.1 PostToolUse/Stop row).
|
|
3553
|
+
const stopPayload = parseHookPayload(raw);
|
|
3554
|
+
if (stopPayload?.hook_event_name === 'Stop') {
|
|
3555
|
+
const obs = stopObservation(stopPayload);
|
|
3556
|
+
if (obs && instanceId) {
|
|
3557
|
+
await handleTool('auto_learn_session', { instance_id: instanceId, observations: [obs] });
|
|
3558
|
+
}
|
|
3559
|
+
process.exit(0);
|
|
3560
|
+
}
|
|
3561
|
+
let reported;
|
|
3562
|
+
const out = await runAmbient(raw, {
|
|
3563
|
+
timeoutMs: 3000, // §6.3 guardrail 4: never stall the prompt on a slow brain
|
|
3564
|
+
// Ambient injection budget: one relevance-ranked briefing, hard-capped.
|
|
3565
|
+
// smart_recall already returns its hits best-first, so the capped head is
|
|
3566
|
+
// the high-signal part. Per-lesson structured candidates are a future slice.
|
|
3567
|
+
gate: { topK: 1, maxTokens: 500 },
|
|
3568
|
+
backoff: async () => shouldBackoff(await readLedger()),
|
|
3569
|
+
onInject: (tokens, event) => {
|
|
3570
|
+
const entry = { ts: new Date().toISOString(), event, injected: tokens, prevented: 0 };
|
|
3571
|
+
void appendLedgerEntry(entry);
|
|
3572
|
+
reported = reportAmbientLedgerEvent(instanceId, entry); // org dashboard mirror
|
|
3573
|
+
},
|
|
3574
|
+
recall: async (query) => {
|
|
3575
|
+
if (!instanceId)
|
|
3576
|
+
return [];
|
|
3577
|
+
const text = String((await handleTool('smart_recall', { instance_id: instanceId, query })) ?? '');
|
|
3578
|
+
const miss = text.length < 80 || /No lessons found|no lessons|No matches found/i.test(text);
|
|
3579
|
+
if (miss)
|
|
3580
|
+
return [];
|
|
3581
|
+
return [{ id: 'ambient', summary: truncateToTokens(text, 500), confidence: 0.9, score: 0.9 }];
|
|
3582
|
+
},
|
|
3583
|
+
});
|
|
3584
|
+
if (out)
|
|
3585
|
+
process.stdout.write(out);
|
|
3586
|
+
// Bounded flush so the dashboard mirror survives process.exit — never more
|
|
3587
|
+
// than 500ms on top of a turn that already injected.
|
|
3588
|
+
if (reported)
|
|
3589
|
+
await Promise.race([reported, new Promise((r) => setTimeout(r, 500))]);
|
|
3590
|
+
}
|
|
3591
|
+
catch {
|
|
3592
|
+
// Swallow everything — an ambient hook must never break the agent.
|
|
3593
|
+
}
|
|
3594
|
+
process.exit(0);
|
|
3595
|
+
}
|
|
3596
|
+
// ── ambient-credit: agent-reported prevented-token credit (§6.2) ──────────────
|
|
3597
|
+
// Invoked as: cachly ambient-credit <tokens> [note…] — the injected context's
|
|
3598
|
+
// footer invites the agent to call this when a recalled lesson changed its path.
|
|
3599
|
+
// This is the client-side signal that makes the net ledger (and auto-backoff)
|
|
3600
|
+
// meaningful before the server-side dashboard exists. Silent + exit 0 always.
|
|
3601
|
+
if (process.argv[2] === 'ambient-credit') {
|
|
3602
|
+
try {
|
|
3603
|
+
const tokens = Math.round(Number(process.argv[3]));
|
|
3604
|
+
if (Number.isFinite(tokens) && tokens > 0) {
|
|
3605
|
+
// Cap a single credit: self-reported savings should never let one
|
|
3606
|
+
// enthusiastic claim mask weeks of negative balance.
|
|
3607
|
+
const capped = Math.min(tokens, 20_000);
|
|
3608
|
+
const note = process.argv.slice(4).join(' ').slice(0, 200) || undefined;
|
|
3609
|
+
const entry = { ts: new Date().toISOString(), event: 'credit', injected: 0, prevented: capped, note };
|
|
3610
|
+
await appendLedgerEntry(entry);
|
|
3611
|
+
await reportAmbientLedgerEvent(process.env.CACHLY_BRAIN_INSTANCE_ID ?? _defaultInstanceId, entry); // org dashboard mirror
|
|
3612
|
+
console.log(`✅ ambient credit recorded: ${capped} tokens${note ? ` (${note})` : ''}`);
|
|
3613
|
+
}
|
|
3614
|
+
else {
|
|
3615
|
+
console.error('Usage: npx @cachly-dev/mcp-server@latest ambient-credit <tokens-saved> [note]');
|
|
3616
|
+
}
|
|
3617
|
+
}
|
|
3618
|
+
catch { /* telemetry — never fail loudly */ }
|
|
3619
|
+
process.exit(0);
|
|
3620
|
+
}
|
|
3621
|
+
// ── ambient-stats: the honest net-token readout (§6.2) ────────────────────────
|
|
3622
|
+
// Shows injected vs prevented and the NET — even when it is negative — plus
|
|
3623
|
+
// whether auto-backoff is currently pausing injection.
|
|
3624
|
+
if (process.argv[2] === 'ambient-stats') {
|
|
3625
|
+
const entries = await readLedger();
|
|
3626
|
+
const bal = netBalance(entries);
|
|
3627
|
+
const backing = shouldBackoff(entries);
|
|
3628
|
+
const recent = entries.slice(-5);
|
|
3629
|
+
console.log('\n🧠 Ambient Recall — net-token ledger\n');
|
|
3630
|
+
console.log(` Turns recorded: ${entries.length}`);
|
|
3631
|
+
console.log(` Injected tokens: ${bal.injected}`);
|
|
3632
|
+
console.log(` Prevented tokens: ${bal.prevented} (agent-reported via ambient-credit)`);
|
|
3633
|
+
console.log(` NET: ${bal.net >= 0 ? '+' : ''}${bal.net} tokens`);
|
|
3634
|
+
console.log(` Auto-backoff: ${backing ? '🔴 ACTIVE — recent window is net-negative, injection paused' : '🟢 inactive'}`);
|
|
3635
|
+
if (recent.length > 0) {
|
|
3636
|
+
console.log('\n Last entries:');
|
|
3637
|
+
for (const e of recent) {
|
|
3638
|
+
const what = e.prevented > 0 ? `+${e.prevented} prevented` : `-${e.injected} injected`;
|
|
3639
|
+
console.log(` • ${e.ts.slice(0, 19)} ${e.event}: ${what}${e.note ? ` (${e.note})` : ''}`);
|
|
3640
|
+
}
|
|
3641
|
+
}
|
|
3642
|
+
console.log('');
|
|
3643
|
+
process.exit(0);
|
|
3644
|
+
}
|
|
3645
|
+
// ── doctor: one-command setup diagnosis ───────────────────────────────────────
|
|
3646
|
+
// Walks the whole activation chain — runtime → credential → API → auth →
|
|
3647
|
+
// instance → hooks → ledger — and prints one ✅/⚠️/❌ report with a concrete
|
|
3648
|
+
// next step per finding. Exit 1 only on hard failures (warnings stay 0).
|
|
3649
|
+
if (process.argv[2] === 'doctor') {
|
|
3650
|
+
const { checkNodeVersion, checkCredential, checkApiReachable, checkAuthAccepted, checkInstance, inspectAmbientHooks, checkHooks, checkLedger, renderDoctorReport, doctorExitCode, } = await import('./doctor.js');
|
|
3651
|
+
const checks = [
|
|
3652
|
+
checkNodeVersion(),
|
|
3653
|
+
checkCredential(JWT),
|
|
3654
|
+
await checkApiReachable(API_URL),
|
|
3655
|
+
await checkAuthAccepted(API_URL, JWT),
|
|
3656
|
+
checkInstance(process.env.CACHLY_BRAIN_INSTANCE_ID ?? _defaultInstanceId),
|
|
3657
|
+
checkHooks(inspectAmbientHooks(process.cwd())),
|
|
3658
|
+
checkLedger(await readLedger(), defaultLedgerPath()),
|
|
3659
|
+
];
|
|
3660
|
+
console.log(renderDoctorReport(checks));
|
|
3661
|
+
process.exit(doctorExitCode(checks));
|
|
3662
|
+
}
|
|
3398
3663
|
// ── cls-ingest: CLI entrypoint for the git post-commit hook ───────────────────
|
|
3399
3664
|
// Invoked as: cachly cls-ingest '<json>' where json = { instance_id, source, payload }.
|
|
3400
3665
|
// Auth comes from CACHLY_JWT (embedded in the generated hook). This is a silent,
|
|
@@ -3468,7 +3733,7 @@ if (!JWT) {
|
|
|
3468
3733
|
// • Editor as an MCP stdio server (non-TTY) → ONE stderr hint, then KEEP RUNNING
|
|
3469
3734
|
// so tools/list works and the first tool call starts the browser sign-in.
|
|
3470
3735
|
// Skip entirely for CLI subcommands that intentionally run without credentials.
|
|
3471
|
-
const _cliNoAuthCommands = ['demo', 'share', 'publish', 'health', 'autosetup', 'setup', 'autopilot', 'init', 'digest', 'invite', 'badge', 'join', 'upgrade', 'bench', 'tool-specs', 'openapi'];
|
|
3736
|
+
const _cliNoAuthCommands = ['demo', 'share', 'publish', 'health', 'autosetup', 'setup', 'autopilot', 'init', 'digest', 'invite', 'badge', 'join', 'upgrade', 'bench', 'tool-specs', 'openapi', 'ambient-credit', 'ambient-stats', 'doctor'];
|
|
3472
3737
|
if (!JWT && !_cliNoAuthCommands.includes(process.argv[2] ?? '')) {
|
|
3473
3738
|
const runningInTerminal = !process.argv[2] && process.stdout.isTTY === true && _isMain;
|
|
3474
3739
|
if (runningInTerminal) {
|