@cachly-dev/mcp-server 0.10.118 → 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.
@@ -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) ─────────────────────────────────
@@ -697,6 +751,8 @@ import { installClsPostCommitHook, CLS_HOOK_VERSION } from './cls-hook.js';
697
751
  import { installAmbientHooks, AMBIENT_HOOK_VERSION } from './ambient-hooks.js';
698
752
  import { runAmbient, truncateToTokens, parseHookPayload, stopObservation } from './ambient-cli.js';
699
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';
700
756
  import { netBalance, shouldBackoff } from './ambient-recall.js';
701
757
  import { handleShareTool } from './handlers/share.js';
702
758
  import { handleVizTool } from './handlers/viz.js';
@@ -707,12 +763,10 @@ import { TOOLS } from './tools.js';
707
763
  let _telemetryPingSent = false;
708
764
  // Fires once per process on the first successful Brain tool call — key activation metric
709
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.
710
768
  function detectEditor() {
711
- return process.env.CURSOR_TRACE_ID ? 'cursor'
712
- : process.env.WINDSURF_SESSION_ID ? 'windsurf'
713
- : process.env.GITHUB_COPILOT_WORKSPACE ? 'copilot'
714
- : process.env.CLAUDE_CODE_ENTRYPOINT ? 'claude'
715
- : 'unknown';
769
+ return detectEditorImpl();
716
770
  }
717
771
  /** Derive an anonymous, non-reversible fingerprint from the JWT sub claim. */
718
772
  function _jwtUserFingerprint(jwt) {
@@ -852,7 +906,7 @@ async function handleTool(name, args) {
852
906
  // 1st call: start device flow, return code + URL
853
907
  // 2nd+ calls: poll for token; once authenticated, proceed transparently
854
908
  if (_deviceFlow) {
855
- const result = await pollDeviceFlow(_deviceFlow);
909
+ const result = await pollDeviceFlowGuarded(_deviceFlow);
856
910
  if (result === 'done') {
857
911
  // Auth complete — re-enter handleTool with now-valid JWT
858
912
  return handleTool(name, args);
@@ -862,14 +916,14 @@ async function handleTool(name, args) {
862
916
  _deviceFlow = null;
863
917
  return '⌛ **Authentication timed out.** Please call any tool again to restart the sign-in flow.';
864
918
  }
865
- // Still pending
919
+ // Still pending — the background poller will complete sign-in on its own.
866
920
  return [
867
- '⏳ **Waiting for authentication...**',
921
+ '⏳ **Signing you in…**',
868
922
  '',
869
- `Sign in at: **${_deviceFlow.verifyUrl}**`,
870
- `Enter code: **${_deviceFlow.userCode}**`,
923
+ `Finish in your browser: **${_deviceFlow.verifyUrl}**`,
924
+ `Code: **${_deviceFlow.userCode}**`,
871
925
  '',
872
- 'Once you complete sign-in in your browser, call this tool again and it will proceed automatically.',
926
+ 'This completes automatically once you sign in — no need to come back here.',
873
927
  ].join('\n');
874
928
  }
875
929
  // No pending flow — start a new one
@@ -879,6 +933,9 @@ async function handleTool(name, args) {
879
933
  sendFunnelEvent('device_flow_started', { tool: name });
880
934
  // Try to open the browser automatically — fire-and-forget, never block
881
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);
882
939
  return [
883
940
  '🧠 **cachly AI Brain — sign in to activate** (browser opening...)',
884
941
  '',
@@ -886,7 +943,8 @@ async function handleTool(name, args) {
886
943
  '',
887
944
  `Code: **${flow.userCode}** (pre-filled if browser opened automatically)`,
888
945
  '',
889
- 'After sign-in: call **any tool again** — your Brain activates instantly.',
946
+ 'That’s it — your Brain activates automatically the moment you finish',
947
+ 'signing in. Just keep working; your next request arrives brain-powered.',
890
948
  '',
891
949
  '✨ Free forever · No credit card · 126 MCP tools · GDPR · EU servers',
892
950
  ].join('\n');
@@ -923,7 +981,12 @@ async function handleTool(name, args) {
923
981
  if (brainResult !== null) {
924
982
  if (!_firstCallSuccessSent && JWT) {
925
983
  _firstCallSuccessSent = true;
926
- sendFunnelEvent('first_call_success', { tool: name, instance_id: args.instance_id ?? _defaultInstanceId ?? '' });
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
+ }
927
990
  }
928
991
  // Per-tool telemetry — fires after every successful brain tool call.
929
992
  // api_key carries the cky_live_... token so the backend can resolve tenant + increment counters.
@@ -1026,7 +1089,12 @@ async function handleTool(name, args) {
1026
1089
  if (contextResult !== null) {
1027
1090
  if (!_firstCallSuccessSent && JWT) {
1028
1091
  _firstCallSuccessSent = true;
1029
- sendFunnelEvent('first_call_success', { tool: name, instance_id: args.instance_id ?? _defaultInstanceId ?? '' });
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
+ }
1030
1098
  }
1031
1099
  return contextResult;
1032
1100
  }