rentline-sandbox 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ DEFAULT_API_URL,
4
+ deleteConfig,
5
+ loadConfig,
6
+ saveConfig
7
+ } from "./chunk-Q5AKRRZ2.js";
8
+ import {
9
+ createClient
10
+ } from "./chunk-X3OZHOPM.js";
11
+
12
+ // src/commands/auth.ts
13
+ import { createInterface } from "readline";
14
+ var CLI_AUTH_URL = "https://sandbox.rentline.xyz/cli-auth";
15
+ async function openBrowser(url) {
16
+ const { platform } = process;
17
+ const { spawn } = await import("child_process");
18
+ const cmd = platform === "win32" ? "cmd" : platform === "darwin" ? "open" : "xdg-open";
19
+ const args = platform === "win32" ? ["/c", "start", "", url] : [url];
20
+ spawn(cmd, args, { detached: true, stdio: "ignore" }).unref();
21
+ }
22
+ function prompt(question) {
23
+ return new Promise((resolve) => {
24
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
25
+ rl.question(question, (answer) => {
26
+ rl.close();
27
+ resolve(answer.trim());
28
+ });
29
+ });
30
+ }
31
+ function registerAuth(program) {
32
+ const auth = program.command("auth").description("Manage sandbox API credentials");
33
+ auth.command("login").description("Authenticate with sandbox-api (browser OAuth or direct API key)").option("--key <key>", "API key for direct login (skips browser)").option("--url <url>", "Sandbox API base URL", DEFAULT_API_URL).option("--name <name>", "Your default display name in games", "Player").action(async (opts) => {
34
+ if (opts.key) {
35
+ const client2 = createClient({ apiUrl: opts.url, apiKey: opts.key });
36
+ process.stdout.write("Verifying credentials\u2026 ");
37
+ try {
38
+ const health = await client2.health();
39
+ console.log(`OK (${health.service})`);
40
+ } catch (e) {
41
+ console.log(`FAILED`);
42
+ console.error(`Could not reach ${opts.url}: ${e}`);
43
+ console.error("Check that sandbox-api is running and the URL is correct.");
44
+ process.exit(1);
45
+ }
46
+ saveConfig({
47
+ api_key: opts.key,
48
+ api_url: opts.url,
49
+ display_name: opts.name,
50
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
51
+ });
52
+ console.log(`
53
+ Credentials saved to ~/.rentline-sandbox/credentials.json`);
54
+ console.log(`API URL: ${opts.url}`);
55
+ console.log(`Display name: ${opts.name}`);
56
+ return;
57
+ }
58
+ console.log("\nOpening browser to sign in with Clerk\u2026");
59
+ console.log(`
60
+ ${CLI_AUTH_URL}
61
+ `);
62
+ console.log("If the browser does not open automatically, visit the URL above.\n");
63
+ try {
64
+ await openBrowser(CLI_AUTH_URL);
65
+ } catch {
66
+ }
67
+ const rawKey = await prompt("Paste the key shown in the browser and press Enter: ");
68
+ if (!rawKey || rawKey.length < 8) {
69
+ console.error("No key provided. Login cancelled.");
70
+ process.exit(1);
71
+ }
72
+ const client = createClient({ apiUrl: opts.url, apiKey: rawKey });
73
+ process.stdout.write("Verifying key\u2026 ");
74
+ try {
75
+ const health = await client.health();
76
+ console.log(`OK (${health.service})`);
77
+ } catch (e) {
78
+ console.log(`FAILED`);
79
+ console.error(`Could not reach ${opts.url}: ${e}`);
80
+ console.error("Check that sandbox-api is running and the URL is correct.");
81
+ process.exit(1);
82
+ }
83
+ saveConfig({
84
+ api_key: rawKey,
85
+ api_url: opts.url,
86
+ display_name: opts.name,
87
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
88
+ });
89
+ console.log(`
90
+ Logged in. Credentials saved to ~/.rentline-sandbox/credentials.json`);
91
+ console.log(`API URL: ${opts.url}`);
92
+ console.log(`Display name: ${opts.name}`);
93
+ });
94
+ auth.command("logout").description("Remove saved credentials").action(() => {
95
+ deleteConfig();
96
+ console.log("Logged out. Credentials removed.");
97
+ });
98
+ auth.command("whoami").description("Show current credentials and API connectivity").action(async () => {
99
+ const cfg = loadConfig();
100
+ if (!cfg) {
101
+ console.log("Not logged in. Run: sandbox auth login");
102
+ return;
103
+ }
104
+ console.log(`API URL: ${cfg.api_url}`);
105
+ console.log(`Display name: ${cfg.display_name ?? "(not set)"}`);
106
+ console.log(`Key prefix: ${cfg.api_key.slice(0, 8)}\u2026`);
107
+ const client = createClient({ apiUrl: cfg.api_url, apiKey: cfg.api_key });
108
+ process.stdout.write("Connectivity: ");
109
+ try {
110
+ const h = await client.health();
111
+ console.log(`\u2713 ${h.service} is reachable`);
112
+ } catch {
113
+ console.log(`\u2717 Cannot reach ${cfg.api_url}`);
114
+ }
115
+ });
116
+ }
117
+ export {
118
+ registerAuth
119
+ };
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/config.ts
4
+ import { readFileSync, writeFileSync, mkdirSync, chmodSync, unlinkSync } from "fs";
5
+ import { homedir } from "os";
6
+ import { join } from "path";
7
+ var CONFIG_DIR = join(homedir(), ".rentline-sandbox");
8
+ var CONFIG_FILE = join(CONFIG_DIR, "credentials.json");
9
+ var DEFAULT_API_URL = "https://sandbox-api.rentline.xyz";
10
+ function loadConfig() {
11
+ try {
12
+ const raw = readFileSync(CONFIG_FILE, "utf-8");
13
+ return JSON.parse(raw);
14
+ } catch {
15
+ return null;
16
+ }
17
+ }
18
+ function requireConfig() {
19
+ const cfg = loadConfig();
20
+ if (!cfg) {
21
+ console.error(
22
+ "Not authenticated. Run:\n\n sandbox auth login --key <your-api-key>\n"
23
+ );
24
+ process.exit(1);
25
+ }
26
+ return cfg;
27
+ }
28
+ function saveConfig(cfg) {
29
+ mkdirSync(CONFIG_DIR, { recursive: true });
30
+ writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2), { mode: 384 });
31
+ try {
32
+ chmodSync(CONFIG_FILE, 384);
33
+ } catch {
34
+ }
35
+ }
36
+ function deleteConfig() {
37
+ try {
38
+ unlinkSync(CONFIG_FILE);
39
+ } catch {
40
+ }
41
+ }
42
+ function getApiUrl(override) {
43
+ const cfg = loadConfig();
44
+ return override || process.env.SANDBOX_API_URL || cfg?.api_url || DEFAULT_API_URL;
45
+ }
46
+ function getApiKey(override) {
47
+ const cfg = loadConfig();
48
+ return override || process.env.SANDBOX_API_KEY || cfg?.api_key;
49
+ }
50
+
51
+ export {
52
+ DEFAULT_API_URL,
53
+ loadConfig,
54
+ requireConfig,
55
+ saveConfig,
56
+ deleteConfig,
57
+ getApiUrl,
58
+ getApiKey
59
+ };
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/client.ts
4
+ async function request(opts, method, path, body) {
5
+ const url = `${opts.apiUrl.replace(/\/$/, "")}${path}`;
6
+ const headers = {
7
+ "Content-Type": "application/json"
8
+ };
9
+ if (opts.apiKey) {
10
+ headers["X-API-Key"] = opts.apiKey;
11
+ }
12
+ const res = await fetch(url, {
13
+ method,
14
+ headers,
15
+ body: body !== void 0 ? JSON.stringify(body) : void 0
16
+ });
17
+ if (!res.ok) {
18
+ let detail = res.statusText;
19
+ try {
20
+ const j = await res.json();
21
+ detail = j.detail ?? JSON.stringify(j);
22
+ } catch {
23
+ }
24
+ throw new Error(`[${res.status}] ${detail}`);
25
+ }
26
+ if (res.status === 204) return void 0;
27
+ return res.json();
28
+ }
29
+ function createClient(opts) {
30
+ const r = (method, path, body) => request(opts, method, path, body);
31
+ return {
32
+ // ── Health ───────────────────────────────────────────────────────────────
33
+ health: () => r("GET", "/health"),
34
+ // ── Games ────────────────────────────────────────────────────────────────
35
+ listGames: () => r("GET", "/api/sandbox/games"),
36
+ getGame: (id) => r("GET", `/api/sandbox/games/${id}`),
37
+ createGame: (body) => r("POST", "/api/sandbox/games", body),
38
+ joinGame: (id, body) => r("POST", `/api/sandbox/games/${id}/join`, body),
39
+ leaveGame: (id) => r("DELETE", `/api/sandbox/games/${id}/leave`),
40
+ markReady: (id) => r("POST", `/api/sandbox/games/${id}/ready`),
41
+ advanceTurn: (id) => r("POST", `/api/sandbox/games/${id}/advance-turn`),
42
+ getFeed: (id, params) => {
43
+ const q = new URLSearchParams();
44
+ if (params?.turn !== void 0) q.set("turn", String(params.turn));
45
+ if (params?.limit !== void 0) q.set("limit", String(params.limit));
46
+ const qs = q.toString() ? `?${q.toString()}` : "";
47
+ return r("GET", `/api/sandbox/games/${id}/feed${qs}`);
48
+ },
49
+ getLeaderboard: (id) => r("GET", `/api/sandbox/games/${id}/leaderboard`),
50
+ getGlobalLeaderboard: (limit = 50) => r("GET", `/api/sandbox/leaderboard?limit=${limit}`),
51
+ // ── Portfolio / debt ─────────────────────────────────────────────────────
52
+ getPortfolio: (gameId, playerId) => r("GET", `/api/sandbox/games/${gameId}/portfolio/${playerId}`),
53
+ getDebt: (gameId, playerId) => r("GET", `/api/sandbox/games/${gameId}/debt/${playerId}`),
54
+ // ── Trading ──────────────────────────────────────────────────────────────
55
+ trade: (gameId, body) => r("POST", `/api/sandbox/games/${gameId}/trade`, body),
56
+ // ── Mortgage ─────────────────────────────────────────────────────────────
57
+ originateMortgage: (gameId, body) => r("POST", `/api/sandbox/games/${gameId}/mortgage`, body),
58
+ refi: (gameId, body) => r("POST", `/api/sandbox/games/${gameId}/refi`, body),
59
+ helocDraw: (gameId, body) => r("POST", `/api/sandbox/games/${gameId}/heloc/draw`, body),
60
+ helocRepay: (gameId, body) => r("POST", `/api/sandbox/games/${gameId}/heloc/repay`, body),
61
+ prepayPrincipal: (gameId, body) => r("POST", `/api/sandbox/games/${gameId}/prepay-principal`, body),
62
+ improveProperty: (gameId, body) => r("POST", `/api/sandbox/games/${gameId}/improve-property`, body),
63
+ originatePaceLien: (gameId, body) => r("POST", `/api/sandbox/games/${gameId}/pace-lien`, body),
64
+ // ── Fed ──────────────────────────────────────────────────────────────────
65
+ getFedHistory: (gameId) => r("GET", `/api/sandbox/games/${gameId}/fed`),
66
+ // ── Property pool (admin) ────────────────────────────────────────────────
67
+ listProperties: (activeOnly = true) => r("GET", `/api/sandbox/properties?active_only=${activeOnly}`),
68
+ syncProperties: () => r("POST", "/api/sandbox/properties/sync"),
69
+ mintTusdc: (gameId, playerId, amount) => r("POST", `/api/sandbox/games/${gameId}/mint-tusdc`, { player_id: playerId, amount }),
70
+ // ── Bots ─────────────────────────────────────────────────────────────────
71
+ addBot: (gameId, body) => r("POST", `/api/sandbox/games/${gameId}/bots`, body),
72
+ removeBot: (gameId, botPlayerId) => r("DELETE", `/api/sandbox/games/${gameId}/bots/${botPlayerId}`),
73
+ // ── Autonomous mode ───────────────────────────────────────────────────────
74
+ startAutonomous: (gameId, delaySeconds) => r("POST", `/api/sandbox/games/${gameId}/autonomous`, { delay_seconds: delaySeconds ?? 30 }),
75
+ stopAutonomous: (gameId) => r("DELETE", `/api/sandbox/games/${gameId}/autonomous`),
76
+ // ── Agent delegation ──────────────────────────────────────────────────────
77
+ setDelegate: (gameId, body) => r("POST", `/api/sandbox/games/${gameId}/delegate`, body),
78
+ // ── API keys ──────────────────────────────────────────────────────────────
79
+ createApiKey: (name = "CLI key") => r("POST", "/api/sandbox/api-keys", { name }),
80
+ // ── Market & Intel ────────────────────────────────────────────────────────
81
+ getMarketSummary: (gameId) => r("GET", `/api/sandbox/games/${gameId}/market-summary`),
82
+ getPlayerActions: (gameId, playerId, limit, turn) => r("GET", `/api/sandbox/games/${gameId}/players/${playerId}/actions${limit || turn ? `?${new URLSearchParams({ ...limit ? { limit: String(limit) } : {}, ...turn !== void 0 ? { turn: String(turn) } : {} })}` : ""}`),
83
+ spectate: (gameId) => r("GET", `/api/sandbox/games/${gameId}/spectate`),
84
+ createGameFromPreset: (body) => r("POST", "/api/sandbox/games/from-preset", body)
85
+ };
86
+ }
87
+
88
+ export {
89
+ createClient
90
+ };
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ getApiKey,
4
+ getApiUrl,
5
+ requireConfig
6
+ } from "./chunk-Q5AKRRZ2.js";
7
+ import {
8
+ createClient
9
+ } from "./chunk-X3OZHOPM.js";
10
+
11
+ // src/commands/game.ts
12
+ function client(cmd) {
13
+ const opts = cmd.optsWithGlobals();
14
+ const cfg = requireConfig();
15
+ return createClient({ apiUrl: getApiUrl(opts.url), apiKey: getApiKey(opts.apiKey) ?? cfg.api_key });
16
+ }
17
+ function fmt(obj) {
18
+ console.log(JSON.stringify(obj, null, 2));
19
+ }
20
+ function registerGame(program) {
21
+ const game = program.command("game").description("Manage sandbox game rooms");
22
+ game.command("list").description("List open game rooms (lobby, trading, advancing)").action(async (_, cmd) => {
23
+ fmt(await client(cmd).listGames());
24
+ });
25
+ game.command("create").description("Create a new game room").requiredOption("--name <name>", "Game name").option("--display-name <n>", "Your display name in this game").option("--turns <n>", "Max turns (default 12)", "12").option("--balance <n>", "Starting tUSDC per player", "100000").option("--ltv <n>", "LTV limit 0.0-0.95 (default 0.70)", "0.70").option("--rate-type <t>", "Default mortgage rate type: fixed|arm", "fixed").option("--amortizing", "Enable amortizing mortgages (default: interest-only)").option("--fed-interval <n>", "Fed meeting every N turns (0=disabled)", "6").option("--properties <ids...>", "Specific property IDs to include (default: all active)").option("--auto-advance", "Start autonomous mode immediately after creation").option("--auto-advance-delay <n>", "Seconds between auto-advance turns (5-3600, default 30)", "30").action(async (opts, cmd) => {
26
+ const cfg = requireConfig();
27
+ const body = {
28
+ name: opts.name,
29
+ display_name: opts.displayName ?? cfg.display_name ?? "Host",
30
+ max_turns: parseInt(opts.turns),
31
+ starting_balance_usdc: parseFloat(opts.balance),
32
+ ltv_limit: parseFloat(opts.ltv),
33
+ default_rate_type: opts.rateType,
34
+ amortizing: !!opts.amortizing,
35
+ fed_meeting_interval: parseInt(opts.fedInterval),
36
+ property_ids: opts.properties,
37
+ auto_advance: !!opts.autoAdvance,
38
+ auto_advance_delay_seconds: parseInt(opts.autoAdvanceDelay)
39
+ };
40
+ const g = await client(cmd).createGame(body);
41
+ console.log(`
42
+ Game created: ${g.id}`);
43
+ console.log(`Invite code: ${g.invite_code}`);
44
+ console.log(`Share link: sandbox.rentline.xyz/join/${g.invite_code}
45
+ `);
46
+ fmt(g);
47
+ });
48
+ game.command("get <id>").description("Get full game state (players, properties, status)").action(async (id, _, cmd) => {
49
+ fmt(await client(cmd).getGame(id));
50
+ });
51
+ game.command("join <id>").description("Join a game via invite code").requiredOption("--invite <code>", "Invite code").option("--name <n>", "Your display name").action(async (id, opts, cmd) => {
52
+ const cfg = requireConfig();
53
+ const res = await client(cmd).joinGame(id, {
54
+ invite_code: opts.invite,
55
+ display_name: opts.name ?? cfg.display_name ?? "Player"
56
+ });
57
+ console.log(`Joined game ${res.game_id} as player ${res.player_id}`);
58
+ });
59
+ game.command("leave <id>").description("Leave a game (only before it starts)").action(async (id, _, cmd) => {
60
+ await client(cmd).leaveGame(id);
61
+ console.log(`Left game ${id}`);
62
+ });
63
+ game.command("ready <id>").description("Toggle your ready state for the next turn").action(async (id, _, cmd) => {
64
+ const res = await client(cmd).markReady(id);
65
+ console.log(`Ready: ${res.is_ready}`);
66
+ });
67
+ game.command("advance <id>").description("Advance the game by one turn (host only \u2014 runs all engine phases)").action(async (id, _, cmd) => {
68
+ const res = await client(cmd).advanceTurn(id);
69
+ console.log(`Turn ${res.current_turn}/${res.max_turns} \u2014 status: ${res.status}`);
70
+ });
71
+ game.command("feed <id>").description("Show the turn event feed (macro events, rent, price moves, debt service)").option("--turn <n>", "Filter by turn number").option("--limit <n>", "Max events to show", "30").action(async (id, opts, cmd) => {
72
+ const events = await client(cmd).getFeed(id, {
73
+ turn: opts.turn !== void 0 ? parseInt(opts.turn) : void 0,
74
+ limit: parseInt(opts.limit)
75
+ });
76
+ if (!events.length) {
77
+ console.log("No events yet.");
78
+ return;
79
+ }
80
+ for (const e of events) {
81
+ const sign = e.delta_usdc > 0 ? "+" : "";
82
+ const money = e.delta_usdc !== 0 ? ` [${sign}$${Math.abs(e.delta_usdc).toFixed(2)}]` : "";
83
+ console.log(`T${e.turn} ${e.event_type.padEnd(18)} ${e.description}${money}`);
84
+ }
85
+ });
86
+ game.command("leaderboard [id]").description("Show leaderboard for a game (or global all-time if no game ID given)").option("--limit <n>", "Number of entries (global only)", "50").action(async (id, opts, cmd) => {
87
+ const rows = id ? await client(cmd).getLeaderboard(id) : await client(cmd).getGlobalLeaderboard(parseInt(opts.limit));
88
+ console.log(`
89
+ ${"#".padEnd(4)}${"Player".padEnd(22)}${"NAV".padStart(14)}${"Cash".padStart(14)}`);
90
+ console.log("\u2500".repeat(54));
91
+ for (const r of rows) {
92
+ console.log(
93
+ `${String(r.rank).padEnd(4)}${r.display_name.padEnd(22)}$${r.nav.toLocaleString("en-US", { maximumFractionDigits: 2 }).padStart(13)}$${r.usdc_balance.toLocaleString("en-US", { maximumFractionDigits: 2 }).padStart(13)}`
94
+ );
95
+ }
96
+ });
97
+ game.command("fed <id>").description("Show FOMC decision history for a game").action(async (id, _, cmd) => {
98
+ const history = await client(cmd).getFedHistory(id);
99
+ if (!history.length) {
100
+ console.log("No Fed decisions yet.");
101
+ return;
102
+ }
103
+ for (const d of history) {
104
+ const arrow = d.outcome === "hike" ? "\u2191" : d.outcome === "cut" ? "\u2193" : "\u2192";
105
+ console.log(
106
+ `T${d.turn} ${arrow} ${d.outcome.toUpperCase().padEnd(5)} ${d.move_bps > 0 ? "+" : ""}${d.move_bps}bps Fed: ${(d.rate_after * 100).toFixed(2)}% Mortgage: ${(d.mortgage_rate_after * 100).toFixed(2)}%`
107
+ );
108
+ console.log(` ${d.statement}`);
109
+ }
110
+ });
111
+ game.command("autonomous <id>").description("Enable autonomous mode \u2014 game advances turns automatically (host only)").option("--delay <n>", "Seconds between turns (5\u20133600, default 30)", "30").action(async (id, opts, cmd) => {
112
+ const res = await client(cmd).startAutonomous(id, parseInt(opts.delay));
113
+ console.log(`Autonomous mode enabled for game ${id}`);
114
+ console.log(`Delay: ${res.auto_advance_delay_seconds}s between turns`);
115
+ console.log(res.message);
116
+ });
117
+ game.command("stop-autonomous <id>").description("Disable autonomous mode \u2014 game waits for manual advance-turn (host only)").action(async (id, _, cmd) => {
118
+ const res = await client(cmd).stopAutonomous(id);
119
+ console.log(`Autonomous mode disabled for game ${id}`);
120
+ console.log(res.message);
121
+ });
122
+ game.command("delegate <id>").description("Opt in to agent delegation \u2014 an LLM acts for you if you don't move in time").option("--strategy <s>", "Bot strategy to use: aggressive|conservative|balanced|momentum|income", "balanced").option("--off", "Opt out of delegation").action(async (id, opts, cmd) => {
123
+ const res = await client(cmd).setDelegate(id, {
124
+ agent_delegate: !opts.off,
125
+ delegate_strategy: opts.strategy
126
+ });
127
+ console.log(res.message);
128
+ console.log(`Strategy: ${res.delegate_strategy}`);
129
+ });
130
+ }
131
+ export {
132
+ registerGame
133
+ };
package/dist/index.js ADDED
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+ import dotenv from "dotenv";
6
+ import { createRequire } from "module";
7
+ dotenv.config();
8
+ var _require = createRequire(import.meta.url);
9
+ var { version } = _require("../package.json");
10
+ var args = process.argv.slice(2);
11
+ if (args[0] === "setup" || args[0] === "--setup") {
12
+ const { runSetup, parseSetupArgs } = await import("./setup-JPLRGUPW.js");
13
+ const opts = parseSetupArgs(args.filter((a) => a !== "setup" && a !== "--setup"));
14
+ await runSetup(opts);
15
+ process.exit(0);
16
+ }
17
+ if (args.length === 0 || args[0] === "server" || args[0] === "--server") {
18
+ const { startServer } = await import("./server-NRWOBJJN.js");
19
+ await startServer();
20
+ } else {
21
+ const program = new Command();
22
+ program.name("sandbox").description("Rentline Sandbox \u2014 CLI and MCP server for the real estate simulation game").version(version).option("--url <url>", "Sandbox API base URL (overrides saved config)").option("--api-key <key>", "API key (overrides saved config)");
23
+ const { registerAuth } = await import("./auth-M7RU2YB3.js");
24
+ const { registerGame } = await import("./game-KCQW5KOZ.js");
25
+ const { registerTrade } = await import("./trade-5BAZ6QFQ.js");
26
+ const { registerMortgage } = await import("./mortgage-7UCDSSUV.js");
27
+ const { registerAdmin } = await import("./admin-YB2G7ZYZ.js");
28
+ registerAuth(program);
29
+ registerGame(program);
30
+ registerTrade(program);
31
+ registerMortgage(program);
32
+ registerAdmin(program);
33
+ program.command("mcp-setup", { hidden: true }).allowUnknownOption().action(async () => {
34
+ const { runSetup, parseSetupArgs } = await import("./setup-JPLRGUPW.js");
35
+ const opts = parseSetupArgs(process.argv.slice(3));
36
+ await runSetup(opts);
37
+ });
38
+ await program.parseAsync(process.argv);
39
+ }
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ getApiKey,
4
+ getApiUrl,
5
+ requireConfig
6
+ } from "./chunk-Q5AKRRZ2.js";
7
+ import {
8
+ createClient
9
+ } from "./chunk-X3OZHOPM.js";
10
+
11
+ // src/commands/mortgage.ts
12
+ function client(cmd) {
13
+ const opts = cmd.optsWithGlobals();
14
+ const cfg = requireConfig();
15
+ return createClient({ apiUrl: getApiUrl(opts.url), apiKey: getApiKey(opts.apiKey) ?? cfg.api_key });
16
+ }
17
+ function printMortgage(m) {
18
+ console.log(`
19
+ Mortgage: ${m.id}`);
20
+ console.log(` Type: ${m.mortgage_type}`);
21
+ console.log(` Status: ${m.status}`);
22
+ console.log(` Balance: $${m.current_balance.toLocaleString("en-US", { maximumFractionDigits: 2 })}`);
23
+ console.log(` Rate: ${(m.current_rate * 100).toFixed(3)}% ${m.rate_type.toUpperCase()} ${m.amortizing ? "(amortizing)" : "(interest-only)"}`);
24
+ console.log(` Monthly pmt: $${m.monthly_payment.toFixed(2)}`);
25
+ if (m.credit_limit !== null) {
26
+ console.log(` HELOC limit: $${m.credit_limit.toFixed(2)} drawn: $${(m.drawn_balance ?? 0).toFixed(2)}`);
27
+ }
28
+ if (m.turns_in_arrears > 0) {
29
+ console.log(` \u26A0 IN ARREARS: ${m.turns_in_arrears} turn(s)`);
30
+ }
31
+ console.log(` Interest paid: $${m.total_interest_paid.toFixed(2)} Principal paid: $${m.total_principal_paid.toFixed(2)}`);
32
+ }
33
+ function registerMortgage(program) {
34
+ const mtg = program.command("mortgage").alias("mtg").description("Leveraged buying, refinancing, and HELOC operations");
35
+ mtg.command("buy <game-id>").description("Buy tokens using an acquisition mortgage (down payment + financing)").requiredOption("--property <id>", "Property ID from the game pool").requiredOption("--tokens <n>", "Tokens to purchase (LTV limit applies to purchase price)").option("--rate-type <t>", "fixed or arm", "fixed").action(async (gameId, opts, cmd) => {
36
+ const res = await client(cmd).originateMortgage(gameId, {
37
+ property_id: opts.property,
38
+ tokens_to_buy: parseFloat(opts.tokens),
39
+ rate_type: opts.rateType
40
+ });
41
+ console.log(`Acquisition mortgage originated`);
42
+ printMortgage(res);
43
+ });
44
+ mtg.command("refi <game-id>").description("Refinance existing first lien (rate-and-term or cash-out)").requiredOption("--property <id>", "Property ID").option("--cash-out <n>", "Cash-out amount in USD (0 = rate-and-term refi)", "0").option("--rate-type <t>", "New rate type: fixed or arm").action(async (gameId, opts, cmd) => {
45
+ const res = await client(cmd).refi(gameId, {
46
+ property_id: opts.property,
47
+ cash_out_amount: parseFloat(opts.cashOut),
48
+ new_rate_type: opts.rateType
49
+ });
50
+ const type = parseFloat(opts.cashOut) > 0 ? "Cash-out refi" : "Rate-and-term refi";
51
+ console.log(`${type} completed`);
52
+ printMortgage(res);
53
+ });
54
+ mtg.command("heloc <game-id>").description("Draw from a HELOC (opens one if none exists)").requiredOption("--property <id>", "Property ID").requiredOption("--draw <n>", "Amount to draw in USD").action(async (gameId, opts, cmd) => {
55
+ const res = await client(cmd).helocDraw(gameId, {
56
+ property_id: opts.property,
57
+ draw_amount: parseFloat(opts.draw)
58
+ });
59
+ console.log(`HELOC draw of $${parseFloat(opts.draw).toFixed(2)} complete`);
60
+ printMortgage(res);
61
+ });
62
+ mtg.command("repay <game-id>").description("Repay drawn HELOC balance (reduces interest cost)").requiredOption("--property <id>", "Property ID").requiredOption("--amount <n>", "Amount to repay in USD").action(async (gameId, opts, cmd) => {
63
+ const res = await client(cmd).helocRepay(gameId, {
64
+ property_id: opts.property,
65
+ repay_amount: parseFloat(opts.amount)
66
+ });
67
+ console.log(`HELOC repayment of $${parseFloat(opts.amount).toFixed(2)} applied`);
68
+ printMortgage(res);
69
+ });
70
+ mtg.command("list <game-id> <player-id>").description("List all mortgages (active and historical) for a player").action(async (gameId, playerId, cmd) => {
71
+ const mortgages = await client(cmd).getDebt(gameId, playerId);
72
+ if (!mortgages.length) {
73
+ console.log("No mortgages on record.");
74
+ return;
75
+ }
76
+ const active = mortgages.filter((m) => m.status === "active");
77
+ const inactive = mortgages.filter((m) => m.status !== "active");
78
+ console.log(`
79
+ Active (${active.length}):`);
80
+ for (const m of active) printMortgage(m);
81
+ if (inactive.length) {
82
+ console.log(`
83
+ Inactive (${inactive.length}):`);
84
+ for (const m of inactive) {
85
+ console.log(` ${m.id.slice(0, 8)} ${m.mortgage_type} ${m.status} balance=$${m.current_balance.toFixed(2)}`);
86
+ }
87
+ }
88
+ });
89
+ }
90
+ export {
91
+ registerMortgage
92
+ };