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
@@ -0,0 +1,265 @@
1
+ import { mkdir, readFile, writeFile, copyFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { configPath, DEFAULT_API_BASE, loadConfig } from "./config.js";
4
+ import { defaultMcpServerConfig } from "./mcp-install-plan.js";
5
+ const RISKMODELS_SERVER_NAME = "riskmodels";
6
+ const TOML_MANAGED_HEADER = "# RiskModels MCP (managed by riskmodels CLI)";
7
+ function timestamp(date = new Date()) {
8
+ return date.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
9
+ }
10
+ async function readIfExists(filePath) {
11
+ try {
12
+ return { exists: true, text: await readFile(filePath, "utf8") };
13
+ }
14
+ catch (error) {
15
+ if (error.code === "ENOENT") {
16
+ return { exists: false, text: "" };
17
+ }
18
+ throw error;
19
+ }
20
+ }
21
+ async function backupIfExists(filePath, exists, now) {
22
+ if (!exists)
23
+ return undefined;
24
+ const backupPath = `${filePath}.riskmodels-backup-${timestamp(now)}`;
25
+ await copyFile(filePath, backupPath);
26
+ return backupPath;
27
+ }
28
+ function assertPlainObject(value, filePath) {
29
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
30
+ throw new Error(`${filePath} must contain a JSON object`);
31
+ }
32
+ }
33
+ export function mergeJsonMcpConfig(existingText, mcpServer) {
34
+ const parsed = existingText.trim() ? JSON.parse(existingText) : {};
35
+ assertPlainObject(parsed, "MCP config");
36
+ const currentServers = parsed.mcpServers;
37
+ if (currentServers !== undefined) {
38
+ assertPlainObject(currentServers, "mcpServers");
39
+ }
40
+ const merged = {
41
+ ...parsed,
42
+ mcpServers: {
43
+ ...currentServers,
44
+ [RISKMODELS_SERVER_NAME]: mcpServer,
45
+ },
46
+ };
47
+ const text = JSON.stringify(merged, null, 2) + "\n";
48
+ JSON.parse(text);
49
+ return text;
50
+ }
51
+ function tomlString(value) {
52
+ return JSON.stringify(value);
53
+ }
54
+ function tomlArray(values) {
55
+ return `[${values.map(tomlString).join(", ")}]`;
56
+ }
57
+ function removeRiskmodelsTomlBlock(existingText) {
58
+ const lines = existingText.split(/\r?\n/);
59
+ const out = [];
60
+ let skipping = false;
61
+ for (const line of lines) {
62
+ const trimmed = line.trim();
63
+ const startsRiskmodelsBlock = trimmed === "[mcp_servers.riskmodels]" ||
64
+ trimmed === "[mcp_servers.riskmodels.env]" ||
65
+ trimmed === "[mcpServers.riskmodels]" ||
66
+ trimmed === "[mcpServers.riskmodels.env]";
67
+ const startsAnySection = /^\[[^\]]+\]$/.test(trimmed);
68
+ if (startsRiskmodelsBlock) {
69
+ skipping = true;
70
+ if (out[out.length - 1]?.trim() === TOML_MANAGED_HEADER) {
71
+ out.pop();
72
+ }
73
+ continue;
74
+ }
75
+ if (skipping && startsAnySection && !startsRiskmodelsBlock) {
76
+ skipping = false;
77
+ }
78
+ if (!skipping && trimmed !== TOML_MANAGED_HEADER) {
79
+ out.push(line);
80
+ }
81
+ }
82
+ return out.join("\n").trimEnd();
83
+ }
84
+ export function mergeCodexTomlConfig(existingText, mcpServer) {
85
+ const server = mcpServer;
86
+ if (typeof server.command !== "string" || !Array.isArray(server.args)) {
87
+ throw new Error("Codex MCP server config requires command and args");
88
+ }
89
+ const base = removeRiskmodelsTomlBlock(existingText);
90
+ const lines = [
91
+ TOML_MANAGED_HEADER,
92
+ "[mcp_servers.riskmodels]",
93
+ `command = ${tomlString(server.command)}`,
94
+ `args = ${tomlArray(server.args.map(String))}`,
95
+ ];
96
+ if (server.env && typeof server.env === "object" && !Array.isArray(server.env)) {
97
+ lines.push("", "[mcp_servers.riskmodels.env]");
98
+ for (const [key, value] of Object.entries(server.env)) {
99
+ if (typeof value === "string") {
100
+ lines.push(`${key} = ${tomlString(value)}`);
101
+ }
102
+ }
103
+ }
104
+ const text = `${base ? `${base}\n\n` : ""}${lines.join("\n")}\n`;
105
+ validateRiskmodelsToml(text);
106
+ return text;
107
+ }
108
+ export function validateRiskmodelsToml(text) {
109
+ const lines = text.split(/\r?\n/);
110
+ let inRiskmodelsBlock = false;
111
+ for (const line of lines) {
112
+ const trimmed = line.trim();
113
+ if (!trimmed || trimmed.startsWith("#"))
114
+ continue;
115
+ if (/^\[[^\]]+\]$/.test(trimmed)) {
116
+ inRiskmodelsBlock =
117
+ trimmed === "[mcp_servers.riskmodels]" ||
118
+ trimmed === "[mcp_servers.riskmodels.env]";
119
+ continue;
120
+ }
121
+ if (!inRiskmodelsBlock)
122
+ continue;
123
+ if (/^[A-Za-z0-9_]+ = (".*"|\[[^\]]*\])$/.test(trimmed))
124
+ continue;
125
+ throw new Error(`Unsupported TOML line: ${line}`);
126
+ }
127
+ }
128
+ export async function writeSharedApiKey(apiKey, apiBaseUrl = DEFAULT_API_BASE, now) {
129
+ const existing = await loadConfig();
130
+ const cfg = {
131
+ ...(existing ?? { mode: "billed" }),
132
+ mode: "billed",
133
+ apiKey,
134
+ apiBaseUrl: (existing?.apiBaseUrl ?? apiBaseUrl).replace(/\/$/, ""),
135
+ };
136
+ const p = configPath();
137
+ const current = await readIfExists(p);
138
+ const backupPath = await backupIfExists(p, current.exists, now);
139
+ await mkdir(path.dirname(p), { recursive: true });
140
+ const text = JSON.stringify(cfg, null, 2) + "\n";
141
+ JSON.parse(text);
142
+ await writeFile(p, text, "utf8");
143
+ return {
144
+ configPath: p,
145
+ backupPath,
146
+ message: current.exists ? "Stored API key in shared config and created backup." : "Stored API key in shared config.",
147
+ };
148
+ }
149
+ async function writeTextWithBackup(filePath, text, now) {
150
+ const existing = await readIfExists(filePath);
151
+ const backupPath = await backupIfExists(filePath, existing.exists, now);
152
+ await mkdir(path.dirname(filePath), { recursive: true });
153
+ await writeFile(filePath, text, "utf8");
154
+ return backupPath;
155
+ }
156
+ export async function installMcpConfig(detection, opts = {}) {
157
+ if (!detection.configPath || detection.client === "vscode") {
158
+ return {
159
+ client: detection.client,
160
+ label: detection.label,
161
+ configPath: detection.configPath,
162
+ action: "skipped",
163
+ message: "No verified auto-write config target for this client.",
164
+ };
165
+ }
166
+ try {
167
+ const { exists, text } = await readIfExists(detection.configPath);
168
+ const mcpServer = defaultMcpServerConfig(opts.apiKey, !!opts.embedKey);
169
+ const nextText = detection.client === "codex"
170
+ ? mergeCodexTomlConfig(text, mcpServer)
171
+ : mergeJsonMcpConfig(text, mcpServer);
172
+ const backupPath = await writeTextWithBackup(detection.configPath, nextText, opts.now);
173
+ return {
174
+ client: detection.client,
175
+ label: detection.label,
176
+ configPath: detection.configPath,
177
+ action: "written",
178
+ backupPath,
179
+ message: exists ? "Merged riskmodels MCP server and created backup." : "Created config with riskmodels MCP server.",
180
+ };
181
+ }
182
+ catch (error) {
183
+ return {
184
+ client: detection.client,
185
+ label: detection.label,
186
+ configPath: detection.configPath,
187
+ action: "error",
188
+ message: error instanceof Error ? error.message : String(error),
189
+ };
190
+ }
191
+ }
192
+ export function removeJsonMcpConfig(existingText) {
193
+ const parsed = existingText.trim() ? JSON.parse(existingText) : {};
194
+ assertPlainObject(parsed, "MCP config");
195
+ const servers = parsed.mcpServers;
196
+ if (!servers || typeof servers !== "object" || Array.isArray(servers)) {
197
+ return { text: JSON.stringify(parsed, null, 2) + "\n", removed: false };
198
+ }
199
+ const nextServers = { ...servers };
200
+ const removed = Object.prototype.hasOwnProperty.call(nextServers, RISKMODELS_SERVER_NAME);
201
+ delete nextServers[RISKMODELS_SERVER_NAME];
202
+ const next = { ...parsed, mcpServers: nextServers };
203
+ return { text: JSON.stringify(next, null, 2) + "\n", removed };
204
+ }
205
+ export function removeCodexTomlConfig(existingText) {
206
+ const nextText = removeRiskmodelsTomlBlock(existingText);
207
+ validateRiskmodelsToml(nextText);
208
+ return {
209
+ text: `${nextText}${nextText ? "\n" : ""}`,
210
+ removed: nextText.trimEnd() !== existingText.trimEnd(),
211
+ };
212
+ }
213
+ export async function uninstallMcpConfig(detection, opts = {}) {
214
+ if (!detection.configPath || detection.client === "vscode") {
215
+ return {
216
+ client: detection.client,
217
+ label: detection.label,
218
+ configPath: detection.configPath,
219
+ action: "skipped",
220
+ message: "No verified auto-write config target for this client.",
221
+ };
222
+ }
223
+ try {
224
+ const { exists, text } = await readIfExists(detection.configPath);
225
+ if (!exists) {
226
+ return {
227
+ client: detection.client,
228
+ label: detection.label,
229
+ configPath: detection.configPath,
230
+ action: "skipped",
231
+ message: "Config file does not exist.",
232
+ };
233
+ }
234
+ const removal = detection.client === "codex"
235
+ ? removeCodexTomlConfig(text)
236
+ : removeJsonMcpConfig(text);
237
+ if (!removal.removed) {
238
+ return {
239
+ client: detection.client,
240
+ label: detection.label,
241
+ configPath: detection.configPath,
242
+ action: "skipped",
243
+ message: "No riskmodels MCP server block found.",
244
+ };
245
+ }
246
+ const backupPath = await writeTextWithBackup(detection.configPath, removal.text, opts.now);
247
+ return {
248
+ client: detection.client,
249
+ label: detection.label,
250
+ configPath: detection.configPath,
251
+ action: "written",
252
+ backupPath,
253
+ message: "Removed only the riskmodels MCP server block and created backup.",
254
+ };
255
+ }
256
+ catch (error) {
257
+ return {
258
+ client: detection.client,
259
+ label: detection.label,
260
+ configPath: detection.configPath,
261
+ action: "error",
262
+ message: error instanceof Error ? error.message : String(error),
263
+ };
264
+ }
265
+ }
@@ -0,0 +1,16 @@
1
+ import type { ClientDetection } from "./mcp-config-paths.js";
2
+ export interface InstallPlan {
3
+ client: string;
4
+ label: string;
5
+ status: string;
6
+ mode: string;
7
+ configPath?: string;
8
+ notes: string[];
9
+ mcpServer: unknown;
10
+ }
11
+ export declare function defaultMcpServerConfig(apiKey?: string, embedKey?: boolean): unknown;
12
+ export declare function buildInstallPlans(detections: ClientDetection[], opts: {
13
+ apiKey?: string;
14
+ embedKey?: boolean;
15
+ }): InstallPlan[];
16
+ export declare function firstPrompt(): string;
@@ -0,0 +1,28 @@
1
+ import { redactJson } from "./redact.js";
2
+ export function defaultMcpServerConfig(apiKey, embedKey = false) {
3
+ return {
4
+ command: "npx",
5
+ args: ["-y", "@riskmodels/mcp"],
6
+ ...(embedKey && apiKey
7
+ ? {
8
+ env: {
9
+ RISKMODELS_API_KEY: apiKey,
10
+ },
11
+ }
12
+ : {}),
13
+ };
14
+ }
15
+ export function buildInstallPlans(detections, opts) {
16
+ return detections.map((detection) => ({
17
+ client: detection.client,
18
+ label: detection.label,
19
+ status: detection.status,
20
+ mode: detection.mode,
21
+ configPath: detection.configPath,
22
+ notes: detection.notes,
23
+ mcpServer: redactJson(defaultMcpServerConfig(opts.apiKey, opts.embedKey)),
24
+ }));
25
+ }
26
+ export function firstPrompt() {
27
+ return "Compare AAPL and NVDA using RiskModels. What am I really betting on?";
28
+ }
@@ -0,0 +1,2 @@
1
+ export declare function invalidateOAuthToken(apiRoot: string, clientId: string, scope: string): void;
2
+ export declare function fetchOAuthAccessToken(apiRoot: string, clientId: string, clientSecret: string, scope: string): Promise<string>;
@@ -0,0 +1,47 @@
1
+ const skewSeconds = 60;
2
+ const cache = new Map();
3
+ function cacheKey(apiRoot, clientId, scope) {
4
+ return `${apiRoot}\0${clientId}\0${scope}`;
5
+ }
6
+ export function invalidateOAuthToken(apiRoot, clientId, scope) {
7
+ cache.delete(cacheKey(apiRoot, clientId, scope));
8
+ }
9
+ export async function fetchOAuthAccessToken(apiRoot, clientId, clientSecret, scope) {
10
+ const key = cacheKey(apiRoot, clientId, scope);
11
+ const now = performance.now();
12
+ const hit = cache.get(key);
13
+ if (hit && now < hit.expiresAtMono - skewSeconds * 1000) {
14
+ return hit.token;
15
+ }
16
+ const url = `${apiRoot.replace(/\/$/, "")}/auth/token`;
17
+ const res = await fetch(url, {
18
+ method: "POST",
19
+ headers: { "Content-Type": "application/json" },
20
+ body: JSON.stringify({
21
+ grant_type: "client_credentials",
22
+ client_id: clientId,
23
+ client_secret: clientSecret,
24
+ scope,
25
+ }),
26
+ });
27
+ const text = await res.text();
28
+ let data;
29
+ try {
30
+ data = JSON.parse(text);
31
+ }
32
+ catch {
33
+ throw new Error(`OAuth token: HTTP ${res.status}: ${text.slice(0, 200)}`);
34
+ }
35
+ if (!res.ok) {
36
+ throw new Error(data?.error ?? `OAuth token: HTTP ${res.status}: ${text.slice(0, 200)}`);
37
+ }
38
+ if (!data.access_token) {
39
+ throw new Error("OAuth token response missing access_token");
40
+ }
41
+ const expiresIn = Math.max(30, Number(data.expires_in ?? 900));
42
+ cache.set(key, {
43
+ token: data.access_token,
44
+ expiresAtMono: now + expiresIn * 1000,
45
+ });
46
+ return data.access_token;
47
+ }
@@ -0,0 +1,2 @@
1
+ export declare function redactSecret(value: string | undefined | null, visiblePrefix?: number, visibleSuffix?: number): string;
2
+ export declare function redactJson(value: unknown): unknown;
@@ -0,0 +1,28 @@
1
+ export function redactSecret(value, visiblePrefix = 6, visibleSuffix = 4) {
2
+ if (!value)
3
+ return "(not set)";
4
+ const trimmed = value.trim();
5
+ if (!trimmed)
6
+ return "(not set)";
7
+ if (trimmed.length <= visiblePrefix + visibleSuffix)
8
+ return "***";
9
+ return `${trimmed.slice(0, visiblePrefix)}...${trimmed.slice(-visibleSuffix)}`;
10
+ }
11
+ export function redactJson(value) {
12
+ if (Array.isArray(value)) {
13
+ return value.map((item) => redactJson(item));
14
+ }
15
+ if (!value || typeof value !== "object") {
16
+ return value;
17
+ }
18
+ const out = {};
19
+ for (const [key, item] of Object.entries(value)) {
20
+ if (/key|secret|token|authorization/i.test(key) && typeof item === "string") {
21
+ out[key] = redactSecret(item);
22
+ }
23
+ else {
24
+ out[key] = redactJson(item);
25
+ }
26
+ }
27
+ return out;
28
+ }
@@ -0,0 +1,9 @@
1
+ /** Mirrors app/api/cli/query/route.ts validation. */
2
+ export declare function validateQuery(sql: string): {
3
+ valid: true;
4
+ sanitized: string;
5
+ } | {
6
+ valid: false;
7
+ error: string;
8
+ };
9
+ export declare function ensureLimitClause(sql: string, limit: number): string;
@@ -0,0 +1,21 @@
1
+ /** Mirrors app/api/cli/query/route.ts validation. */
2
+ export function validateQuery(sql) {
3
+ const trimmed = sql.trim();
4
+ if (!/^select\b/i.test(trimmed)) {
5
+ return { valid: false, error: "Only SELECT queries allowed" };
6
+ }
7
+ if (trimmed.replace(/'[^']*'/g, "").includes(";")) {
8
+ return { valid: false, error: "Multiple statements not allowed" };
9
+ }
10
+ if (/\b(drop|delete|insert|update|alter|create|truncate|grant|revoke)\b/i.test(trimmed)) {
11
+ return { valid: false, error: "Only SELECT queries are permitted" };
12
+ }
13
+ return { valid: true, sanitized: trimmed };
14
+ }
15
+ export function ensureLimitClause(sql, limit) {
16
+ const capped = Math.min(Math.max(limit, 1), 10000);
17
+ if (/\blimit\b/i.test(sql) || /\bfetch\b/i.test(sql)) {
18
+ return sql;
19
+ }
20
+ return `${sql} LIMIT ${capped}`;
21
+ }
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "riskmodels",
3
+ "version": "2.0.0",
4
+ "description": "RiskModels CLI — REST API, SQL query, schema, billing, and agent manifests",
5
+ "type": "module",
6
+ "bin": {
7
+ "riskmodels": "./dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "README.md"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsc",
15
+ "prepublishOnly": "npm run build",
16
+ "install:global": "npm run build && npm link"
17
+ },
18
+ "engines": {
19
+ "node": ">=18.0.0"
20
+ },
21
+ "keywords": [
22
+ "riskmodels",
23
+ "cli",
24
+ "sql",
25
+ "hedge",
26
+ "risk"
27
+ ],
28
+ "license": "MIT",
29
+ "dependencies": {
30
+ "@supabase/supabase-js": "^2.49.1",
31
+ "chalk": "^5.4.1",
32
+ "commander": "^13.1.0",
33
+ "inquirer": "^9.3.7",
34
+ "ora": "^8.1.1"
35
+ },
36
+ "devDependencies": {
37
+ "@types/inquirer": "^9.0.7",
38
+ "@types/node": "^22.10.0",
39
+ "typescript": "^5.6.0"
40
+ }
41
+ }