riskmodels 2.0.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.
Files changed (72) hide show
  1. package/README.md +104 -0
  2. package/dist/commands/agent.d.ts +2 -0
  3. package/dist/commands/agent.js +76 -0
  4. package/dist/commands/balance.d.ts +2 -0
  5. package/dist/commands/balance.js +25 -0
  6. package/dist/commands/batch.d.ts +2 -0
  7. package/dist/commands/batch.js +76 -0
  8. package/dist/commands/config.d.ts +2 -0
  9. package/dist/commands/config.js +152 -0
  10. package/dist/commands/correlation.d.ts +2 -0
  11. package/dist/commands/correlation.js +80 -0
  12. package/dist/commands/doctor.d.ts +2 -0
  13. package/dist/commands/doctor.js +51 -0
  14. package/dist/commands/estimate.d.ts +2 -0
  15. package/dist/commands/estimate.js +40 -0
  16. package/dist/commands/health.d.ts +2 -0
  17. package/dist/commands/health.js +60 -0
  18. package/dist/commands/install.d.ts +2 -0
  19. package/dist/commands/install.js +136 -0
  20. package/dist/commands/l3.d.ts +2 -0
  21. package/dist/commands/l3.js +33 -0
  22. package/dist/commands/macro-factors.d.ts +2 -0
  23. package/dist/commands/macro-factors.js +39 -0
  24. package/dist/commands/manifest.d.ts +2 -0
  25. package/dist/commands/manifest.js +270 -0
  26. package/dist/commands/mcp-config.d.ts +2 -0
  27. package/dist/commands/mcp-config.js +95 -0
  28. package/dist/commands/mcp.d.ts +12 -0
  29. package/dist/commands/mcp.js +59 -0
  30. package/dist/commands/metrics.d.ts +2 -0
  31. package/dist/commands/metrics.js +29 -0
  32. package/dist/commands/portfolio.d.ts +2 -0
  33. package/dist/commands/portfolio.js +80 -0
  34. package/dist/commands/query.d.ts +2 -0
  35. package/dist/commands/query.js +90 -0
  36. package/dist/commands/rankings.d.ts +2 -0
  37. package/dist/commands/rankings.js +108 -0
  38. package/dist/commands/returns.d.ts +2 -0
  39. package/dist/commands/returns.js +77 -0
  40. package/dist/commands/schema.d.ts +2 -0
  41. package/dist/commands/schema.js +77 -0
  42. package/dist/commands/status.d.ts +2 -0
  43. package/dist/commands/status.js +25 -0
  44. package/dist/commands/tickers.d.ts +2 -0
  45. package/dist/commands/tickers.js +36 -0
  46. package/dist/commands/uninstall.d.ts +2 -0
  47. package/dist/commands/uninstall.js +45 -0
  48. package/dist/index.d.ts +2 -0
  49. package/dist/index.js +67 -0
  50. package/dist/lib/api-client.d.ts +22 -0
  51. package/dist/lib/api-client.js +100 -0
  52. package/dist/lib/api-url.d.ts +2 -0
  53. package/dist/lib/api-url.js +8 -0
  54. package/dist/lib/config.d.ts +22 -0
  55. package/dist/lib/config.js +46 -0
  56. package/dist/lib/credentials.d.ts +26 -0
  57. package/dist/lib/credentials.js +70 -0
  58. package/dist/lib/display.d.ts +1 -0
  59. package/dist/lib/display.js +19 -0
  60. package/dist/lib/mcp-config-paths.d.ts +16 -0
  61. package/dist/lib/mcp-config-paths.js +100 -0
  62. package/dist/lib/mcp-config-writer.d.ts +36 -0
  63. package/dist/lib/mcp-config-writer.js +265 -0
  64. package/dist/lib/mcp-install-plan.d.ts +16 -0
  65. package/dist/lib/mcp-install-plan.js +28 -0
  66. package/dist/lib/oauth.d.ts +2 -0
  67. package/dist/lib/oauth.js +47 -0
  68. package/dist/lib/redact.d.ts +2 -0
  69. package/dist/lib/redact.js +28 -0
  70. package/dist/lib/sql-validation.d.ts +9 -0
  71. package/dist/lib/sql-validation.js +21 -0
  72. package/package.json +41 -0
package/README.md ADDED
@@ -0,0 +1,104 @@
1
+ # riskmodels
2
+
3
+ Command-line interface for [RiskModels](https://riskmodels.app): call the REST API (metrics, batch, portfolio, returns, rankings, etc.), run billed SQL queries, explore schema in direct (Supabase) mode, check balance, and export agent tool manifests. Same contract as the Python SDK: **POST /decompose** returns variance shares and ETF hedge ratios per ticker.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g riskmodels
9
+ ```
10
+
11
+ Agent-first installer preview:
12
+
13
+ ```bash
14
+ RISKMODELS_API_KEY=rm_agent_live_... npx riskmodels install
15
+ ```
16
+
17
+ Local development can run the same flow with:
18
+
19
+ ```bash
20
+ cd cli
21
+ npm run build
22
+ node dist/index.js install --dry-run
23
+ ```
24
+
25
+ ## Authentication
26
+
27
+ - **API key (recommended):** `riskmodels config init` (billed mode) or `riskmodels config set apiKey <rm_agent_...>`.
28
+ - **OAuth client credentials:** `riskmodels config set clientId …` and `config set clientSecret …`, or set `RISKMODELS_CLIENT_ID` / `RISKMODELS_CLIENT_SECRET` (optional `RISKMODELS_OAUTH_SCOPE`; default matches the Python SDK).
29
+ - **Environment:** `RISKMODELS_API_KEY` works without a config file for REST commands.
30
+ - **Direct (Supabase) mode** is for `query` + `schema` only. REST analytics need an API key or OAuth (config or env).
31
+
32
+ Base URL: stored as `apiBaseUrl` (default `https://riskmodels.app`). The CLI calls paths under `…/api/...` (same as `OPENAPI_SPEC.yaml`).
33
+
34
+ ## Quick start (billed / recommended)
35
+
36
+ ```bash
37
+ riskmodels config init
38
+ # Choose API Key mode and enter your rm_agent_* key
39
+
40
+ riskmodels health
41
+ riskmodels metrics NVDA
42
+ riskmodels query "SELECT ticker, company_name FROM ticker_metadata LIMIT 3"
43
+ riskmodels balance
44
+ ```
45
+
46
+ Config file: `~/.config/riskmodels/config.json`
47
+
48
+ ## Commands
49
+
50
+ | Command | Description |
51
+ |--------|-------------|
52
+ | `riskmodels config init \| set \| list` | API key, OAuth fields (`clientId`, `clientSecret`, `oauthScope`), `apiBaseUrl`, or Supabase (direct) |
53
+ | `riskmodels install` | Detect Claude, Cursor, Codex, and VS Code MCP targets, store the API key in shared config, merge MCP configs with backups, and run a connection test. |
54
+ | `riskmodels status` | Show shared config and MCP client detection status. |
55
+ | `riskmodels doctor` | Local install-readiness diagnostics: Node, npx, API credentials, and client detection. |
56
+ | `riskmodels uninstall` | Remove only the `riskmodels` MCP server block from supported client configs, with backups. |
57
+ | `riskmodels query "<sql>"` | SELECT only (billed → `POST /api/cli/query`, direct → Supabase `exec_sql`) |
58
+ | `riskmodels metrics <ticker>` | Latest snapshot (`GET /api/metrics/{ticker}`) |
59
+ | `riskmodels batch analyze` | `POST /api/batch/analyze` (`--tickers`, `--metrics`, `--years`) |
60
+ | `riskmodels portfolio risk-index` | `POST /api/portfolio/risk-index` (`--file` or `--stdin`) |
61
+ | `riskmodels returns ticker <SYMBOL>` | `GET /api/ticker-returns` — daily returns for both stocks and ETFs. Stocks include L1/L2/L3 hedge ratios + explained-risk columns; ETFs (e.g. `SPY`) return date/returns_gross/price_close (L* null). `stock` / `etf` subcommands are deprecated aliases. |
62
+ | `riskmodels l3 <ticker>` | `GET /api/l3-decomposition` |
63
+ | `riskmodels correlation post\|metrics` | `POST /api/correlation`, `GET /api/metrics/{ticker}/correlation` |
64
+ | `riskmodels macro-factors` | `GET /api/macro-factors` (daily macro returns, no ticker) |
65
+ | `riskmodels rankings snapshot\|badge\|top` | Rankings endpoints |
66
+ | `riskmodels tickers` | Universe search (`GET /api/tickers`, no auth) |
67
+ | `riskmodels health` | `GET /api/health` (no auth) |
68
+ | `riskmodels estimate` | `POST /api/estimate` (pre-flight cost) |
69
+ | `riskmodels schema` | PostgREST OpenAPI (direct mode only) |
70
+ | `riskmodels balance` | Account balance (`GET /api/balance`) |
71
+ | `riskmodels manifest [--format openai\|anthropic\|zed]` | Static tool manifest (no auth) |
72
+ | `riskmodels agent decompose\|monitor` | Shortcuts → batch analyze / metrics |
73
+
74
+ Global flag: `--json` for machine-readable output on supported commands.
75
+
76
+ ### MCP installer safety
77
+
78
+ `riskmodels install --dry-run` detects supported clients, prints target config paths, redacts secrets, and shows the exact `npx -y @riskmodels/mcp` server entry it will merge. `riskmodels install` performs the write with timestamped backups.
79
+
80
+ Default credential policy:
81
+
82
+ - Use `~/.config/riskmodels/config.json`.
83
+ - Do not embed API keys in MCP config files.
84
+ - `--embed-key` is explicit opt-in and redacted in dry-run output.
85
+ - Safe writes create timestamped backups, validate JSON/TOML before writing, run a connection test, and keep uninstall scoped to the `riskmodels` MCP server block.
86
+
87
+ ## Develop
88
+
89
+ ```bash
90
+ cd cli
91
+ npm install
92
+ npm run build
93
+ npm run install:global # npm link for local testing
94
+ ```
95
+
96
+ The repo root `npm run typecheck` includes `cli/src` (see `AGENTS.md`). Maintainer drift check: `npm run cli:openapi-check` at the repo root.
97
+
98
+ ## npm releases (maintainers)
99
+
100
+ Publishing (`npm publish`, version bumps, npm login / tokens) is **not** documented in this public README. It is maintained in the private **BWMACRO** monorepo at **`docs/RISKMODELS_CLI_NPM_PUBLISHING.md`** — open that file from your internal BWMACRO clone.
101
+
102
+ **Rule of thumb:** run publish commands only from **`cli/`**; the repo root package is the Next.js portal, not this CLI.
103
+
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`.
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function agentCommand(): Command;
@@ -0,0 +1,76 @@
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
+ const DOCS = "https://riskmodels.app/docs/api";
8
+ function parseTickers(s) {
9
+ return s
10
+ .split(/[\s,]+/)
11
+ .map((t) => t.trim())
12
+ .filter(Boolean);
13
+ }
14
+ export function agentCommand() {
15
+ const agent = new Command("agent").description("Shortcuts for portfolio-style workflows (wraps REST endpoints)");
16
+ agent
17
+ .command("decompose")
18
+ .description("Batch L3-style screen: POST /batch/analyze with full_metrics (+ hedge_ratios)")
19
+ .requiredOption("--tickers <symbols>", "Comma or space separated tickers (max 100)")
20
+ .option("--years <n>", "Years when returns blocks are requested", "1")
21
+ .action(async (opts, cmd) => {
22
+ const json = cmd.optsWithGlobals().json ?? false;
23
+ const cfg = await loadConfig();
24
+ const auth = requireResolvedAuth(cfg, chalk.yellow);
25
+ if (!auth)
26
+ return;
27
+ const tickers = parseTickers(opts.tickers);
28
+ if (tickers.length === 0 || tickers.length > 100) {
29
+ console.error(chalk.red("Provide 1–100 tickers."));
30
+ process.exitCode = 1;
31
+ return;
32
+ }
33
+ const years = parseInt(String(opts.years ?? "1"), 10) || 1;
34
+ try {
35
+ const { body, costUsd } = await apiFetchJson(auth, "POST", "/batch/analyze", {
36
+ jsonBody: {
37
+ tickers,
38
+ metrics: ["full_metrics", "hedge_ratios"],
39
+ years,
40
+ format: "json",
41
+ },
42
+ });
43
+ printResults(body, json);
44
+ if (!json && costUsd)
45
+ console.log(chalk.dim(`Cost: $${costUsd}`));
46
+ }
47
+ catch (e) {
48
+ console.error(chalk.red(e instanceof Error ? e.message : String(e)));
49
+ process.exitCode = 1;
50
+ }
51
+ });
52
+ agent
53
+ .command("monitor")
54
+ .description("Latest risk snapshot for one holding: GET /metrics/{ticker}")
55
+ .argument("<ticker>", "Symbol to poll")
56
+ .action(async (ticker, _opts, cmd) => {
57
+ const json = cmd.optsWithGlobals().json ?? false;
58
+ const cfg = await loadConfig();
59
+ const auth = requireResolvedAuth(cfg, chalk.yellow);
60
+ if (!auth)
61
+ return;
62
+ const enc = encodeURIComponent(ticker.trim());
63
+ try {
64
+ const { body, costUsd } = await apiFetchJson(auth, "GET", `/metrics/${enc}`);
65
+ printResults(body, json);
66
+ if (!json && costUsd)
67
+ console.log(chalk.dim(`Cost: $${costUsd}`));
68
+ }
69
+ catch (e) {
70
+ console.error(chalk.red(e instanceof Error ? e.message : String(e)));
71
+ process.exitCode = 1;
72
+ }
73
+ });
74
+ agent.addHelpText("after", `\n${chalk.dim(`More endpoints: ${DOCS} — or run \`riskmodels --help\` for the full CLI.`)}`);
75
+ return agent;
76
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function balanceCommand(): Command;
@@ -0,0 +1,25 @@
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 balanceCommand() {
8
+ return new Command("balance")
9
+ .description("Show account balance (billed credentials)")
10
+ .action(async (_opts, cmd) => {
11
+ const json = cmd.optsWithGlobals().json ?? false;
12
+ const cfg = await loadConfig();
13
+ const auth = requireResolvedAuth(cfg, chalk.yellow);
14
+ if (!auth)
15
+ return;
16
+ try {
17
+ const { body } = await apiFetchJson(auth, "GET", "/balance");
18
+ printResults(body, json);
19
+ }
20
+ catch (e) {
21
+ console.error(chalk.red(e instanceof Error ? e.message : String(e)));
22
+ process.exitCode = 1;
23
+ }
24
+ });
25
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function batchCommand(): Command;
@@ -0,0 +1,76 @@
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
+ const METRIC_CHOICES = ["returns", "l3_decomposition", "hedge_ratios", "full_metrics"];
8
+ function parseTickers(s) {
9
+ return s
10
+ .split(/[\s,]+/)
11
+ .map((t) => t.trim())
12
+ .filter(Boolean);
13
+ }
14
+ function parseMetrics(s) {
15
+ if (!s?.trim())
16
+ return ["full_metrics"];
17
+ const parts = s.split(/[\s,]+/).map((x) => x.trim());
18
+ for (const p of parts) {
19
+ if (!METRIC_CHOICES.includes(p)) {
20
+ throw new Error(`Invalid metric "${p}". Use: ${METRIC_CHOICES.join(", ")}`);
21
+ }
22
+ }
23
+ return parts;
24
+ }
25
+ export function batchCommand() {
26
+ const batch = new Command("batch").description("Multi-ticker batch analysis (POST /batch/analyze)");
27
+ batch
28
+ .command("analyze")
29
+ .description("Fetch metrics for up to 100 tickers")
30
+ .requiredOption("--tickers <symbols>", "Comma or space separated tickers, e.g. AAPL,MSFT,NVDA")
31
+ .option("--metrics <list>", `Comma-separated: ${METRIC_CHOICES.join(", ")} (default: full_metrics)`)
32
+ .option("--years <n>", "Years of history for returns / l3_decomposition", "1")
33
+ .option("--format <fmt>", "json | parquet | csv", "json")
34
+ .action(async (opts, cmd) => {
35
+ const json = cmd.optsWithGlobals().json ?? false;
36
+ const cfg = await loadConfig();
37
+ const auth = requireResolvedAuth(cfg, chalk.yellow);
38
+ if (!auth)
39
+ return;
40
+ let metrics;
41
+ try {
42
+ metrics = parseMetrics(opts.metrics);
43
+ }
44
+ catch (e) {
45
+ console.error(chalk.red(e instanceof Error ? e.message : String(e)));
46
+ process.exitCode = 1;
47
+ return;
48
+ }
49
+ const years = parseInt(String(opts.years ?? "1"), 10) || 1;
50
+ const format = (opts.format ?? "json");
51
+ const tickers = parseTickers(opts.tickers);
52
+ if (tickers.length === 0 || tickers.length > 100) {
53
+ console.error(chalk.red("Provide 1–100 tickers."));
54
+ process.exitCode = 1;
55
+ return;
56
+ }
57
+ if (format !== "json") {
58
+ console.error(chalk.red("CLI supports JSON responses only; omit --format or use --format json."));
59
+ process.exitCode = 1;
60
+ return;
61
+ }
62
+ try {
63
+ const { body, costUsd } = await apiFetchJson(auth, "POST", "/batch/analyze", {
64
+ jsonBody: { tickers, metrics, years, format: "json" },
65
+ });
66
+ printResults(body, json);
67
+ if (!json && costUsd)
68
+ console.log(chalk.dim(`Cost: $${costUsd}`));
69
+ }
70
+ catch (e) {
71
+ console.error(chalk.red(e instanceof Error ? e.message : String(e)));
72
+ process.exitCode = 1;
73
+ }
74
+ });
75
+ return batch;
76
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function configCommand(): Command;
@@ -0,0 +1,152 @@
1
+ import { Command } from "commander";
2
+ import inquirer from "inquirer";
3
+ import chalk from "chalk";
4
+ import { loadConfig, saveConfig, maskSecret, DEFAULT_API_BASE, } from "../lib/config.js";
5
+ import { printResults } from "../lib/display.js";
6
+ export function configCommand() {
7
+ const cmd = new Command("config").description("Manage CLI configuration (~/.config/riskmodels/config.json)");
8
+ cmd
9
+ .command("init")
10
+ .description("Interactive setup (billed API key or direct Supabase)")
11
+ .action(async () => {
12
+ const existing = await loadConfig();
13
+ const { mode } = await inquirer.prompt([
14
+ {
15
+ type: "list",
16
+ name: "mode",
17
+ message: "How should the CLI authenticate?",
18
+ choices: [
19
+ { name: "API Key (billed, recommended for production)", value: "billed" },
20
+ { name: "Service Role Key (direct Supabase, for development)", value: "direct" },
21
+ ],
22
+ default: existing?.mode ?? "billed",
23
+ },
24
+ ]);
25
+ if (mode === "billed") {
26
+ const answers = await inquirer.prompt([
27
+ {
28
+ type: "password",
29
+ name: "apiKey",
30
+ message: "RiskModels API key (rm_agent_...)",
31
+ mask: "*",
32
+ validate: (v) => (v?.trim() ? true : "API key is required"),
33
+ },
34
+ {
35
+ type: "input",
36
+ name: "apiBaseUrl",
37
+ message: "API base URL",
38
+ default: existing?.apiBaseUrl ?? DEFAULT_API_BASE,
39
+ validate: (v) => (v?.trim()?.startsWith("http") ? true : "Must be an https URL"),
40
+ },
41
+ ]);
42
+ const cfg = {
43
+ mode: "billed",
44
+ apiKey: answers.apiKey.trim(),
45
+ apiBaseUrl: answers.apiBaseUrl.replace(/\/$/, ""),
46
+ };
47
+ await saveConfig(cfg);
48
+ console.log(chalk.green("Saved billed mode configuration."));
49
+ }
50
+ else {
51
+ const answers = await inquirer.prompt([
52
+ {
53
+ type: "input",
54
+ name: "supabaseUrl",
55
+ message: "Supabase project URL",
56
+ default: existing?.supabaseUrl ?? "",
57
+ validate: (v) => v?.includes("supabase.co") ? true : "URL should contain supabase.co",
58
+ },
59
+ {
60
+ type: "password",
61
+ name: "serviceRoleKey",
62
+ message: "Supabase service role key (JWT, often starts with ey...)",
63
+ mask: "*",
64
+ validate: (v) => (v?.trim()?.startsWith("ey") ? true : "Key should start with ey"),
65
+ },
66
+ ]);
67
+ const cfg = {
68
+ mode: "direct",
69
+ supabaseUrl: answers.supabaseUrl.trim().replace(/\/$/, ""),
70
+ serviceRoleKey: answers.serviceRoleKey.trim(),
71
+ };
72
+ await saveConfig(cfg);
73
+ console.log(chalk.green("Saved direct (Supabase) configuration."));
74
+ }
75
+ });
76
+ cmd
77
+ .command("set")
78
+ .description("Set a config value")
79
+ .argument("<key>", "apiKey | apiBaseUrl | clientId | clientSecret | oauthScope | supabaseUrl | serviceRoleKey")
80
+ .argument("<value>", "value to store")
81
+ .action(async (key, value) => {
82
+ const allowed = new Set([
83
+ "apiKey",
84
+ "apiBaseUrl",
85
+ "clientId",
86
+ "clientSecret",
87
+ "oauthScope",
88
+ "supabaseUrl",
89
+ "serviceRoleKey",
90
+ ]);
91
+ if (!allowed.has(key)) {
92
+ console.error(chalk.red(`Unknown key: ${key}`));
93
+ process.exitCode = 1;
94
+ return;
95
+ }
96
+ const cfg = (await loadConfig()) ?? { mode: "billed" };
97
+ if (key === "apiKey") {
98
+ cfg.mode = "billed";
99
+ cfg.apiKey = value;
100
+ }
101
+ else if (key === "apiBaseUrl") {
102
+ cfg.apiBaseUrl = value.replace(/\/$/, "");
103
+ }
104
+ else if (key === "clientId") {
105
+ cfg.mode = "billed";
106
+ cfg.clientId = value;
107
+ }
108
+ else if (key === "clientSecret") {
109
+ cfg.mode = "billed";
110
+ cfg.clientSecret = value;
111
+ }
112
+ else if (key === "oauthScope") {
113
+ cfg.mode = "billed";
114
+ cfg.oauthScope = value;
115
+ }
116
+ else if (key === "supabaseUrl") {
117
+ cfg.mode = "direct";
118
+ cfg.supabaseUrl = value.replace(/\/$/, "");
119
+ }
120
+ else if (key === "serviceRoleKey") {
121
+ cfg.mode = "direct";
122
+ cfg.serviceRoleKey = value;
123
+ }
124
+ await saveConfig(cfg);
125
+ console.log(chalk.green(`Set ${key}.`));
126
+ });
127
+ cmd
128
+ .command("list")
129
+ .description("Show current configuration (secrets masked)")
130
+ .option("--json", "JSON output")
131
+ .action(async (opts, cmd) => {
132
+ const json = opts.json || cmd.optsWithGlobals().json;
133
+ const cfg = await loadConfig();
134
+ if (!cfg) {
135
+ const empty = { mode: null, message: "No config file yet. Run: riskmodels config init" };
136
+ printResults(empty, !!json);
137
+ return;
138
+ }
139
+ const view = {
140
+ mode: cfg.mode,
141
+ apiBaseUrl: cfg.apiBaseUrl ?? DEFAULT_API_BASE,
142
+ apiKey: maskSecret(cfg.apiKey),
143
+ clientId: maskSecret(cfg.clientId),
144
+ clientSecret: maskSecret(cfg.clientSecret),
145
+ oauthScope: cfg.oauthScope ?? "(not set)",
146
+ supabaseUrl: cfg.supabaseUrl ?? "(not set)",
147
+ serviceRoleKey: maskSecret(cfg.serviceRoleKey),
148
+ };
149
+ printResults(view, !!json);
150
+ });
151
+ return cmd;
152
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function correlationCommand(): Command;
@@ -0,0 +1,80 @@
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 correlationCommand() {
8
+ const corr = new Command("correlation").description("Macro factor correlation (POST /correlation, GET /metrics/.../correlation)");
9
+ corr
10
+ .command("post")
11
+ .description("Batch or single ticker correlation vs macro factors (POST /correlation)")
12
+ .requiredOption("--ticker <sym>", "One ticker, or comma-separated list (max 50)")
13
+ .option("--factors <list>", "Comma-separated factor keys (default: all six)")
14
+ .option("--return-type <t>", "gross | l1 | l2 | l3_residual", "l3_residual")
15
+ .option("--window-days <n>", "Rolling window length", "252")
16
+ .option("--method <m>", "pearson | spearman", "pearson")
17
+ .action(async (opts, cmd) => {
18
+ const json = cmd.optsWithGlobals().json ?? false;
19
+ const cfg = await loadConfig();
20
+ const auth = requireResolvedAuth(cfg, chalk.yellow);
21
+ if (!auth)
22
+ return;
23
+ const tickersStr = opts.ticker.trim();
24
+ const tickersParts = tickersStr.split(/[\s,]+/).filter(Boolean);
25
+ const tickerField = tickersParts.length > 1 ? tickersParts : tickersStr;
26
+ const body = {
27
+ ticker: tickerField,
28
+ return_type: opts.returnType ?? "l3_residual",
29
+ window_days: parseInt(String(opts.windowDays ?? "252"), 10) || 252,
30
+ method: opts.method ?? "pearson",
31
+ };
32
+ if (opts.factors?.trim()) {
33
+ body.factors = opts.factors.split(/[\s,]+/).map((s) => s.trim()).filter(Boolean);
34
+ }
35
+ try {
36
+ const { body: resBody, costUsd } = await apiFetchJson(auth, "POST", "/correlation", { jsonBody: body });
37
+ printResults(resBody, json);
38
+ if (!json && costUsd)
39
+ console.log(chalk.dim(`Cost: $${costUsd}`));
40
+ }
41
+ catch (e) {
42
+ console.error(chalk.red(e instanceof Error ? e.message : String(e)));
43
+ process.exitCode = 1;
44
+ }
45
+ });
46
+ corr
47
+ .command("metrics")
48
+ .description("Single-ticker correlation via GET /metrics/{ticker}/correlation")
49
+ .argument("<ticker>", "Symbol")
50
+ .option("--factors <list>", "Comma-separated macro keys")
51
+ .option("--return-type <t>", "gross | l1 | l2 | l3_residual", "l3_residual")
52
+ .option("--window-days <n>", "Default 252", "252")
53
+ .option("--method <m>", "pearson | spearman", "pearson")
54
+ .action(async (ticker, opts, cmd) => {
55
+ const json = cmd.optsWithGlobals().json ?? false;
56
+ const cfg = await loadConfig();
57
+ const auth = requireResolvedAuth(cfg, chalk.yellow);
58
+ if (!auth)
59
+ return;
60
+ const enc = encodeURIComponent(ticker.trim());
61
+ const query = {
62
+ return_type: opts.returnType ?? "l3_residual",
63
+ window_days: parseInt(String(opts.windowDays ?? "252"), 10) || 252,
64
+ method: opts.method ?? "pearson",
65
+ };
66
+ if (opts.factors?.trim())
67
+ query.factors = opts.factors.trim();
68
+ try {
69
+ const { body, costUsd } = await apiFetchJson(auth, "GET", `/metrics/${enc}/correlation`, { query });
70
+ printResults(body, json);
71
+ if (!json && costUsd)
72
+ console.log(chalk.dim(`Cost: $${costUsd}`));
73
+ }
74
+ catch (e) {
75
+ console.error(chalk.red(e instanceof Error ? e.message : String(e)));
76
+ process.exitCode = 1;
77
+ }
78
+ });
79
+ return corr;
80
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function doctorCommand(): Command;
@@ -0,0 +1,51 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { Command } from "commander";
3
+ import { configPath, loadConfig, maskSecret } from "../lib/config.js";
4
+ import { detectClients } from "../lib/mcp-config-paths.js";
5
+ import { printResults } from "../lib/display.js";
6
+ function commandOk(command, args = ["--version"]) {
7
+ const result = spawnSync(command, args, { stdio: "ignore" });
8
+ return result.status === 0 || result.status === 1;
9
+ }
10
+ export function doctorCommand() {
11
+ return new Command("doctor")
12
+ .description("Run local diagnostics for RiskModels CLI and MCP install readiness")
13
+ .option("--json", "JSON output")
14
+ .action(async (opts, cmd) => {
15
+ const json = opts.json || cmd.optsWithGlobals().json || false;
16
+ const cfg = await loadConfig();
17
+ const clients = await detectClients(["claude", "cursor", "codex", "vscode"]);
18
+ const checks = [
19
+ {
20
+ id: "node",
21
+ ok: true,
22
+ detail: process.version,
23
+ },
24
+ {
25
+ id: "npx",
26
+ ok: commandOk("npx"),
27
+ detail: "Required to launch @riskmodels/mcp from MCP client configs.",
28
+ },
29
+ {
30
+ id: "api_credentials",
31
+ ok: !!cfg?.apiKey || !!process.env.RISKMODELS_API_KEY || (!!cfg?.clientId && !!cfg?.clientSecret),
32
+ detail: cfg?.apiKey
33
+ ? `Config API key ${maskSecret(cfg.apiKey)} in ${configPath()}`
34
+ : process.env.RISKMODELS_API_KEY
35
+ ? "RISKMODELS_API_KEY is set in the environment."
36
+ : "No API key found. Get one at https://riskmodels.app/get-api-key",
37
+ },
38
+ {
39
+ id: "mcp_package_target",
40
+ ok: true,
41
+ detail: "Installer targets npx -y @riskmodels/mcp once the package is published.",
42
+ },
43
+ ];
44
+ printResults({
45
+ ok: checks.every((check) => check.ok),
46
+ checks,
47
+ clients,
48
+ 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);
50
+ });
51
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function estimateCommand(): Command;
@@ -0,0 +1,40 @@
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 estimateCommand() {
8
+ return new Command("estimate")
9
+ .description("Estimate request cost before calling a metered endpoint (POST /estimate)")
10
+ .requiredOption("--endpoint <name>", "Target endpoint slug, e.g. ticker-returns, batch-analyze")
11
+ .option("--params-json <json>", "JSON object of params for the target endpoint (string)")
12
+ .action(async (opts, cmd) => {
13
+ const json = cmd.optsWithGlobals().json ?? false;
14
+ const cfg = await loadConfig();
15
+ const auth = requireResolvedAuth(cfg, chalk.yellow);
16
+ if (!auth)
17
+ return;
18
+ let params = {};
19
+ if (opts.paramsJson?.trim()) {
20
+ try {
21
+ params = JSON.parse(opts.paramsJson);
22
+ }
23
+ catch {
24
+ console.error(chalk.red("Invalid --params-json (must be JSON)."));
25
+ process.exitCode = 1;
26
+ return;
27
+ }
28
+ }
29
+ try {
30
+ const { body } = await apiFetchJson(auth, "POST", "/estimate", {
31
+ jsonBody: { endpoint: opts.endpoint, params },
32
+ });
33
+ printResults(body, json);
34
+ }
35
+ catch (e) {
36
+ console.error(chalk.red(e instanceof Error ? e.message : String(e)));
37
+ process.exitCode = 1;
38
+ }
39
+ });
40
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function healthCommand(): Command;