riskmodels 2.0.0 → 2.1.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 CHANGED
@@ -8,10 +8,12 @@ Command-line interface for [RiskModels](https://riskmodels.app): call the REST A
8
8
  npm install -g riskmodels
9
9
  ```
10
10
 
11
- Agent-first installer preview:
11
+ Agent-first installer (pin `@latest` so `npx` does not resolve a stale cached package):
12
12
 
13
13
  ```bash
14
- RISKMODELS_API_KEY=rm_agent_live_... npx riskmodels install
14
+ # Prerequisites: Node.js LTS (includes npx). macOS/Homebrew: brew install node; others: https://nodejs.org
15
+ RISKMODELS_API_KEY=rm_agent_live_... npx -y riskmodels@latest install --dry-run # optional: inspect plan first
16
+ RISKMODELS_API_KEY=rm_agent_live_... npx -y riskmodels@latest install
15
17
  ```
16
18
 
17
19
  Local development can run the same flow with:
@@ -101,4 +103,4 @@ Publishing (`npm publish`, version bumps, npm login / tokens) is **not** documen
101
103
 
102
104
  **Rule of thumb:** run publish commands only from **`cli/`**; the repo root package is the Next.js portal, not this CLI.
103
105
 
104
- The legacy `riskmodels-cli` npm package remains a backward-compatible published artifact. New releases from this directory target the unscoped `riskmodels` package for `npx riskmodels install`.
106
+ The legacy `riskmodels-cli` npm package remains a backward-compatible published artifact. New releases from this directory target the unscoped `riskmodels` package; use `npx -y riskmodels@latest install` for a deterministic one-shot MCP setup.
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function decomposeCommand(): Command;
@@ -0,0 +1,31 @@
1
+ import { Command } from "commander";
2
+ import chalk from "chalk";
3
+ import { loadConfig } from "../lib/config.js";
4
+ import { requireResolvedAuth } from "../lib/credentials.js";
5
+ import { apiFetchJson } from "../lib/api-client.js";
6
+ import { printResults } from "../lib/display.js";
7
+ export function decomposeCommand() {
8
+ return new Command("decompose")
9
+ .description("Four-layer decomposition + hedge map for one ticker (POST /decompose)")
10
+ .argument("<ticker>", "Ticker symbol, e.g. NVDA")
11
+ .action(async (ticker, _opts, cmd) => {
12
+ const json = cmd.optsWithGlobals().json ?? false;
13
+ const cfg = await loadConfig();
14
+ const auth = requireResolvedAuth(cfg, chalk.yellow);
15
+ if (!auth)
16
+ return;
17
+ const t = ticker.trim();
18
+ try {
19
+ const { body, costUsd } = await apiFetchJson(auth, "POST", "/decompose", {
20
+ jsonBody: { ticker: t },
21
+ });
22
+ printResults(body, json);
23
+ if (!json && costUsd)
24
+ console.log(chalk.dim(`Cost: $${costUsd}`));
25
+ }
26
+ catch (e) {
27
+ console.error(chalk.red(e instanceof Error ? e.message : String(e)));
28
+ process.exitCode = 1;
29
+ }
30
+ });
31
+ }
@@ -3,14 +3,17 @@ import { Command } from "commander";
3
3
  import { configPath, loadConfig, maskSecret } from "../lib/config.js";
4
4
  import { detectClients } from "../lib/mcp-config-paths.js";
5
5
  import { printResults } from "../lib/display.js";
6
+ import { printDoctorHuman } from "../lib/cli-human-diagnostics.js";
7
+ import { warnIfGlobalJsonBeforeSubcommand } from "../lib/global-json-hint.js";
8
+ import { abbreviatePath } from "../lib/path-display.js";
6
9
  function commandOk(command, args = ["--version"]) {
7
10
  const result = spawnSync(command, args, { stdio: "ignore" });
8
11
  return result.status === 0 || result.status === 1;
9
12
  }
10
13
  export function doctorCommand() {
11
14
  return new Command("doctor")
12
- .description("Run local diagnostics for RiskModels CLI and MCP install readiness")
13
- .option("--json", "JSON output")
15
+ .description("Run local diagnostics for RiskModels CLI and MCP install readiness (default: readable summary; use --json for scripts)")
16
+ .option("--json", "Structured JSON instead of readable summary")
14
17
  .action(async (opts, cmd) => {
15
18
  const json = opts.json || cmd.optsWithGlobals().json || false;
16
19
  const cfg = await loadConfig();
@@ -24,28 +27,35 @@ export function doctorCommand() {
24
27
  {
25
28
  id: "npx",
26
29
  ok: commandOk("npx"),
27
- detail: "Required to launch @riskmodels/mcp from MCP client configs.",
30
+ detail: "Required so MCP configs can run `npx -y @riskmodels/mcp` (stdio server from `riskmodels install`).",
28
31
  },
29
32
  {
30
33
  id: "api_credentials",
31
34
  ok: !!cfg?.apiKey || !!process.env.RISKMODELS_API_KEY || (!!cfg?.clientId && !!cfg?.clientSecret),
32
35
  detail: cfg?.apiKey
33
- ? `Config API key ${maskSecret(cfg.apiKey)} in ${configPath()}`
36
+ ? `Config API key ${maskSecret(cfg.apiKey)} in ${abbreviatePath(configPath())}`
34
37
  : process.env.RISKMODELS_API_KEY
35
38
  ? "RISKMODELS_API_KEY is set in the environment."
36
- : "No API key found. Get one at https://riskmodels.app/get-api-key",
39
+ : "No API key found. Get one at https://riskmodels.app/get-key",
37
40
  },
38
41
  {
39
42
  id: "mcp_package_target",
40
43
  ok: true,
41
- detail: "Installer targets npx -y @riskmodels/mcp once the package is published.",
44
+ detail: "`riskmodels install` merges `npx -y @riskmodels/mcp` into detected client configs (stdio MCP).",
42
45
  },
43
46
  ];
44
- printResults({
47
+ const payload = {
45
48
  ok: checks.every((check) => check.ok),
46
49
  checks,
47
50
  clients,
48
51
  note: "Run `riskmodels install --dry-run` to inspect config writes, or `riskmodels install` to write configs with backups and run a connection test.",
49
- }, json);
52
+ };
53
+ if (json) {
54
+ warnIfGlobalJsonBeforeSubcommand("doctor");
55
+ printResults(payload, json);
56
+ }
57
+ else {
58
+ printDoctorHuman(payload);
59
+ }
50
60
  });
51
61
  }
@@ -1,14 +1,16 @@
1
1
  import { Command } from "commander";
2
2
  import inquirer from "inquirer";
3
3
  import chalk from "chalk";
4
- import { configPath, DEFAULT_API_BASE, loadConfig } from "../lib/config.js";
4
+ import { configPath, DEFAULT_API_BASE, loadConfig, sharedRiskmodelsConfigExists, } from "../lib/config.js";
5
5
  import { detectClients, selectedClients } from "../lib/mcp-config-paths.js";
6
- import { buildInstallPlans, firstPrompt } from "../lib/mcp-install-plan.js";
6
+ import { buildInstallPlans, firstPrompt, looksLikeRiskmodelsKey, } from "../lib/mcp-install-plan.js";
7
7
  import { printResults } from "../lib/display.js";
8
+ import { API_KEY_EMAIL_HINT, printInstallDryRunHuman, printInstallMissingKeyHuman, printInstallSuccessHuman, } from "../lib/mcp-cli-human-output.js";
8
9
  import { redactSecret } from "../lib/redact.js";
9
- import { installMcpConfig, writeSharedApiKey } from "../lib/mcp-config-writer.js";
10
+ import { installClaudeCodeRemote, installMcpConfig, writeSharedApiKey, } from "../lib/mcp-config-writer.js";
10
11
  import { apiRootFromUserBase } from "../lib/api-url.js";
11
12
  import { apiFetchOptionalAuth } from "../lib/api-client.js";
13
+ import { warnIfGlobalJsonBeforeSubcommand } from "../lib/global-json-hint.js";
12
14
  function envApiKey() {
13
15
  return process.env.RISKMODELS_API_KEY?.trim() || undefined;
14
16
  }
@@ -41,7 +43,8 @@ async function resolveApiKey(opts) {
41
43
  if (opts.yes)
42
44
  return { source: "missing" };
43
45
  console.error(chalk.yellow("No RiskModels API key found."));
44
- console.error(chalk.dim("Get a key: https://riskmodels.app/get-api-key"));
46
+ console.error(chalk.dim("Get a key: https://riskmodels.app/get-key"));
47
+ console.error(chalk.dim(API_KEY_EMAIL_HINT));
45
48
  const answer = await inquirer.prompt([
46
49
  {
47
50
  type: "password",
@@ -55,14 +58,16 @@ async function resolveApiKey(opts) {
55
58
  }
56
59
  export function installCommand() {
57
60
  return new Command("install")
58
- .description("Detect AI clients and install/register the RiskModels MCP server")
61
+ .description("Detect AI clients and install/register the RiskModels MCP server (default: friendly summary; pass --json for machine-readable)")
59
62
  .option("--client <name>", "claude | cursor | codex | vscode")
60
63
  .option("--all", "Target all detected clients")
61
64
  .option("--api-key <key>", "RiskModels API key for one-shot setup")
62
65
  .option("--dry-run", "Show planned config changes without writing")
63
66
  .option("--yes", "Non-interactive mode")
64
- .option("--embed-key", "Explicitly embed the API key in MCP config env (not recommended)")
65
- .option("--json", "JSON output")
67
+ .option("--remote", "Use the hosted endpoint via mcp-remote (default)")
68
+ .option("--local", "Use the local stdio server (npx @riskmodels/mcp) instead of the hosted endpoint")
69
+ .option("--embed-key", "Local stdio only: embed the API key in MCP config env (not recommended)")
70
+ .option("--json", "Structured JSON instead of formatted text (matches historical output shape)")
66
71
  .action(async (opts, cmd) => {
67
72
  const json = opts.json || cmd.optsWithGlobals().json || false;
68
73
  let clients;
@@ -75,19 +80,36 @@ export function installCommand() {
75
80
  return;
76
81
  }
77
82
  const dryRun = opts.dryRun ?? false;
83
+ const transport = opts.local ? "local" : "remote";
78
84
  const { apiKey, source } = await resolveApiKey(opts);
85
+ if (apiKey && !looksLikeRiskmodelsKey(apiKey)) {
86
+ const msg = 'That does not look like a RiskModels API key (expected an "rm_…" value). ' +
87
+ "Get one at https://riskmodels.app/get-key";
88
+ if (json) {
89
+ warnIfGlobalJsonBeforeSubcommand("install");
90
+ printResults({ error: msg, apiKey: { source } }, json);
91
+ }
92
+ else {
93
+ console.error(chalk.red(msg));
94
+ }
95
+ process.exitCode = 1;
96
+ return;
97
+ }
79
98
  const detections = await detectClients(clients);
80
- const plans = buildInstallPlans(detections, { apiKey, embedKey: opts.embedKey });
99
+ const plans = buildInstallPlans(detections, { apiKey, embedKey: opts.embedKey, transport });
81
100
  const existingConfig = await loadConfig();
82
101
  const output = {
83
102
  dryRun,
103
+ transport,
84
104
  apiKey: {
85
105
  found: !!apiKey,
86
106
  source,
87
107
  value: redactSecret(apiKey),
88
108
  storage: configPath(),
89
109
  willStoreInSharedConfig: !!apiKey && source !== configPath() && !dryRun,
90
- embeddedInMcpConfig: !!opts.embedKey,
110
+ // Remote always carries the key in the Authorization header; local only
111
+ // embeds it when --embed-key is passed.
112
+ embeddedInMcpConfig: transport === "remote" ? !!apiKey : !!opts.embedKey,
91
113
  },
92
114
  clients: plans,
93
115
  firstPrompt: firstPrompt(),
@@ -96,26 +118,48 @@ export function installCommand() {
96
118
  : "Install complete. Restart your MCP client, then try the first prompt.",
97
119
  };
98
120
  if (dryRun) {
99
- printResults(output, json);
100
- if (!json) {
101
- console.error(chalk.green(`First prompt to try: "${firstPrompt()}"`));
121
+ if (json) {
122
+ warnIfGlobalJsonBeforeSubcommand("install");
123
+ printResults(output, json);
124
+ }
125
+ else {
126
+ printInstallDryRunHuman({
127
+ plans,
128
+ willStoreSharedKey: !!apiKey && source !== configPath(),
129
+ firstPrompt: firstPrompt(),
130
+ });
102
131
  }
103
132
  return;
104
133
  }
105
134
  if (!apiKey) {
106
- printResults({
107
- ...output,
108
- error: "No RiskModels API key found. Get one at https://riskmodels.app/get-api-key or pass --api-key.",
109
- }, json);
135
+ if (json) {
136
+ warnIfGlobalJsonBeforeSubcommand("install");
137
+ printResults({
138
+ ...output,
139
+ error: "No RiskModels API key found. Get one at https://riskmodels.app/get-key or pass --api-key.",
140
+ }, json);
141
+ }
142
+ else {
143
+ printInstallMissingKeyHuman();
144
+ }
110
145
  process.exitCode = 1;
111
146
  return;
112
147
  }
148
+ const sharedBefore = await sharedRiskmodelsConfigExists();
113
149
  const sharedConfigWrite = await writeSharedApiKey(apiKey, existingConfig?.apiBaseUrl ?? DEFAULT_API_BASE);
114
- const writes = await Promise.all(detections.map((detection) => installMcpConfig(detection, {
115
- apiKey,
116
- embedKey: opts.embedKey,
117
- apiBaseUrl: existingConfig?.apiBaseUrl,
118
- })));
150
+ const writes = await Promise.all(detections.map((detection) => {
151
+ // Claude Code reads its own config, not claude_desktop_config.json —
152
+ // register via its CLI over HTTP transport instead of writing Desktop JSON.
153
+ if (transport === "remote" && detection.client === "claude" && detection.commandAvailable) {
154
+ return Promise.resolve(installClaudeCodeRemote(detection, { apiKey }));
155
+ }
156
+ return installMcpConfig(detection, {
157
+ apiKey,
158
+ embedKey: opts.embedKey,
159
+ apiBaseUrl: existingConfig?.apiBaseUrl,
160
+ transport,
161
+ });
162
+ }));
119
163
  const test = await connectionTest(existingConfig?.apiBaseUrl);
120
164
  const result = {
121
165
  ...output,
@@ -123,14 +167,24 @@ export function installCommand() {
123
167
  writes,
124
168
  connectionTest: test,
125
169
  };
126
- printResults(result, json);
170
+ if (json) {
171
+ warnIfGlobalJsonBeforeSubcommand("install");
172
+ printResults(result, json);
173
+ }
174
+ else {
175
+ printInstallSuccessHuman({
176
+ isFirstInstall: !sharedBefore,
177
+ writes,
178
+ sharedConfigWrite,
179
+ connectionTest: test,
180
+ firstPrompt: firstPrompt(),
181
+ hadErrors: writes.some((w) => w.action === "error") || !test.ok,
182
+ showClaudeCodeMcpTip: transport === "local" && detections.some((d) => d.client === "claude" && d.commandAvailable),
183
+ });
184
+ }
127
185
  if (writes.some((write) => write.action === "error") || !test.ok) {
128
186
  process.exitCode = 1;
129
187
  return;
130
188
  }
131
- if (!json) {
132
- console.error(chalk.green("RiskModels MCP install completed with backups."));
133
- console.error(chalk.green(`First prompt to try: "${firstPrompt()}"`));
134
- }
135
189
  });
136
190
  }
@@ -2,24 +2,33 @@ import { Command } from "commander";
2
2
  import { configPath, loadConfig, maskSecret } from "../lib/config.js";
3
3
  import { detectClients, selectedClients } from "../lib/mcp-config-paths.js";
4
4
  import { printResults } from "../lib/display.js";
5
+ import { printStatusHuman } from "../lib/cli-human-diagnostics.js";
6
+ import { warnIfGlobalJsonBeforeSubcommand } from "../lib/global-json-hint.js";
5
7
  export function statusCommand() {
6
8
  return new Command("status")
7
- .description("Show RiskModels CLI and MCP client configuration status")
9
+ .description("Show RiskModels CLI and MCP client configuration status (default: readable summary; use --json for scripts)")
8
10
  .option("--client <name>", "claude | cursor | codex | vscode")
9
11
  .option("--all", "Check all supported clients")
10
- .option("--json", "JSON output")
12
+ .option("--json", "Structured JSON instead of readable summary")
11
13
  .action(async (opts, cmd) => {
12
14
  const json = opts.json || cmd.optsWithGlobals().json || false;
13
15
  const cfg = await loadConfig();
14
16
  const clients = selectedClients({ client: opts.client, all: opts.all });
15
17
  const detections = await detectClients(clients);
16
- printResults({
18
+ const payload = {
17
19
  configPath: configPath(),
18
20
  configFound: !!cfg,
19
21
  apiBaseUrl: cfg?.apiBaseUrl ?? "https://riskmodels.app",
20
22
  apiKey: maskSecret(cfg?.apiKey),
21
23
  oauthConfigured: !!cfg?.clientId && !!cfg?.clientSecret,
22
24
  mcpClients: detections,
23
- }, json);
25
+ };
26
+ if (json) {
27
+ warnIfGlobalJsonBeforeSubcommand("status");
28
+ printResults(payload, json);
29
+ }
30
+ else {
31
+ printStatusHuman(payload);
32
+ }
24
33
  });
25
34
  }
@@ -1,15 +1,16 @@
1
1
  import { Command } from "commander";
2
- import chalk from "chalk";
3
2
  import { detectClients, selectedClients } from "../lib/mcp-config-paths.js";
4
3
  import { printResults } from "../lib/display.js";
4
+ import { printUninstallPlannedHuman, printUninstallSuccessHuman, } from "../lib/mcp-cli-human-output.js";
5
5
  import { uninstallMcpConfig } from "../lib/mcp-config-writer.js";
6
+ import { warnIfGlobalJsonBeforeSubcommand } from "../lib/global-json-hint.js";
6
7
  export function uninstallCommand() {
7
8
  return new Command("uninstall")
8
- .description("Remove the RiskModels MCP server from client configs")
9
+ .description("Remove the RiskModels MCP server from client configs (default: friendly summary; pass --json for machine-readable)")
9
10
  .option("--client <name>", "claude | cursor | codex | vscode")
10
11
  .option("--all", "Check all supported clients")
11
12
  .option("--dry-run", "Show planned removals without writing")
12
- .option("--json", "JSON output")
13
+ .option("--json", "Structured JSON instead of formatted text (matches historical output shape)")
13
14
  .action(async (opts, cmd) => {
14
15
  const json = opts.json || cmd.optsWithGlobals().json || false;
15
16
  const clients = selectedClients({ client: opts.client, all: opts.all });
@@ -29,17 +30,26 @@ export function uninstallCommand() {
29
30
  })),
30
31
  };
31
32
  if (dryRun) {
32
- printResults(output, json);
33
+ if (json) {
34
+ warnIfGlobalJsonBeforeSubcommand("uninstall");
35
+ printResults(output, json);
36
+ }
37
+ else {
38
+ printUninstallPlannedHuman(detections);
39
+ }
33
40
  return;
34
41
  }
35
42
  const removals = await Promise.all(detections.map((detection) => uninstallMcpConfig(detection)));
36
- printResults({ ...output, removals }, json);
43
+ if (json) {
44
+ warnIfGlobalJsonBeforeSubcommand("uninstall");
45
+ printResults({ ...output, removals }, json);
46
+ }
47
+ else {
48
+ printUninstallSuccessHuman(removals);
49
+ }
37
50
  if (removals.some((removal) => removal.action === "error")) {
38
51
  process.exitCode = 1;
39
52
  return;
40
53
  }
41
- if (!json) {
42
- console.error(chalk.green("RiskModels MCP uninstall completed with backups where files changed."));
43
- }
44
54
  });
45
55
  }
package/dist/index.js CHANGED
@@ -10,6 +10,7 @@ import { mcpConfigCommand } from "./commands/mcp-config.js";
10
10
  import { mcpServeCommand } from "./commands/mcp.js";
11
11
  import { agentCommand } from "./commands/agent.js";
12
12
  import { metricsCommand } from "./commands/metrics.js";
13
+ import { decomposeCommand } from "./commands/decompose.js";
13
14
  import { batchCommand } from "./commands/batch.js";
14
15
  import { portfolioCommand } from "./commands/portfolio.js";
15
16
  import { tickersCommand } from "./commands/tickers.js";
@@ -28,15 +29,17 @@ const program = new Command();
28
29
  program
29
30
  .name("riskmodels")
30
31
  .description("RiskModels CLI — REST API, SQL query, schema, billing, and agent manifests")
31
- .version("2.0.0", "-V, --version", "output version")
32
+ .version("2.0.2", "-V, --version", "output version")
32
33
  .option("--json", "JSON output for supported commands (query, schema, balance, API calls, config list)")
33
34
  .configureHelp({ sortSubcommands: true })
34
35
  .addHelpText("after", `
35
36
  ${chalk.bold("Quick start")}
36
37
  ${chalk.dim("$")} riskmodels config init
37
38
  ${chalk.dim("$")} riskmodels install --dry-run
39
+ ${chalk.dim("$")} riskmodels uninstall --dry-run
38
40
  ${chalk.dim("$")} riskmodels health
39
41
  ${chalk.dim("$")} riskmodels metrics NVDA
42
+ ${chalk.dim("$")} riskmodels decompose NVDA
40
43
  ${chalk.dim("$")} riskmodels query ${chalk.green('"SELECT ticker FROM ticker_metadata LIMIT 3"')}
41
44
  ${chalk.dim("$")} riskmodels manifest --format anthropic
42
45
 
@@ -48,6 +51,7 @@ program.addCommand(doctorCommand());
48
51
  program.addCommand(uninstallCommand());
49
52
  program.addCommand(queryCommand());
50
53
  program.addCommand(metricsCommand());
54
+ program.addCommand(decomposeCommand());
51
55
  program.addCommand(batchCommand());
52
56
  program.addCommand(portfolioCommand());
53
57
  program.addCommand(returnsCommand());
@@ -0,0 +1,21 @@
1
+ import type { ClientDetection } from "./mcp-config-paths.js";
2
+ export type DoctorPayload = {
3
+ ok: boolean;
4
+ checks: {
5
+ id: string;
6
+ ok: boolean;
7
+ detail: string;
8
+ }[];
9
+ clients: ClientDetection[];
10
+ note: string;
11
+ };
12
+ export declare function printDoctorHuman(payload: DoctorPayload): void;
13
+ export type StatusPayload = {
14
+ configPath: string;
15
+ configFound: boolean;
16
+ apiBaseUrl: string;
17
+ apiKey: string;
18
+ oauthConfigured: boolean;
19
+ mcpClients: ClientDetection[];
20
+ };
21
+ export declare function printStatusHuman(payload: StatusPayload): void;
@@ -0,0 +1,49 @@
1
+ import chalk from "chalk";
2
+ import { abbreviatePath } from "./path-display.js";
3
+ import { formatCliVersionLabel } from "./cli-version.js";
4
+ const BULLET = "•";
5
+ function logLine(line = "") {
6
+ console.log(line);
7
+ }
8
+ export function printDoctorHuman(payload) {
9
+ logLine("");
10
+ logLine(`${chalk.bold("riskmodels doctor")} ${chalk.dim(`(CLI ${formatCliVersionLabel()})`)}`);
11
+ logLine("");
12
+ logLine(payload.ok ? chalk.green.bold("All checks passed") : chalk.yellow.bold("Some checks need attention"));
13
+ logLine("");
14
+ logLine(chalk.bold("Checks:"));
15
+ for (const c of payload.checks) {
16
+ const icon = c.ok ? chalk.green("✓") : chalk.red("✗");
17
+ logLine(` ${icon} ${chalk.bold(c.id)}${c.detail ? chalk.dim(` — ${c.detail}`) : ""}`);
18
+ }
19
+ logLine("");
20
+ logLine(chalk.bold("MCP client detection:"));
21
+ for (const d of payload.clients) {
22
+ const p = d.configPath ? chalk.dim(` — ${abbreviatePath(d.configPath)}`) : "";
23
+ logLine(` ${BULLET} ${d.label} (${d.status}${d.mode !== "guidance" ? `, ${d.mode}` : ""})${p}`);
24
+ }
25
+ logLine("");
26
+ logLine(chalk.dim(payload.note));
27
+ logLine("");
28
+ }
29
+ export function printStatusHuman(payload) {
30
+ logLine("");
31
+ logLine(`${chalk.bold("riskmodels status")} ${chalk.dim(`(CLI ${formatCliVersionLabel()})`)}`);
32
+ logLine("");
33
+ const cfgDisp = abbreviatePath(payload.configPath);
34
+ logLine(`${chalk.bold("Shared config:")} ${cfgDisp}`);
35
+ if (cfgDisp !== payload.configPath) {
36
+ logLine(chalk.dim(` (full path: ${payload.configPath})`));
37
+ }
38
+ logLine(` ${BULLET} ${payload.configFound ? chalk.green("file present") : chalk.dim("file missing (run install or config init)")}`);
39
+ logLine(` ${BULLET} ${chalk.bold("apiBaseUrl")} ${chalk.dim(payload.apiBaseUrl)}`);
40
+ logLine(` ${BULLET} ${chalk.bold("API key")} ${payload.apiKey}`);
41
+ logLine(` ${BULLET} ${chalk.bold("OAuth")} ${payload.oauthConfigured ? chalk.green("configured") : chalk.dim("not configured")}`);
42
+ logLine("");
43
+ logLine(chalk.bold("MCP-related clients:"));
44
+ for (const d of payload.mcpClients) {
45
+ const p = d.configPath ? chalk.dim(` — ${abbreviatePath(d.configPath)}`) : "";
46
+ logLine(` ${BULLET} ${d.label}: ${d.status}${p}`);
47
+ }
48
+ logLine("");
49
+ }
@@ -0,0 +1,4 @@
1
+ /** Resolved from sibling `cli/package.json` at runtime (`dist/` → repo `cli/`). */
2
+ export declare function getCliPackageVersion(): string;
3
+ /** e.g. `v2.0.2` for human-facing footers (package.json omits leading v). */
4
+ export declare function formatCliVersionLabel(): string;
@@ -0,0 +1,25 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ const pkgDir = dirname(fileURLToPath(import.meta.url));
5
+ let cachedVersion = null;
6
+ /** Resolved from sibling `cli/package.json` at runtime (`dist/` → repo `cli/`). */
7
+ export function getCliPackageVersion() {
8
+ if (cachedVersion !== null)
9
+ return cachedVersion;
10
+ try {
11
+ const pkgPath = join(pkgDir, "..", "..", "package.json");
12
+ const raw = readFileSync(pkgPath, "utf8");
13
+ cachedVersion = JSON.parse(raw).version;
14
+ return cachedVersion;
15
+ }
16
+ catch {
17
+ cachedVersion = "0.0.0";
18
+ return cachedVersion;
19
+ }
20
+ }
21
+ /** e.g. `v2.0.2` for human-facing footers (package.json omits leading v). */
22
+ export function formatCliVersionLabel() {
23
+ const v = getCliPackageVersion();
24
+ return v.startsWith("v") ? v : `v${v}`;
25
+ }
@@ -13,6 +13,8 @@ export interface RiskmodelsConfig {
13
13
  }
14
14
  export declare const DEFAULT_API_BASE = "https://riskmodels.app";
15
15
  export declare function configPath(): string;
16
+ /** True when shared config file exists (used for MCP install welcome on first-ever setup). */
17
+ export declare function sharedRiskmodelsConfigExists(): Promise<boolean>;
16
18
  export declare function loadConfig(): Promise<RiskmodelsConfig | null>;
17
19
  export declare function saveConfig(cfg: RiskmodelsConfig): Promise<void>;
18
20
  export declare function maskSecret(value: string | undefined, visible?: number): string;
@@ -1,10 +1,22 @@
1
- import { mkdir, readFile, writeFile } from "node:fs/promises";
1
+ import { mkdir, readFile, writeFile, stat } from "node:fs/promises";
2
2
  import { homedir } from "node:os";
3
3
  import path from "node:path";
4
4
  export const DEFAULT_API_BASE = "https://riskmodels.app";
5
5
  export function configPath() {
6
6
  return path.join(homedir(), ".config", "riskmodels", "config.json");
7
7
  }
8
+ /** True when shared config file exists (used for MCP install welcome on first-ever setup). */
9
+ export async function sharedRiskmodelsConfigExists() {
10
+ try {
11
+ await stat(configPath());
12
+ return true;
13
+ }
14
+ catch (err) {
15
+ if (err.code === "ENOENT")
16
+ return false;
17
+ throw err;
18
+ }
19
+ }
8
20
  export async function loadConfig() {
9
21
  const p = configPath();
10
22
  try {
@@ -0,0 +1,4 @@
1
+ /** True when `--json` appears in argv before the subcommand (parent-level flag). */
2
+ export declare function usedGlobalJsonBeforeSubcommand(subcommandName: string): boolean;
3
+ /** Explains JSON output when `--json` was set before the subcommand (stderr, dim). */
4
+ export declare function warnIfGlobalJsonBeforeSubcommand(subcommandName: string): void;
@@ -0,0 +1,16 @@
1
+ import chalk from "chalk";
2
+ /** True when `--json` appears in argv before the subcommand (parent-level flag). */
3
+ export function usedGlobalJsonBeforeSubcommand(subcommandName) {
4
+ const args = process.argv.slice(2);
5
+ const jsonIdx = args.findIndex((a) => a === "--json" || a.startsWith("--json="));
6
+ const subIdx = args.indexOf(subcommandName);
7
+ if (jsonIdx === -1 || subIdx === -1)
8
+ return false;
9
+ return jsonIdx < subIdx;
10
+ }
11
+ /** Explains JSON output when `--json` was set before the subcommand (stderr, dim). */
12
+ export function warnIfGlobalJsonBeforeSubcommand(subcommandName) {
13
+ if (!usedGlobalJsonBeforeSubcommand(subcommandName))
14
+ return;
15
+ console.error(chalk.dim(`Note: JSON mode is on because --json appears before "${subcommandName}". Move --json after "${subcommandName}" when you want default human output.`));
16
+ }
@@ -0,0 +1,28 @@
1
+ import type { SharedConfigWriteResult, ConfigWriteResult } from "./mcp-config-writer.js";
2
+ import type { InstallPlan } from "./mcp-install-plan.js";
3
+ import type { ClientDetection } from "./mcp-config-paths.js";
4
+ /** Shown wherever we direct users to obtain an API key (matches key-issued email sender). */
5
+ export declare const API_KEY_EMAIL_HINT = "If you requested a key already, check your inbox for mail from RiskModels (service@riskmodels.app).";
6
+ export declare function printInstallSuccessHuman(opts: {
7
+ isFirstInstall: boolean;
8
+ writes: ConfigWriteResult[];
9
+ sharedConfigWrite: SharedConfigWriteResult;
10
+ connectionTest: {
11
+ ok: boolean;
12
+ endpoint: string;
13
+ message: string;
14
+ };
15
+ firstPrompt: string;
16
+ hadErrors: boolean;
17
+ /** When Claude CLI is detected, suggest native `claude mcp` as an alternative. */
18
+ showClaudeCodeMcpTip?: boolean;
19
+ }): void;
20
+ export declare function printInstallDryRunHuman(opts: {
21
+ plans: InstallPlan[];
22
+ willStoreSharedKey?: boolean;
23
+ firstPrompt: string;
24
+ }): void;
25
+ export declare function printInstallMissingKeyHuman(): void;
26
+ export declare function printUninstallSuccessHuman(removals: ConfigWriteResult[]): void;
27
+ /** Human-readable detection list for uninstall dry-run (before removals computed). */
28
+ export declare function printUninstallPlannedHuman(detections: ClientDetection[]): void;
@@ -0,0 +1,224 @@
1
+ import chalk from "chalk";
2
+ import { platform } from "node:os";
3
+ import { configPath } from "./config.js";
4
+ import { formatCliVersionLabel } from "./cli-version.js";
5
+ import { abbreviatePath } from "./path-display.js";
6
+ const BULLET = "•";
7
+ /** Shown wherever we direct users to obtain an API key (matches key-issued email sender). */
8
+ export const API_KEY_EMAIL_HINT = "If you requested a key already, check your inbox for mail from RiskModels (service@riskmodels.app).";
9
+ function logRiskmodelsCliFooter(phase) {
10
+ logLine("");
11
+ logLine(chalk.bold("riskmodels CLI (still on PATH):"));
12
+ logLine(chalk.dim(phase === "dry-run"
13
+ ? "Uninstalling MCP only edits AI client configs; it does not remove the riskmodels terminal command."
14
+ : 'The RiskModels MCP integration was removed from the configs above. The "riskmodels" terminal command was not.'));
15
+ logLine(chalk.dim("To remove the CLI, use what matches how you installed it:"));
16
+ if (platform() !== "win32") {
17
+ logLine(chalk.dim(` ${BULLET} Homebrew (macOS / Linux): brew uninstall riskmodels`));
18
+ }
19
+ logLine(chalk.dim(` ${BULLET} npm (any OS): npm uninstall -g riskmodels`));
20
+ if (platform() === "win32") {
21
+ logLine(chalk.dim(` ${BULLET} Package manager installs (winget / Chocolatey / Scoop / etc.): use that tool's uninstall`));
22
+ }
23
+ logLine("");
24
+ logLine(chalk.dim(`riskmodels CLI ${formatCliVersionLabel()}`));
25
+ }
26
+ function logLine(line = "") {
27
+ console.log(line);
28
+ }
29
+ function formatBackupBullets(paths) {
30
+ return paths
31
+ .filter(Boolean)
32
+ .sort()
33
+ .map((p) => `${BULLET} ${abbreviatePath(p)}`);
34
+ }
35
+ export function printInstallSuccessHuman(opts) {
36
+ const { isFirstInstall, writes, sharedConfigWrite, connectionTest, firstPrompt, hadErrors, showClaudeCodeMcpTip, } = opts;
37
+ if (isFirstInstall) {
38
+ logLine("");
39
+ logLine(chalk.bold("👋 Welcome to RiskModels MCP!"));
40
+ logLine("");
41
+ logLine("This one-time setup connects your AI coding tools (Claude, Cursor, etc.)");
42
+ logLine("to real-time portfolio risk snapshots, hedge ratios, and factor models.");
43
+ logLine("");
44
+ }
45
+ logLine("");
46
+ if (hadErrors) {
47
+ logLine(chalk.yellow.bold("⚠ RiskModels MCP install finished with errors"));
48
+ }
49
+ else {
50
+ logLine(chalk.green.bold("✅ RiskModels MCP install completed"));
51
+ }
52
+ logLine("");
53
+ logLine(chalk.bold("Installed / updated in:"));
54
+ const installed = writes.filter((w) => w.action === "written");
55
+ if (installed.length === 0) {
56
+ logLine(` ${chalk.dim("(no client MCP configs were modified — check detection / permissions)")}`);
57
+ }
58
+ else {
59
+ for (const w of installed) {
60
+ logLine(` ${BULLET} ${w.label}`);
61
+ }
62
+ }
63
+ const skipped = writes.filter((w) => w.action === "skipped" && w.client !== "vscode");
64
+ if (skipped.length > 0) {
65
+ logLine("");
66
+ logLine(chalk.dim("Skipped (no change):"));
67
+ for (const w of skipped) {
68
+ logLine(` ${BULLET} ${w.label} — ${chalk.dim(w.message)}`);
69
+ }
70
+ }
71
+ const vsSkipped = writes.find((w) => w.action === "skipped" && w.client === "vscode");
72
+ if (vsSkipped) {
73
+ logLine("");
74
+ logLine(chalk.dim(`VS Code: ${vsSkipped.message}`));
75
+ }
76
+ const backupPaths = [];
77
+ if (sharedConfigWrite.backupPath)
78
+ backupPaths.push(sharedConfigWrite.backupPath);
79
+ for (const w of writes) {
80
+ if (w.backupPath)
81
+ backupPaths.push(w.backupPath);
82
+ }
83
+ logLine("");
84
+ logLine(chalk.bold("Backups created for safety:"));
85
+ if (backupPaths.length === 0) {
86
+ logLine(` ${chalk.dim("(none — configs were newly created)")}`);
87
+ }
88
+ else {
89
+ for (const line of formatBackupBullets(backupPaths))
90
+ logLine(` ${line}`);
91
+ }
92
+ logLine("");
93
+ if (connectionTest.ok) {
94
+ logLine(`${chalk.bold("Connection test:")} OK (${chalk.cyan(connectionTest.endpoint)})`);
95
+ }
96
+ else {
97
+ logLine(`${chalk.bold("Connection test:")} ${chalk.red("FAILED")} ${chalk.dim(`(${connectionTest.endpoint})`)}`);
98
+ logLine(chalk.dim(` ${connectionTest.message}`));
99
+ logLine(chalk.dim("Tip: check network/VPN, apiBaseUrl in shared config if non-default, corporate proxy, then run: riskmodels health"));
100
+ }
101
+ if (showClaudeCodeMcpTip) {
102
+ logLine("");
103
+ logLine("Claude Code users: you can also run");
104
+ logLine(chalk.dim(" claude mcp add riskmodels npx -y @riskmodels/mcp"));
105
+ }
106
+ if (!hadErrors) {
107
+ logLine("");
108
+ logLine(`${chalk.bold("Next step:")} Restart your AI clients, then try:`);
109
+ logLine("");
110
+ logLine(`"${firstPrompt}"`);
111
+ logLine("");
112
+ }
113
+ else {
114
+ logLine("");
115
+ const failed = writes.filter((w) => w.action === "error");
116
+ if (failed.length > 0) {
117
+ logLine(chalk.bold("Errors:"));
118
+ for (const w of failed) {
119
+ logLine(` ${BULLET} ${w.label}: ${chalk.red(w.message)}`);
120
+ }
121
+ logLine("");
122
+ }
123
+ }
124
+ logLine(chalk.dim(`riskmodels CLI ${formatCliVersionLabel()}`));
125
+ }
126
+ export function printInstallDryRunHuman(opts) {
127
+ logLine("");
128
+ logLine(chalk.bold("Dry run — planned RiskModels MCP install"));
129
+ logLine("");
130
+ logLine(chalk.bold("Clients to merge into:"));
131
+ for (const p of opts.plans) {
132
+ const pathPart = p.configPath ? chalk.dim(` — ${abbreviatePath(p.configPath)}`) : "";
133
+ logLine(` ${BULLET} ${p.label}${pathPart}`);
134
+ }
135
+ logLine("");
136
+ const cfgAbs = configPath();
137
+ const cfgShort = abbreviatePath(cfgAbs);
138
+ if (opts.willStoreSharedKey) {
139
+ logLine(chalk.dim(`A shared API key file would be created or updated at ${cfgShort}`));
140
+ if (cfgShort !== cfgAbs)
141
+ logLine(chalk.dim(` (full path: ${cfgAbs})`));
142
+ logLine(chalk.dim("(with backup if replacing)."));
143
+ }
144
+ else {
145
+ logLine(chalk.dim("No API key resolved — rerun with --api-key or set RISKMODELS_API_KEY after obtaining a key."));
146
+ logLine(chalk.dim(API_KEY_EMAIL_HINT));
147
+ }
148
+ logLine("");
149
+ logLine(chalk.dim("Rerun without --dry-run after you are happy with this plan."));
150
+ logLine("");
151
+ logLine(`${chalk.bold("First prompt to paste into your AI client:")} "${opts.firstPrompt}"`);
152
+ logLine("");
153
+ logLine(chalk.dim(`riskmodels CLI ${formatCliVersionLabel()}`));
154
+ }
155
+ export function printInstallMissingKeyHuman() {
156
+ logLine("");
157
+ logLine(chalk.red.bold("✗ Missing RiskModels API key"));
158
+ logLine("");
159
+ logLine(`Get a key: ${chalk.cyan("https://riskmodels.app/get-key")}`);
160
+ logLine("");
161
+ logLine(chalk.dim(API_KEY_EMAIL_HINT));
162
+ logLine("");
163
+ logLine(chalk.dim("Pass --api-key, set RISKMODELS_API_KEY, or rerun without --yes to enter it interactively."));
164
+ logLine("");
165
+ logLine(chalk.dim(`riskmodels CLI ${formatCliVersionLabel()}`));
166
+ }
167
+ export function printUninstallSuccessHuman(removals) {
168
+ logLine("");
169
+ logLine(chalk.green.bold("✅ RiskModels MCP uninstall completed"));
170
+ logLine("");
171
+ logLine(chalk.bold("Updates to MCP configs:"));
172
+ logLine("");
173
+ for (const r of removals) {
174
+ const bullet = `${BULLET} ${r.label}`;
175
+ const pathSuffix = r.configPath !== undefined ? chalk.dim(` — ${abbreviatePath(r.configPath)}`) : "";
176
+ if (r.action === "written") {
177
+ logLine(` ${bullet}${pathSuffix}`);
178
+ logLine(chalk.green.dim(` ${r.message}`));
179
+ if (r.backupPath)
180
+ logLine(chalk.dim(` backup: ${abbreviatePath(r.backupPath)}`));
181
+ }
182
+ else if (r.action === "skipped") {
183
+ logLine(` ${bullet}${pathSuffix}`);
184
+ logLine(chalk.dim(` (skipped — ${r.message})`));
185
+ }
186
+ else if (r.action === "error") {
187
+ logLine(` ${bullet}${pathSuffix}`);
188
+ logLine(chalk.red(` error: ${r.message}`));
189
+ }
190
+ }
191
+ logLine("");
192
+ const sharedAbs = configPath();
193
+ const sharedShort = abbreviatePath(sharedAbs);
194
+ logLine(`${chalk.bold("Shared API key / billing profile:")} unchanged ${chalk.dim(`(${sharedShort})`)}`);
195
+ if (sharedShort !== sharedAbs) {
196
+ logLine(chalk.dim(` (full path: ${sharedAbs})`));
197
+ }
198
+ logLine(chalk.dim("Only the riskmodels MCP server block was removed from each AI client config above; your key file was not modified."));
199
+ const backupPaths = removals.filter((r) => r.backupPath).map((r) => r.backupPath);
200
+ if (backupPaths.length > 0) {
201
+ logLine("");
202
+ logLine(chalk.bold("Backups stored:"));
203
+ for (const line of formatBackupBullets(backupPaths))
204
+ logLine(` ${line}`);
205
+ }
206
+ logRiskmodelsCliFooter("done");
207
+ }
208
+ /** Human-readable detection list for uninstall dry-run (before removals computed). */
209
+ export function printUninstallPlannedHuman(detections) {
210
+ logLine("");
211
+ logLine(chalk.bold("Dry run — RiskModels MCP would be inspected on:"));
212
+ logLine("");
213
+ for (const d of detections) {
214
+ const p = d.configPath ? chalk.dim(` — ${abbreviatePath(d.configPath)}`) : "";
215
+ logLine(` ${BULLET} ${d.label}${p}`);
216
+ }
217
+ logLine("");
218
+ const sharedAbs = configPath();
219
+ const sharedShort = abbreviatePath(sharedAbs);
220
+ logLine(chalk.dim(`Only the "riskmodels" MCP server block would be removed; ${sharedShort} would not be touched.`));
221
+ if (sharedShort !== sharedAbs)
222
+ logLine(chalk.dim(` (full path: ${sharedAbs})`));
223
+ logRiskmodelsCliFooter("dry-run");
224
+ }
@@ -1,4 +1,5 @@
1
1
  import type { ClientDetection } from "./mcp-config-paths.js";
2
+ import { type McpTransport } from "./mcp-install-plan.js";
2
3
  export interface ConfigWriteResult {
3
4
  client: string;
4
5
  label: string;
@@ -11,6 +12,7 @@ export interface SafeWriteOptions {
11
12
  apiKey?: string;
12
13
  embedKey?: boolean;
13
14
  apiBaseUrl?: string;
15
+ transport?: McpTransport;
14
16
  now?: Date;
15
17
  }
16
18
  export interface SharedConfigWriteResult {
@@ -23,6 +25,16 @@ export declare function mergeCodexTomlConfig(existingText: string, mcpServer: un
23
25
  export declare function validateRiskmodelsToml(text: string): void;
24
26
  export declare function writeSharedApiKey(apiKey: string, apiBaseUrl?: string, now?: Date): Promise<SharedConfigWriteResult>;
25
27
  export declare function installMcpConfig(detection: ClientDetection, opts?: SafeWriteOptions): Promise<ConfigWriteResult>;
28
+ /**
29
+ * Register the hosted endpoint with Claude Code via its own CLI
30
+ * (`claude mcp add --transport http …`) instead of writing Desktop JSON —
31
+ * Claude Code reads its own config, not claude_desktop_config.json. Idempotent:
32
+ * removes any existing `riskmodels` entry first so re-running doesn't error on
33
+ * "already exists". Used for the remote transport when the `claude` CLI is present.
34
+ */
35
+ export declare function installClaudeCodeRemote(detection: ClientDetection, opts?: {
36
+ apiKey?: string;
37
+ }): ConfigWriteResult;
26
38
  export declare function removeJsonMcpConfig(existingText: string): {
27
39
  text: string;
28
40
  removed: boolean;
@@ -1,7 +1,8 @@
1
1
  import { mkdir, readFile, writeFile, copyFile } from "node:fs/promises";
2
+ import { spawnSync } from "node:child_process";
2
3
  import path from "node:path";
3
4
  import { configPath, DEFAULT_API_BASE, loadConfig } from "./config.js";
4
- import { defaultMcpServerConfig } from "./mcp-install-plan.js";
5
+ import { claudeCodeRemoteAddArgs, mcpServerConfigFor, } from "./mcp-install-plan.js";
5
6
  const RISKMODELS_SERVER_NAME = "riskmodels";
6
7
  const TOML_MANAGED_HEADER = "# RiskModels MCP (managed by riskmodels CLI)";
7
8
  function timestamp(date = new Date()) {
@@ -165,7 +166,7 @@ export async function installMcpConfig(detection, opts = {}) {
165
166
  }
166
167
  try {
167
168
  const { exists, text } = await readIfExists(detection.configPath);
168
- const mcpServer = defaultMcpServerConfig(opts.apiKey, !!opts.embedKey);
169
+ const mcpServer = mcpServerConfigFor(opts.transport ?? "remote", opts.apiKey, !!opts.embedKey);
169
170
  const nextText = detection.client === "codex"
170
171
  ? mergeCodexTomlConfig(text, mcpServer)
171
172
  : mergeJsonMcpConfig(text, mcpServer);
@@ -189,6 +190,42 @@ export async function installMcpConfig(detection, opts = {}) {
189
190
  };
190
191
  }
191
192
  }
193
+ /**
194
+ * Register the hosted endpoint with Claude Code via its own CLI
195
+ * (`claude mcp add --transport http …`) instead of writing Desktop JSON —
196
+ * Claude Code reads its own config, not claude_desktop_config.json. Idempotent:
197
+ * removes any existing `riskmodels` entry first so re-running doesn't error on
198
+ * "already exists". Used for the remote transport when the `claude` CLI is present.
199
+ */
200
+ export function installClaudeCodeRemote(detection, opts = {}) {
201
+ const base = {
202
+ client: detection.client,
203
+ label: detection.label,
204
+ configPath: detection.configPath,
205
+ };
206
+ // Best-effort removal of a prior entry — ignore failure (none registered yet).
207
+ spawnSync("claude", ["mcp", "remove", "--scope", "user", "riskmodels"], { stdio: "ignore" });
208
+ const res = spawnSync("claude", claudeCodeRemoteAddArgs(opts.apiKey), {
209
+ stdio: "pipe",
210
+ encoding: "utf8",
211
+ });
212
+ if (res.error) {
213
+ return { ...base, action: "error", message: `Failed to run \`claude\`: ${res.error.message}` };
214
+ }
215
+ if (res.status === 0) {
216
+ return {
217
+ ...base,
218
+ action: "written",
219
+ message: "Registered with Claude Code via `claude mcp add --transport http` (user scope).",
220
+ };
221
+ }
222
+ const detail = (res.stderr || res.stdout || "").trim();
223
+ return {
224
+ ...base,
225
+ action: "error",
226
+ message: detail || `\`claude mcp add\` exited with status ${res.status ?? "unknown"}.`,
227
+ };
228
+ }
192
229
  export function removeJsonMcpConfig(existingText) {
193
230
  const parsed = existingText.trim() ? JSON.parse(existingText) : {};
194
231
  assertPlainObject(parsed, "MCP config");
@@ -8,9 +8,39 @@ export interface InstallPlan {
8
8
  notes: string[];
9
9
  mcpServer: unknown;
10
10
  }
11
+ /** Hosted MCP endpoint (Streamable HTTP). Key-first auth, no npm publish needed. */
12
+ export declare const REMOTE_MCP_URL = "https://riskmodels.app/api/mcp/sse";
13
+ export type McpTransport = "remote" | "local";
14
+ /**
15
+ * Local stdio path: runs the published `@riskmodels/mcp` server via npx. The key
16
+ * is read from the shared config by default; `--embed-key` writes it into env.
17
+ */
11
18
  export declare function defaultMcpServerConfig(apiKey?: string, embedKey?: boolean): unknown;
19
+ /**
20
+ * Remote path (default): bridges a stdio client to the hosted endpoint via
21
+ * `mcp-remote`. The key rides in an `Authorization: Bearer` header — kept out of
22
+ * the URL so it never lands in server access logs or referrers. Works today with
23
+ * no npm publish dependency.
24
+ */
25
+ export declare function remoteMcpServerConfig(apiKey?: string): unknown;
26
+ /**
27
+ * Native `claude mcp add` args for Claude Code — registers the hosted endpoint
28
+ * over HTTP transport at user scope (no mcp-remote shim needed; Claude Code
29
+ * speaks Streamable HTTP directly).
30
+ */
31
+ export declare function claudeCodeRemoteAddArgs(apiKey?: string): string[];
32
+ export declare function mcpServerConfigFor(transport: McpTransport, apiKey?: string, embedKey?: boolean): unknown;
33
+ /** RiskModels keys are `rm_agent_live_…` / `rm_…`. Cheap client-side sanity check. */
34
+ export declare function looksLikeRiskmodelsKey(key: string): boolean;
35
+ /**
36
+ * Mask `Authorization: Bearer <token>` strings anywhere in a config tree.
37
+ * `redactJson` only masks values whose *key name* matches (e.g. `env.RISKMODELS_API_KEY`);
38
+ * the remote header lives inside an `args` string, so it needs this pass too.
39
+ */
40
+ export declare function redactBearerStrings(value: unknown): unknown;
12
41
  export declare function buildInstallPlans(detections: ClientDetection[], opts: {
13
42
  apiKey?: string;
14
43
  embedKey?: boolean;
44
+ transport: McpTransport;
15
45
  }): InstallPlan[];
16
46
  export declare function firstPrompt(): string;
@@ -1,4 +1,10 @@
1
- import { redactJson } from "./redact.js";
1
+ import { redactJson, redactSecret } from "./redact.js";
2
+ /** Hosted MCP endpoint (Streamable HTTP). Key-first auth, no npm publish needed. */
3
+ export const REMOTE_MCP_URL = "https://riskmodels.app/api/mcp/sse";
4
+ /**
5
+ * Local stdio path: runs the published `@riskmodels/mcp` server via npx. The key
6
+ * is read from the shared config by default; `--embed-key` writes it into env.
7
+ */
2
8
  export function defaultMcpServerConfig(apiKey, embedKey = false) {
3
9
  return {
4
10
  command: "npx",
@@ -12,16 +18,76 @@ export function defaultMcpServerConfig(apiKey, embedKey = false) {
12
18
  : {}),
13
19
  };
14
20
  }
21
+ /**
22
+ * Remote path (default): bridges a stdio client to the hosted endpoint via
23
+ * `mcp-remote`. The key rides in an `Authorization: Bearer` header — kept out of
24
+ * the URL so it never lands in server access logs or referrers. Works today with
25
+ * no npm publish dependency.
26
+ */
27
+ export function remoteMcpServerConfig(apiKey) {
28
+ const args = ["-y", "mcp-remote", REMOTE_MCP_URL];
29
+ if (apiKey) {
30
+ args.push("--header", `Authorization: Bearer ${apiKey}`);
31
+ }
32
+ return { command: "npx", args };
33
+ }
34
+ /**
35
+ * Native `claude mcp add` args for Claude Code — registers the hosted endpoint
36
+ * over HTTP transport at user scope (no mcp-remote shim needed; Claude Code
37
+ * speaks Streamable HTTP directly).
38
+ */
39
+ export function claudeCodeRemoteAddArgs(apiKey) {
40
+ const args = ["mcp", "add", "--scope", "user", "--transport", "http", "riskmodels", REMOTE_MCP_URL];
41
+ if (apiKey) {
42
+ args.push("--header", `Authorization: Bearer ${apiKey}`);
43
+ }
44
+ return args;
45
+ }
46
+ export function mcpServerConfigFor(transport, apiKey, embedKey = false) {
47
+ return transport === "remote"
48
+ ? remoteMcpServerConfig(apiKey)
49
+ : defaultMcpServerConfig(apiKey, embedKey);
50
+ }
51
+ /** RiskModels keys are `rm_agent_live_…` / `rm_…`. Cheap client-side sanity check. */
52
+ export function looksLikeRiskmodelsKey(key) {
53
+ return /^rm_[A-Za-z0-9_]+$/.test(key.trim());
54
+ }
55
+ /**
56
+ * Mask `Authorization: Bearer <token>` strings anywhere in a config tree.
57
+ * `redactJson` only masks values whose *key name* matches (e.g. `env.RISKMODELS_API_KEY`);
58
+ * the remote header lives inside an `args` string, so it needs this pass too.
59
+ */
60
+ export function redactBearerStrings(value) {
61
+ if (Array.isArray(value))
62
+ return value.map(redactBearerStrings);
63
+ if (typeof value === "string") {
64
+ const m = value.match(/^(Authorization:\s*Bearer\s+)(.+)$/i);
65
+ return m ? `${m[1]}${redactSecret(m[2])}` : value;
66
+ }
67
+ if (value && typeof value === "object") {
68
+ const out = {};
69
+ for (const [k, v] of Object.entries(value))
70
+ out[k] = redactBearerStrings(v);
71
+ return out;
72
+ }
73
+ return value;
74
+ }
15
75
  export function buildInstallPlans(detections, opts) {
16
- return detections.map((detection) => ({
17
- client: detection.client,
18
- label: detection.label,
19
- status: detection.status,
20
- mode: detection.mode,
21
- configPath: detection.configPath,
22
- notes: detection.notes,
23
- mcpServer: redactJson(defaultMcpServerConfig(opts.apiKey, opts.embedKey)),
24
- }));
76
+ return detections.map((detection) => {
77
+ const claudeCodeNative = opts.transport === "remote" && detection.client === "claude" && detection.commandAvailable;
78
+ const mcpServer = redactBearerStrings(redactJson(mcpServerConfigFor(opts.transport, opts.apiKey, opts.embedKey)));
79
+ return {
80
+ client: detection.client,
81
+ label: detection.label,
82
+ status: detection.status,
83
+ mode: claudeCodeNative ? "command" : detection.mode,
84
+ configPath: detection.configPath,
85
+ notes: claudeCodeNative
86
+ ? [...detection.notes, "Remote: will register via `claude mcp add --transport http` (user scope)."]
87
+ : detection.notes,
88
+ mcpServer,
89
+ };
90
+ });
25
91
  }
26
92
  export function firstPrompt() {
27
93
  return "Compare AAPL and NVDA using RiskModels. What am I really betting on?";
@@ -0,0 +1,2 @@
1
+ /** Shorten home-prefixed paths for readability (~ on POSIX; Windows uses same tilde-in-shell convention in Git Bash). */
2
+ export declare function abbreviatePath(absolutePath: string): string;
@@ -0,0 +1,18 @@
1
+ import { homedir } from "node:os";
2
+ /** Shorten home-prefixed paths for readability (~ on POSIX; Windows uses same tilde-in-shell convention in Git Bash). */
3
+ export function abbreviatePath(absolutePath) {
4
+ if (!absolutePath.trim())
5
+ return absolutePath;
6
+ const home = homedir();
7
+ if (absolutePath === home)
8
+ return "~";
9
+ const sep = absolutePath.includes("\\") ? "\\" : "/";
10
+ const homeNorm = home.replace(/\\/g, "/");
11
+ const pathNorm = absolutePath.replace(/\\/g, "/");
12
+ if (pathNorm === homeNorm || pathNorm === `${homeNorm}/`)
13
+ return "~";
14
+ if (pathNorm.startsWith(`${homeNorm}/`)) {
15
+ return `~/${pathNorm.slice(homeNorm.length + 1)}`;
16
+ }
17
+ return absolutePath;
18
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "riskmodels",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "RiskModels CLI — REST API, SQL query, schema, billing, and agent manifests",
5
5
  "type": "module",
6
6
  "bin": {