@cachly-dev/mcp-server 0.10.116 → 0.10.118
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 +8 -3
- 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/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/handlers/brain.d.ts +1 -0
- package/dist/src/handlers/brain.js +37 -7
- 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 +8 -5
- package/dist/src/index.js +213 -33
- package/dist/src/index.js.map +1 -1
- package/dist/src/tools.d.ts +5 -196
- package/dist/src/tools.js +12 -179
- package/dist/src/tools.js.map +1 -1
- package/dist/src/toolspecs.d.ts +1 -1
- package/dist/src/toolspecs.js +1 -1
- package/glama.json +1 -1
- package/package.json +14 -4
- package/server.json +4 -4
- package/dist/src/handlers/tco.d.ts +0 -16
- package/dist/src/handlers/tco.js +0 -304
- package/dist/src/handlers/tco.js.map +0 -1
package/dist/src/index.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { jwtExpiryMs, checkJwt, handleApiError, diagnoseAuth, planAuthHeal, readClientCredentialsFromEnv, buildClientCredentialsBody, clientCredentialsTokenUrl } from './auth.js';
|
|
3
|
-
import { handleTcoTool } from './handlers/tco.js';
|
|
4
3
|
import { notify } from './notifier.js';
|
|
5
4
|
import { fileURLToPath } from 'node:url';
|
|
6
5
|
import { createRequire } from 'node:module';
|
|
@@ -37,24 +36,12 @@ const _isMain = process.argv[1] != null &&
|
|
|
37
36
|
* ── Auth & Status ────────────────────────────────────────────────────────────
|
|
38
37
|
* • get_api_status – check API health + JWT auth info (Keycloak)
|
|
39
38
|
*
|
|
40
|
-
* ── Travel Chaos Organizer (TCO) ────────────────────────────────────────────
|
|
41
|
-
* • tco_list_trips – list all trips
|
|
42
|
-
* • tco_create_trip – create a new trip
|
|
43
|
-
* • tco_get_timeline – get timeline items for a trip
|
|
44
|
-
* • tco_delete_trip – delete a trip
|
|
45
|
-
* • tco_inbox_list – list Chaos Inbox items
|
|
46
|
-
* • tco_inbox_assign – assign inbox item to a trip
|
|
47
|
-
* • tco_inbox_reject – reject/discard an inbox item
|
|
48
|
-
* • tco_parse_url – fetch a URL and parse with Ollama
|
|
49
|
-
* • tco_import_email – import raw email text
|
|
50
|
-
* (Requires TCO_API_URL env var; auth forwarded from CACHLY_JWT — same Keycloak realm)
|
|
51
39
|
*
|
|
52
40
|
* Configuration (env vars):
|
|
53
41
|
* CACHLY_API_URL – default https://api.cachly.dev
|
|
54
42
|
* CACHLY_JWT – your JWT (Keycloak access token)
|
|
55
43
|
* CACHLY_EMBED_PROVIDER – embedding backend: openai (default), gemini, mistral, cohere, ollama, cachly (server fallback)
|
|
56
44
|
* CACHLY_EMBED_MODEL – override embedding model (optional)
|
|
57
|
-
* TCO_API_URL – Travel Chaos Organizer backend URL (optional, enables tco_* tools)
|
|
58
45
|
*/
|
|
59
46
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
60
47
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
@@ -707,6 +694,10 @@ import { handleAdvancedTool } from './handlers/advanced.js';
|
|
|
707
694
|
import { handleSyndicateTool } from './handlers/syndicate.js';
|
|
708
695
|
import { handleFedbrainTool, _lastBrainFromGitCounts } from './handlers/fedbrain.js';
|
|
709
696
|
import { installClsPostCommitHook, CLS_HOOK_VERSION } from './cls-hook.js';
|
|
697
|
+
import { installAmbientHooks, AMBIENT_HOOK_VERSION } from './ambient-hooks.js';
|
|
698
|
+
import { runAmbient, truncateToTokens, parseHookPayload, stopObservation } from './ambient-cli.js';
|
|
699
|
+
import { appendLedgerEntry, readLedger, defaultLedgerPath } from './ambient-ledger.js';
|
|
700
|
+
import { netBalance, shouldBackoff } from './ambient-recall.js';
|
|
710
701
|
import { handleShareTool } from './handlers/share.js';
|
|
711
702
|
import { handleVizTool } from './handlers/viz.js';
|
|
712
703
|
// ── Tools (imported from tools.ts) ─────────────────────────────────────────
|
|
@@ -761,6 +752,21 @@ function sendFunnelEvent(event, extra, metrics) {
|
|
|
761
752
|
signal: AbortSignal.timeout(3000),
|
|
762
753
|
}).catch(() => { });
|
|
763
754
|
}
|
|
755
|
+
// Mirrors one ambient net-token ledger entry to the API (Phase 4 v2: the
|
|
756
|
+
// org-wide dashboard aggregates these per team). Best-effort fire-and-forget —
|
|
757
|
+
// the local JSONL ledger stays authoritative; this never throws and never
|
|
758
|
+
// blocks a hook. Returns the fetch promise so short-lived CLI paths can flush
|
|
759
|
+
// (bounded) before process.exit kills the socket.
|
|
760
|
+
function reportAmbientLedgerEvent(instanceId, entry) {
|
|
761
|
+
if (!instanceId || !JWT || process.env.CACHLY_NO_TELEMETRY === '1')
|
|
762
|
+
return Promise.resolve();
|
|
763
|
+
return fetch(`${API_URL}/api/v1/instances/${instanceId}/ambient-events`, {
|
|
764
|
+
method: 'POST',
|
|
765
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${JWT}` },
|
|
766
|
+
body: JSON.stringify({ events: [entry] }),
|
|
767
|
+
signal: AbortSignal.timeout(3000),
|
|
768
|
+
}).then(() => undefined, () => undefined);
|
|
769
|
+
}
|
|
764
770
|
async function sendAnonymousTelemetry(toolName) {
|
|
765
771
|
if (_telemetryPingSent)
|
|
766
772
|
return;
|
|
@@ -882,7 +888,7 @@ async function handleTool(name, args) {
|
|
|
882
888
|
'',
|
|
883
889
|
'After sign-in: call **any tool again** — your Brain activates instantly.',
|
|
884
890
|
'',
|
|
885
|
-
'✨ Free forever · No credit card ·
|
|
891
|
+
'✨ Free forever · No credit card · 126 MCP tools · GDPR · EU servers',
|
|
886
892
|
].join('\n');
|
|
887
893
|
}
|
|
888
894
|
// Device flow unavailable (network issue) — fall back to manual setup
|
|
@@ -1071,10 +1077,6 @@ async function handleTool(name, args) {
|
|
|
1071
1077
|
}
|
|
1072
1078
|
return fedbrainResult;
|
|
1073
1079
|
}
|
|
1074
|
-
// Delegate Travel Chaos Organizer tools (requires TCO_API_URL env var)
|
|
1075
|
-
const tcoResult = await handleTcoTool(name, args, JWT ?? '');
|
|
1076
|
-
if (tcoResult !== null)
|
|
1077
|
-
return tcoResult;
|
|
1078
1080
|
switch (name) {
|
|
1079
1081
|
// ── Instance management ──────────────────────────────────────────────
|
|
1080
1082
|
case 'get_api_status': {
|
|
@@ -1195,7 +1197,19 @@ async function handleTool(name, args) {
|
|
|
1195
1197
|
return `⚠️ Unknown tool: ${name}`;
|
|
1196
1198
|
}
|
|
1197
1199
|
}
|
|
1198
|
-
|
|
1200
|
+
// Pushed to every MCP host at connect time (initialize response). This is the
|
|
1201
|
+
// "SessionStart-equivalent" for harnesses without a per-prompt hook — Cursor,
|
|
1202
|
+
// Windsurf, Cline, Copilot agent-mode all surface `instructions`. It's a one-time
|
|
1203
|
+
// nudge (not per-prompt recall), so it stays short and behavior-focused.
|
|
1204
|
+
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.
|
|
1205
|
+
|
|
1206
|
+
- 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.
|
|
1207
|
+
- WHEN debugging an error: call causal_trace with the exact error first — a known fix may already exist.
|
|
1208
|
+
- AFTER a fix, deploy, or discovery: call learn_from_attempts so the knowledge persists.
|
|
1209
|
+
- BEFORE a risky op (deploy, migration, dependency bump): call brain_predict.
|
|
1210
|
+
|
|
1211
|
+
The Brain instance id comes from the CACHLY_BRAIN_INSTANCE_ID environment variable.`;
|
|
1212
|
+
const server = new Server({ name: 'cachly-mcp', version: CURRENT_VERSION }, { capabilities: { tools: {} }, instructions: CACHLY_MCP_INSTRUCTIONS });
|
|
1199
1213
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
1200
1214
|
// ── Auto-session management ───────────────────────────────────────────────────
|
|
1201
1215
|
// Transparently starts a Brain session on the first tool call of a process
|
|
@@ -1484,31 +1498,49 @@ Report predicted failure risks to the user before proceeding.
|
|
|
1484
1498
|
Never commit code that does not compile. Run \`tsc --noEmit\` / \`go build ./...\` before every commit.
|
|
1485
1499
|
${CLAUDE_MD_MARKER_END}`;
|
|
1486
1500
|
}
|
|
1501
|
+
// Cursor loads rules from .cursor/rules/*.mdc with YAML frontmatter. alwaysApply
|
|
1502
|
+
// makes the block active in every request (the closest Cursor gets to a
|
|
1503
|
+
// SessionStart briefing). The frontmatter sits OUTSIDE the brain markers so
|
|
1504
|
+
// idempotent marker-replacement leaves it intact.
|
|
1505
|
+
const CURSOR_MDC_FRONTMATTER = '---\n' +
|
|
1506
|
+
'description: Cachly AI Brain — persistent memory protocol (recall before tasks, learn after fixes)\n' +
|
|
1507
|
+
'alwaysApply: true\n' +
|
|
1508
|
+
'---\n\n';
|
|
1487
1509
|
/**
|
|
1488
|
-
* Schreibt das Brain-Protokoll in alle relevanten Instruction-Dateien:
|
|
1489
|
-
* - CLAUDE.md
|
|
1490
|
-
* - AGENTS.md
|
|
1491
|
-
* - .github/copilot-instructions.md
|
|
1492
|
-
*
|
|
1510
|
+
* Schreibt das Brain-Protokoll in alle relevanten Instruction-/Rules-Dateien:
|
|
1511
|
+
* - CLAUDE.md (Claude Code)
|
|
1512
|
+
* - AGENTS.md (Codex / generic agents)
|
|
1513
|
+
* - .github/copilot-instructions.md (GitHub Copilot)
|
|
1514
|
+
* - .cursor/rules/cachly.mdc (Cursor — frontmatter + alwaysApply)
|
|
1515
|
+
* - .windsurfrules (Windsurf / Cascade)
|
|
1516
|
+
* - .clinerules (Cline)
|
|
1517
|
+
* Idempotent, Marker-basiert. Cross-Harness Tier A (siehe make_cachly_great_again.md §6.7).
|
|
1493
1518
|
*/
|
|
1494
1519
|
export async function writeInstructions(projectDir, instanceId) {
|
|
1495
1520
|
const { writeFile, appendFile, readFile, mkdir } = await import('node:fs/promises');
|
|
1496
1521
|
const { existsSync } = await import('node:fs');
|
|
1497
1522
|
const { resolve, dirname } = await import('node:path');
|
|
1523
|
+
// `prefix` is written only when the file is created fresh (Cursor .mdc needs it);
|
|
1524
|
+
// on marker-update/append the prefix is left untouched.
|
|
1498
1525
|
const files = [
|
|
1499
|
-
resolve(projectDir, 'CLAUDE.md'),
|
|
1500
|
-
resolve(projectDir, 'AGENTS.md'),
|
|
1501
|
-
resolve(projectDir, '.github', 'copilot-instructions.md'),
|
|
1526
|
+
{ path: resolve(projectDir, 'CLAUDE.md') },
|
|
1527
|
+
{ path: resolve(projectDir, 'AGENTS.md') },
|
|
1528
|
+
{ path: resolve(projectDir, '.github', 'copilot-instructions.md') },
|
|
1529
|
+
{ path: resolve(projectDir, '.cursor', 'rules', 'cachly.mdc'), prefix: CURSOR_MDC_FRONTMATTER },
|
|
1530
|
+
{ path: resolve(projectDir, '.windsurfrules') },
|
|
1531
|
+
{ path: resolve(projectDir, '.clinerules') },
|
|
1502
1532
|
];
|
|
1503
1533
|
const block = '\n' + buildClaudeMdBlock(instanceId) + '\n';
|
|
1504
1534
|
const results = {};
|
|
1505
|
-
for (const file of files) {
|
|
1535
|
+
for (const { path: file, prefix } of files) {
|
|
1506
1536
|
await mkdir(dirname(file), { recursive: true });
|
|
1507
1537
|
if (existsSync(file)) {
|
|
1508
1538
|
const existing = await readFile(file, 'utf-8');
|
|
1509
1539
|
if (existing.includes(CLAUDE_MD_MARKER_START)) {
|
|
1510
|
-
// Idempotent update: replace existing block
|
|
1511
|
-
|
|
1540
|
+
// Idempotent update: replace existing block ([\s\S] must keep its
|
|
1541
|
+
// backslashes so it matches any char — a bare [\s\S] in the template
|
|
1542
|
+
// literal collapses to [sS] and the replace silently no-ops).
|
|
1543
|
+
const updated = existing.replace(new RegExp(`${CLAUDE_MD_MARKER_START}[\\s\\S]*?${CLAUDE_MD_MARKER_END}`), buildClaudeMdBlock(instanceId));
|
|
1512
1544
|
await writeFile(file, updated, 'utf-8');
|
|
1513
1545
|
results[file] = 'updated';
|
|
1514
1546
|
continue;
|
|
@@ -1517,7 +1549,7 @@ export async function writeInstructions(projectDir, instanceId) {
|
|
|
1517
1549
|
results[file] = 'appended';
|
|
1518
1550
|
continue;
|
|
1519
1551
|
}
|
|
1520
|
-
await writeFile(file, block.trimStart(), 'utf-8');
|
|
1552
|
+
await writeFile(file, (prefix ?? '') + block.trimStart(), 'utf-8');
|
|
1521
1553
|
results[file] = 'written';
|
|
1522
1554
|
}
|
|
1523
1555
|
return results;
|
|
@@ -2341,7 +2373,7 @@ if (!process.argv[2] && process.stdout.isTTY) {
|
|
|
2341
2373
|
console.log(' \x1b[36m npx @cachly-dev/mcp-server@latest upgrade\x1b[0m ← Check for updates');
|
|
2342
2374
|
console.log('');
|
|
2343
2375
|
console.log(' \x1b[90mWorks with: Claude Code · Cursor · Windsurf · GitHub Copilot · Cline · Zed\x1b[0m');
|
|
2344
|
-
console.log(' \x1b[90mFree forever · GDPR · German servers ·
|
|
2376
|
+
console.log(' \x1b[90mFree forever · GDPR · German servers · 126 MCP tools\x1b[0m');
|
|
2345
2377
|
console.log('');
|
|
2346
2378
|
process.exit(0);
|
|
2347
2379
|
}
|
|
@@ -2425,6 +2457,22 @@ if (process.argv[2] === 'init') {
|
|
|
2425
2457
|
console.log(`✓ CLS hook already current in .git/hooks/post-commit`);
|
|
2426
2458
|
}
|
|
2427
2459
|
catch { /* non-critical — git hook is a best-effort feature */ }
|
|
2460
|
+
// ── Ambient Recall Phase 4: auto-install SessionStart + UserPromptSubmit hooks ─
|
|
2461
|
+
// Only for Claude Code (the hooks config lives in .claude/settings.json).
|
|
2462
|
+
if (editor === 'claude') {
|
|
2463
|
+
try {
|
|
2464
|
+
const a = await installAmbientHooks(projectDir, instanceId, JWT || undefined);
|
|
2465
|
+
const scriptVerb = a.scripts === 'written' ? '✅ Written' : a.scripts === 'upgraded' ? `✅ Upgraded (→ ${AMBIENT_HOOK_VERSION})` : '✓ Unchanged';
|
|
2466
|
+
console.log(`${scriptVerb}: .claude/hooks/ (Ambient Recall — session briefing, per-prompt recall, file briefing, auto-learn)`);
|
|
2467
|
+
if (a.settings === 'written')
|
|
2468
|
+
console.log(`✅ Written: .claude/settings.json (Ambient Recall hooks wired)`);
|
|
2469
|
+
else if (a.settings === 'merged')
|
|
2470
|
+
console.log(`✅ Merged: .claude/settings.json (Ambient Recall hooks wired)`);
|
|
2471
|
+
else
|
|
2472
|
+
console.log(`✓ Ambient Recall hooks already wired in .claude/settings.json`);
|
|
2473
|
+
}
|
|
2474
|
+
catch { /* non-critical — ambient hooks are a best-effort feature */ }
|
|
2475
|
+
}
|
|
2428
2476
|
const initSecs = ((Date.now() - initStart) / 1000).toFixed(1);
|
|
2429
2477
|
console.log(`\n🧠 Cachly AI Brain configured for ${editor === 'claude' ? 'Claude Code' : editor}! (${initSecs}s)`);
|
|
2430
2478
|
console.log(` Restart your editor — the \`cachly\` MCP tools will appear.`);
|
|
@@ -3412,6 +3460,138 @@ if (process.argv[2] === 'learn-git') {
|
|
|
3412
3460
|
}
|
|
3413
3461
|
process.exit(0);
|
|
3414
3462
|
}
|
|
3463
|
+
// ── ambient-recall: CLI entrypoint for the Claude Code hooks ──────────────────
|
|
3464
|
+
// Invoked as: <hook payload JSON on stdin> | cachly ambient-recall
|
|
3465
|
+
// SessionStart/UserPromptSubmit/PreToolUse payloads recall through the §6.3
|
|
3466
|
+
// relevance gate (ambient-cli.ts) and print the `hookSpecificOutput` JSON that
|
|
3467
|
+
// Claude Code injects as additionalContext — or nothing. Stop payloads instead
|
|
3468
|
+
// feed a fix-signal observation to auto_learn_session (the automatic
|
|
3469
|
+
// `learn_from_attempts`). Injections are booked into the net-token ledger
|
|
3470
|
+
// (§6.2) and auto-backoff kicks in when the recent window is net-negative.
|
|
3471
|
+
// Best-effort: no JWT, no stdin, a slow brain or any error → prints nothing and
|
|
3472
|
+
// exits 0 so a hook can NEVER block or corrupt the agent's turn.
|
|
3473
|
+
if (process.argv[2] === 'ambient-recall') {
|
|
3474
|
+
try {
|
|
3475
|
+
if (process.stdin.isTTY)
|
|
3476
|
+
process.exit(0); // no piped payload → nothing to do
|
|
3477
|
+
const chunks = [];
|
|
3478
|
+
for await (const chunk of process.stdin)
|
|
3479
|
+
chunks.push(chunk);
|
|
3480
|
+
const raw = Buffer.concat(chunks).toString('utf-8');
|
|
3481
|
+
if (!JWT)
|
|
3482
|
+
process.exit(0); // not authenticated → silent no-op
|
|
3483
|
+
const instanceId = process.env.CACHLY_BRAIN_INSTANCE_ID ?? _defaultInstanceId;
|
|
3484
|
+
// Stop event → auto-learn, never inject (roadmap §6.1 PostToolUse/Stop row).
|
|
3485
|
+
const stopPayload = parseHookPayload(raw);
|
|
3486
|
+
if (stopPayload?.hook_event_name === 'Stop') {
|
|
3487
|
+
const obs = stopObservation(stopPayload);
|
|
3488
|
+
if (obs && instanceId) {
|
|
3489
|
+
await handleTool('auto_learn_session', { instance_id: instanceId, observations: [obs] });
|
|
3490
|
+
}
|
|
3491
|
+
process.exit(0);
|
|
3492
|
+
}
|
|
3493
|
+
let reported;
|
|
3494
|
+
const out = await runAmbient(raw, {
|
|
3495
|
+
timeoutMs: 3000, // §6.3 guardrail 4: never stall the prompt on a slow brain
|
|
3496
|
+
// Ambient injection budget: one relevance-ranked briefing, hard-capped.
|
|
3497
|
+
// smart_recall already returns its hits best-first, so the capped head is
|
|
3498
|
+
// the high-signal part. Per-lesson structured candidates are a future slice.
|
|
3499
|
+
gate: { topK: 1, maxTokens: 500 },
|
|
3500
|
+
backoff: async () => shouldBackoff(await readLedger()),
|
|
3501
|
+
onInject: (tokens, event) => {
|
|
3502
|
+
const entry = { ts: new Date().toISOString(), event, injected: tokens, prevented: 0 };
|
|
3503
|
+
void appendLedgerEntry(entry);
|
|
3504
|
+
reported = reportAmbientLedgerEvent(instanceId, entry); // org dashboard mirror
|
|
3505
|
+
},
|
|
3506
|
+
recall: async (query) => {
|
|
3507
|
+
if (!instanceId)
|
|
3508
|
+
return [];
|
|
3509
|
+
const text = String((await handleTool('smart_recall', { instance_id: instanceId, query })) ?? '');
|
|
3510
|
+
const miss = text.length < 80 || /No lessons found|no lessons|No matches found/i.test(text);
|
|
3511
|
+
if (miss)
|
|
3512
|
+
return [];
|
|
3513
|
+
return [{ id: 'ambient', summary: truncateToTokens(text, 500), confidence: 0.9, score: 0.9 }];
|
|
3514
|
+
},
|
|
3515
|
+
});
|
|
3516
|
+
if (out)
|
|
3517
|
+
process.stdout.write(out);
|
|
3518
|
+
// Bounded flush so the dashboard mirror survives process.exit — never more
|
|
3519
|
+
// than 500ms on top of a turn that already injected.
|
|
3520
|
+
if (reported)
|
|
3521
|
+
await Promise.race([reported, new Promise((r) => setTimeout(r, 500))]);
|
|
3522
|
+
}
|
|
3523
|
+
catch {
|
|
3524
|
+
// Swallow everything — an ambient hook must never break the agent.
|
|
3525
|
+
}
|
|
3526
|
+
process.exit(0);
|
|
3527
|
+
}
|
|
3528
|
+
// ── ambient-credit: agent-reported prevented-token credit (§6.2) ──────────────
|
|
3529
|
+
// Invoked as: cachly ambient-credit <tokens> [note…] — the injected context's
|
|
3530
|
+
// footer invites the agent to call this when a recalled lesson changed its path.
|
|
3531
|
+
// This is the client-side signal that makes the net ledger (and auto-backoff)
|
|
3532
|
+
// meaningful before the server-side dashboard exists. Silent + exit 0 always.
|
|
3533
|
+
if (process.argv[2] === 'ambient-credit') {
|
|
3534
|
+
try {
|
|
3535
|
+
const tokens = Math.round(Number(process.argv[3]));
|
|
3536
|
+
if (Number.isFinite(tokens) && tokens > 0) {
|
|
3537
|
+
// Cap a single credit: self-reported savings should never let one
|
|
3538
|
+
// enthusiastic claim mask weeks of negative balance.
|
|
3539
|
+
const capped = Math.min(tokens, 20_000);
|
|
3540
|
+
const note = process.argv.slice(4).join(' ').slice(0, 200) || undefined;
|
|
3541
|
+
const entry = { ts: new Date().toISOString(), event: 'credit', injected: 0, prevented: capped, note };
|
|
3542
|
+
await appendLedgerEntry(entry);
|
|
3543
|
+
await reportAmbientLedgerEvent(process.env.CACHLY_BRAIN_INSTANCE_ID ?? _defaultInstanceId, entry); // org dashboard mirror
|
|
3544
|
+
console.log(`✅ ambient credit recorded: ${capped} tokens${note ? ` (${note})` : ''}`);
|
|
3545
|
+
}
|
|
3546
|
+
else {
|
|
3547
|
+
console.error('Usage: npx @cachly-dev/mcp-server@latest ambient-credit <tokens-saved> [note]');
|
|
3548
|
+
}
|
|
3549
|
+
}
|
|
3550
|
+
catch { /* telemetry — never fail loudly */ }
|
|
3551
|
+
process.exit(0);
|
|
3552
|
+
}
|
|
3553
|
+
// ── ambient-stats: the honest net-token readout (§6.2) ────────────────────────
|
|
3554
|
+
// Shows injected vs prevented and the NET — even when it is negative — plus
|
|
3555
|
+
// whether auto-backoff is currently pausing injection.
|
|
3556
|
+
if (process.argv[2] === 'ambient-stats') {
|
|
3557
|
+
const entries = await readLedger();
|
|
3558
|
+
const bal = netBalance(entries);
|
|
3559
|
+
const backing = shouldBackoff(entries);
|
|
3560
|
+
const recent = entries.slice(-5);
|
|
3561
|
+
console.log('\n🧠 Ambient Recall — net-token ledger\n');
|
|
3562
|
+
console.log(` Turns recorded: ${entries.length}`);
|
|
3563
|
+
console.log(` Injected tokens: ${bal.injected}`);
|
|
3564
|
+
console.log(` Prevented tokens: ${bal.prevented} (agent-reported via ambient-credit)`);
|
|
3565
|
+
console.log(` NET: ${bal.net >= 0 ? '+' : ''}${bal.net} tokens`);
|
|
3566
|
+
console.log(` Auto-backoff: ${backing ? '🔴 ACTIVE — recent window is net-negative, injection paused' : '🟢 inactive'}`);
|
|
3567
|
+
if (recent.length > 0) {
|
|
3568
|
+
console.log('\n Last entries:');
|
|
3569
|
+
for (const e of recent) {
|
|
3570
|
+
const what = e.prevented > 0 ? `+${e.prevented} prevented` : `-${e.injected} injected`;
|
|
3571
|
+
console.log(` • ${e.ts.slice(0, 19)} ${e.event}: ${what}${e.note ? ` (${e.note})` : ''}`);
|
|
3572
|
+
}
|
|
3573
|
+
}
|
|
3574
|
+
console.log('');
|
|
3575
|
+
process.exit(0);
|
|
3576
|
+
}
|
|
3577
|
+
// ── doctor: one-command setup diagnosis ───────────────────────────────────────
|
|
3578
|
+
// Walks the whole activation chain — runtime → credential → API → auth →
|
|
3579
|
+
// instance → hooks → ledger — and prints one ✅/⚠️/❌ report with a concrete
|
|
3580
|
+
// next step per finding. Exit 1 only on hard failures (warnings stay 0).
|
|
3581
|
+
if (process.argv[2] === 'doctor') {
|
|
3582
|
+
const { checkNodeVersion, checkCredential, checkApiReachable, checkAuthAccepted, checkInstance, inspectAmbientHooks, checkHooks, checkLedger, renderDoctorReport, doctorExitCode, } = await import('./doctor.js');
|
|
3583
|
+
const checks = [
|
|
3584
|
+
checkNodeVersion(),
|
|
3585
|
+
checkCredential(JWT),
|
|
3586
|
+
await checkApiReachable(API_URL),
|
|
3587
|
+
await checkAuthAccepted(API_URL, JWT),
|
|
3588
|
+
checkInstance(process.env.CACHLY_BRAIN_INSTANCE_ID ?? _defaultInstanceId),
|
|
3589
|
+
checkHooks(inspectAmbientHooks(process.cwd())),
|
|
3590
|
+
checkLedger(await readLedger(), defaultLedgerPath()),
|
|
3591
|
+
];
|
|
3592
|
+
console.log(renderDoctorReport(checks));
|
|
3593
|
+
process.exit(doctorExitCode(checks));
|
|
3594
|
+
}
|
|
3415
3595
|
// ── cls-ingest: CLI entrypoint for the git post-commit hook ───────────────────
|
|
3416
3596
|
// Invoked as: cachly cls-ingest '<json>' where json = { instance_id, source, payload }.
|
|
3417
3597
|
// Auth comes from CACHLY_JWT (embedded in the generated hook). This is a silent,
|
|
@@ -3485,7 +3665,7 @@ if (!JWT) {
|
|
|
3485
3665
|
// • Editor as an MCP stdio server (non-TTY) → ONE stderr hint, then KEEP RUNNING
|
|
3486
3666
|
// so tools/list works and the first tool call starts the browser sign-in.
|
|
3487
3667
|
// Skip entirely for CLI subcommands that intentionally run without credentials.
|
|
3488
|
-
const _cliNoAuthCommands = ['demo', 'share', 'publish', 'health', 'autosetup', 'setup', 'autopilot', 'init', 'digest', 'invite', 'badge', 'join', 'upgrade', 'bench', 'tool-specs', 'openapi'];
|
|
3668
|
+
const _cliNoAuthCommands = ['demo', 'share', 'publish', 'health', 'autosetup', 'setup', 'autopilot', 'init', 'digest', 'invite', 'badge', 'join', 'upgrade', 'bench', 'tool-specs', 'openapi', 'ambient-credit', 'ambient-stats', 'doctor'];
|
|
3489
3669
|
if (!JWT && !_cliNoAuthCommands.includes(process.argv[2] ?? '')) {
|
|
3490
3670
|
const runningInTerminal = !process.argv[2] && process.stdout.isTTY === true && _isMain;
|
|
3491
3671
|
if (runningInTerminal) {
|