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,45 @@
1
+ import { Command } from "commander";
2
+ import chalk from "chalk";
3
+ import { detectClients, selectedClients } from "../lib/mcp-config-paths.js";
4
+ import { printResults } from "../lib/display.js";
5
+ import { uninstallMcpConfig } from "../lib/mcp-config-writer.js";
6
+ export function uninstallCommand() {
7
+ return new Command("uninstall")
8
+ .description("Remove the RiskModels MCP server from client configs")
9
+ .option("--client <name>", "claude | cursor | codex | vscode")
10
+ .option("--all", "Check all supported clients")
11
+ .option("--dry-run", "Show planned removals without writing")
12
+ .option("--json", "JSON output")
13
+ .action(async (opts, cmd) => {
14
+ const json = opts.json || cmd.optsWithGlobals().json || false;
15
+ const clients = selectedClients({ client: opts.client, all: opts.all });
16
+ const detections = await detectClients(clients);
17
+ const dryRun = opts.dryRun ?? false;
18
+ const output = {
19
+ dryRun,
20
+ plannedAction: "Remove only the `riskmodels` MCP server block from detected client configs.",
21
+ preservesSharedApiKey: true,
22
+ clients: detections.map((detection) => ({
23
+ client: detection.client,
24
+ label: detection.label,
25
+ configPath: detection.configPath,
26
+ mode: detection.mode,
27
+ status: detection.status,
28
+ notes: detection.notes,
29
+ })),
30
+ };
31
+ if (dryRun) {
32
+ printResults(output, json);
33
+ return;
34
+ }
35
+ const removals = await Promise.all(detections.map((detection) => uninstallMcpConfig(detection)));
36
+ printResults({ ...output, removals }, json);
37
+ if (removals.some((removal) => removal.action === "error")) {
38
+ process.exitCode = 1;
39
+ return;
40
+ }
41
+ if (!json) {
42
+ console.error(chalk.green("RiskModels MCP uninstall completed with backups where files changed."));
43
+ }
44
+ });
45
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import chalk from "chalk";
4
+ import { configCommand } from "./commands/config.js";
5
+ import { queryCommand } from "./commands/query.js";
6
+ import { schemaCommand } from "./commands/schema.js";
7
+ import { balanceCommand } from "./commands/balance.js";
8
+ import { manifestCommand } from "./commands/manifest.js";
9
+ import { mcpConfigCommand } from "./commands/mcp-config.js";
10
+ import { mcpServeCommand } from "./commands/mcp.js";
11
+ import { agentCommand } from "./commands/agent.js";
12
+ import { metricsCommand } from "./commands/metrics.js";
13
+ import { batchCommand } from "./commands/batch.js";
14
+ import { portfolioCommand } from "./commands/portfolio.js";
15
+ import { tickersCommand } from "./commands/tickers.js";
16
+ import { healthCommand } from "./commands/health.js";
17
+ import { estimateCommand } from "./commands/estimate.js";
18
+ import { returnsCommand } from "./commands/returns.js";
19
+ import { l3Command } from "./commands/l3.js";
20
+ import { correlationCommand } from "./commands/correlation.js";
21
+ import { macroFactorsCommand } from "./commands/macro-factors.js";
22
+ import { rankingsCommand } from "./commands/rankings.js";
23
+ import { installCommand } from "./commands/install.js";
24
+ import { statusCommand } from "./commands/status.js";
25
+ import { doctorCommand } from "./commands/doctor.js";
26
+ import { uninstallCommand } from "./commands/uninstall.js";
27
+ const program = new Command();
28
+ program
29
+ .name("riskmodels")
30
+ .description("RiskModels CLI — REST API, SQL query, schema, billing, and agent manifests")
31
+ .version("2.0.0", "-V, --version", "output version")
32
+ .option("--json", "JSON output for supported commands (query, schema, balance, API calls, config list)")
33
+ .configureHelp({ sortSubcommands: true })
34
+ .addHelpText("after", `
35
+ ${chalk.bold("Quick start")}
36
+ ${chalk.dim("$")} riskmodels config init
37
+ ${chalk.dim("$")} riskmodels install --dry-run
38
+ ${chalk.dim("$")} riskmodels health
39
+ ${chalk.dim("$")} riskmodels metrics NVDA
40
+ ${chalk.dim("$")} riskmodels query ${chalk.green('"SELECT ticker FROM ticker_metadata LIMIT 3"')}
41
+ ${chalk.dim("$")} riskmodels manifest --format anthropic
42
+
43
+ ${chalk.bold("Docs")} https://riskmodels.app/docs/api`);
44
+ program.addCommand(configCommand());
45
+ program.addCommand(installCommand());
46
+ program.addCommand(statusCommand());
47
+ program.addCommand(doctorCommand());
48
+ program.addCommand(uninstallCommand());
49
+ program.addCommand(queryCommand());
50
+ program.addCommand(metricsCommand());
51
+ program.addCommand(batchCommand());
52
+ program.addCommand(portfolioCommand());
53
+ program.addCommand(returnsCommand());
54
+ program.addCommand(l3Command());
55
+ program.addCommand(correlationCommand());
56
+ program.addCommand(macroFactorsCommand());
57
+ program.addCommand(rankingsCommand());
58
+ program.addCommand(tickersCommand());
59
+ program.addCommand(healthCommand());
60
+ program.addCommand(estimateCommand());
61
+ program.addCommand(schemaCommand());
62
+ program.addCommand(balanceCommand());
63
+ program.addCommand(manifestCommand());
64
+ program.addCommand(mcpConfigCommand());
65
+ program.addCommand(mcpServeCommand());
66
+ program.addCommand(agentCommand());
67
+ await program.parseAsync(process.argv);
@@ -0,0 +1,22 @@
1
+ import type { ResolvedApiAuth } from "./credentials.js";
2
+ /** Thrown on non-2xx API responses so callers can inspect `status` (e.g. retries on 503). */
3
+ export declare class ApiHttpError extends Error {
4
+ readonly status: number;
5
+ constructor(status: number, message: string);
6
+ }
7
+ export type ApiJsonResult = {
8
+ body: unknown;
9
+ costUsd?: string;
10
+ status: number;
11
+ headers: Headers;
12
+ };
13
+ export declare function apiFetchJson(auth: ResolvedApiAuth, method: string, path: string, options?: {
14
+ query?: Record<string, string | number | boolean | undefined | null>;
15
+ jsonBody?: unknown;
16
+ }): Promise<ApiJsonResult>;
17
+ export declare function apiFetchOptionalAuth(apiRoot: string, method: string, path: string, options?: {
18
+ query?: Record<string, string | number | boolean | undefined | null>;
19
+ jsonBody?: unknown;
20
+ auth?: ResolvedApiAuth | null;
21
+ signal?: AbortSignal;
22
+ }): Promise<ApiJsonResult>;
@@ -0,0 +1,100 @@
1
+ import { getAuthorizationHeader, invalidateAuthForRetry } from "./credentials.js";
2
+ /** Thrown on non-2xx API responses so callers can inspect `status` (e.g. retries on 503). */
3
+ export class ApiHttpError extends Error {
4
+ status;
5
+ constructor(status, message) {
6
+ super(message);
7
+ this.name = "ApiHttpError";
8
+ this.status = status;
9
+ }
10
+ }
11
+ function errorMessageFromBody(body) {
12
+ if (body && typeof body === "object") {
13
+ const o = body;
14
+ const msg = o.message ?? o.error;
15
+ if (typeof msg === "string" && msg.trim())
16
+ return msg;
17
+ const detail = o.detail;
18
+ if (typeof detail === "string" && detail.trim())
19
+ return detail;
20
+ }
21
+ return "";
22
+ }
23
+ export async function apiFetchJson(auth, method, path, options) {
24
+ const url = buildUrl(auth.apiRoot, path, options?.query);
25
+ for (let attempt = 1; attempt <= 2; attempt += 1) {
26
+ const headers = await getAuthorizationHeader(auth);
27
+ const init = {
28
+ method,
29
+ headers: {
30
+ ...headers,
31
+ Accept: "application/json",
32
+ ...(options?.jsonBody !== undefined ? { "Content-Type": "application/json" } : {}),
33
+ },
34
+ body: options?.jsonBody !== undefined ? JSON.stringify(options.jsonBody) : undefined,
35
+ };
36
+ const res = await fetch(url, init);
37
+ if (res.status === 401 && attempt === 1 && auth.oauth) {
38
+ invalidateAuthForRetry(auth);
39
+ continue;
40
+ }
41
+ const costUsd = res.headers.get("x-api-cost-usd") ?? undefined;
42
+ const text = await res.text();
43
+ let body;
44
+ try {
45
+ body = text ? JSON.parse(text) : null;
46
+ }
47
+ catch {
48
+ body = { raw: text };
49
+ }
50
+ if (!res.ok) {
51
+ const msg = errorMessageFromBody(body) || `HTTP ${res.status}`;
52
+ throw new ApiHttpError(res.status, msg);
53
+ }
54
+ return { body, costUsd, status: res.status, headers: res.headers };
55
+ }
56
+ throw new Error("Unreachable");
57
+ }
58
+ export async function apiFetchOptionalAuth(apiRoot, method, path, options) {
59
+ const url = buildUrl(apiRoot, path, options?.query);
60
+ const headers = { Accept: "application/json" };
61
+ if (options?.auth) {
62
+ Object.assign(headers, await getAuthorizationHeader(options.auth));
63
+ }
64
+ if (options?.jsonBody !== undefined) {
65
+ headers["Content-Type"] = "application/json";
66
+ }
67
+ const res = await fetch(url, {
68
+ method,
69
+ headers,
70
+ body: options?.jsonBody !== undefined ? JSON.stringify(options.jsonBody) : undefined,
71
+ signal: options?.signal,
72
+ });
73
+ const costUsd = res.headers.get("x-api-cost-usd") ?? undefined;
74
+ const text = await res.text();
75
+ let body;
76
+ try {
77
+ body = text ? JSON.parse(text) : null;
78
+ }
79
+ catch {
80
+ body = { raw: text };
81
+ }
82
+ if (!res.ok) {
83
+ const msg = errorMessageFromBody(body) || `HTTP ${res.status}`;
84
+ throw new ApiHttpError(res.status, msg);
85
+ }
86
+ return { body, costUsd, status: res.status, headers: res.headers };
87
+ }
88
+ function buildUrl(apiRoot, path, query) {
89
+ const base = apiRoot.replace(/\/$/, "");
90
+ const p = path.startsWith("/") ? path : `/${path}`;
91
+ const u = new URL(base + p);
92
+ if (query) {
93
+ for (const [k, v] of Object.entries(query)) {
94
+ if (v === undefined || v === null)
95
+ continue;
96
+ u.searchParams.set(k, String(v));
97
+ }
98
+ }
99
+ return u.toString();
100
+ }
@@ -0,0 +1,2 @@
1
+ /** User-facing base is usually `https://riskmodels.app`; SDK uses `…/api`. */
2
+ export declare function apiRootFromUserBase(apiBaseUrl: string | undefined): string;
@@ -0,0 +1,8 @@
1
+ import { DEFAULT_API_BASE } from "./config.js";
2
+ /** User-facing base is usually `https://riskmodels.app`; SDK uses `…/api`. */
3
+ export function apiRootFromUserBase(apiBaseUrl) {
4
+ const raw = (apiBaseUrl ?? DEFAULT_API_BASE).replace(/\/$/, "");
5
+ if (raw.endsWith("/api"))
6
+ return raw;
7
+ return `${raw}/api`;
8
+ }
@@ -0,0 +1,22 @@
1
+ export type AuthMode = "billed" | "direct";
2
+ export interface RiskmodelsConfig {
3
+ mode: AuthMode;
4
+ apiKey?: string;
5
+ /** Base URL without trailing slash, e.g. https://riskmodels.app */
6
+ apiBaseUrl?: string;
7
+ /** OAuth client credentials (billed mode); scope defaults match the Python SDK. */
8
+ clientId?: string;
9
+ clientSecret?: string;
10
+ oauthScope?: string;
11
+ supabaseUrl?: string;
12
+ serviceRoleKey?: string;
13
+ }
14
+ export declare const DEFAULT_API_BASE = "https://riskmodels.app";
15
+ export declare function configPath(): string;
16
+ export declare function loadConfig(): Promise<RiskmodelsConfig | null>;
17
+ export declare function saveConfig(cfg: RiskmodelsConfig): Promise<void>;
18
+ export declare function maskSecret(value: string | undefined, visible?: number): string;
19
+ export declare function isBilledReady(cfg: RiskmodelsConfig | null): boolean;
20
+ export declare function isDirectReady(cfg: RiskmodelsConfig | null): boolean;
21
+ export declare function needsConfigMessage(): string;
22
+ export declare function needsApiKeyMessage(): string;
@@ -0,0 +1,46 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import path from "node:path";
4
+ export const DEFAULT_API_BASE = "https://riskmodels.app";
5
+ export function configPath() {
6
+ return path.join(homedir(), ".config", "riskmodels", "config.json");
7
+ }
8
+ export async function loadConfig() {
9
+ const p = configPath();
10
+ try {
11
+ const raw = await readFile(p, "utf8");
12
+ return JSON.parse(raw);
13
+ }
14
+ catch {
15
+ return null;
16
+ }
17
+ }
18
+ export async function saveConfig(cfg) {
19
+ const p = configPath();
20
+ await mkdir(path.dirname(p), { recursive: true });
21
+ await writeFile(p, JSON.stringify(cfg, null, 2) + "\n", "utf8");
22
+ }
23
+ export function maskSecret(value, visible = 6) {
24
+ if (!value)
25
+ return "(not set)";
26
+ if (value.length <= visible)
27
+ return "***";
28
+ return `${value.slice(0, visible)}...`;
29
+ }
30
+ export function isBilledReady(cfg) {
31
+ return (!!cfg &&
32
+ cfg.mode === "billed" &&
33
+ (!!cfg.apiKey?.trim() || (!!cfg.clientId?.trim() && !!cfg.clientSecret?.trim())));
34
+ }
35
+ export function isDirectReady(cfg) {
36
+ return (!!cfg &&
37
+ cfg.mode === "direct" &&
38
+ !!cfg.supabaseUrl?.trim() &&
39
+ !!cfg.serviceRoleKey?.trim());
40
+ }
41
+ export function needsConfigMessage() {
42
+ return "Supabase credentials not configured. Run: riskmodels config init";
43
+ }
44
+ export function needsApiKeyMessage() {
45
+ return "API key not configured. Run: riskmodels config init (billed mode) or: riskmodels config set apiKey <rm_agent_...>";
46
+ }
@@ -0,0 +1,26 @@
1
+ import type { RiskmodelsConfig } from "./config.js";
2
+ /** Matches sdk/riskmodels/client.py DEFAULT_SCOPE. */
3
+ export declare const DEFAULT_OAUTH_SCOPE = "ticker-returns risk-decomposition batch-analysis factor-correlation macro-factor-series rankings";
4
+ export type ResolvedApiAuth = {
5
+ apiRoot: string;
6
+ /** Static Bearer (API key) — preferred when set */
7
+ apiKey?: string;
8
+ oauth?: {
9
+ clientId: string;
10
+ clientSecret: string;
11
+ scope: string;
12
+ };
13
+ };
14
+ export declare function resolveApiAuth(cfg: RiskmodelsConfig | null): ResolvedApiAuth | null;
15
+ /** True when user has API key or OAuth credentials (config and/or env). */
16
+ export declare function hasRestApiCredentials(cfg: RiskmodelsConfig | null): boolean;
17
+ /**
18
+ * REST analytics require Bearer auth. Direct (Supabase-only) config is OK if env provides credentials.
19
+ */
20
+ export declare function assertRestApiAuth(cfg: RiskmodelsConfig | null, chalkYellow: (s: string) => string): void;
21
+ /** Returns auth or sets process.exitCode and prints via assertRestApiAuth. */
22
+ export declare function requireResolvedAuth(cfg: RiskmodelsConfig | null, chalkYellow: (s: string) => string): ResolvedApiAuth | null;
23
+ export declare function getAuthorizationHeader(auth: ResolvedApiAuth): Promise<Record<string, string>>;
24
+ export declare function invalidateAuthForRetry(auth: ResolvedApiAuth): void;
25
+ /** Public origin for docs (no `/api` suffix). */
26
+ export declare function displayApiOrigin(cfg: RiskmodelsConfig | null): string;
@@ -0,0 +1,70 @@
1
+ import { apiRootFromUserBase } from "./api-url.js";
2
+ import { DEFAULT_API_BASE } from "./config.js";
3
+ import { fetchOAuthAccessToken, invalidateOAuthToken } from "./oauth.js";
4
+ /** Matches sdk/riskmodels/client.py DEFAULT_SCOPE. */
5
+ export const DEFAULT_OAUTH_SCOPE = "ticker-returns risk-decomposition batch-analysis factor-correlation macro-factor-series rankings";
6
+ function trimEnv(key) {
7
+ const v = process.env[key];
8
+ return v?.trim() || undefined;
9
+ }
10
+ export function resolveApiAuth(cfg) {
11
+ const apiRoot = apiRootFromUserBase(cfg?.apiBaseUrl);
12
+ const apiKey = cfg?.apiKey?.trim() || trimEnv("RISKMODELS_API_KEY");
13
+ if (apiKey) {
14
+ return { apiRoot, apiKey };
15
+ }
16
+ const clientId = cfg?.clientId?.trim() || trimEnv("RISKMODELS_CLIENT_ID");
17
+ const clientSecret = cfg?.clientSecret?.trim() || trimEnv("RISKMODELS_CLIENT_SECRET");
18
+ const scope = cfg?.oauthScope?.trim() || trimEnv("RISKMODELS_OAUTH_SCOPE") || DEFAULT_OAUTH_SCOPE;
19
+ if (clientId && clientSecret) {
20
+ return { apiRoot, oauth: { clientId, clientSecret, scope } };
21
+ }
22
+ return null;
23
+ }
24
+ /** True when user has API key or OAuth credentials (config and/or env). */
25
+ export function hasRestApiCredentials(cfg) {
26
+ return resolveApiAuth(cfg) !== null;
27
+ }
28
+ /**
29
+ * REST analytics require Bearer auth. Direct (Supabase-only) config is OK if env provides credentials.
30
+ */
31
+ export function assertRestApiAuth(cfg, chalkYellow) {
32
+ if (hasRestApiCredentials(cfg))
33
+ return;
34
+ if (cfg?.mode === "direct") {
35
+ console.error(chalkYellow("REST API commands need an API key or OAuth client credentials. " +
36
+ "Set RISKMODELS_API_KEY (or RISKMODELS_CLIENT_ID + RISKMODELS_CLIENT_SECRET), " +
37
+ "or run `riskmodels config init` in billed mode."));
38
+ process.exitCode = 1;
39
+ return;
40
+ }
41
+ console.error(chalkYellow("API credentials not configured. Run: riskmodels config init"));
42
+ process.exitCode = 1;
43
+ }
44
+ /** Returns auth or sets process.exitCode and prints via assertRestApiAuth. */
45
+ export function requireResolvedAuth(cfg, chalkYellow) {
46
+ const auth = resolveApiAuth(cfg);
47
+ if (auth)
48
+ return auth;
49
+ assertRestApiAuth(cfg, chalkYellow);
50
+ return null;
51
+ }
52
+ export async function getAuthorizationHeader(auth) {
53
+ if (auth.apiKey) {
54
+ return { Authorization: `Bearer ${auth.apiKey}` };
55
+ }
56
+ if (auth.oauth) {
57
+ const token = await fetchOAuthAccessToken(auth.apiRoot, auth.oauth.clientId, auth.oauth.clientSecret, auth.oauth.scope);
58
+ return { Authorization: `Bearer ${token}` };
59
+ }
60
+ throw new Error("No API credentials");
61
+ }
62
+ export function invalidateAuthForRetry(auth) {
63
+ if (auth.oauth) {
64
+ invalidateOAuthToken(auth.apiRoot, auth.oauth.clientId, auth.oauth.scope);
65
+ }
66
+ }
67
+ /** Public origin for docs (no `/api` suffix). */
68
+ export function displayApiOrigin(cfg) {
69
+ return (cfg?.apiBaseUrl ?? DEFAULT_API_BASE).replace(/\/$/, "").replace(/\/api$/, "");
70
+ }
@@ -0,0 +1 @@
1
+ export declare function printResults(data: unknown, asJson: boolean): void;
@@ -0,0 +1,19 @@
1
+ export function printResults(data, asJson) {
2
+ if (asJson) {
3
+ console.log(JSON.stringify(data, null, 2));
4
+ return;
5
+ }
6
+ if (data === null || data === undefined) {
7
+ console.log("(no data)");
8
+ return;
9
+ }
10
+ if (Array.isArray(data) && data.length > 0 && typeof data[0] === "object") {
11
+ console.table(data);
12
+ return;
13
+ }
14
+ if (typeof data === "object") {
15
+ console.log(JSON.stringify(data, null, 2));
16
+ return;
17
+ }
18
+ console.log(String(data));
19
+ }
@@ -0,0 +1,16 @@
1
+ export type InstallClient = "claude" | "cursor" | "codex" | "vscode";
2
+ export interface ClientDetection {
3
+ client: InstallClient;
4
+ label: string;
5
+ mode: "auto-write" | "command" | "guidance";
6
+ status: "found" | "missing" | "unknown";
7
+ configPath?: string;
8
+ commandAvailable?: boolean;
9
+ notes: string[];
10
+ }
11
+ export declare function selectedClients(opts: {
12
+ client?: string;
13
+ all?: boolean;
14
+ }): InstallClient[];
15
+ export declare function detectClient(client: InstallClient, cwd?: string): Promise<ClientDetection>;
16
+ export declare function detectClients(clients: InstallClient[], cwd?: string): Promise<ClientDetection[]>;
@@ -0,0 +1,100 @@
1
+ import { access } from "node:fs/promises";
2
+ import { homedir, platform } from "node:os";
3
+ import path from "node:path";
4
+ import { spawnSync } from "node:child_process";
5
+ async function exists(filePath) {
6
+ try {
7
+ await access(filePath);
8
+ return true;
9
+ }
10
+ catch {
11
+ return false;
12
+ }
13
+ }
14
+ function commandAvailable(command, args = ["--version"]) {
15
+ const result = spawnSync(command, args, { stdio: "ignore" });
16
+ return result.status === 0 || result.status === 1;
17
+ }
18
+ function claudeDesktopPath() {
19
+ const home = homedir();
20
+ const os = platform();
21
+ if (os === "darwin") {
22
+ return path.join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
23
+ }
24
+ if (os === "win32") {
25
+ return path.join(process.env.APPDATA ?? path.join(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
26
+ }
27
+ return path.join(home, ".config", "Claude", "claude_desktop_config.json");
28
+ }
29
+ export function selectedClients(opts) {
30
+ if (opts.all || !opts.client)
31
+ return ["claude", "cursor", "codex", "vscode"];
32
+ const value = opts.client.toLowerCase();
33
+ if (!["claude", "cursor", "codex", "vscode"].includes(value)) {
34
+ throw new Error(`Unknown client: ${opts.client}. Use claude, cursor, codex, vscode, or --all.`);
35
+ }
36
+ return [value];
37
+ }
38
+ export async function detectClient(client, cwd = process.cwd()) {
39
+ if (client === "claude") {
40
+ const configPath = claudeDesktopPath();
41
+ const claudeCodeAvailable = commandAvailable("claude", ["--version"]);
42
+ const found = (await exists(configPath)) || claudeCodeAvailable;
43
+ return {
44
+ client,
45
+ label: "Claude Desktop / Claude Code",
46
+ mode: claudeCodeAvailable ? "command" : "auto-write",
47
+ status: found ? "found" : "missing",
48
+ configPath,
49
+ commandAvailable: claudeCodeAvailable,
50
+ notes: [
51
+ claudeCodeAvailable
52
+ ? "Claude Code CLI detected; future safe-write flow should prefer `claude mcp add`."
53
+ : "Claude Code CLI not detected; Claude Desktop config path is the fallback target.",
54
+ ],
55
+ };
56
+ }
57
+ if (client === "cursor") {
58
+ const globalPath = path.join(homedir(), ".cursor", "mcp.json");
59
+ const projectPath = path.join(cwd, ".cursor", "mcp.json");
60
+ const globalExists = await exists(globalPath);
61
+ const projectExists = await exists(projectPath);
62
+ return {
63
+ client,
64
+ label: "Cursor",
65
+ mode: "auto-write",
66
+ status: globalExists || projectExists ? "found" : "missing",
67
+ configPath: globalExists ? globalPath : projectPath,
68
+ notes: [
69
+ globalExists
70
+ ? "Global Cursor MCP config exists."
71
+ : "Workspace .cursor/mcp.json is the first dry-run target; safe writes should ask before project-scoped edits.",
72
+ ],
73
+ };
74
+ }
75
+ if (client === "codex") {
76
+ const configPath = path.join(homedir(), ".codex", "config.toml");
77
+ return {
78
+ client,
79
+ label: "Codex",
80
+ mode: "auto-write",
81
+ status: (await exists(configPath)) ? "found" : "missing",
82
+ configPath,
83
+ notes: ["Codex uses TOML config; safe-write flow must validate TOML before and after merge."],
84
+ };
85
+ }
86
+ const codeAvailable = commandAvailable("code", ["--version"]);
87
+ return {
88
+ client,
89
+ label: "VS Code",
90
+ mode: "guidance",
91
+ status: codeAvailable ? "found" : "unknown",
92
+ commandAvailable: codeAvailable,
93
+ notes: [
94
+ "VS Code support is detect + guidance only in v1; no auto-write target is selected until the MCP extension/config surface is verified.",
95
+ ],
96
+ };
97
+ }
98
+ export async function detectClients(clients, cwd = process.cwd()) {
99
+ return Promise.all(clients.map((client) => detectClient(client, cwd)));
100
+ }
@@ -0,0 +1,36 @@
1
+ import type { ClientDetection } from "./mcp-config-paths.js";
2
+ export interface ConfigWriteResult {
3
+ client: string;
4
+ label: string;
5
+ configPath?: string;
6
+ action: "written" | "skipped" | "error";
7
+ backupPath?: string;
8
+ message: string;
9
+ }
10
+ export interface SafeWriteOptions {
11
+ apiKey?: string;
12
+ embedKey?: boolean;
13
+ apiBaseUrl?: string;
14
+ now?: Date;
15
+ }
16
+ export interface SharedConfigWriteResult {
17
+ configPath: string;
18
+ backupPath?: string;
19
+ message: string;
20
+ }
21
+ export declare function mergeJsonMcpConfig(existingText: string, mcpServer: unknown): string;
22
+ export declare function mergeCodexTomlConfig(existingText: string, mcpServer: unknown): string;
23
+ export declare function validateRiskmodelsToml(text: string): void;
24
+ export declare function writeSharedApiKey(apiKey: string, apiBaseUrl?: string, now?: Date): Promise<SharedConfigWriteResult>;
25
+ export declare function installMcpConfig(detection: ClientDetection, opts?: SafeWriteOptions): Promise<ConfigWriteResult>;
26
+ export declare function removeJsonMcpConfig(existingText: string): {
27
+ text: string;
28
+ removed: boolean;
29
+ };
30
+ export declare function removeCodexTomlConfig(existingText: string): {
31
+ text: string;
32
+ removed: boolean;
33
+ };
34
+ export declare function uninstallMcpConfig(detection: ClientDetection, opts?: {
35
+ now?: Date;
36
+ }): Promise<ConfigWriteResult>;