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.
- package/README.md +104 -0
- package/dist/commands/agent.d.ts +2 -0
- package/dist/commands/agent.js +76 -0
- package/dist/commands/balance.d.ts +2 -0
- package/dist/commands/balance.js +25 -0
- package/dist/commands/batch.d.ts +2 -0
- package/dist/commands/batch.js +76 -0
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.js +152 -0
- package/dist/commands/correlation.d.ts +2 -0
- package/dist/commands/correlation.js +80 -0
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.js +51 -0
- package/dist/commands/estimate.d.ts +2 -0
- package/dist/commands/estimate.js +40 -0
- package/dist/commands/health.d.ts +2 -0
- package/dist/commands/health.js +60 -0
- package/dist/commands/install.d.ts +2 -0
- package/dist/commands/install.js +136 -0
- package/dist/commands/l3.d.ts +2 -0
- package/dist/commands/l3.js +33 -0
- package/dist/commands/macro-factors.d.ts +2 -0
- package/dist/commands/macro-factors.js +39 -0
- package/dist/commands/manifest.d.ts +2 -0
- package/dist/commands/manifest.js +270 -0
- package/dist/commands/mcp-config.d.ts +2 -0
- package/dist/commands/mcp-config.js +95 -0
- package/dist/commands/mcp.d.ts +12 -0
- package/dist/commands/mcp.js +59 -0
- package/dist/commands/metrics.d.ts +2 -0
- package/dist/commands/metrics.js +29 -0
- package/dist/commands/portfolio.d.ts +2 -0
- package/dist/commands/portfolio.js +80 -0
- package/dist/commands/query.d.ts +2 -0
- package/dist/commands/query.js +90 -0
- package/dist/commands/rankings.d.ts +2 -0
- package/dist/commands/rankings.js +108 -0
- package/dist/commands/returns.d.ts +2 -0
- package/dist/commands/returns.js +77 -0
- package/dist/commands/schema.d.ts +2 -0
- package/dist/commands/schema.js +77 -0
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.js +25 -0
- package/dist/commands/tickers.d.ts +2 -0
- package/dist/commands/tickers.js +36 -0
- package/dist/commands/uninstall.d.ts +2 -0
- package/dist/commands/uninstall.js +45 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +67 -0
- package/dist/lib/api-client.d.ts +22 -0
- package/dist/lib/api-client.js +100 -0
- package/dist/lib/api-url.d.ts +2 -0
- package/dist/lib/api-url.js +8 -0
- package/dist/lib/config.d.ts +22 -0
- package/dist/lib/config.js +46 -0
- package/dist/lib/credentials.d.ts +26 -0
- package/dist/lib/credentials.js +70 -0
- package/dist/lib/display.d.ts +1 -0
- package/dist/lib/display.js +19 -0
- package/dist/lib/mcp-config-paths.d.ts +16 -0
- package/dist/lib/mcp-config-paths.js +100 -0
- package/dist/lib/mcp-config-writer.d.ts +36 -0
- package/dist/lib/mcp-config-writer.js +265 -0
- package/dist/lib/mcp-install-plan.d.ts +16 -0
- package/dist/lib/mcp-install-plan.js +28 -0
- package/dist/lib/oauth.d.ts +2 -0
- package/dist/lib/oauth.js +47 -0
- package/dist/lib/redact.d.ts +2 -0
- package/dist/lib/redact.js +28 -0
- package/dist/lib/sql-validation.d.ts +9 -0
- package/dist/lib/sql-validation.js +21 -0
- 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,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,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,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,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,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,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,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
|
+
}
|