@cachly-dev/mcp-server 0.10.118 → 0.10.120
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 +7 -3
- 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/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.js +8 -1
- package/dist/src/handlers/brain.js.map +1 -1
- package/dist/src/index.d.ts +19 -0
- package/dist/src/index.js +104 -15
- package/dist/src/index.js.map +1 -1
- package/glama.json +1 -1
- package/package.json +1 -1
- package/server.json +2 -2
package/dist/src/index.d.ts
CHANGED
|
@@ -1,4 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
interface DeviceFlowState {
|
|
3
|
+
deviceCode: string;
|
|
4
|
+
userCode: string;
|
|
5
|
+
verifyUrl: string;
|
|
6
|
+
pollInterval: number;
|
|
7
|
+
deadline: number;
|
|
8
|
+
polling: boolean;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Once the device code is issued, poll in the BACKGROUND on the flow's interval
|
|
12
|
+
* so browser sign-in self-completes. Without this, the flow only advanced when
|
|
13
|
+
* the user manually triggered ANOTHER tool call — the #1 onboarding drop-off
|
|
14
|
+
* (report: device flow 36→1 = 3%). Now the user signs in and their Brain
|
|
15
|
+
* activates on its own; their next natural request just works instead of
|
|
16
|
+
* showing "still waiting". Bounded by the flow deadline, timer unref'd so it
|
|
17
|
+
* never keeps the process alive, fully fail-safe (never throws). `poll` and
|
|
18
|
+
* `isCurrent` are injectable for testing.
|
|
19
|
+
*/
|
|
20
|
+
export declare function startBackgroundDevicePoll(flow: DeviceFlowState, poll?: (f: DeviceFlowState) => Promise<'pending' | 'expired' | 'done'>, isCurrent?: (f: DeviceFlowState) => boolean, schedule?: (fn: () => void, ms: number) => void): void;
|
|
2
21
|
import { tokenize, splitMultiQuery, levenshtein, recencyBoost, extractTimestamp, STOPWORDS, katakanaToRomaji, arabicLightStem, expandCrossLingual, CROSS_LINGUAL_MAP } from './search.js';
|
|
3
22
|
export { tokenize, splitMultiQuery, levenshtein, recencyBoost, extractTimestamp, STOPWORDS, katakanaToRomaji, arabicLightStem, expandCrossLingual, CROSS_LINGUAL_MAP };
|
|
4
23
|
export declare const EDITOR_FILES: Record<string, string>;
|
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) ─────────────────────────────────
|
|
@@ -622,7 +676,14 @@ async function getConnection(instance_id) {
|
|
|
622
676
|
connectTimeout: 8000,
|
|
623
677
|
retryStrategy: () => null, // fail fast, no reconnect loops in MCP context
|
|
624
678
|
});
|
|
625
|
-
|
|
679
|
+
// Drop the client from the pool AND close its socket on error/end, so a
|
|
680
|
+
// mid-session Redis drop doesn't leak sockets across reconnects. disconnect()
|
|
681
|
+
// is idempotent and safe to call from these handlers. The next tool call
|
|
682
|
+
// rebuilds the connection (self-healing).
|
|
683
|
+
const evict = () => {
|
|
684
|
+
pool.delete(instance_id);
|
|
685
|
+
client.disconnect();
|
|
686
|
+
};
|
|
626
687
|
client.on('error', evict);
|
|
627
688
|
client.on('end', evict);
|
|
628
689
|
try {
|
|
@@ -697,6 +758,8 @@ import { installClsPostCommitHook, CLS_HOOK_VERSION } from './cls-hook.js';
|
|
|
697
758
|
import { installAmbientHooks, AMBIENT_HOOK_VERSION } from './ambient-hooks.js';
|
|
698
759
|
import { runAmbient, truncateToTokens, parseHookPayload, stopObservation } from './ambient-cli.js';
|
|
699
760
|
import { appendLedgerEntry, readLedger, defaultLedgerPath } from './ambient-ledger.js';
|
|
761
|
+
import { detectEditor as detectEditorImpl } from './editor.js';
|
|
762
|
+
import { milestoneSent, markMilestoneSent } from './funnel-milestones.js';
|
|
700
763
|
import { netBalance, shouldBackoff } from './ambient-recall.js';
|
|
701
764
|
import { handleShareTool } from './handlers/share.js';
|
|
702
765
|
import { handleVizTool } from './handlers/viz.js';
|
|
@@ -707,12 +770,10 @@ import { TOOLS } from './tools.js';
|
|
|
707
770
|
let _telemetryPingSent = false;
|
|
708
771
|
// Fires once per process on the first successful Brain tool call — key activation metric
|
|
709
772
|
let _firstCallSuccessSent = false;
|
|
773
|
+
// Host detection lives in editor.ts (pure + unit-tested). Kept as a thin
|
|
774
|
+
// wrapper so the many call sites here stay unchanged.
|
|
710
775
|
function detectEditor() {
|
|
711
|
-
return
|
|
712
|
-
: process.env.WINDSURF_SESSION_ID ? 'windsurf'
|
|
713
|
-
: process.env.GITHUB_COPILOT_WORKSPACE ? 'copilot'
|
|
714
|
-
: process.env.CLAUDE_CODE_ENTRYPOINT ? 'claude'
|
|
715
|
-
: 'unknown';
|
|
776
|
+
return detectEditorImpl();
|
|
716
777
|
}
|
|
717
778
|
/** Derive an anonymous, non-reversible fingerprint from the JWT sub claim. */
|
|
718
779
|
function _jwtUserFingerprint(jwt) {
|
|
@@ -852,7 +913,7 @@ async function handleTool(name, args) {
|
|
|
852
913
|
// 1st call: start device flow, return code + URL
|
|
853
914
|
// 2nd+ calls: poll for token; once authenticated, proceed transparently
|
|
854
915
|
if (_deviceFlow) {
|
|
855
|
-
const result = await
|
|
916
|
+
const result = await pollDeviceFlowGuarded(_deviceFlow);
|
|
856
917
|
if (result === 'done') {
|
|
857
918
|
// Auth complete — re-enter handleTool with now-valid JWT
|
|
858
919
|
return handleTool(name, args);
|
|
@@ -862,14 +923,14 @@ async function handleTool(name, args) {
|
|
|
862
923
|
_deviceFlow = null;
|
|
863
924
|
return '⌛ **Authentication timed out.** Please call any tool again to restart the sign-in flow.';
|
|
864
925
|
}
|
|
865
|
-
// Still pending
|
|
926
|
+
// Still pending — the background poller will complete sign-in on its own.
|
|
866
927
|
return [
|
|
867
|
-
'⏳ **
|
|
928
|
+
'⏳ **Signing you in…**',
|
|
868
929
|
'',
|
|
869
|
-
`
|
|
870
|
-
`
|
|
930
|
+
`Finish in your browser: **${_deviceFlow.verifyUrl}**`,
|
|
931
|
+
`Code: **${_deviceFlow.userCode}**`,
|
|
871
932
|
'',
|
|
872
|
-
'
|
|
933
|
+
'This completes automatically once you sign in — no need to come back here.',
|
|
873
934
|
].join('\n');
|
|
874
935
|
}
|
|
875
936
|
// No pending flow — start a new one
|
|
@@ -879,6 +940,9 @@ async function handleTool(name, args) {
|
|
|
879
940
|
sendFunnelEvent('device_flow_started', { tool: name });
|
|
880
941
|
// Try to open the browser automatically — fire-and-forget, never block
|
|
881
942
|
openInBrowser(flow.verifyUrl);
|
|
943
|
+
// Poll in the background so sign-in self-completes — the user no longer
|
|
944
|
+
// has to trigger another tool call to drive the flow (the #1 drop-off).
|
|
945
|
+
startBackgroundDevicePoll(flow);
|
|
882
946
|
return [
|
|
883
947
|
'🧠 **cachly AI Brain — sign in to activate** (browser opening...)',
|
|
884
948
|
'',
|
|
@@ -886,7 +950,8 @@ async function handleTool(name, args) {
|
|
|
886
950
|
'',
|
|
887
951
|
`Code: **${flow.userCode}** (pre-filled if browser opened automatically)`,
|
|
888
952
|
'',
|
|
889
|
-
'
|
|
953
|
+
'That’s it — your Brain activates automatically the moment you finish',
|
|
954
|
+
'signing in. Just keep working; your next request arrives brain-powered.',
|
|
890
955
|
'',
|
|
891
956
|
'✨ Free forever · No credit card · 126 MCP tools · GDPR · EU servers',
|
|
892
957
|
].join('\n');
|
|
@@ -923,7 +988,12 @@ async function handleTool(name, args) {
|
|
|
923
988
|
if (brainResult !== null) {
|
|
924
989
|
if (!_firstCallSuccessSent && JWT) {
|
|
925
990
|
_firstCallSuccessSent = true;
|
|
926
|
-
|
|
991
|
+
// Persistent guard: ambient hooks spawn a fresh process per prompt, so the
|
|
992
|
+
// in-memory flag alone re-sent "first call" on every hook invocation.
|
|
993
|
+
if (!milestoneSent('first_call_success')) {
|
|
994
|
+
markMilestoneSent('first_call_success');
|
|
995
|
+
sendFunnelEvent('first_call_success', { tool: name, instance_id: args.instance_id ?? _defaultInstanceId ?? '' });
|
|
996
|
+
}
|
|
927
997
|
}
|
|
928
998
|
// Per-tool telemetry — fires after every successful brain tool call.
|
|
929
999
|
// api_key carries the cky_live_... token so the backend can resolve tenant + increment counters.
|
|
@@ -1026,7 +1096,12 @@ async function handleTool(name, args) {
|
|
|
1026
1096
|
if (contextResult !== null) {
|
|
1027
1097
|
if (!_firstCallSuccessSent && JWT) {
|
|
1028
1098
|
_firstCallSuccessSent = true;
|
|
1029
|
-
|
|
1099
|
+
// Persistent guard: ambient hooks spawn a fresh process per prompt, so the
|
|
1100
|
+
// in-memory flag alone re-sent "first call" on every hook invocation.
|
|
1101
|
+
if (!milestoneSent('first_call_success')) {
|
|
1102
|
+
markMilestoneSent('first_call_success');
|
|
1103
|
+
sendFunnelEvent('first_call_success', { tool: name, instance_id: args.instance_id ?? _defaultInstanceId ?? '' });
|
|
1104
|
+
}
|
|
1030
1105
|
}
|
|
1031
1106
|
return contextResult;
|
|
1032
1107
|
}
|
|
@@ -1332,6 +1407,20 @@ process.on('unhandledRejection', (reason) => {
|
|
|
1332
1407
|
// Write to stderr only (stdout is the JSON-RPC channel)
|
|
1333
1408
|
process.stderr.write(`[cachly-mcp] unhandledRejection: ${msg}\n`);
|
|
1334
1409
|
});
|
|
1410
|
+
// Symmetric net for synchronous/async *exceptions* thrown outside a try/catch
|
|
1411
|
+
// (a timer or stream callback, the background device poller, ioredis internals).
|
|
1412
|
+
// Without this the process exits hard and the editor reports "Connection is
|
|
1413
|
+
// closed" mid-session. Log to stderr and stay alive — never exit here.
|
|
1414
|
+
process.on('uncaughtException', (err) => {
|
|
1415
|
+
process.stderr.write(`[cachly-mcp] uncaughtException: ${err?.stack ?? String(err)}\n`);
|
|
1416
|
+
});
|
|
1417
|
+
// The single most common stdio-MCP killer: the editor closes the read side of
|
|
1418
|
+
// our stdout pipe (window closed, server restart, client reconnect) and the
|
|
1419
|
+
// next JSON-RPC write throws EPIPE — an async stream error that would otherwise
|
|
1420
|
+
// crash the process. Swallow stream errors on both stdio ends so a client that
|
|
1421
|
+
// closes its pipe can never take the server down with it.
|
|
1422
|
+
process.stdout.on('error', () => { });
|
|
1423
|
+
process.stdin.on('error', () => { });
|
|
1335
1424
|
// ── CLI helpers ───────────────────────────────────────────────────────────────
|
|
1336
1425
|
export const EDITOR_FILES = {
|
|
1337
1426
|
claude: '.mcp.json',
|