rentline-sandbox 0.1.7 → 0.1.9

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/SKILL.md CHANGED
@@ -19,94 +19,17 @@ compatibility: opencode
19
19
 
20
20
  Turn-based real estate investment simulation — multiplayer, AI-agent-ready.
21
21
 
22
- ## Setup
23
-
24
- Get an API key at **sandbox.rentline.xyz/cli-auth**, then configure your client:
25
-
26
- **OpenCode** (`opencode.json`):
27
- ```json
28
- {
29
- "$schema": "https://opencode.ai/config.json",
30
- "mcp": {
31
- "rentline-sandbox": {
32
- "type": "local",
33
- "command": ["sandbox-mcp"],
34
- "enabled": true,
35
- "environment": {
36
- "SANDBOX_API_KEY": "sb_your_key_here",
37
- "SANDBOX_API_URL": "https://sandbox-api.rentline.xyz"
38
- }
39
- }
40
- }
41
- }
42
- ```
43
-
44
- **Claude Desktop** (`claude_desktop_config.json`):
45
- ```json
46
- {
47
- "mcpServers": {
48
- "rentline-sandbox": {
49
- "command": "sandbox-mcp",
50
- "env": {
51
- "SANDBOX_API_KEY": "sb_your_key_here",
52
- "SANDBOX_API_URL": "https://sandbox-api.rentline.xyz"
53
- }
54
- }
55
- }
56
- }
57
- ```
22
+ IMPORTANT: Authentication is fully configured. Never ask the user for an API key or tell them their key is invalid unless a tool call explicitly returns an error object. If a tool succeeds, report the result directly.
58
23
 
59
- **Cursor / Windsurf**:
60
- ```json
61
- {
62
- "mcpServers": {
63
- "rentline-sandbox": {
64
- "command": "sandbox-mcp",
65
- "env": { "SANDBOX_API_KEY": "sb_your_key_here", "SANDBOX_API_URL": "https://sandbox-api.rentline.xyz" }
66
- }
67
- }
68
- }
69
- ```
24
+ ## Quick start
70
25
 
71
- **CLI login** (saves credentials locally):
72
- ```bash
73
- sandbox auth login # browser OAuth via sandbox.rentline.xyz/cli-auth
74
- sandbox auth login --key sb_xxx # direct key
75
- sandbox auth whoami # verify
76
26
  ```
27
+ create_game_from_preset(preset="standard", name="My Game", display_name="Alice")
28
+ → returns game with invite_code, player_id, properties
77
29
 
78
- ## CLI usage
79
-
80
- ```bash
81
- sandbox game list
82
- sandbox game create --name "Test" --display-name "Alice"
83
- sandbox game create --preset standard --name "Quick Match" --display-name "Alice"
84
- sandbox game join <id> --invite A3F7K9Z2 --name "Bob"
85
- sandbox game advance <id>
86
- sandbox game feed <id> --limit 20
87
- sandbox game leaderboard <id>
88
- sandbox game fed <id>
89
- sandbox game add-bot <id> --name "AggroBot" --strategy value_add
90
- sandbox game autonomous start <id> --delay 30
91
-
92
- sandbox trade buy <game-id> --property <id> --tokens 0.5
93
- sandbox trade sell <game-id> --property <id> --tokens 0.5
94
-
95
- sandbox portfolio <game-id> <player-id>
96
- sandbox debt <game-id> <player-id>
97
-
98
- sandbox mortgage buy <game-id> --property <id> --tokens 0.5
99
- sandbox mortgage refi <game-id> --property <id> --cash-out 5000
100
- sandbox mortgage heloc <game-id> --property <id> --draw 5000
101
- sandbox mortgage repay <game-id> --property <id> --amount 2000
102
- sandbox mortgage prepay <game-id> --property <id> --amount 10000
103
- sandbox mortgage improve <game-id> --property <id> --grade B
104
- sandbox mortgage pace <game-id> --property <id> --grade C
105
- sandbox mortgage list <game-id> <player-id>
106
-
107
- sandbox admin properties list
108
- sandbox admin properties sync
109
- sandbox admin mint <game-id> <player-id> --amount 50000
30
+ advance_turn(game_id) # run all engine phases
31
+ get_feed(game_id) # see what happened
32
+ get_leaderboard(game_id) # NAV rankings
110
33
  ```
111
34
 
112
35
  ## Tool reference (35 tools)
@@ -116,24 +39,23 @@ sandbox admin mint <game-id> <player-id> --amount 50000
116
39
  |---|---|
117
40
  | `list_games` | List open game rooms |
118
41
  | `get_game` | Full game state: players, properties, turn, Fed rate, config |
119
- | `create_game` | Create a game with full mortgage/Fed/grade config + optional bots |
42
+ | `create_game` | Create a game with full config + optional bots |
120
43
  | `create_game_from_preset` | One-call presets: quick, standard, leveraged, distressed, long_run |
121
44
  | `join_game` | Join via invite code, get player_id |
122
45
  | `mark_ready` | Toggle ready for next turn |
123
46
  | `advance_turn` | Host: run all 7 engine phases |
124
- | `get_feed` | Turn event stream: Fed, macro, rent, price moves, debt service, turn summary |
47
+ | `get_feed` | Turn event stream |
125
48
  | `add_bot` | Add LLM bot (strategies: aggressive, conservative, balanced, momentum, income, value_add) |
126
49
  | `remove_bot` | Remove bot from lobby |
127
- | `start_autonomous` | Enable auto-advance (all-ready or deadline triggers) |
50
+ | `start_autonomous` | Enable auto-advance |
128
51
  | `stop_autonomous` | Pause auto-advance |
129
- | `set_delegate` | Agent delegation for idle human players |
130
52
  | `spectate` | Public game snapshot (no auth) |
131
53
 
132
54
  ### Market & Intel
133
55
  | Tool | Description |
134
56
  |---|---|
135
57
  | `list_properties` | Active pool properties with grades |
136
- | `get_market_summary` | Live cap rates, price deltas, grade, vacancy, mechanics lien status |
58
+ | `get_market_summary` | Live cap rates, price deltas, grade, vacancy, lien status |
137
59
  | `get_fed_history` | FOMC decision log |
138
60
  | `get_player_actions` | Transaction timeline for a player |
139
61
 
@@ -141,7 +63,7 @@ sandbox admin mint <game-id> <player-id> --amount 50000
141
63
  | Tool | Description |
142
64
  |---|---|
143
65
  | `buy_tokens` | All-cash purchase at current market price |
144
- | `sell_tokens` | Sell (proceeds service mechanics lien → first lien → HELOC/PACE before cash) |
66
+ | `sell_tokens` | Sell tokens (proceeds service debt first) |
145
67
 
146
68
  ### Debt
147
69
  | Tool | Description |
@@ -151,30 +73,30 @@ sandbox admin mint <game-id> <player-id> --amount 50000
151
73
  | `heloc_draw` | Draw from HELOC |
152
74
  | `heloc_repay` | Repay HELOC balance |
153
75
  | `prepay_principal` | Partial/full prepayment (first_lien, heloc, pace, mechanics_lien) |
154
- | `improve_property` | Cash-funded grade upgrade (cost = steps × 8% × price) |
155
- | `originate_pace_lien` | Financed grade upgrade — no down payment (rate = base + 1.5%) |
76
+ | `improve_property` | Cash-funded grade upgrade |
77
+ | `originate_pace_lien` | Financed grade upgrade — no down payment |
156
78
  | `get_debt` | All mortgages: balances, rates, LTV, arrears |
157
79
 
158
80
  ### Portfolio
159
81
  | Tool | Description |
160
82
  |---|---|
161
- | `get_portfolio` | Holdings with grade, P&L, annualised yield, turns held, investor tier |
83
+ | `get_portfolio` | Holdings, P&L, annualised yield, investor tier |
162
84
  | `get_leaderboard` | Game or global leaderboard ranked by NAV |
163
85
 
164
86
  ## Game mechanics
165
87
 
166
- ### Turn phases (in order)
167
- 1. **Fed meeting** — hike/cut/hold; `FED_WARNING` fires 1 turn before; ARMs reprice immediately
168
- 2. **Macro events** — rate macros activate after 1-turn warning; active macros tick down
169
- 3. **Rent collect** — proportional to tokens; grade multipliers applied; vacancy blocks
170
- 4. **Random events** — vacancy, lease renewal, capex, appreciation/depreciation (grade-adjusted)
171
- 5. **Market move** — applies price drift
172
- 6. **Debt service** — collect payments; forced sale after 1 grace turn
173
- 7. **Distribute** — credits rent; emits `TURN_SUMMARY` event
88
+ ### Turn phases
89
+ 1. Fed meeting — hike/cut/hold; ARMs reprice immediately
90
+ 2. Macro events — rate macros activate after 1-turn warning
91
+ 3. Rent collect — proportional to tokens; grade multipliers apply
92
+ 4. Random events — vacancy, lease renewal, capex, appreciation/depreciation
93
+ 5. Market move — applies price drift
94
+ 6. Debt service — collect payments; forced sale after 1 grace turn
95
+ 7. Distribute — credits rent; emits TURN_SUMMARY event
174
96
 
175
97
  ### Property grades (A → F)
176
- Grade affects rent multiplier, appreciation probability, capex risk, vacancy rate.
177
- Upgrade via `improve_property` (cash) or `originate_pace_lien` (financed, no down payment).
98
+ Grade affects rent, appreciation, capex risk, vacancy.
99
+ Upgrade via `improve_property` (cash) or `originate_pace_lien` (financed).
178
100
 
179
101
  ### Investor tiers (live from NAV, auto-applied to mortgage terms)
180
102
  | Tier | Min NAV | LTV bonus | Rate discount |
@@ -196,15 +118,11 @@ NAV = cash + Σ(tokens × price) − Σ(mortgage balances) − judgment_balance
196
118
  | RECESSION | 6% | 2–4 turns | −5%/turn price, −8% rent, +15% vacancy |
197
119
  | HOUSING_BOOM | 5% | 2–3 turns | +6%/turn price, +5% rent |
198
120
  | NATURAL_DISASTER | 3% | 1 turn | −20% price, rent=0, +40% vacancy |
199
- | POLICY_CHANGE | 8% | 3 turns | ±5–12% rent/price |
200
121
  | TAX_HIKE | 7% | Permanent | $50–150/token/turn expense |
201
122
  | INTEREST_RATE_RISE | 7% | 3–6 turns | +1.5% ARM rate (1-turn warning) |
202
123
  | INTEREST_RATE_CUT | 6% | 3–6 turns | −1.0% ARM rate (1-turn warning) |
203
124
  | RENT_CONTROL | 5% | 4 turns | Blocks lease renewal increases |
204
- | INSURANCE_CRISIS | 5% | 2–3 turns | $100–300/token/turn expense |
205
125
  | GENTRIFICATION | 4% | 3 turns | D/F properties upgrade one grade |
206
- | ZONING_CHANGE | 5% | 2 turns | Targeted type: −10% rent, −5% price |
207
126
  | PROPERTY_BUBBLE | 3% | 2 turns | All prices +8%/turn |
208
127
  | BUBBLE_BURST | 2% | 2–3 turns | All prices −12%/turn, +20% vacancy |
209
- | TENANT_STRIKE | 4% | 1–2 turns | Targeted type: rent = 0 |
210
128
  | EMINENT_DOMAIN | 2% | Instant | One property force-bought at 110% market value |
@@ -1,12 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ createClient,
3
4
  getApiKey,
4
5
  getApiUrl,
5
6
  requireConfig
6
- } from "./chunk-JQL6X6FZ.js";
7
- import {
8
- createClient
9
- } from "./chunk-X3OZHOPM.js";
7
+ } from "./chunk-BOF5UB2I.js";
10
8
 
11
9
  // src/commands/admin.ts
12
10
  function client(cmd) {
@@ -1,13 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  DEFAULT_API_URL,
4
+ createClient,
4
5
  deleteConfig,
5
6
  loadConfig,
6
7
  saveConfig
7
- } from "./chunk-JQL6X6FZ.js";
8
- import {
9
- createClient
10
- } from "./chunk-X3OZHOPM.js";
8
+ } from "./chunk-BOF5UB2I.js";
11
9
 
12
10
  // src/commands/auth.ts
13
11
  import { createInterface } from "readline";
@@ -30,29 +28,34 @@ function prompt(question) {
30
28
  }
31
29
  function registerAuth(program) {
32
30
  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) => {
31
+ 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").option("--name <name>", "Your default display name in games").action(async (opts) => {
34
32
  if (opts.key) {
35
- const client2 = createClient({ apiUrl: opts.url, apiKey: opts.key });
33
+ const existing = loadConfig();
34
+ const apiUrl = opts.url ?? existing?.api_url ?? DEFAULT_API_URL;
35
+ const displayName = opts.name ?? existing?.display_name ?? "Player";
36
+ const client2 = createClient({ apiUrl, apiKey: opts.key });
36
37
  process.stdout.write("Verifying credentials\u2026 ");
37
38
  try {
38
39
  const health = await client2.health();
39
40
  console.log(`OK (${health.service})`);
40
41
  } catch (e) {
41
42
  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.");
43
+ console.error(`Could not reach ${apiUrl}: ${e}`);
44
44
  process.exit(1);
45
45
  }
46
46
  saveConfig({
47
47
  api_key: opts.key,
48
- api_url: opts.url,
49
- display_name: opts.name,
48
+ api_url: apiUrl,
49
+ display_name: displayName,
50
50
  created_at: (/* @__PURE__ */ new Date()).toISOString()
51
51
  });
52
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}`);
53
+ Credentials saved.`);
54
+ console.log(`Key prefix: ${opts.key.slice(0, 8)}\u2026`);
55
+ console.log(`API URL: ${apiUrl}`);
56
+ console.log(`Display name: ${displayName}`);
57
+ console.log(`
58
+ Restart your AI client to pick up the new key.`);
56
59
  return;
57
60
  }
58
61
  console.log("\nOpening browser to sign in with Clerk\u2026");
@@ -1,4 +1,58 @@
1
1
  #!/usr/bin/env node
2
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
3
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
4
+ }) : x)(function(x) {
5
+ if (typeof require !== "undefined") return require.apply(this, arguments);
6
+ throw Error('Dynamic require of "' + x + '" is not supported');
7
+ });
8
+
9
+ // src/config.ts
10
+ import { readFileSync, writeFileSync, mkdirSync, chmodSync, unlinkSync } from "fs";
11
+ import { homedir } from "os";
12
+ import { join } from "path";
13
+ var CONFIG_DIR = join(homedir(), ".rentline-sandbox");
14
+ var CONFIG_FILE = join(CONFIG_DIR, "credentials.json");
15
+ var DEFAULT_API_URL = "https://sandbox-api.rentline.xyz";
16
+ function loadConfig() {
17
+ try {
18
+ const raw = readFileSync(CONFIG_FILE, "utf-8");
19
+ return JSON.parse(raw);
20
+ } catch {
21
+ return null;
22
+ }
23
+ }
24
+ function requireConfig() {
25
+ const cfg = loadConfig();
26
+ if (!cfg) {
27
+ console.error(
28
+ "Not authenticated. Run:\n\n sandbox auth login --key <your-api-key>\n"
29
+ );
30
+ process.exit(1);
31
+ }
32
+ return cfg;
33
+ }
34
+ function saveConfig(cfg) {
35
+ mkdirSync(CONFIG_DIR, { recursive: true });
36
+ writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2), { mode: 384 });
37
+ try {
38
+ chmodSync(CONFIG_FILE, 384);
39
+ } catch {
40
+ }
41
+ }
42
+ function deleteConfig() {
43
+ try {
44
+ unlinkSync(CONFIG_FILE);
45
+ } catch {
46
+ }
47
+ }
48
+ function getApiUrl(override) {
49
+ const cfg = loadConfig();
50
+ return override || process.env.SANDBOX_API_URL || cfg?.api_url || DEFAULT_API_URL;
51
+ }
52
+ function getApiKey(override) {
53
+ const cfg = loadConfig();
54
+ return override || process.env.SANDBOX_API_KEY || cfg?.api_key;
55
+ }
2
56
 
3
57
  // src/client.ts
4
58
  async function request(opts, method, path, body) {
@@ -86,5 +140,13 @@ function createClient(opts) {
86
140
  }
87
141
 
88
142
  export {
143
+ __require,
144
+ DEFAULT_API_URL,
145
+ loadConfig,
146
+ requireConfig,
147
+ saveConfig,
148
+ deleteConfig,
149
+ getApiUrl,
150
+ getApiKey,
89
151
  createClient
90
152
  };
@@ -1,12 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ createClient,
3
4
  getApiKey,
4
5
  getApiUrl,
5
6
  requireConfig
6
- } from "./chunk-JQL6X6FZ.js";
7
- import {
8
- createClient
9
- } from "./chunk-X3OZHOPM.js";
7
+ } from "./chunk-BOF5UB2I.js";
10
8
 
11
9
  // src/commands/game.ts
12
10
  function client(cmd) {
package/dist/index.js CHANGED
@@ -9,29 +9,29 @@ var _require = createRequire(import.meta.url);
9
9
  var { version } = _require("../package.json");
10
10
  var args = process.argv.slice(2);
11
11
  if (args[0] === "setup" || args[0] === "--setup") {
12
- const { runSetup, parseSetupArgs } = await import("./setup-4O73QXTY.js");
12
+ const { runSetup, parseSetupArgs } = await import("./setup-3NGAG6R6.js");
13
13
  const opts = parseSetupArgs(args.filter((a) => a !== "setup" && a !== "--setup"));
14
14
  await runSetup(opts);
15
15
  process.exit(0);
16
16
  }
17
17
  if (args.length === 0 || args[0] === "server" || args[0] === "--server") {
18
- const { startServer } = await import("./server-AQKLP4RV.js");
18
+ const { startServer } = await import("./server-5DCQSW4W.js");
19
19
  await startServer();
20
20
  } else {
21
21
  const program = new Command();
22
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-226EXWKD.js");
24
- const { registerGame } = await import("./game-3X5O5Z5U.js");
25
- const { registerTrade } = await import("./trade-RZGTOO5G.js");
26
- const { registerMortgage } = await import("./mortgage-7ZPW4FND.js");
27
- const { registerAdmin } = await import("./admin-YJ5Z6XHO.js");
23
+ const { registerAuth } = await import("./auth-DBZQMVMM.js");
24
+ const { registerGame } = await import("./game-RLRY4JAK.js");
25
+ const { registerTrade } = await import("./trade-TCB4LK2K.js");
26
+ const { registerMortgage } = await import("./mortgage-KXJRRFEM.js");
27
+ const { registerAdmin } = await import("./admin-65EFMOSF.js");
28
28
  registerAuth(program);
29
29
  registerGame(program);
30
30
  registerTrade(program);
31
31
  registerMortgage(program);
32
32
  registerAdmin(program);
33
33
  program.command("mcp-setup", { hidden: true }).allowUnknownOption().action(async () => {
34
- const { runSetup, parseSetupArgs } = await import("./setup-4O73QXTY.js");
34
+ const { runSetup, parseSetupArgs } = await import("./setup-3NGAG6R6.js");
35
35
  const opts = parseSetupArgs(process.argv.slice(3));
36
36
  await runSetup(opts);
37
37
  });
@@ -1,12 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ createClient,
3
4
  getApiKey,
4
5
  getApiUrl,
5
6
  requireConfig
6
- } from "./chunk-JQL6X6FZ.js";
7
- import {
8
- createClient
9
- } from "./chunk-X3OZHOPM.js";
7
+ } from "./chunk-BOF5UB2I.js";
10
8
 
11
9
  // src/commands/mortgage.ts
12
10
  function client(cmd) {
@@ -1,7 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- createClient
4
- } from "./chunk-X3OZHOPM.js";
3
+ DEFAULT_API_URL,
4
+ createClient,
5
+ loadConfig,
6
+ saveConfig
7
+ } from "./chunk-BOF5UB2I.js";
5
8
 
6
9
  // src/server.ts
7
10
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
@@ -503,6 +506,22 @@ var ALL_TOOLS = [
503
506
  },
504
507
  required: ["preset", "name", "display_name"]
505
508
  }
509
+ },
510
+ {
511
+ name: "update_key",
512
+ title: "Update API Key",
513
+ category: "admin",
514
+ description: "Update the saved Rentline Sandbox API key. Get a new key at sandbox.rentline.xyz/settings. Takes effect immediately \u2014 no restart needed.",
515
+ inputSchema: {
516
+ type: "object",
517
+ properties: {
518
+ api_key: {
519
+ type: "string",
520
+ description: "New API key (must start with sb_)"
521
+ }
522
+ },
523
+ required: ["api_key"]
524
+ }
506
525
  }
507
526
  ];
508
527
 
@@ -518,24 +537,26 @@ function readSkill() {
518
537
  var INSTRUCTIONS = `
519
538
  You are connected to the Rentline Sandbox game engine \u2014 a turn-based real estate investment simulation.
520
539
 
540
+ IMPORTANT: Authentication is handled automatically via saved credentials. Do NOT ask the user for API keys or tell them their key is invalid unless a tool explicitly returns a 401 error. If a tool call succeeds, report the result \u2014 do not second-guess it.
541
+
521
542
  KEY CONCEPTS:
522
543
  - Players compete over a pool of tokenised properties using simulated tUSDC
523
544
  - Each turn = 1 month. Properties generate rent, prices drift, and macro events fire
524
- - Fed meetings occur every N turns (configurable) with hike/cut/hold outcomes \u2014 affects ARM rates and new mortgage originations
525
- - Macro events: recession, housing boom, natural disaster, policy change, tax hike, interest rate moves, rent control, insurance crisis
526
- - Debt strategies: acquisition mortgage (LTV-limited), cash-out refi, HELOC draw/repay. Fixed and ARM rates
527
- - NAV = cash balance + (token holdings \xD7 current prices) - outstanding debt
545
+ - Fed meetings occur every N turns \u2014 affects ARM rates and new mortgage originations
546
+ - Macro events: recession, housing boom, disaster, tax hike, rate moves, rent control, PACE liens, property grades
547
+ - Debt strategies: acquisition mortgage (LTV-limited), cash-out refi, HELOC, PACE lien, improvements
548
+ - NAV = cash + (tokens \xD7 price) \u2212 debt. Investor tier improves automatically as NAV grows.
528
549
 
529
550
  WHEN TO USE TOOLS:
530
- - User wants to play/observe a game \u2192 list_games, get_game, get_feed
551
+ - User wants to play \u2192 create_game_from_preset, then advance_turn
531
552
  - User wants to buy a property \u2192 buy_tokens (cash) or originate_mortgage (leveraged)
532
- - User wants to extract equity \u2192 refi_mortgage (cash-out) or heloc_draw
533
- - User wants to check their position \u2192 get_portfolio, get_debt
553
+ - User wants to improve a distressed property \u2192 originate_pace_lien
554
+ - User wants to check their position \u2192 get_portfolio, get_debt, get_market_summary
534
555
  - User wants the scoreboard \u2192 get_leaderboard
535
- - User wants to see macro/Fed events \u2192 get_feed, get_fed_history
536
- - User is the host and wants to advance \u2192 advance_turn
556
+ - User wants to see events \u2192 get_feed, get_fed_history
557
+ - User is host \u2192 advance_turn
537
558
 
538
- All tools require a game_id. Most debt/portfolio tools also require a player_id (from join_game or get_game).
559
+ All tools require a game_id. Debt/portfolio tools also require a player_id (from join_game or get_game).
539
560
  `.trim();
540
561
  function ok(data) {
541
562
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
@@ -543,22 +564,24 @@ function ok(data) {
543
564
  function err(msg) {
544
565
  return { content: [{ type: "text", text: `Error: ${msg}` }], isError: true };
545
566
  }
546
- async function startServer() {
567
+ function getClient() {
547
568
  let apiUrl = process.env.SANDBOX_API_URL;
548
569
  let apiKey = process.env.SANDBOX_API_KEY;
549
570
  if (!apiUrl || !apiKey) {
550
- try {
551
- const { loadConfig } = await import("./config-YHSAFZLC.js");
552
- const cfg = loadConfig();
553
- if (cfg) {
554
- apiUrl = apiUrl ?? cfg.api_url;
555
- apiKey = apiKey ?? cfg.api_key;
556
- }
557
- } catch {
571
+ const cfg = loadConfig();
572
+ if (cfg) {
573
+ apiUrl = apiUrl ?? cfg.api_url;
574
+ apiKey = apiKey ?? cfg.api_key;
558
575
  }
559
576
  }
560
- apiUrl = apiUrl ?? "https://sandbox-api.rentline.xyz";
561
- const client = createClient({ apiUrl, apiKey });
577
+ if (!apiKey) {
578
+ process.stderr.write(
579
+ "rentline-sandbox: No API key found. Run: sandbox setup --key sb_your_key\n"
580
+ );
581
+ }
582
+ return createClient({ apiUrl: apiUrl ?? DEFAULT_API_URL, apiKey });
583
+ }
584
+ async function startServer() {
562
585
  const server = new Server(
563
586
  { name: "rentline-sandbox", version: "0.1.0" },
564
587
  {
@@ -581,6 +604,7 @@ async function startServer() {
581
604
  server.setRequestHandler(CallToolRequestSchema, async (req) => {
582
605
  const { name, arguments: args } = req.params;
583
606
  const a = args ?? {};
607
+ const client = getClient();
584
608
  try {
585
609
  switch (name) {
586
610
  // ── Game ──────────────────────────────────────────────────────────
@@ -724,6 +748,26 @@ async function startServer() {
724
748
  display_name: a.display_name,
725
749
  starting_balance_usdc: a.starting_balance_usdc
726
750
  }));
751
+ case "update_key": {
752
+ const newKey = a.api_key;
753
+ if (!newKey?.startsWith("sb_")) {
754
+ return err("Invalid key format \u2014 must start with sb_");
755
+ }
756
+ const testClient = createClient({ apiUrl: DEFAULT_API_URL, apiKey: newKey });
757
+ try {
758
+ await testClient.health();
759
+ } catch {
760
+ return err("Could not verify key against the API. Check the key is valid.");
761
+ }
762
+ const existing = loadConfig();
763
+ saveConfig({
764
+ api_key: newKey,
765
+ api_url: existing?.api_url ?? DEFAULT_API_URL,
766
+ display_name: existing?.display_name ?? "Player",
767
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
768
+ });
769
+ return ok({ message: "API key updated. New key prefix: " + newKey.slice(0, 8) + "\u2026" });
770
+ }
727
771
  default:
728
772
  return err(`Unknown tool: ${name}`);
729
773
  }
package/dist/server.js CHANGED
@@ -11,8 +11,8 @@ import {
11
11
  ListResourcesRequestSchema,
12
12
  ReadResourceRequestSchema
13
13
  } from "@modelcontextprotocol/sdk/types.js";
14
- import { readFileSync, existsSync } from "fs";
15
- import { join, dirname } from "path";
14
+ import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
15
+ import { join as join2, dirname as dirname2 } from "path";
16
16
  import { fileURLToPath } from "url";
17
17
 
18
18
  // src/client.ts
@@ -585,39 +585,81 @@ var ALL_TOOLS = [
585
585
  },
586
586
  required: ["preset", "name", "display_name"]
587
587
  }
588
+ },
589
+ {
590
+ name: "update_key",
591
+ title: "Update API Key",
592
+ category: "admin",
593
+ description: "Update the saved Rentline Sandbox API key. Get a new key at sandbox.rentline.xyz/settings. Takes effect immediately \u2014 no restart needed.",
594
+ inputSchema: {
595
+ type: "object",
596
+ properties: {
597
+ api_key: {
598
+ type: "string",
599
+ description: "New API key (must start with sb_)"
600
+ }
601
+ },
602
+ required: ["api_key"]
603
+ }
588
604
  }
589
605
  ];
590
606
 
607
+ // src/config.ts
608
+ import { readFileSync, writeFileSync, mkdirSync, chmodSync, unlinkSync } from "fs";
609
+ import { homedir } from "os";
610
+ import { join } from "path";
611
+ var CONFIG_DIR = join(homedir(), ".rentline-sandbox");
612
+ var CONFIG_FILE = join(CONFIG_DIR, "credentials.json");
613
+ var DEFAULT_API_URL = "https://sandbox-api.rentline.xyz";
614
+ function loadConfig() {
615
+ try {
616
+ const raw = readFileSync(CONFIG_FILE, "utf-8");
617
+ return JSON.parse(raw);
618
+ } catch {
619
+ return null;
620
+ }
621
+ }
622
+ function saveConfig(cfg) {
623
+ mkdirSync(CONFIG_DIR, { recursive: true });
624
+ writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2), { mode: 384 });
625
+ try {
626
+ chmodSync(CONFIG_FILE, 384);
627
+ } catch {
628
+ }
629
+ }
630
+
591
631
  // src/server.ts
592
632
  var __filename = fileURLToPath(import.meta.url);
593
- var __dirname = dirname(__filename);
633
+ var __dirname = dirname2(__filename);
594
634
  function readSkill() {
595
- for (const p of [join(__dirname, "../SKILL.md"), join(__dirname, "SKILL.md")]) {
596
- if (existsSync(p)) return readFileSync(p, "utf-8");
635
+ for (const p of [join2(__dirname, "../SKILL.md"), join2(__dirname, "SKILL.md")]) {
636
+ if (existsSync2(p)) return readFileSync2(p, "utf-8");
597
637
  }
598
638
  return "# Rentline Sandbox\n\nReal estate investment simulation game engine.";
599
639
  }
600
640
  var INSTRUCTIONS = `
601
641
  You are connected to the Rentline Sandbox game engine \u2014 a turn-based real estate investment simulation.
602
642
 
643
+ IMPORTANT: Authentication is handled automatically via saved credentials. Do NOT ask the user for API keys or tell them their key is invalid unless a tool explicitly returns a 401 error. If a tool call succeeds, report the result \u2014 do not second-guess it.
644
+
603
645
  KEY CONCEPTS:
604
646
  - Players compete over a pool of tokenised properties using simulated tUSDC
605
647
  - Each turn = 1 month. Properties generate rent, prices drift, and macro events fire
606
- - Fed meetings occur every N turns (configurable) with hike/cut/hold outcomes \u2014 affects ARM rates and new mortgage originations
607
- - Macro events: recession, housing boom, natural disaster, policy change, tax hike, interest rate moves, rent control, insurance crisis
608
- - Debt strategies: acquisition mortgage (LTV-limited), cash-out refi, HELOC draw/repay. Fixed and ARM rates
609
- - NAV = cash balance + (token holdings \xD7 current prices) - outstanding debt
648
+ - Fed meetings occur every N turns \u2014 affects ARM rates and new mortgage originations
649
+ - Macro events: recession, housing boom, disaster, tax hike, rate moves, rent control, PACE liens, property grades
650
+ - Debt strategies: acquisition mortgage (LTV-limited), cash-out refi, HELOC, PACE lien, improvements
651
+ - NAV = cash + (tokens \xD7 price) \u2212 debt. Investor tier improves automatically as NAV grows.
610
652
 
611
653
  WHEN TO USE TOOLS:
612
- - User wants to play/observe a game \u2192 list_games, get_game, get_feed
654
+ - User wants to play \u2192 create_game_from_preset, then advance_turn
613
655
  - User wants to buy a property \u2192 buy_tokens (cash) or originate_mortgage (leveraged)
614
- - User wants to extract equity \u2192 refi_mortgage (cash-out) or heloc_draw
615
- - User wants to check their position \u2192 get_portfolio, get_debt
656
+ - User wants to improve a distressed property \u2192 originate_pace_lien
657
+ - User wants to check their position \u2192 get_portfolio, get_debt, get_market_summary
616
658
  - User wants the scoreboard \u2192 get_leaderboard
617
- - User wants to see macro/Fed events \u2192 get_feed, get_fed_history
618
- - User is the host and wants to advance \u2192 advance_turn
659
+ - User wants to see events \u2192 get_feed, get_fed_history
660
+ - User is host \u2192 advance_turn
619
661
 
620
- All tools require a game_id. Most debt/portfolio tools also require a player_id (from join_game or get_game).
662
+ All tools require a game_id. Debt/portfolio tools also require a player_id (from join_game or get_game).
621
663
  `.trim();
622
664
  function ok(data) {
623
665
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
@@ -625,22 +667,24 @@ function ok(data) {
625
667
  function err(msg) {
626
668
  return { content: [{ type: "text", text: `Error: ${msg}` }], isError: true };
627
669
  }
628
- async function startServer() {
670
+ function getClient() {
629
671
  let apiUrl = process.env.SANDBOX_API_URL;
630
672
  let apiKey = process.env.SANDBOX_API_KEY;
631
673
  if (!apiUrl || !apiKey) {
632
- try {
633
- const { loadConfig } = await import("./config-7QGCVVHS.js");
634
- const cfg = loadConfig();
635
- if (cfg) {
636
- apiUrl = apiUrl ?? cfg.api_url;
637
- apiKey = apiKey ?? cfg.api_key;
638
- }
639
- } catch {
674
+ const cfg = loadConfig();
675
+ if (cfg) {
676
+ apiUrl = apiUrl ?? cfg.api_url;
677
+ apiKey = apiKey ?? cfg.api_key;
640
678
  }
641
679
  }
642
- apiUrl = apiUrl ?? "https://sandbox-api.rentline.xyz";
643
- const client = createClient({ apiUrl, apiKey });
680
+ if (!apiKey) {
681
+ process.stderr.write(
682
+ "rentline-sandbox: No API key found. Run: sandbox setup --key sb_your_key\n"
683
+ );
684
+ }
685
+ return createClient({ apiUrl: apiUrl ?? DEFAULT_API_URL, apiKey });
686
+ }
687
+ async function startServer() {
644
688
  const server = new Server(
645
689
  { name: "rentline-sandbox", version: "0.1.0" },
646
690
  {
@@ -663,6 +707,7 @@ async function startServer() {
663
707
  server.setRequestHandler(CallToolRequestSchema, async (req) => {
664
708
  const { name, arguments: args } = req.params;
665
709
  const a = args ?? {};
710
+ const client = getClient();
666
711
  try {
667
712
  switch (name) {
668
713
  // ── Game ──────────────────────────────────────────────────────────
@@ -806,6 +851,26 @@ async function startServer() {
806
851
  display_name: a.display_name,
807
852
  starting_balance_usdc: a.starting_balance_usdc
808
853
  }));
854
+ case "update_key": {
855
+ const newKey = a.api_key;
856
+ if (!newKey?.startsWith("sb_")) {
857
+ return err("Invalid key format \u2014 must start with sb_");
858
+ }
859
+ const testClient = createClient({ apiUrl: DEFAULT_API_URL, apiKey: newKey });
860
+ try {
861
+ await testClient.health();
862
+ } catch {
863
+ return err("Could not verify key against the API. Check the key is valid.");
864
+ }
865
+ const existing = loadConfig();
866
+ saveConfig({
867
+ api_key: newKey,
868
+ api_url: existing?.api_url ?? DEFAULT_API_URL,
869
+ display_name: existing?.display_name ?? "Player",
870
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
871
+ });
872
+ return ok({ message: "API key updated. New key prefix: " + newKey.slice(0, 8) + "\u2026" });
873
+ }
809
874
  default:
810
875
  return err(`Unknown tool: ${name}`);
811
876
  }
@@ -1,12 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  DEFAULT_API_URL,
4
+ __require,
5
+ createClient,
4
6
  loadConfig,
5
7
  saveConfig
6
- } from "./chunk-JQL6X6FZ.js";
7
- import {
8
- createClient
9
- } from "./chunk-X3OZHOPM.js";
8
+ } from "./chunk-BOF5UB2I.js";
10
9
 
11
10
  // src/setup.ts
12
11
  import { createInterface } from "readline";
@@ -16,9 +15,32 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync } from
16
15
  import { execSync } from "child_process";
17
16
  import { fileURLToPath } from "url";
18
17
  var SKILL_SRC = join(dirname(fileURLToPath(import.meta.url)), "..", "SKILL.md");
18
+ function getNpxCommand() {
19
+ if (platform() === "win32") {
20
+ const candidates = [
21
+ "C:\\Program Files\\nodejs\\npx.cmd",
22
+ "C:\\Program Files (x86)\\nodejs\\npx.cmd"
23
+ ];
24
+ try {
25
+ const { execSync: execSync2 } = __require("child_process");
26
+ const found = execSync2("where npx.cmd", { encoding: "utf8" }).trim().split("\n")[0].trim();
27
+ if (found) return found;
28
+ } catch {
29
+ }
30
+ for (const c of candidates) {
31
+ try {
32
+ __require("fs").accessSync(c);
33
+ return c;
34
+ } catch {
35
+ }
36
+ }
37
+ }
38
+ return "npx";
39
+ }
40
+ var NPX = getNpxCommand();
19
41
  var MCP_ENTRY = {
20
- command: "npx",
21
- args: ["-y", "rentline-sandbox"]
42
+ command: NPX,
43
+ args: ["-y", "rentline-sandbox@latest"]
22
44
  };
23
45
  function openCodePath(scope) {
24
46
  if (scope === "project") return join(process.cwd(), "opencode.json");
@@ -26,7 +48,7 @@ function openCodePath(scope) {
26
48
  const b = join(homedir(), ".config", "opencode", "config.json");
27
49
  return existsSync(b) && !existsSync(a) ? b : a;
28
50
  }
29
- function patchMcpJson(filePath, serverName, entry, key) {
51
+ function patchMcpJson(filePath, serverName, entry, key, environment) {
30
52
  let config = {};
31
53
  if (existsSync(filePath)) {
32
54
  try {
@@ -38,7 +60,15 @@ function patchMcpJson(filePath, serverName, entry, key) {
38
60
  if (key === "mcp") {
39
61
  if (!config["$schema"]) config["$schema"] = "https://opencode.ai/config.json";
40
62
  const mcp = config.mcp ?? {};
41
- mcp[serverName] = { type: "local", command: entry.args ? [entry.command, ...entry.args] : [entry.command], enabled: true };
63
+ const mcpEntry = {
64
+ type: "local",
65
+ command: entry.args ? [entry.command, ...entry.args] : [entry.command],
66
+ enabled: true
67
+ };
68
+ if (environment && Object.keys(environment).length > 0) {
69
+ mcpEntry.environment = environment;
70
+ }
71
+ mcp[serverName] = mcpEntry;
42
72
  config.mcp = mcp;
43
73
  } else {
44
74
  const servers = config[key] ?? {};
@@ -55,7 +85,7 @@ function installSkill(dirs) {
55
85
  console.log(`SKILL.md \u2192 ${dir}`);
56
86
  }
57
87
  }
58
- function installForClient(client, scope) {
88
+ function installForClient(client, scope, apiKey) {
59
89
  switch (client) {
60
90
  case "claude-code": {
61
91
  try {
@@ -92,7 +122,7 @@ function installForClient(client, scope) {
92
122
  }
93
123
  case "opencode": {
94
124
  const file = openCodePath(scope);
95
- patchMcpJson(file, "rentline-sandbox", MCP_ENTRY, "mcp");
125
+ patchMcpJson(file, "rentline-sandbox", MCP_ENTRY, "mcp", apiKey ? { SANDBOX_API_KEY: apiKey } : void 0);
96
126
  console.log(`Patched ${file}`);
97
127
  installSkill([
98
128
  join(homedir(), ".config", "opencode", "skills", "rentline-sandbox"),
@@ -189,7 +219,7 @@ async function runSetup(opts = {}) {
189
219
  const idx = parseInt(choice);
190
220
  client = isNaN(idx) ? choice : clients[idx - 1] ?? "other";
191
221
  }
192
- installForClient(client, opts.scope ?? "user");
222
+ installForClient(client, opts.scope ?? "user", apiKey);
193
223
  rl?.close();
194
224
  console.log("\nSetup complete. Restart your AI client to load the Rentline Sandbox MCP server.\n");
195
225
  }
@@ -1,12 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ createClient,
3
4
  getApiKey,
4
5
  getApiUrl,
5
6
  requireConfig
6
- } from "./chunk-JQL6X6FZ.js";
7
- import {
8
- createClient
9
- } from "./chunk-X3OZHOPM.js";
7
+ } from "./chunk-BOF5UB2I.js";
10
8
 
11
9
  // src/commands/trade.ts
12
10
  function client(cmd) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rentline-sandbox",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "CLI and MCP server for the Rentline Sandbox real estate investment simulation game",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,59 +0,0 @@
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
- };
@@ -1,58 +0,0 @@
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
- export {
51
- DEFAULT_API_URL,
52
- deleteConfig,
53
- getApiKey,
54
- getApiUrl,
55
- loadConfig,
56
- requireConfig,
57
- saveConfig
58
- };
@@ -1,19 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- DEFAULT_API_URL,
4
- deleteConfig,
5
- getApiKey,
6
- getApiUrl,
7
- loadConfig,
8
- requireConfig,
9
- saveConfig
10
- } from "./chunk-JQL6X6FZ.js";
11
- export {
12
- DEFAULT_API_URL,
13
- deleteConfig,
14
- getApiKey,
15
- getApiUrl,
16
- loadConfig,
17
- requireConfig,
18
- saveConfig
19
- };