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,215 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ DEFAULT_API_URL,
4
+ loadConfig,
5
+ saveConfig
6
+ } from "./chunk-Q5AKRRZ2.js";
7
+ import {
8
+ createClient
9
+ } from "./chunk-X3OZHOPM.js";
10
+
11
+ // src/setup.ts
12
+ import { createInterface } from "readline";
13
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, copyFileSync } from "fs";
14
+ import { homedir, platform } from "os";
15
+ import { join, dirname } from "path";
16
+ import { fileURLToPath } from "url";
17
+ var __filename = fileURLToPath(import.meta.url);
18
+ var __dirname = dirname(__filename);
19
+ function parseSetupArgs(args) {
20
+ const opts = {};
21
+ for (let i = 0; i < args.length; i++) {
22
+ const a = args[i];
23
+ if (a === "--key" || a === "-k") opts.key = args[++i];
24
+ else if (a === "--url" || a === "-u") opts.url = args[++i];
25
+ else if (a === "--name") opts.name = args[++i];
26
+ else if (a === "--client" || a === "-c") opts.client = args[++i];
27
+ else if (a === "--scope") opts.scope = args[++i];
28
+ else if (a === "--yes" || a === "-y") opts.yes = true;
29
+ }
30
+ return opts;
31
+ }
32
+ async function runSetup(opts) {
33
+ console.log("\nRentline Sandbox \u2014 MCP Setup\n");
34
+ const rl = opts.yes ? null : createInterface({ input: process.stdin, output: process.stdout });
35
+ const ask = (q, fallback = "") => {
36
+ if (opts.yes || !rl) return Promise.resolve(fallback);
37
+ return new Promise((resolve) => {
38
+ rl.question(q, (ans) => resolve(ans.trim() || fallback));
39
+ });
40
+ };
41
+ let apiKey = opts.key;
42
+ if (!apiKey) {
43
+ const existing2 = loadConfig();
44
+ if (existing2?.api_key && opts.yes) {
45
+ apiKey = existing2.api_key;
46
+ } else {
47
+ apiKey = await ask(
48
+ `Enter your sandbox API key${existing2?.api_key ? " (press Enter to keep existing)" : ""}: `,
49
+ existing2?.api_key ?? ""
50
+ );
51
+ }
52
+ }
53
+ if (!apiKey) {
54
+ console.error("API key is required. Get one from your sandbox-api admin.");
55
+ process.exit(1);
56
+ }
57
+ const existing = loadConfig();
58
+ const apiUrl = opts.url ?? await ask(
59
+ `Sandbox API URL [${existing?.api_url ?? DEFAULT_API_URL}]: `,
60
+ existing?.api_url ?? DEFAULT_API_URL
61
+ );
62
+ const displayName = opts.name ?? await ask(
63
+ `Your default display name [${existing?.display_name ?? "Player"}]: `,
64
+ existing?.display_name ?? "Player"
65
+ );
66
+ process.stdout.write("Verifying connectivity\u2026 ");
67
+ try {
68
+ const c = createClient({ apiUrl, apiKey });
69
+ const h = await c.health();
70
+ console.log(`OK (${h.service})`);
71
+ } catch (e) {
72
+ console.log("FAILED");
73
+ console.error(`Cannot reach ${apiUrl}: ${e}`);
74
+ if (!opts.yes) {
75
+ const cont = await ask("Save config anyway? [y/N]: ", "n");
76
+ if (cont.toLowerCase() !== "y") {
77
+ rl?.close();
78
+ process.exit(1);
79
+ }
80
+ }
81
+ }
82
+ saveConfig({ api_key: apiKey, api_url: apiUrl, display_name: displayName, created_at: (/* @__PURE__ */ new Date()).toISOString() });
83
+ console.log("Credentials saved.\n");
84
+ let clientName = opts.client ?? detectClient();
85
+ if (!clientName) {
86
+ console.log("Which MCP client do you use?");
87
+ const clients = ["claude-code", "claude-desktop", "cursor", "windsurf", "opencode", "zed", "cline", "other"];
88
+ clients.forEach((c, i) => console.log(` ${i + 1}. ${c}`));
89
+ const choice = await ask("Enter number or name: ", "other");
90
+ const idx = parseInt(choice);
91
+ clientName = isNaN(idx) ? choice : clients[idx - 1] ?? "other";
92
+ }
93
+ await installForClient(clientName, opts.scope ?? "user", apiKey, apiUrl, displayName);
94
+ rl?.close();
95
+ console.log("\nSetup complete. Restart your AI client to load the Rentline Sandbox MCP server.\n");
96
+ }
97
+ function detectClient() {
98
+ const env = process.env;
99
+ if (env.CLAUDE_CODE || env.ANTHROPIC_CLAUDE_CODE) return "claude-code";
100
+ if (env.CURSOR_TRACE_ID || env.CURSOR_SESSION_ID) return "cursor";
101
+ if (env.WINDSURF_SESSION) return "windsurf";
102
+ if (env.OPENCODE_PROJECT || env.OPENCODE_SESSION) return "opencode";
103
+ return void 0;
104
+ }
105
+ var MCP_SERVER_ENTRY = {
106
+ command: "npx",
107
+ args: ["-y", "rentline-sandbox"],
108
+ env: {}
109
+ };
110
+ function mcpEntry(apiKey, apiUrl) {
111
+ return {
112
+ ...MCP_SERVER_ENTRY,
113
+ env: {
114
+ SANDBOX_API_KEY: apiKey,
115
+ SANDBOX_API_URL: apiUrl
116
+ }
117
+ };
118
+ }
119
+ async function installForClient(clientName, scope, apiKey, apiUrl, displayName) {
120
+ const entry = mcpEntry(apiKey, apiUrl);
121
+ switch (clientName) {
122
+ case "claude-code": {
123
+ const { execSync } = await import("child_process");
124
+ const envFlags = Object.entries(entry.env ?? {}).map(([k, v]) => `-e ${k}=${v}`).join(" ");
125
+ const cmd = `claude mcp add rentline-sandbox --scope ${scope} ${envFlags} -- npx -y rentline-sandbox`;
126
+ try {
127
+ execSync(cmd, { stdio: "pipe" });
128
+ console.log(`Installed via claude CLI (scope=${scope})`);
129
+ } catch {
130
+ const file = join(homedir(), ".claude.json");
131
+ patchMcpJson(file, "rentline-sandbox", entry, "mcpServers");
132
+ console.log(`Patched ${file}`);
133
+ }
134
+ const skillSrc = join(__dirname, "../SKILL.md");
135
+ if (existsSync(skillSrc)) {
136
+ const targets = [
137
+ join(homedir(), ".claude", "skills", "rentline-sandbox"),
138
+ join(homedir(), ".agents", "skills", "rentline-sandbox")
139
+ ];
140
+ for (const dir of targets) {
141
+ mkdirSync(dir, { recursive: true });
142
+ copyFileSync(skillSrc, join(dir, "SKILL.md"));
143
+ console.log(`SKILL.md \u2192 ${dir}`);
144
+ }
145
+ }
146
+ break;
147
+ }
148
+ case "claude-desktop": {
149
+ const file = platform() === "win32" ? join(process.env.APPDATA ?? homedir(), "Claude", "claude_desktop_config.json") : join(homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
150
+ patchMcpJson(file, "rentline-sandbox", entry, "mcpServers");
151
+ console.log(`Patched ${file}`);
152
+ break;
153
+ }
154
+ case "cursor": {
155
+ const file = scope === "project" ? join(process.cwd(), ".cursor", "mcp.json") : join(homedir(), ".cursor", "mcp.json");
156
+ patchMcpJson(file, "rentline-sandbox", entry, "mcpServers");
157
+ console.log(`Patched ${file}`);
158
+ break;
159
+ }
160
+ case "windsurf": {
161
+ const file = join(process.cwd(), ".windsurf", "mcp.json");
162
+ patchMcpJson(file, "rentline-sandbox", entry, "mcpServers");
163
+ console.log(`Patched ${file}`);
164
+ break;
165
+ }
166
+ case "opencode": {
167
+ const file = scope === "project" ? join(process.cwd(), "opencode.json") : join(homedir(), ".config", "opencode", "config.json");
168
+ patchMcpJson(file, "rentline-sandbox", entry, "mcp");
169
+ console.log(`Patched ${file}`);
170
+ const skillSrc = join(__dirname, "../SKILL.md");
171
+ if (existsSync(skillSrc)) {
172
+ const targets = [
173
+ join(homedir(), ".config", "opencode", "skills", "rentline-sandbox"),
174
+ join(homedir(), ".claude", "skills", "rentline-sandbox"),
175
+ join(homedir(), ".agents", "skills", "rentline-sandbox")
176
+ ];
177
+ for (const dir of targets) {
178
+ mkdirSync(dir, { recursive: true });
179
+ copyFileSync(skillSrc, join(dir, "SKILL.md"));
180
+ console.log(`SKILL.md \u2192 ${dir}`);
181
+ }
182
+ }
183
+ break;
184
+ }
185
+ case "zed":
186
+ case "cline":
187
+ case "warp":
188
+ case "other":
189
+ default: {
190
+ console.log(`
191
+ Add the following to your MCP client config:
192
+ `);
193
+ console.log(JSON.stringify({ "rentline-sandbox": entry }, null, 2));
194
+ break;
195
+ }
196
+ }
197
+ }
198
+ function patchMcpJson(filePath, serverName, entry, key) {
199
+ mkdirSync(dirname(filePath), { recursive: true });
200
+ let config = {};
201
+ if (existsSync(filePath)) {
202
+ try {
203
+ config = JSON.parse(readFileSync(filePath, "utf-8"));
204
+ } catch {
205
+ }
206
+ }
207
+ const servers = config[key] ?? {};
208
+ servers[serverName] = entry;
209
+ config[key] = servers;
210
+ writeFileSync(filePath, JSON.stringify(config, null, 2));
211
+ }
212
+ export {
213
+ parseSetupArgs,
214
+ runSetup
215
+ };
@@ -0,0 +1,67 @@
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/trade.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 registerTrade(program) {
18
+ const trade = program.command("trade").description("Buy and sell property tokens");
19
+ trade.command("buy <game-id>").description("Buy fractional property tokens (cash purchase, no mortgage)").requiredOption("--property <id>", "Property ID from the game pool").requiredOption("--tokens <n>", "Number of tokens to buy (fractions allowed)").action(async (gameId, opts, cmd) => {
20
+ const res = await client(cmd).trade(gameId, {
21
+ property_id: opts.property,
22
+ direction: "buy",
23
+ tokens: parseFloat(opts.tokens)
24
+ });
25
+ console.log(`BUY ${res.tokens} tokens @ $${res.price_per_token_usd?.toFixed(2)}`);
26
+ console.log(`Cost: $${res.amount_usdc?.toFixed(2)}`);
27
+ console.log(`Transaction: ${res.transaction_id}`);
28
+ });
29
+ trade.command("sell <game-id>").description("Sell fractional property tokens back to the pool").requiredOption("--property <id>", "Property ID").requiredOption("--tokens <n>", "Number of tokens to sell").action(async (gameId, opts, cmd) => {
30
+ const res = await client(cmd).trade(gameId, {
31
+ property_id: opts.property,
32
+ direction: "sell",
33
+ tokens: parseFloat(opts.tokens)
34
+ });
35
+ console.log(`SELL ${res.tokens} tokens @ $${res.price_per_token_usd?.toFixed(2)}`);
36
+ console.log(`Proceeds: $${res.amount_usdc?.toFixed(2)}`);
37
+ console.log(`Transaction: ${res.transaction_id}`);
38
+ });
39
+ program.command("portfolio <game-id> <player-id>").description("Show holdings, P&L, NAV, and leverage for a player").action(async (gameId, playerId, cmd) => {
40
+ const p = await client(cmd).getPortfolio(gameId, playerId);
41
+ console.log(`
42
+ Portfolio: ${p.display_name}`);
43
+ console.log(`${"\u2500".repeat(70)}`);
44
+ console.log(`Cash balance: $${p.usdc_balance.toLocaleString("en-US", { maximumFractionDigits: 2 })}`);
45
+ console.log(`Total debt: -$${p.total_debt.toLocaleString("en-US", { maximumFractionDigits: 2 })}`);
46
+ console.log(`Gross assets: $${p.gross_asset_value.toLocaleString("en-US", { maximumFractionDigits: 2 })}`);
47
+ console.log(`NAV: $${p.nav.toLocaleString("en-US", { maximumFractionDigits: 2 })}`);
48
+ console.log(`Leverage ratio: ${(p.leverage_ratio * 100).toFixed(1)}%`);
49
+ if (p.holdings.length) {
50
+ console.log(`
51
+ Holdings:`);
52
+ console.log(`${"Property".padEnd(30)}${"Tokens".padStart(10)}${"Value".padStart(14)}${"P&L".padStart(12)}${"Yield".padStart(10)}`);
53
+ console.log("\u2500".repeat(76));
54
+ for (const h of p.holdings) {
55
+ const pnlSign = h.unrealized_pnl_usd >= 0 ? "+" : "";
56
+ console.log(
57
+ `${(h.property_name ?? h.property_id).slice(0, 28).padEnd(30)}${h.tokens_held.toFixed(2).padStart(10)}$${h.current_value_usd.toFixed(2).padStart(13)}${pnlSign}$${h.unrealized_pnl_usd.toFixed(2).padStart(11)}$${h.total_rent_received_usd.toFixed(2).padStart(9)}`
58
+ );
59
+ }
60
+ } else {
61
+ console.log("\nNo holdings yet.");
62
+ }
63
+ });
64
+ }
65
+ export {
66
+ registerTrade
67
+ };
package/llms.txt ADDED
@@ -0,0 +1,78 @@
1
+ # Rentline Sandbox
2
+
3
+ Turn-based real estate investment simulation. Players compete over tokenised properties using
4
+ simulated tUSDC with mortgages, Fed rate cycles, macro events, property condition grades,
5
+ PACE liens, and investor tiers.
6
+
7
+ ## MCP server
8
+
9
+ Command: sandbox-mcp
10
+ Transport: stdio JSON-RPC (Model Context Protocol)
11
+
12
+ Required env vars:
13
+ - SANDBOX_API_URL — API base URL (e.g. https://sandbox-api.rentline.xyz)
14
+ - SANDBOX_API_KEY — sb_ user key or admin key
15
+
16
+ ## 35 tools
17
+
18
+ ### Game management
19
+ - list_games — list open game rooms (lobby/trading)
20
+ - get_game — full game state: players, properties, turn, Fed rate, settings
21
+ - create_game — create a game with full mortgage/Fed/grade config + optional bots
22
+ - create_game_from_preset — one-call presets: quick, standard, leveraged, distressed, long_run
23
+ - join_game — join via invite code
24
+ - mark_ready — toggle ready state; game advances when all humans ready
25
+ - advance_turn — host: run all 7 engine phases
26
+ - get_feed — turn event stream (Fed, macro, rent, price moves, debt service, turn summary)
27
+ - add_bot — add LLM bot to lobby (strategies: aggressive, conservative, balanced, momentum, income, value_add)
28
+ - remove_bot — remove bot from lobby
29
+ - start_autonomous — enable auto-advance (advances on all-ready or turn deadline)
30
+ - stop_autonomous — pause auto-advance
31
+ - set_delegate — opt in to agent delegation when idle
32
+ - spectate — public game snapshot (no auth required)
33
+
34
+ ### Market & Intel
35
+ - list_properties — active property pool with grades
36
+ - get_market_summary — live cap rates, price delta this turn, grade, vacancy, mechanics lien status
37
+ - get_fed_history — FOMC decision log with bps moves and mortgage rate impact
38
+ - get_player_actions — human-readable transaction timeline for a player
39
+
40
+ ### Trading
41
+ - buy_tokens — all-cash token purchase at current market price
42
+ - sell_tokens — sell tokens (proceeds pay mechanics lien → first lien → HELOC/PACE before cash credited)
43
+
44
+ ### Debt
45
+ - originate_mortgage — leveraged buy (tier-adjusted LTV and rate applied automatically)
46
+ - refi_mortgage — rate-and-term or cash-out refi
47
+ - heloc_draw — draw from home equity line of credit
48
+ - heloc_repay — repay HELOC balance
49
+ - prepay_principal — partial/full principal prepayment (works for first_lien, heloc, pace, mechanics_lien)
50
+ - improve_property — cash-funded grade upgrade (cost = steps × 8% × price, price bumps 5%/step)
51
+ - originate_pace_lien — financed grade upgrade, no down payment (rate = base + 1.5%)
52
+ - get_debt — all active mortgages: balances, LTV, arrears, type
53
+
54
+ ### Portfolio
55
+ - get_portfolio — NAV, holdings with grade/yield/turns_held, investor tier, leverage ratio
56
+ - get_leaderboard — game or global leaderboard ranked by NAV with tier names
57
+
58
+ ## Turn phases (in order)
59
+ 1. fed_meeting — hike/cut/hold; FED_WARNING fires 1 turn before; ARMs reprice immediately
60
+ 2. macro_events — rate macros take effect (1-turn warning system); active macros tick down
61
+ 3. rent_collect — proportional to tokens; grade multipliers applied; vacancy blocks
62
+ 4. random_events — vacancy, lease renewal, capex, appreciation/depreciation (all grade-adjusted)
63
+ 5. market_move — applies price drift; optional live AVM blend
64
+ 6. debt_service — collect monthly payments; forced sale after 1 grace turn
65
+ 7. distribute — credits rent to balances; emits TURN_SUMMARY event
66
+
67
+ ## Property grades
68
+ A (excellent) → B (good) → C (average) → D (below avg) → F (distressed)
69
+ Grade affects: rent multiplier, appreciation probability/range, capex risk, vacancy probability.
70
+ Upgrade via improve_property (cash) or originate_pace_lien (financed).
71
+
72
+ ## Investor tiers (live from NAV, auto-applied to mortgage terms)
73
+ Retail (<$100k) → Accredited ($100k) → Professional ($500k) → Institutional ($2.5M) → Developer ($25M)
74
+ Higher tiers: +5% LTV and -25bps rate per tier step.
75
+
76
+ ## Docs
77
+ https://sandbox.rentline.xyz
78
+ https://sandbox-api.rentline.xyz/docs
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "rentline-sandbox",
3
+ "version": "0.1.2",
4
+ "description": "CLI and MCP server for the Rentline Sandbox real estate investment simulation game",
5
+ "type": "module",
6
+ "bin": {
7
+ "sandbox": "dist/index.js",
8
+ "sandbox-mcp": "dist/server.js"
9
+ },
10
+ "main": "./dist/index.js",
11
+ "exports": {
12
+ ".": "./dist/index.js",
13
+ "./server": "./dist/server.js"
14
+ },
15
+ "scripts": {
16
+ "build": "tsup",
17
+ "dev": "tsup --watch",
18
+ "prepublishOnly": "npm run build"
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "LICENSE",
23
+ "SKILL.md",
24
+ "llms.txt",
25
+ "README.md"
26
+ ],
27
+ "dependencies": {
28
+ "@modelcontextprotocol/sdk": "^1.10.0",
29
+ "commander": "^13.0.0",
30
+ "dotenv": "^16.4.0"
31
+ },
32
+ "devDependencies": {
33
+ "@types/node": "^22.0.0",
34
+ "tsup": "^8.0.0",
35
+ "typescript": "^5.0.0"
36
+ },
37
+ "engines": {
38
+ "node": ">=18"
39
+ },
40
+ "license": "SEE LICENSE IN LICENSE",
41
+ "author": "Brandyn Hamilton",
42
+ "homepage": "https://sandbox.rentline.xyz",
43
+ "repository": {
44
+ "type": "git",
45
+ "url": "git+https://github.com/BrandynHamilton/rentline-sandbox.git",
46
+ "directory": "sandbox-cli"
47
+ },
48
+ "bugs": {
49
+ "url": "https://github.com/BrandynHamilton/rentline-sandbox/issues"
50
+ },
51
+ "publishConfig": {
52
+ "access": "public"
53
+ },
54
+ "keywords": [
55
+ "rentline",
56
+ "sandbox",
57
+ "real-estate",
58
+ "simulation",
59
+ "mcp",
60
+ "cli",
61
+ "model-context-protocol",
62
+ "ai-agent",
63
+ "game"
64
+ ]
65
+ }