@cachly-dev/mcp-server 0.10.83 → 0.10.85

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/dist/src/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { jwtExpiryMs, checkJwt, handleApiError, diagnoseAuth, planAuthHeal } from './auth.js';
2
+ import { jwtExpiryMs, checkJwt, handleApiError, diagnoseAuth, planAuthHeal, readClientCredentialsFromEnv, buildClientCredentialsBody, clientCredentialsTokenUrl } from './auth.js';
3
3
  import { handleTcoTool } from './handlers/tco.js';
4
4
  import { notify } from './notifier.js';
5
5
  import { fileURLToPath } from 'node:url';
@@ -65,7 +65,7 @@ import { Redis } from 'ioredis';
65
65
  let API_URL = process.env.CACHLY_API_URL ?? 'https://api.cachly.dev';
66
66
  let JWT = process.env.CACHLY_JWT ?? '';
67
67
  const _EMBED_MODEL = process.env.CACHLY_EMBED_MODEL ?? '';
68
- const CURRENT_VERSION = '0.10.83';
68
+ const CURRENT_VERSION = '0.10.85';
69
69
  // Max time to wait for a freshly-provisioned instance to become "running" before
70
70
  // giving up. Free-tier provisioning in high-latency regions can take 45–90s, so the
71
71
  // old hard-coded 25s caused spurious "instance_not_reachable" on first run.
@@ -1977,6 +1977,36 @@ if (process.argv[2] === 'publish') {
1977
1977
  }
1978
1978
  process.exit(0);
1979
1979
  }
1980
+ // ── CLI: cachly tool-specs / openapi ───────────────────────────────────────────
1981
+ // Emit the 114-tool surface in any agent framework's dialect, derived from the
1982
+ // single TOOLS source of truth. Lets OpenAI Assistants, the Anthropic Messages
1983
+ // API, LangChain, CrewAI and AutoGen wrap cachly without hand-written glue.
1984
+ // npx @cachly-dev/mcp-server tool-specs --format=openai > cachly.openai.json
1985
+ // npx @cachly-dev/mcp-server openapi > cachly.openapi.json
1986
+ if (process.argv[2] === 'tool-specs' || process.argv[2] === 'openapi') {
1987
+ const { renderToolSpecs } = await import('./toolspecs.js');
1988
+ // Accept both "--format openai" and "--format=openai".
1989
+ let format = 'openapi';
1990
+ if (process.argv[2] !== 'openapi') {
1991
+ const eqArg = process.argv.find(a => a.startsWith('--format='));
1992
+ if (eqArg) {
1993
+ format = eqArg.slice('--format='.length);
1994
+ }
1995
+ else {
1996
+ const fmtFlag = process.argv.indexOf('--format');
1997
+ if (fmtFlag !== -1)
1998
+ format = process.argv[fmtFlag + 1] ?? 'openapi';
1999
+ }
2000
+ }
2001
+ const valid = ['openapi', 'openai', 'anthropic', 'langchain'];
2002
+ if (!valid.includes(format)) {
2003
+ process.stderr.write(`\n❌ Unknown format "${format}". Use one of: ${valid.join(', ')}\n\n`);
2004
+ process.exit(1);
2005
+ }
2006
+ // Exit only after stdout has fully flushed — on a pipe, write() is async and an
2007
+ // immediate process.exit() truncates large payloads (~64KB) mid-flush.
2008
+ process.stdout.write(renderToolSpecs(TOOLS, format, CURRENT_VERSION) + '\n', () => process.exit(0));
2009
+ }
1980
2010
  // ── CLI: cachly badge ─────────────────────────────────────────────────────────
1981
2011
  // Outputs the Markdown + HTML snippet for embedding a live Brain lesson-count
1982
2012
  // badge in any README or website. Badge SVG served by cachly API (public, no auth).
@@ -2026,8 +2056,9 @@ if (!process.argv[2] && process.stdout.isTTY) {
2026
2056
  console.log(' cachly gives it a permanent brain — pre-briefed every session.');
2027
2057
  console.log('');
2028
2058
  console.log(' \x1b[1mCommands:\x1b[0m');
2029
- console.log(' \x1b[32m npx @cachly-dev/mcp-server@latest demo\x1b[0m ← Start here (no account)');
2030
- console.log(' \x1b[32m npx @cachly-dev/mcp-server@latest setup\x1b[0m ← Wire up your AI editors');
2059
+ console.log(' \x1b[1m\x1b[32m npx @cachly-dev/mcp-server@latest autopilot\x1b[0m ← \x1b[1mOne command, fully ready\x1b[0m');
2060
+ console.log(' \x1b[32m npx @cachly-dev/mcp-server@latest demo\x1b[0m ← Try it first (no account)');
2061
+ console.log(' \x1b[36m npx @cachly-dev/mcp-server@latest setup\x1b[0m ← Interactive setup (pick editors)');
2031
2062
  console.log(' \x1b[36m npx @cachly-dev/mcp-server@latest health\x1b[0m ← Check everything works');
2032
2063
  console.log(' \x1b[36m npx @cachly-dev/mcp-server@latest digest\x1b[0m ← Weekly Brain summary');
2033
2064
  console.log(' \x1b[36m npx @cachly-dev/mcp-server@latest share\x1b[0m ← Share your Brain stats');
@@ -2037,7 +2068,7 @@ if (!process.argv[2] && process.stdout.isTTY) {
2037
2068
  console.log(' \x1b[36m npx @cachly-dev/mcp-server@latest upgrade\x1b[0m ← Check for updates');
2038
2069
  console.log('');
2039
2070
  console.log(' \x1b[90mWorks with: Claude Code · Cursor · Windsurf · GitHub Copilot · Cline · Zed\x1b[0m');
2040
- console.log(' \x1b[90mFree forever · GDPR · German servers · 107 MCP tools\x1b[0m');
2071
+ console.log(' \x1b[90mFree forever · GDPR · German servers · 114 MCP tools\x1b[0m');
2041
2072
  console.log('');
2042
2073
  process.exit(0);
2043
2074
  }
@@ -2453,7 +2484,11 @@ if (process.argv[2] === 'health') {
2453
2484
  }
2454
2485
  // ── CLI: cachly setup (interactive — no flags required) ───────────────────────
2455
2486
  // Usage: npx @cachly-dev/mcp-server setup
2456
- if (process.argv[2] === 'setup') {
2487
+ // `autopilot` is the one-command entrypoint: it runs the exact same wizard as
2488
+ // `setup` but fully automatic (no prompts) — auth → instance → all editor
2489
+ // configs → CLAUDE.md → git/starter bootstrap → health, in a single shot.
2490
+ const _isAutopilotCli = process.argv[2] === 'autopilot';
2491
+ if (process.argv[2] === 'setup' || _isAutopilotCli) {
2457
2492
  const { writeFile, mkdir, readFile } = await import('node:fs/promises');
2458
2493
  const { existsSync } = await import('node:fs');
2459
2494
  const { resolve, dirname } = await import('node:path');
@@ -2472,8 +2507,12 @@ if (process.argv[2] === 'setup') {
2472
2507
  // a readline question against a non-TTY stdin never resolves and hangs the
2473
2508
  // wizard forever. Treat that exactly like --yes so we never block.
2474
2509
  const stdinIsInteractive = process.stdin.isTTY === true;
2475
- const nonInteractive = process.argv.includes('--yes') || process.argv.includes('-y') || !stdinIsInteractive;
2476
- if (!stdinIsInteractive && !process.argv.includes('--yes') && !process.argv.includes('-y')) {
2510
+ // `autopilot` is always fully automatic — it IS the one-step onboarding.
2511
+ const nonInteractive = _isAutopilotCli || process.argv.includes('--yes') || process.argv.includes('-y') || !stdinIsInteractive;
2512
+ if (_isAutopilotCli) {
2513
+ console.log('🤖 Autopilot — one-command setup (auth → instance → configs → Brain), zero prompts.\n');
2514
+ }
2515
+ else if (!stdinIsInteractive && !process.argv.includes('--yes') && !process.argv.includes('-y')) {
2477
2516
  console.log('ℹ️ Non-interactive terminal detected — running in automatic mode (no prompts).\n');
2478
2517
  }
2479
2518
  const rl = createInterface({ input: process.stdin, output: process.stdout });
@@ -2842,22 +2881,40 @@ if (process.argv[2] === 'setup') {
2842
2881
  // ── Step 5b: Bootstrap Brain from git history ─────────────────────────────
2843
2882
  // Pre-populates the Brain with real lessons so the first session_start shows
2844
2883
  // actual project knowledge instead of an empty brain.
2884
+ JWT = token; // set global JWT so handleTool can authenticate
2885
+ let gitLessonCount = 0;
2845
2886
  if (existsSync(resolve(cwd, '.git'))) {
2846
2887
  process.stdout.write('⏳ Bootstrapping Brain from git history (~10s)...');
2847
2888
  try {
2848
- JWT = token; // set global JWT so handleTool can authenticate
2849
2889
  const gitResult = await handleTool('brain_from_git', {
2850
2890
  instance_id: instance.id,
2851
2891
  repo_path: cwd,
2852
2892
  limit: 100,
2853
2893
  });
2854
2894
  const match = gitResult.match(/(\d+) lesson/);
2895
+ gitLessonCount = match ? parseInt(match[1], 10) : 0;
2855
2896
  console.log(` ✓ ${match ? match[1] + ' lessons' : 'done'} extracted from git history`);
2856
2897
  }
2857
2898
  catch (e) {
2858
2899
  console.log(` (skipped: ${e.message.slice(0, 80)})`);
2859
2900
  }
2860
2901
  }
2902
+ // ── Step 5c: Seed the starter corpus when git yielded nothing ─────────────
2903
+ // Closes the empty-brain gap: a fresh repo (no .git, shallow clone, or no
2904
+ // fix-shaped commits) would otherwise leave the Brain empty and the very first
2905
+ // smart_recall returning nothing. Seeding 16 curated universal lessons makes
2906
+ // the single setup command produce an immediately-useful Brain — true 1-step
2907
+ // onboarding. Idempotent; never overwrites real lessons.
2908
+ if (gitLessonCount === 0) {
2909
+ process.stdout.write('🌱 Seeding 16 universal engineering lessons (first recall works instantly)...');
2910
+ try {
2911
+ await handleTool('brain_seed_starter', { instance_id: instance.id });
2912
+ console.log(' ✓');
2913
+ }
2914
+ catch (e) {
2915
+ console.log(` (skipped: ${e.message.slice(0, 60)})`);
2916
+ }
2917
+ }
2861
2918
  // ── Step 6: Show Brain health (Aha moment) ────────────────────────────────
2862
2919
  // Fetch brain health from the API to show the user what their agent will see.
2863
2920
  process.stdout.write('\n⏳ Fetching your Brain health preview...');
@@ -3103,6 +3160,40 @@ if (process.argv[2] === 'learn-git') {
3103
3160
  process.exit(0);
3104
3161
  }
3105
3162
  // ── Start ─────────────────────────────────────────────────────────────────────
3163
+ // M2M / headless auth: if no JWT but client credentials are present, run the
3164
+ // OAuth2 client_credentials grant before any credential-missing handling. This
3165
+ // makes cachly fully non-interactive for CI runners, agent orchestrators and
3166
+ // AI-to-AI pipelines — no device flow, no human, no browser.
3167
+ if (!JWT) {
3168
+ const creds = readClientCredentialsFromEnv();
3169
+ if (creds) {
3170
+ try {
3171
+ const res = await fetch(clientCredentialsTokenUrl(creds), {
3172
+ method: 'POST',
3173
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
3174
+ body: buildClientCredentialsBody(creds),
3175
+ signal: AbortSignal.timeout(8000),
3176
+ });
3177
+ if (res.ok) {
3178
+ const tok = await res.json();
3179
+ if (tok.access_token) {
3180
+ JWT = tok.access_token;
3181
+ setEmbedJwt(tok.access_token);
3182
+ sendFunnelEvent('m2m_auth_completed', { grant: 'client_credentials', client_id: creds.clientId });
3183
+ process.stderr.write('🤖 cachly: authenticated via client_credentials (M2M, headless).\n');
3184
+ }
3185
+ }
3186
+ else {
3187
+ process.stderr.write(`⚠️ cachly: client_credentials auth failed (${res.status}). Check CACHLY_CLIENT_ID / CACHLY_CLIENT_SECRET.\n`);
3188
+ sendFunnelEvent('m2m_auth_failed', { status: res.status });
3189
+ }
3190
+ }
3191
+ catch (err) {
3192
+ process.stderr.write(`⚠️ cachly: client_credentials auth error: ${err.message}\n`);
3193
+ sendFunnelEvent('m2m_auth_failed', { reason: 'network' });
3194
+ }
3195
+ }
3196
+ }
3106
3197
  // Credential-missing handling at startup. The critical distinction is HOW we were
3107
3198
  // launched, because writing anything to stdout in stdio-MCP mode corrupts the
3108
3199
  // JSON-RPC stream and exiting kills the zero-credential device-flow onboarding.
@@ -3110,7 +3201,7 @@ if (process.argv[2] === 'learn-git') {
3110
3201
  // • Editor as an MCP stdio server (non-TTY) → ONE stderr hint, then KEEP RUNNING
3111
3202
  // so tools/list works and the first tool call starts the browser sign-in.
3112
3203
  // Skip entirely for CLI subcommands that intentionally run without credentials.
3113
- const _cliNoAuthCommands = ['demo', 'share', 'publish', 'health', 'setup', 'init', 'digest', 'invite', 'badge', 'join', 'upgrade'];
3204
+ const _cliNoAuthCommands = ['demo', 'share', 'publish', 'health', 'setup', 'autopilot', 'init', 'digest', 'invite', 'badge', 'join', 'upgrade', 'tool-specs', 'openapi'];
3114
3205
  if (!JWT && !_cliNoAuthCommands.includes(process.argv[2] ?? '')) {
3115
3206
  const runningInTerminal = !process.argv[2] && process.stdout.isTTY === true && _isMain;
3116
3207
  if (runningInTerminal) {