riskmodels 2.0.0 → 2.0.2

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
6
  import { buildInstallPlans, firstPrompt } 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
10
  import { 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,14 @@ 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
67
  .option("--embed-key", "Explicitly embed the API key in MCP config env (not recommended)")
65
- .option("--json", "JSON output")
68
+ .option("--json", "Structured JSON instead of formatted text (matches historical output shape)")
66
69
  .action(async (opts, cmd) => {
67
70
  const json = opts.json || cmd.optsWithGlobals().json || false;
68
71
  let clients;
@@ -96,20 +99,34 @@ export function installCommand() {
96
99
  : "Install complete. Restart your MCP client, then try the first prompt.",
97
100
  };
98
101
  if (dryRun) {
99
- printResults(output, json);
100
- if (!json) {
101
- console.error(chalk.green(`First prompt to try: "${firstPrompt()}"`));
102
+ if (json) {
103
+ warnIfGlobalJsonBeforeSubcommand("install");
104
+ printResults(output, json);
105
+ }
106
+ else {
107
+ printInstallDryRunHuman({
108
+ plans,
109
+ willStoreSharedKey: !!apiKey && source !== configPath(),
110
+ firstPrompt: firstPrompt(),
111
+ });
102
112
  }
103
113
  return;
104
114
  }
105
115
  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);
116
+ if (json) {
117
+ warnIfGlobalJsonBeforeSubcommand("install");
118
+ printResults({
119
+ ...output,
120
+ error: "No RiskModels API key found. Get one at https://riskmodels.app/get-key or pass --api-key.",
121
+ }, json);
122
+ }
123
+ else {
124
+ printInstallMissingKeyHuman();
125
+ }
110
126
  process.exitCode = 1;
111
127
  return;
112
128
  }
129
+ const sharedBefore = await sharedRiskmodelsConfigExists();
113
130
  const sharedConfigWrite = await writeSharedApiKey(apiKey, existingConfig?.apiBaseUrl ?? DEFAULT_API_BASE);
114
131
  const writes = await Promise.all(detections.map((detection) => installMcpConfig(detection, {
115
132
  apiKey,
@@ -123,14 +140,24 @@ export function installCommand() {
123
140
  writes,
124
141
  connectionTest: test,
125
142
  };
126
- printResults(result, json);
143
+ if (json) {
144
+ warnIfGlobalJsonBeforeSubcommand("install");
145
+ printResults(result, json);
146
+ }
147
+ else {
148
+ printInstallSuccessHuman({
149
+ isFirstInstall: !sharedBefore,
150
+ writes,
151
+ sharedConfigWrite,
152
+ connectionTest: test,
153
+ firstPrompt: firstPrompt(),
154
+ hadErrors: writes.some((w) => w.action === "error") || !test.ok,
155
+ showClaudeCodeMcpTip: detections.some((d) => d.client === "claude" && d.commandAvailable),
156
+ });
157
+ }
127
158
  if (writes.some((write) => write.action === "error") || !test.ok) {
128
159
  process.exitCode = 1;
129
160
  return;
130
161
  }
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
162
  });
136
163
  }
@@ -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
+ }
@@ -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.0.2",
4
4
  "description": "RiskModels CLI — REST API, SQL query, schema, billing, and agent manifests",
5
5
  "type": "module",
6
6
  "bin": {