@betterness/cli 1.2.2 → 1.3.1

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.
Files changed (3) hide show
  1. package/README.md +27 -0
  2. package/dist/index.js +235 -14
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -172,6 +172,30 @@ betterness workflow daily-brief # Full health snapshot
172
172
  betterness workflow next-actions # AI-driven recommendations
173
173
  ```
174
174
 
175
+ ### MCP Integration
176
+
177
+ One-command setup for the [Betterness MCP server](https://www.betterness.ai/mcp) in your AI client:
178
+
179
+ ```bash
180
+ # Install into Claude Desktop, Claude Code, Cursor, or Windsurf
181
+ betterness mcp install claude
182
+ betterness mcp install claude-code
183
+ betterness mcp install claude-code --scope project # project-level .mcp.json
184
+ betterness mcp install cursor
185
+ betterness mcp install windsurf
186
+
187
+ # Preview without writing
188
+ betterness mcp install claude --dry-run
189
+
190
+ # Check status across all clients
191
+ betterness mcp status
192
+
193
+ # Remove
194
+ betterness mcp uninstall claude
195
+ ```
196
+
197
+ The install command reads your stored credentials, merges the MCP config into the client's config file (preserving existing entries), and creates a `.bak` backup.
198
+
175
199
  ### Schema Discovery
176
200
 
177
201
  ```bash
@@ -189,6 +213,7 @@ The CLI is designed to be consumed by AI agents. Key features:
189
213
  - `betterness schema` for runtime capability discovery
190
214
  - `--quiet` mode for exit-code-only checks
191
215
  - `--dry-run` on destructive commands for safe planning
216
+ - `betterness mcp install <client>` for one-command MCP setup
192
217
 
193
218
  ### Skills
194
219
 
@@ -211,6 +236,8 @@ Agent skills teach your AI coding agent the Betterness API surface — endpoints
211
236
  | [betterness-purchases](https://github.com/Betterness/betterness-cli/tree/main/skills/betterness-purchases) | Purchases and Stripe checkout |
212
237
  | [betterness-smart-listings](https://github.com/Betterness/betterness-cli/tree/main/skills/betterness-smart-listings) | Wellness provider search |
213
238
  | [betterness-workflow](https://github.com/Betterness/betterness-cli/tree/main/skills/betterness-workflow) | Composite commands (daily brief, next actions) |
239
+ | [betterness-health-profile](https://github.com/Betterness/betterness-cli/tree/main/skills/betterness-health-profile) | Health profile questionnaire management |
240
+ | [betterness-mcp](https://github.com/Betterness/betterness-cli/tree/main/skills/betterness-mcp) | MCP server integration for AI clients |
214
241
 
215
242
  **Persona skills:**
216
243
 
package/dist/index.js CHANGED
@@ -408,7 +408,7 @@ var ApiClient = class {
408
408
  headers: {
409
409
  "Authorization": `Bearer ${this.apiKey}`,
410
410
  "Content-Type": "application/json",
411
- "User-Agent": `betterness-cli/${"1.2.2"}`,
411
+ "User-Agent": `betterness-cli/${"1.3.1"}`,
412
412
  "Accept": "application/json"
413
413
  },
414
414
  body: body ? JSON.stringify(body) : void 0,
@@ -456,9 +456,9 @@ var ApiClient = class {
456
456
  return this.unwrap(raw, schema);
457
457
  }
458
458
  async upload(path, filePath, fieldName = "file", schema) {
459
- const { readFileSync: readFileSync3 } = await import("fs");
459
+ const { readFileSync: readFileSync4 } = await import("fs");
460
460
  const { basename, extname } = await import("path");
461
- const buffer = readFileSync3(filePath);
461
+ const buffer = readFileSync4(filePath);
462
462
  const fileName = basename(filePath);
463
463
  const ext = extname(filePath).toLowerCase();
464
464
  const mimeType = ext === ".pdf" ? "application/pdf" : "application/octet-stream";
@@ -472,7 +472,7 @@ var ApiClient = class {
472
472
  method: "POST",
473
473
  headers: {
474
474
  "Authorization": `Bearer ${this.apiKey}`,
475
- "User-Agent": `betterness-cli/${"1.2.2"}`,
475
+ "User-Agent": `betterness-cli/${"1.3.1"}`,
476
476
  "Accept": "application/json"
477
477
  },
478
478
  body: formData,
@@ -1661,10 +1661,10 @@ function registerLabResultsCommands(program2) {
1661
1661
  });
1662
1662
  labResults.command("upload").description("Upload a lab result PDF for processing").requiredOption("--file <path>", "Path to the PDF file").action(async (opts, cmd) => {
1663
1663
  try {
1664
- const { existsSync: existsSync4 } = await import("fs");
1664
+ const { existsSync: existsSync5 } = await import("fs");
1665
1665
  const { resolve } = await import("path");
1666
1666
  const filePath = resolve(opts.file);
1667
- if (!existsSync4(filePath)) {
1667
+ if (!existsSync5(filePath)) {
1668
1668
  console.error(`File not found: ${filePath}`);
1669
1669
  process.exit(1);
1670
1670
  }
@@ -2141,7 +2141,7 @@ function registerDebugCommands(program2) {
2141
2141
  const parentOpts = cmd.optsWithGlobals();
2142
2142
  const authSource = parentOpts.apiKey ? "--api-key flag" : envKey ? "BETTERNESS_API_KEY env" : tokens && !isTokenExpired(tokens) ? "OAuth tokens (~/.betterness/tokens.json)" : stored ? "API key (~/.betterness/credentials.json)" : "none";
2143
2143
  const config = {
2144
- version: "1.2.2",
2144
+ version: "1.3.1",
2145
2145
  apiUrl: "https://api.betterness.ai",
2146
2146
  auth0Domain: "betterness.us.auth0.com",
2147
2147
  auth0ClientId: "g4lqYHRQb2QMgdRKIlKwoTJl6eu41pWn",
@@ -2193,7 +2193,7 @@ function registerHealthProfileCommands(program2) {
2193
2193
  hp.command("schema").description("List all sections and question IDs available in the health profile").option("--section <acronym>", "Show questions for a specific section only").action(async (opts, cmd) => {
2194
2194
  try {
2195
2195
  const globalOpts = cmd.optsWithGlobals();
2196
- const client = new ApiClient(globalOpts.apiKey);
2196
+ const client = await createApiClient({ apiKey: globalOpts.apiKey });
2197
2197
  const schema = await client.get(
2198
2198
  "/api/v1/copilot-config/schema",
2199
2199
  void 0,
@@ -2230,7 +2230,7 @@ function registerHealthProfileCommands(program2) {
2230
2230
  hp.command("get").description("Retrieve the full health profile (all answered questions as a flat map)").action(async (_, cmd) => {
2231
2231
  try {
2232
2232
  const globalOpts = cmd.optsWithGlobals();
2233
- const client = new ApiClient(globalOpts.apiKey);
2233
+ const client = await createApiClient({ apiKey: globalOpts.apiKey });
2234
2234
  const user = await client.get(
2235
2235
  "/api/betterness-user/detail"
2236
2236
  );
@@ -2247,7 +2247,7 @@ function registerHealthProfileCommands(program2) {
2247
2247
  hp.command("get-section").description("Retrieve health profile answers for a specific section").requiredOption("--section <acronym>", "Section acronym (e.g. HWP, DMH, DA)").action(async (opts, cmd) => {
2248
2248
  try {
2249
2249
  const globalOpts = cmd.optsWithGlobals();
2250
- const client = new ApiClient(globalOpts.apiKey);
2250
+ const client = await createApiClient({ apiKey: globalOpts.apiKey });
2251
2251
  const user = await client.get(
2252
2252
  "/api/betterness-user/detail"
2253
2253
  );
@@ -2294,7 +2294,7 @@ function registerHealthProfileCommands(program2) {
2294
2294
  return;
2295
2295
  }
2296
2296
  const globalOpts = cmd.optsWithGlobals();
2297
- const client = new ApiClient(globalOpts.apiKey);
2297
+ const client = await createApiClient({ apiKey: globalOpts.apiKey });
2298
2298
  const user = await client.get(
2299
2299
  "/api/betterness-user/detail"
2300
2300
  );
@@ -2334,7 +2334,7 @@ function registerHealthProfileCommands(program2) {
2334
2334
  return;
2335
2335
  }
2336
2336
  const globalOpts = cmd.optsWithGlobals();
2337
- const client = new ApiClient(globalOpts.apiKey);
2337
+ const client = await createApiClient({ apiKey: globalOpts.apiKey });
2338
2338
  const user = await client.get(
2339
2339
  "/api/betterness-user/detail"
2340
2340
  );
@@ -2349,7 +2349,7 @@ function registerHealthProfileCommands(program2) {
2349
2349
  hp.command("summary").description("Human-readable summary of answered health profile questions").option("--section <acronym>", "Summarize a specific section only").action(async (opts, cmd) => {
2350
2350
  try {
2351
2351
  const globalOpts = cmd.optsWithGlobals();
2352
- const client = new ApiClient(globalOpts.apiKey);
2352
+ const client = await createApiClient({ apiKey: globalOpts.apiKey });
2353
2353
  const [schema, user] = await Promise.all([
2354
2354
  client.get("/api/v1/copilot-config/schema", void 0, healthProfileSchemaSchema),
2355
2355
  client.get("/api/betterness-user/detail")
@@ -2447,10 +2447,230 @@ function formatAnswerValue(value) {
2447
2447
  return value;
2448
2448
  }
2449
2449
 
2450
+ // src/commands/mcp.ts
2451
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync4, mkdirSync as mkdirSync3, copyFileSync } from "fs";
2452
+ import { join as join4, dirname } from "path";
2453
+ import { homedir as homedir4, platform } from "os";
2454
+ var CLIENTS = {
2455
+ claude: {
2456
+ name: "Claude Desktop",
2457
+ supportsScope: false,
2458
+ configPath: () => {
2459
+ if (platform() === "darwin") {
2460
+ return join4(homedir4(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
2461
+ }
2462
+ return join4(homedir4(), ".config", "Claude", "claude_desktop_config.json");
2463
+ }
2464
+ },
2465
+ "claude-code": {
2466
+ name: "Claude Code",
2467
+ supportsScope: true,
2468
+ configPath: (scope) => {
2469
+ if (scope === "project") {
2470
+ return join4(process.cwd(), ".mcp.json");
2471
+ }
2472
+ return join4(homedir4(), ".claude", "settings.json");
2473
+ }
2474
+ },
2475
+ cursor: {
2476
+ name: "Cursor",
2477
+ supportsScope: false,
2478
+ configPath: () => join4(process.cwd(), ".cursor", "mcp.json")
2479
+ },
2480
+ windsurf: {
2481
+ name: "Windsurf",
2482
+ supportsScope: false,
2483
+ configPath: () => join4(homedir4(), ".codeium", "windsurf", "mcp_config.json")
2484
+ }
2485
+ };
2486
+ var VALID_CLIENTS = Object.keys(CLIENTS).join(", ");
2487
+ function validateClient(clientArg) {
2488
+ if (!(clientArg in CLIENTS)) {
2489
+ throw new CliError(
2490
+ `Unknown client "${clientArg}". Supported clients: ${VALID_CLIENTS}`,
2491
+ "INVALID_CLIENT"
2492
+ );
2493
+ }
2494
+ return clientArg;
2495
+ }
2496
+ function getMcpUrl() {
2497
+ const base = "https://api.betterness.ai";
2498
+ if (!base) {
2499
+ throw new CliError(
2500
+ "No API URL configured. This is a build error \u2014 BETTERNESS_API_URL should be baked in at build time.",
2501
+ "CONFIG_MISSING"
2502
+ );
2503
+ }
2504
+ return `${base.replace(/\/$/, "")}/mcp`;
2505
+ }
2506
+ function buildMcpEntry(apiKey) {
2507
+ return {
2508
+ command: "npx",
2509
+ args: ["-y", "mcp-remote", getMcpUrl(), "--header", `Authorization: Bearer ${apiKey}`]
2510
+ };
2511
+ }
2512
+ function readConfigFile(filePath) {
2513
+ if (!existsSync4(filePath)) {
2514
+ return {};
2515
+ }
2516
+ try {
2517
+ const content = readFileSync3(filePath, "utf-8");
2518
+ return JSON.parse(content);
2519
+ } catch {
2520
+ throw new CliError(
2521
+ `Failed to parse config file: ${filePath}. Ensure it contains valid JSON.`,
2522
+ "INVALID_CONFIG"
2523
+ );
2524
+ }
2525
+ }
2526
+ function writeConfigFile(filePath, config) {
2527
+ const dir = dirname(filePath);
2528
+ if (!existsSync4(dir)) {
2529
+ mkdirSync3(dir, { recursive: true });
2530
+ }
2531
+ writeFileSync3(filePath, JSON.stringify(config, null, 2) + "\n");
2532
+ }
2533
+ function truncateKey(apiKey) {
2534
+ if (apiKey.length <= 10) return apiKey;
2535
+ return `${apiKey.slice(0, 6)}\u2026${apiKey.slice(-4)}`;
2536
+ }
2537
+ function extractKeyFromConfig(config) {
2538
+ const servers = config.mcpServers;
2539
+ if (!servers?.betterness) return null;
2540
+ const entry = servers.betterness;
2541
+ const args = entry.args;
2542
+ if (!args) return null;
2543
+ const headerIdx = args.indexOf("--header");
2544
+ if (headerIdx === -1 || headerIdx + 1 >= args.length) return null;
2545
+ const headerVal = args[headerIdx + 1];
2546
+ const match = headerVal.match(/Bearer\s+(.+)/);
2547
+ return match ? match[1] : null;
2548
+ }
2549
+ function registerMcpCommands(program2) {
2550
+ const mcp = program2.command("mcp").description("MCP server integration for AI agents (Claude, Cursor, Windsurf)");
2551
+ mcp.command("install").description("Install Betterness MCP integration into an AI client").argument("<client>", `Client to configure (${VALID_CLIENTS})`).option("--scope <scope>", "Config scope: global or project (claude-code only)", "global").option("--dry-run", "Print config without writing").action(async (clientArg, opts, cmd) => {
2552
+ try {
2553
+ const clientId = validateClient(clientArg);
2554
+ const client = CLIENTS[clientId];
2555
+ const scope = opts.scope === "project" ? "project" : "global";
2556
+ if (opts.scope === "project" && !client.supportsScope) {
2557
+ console.error(`Note: --scope is only supported for claude-code. Using default path for ${client.name}.`);
2558
+ }
2559
+ const globalOpts = cmd.optsWithGlobals();
2560
+ const credentials = await resolveCredentials(globalOpts.apiKey);
2561
+ const entry = buildMcpEntry(credentials.apiKey);
2562
+ const configPath = client.configPath(scope);
2563
+ if (opts.dryRun) {
2564
+ const redactedEntry = buildMcpEntry(`${credentials.apiKey.slice(0, 6)}\u2026****`);
2565
+ const preview = { mcpServers: { betterness: redactedEntry } };
2566
+ console.log(`Would write to: ${configPath}
2567
+ `);
2568
+ console.log(JSON.stringify(preview, null, 2));
2569
+ return;
2570
+ }
2571
+ const config = readConfigFile(configPath);
2572
+ let backupPath = null;
2573
+ if (existsSync4(configPath)) {
2574
+ backupPath = `${configPath}.bak`;
2575
+ copyFileSync(configPath, backupPath);
2576
+ }
2577
+ if (!config.mcpServers || typeof config.mcpServers !== "object") {
2578
+ config.mcpServers = {};
2579
+ }
2580
+ config.mcpServers.betterness = entry;
2581
+ writeConfigFile(configPath, config);
2582
+ outputRecord(cmd, {
2583
+ client: client.name,
2584
+ configFile: configPath,
2585
+ status: "installed",
2586
+ apiKey: truncateKey(credentials.apiKey),
2587
+ backup: backupPath ?? "none (new file)"
2588
+ });
2589
+ console.log(`
2590
+ Restart ${client.name} to activate. Then try asking:
2591
+ "List my connected devices"`);
2592
+ } catch (error) {
2593
+ outputError(error);
2594
+ }
2595
+ });
2596
+ mcp.command("uninstall").description("Remove Betterness MCP integration from an AI client").argument("<client>", `Client to unconfigure (${VALID_CLIENTS})`).option("--scope <scope>", "Config scope: global or project (claude-code only)", "global").action(async (clientArg, opts, cmd) => {
2597
+ try {
2598
+ const clientId = validateClient(clientArg);
2599
+ const client = CLIENTS[clientId];
2600
+ const scope = opts.scope === "project" ? "project" : "global";
2601
+ const configPath = client.configPath(scope);
2602
+ if (!existsSync4(configPath)) {
2603
+ console.log(`No config file found for ${client.name} at ${configPath}`);
2604
+ return;
2605
+ }
2606
+ const config = readConfigFile(configPath);
2607
+ const servers = config.mcpServers;
2608
+ if (!servers?.betterness) {
2609
+ console.log(`Betterness MCP is not configured for ${client.name}.`);
2610
+ return;
2611
+ }
2612
+ delete servers.betterness;
2613
+ if (Object.keys(servers).length === 0) {
2614
+ delete config.mcpServers;
2615
+ }
2616
+ writeConfigFile(configPath, config);
2617
+ outputRecord(cmd, {
2618
+ client: client.name,
2619
+ configFile: configPath,
2620
+ status: "removed"
2621
+ });
2622
+ } catch (error) {
2623
+ outputError(error);
2624
+ }
2625
+ });
2626
+ mcp.command("status").description("Show MCP integration status across all supported clients").action(async (_, cmd) => {
2627
+ try {
2628
+ const mcpUrl = getMcpUrl();
2629
+ const rows = [];
2630
+ for (const [id, client] of Object.entries(CLIENTS)) {
2631
+ const scopes = client.supportsScope ? ["global", "project"] : ["global"];
2632
+ for (const scope of scopes) {
2633
+ const configPath = client.configPath(scope);
2634
+ const exists = existsSync4(configPath);
2635
+ let configured = false;
2636
+ let apiKey = "";
2637
+ if (exists) {
2638
+ try {
2639
+ const config = readConfigFile(configPath);
2640
+ const key = extractKeyFromConfig(config);
2641
+ if (key) {
2642
+ configured = true;
2643
+ apiKey = truncateKey(key);
2644
+ }
2645
+ } catch {
2646
+ }
2647
+ }
2648
+ rows.push({
2649
+ client: client.supportsScope ? `${client.name} (${scope})` : client.name,
2650
+ status: configured ? "\u2713 configured" : "\u2717 not configured",
2651
+ apiKey: apiKey || "\u2014",
2652
+ configFile: configPath
2653
+ });
2654
+ }
2655
+ }
2656
+ console.log(`MCP Endpoint: ${mcpUrl}
2657
+ `);
2658
+ outputList(cmd, rows, [
2659
+ { key: "client", label: "Client", width: 26 },
2660
+ { key: "status", label: "Status", width: 18 },
2661
+ { key: "apiKey", label: "API Key", width: 16 },
2662
+ { key: "configFile", label: "Config File", width: 55 }
2663
+ ]);
2664
+ } catch (error) {
2665
+ outputError(error);
2666
+ }
2667
+ });
2668
+ }
2669
+
2450
2670
  // src/program.ts
2451
2671
  function createProgram() {
2452
2672
  const program2 = new Command();
2453
- program2.name("betterness").description("Betterness CLI - Agent-first terminal interface for the Betterness platform").version("1.2.2").option("--api-key <key>", "API key (overrides env and stored credentials)").option("--json", "Output as JSON").option("--markdown", "Output as Markdown").option("--quiet", "Suppress output (exit code only)");
2673
+ program2.name("betterness").description("Betterness CLI - Agent-first terminal interface for the Betterness platform").version("1.3.1").option("--api-key <key>", "API key (overrides env and stored credentials)").option("--json", "Output as JSON").option("--markdown", "Output as Markdown").option("--quiet", "Suppress output (exit code only)");
2454
2674
  registerAuthCommands(program2);
2455
2675
  registerProfileCommands(program2);
2456
2676
  registerBiomarkersCommands(program2);
@@ -2470,6 +2690,7 @@ function createProgram() {
2470
2690
  registerSchemaCommand(program2);
2471
2691
  registerDebugCommands(program2);
2472
2692
  registerHealthProfileCommands(program2);
2693
+ registerMcpCommands(program2);
2473
2694
  return program2;
2474
2695
  }
2475
2696
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@betterness/cli",
3
- "version": "1.2.2",
3
+ "version": "1.3.1",
4
4
  "description": "Betterness CLI - Agent-first terminal interface for the Betterness platform",
5
5
  "type": "module",
6
6
  "bin": {