@betterness/cli 1.2.2 → 1.3.0
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 +27 -0
- package/dist/index.js +229 -8
- 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.
|
|
411
|
+
"User-Agent": `betterness-cli/${"1.3.0"}`,
|
|
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:
|
|
459
|
+
const { readFileSync: readFileSync4 } = await import("fs");
|
|
460
460
|
const { basename, extname } = await import("path");
|
|
461
|
-
const buffer =
|
|
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.
|
|
475
|
+
"User-Agent": `betterness-cli/${"1.3.0"}`,
|
|
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:
|
|
1664
|
+
const { existsSync: existsSync5 } = await import("fs");
|
|
1665
1665
|
const { resolve } = await import("path");
|
|
1666
1666
|
const filePath = resolve(opts.file);
|
|
1667
|
-
if (!
|
|
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.
|
|
2144
|
+
version: "1.3.0",
|
|
2145
2145
|
apiUrl: "https://api.betterness.ai",
|
|
2146
2146
|
auth0Domain: "betterness.us.auth0.com",
|
|
2147
2147
|
auth0ClientId: "g4lqYHRQb2QMgdRKIlKwoTJl6eu41pWn",
|
|
@@ -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.
|
|
2673
|
+
program2.name("betterness").description("Betterness CLI - Agent-first terminal interface for the Betterness platform").version("1.3.0").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
|
|