projectionlab-mcp 0.0.0-stage → 1.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.
- package/HOW-IT-WORKS.md +67 -0
- package/LICENSE +21 -0
- package/README.md +295 -2
- package/dist/bootstrap.d.ts +9 -0
- package/dist/bootstrap.js +90 -0
- package/dist/bootstrap.js.map +1 -0
- package/dist/bridge.d.ts +29 -0
- package/dist/bridge.js +2 -0
- package/dist/bridge.js.map +1 -0
- package/dist/clonePlan.d.ts +19 -0
- package/dist/clonePlan.js +108 -0
- package/dist/clonePlan.js.map +1 -0
- package/dist/config.d.ts +15 -0
- package/dist/config.js +60 -0
- package/dist/config.js.map +1 -0
- package/dist/currentFinances.d.ts +12 -0
- package/dist/currentFinances.js +43 -0
- package/dist/currentFinances.js.map +1 -0
- package/dist/errors.d.ts +20 -0
- package/dist/errors.js +36 -0
- package/dist/errors.js.map +1 -0
- package/dist/extensionBridge.d.ts +54 -0
- package/dist/extensionBridge.js +440 -0
- package/dist/extensionBridge.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +94 -0
- package/dist/index.js.map +1 -0
- package/dist/instructions.d.ts +10 -0
- package/dist/instructions.js +24 -0
- package/dist/instructions.js.map +1 -0
- package/dist/json.d.ts +6 -0
- package/dist/json.js +12 -0
- package/dist/json.js.map +1 -0
- package/dist/largeOutput.d.ts +7 -0
- package/dist/largeOutput.js +28 -0
- package/dist/largeOutput.js.map +1 -0
- package/dist/logger.d.ts +6 -0
- package/dist/logger.js +25 -0
- package/dist/logger.js.map +1 -0
- package/dist/pathPatch.d.ts +33 -0
- package/dist/pathPatch.js +183 -0
- package/dist/pathPatch.js.map +1 -0
- package/dist/planSettings.d.ts +11 -0
- package/dist/planSettings.js +30 -0
- package/dist/planSettings.js.map +1 -0
- package/dist/planVersion.d.ts +25 -0
- package/dist/planVersion.js +72 -0
- package/dist/planVersion.js.map +1 -0
- package/dist/plans.d.ts +53 -0
- package/dist/plans.js +208 -0
- package/dist/plans.js.map +1 -0
- package/dist/prompts/index.d.ts +3 -0
- package/dist/prompts/index.js +232 -0
- package/dist/prompts/index.js.map +1 -0
- package/dist/redact.d.ts +18 -0
- package/dist/redact.js +42 -0
- package/dist/redact.js.map +1 -0
- package/dist/resources/guide.d.ts +2 -0
- package/dist/resources/guide.js +13 -0
- package/dist/resources/guide.js.map +1 -0
- package/dist/resources/knowledge.d.ts +2 -0
- package/dist/resources/knowledge.js +100 -0
- package/dist/resources/knowledge.js.map +1 -0
- package/dist/resources/planData.d.ts +3 -0
- package/dist/resources/planData.js +39 -0
- package/dist/resources/planData.js.map +1 -0
- package/dist/resources/summary.d.ts +3 -0
- package/dist/resources/summary.js +40 -0
- package/dist/resources/summary.js.map +1 -0
- package/dist/secureFs.d.ts +11 -0
- package/dist/secureFs.js +34 -0
- package/dist/secureFs.js.map +1 -0
- package/dist/snapshot.d.ts +10 -0
- package/dist/snapshot.js +102 -0
- package/dist/snapshot.js.map +1 -0
- package/dist/summary.d.ts +7 -0
- package/dist/summary.js +77 -0
- package/dist/summary.js.map +1 -0
- package/dist/tools/bootstrap.d.ts +4 -0
- package/dist/tools/bootstrap.js +24 -0
- package/dist/tools/bootstrap.js.map +1 -0
- package/dist/tools/clonePlan.d.ts +4 -0
- package/dist/tools/clonePlan.js +113 -0
- package/dist/tools/clonePlan.js.map +1 -0
- package/dist/tools/connection.d.ts +3 -0
- package/dist/tools/connection.js +57 -0
- package/dist/tools/connection.js.map +1 -0
- package/dist/tools/export.d.ts +3 -0
- package/dist/tools/export.js +42 -0
- package/dist/tools/export.js.map +1 -0
- package/dist/tools/getCurrentFinances.d.ts +3 -0
- package/dist/tools/getCurrentFinances.js +68 -0
- package/dist/tools/getCurrentFinances.js.map +1 -0
- package/dist/tools/getPlan.d.ts +3 -0
- package/dist/tools/getPlan.js +58 -0
- package/dist/tools/getPlan.js.map +1 -0
- package/dist/tools/getPlanSection.d.ts +3 -0
- package/dist/tools/getPlanSection.js +125 -0
- package/dist/tools/getPlanSection.js.map +1 -0
- package/dist/tools/getPlanSettings.d.ts +3 -0
- package/dist/tools/getPlanSettings.js +73 -0
- package/dist/tools/getPlanSettings.js.map +1 -0
- package/dist/tools/restore.d.ts +11 -0
- package/dist/tools/restore.js +408 -0
- package/dist/tools/restore.js.map +1 -0
- package/dist/tools/schema.d.ts +3 -0
- package/dist/tools/schema.js +90 -0
- package/dist/tools/schema.js.map +1 -0
- package/dist/tools/snapshots.d.ts +4 -0
- package/dist/tools/snapshots.js +146 -0
- package/dist/tools/snapshots.js.map +1 -0
- package/dist/tools/summary.d.ts +3 -0
- package/dist/tools/summary.js +43 -0
- package/dist/tools/summary.js.map +1 -0
- package/dist/tools/updateAccount.d.ts +4 -0
- package/dist/tools/updateAccount.js +214 -0
- package/dist/tools/updateAccount.js.map +1 -0
- package/dist/tools/updatePlanEvent.d.ts +4 -0
- package/dist/tools/updatePlanEvent.js +223 -0
- package/dist/tools/updatePlanEvent.js.map +1 -0
- package/dist/tools/updatePlanSection.d.ts +4 -0
- package/dist/tools/updatePlanSection.js +206 -0
- package/dist/tools/updatePlanSection.js.map +1 -0
- package/dist/tools/updatePlanSettings.d.ts +4 -0
- package/dist/tools/updatePlanSettings.js +125 -0
- package/dist/tools/updatePlanSettings.js.map +1 -0
- package/dist/types/projectionlab.d.ts +42 -0
- package/dist/types/projectionlab.js +2 -0
- package/dist/types/projectionlab.js.map +1 -0
- package/dist/types/wire.d.ts +88 -0
- package/dist/types/wire.js +27 -0
- package/dist/types/wire.js.map +1 -0
- package/dist/version.d.ts +6 -0
- package/dist/version.js +10 -0
- package/dist/version.js.map +1 -0
- package/package.json +71 -4
- package/skills/projectionlab/SKILL.md +208 -0
package/HOW-IT-WORKS.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# How the ProjectionLab MCP Works
|
|
2
|
+
|
|
3
|
+
Here is a breakdown of exactly how the ProjectionLab Model Context Protocol (MCP) server works, from the underlying problem it solves to the step-by-step data flow.
|
|
4
|
+
|
|
5
|
+
## 1. The Core Problem It Solves
|
|
6
|
+
|
|
7
|
+
ProjectionLab exposes its supported automation surface through a JavaScript object (`window.projectionlabPluginAPI`) inside the logged-in browser tab rather than through a public REST API or webhook endpoint.
|
|
8
|
+
|
|
9
|
+
Because an MCP host runs outside that page, it cannot directly call a JavaScript object inside the browser. This MCP server bridges that gap.
|
|
10
|
+
|
|
11
|
+
## 2. The Architecture
|
|
12
|
+
|
|
13
|
+
The server speaks the Model Context Protocol (MCP) over `stdio` to its host and exposes tools (`projectionlab_update_account`, `projectionlab_export_data`, …), resources, and prompts. Everything above the browser transport depends only on the `Bridge` interface (`src/bridge.ts`), which `ExtensionBridge` implements.
|
|
14
|
+
|
|
15
|
+
The server opens a WebSocket listener on `127.0.0.1:<PL_WS_PORT>`. A browser-side **executor** (either the packaged browser extension or a pasted bootstrap snippet) connects *out* to it, receives command frames, calls `window.projectionlabPluginAPI` in the real, already-logged-in ProjectionLab tab, and returns results. No second browser, no second login.
|
|
16
|
+
|
|
17
|
+
### The wire protocol
|
|
18
|
+
|
|
19
|
+
`src/types/wire.ts` is the contract shared by the server and every executor:
|
|
20
|
+
|
|
21
|
+
- The executor connects and sends a `hello` frame with a shared token and protocol version. The server validates the `Origin` header (extension or app origin only), checks the token, and replies `helloAck`, or closes with code `4001` (auth), `4002` (version), or `4003` (a newer executor replaced this one).
|
|
22
|
+
- Requests are `{ id, method, params }`; responses are `{ id, ok, result | error }`. Methods map directly to Plugin API calls plus `describeApi`/`waitForApi`.
|
|
23
|
+
- A ~20 s `ping`/`pong` heartbeat keeps the connection healthy (and keeps a Manifest V3 service worker from being suspended).
|
|
24
|
+
|
|
25
|
+
### The browser extension
|
|
26
|
+
|
|
27
|
+
Manifest V3, in `extension/`. A background service worker owns the WebSocket, reconnects with exponential backoff, and uses an `alarms` wake after suspension. For each request it locates a ProjectionLab tab (opening one in the background when configured to do so) and uses `scripting.executeScript` to run the operation directly in the page's **MAIN world**, where `window.projectionlabPluginAPI` is visible. The structured result returns through the scripting API without a content-script relay. Chrome/Edge/Brave use the service-worker manifest; Firefox 128 or newer uses an event-page manifest with MAIN-world scripting support.
|
|
28
|
+
|
|
29
|
+
### The bootstrap snippet
|
|
30
|
+
|
|
31
|
+
`src/bootstrap.ts` generates a self-contained snippet (returned by the `projectionlab_get_bootstrap` tool) that plays the same executor role from the page's console, with the same protocol and methods. It's the zero-install path and a handy debugging harness, but it lives only until the tab reloads.
|
|
32
|
+
|
|
33
|
+
## 3. Step-by-Step Execution Lifecycle
|
|
34
|
+
|
|
35
|
+
### A. Initialization
|
|
36
|
+
|
|
37
|
+
The server starts listening immediately. Tool calls made before an executor connects return an actionable `EXTENSION_NOT_CONNECTED` error. Once the extension (or snippet) connects and authenticates, requests flow.
|
|
38
|
+
|
|
39
|
+
### B. Read Operations (`export_data`, `get_plan`, `get_summary`, …)
|
|
40
|
+
|
|
41
|
+
1. The MCP tool validates input with Zod.
|
|
42
|
+
2. The bridge sends an `exportData({ key })` request over the WebSocket to the executor, which calls it in the browser context.
|
|
43
|
+
3. The browser serializes your financial state to JSON and returns it.
|
|
44
|
+
4. The server selects or summarizes the requested data, redacts secrets where appropriate, and returns it to the MCP host. Results above the inline size limit are written to a protected local export file and returned as a file pointer. A short-lived export cache avoids re-serializing the complete state on every read; every write invalidates it.
|
|
45
|
+
|
|
46
|
+
### C. Write Operations & Safety Flow (`update_*`, `restore_*`, `clone_plan`)
|
|
47
|
+
|
|
48
|
+
The Plugin API provides a targeted `updateAccount` operation for Current Finances and whole-area restore operations (`restorePlans` and the others) for everything else. Plan edits, including `clone_plan`, are built as a complete new plans array and written with `restorePlans`. The server builds safe patches on top of those primitives and enforces a multi-layered safety net:
|
|
49
|
+
|
|
50
|
+
1. **Dry-Run by Default:** without `confirm: true`, the tool returns a structured diff preview instead of mutating anything.
|
|
51
|
+
2. **Fresh State:** the tool requests an uncached export and validates the requested change against the current data.
|
|
52
|
+
3. **Auto-Snapshotting:** on `confirm: true`, the tool passes the affected current section to `SnapshotManager`, which writes a restore-safe local JSON snapshot with an ID and timestamp.
|
|
53
|
+
4. **Conflict Check:** because `restorePlans` replaces every plan, a plan write re-reads the plans just before writing and refuses with `PLAN_CONFLICT` if any plan was added, removed, or saved in ProjectionLab since the tool read them (`assertPlansUnchanged` in `src/planVersion.ts`). `projectionlab_update_plan_section` also accepts the dry run's `planVersion` as `expectedPlanVersion` and refuses if the plan was saved after the preview.
|
|
54
|
+
5. **Execution:** only then does the bridge call `updateAccount()` or the appropriate `restore*()` method.
|
|
55
|
+
6. **Verification:** the tool performs a fresh re-read and checks that the requested change landed. A verification failure is reported with the snapshot retained for recovery.
|
|
56
|
+
|
|
57
|
+
### D. Rollbacks & Recovery
|
|
58
|
+
|
|
59
|
+
`projectionlab_rollback` reads a saved snapshot and pipes it back through the matching `restore_*` method, reverting the tab's state. A pre-rollback snapshot is also saved.
|
|
60
|
+
|
|
61
|
+
## 4. Security
|
|
62
|
+
|
|
63
|
+
- The Plugin API key is read only from the MCP host's `env` block, `~/.projectionlab-mcp/.env`, or a development `.env` (gitignored). It is never logged. It stays in the server process and is passed to the page only as an argument to Plugin API calls; the extension does not persist it.
|
|
64
|
+
- The WebSocket binds to `127.0.0.1` only, accepts only the ProjectionLab app and browser-extension origins, and requires a shared token (`PL_WS_TOKEN`, or auto-generated at `~/.projectionlab-mcp/ws-token` with owner-only permissions), so neither other local processes nor arbitrary web pages can drive your data. Native clients without an `Origin` header still need the token.
|
|
65
|
+
- Snapshots scrub the literal Plugin API key while preserving restore data. Exports, plan reads, and the `plan-data` resource additionally redact secret-named fields before they are returned or written to disk.
|
|
66
|
+
- Read-only mode (the default; any `PL_READ_ONLY` other than `false`) rejects mutation tools before they read, snapshot, or write plan data.
|
|
67
|
+
- The published npm package ships only `dist/`, the agent skill, and docs (the `files` allowlist in `package.json`), so source and `.env` files are never distributed.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ted Lanham
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,296 @@
|
|
|
1
|
-
#
|
|
1
|
+
# ProjectionLab MCP Server
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Talk to an AI assistant about your retirement plan, and let it read and update your numbers in [ProjectionLab](https://projectionlab.com) for you.**
|
|
4
|
+
|
|
5
|
+
ProjectionLab is a retirement and financial-planning app. This project connects it to an AI assistant (like Claude, or any assistant that supports the "MCP" standard; more on that below) so you can ask questions in plain English and get real answers based on *your* actual plan:
|
|
6
|
+
|
|
7
|
+
- *"Am I on track to retire at 60?"*
|
|
8
|
+
- *"What happens to my plan if the market returns 2% less than I assumed?"*
|
|
9
|
+
- *"Bump my planned travel spending to $12,000 a year and show me the effect."*
|
|
10
|
+
|
|
11
|
+
The assistant can look at your plan, walk through the tradeoffs with you, and, only when you say so, make changes for you. Every change is previewed before it happens and can be undone.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Is this for me?
|
|
16
|
+
|
|
17
|
+
You'll get value from this if:
|
|
18
|
+
|
|
19
|
+
- You already use (or want to use) **ProjectionLab** for retirement planning, and
|
|
20
|
+
- You use an **AI assistant that supports MCP** (see below).
|
|
21
|
+
|
|
22
|
+
You do **not** need to be a programmer. The recommended path is to let your assistant do the technical setup for you. You'll mostly be answering a few questions and clicking a couple of buttons in your browser.
|
|
23
|
+
|
|
24
|
+
> **A note on trust and safety:** This tool can *change* your financial plan, so it's built to be careful. It starts in **read-only mode** (it can look but not touch). Nothing is changed without showing you a preview first, and every change is backed up so it can be reversed. Your ProjectionLab API key stays on your own computer and is never sent anywhere.
|
|
25
|
+
|
|
26
|
+
### What's "MCP"? Which assistants work?
|
|
27
|
+
|
|
28
|
+
**MCP** (Model Context Protocol) is an open standard that lets AI assistants safely connect to outside tools. This project is an MCP "server," so it works with **any** assistant or app that supports MCP, so you are not locked into one company. Popular options include **Claude Desktop** and **Claude Code**, and a growing list of other MCP-compatible assistants and editors. If your assistant can "add an MCP server," it can use this.
|
|
29
|
+
|
|
30
|
+
*(The author uses Claude Code, but that's just a preference. Pick whatever you like.)*
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## What you'll need
|
|
35
|
+
|
|
36
|
+
1. A **ProjectionLab account** with **Plugins enabled**. In ProjectionLab, go to **Account Settings → Plugins**, turn plugins on, and copy your **Plugin API Key** (a long code; keep it handy, since you'll give it to your assistant).
|
|
37
|
+
2. An **AI assistant that supports MCP** (see above).
|
|
38
|
+
3. A **web browser** you already use: Chrome, Edge, Brave, or Firefox (version 128 or newer).
|
|
39
|
+
4. **Node.js** installed on your computer (version 20 or newer). If you don't have it, your assistant can help you install it, or download it from [nodejs.org](https://nodejs.org).
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## The easy way: let your assistant set it up
|
|
44
|
+
|
|
45
|
+
Open your AI assistant and give it this instruction (copy-paste it as-is):
|
|
46
|
+
|
|
47
|
+
> Please set up the ProjectionLab MCP server (the `projectionlab-mcp` npm
|
|
48
|
+
> package) for me. Follow the "For your AI assistant" section of its README at
|
|
49
|
+
> `https://github.com/Backroads4Me/projectionlab-mcp`. I'm not technical, so
|
|
50
|
+
> walk me through anything you need from me (like my ProjectionLab Plugin API key
|
|
51
|
+
> and loading the browser extension) one step at a time. Keep it in read-only
|
|
52
|
+
> mode until I say otherwise.
|
|
53
|
+
|
|
54
|
+
Your assistant will download the project, install it, and hand you a couple of short, click-by-click steps in your browser (loading a small helper extension and pasting in a code). When it's done, just start asking questions about your plan.
|
|
55
|
+
|
|
56
|
+
If anything gets stuck, tell your assistant what you see on screen; it can troubleshoot from there.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## For your AI assistant
|
|
61
|
+
|
|
62
|
+
*(This section is written for the AI agent doing the install. If you're a person reading this, you can skip it. Your assistant follows these steps.)*
|
|
63
|
+
|
|
64
|
+
You are setting this up for a **non-technical user**. Explain each thing you need from them in plain language, one step at a time, and never ask them to do something you can do yourself.
|
|
65
|
+
|
|
66
|
+
**1. Get the user's Plugin API key.** Ask them to open ProjectionLab → Account Settings → Plugins, enable plugins, and copy their **Plugin API Key**. Do **not** invent one, and never print, log, or commit it.
|
|
67
|
+
|
|
68
|
+
**2. Store the key.** Write it to `~/.projectionlab-mcp/.env` as the line `PL_API_KEY=...` and make the file readable only by the user (`chmod 600`). Every MCP host on the machine reads this file, so the key lives in one place. Do not repeat the key back to the user. Writes stay disabled by default; do not set `PL_READ_ONLY=false` until the user has seen the tool working and explicitly asks.
|
|
69
|
+
|
|
70
|
+
**3. Register the server with the user's MCP host.** The server runs with `npx`, which needs Node.js 20 or newer:
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"mcpServers": {
|
|
74
|
+
"projectionlab": {
|
|
75
|
+
"command": "npx",
|
|
76
|
+
"args": ["-y", "projectionlab-mcp"]
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
The config location varies by host; see [Adding the server to your MCP host](#adding-the-server-to-your-mcp-host) for common ones. Have the user restart the host so it starts the server. On first start the server creates the browser connection token at `~/.projectionlab-mcp/ws-token`.
|
|
82
|
+
|
|
83
|
+
**4. Connect the browser.** The server reaches ProjectionLab through the user's own browser. Pick the path that fits:
|
|
84
|
+
|
|
85
|
+
- **Recommended: the browser extension** (persists across restarts). Download the zip for the user's browser from the [latest release](https://github.com/Backroads4Me/projectionlab-mcp/releases/latest) and unzip it. Then walk the user through loading it (this is a click-by-click browser task; describe exactly what they'll see):
|
|
86
|
+
- **Chrome / Edge / Brave:** open `chrome://extensions`, turn on "Developer mode" (top-right), click "Load unpacked", and choose the unzipped Chromium folder.
|
|
87
|
+
- **Firefox (128+):** open `about:debugging` → "This Firefox" → "Load Temporary Add-on", and choose `manifest.json` in the unzipped Firefox folder. Firefox removes temporary add-ons when it restarts.
|
|
88
|
+
Then have them open the extension's **options page** and enter the WebSocket **port** (default `8788`) and the **shared token** from `~/.projectionlab-mcp/ws-token`.
|
|
89
|
+
|
|
90
|
+
- **Quick alternative: no install.** Call the `projectionlab_get_bootstrap` tool, then have the user paste the returned snippet into their ProjectionLab tab's developer console (F12 → Console). It works the same way but must be re-pasted if they reload the page. Good for a first test drive.
|
|
91
|
+
|
|
92
|
+
**5. Verify.** Have the user open [ProjectionLab](https://app.projectionlab.com), log in, and confirm plugins are enabled. Then call `projectionlab_check_connection`; it should report all checks passing. Do this before any other tool call.
|
|
93
|
+
|
|
94
|
+
**6. Enabling changes (writes).** Leave everything in dry-run/read-only until the user explicitly asks to make changes. When they do: set `PL_READ_ONLY=false` in `~/.projectionlab-mcp/.env` or the host's `env` block, restart the host, and remember that every write still previews first (`confirm: true` required) and auto-snapshots so it can be rolled back. Reassure the user of this before the first change.
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Technical reference
|
|
99
|
+
|
|
100
|
+
The rest of this document is for developers or advanced users. Everyday use needs none of it.
|
|
101
|
+
|
|
102
|
+
### How it works
|
|
103
|
+
|
|
104
|
+
ProjectionLab has no REST API, so the server reaches its browser-side `window.projectionlabPluginAPI` over a localhost WebSocket that a browser-side executor connects out to: the **extension** for normal use, or a pasted **bootstrap snippet** for a zero-install test. See **[HOW-IT-WORKS.md](HOW-IT-WORKS.md)** for the full architecture: the bridge design, wire protocol, extension internals, and safety model.
|
|
105
|
+
|
|
106
|
+
### Adding the server to your MCP host
|
|
107
|
+
|
|
108
|
+
The server is a standard stdio MCP server started with `npx -y projectionlab-mcp`. Put the key in `~/.projectionlab-mcp/.env` (see [Configuration](#configuration)) so no host config holds a secret.
|
|
109
|
+
|
|
110
|
+
**Claude Code:**
|
|
111
|
+
```bash
|
|
112
|
+
claude mcp add --scope user projectionlab -- npx -y projectionlab-mcp
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**Claude Desktop** (`claude_desktop_config.json`) and other hosts that use the `mcpServers` JSON format:
|
|
116
|
+
```json
|
|
117
|
+
{
|
|
118
|
+
"mcpServers": {
|
|
119
|
+
"projectionlab": {
|
|
120
|
+
"command": "npx",
|
|
121
|
+
"args": ["-y", "projectionlab-mcp"]
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**Hermes Agent** (`config.yaml` of the profile):
|
|
128
|
+
```yaml
|
|
129
|
+
mcp_servers:
|
|
130
|
+
projectionlab:
|
|
131
|
+
command: npx
|
|
132
|
+
args: ["-y", "projectionlab-mcp"]
|
|
133
|
+
enabled: true
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Only one server instance can hold the WebSocket port. When a second host starts the server while another instance holds it, the second retries every few seconds and takes over when the first exits; `projectionlab_check_connection` explains the state in the meantime. To run both at once, give one of them its own `PL_WS_PORT` in its `env` block.
|
|
137
|
+
|
|
138
|
+
### Agent instructions
|
|
139
|
+
|
|
140
|
+
The server carries its own instructions for the agents that use it, so no per-host prompt setup is needed:
|
|
141
|
+
|
|
142
|
+
- **Server instructions**, sent in the MCP initialize result. Hosts that surface them give the model the core rules: check the connection first, the read-only/dry-run/confirm workflow, privacy.
|
|
143
|
+
- **The `projectionlab://guide` resource**, the full agent guide: setup walkthrough for each failed connection check, which tool answers which question, the change workflow, and privacy rules. `projectionlab_check_connection` points agents to it.
|
|
144
|
+
- **The `projectionlab` skill** at [`skills/projectionlab/SKILL.md`](skills/projectionlab/SKILL.md), the same guide in the Agent Skills format, for hosts that load skills (copy the folder into the host's skills directory).
|
|
145
|
+
|
|
146
|
+
### Manual setup from source
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
npm install
|
|
150
|
+
npm run build
|
|
151
|
+
cd extension && npm install && npm run build # build the extension
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Register `node /absolute/path/to/projectionlab-mcp/dist/index.js` with your MCP host, load the built extension (`extension/dist/chromium` or `extension/dist/firefox`), configure its port + token on the options page, then run `projectionlab_check_connection`.
|
|
155
|
+
|
|
156
|
+
### Configuration
|
|
157
|
+
|
|
158
|
+
Each setting is read from the first of these that sets it:
|
|
159
|
+
|
|
160
|
+
1. The `env` block of the server entry in the MCP host's config.
|
|
161
|
+
2. `~/.projectionlab-mcp/.env`, shared by every MCP host on the machine.
|
|
162
|
+
3. `.env` at the package root, for a development checkout (copy `.env.example`).
|
|
163
|
+
|
|
164
|
+
The server starts without `PL_API_KEY` so it can tell the agent what is missing; tools that need the key return `API_KEY_MISSING` until it is set.
|
|
165
|
+
|
|
166
|
+
### Environment variables
|
|
167
|
+
|
|
168
|
+
| Variable | Required | Default | Description |
|
|
169
|
+
|---|---|---|---|
|
|
170
|
+
| `PL_API_KEY` | Yes | - | ProjectionLab Plugin API key; tools that reach the plan fail until it is set |
|
|
171
|
+
| `PL_WS_PORT` | No | `8788` | Localhost WebSocket port |
|
|
172
|
+
| `PL_WS_TOKEN` | No | auto-generated | Shared auth token; if unset, generated and saved to `~/.projectionlab-mcp/ws-token` |
|
|
173
|
+
| `PL_APP_URL` | No | `https://app.projectionlab.com` | ProjectionLab app URL |
|
|
174
|
+
| `PL_API_READY_TIMEOUT_MS` | No | `30000` | Timeout waiting for Plugin API to attach |
|
|
175
|
+
| `PL_EXPORT_CACHE_MS` | No | `90000` | How long a full export may be served from cache (writes always invalidate) |
|
|
176
|
+
| `PL_PRETTY_JSON` | No | `false` | Pretty-print JSON output (uses ~25–40% more tokens) |
|
|
177
|
+
| `PL_READ_ONLY` | No | `true` | Write/mutation tools are disabled unless this is exactly `false` |
|
|
178
|
+
| `PL_SNAPSHOT_DIR` | No | `~/.projectionlab-mcp/snapshots` | Directory for pre-mutation snapshots |
|
|
179
|
+
| `LOG_LEVEL` | No | `info` | Log verbosity (`debug`, `info`, `warn`, `error`) |
|
|
180
|
+
|
|
181
|
+
### Available tools
|
|
182
|
+
|
|
183
|
+
| Tool | Description | Mutates |
|
|
184
|
+
|---|---|---|
|
|
185
|
+
| `projectionlab_check_connection` | Verify browser + API connectivity | No |
|
|
186
|
+
| `projectionlab_get_bootstrap` | Return a console snippet to connect a ProjectionLab tab | No |
|
|
187
|
+
| `projectionlab_describe_plugin_api` | Describe the available Plugin API surface | No |
|
|
188
|
+
| `projectionlab_describe_schema` | Describe the data schema (plan names, account IDs) | No |
|
|
189
|
+
| `projectionlab_export_data` | Export full plan state as JSON | No |
|
|
190
|
+
| `projectionlab_get_summary` | Inventory Current Finances balances and current net worth (not projected outcomes) | No |
|
|
191
|
+
| `projectionlab_get_plan` | Retrieve a single plan | No |
|
|
192
|
+
| `projectionlab_get_plan_section` | Retrieve a plan section; supports event/path filters and typed field-path listing | No |
|
|
193
|
+
| `projectionlab_get_plan_settings` | List discrete plan settings as path-addressable values | No |
|
|
194
|
+
| `projectionlab_get_current_finances` | Get Current Finances or list typed paths for one account | No |
|
|
195
|
+
| `projectionlab_update_account` | Patch one Current Finances account by top-level fields or nested dot paths | Yes |
|
|
196
|
+
| `projectionlab_update_plan_event` | Patch one event by top-level fields or nested dot paths | Yes |
|
|
197
|
+
| `projectionlab_update_plan_settings` | Patch existing assumptions, Monte Carlo, or withdrawal settings by path | Yes |
|
|
198
|
+
| `projectionlab_update_plan_section` | Replace a whole section of a plan (dry-run by default) | Yes |
|
|
199
|
+
| `projectionlab_clone_plan` | Copy a plan to a new, named plan for what-if scenarios | Yes |
|
|
200
|
+
| `projectionlab_list_snapshots` | List saved pre-mutation snapshots | No |
|
|
201
|
+
| `projectionlab_restore_current_finances` | Restore Current Finances from snapshot | Yes |
|
|
202
|
+
| `projectionlab_restore_plans` | Restore Plans from snapshot | Yes |
|
|
203
|
+
| `projectionlab_restore_progress` | Restore Progress from snapshot | Yes |
|
|
204
|
+
| `projectionlab_restore_settings` | Restore Settings from snapshot | Yes |
|
|
205
|
+
| `projectionlab_rollback` | Roll back to a previous snapshot | Yes |
|
|
206
|
+
|
|
207
|
+
### Resources
|
|
208
|
+
|
|
209
|
+
| URI | Description |
|
|
210
|
+
|---|---|
|
|
211
|
+
| `projectionlab://plan-data` | Full export of all plans/finances/progress/settings (refreshed per access) |
|
|
212
|
+
| `projectionlab://summary` | High-level snapshot: net worth, account breakdown, plan count |
|
|
213
|
+
| `projectionlab://knowledge` | Curated ProjectionLab data-model + FIRE/tax reference (static) |
|
|
214
|
+
| `projectionlab://guide` | Agent guide: setup walkthrough, tool selection, change workflow, privacy rules (static) |
|
|
215
|
+
|
|
216
|
+
### Prompts
|
|
217
|
+
|
|
218
|
+
| Prompt | Purpose |
|
|
219
|
+
|---|---|
|
|
220
|
+
| `analyze-plan` | Structured analysis: cash flow, tax, risk, withdrawal, estate |
|
|
221
|
+
| `compare-plans` | Side-by-side comparison of two plans |
|
|
222
|
+
| `optimize-tax` | Roth conversion / bracket / IRMAA / gain-harvesting review |
|
|
223
|
+
| `review-assumptions` | Stress-test returns, inflation, healthcare, longevity |
|
|
224
|
+
|
|
225
|
+
### Safety model
|
|
226
|
+
|
|
227
|
+
Write operations (`update_*`, `restore_*`, `clone_plan`) default to **dry-run mode**. Pass `confirm: true` to execute. Before any confirmed mutation, the server automatically snapshots the affected data section to disk so you can roll back with `projectionlab_rollback`. Writes are disabled entirely unless `PL_READ_ONLY=false`.
|
|
228
|
+
|
|
229
|
+
Every write that replaces the plans array re-reads the plans just before writing and refuses with `PLAN_CONFLICT` if any plan was added, removed, or saved in ProjectionLab since the tool read them, so an edit made in the ProjectionLab UI is never silently overwritten. `projectionlab_update_plan_section` also returns a `planVersion` in its dry run; pass it back as `expectedPlanVersion` when confirming to refuse the write if the plan was saved after the preview.
|
|
230
|
+
|
|
231
|
+
For a small assumptions or strategy change, first call
|
|
232
|
+
`projectionlab_get_plan_settings` with the plan name. It returns the current
|
|
233
|
+
`variables`, `montecarlo`, and `withdrawalStrategy` values as dot paths with
|
|
234
|
+
their JSON types. Preview a path-level patch with
|
|
235
|
+
`projectionlab_update_plan_settings`, for example:
|
|
236
|
+
|
|
237
|
+
```json
|
|
238
|
+
{
|
|
239
|
+
"planIdentifier": "Retire at 55",
|
|
240
|
+
"section": "variables",
|
|
241
|
+
"changes": {
|
|
242
|
+
"showFutureDollars": true
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
The update tool preserves sibling settings, rejects unknown paths and JSON type
|
|
248
|
+
changes, and still requires `"confirm": true` before it sends a mutation.
|
|
249
|
+
Advanced inflation, stock-growth, dividend-yield, bond-growth, and bond-yield
|
|
250
|
+
schedules expose both the complete ordered array and indexed control-point
|
|
251
|
+
fields, so one existing year or rate can be adjusted without replacing the
|
|
252
|
+
rest of the schedule.
|
|
253
|
+
|
|
254
|
+
Event forms use the same path workflow. Fetch one event with
|
|
255
|
+
`projectionlab_get_plan_section` using its `eventIdentifier` and
|
|
256
|
+
`"listPaths": true`, then pass selected paths to
|
|
257
|
+
`projectionlab_update_plan_event` as `pathChanges`. The older `patch` input
|
|
258
|
+
remains available for shallow top-level changes.
|
|
259
|
+
|
|
260
|
+
Current Finances accounts also support targeted discovery and nested patches.
|
|
261
|
+
Call `projectionlab_get_current_finances` with an exact `accountId` and
|
|
262
|
+
`"listPaths": true`, then pass selected paths to
|
|
263
|
+
`projectionlab_update_account` as `pathChanges`. The legacy `data` input
|
|
264
|
+
remains available for top-level fields. Dry runs validate the account and
|
|
265
|
+
return before/after values as a safety diff; confirmed no-ops do not create a
|
|
266
|
+
snapshot or send a mutation.
|
|
267
|
+
|
|
268
|
+
### Evaluating plan impact
|
|
269
|
+
|
|
270
|
+
Projected ending net worth and Chance of Success are the primary outcome
|
|
271
|
+
metrics for comparing a plan before and after a change. Current Finances
|
|
272
|
+
balances are inputs. Mutation before/after values verify that the requested
|
|
273
|
+
input changed, but they do not measure the plan's financial impact.
|
|
274
|
+
|
|
275
|
+
ProjectionLab's Plugin API currently exposes plan definitions and simulation
|
|
276
|
+
settings, but not a supported method to recalculate and retrieve projected
|
|
277
|
+
ending net worth or a completed Chance of Success result. After a confirmed
|
|
278
|
+
mutation, re-read the targeted input through MCP for verification, then
|
|
279
|
+
recalculate the plan and run Chance of Success in the ProjectionLab UI before
|
|
280
|
+
drawing a conclusion.
|
|
281
|
+
|
|
282
|
+
### Secret handling
|
|
283
|
+
|
|
284
|
+
The Plugin API key stays on your computer: it is never logged, and the browser side never stores it. The WebSocket accepts only local connections that present a shared token. See [Security in HOW-IT-WORKS.md](HOW-IT-WORKS.md#4-security) for the details.
|
|
285
|
+
|
|
286
|
+
### After code changes
|
|
287
|
+
|
|
288
|
+
This server runs over stdio; the host process keeps compiled `dist/` in memory.
|
|
289
|
+
|
|
290
|
+
```bash
|
|
291
|
+
npm run build
|
|
292
|
+
# Then restart your MCP host
|
|
293
|
+
projectionlab_check_connection # verify before other operations
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
With the extension the browser side reconnects on its own, so you only restart the MCP host. If you change extension source, rebuild it (`cd extension && npm run build`) and reload it in the browser.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tier 0 transport: a self-contained snippet the user pastes into the
|
|
3
|
+
* ProjectionLab tab's devtools console. It plays the same executor role as
|
|
4
|
+
* the browser extension - connects to the local WebSocket server, answers
|
|
5
|
+
* ping and request frames against window.projectionlabPluginAPI - but lives
|
|
6
|
+
* only until the tab reloads. Keep the executor logic in sync with
|
|
7
|
+
* extension/src/page-main.js.
|
|
8
|
+
*/
|
|
9
|
+
export declare function buildBootstrapSnippet(port: number, token: string): string;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { PROTOCOL_VERSION } from "./types/wire.js";
|
|
2
|
+
import { VERSION } from "./version.js";
|
|
3
|
+
/**
|
|
4
|
+
* Tier 0 transport: a self-contained snippet the user pastes into the
|
|
5
|
+
* ProjectionLab tab's devtools console. It plays the same executor role as
|
|
6
|
+
* the browser extension - connects to the local WebSocket server, answers
|
|
7
|
+
* ping and request frames against window.projectionlabPluginAPI - but lives
|
|
8
|
+
* only until the tab reloads. Keep the executor logic in sync with
|
|
9
|
+
* extension/src/page-main.js.
|
|
10
|
+
*/
|
|
11
|
+
export function buildBootstrapSnippet(port, token) {
|
|
12
|
+
const cfg = JSON.stringify({
|
|
13
|
+
port,
|
|
14
|
+
token,
|
|
15
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
16
|
+
executor: `bootstrap/${VERSION}`,
|
|
17
|
+
});
|
|
18
|
+
return `(() => {
|
|
19
|
+
const cfg = ${cfg};
|
|
20
|
+
if (window.__plmcpBootstrap) { console.log("[plmcp] bootstrap already running"); return; }
|
|
21
|
+
window.__plmcpBootstrap = true;
|
|
22
|
+
let stopped = false;
|
|
23
|
+
const log = (m) => console.log("[plmcp]", m);
|
|
24
|
+
function waitForApi(timeoutMs) {
|
|
25
|
+
return new Promise((resolve, reject) => {
|
|
26
|
+
const deadline = Date.now() + timeoutMs;
|
|
27
|
+
(function poll() {
|
|
28
|
+
if (window.projectionlabPluginAPI) return resolve(window.projectionlabPluginAPI);
|
|
29
|
+
if (Date.now() >= deadline) { const e = new Error("Plugin API did not appear within " + Math.round(timeoutMs / 1000) + "s - log in and enable plugins"); e.code = "API_NOT_AVAILABLE"; return reject(e); }
|
|
30
|
+
setTimeout(poll, 500);
|
|
31
|
+
})();
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
function describeApi(api) {
|
|
35
|
+
const out = {}; const seen = new Set(); let o = api;
|
|
36
|
+
while (o && o !== Object.prototype) {
|
|
37
|
+
for (const k of Object.getOwnPropertyNames(o)) {
|
|
38
|
+
if (k === "constructor" || seen.has(k)) continue;
|
|
39
|
+
seen.add(k);
|
|
40
|
+
const v = api[k];
|
|
41
|
+
out[k] = typeof v === "function" ? "function(" + v.length + " args)" : typeof v;
|
|
42
|
+
}
|
|
43
|
+
o = Object.getPrototypeOf(o);
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
async function handle(frame, ws) {
|
|
48
|
+
try {
|
|
49
|
+
const requested = frame.method === "waitForApi" ? Number((frame.params || [])[0]) : NaN;
|
|
50
|
+
const api = await waitForApi(Number.isFinite(requested) && requested > 0 ? requested : 15000);
|
|
51
|
+
let result;
|
|
52
|
+
if (frame.method === "waitForApi") result = true;
|
|
53
|
+
else if (frame.method === "describeApi") result = describeApi(api);
|
|
54
|
+
else {
|
|
55
|
+
const fn = api[frame.method];
|
|
56
|
+
if (typeof fn !== "function") throw new Error("Plugin API method unavailable: " + frame.method);
|
|
57
|
+
result = await fn.apply(api, frame.params);
|
|
58
|
+
}
|
|
59
|
+
ws.send(JSON.stringify({ type: "response", id: frame.id, ok: true, result }));
|
|
60
|
+
} catch (err) {
|
|
61
|
+
const msg = err && err.message ? err.message : String(err);
|
|
62
|
+
const code = err && err.code === "API_NOT_AVAILABLE" ? "API_NOT_AVAILABLE"
|
|
63
|
+
: /api.?key|invalid key|unauthori[sz]ed/i.test(msg) ? "API_KEY_INVALID" : "UNKNOWN";
|
|
64
|
+
ws.send(JSON.stringify({ type: "response", id: frame.id, ok: false, error: { code, message: msg } }));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function connect() {
|
|
68
|
+
if (stopped) return;
|
|
69
|
+
const ws = new WebSocket("ws://127.0.0.1:" + cfg.port);
|
|
70
|
+
ws.onopen = () => ws.send(JSON.stringify({ type: "hello", token: cfg.token, protocolVersion: cfg.protocolVersion, executor: cfg.executor }));
|
|
71
|
+
ws.onmessage = (ev) => {
|
|
72
|
+
let frame;
|
|
73
|
+
try { frame = JSON.parse(ev.data); } catch { return; }
|
|
74
|
+
if (frame.type === "helloAck") log("connected to projectionlab-mcp");
|
|
75
|
+
else if (frame.type === "ping") ws.send(JSON.stringify({ type: "pong" }));
|
|
76
|
+
else if (frame.type === "request") handle(frame, ws);
|
|
77
|
+
};
|
|
78
|
+
ws.onclose = (ev) => {
|
|
79
|
+
if (stopped) return;
|
|
80
|
+
if (ev.code === 4001 || ev.code === 4002) { stopped = true; window.__plmcpBootstrap = false; log("stopped: " + (ev.reason || ev.code)); return; }
|
|
81
|
+
if (ev.code === 4003) { stopped = true; window.__plmcpBootstrap = false; log("replaced by another executor (probably the extension) - bootstrap no longer needed"); return; }
|
|
82
|
+
log("disconnected; retrying in 3s");
|
|
83
|
+
setTimeout(connect, 3000);
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
connect();
|
|
87
|
+
log("bootstrap installed - keep this tab open; re-paste after a page reload");
|
|
88
|
+
})();`;
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=bootstrap.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bootstrap.js","sourceRoot":"","sources":["../src/bootstrap.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAEvC;;;;;;;GAOG;AACH,MAAM,UAAU,qBAAqB,CAAC,IAAY,EAAE,KAAa;IAC/D,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC;QACzB,IAAI;QACJ,KAAK;QACL,eAAe,EAAE,gBAAgB;QACjC,QAAQ,EAAE,aAAa,OAAO,EAAE;KACjC,CAAC,CAAC;IACH,OAAO;gBACO,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;MAqEb,CAAC;AACP,CAAC"}
|
package/dist/bridge.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ExportedData } from "./types/projectionlab.js";
|
|
2
|
+
export interface ConnectionStatus {
|
|
3
|
+
browser: boolean;
|
|
4
|
+
pageLoaded: boolean;
|
|
5
|
+
apiAvailable: boolean;
|
|
6
|
+
apiKeyValid: boolean;
|
|
7
|
+
errors: string[];
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Surface for reaching window.projectionlabPluginAPI in a live ProjectionLab
|
|
11
|
+
* page. Implemented by ExtensionBridge, which relays over a localhost
|
|
12
|
+
* WebSocket to a browser extension or bootstrap snippet. Tools, resources,
|
|
13
|
+
* prompts, and the snapshot manager depend only on this interface.
|
|
14
|
+
*/
|
|
15
|
+
export interface Bridge {
|
|
16
|
+
checkConnection(): Promise<ConnectionStatus>;
|
|
17
|
+
describePluginApi(): Promise<Record<string, string>>;
|
|
18
|
+
/** Full export, served from a short-lived cache unless `fresh` is true. */
|
|
19
|
+
exportData(opts?: {
|
|
20
|
+
fresh?: boolean;
|
|
21
|
+
}): Promise<ExportedData>;
|
|
22
|
+
updateAccount(accountId: string, data: Record<string, unknown>, force?: boolean): Promise<void>;
|
|
23
|
+
restoreCurrentFinances(state: unknown): Promise<void>;
|
|
24
|
+
restorePlans(plans: unknown[]): Promise<void>;
|
|
25
|
+
restoreProgress(progress: unknown): Promise<void>;
|
|
26
|
+
restoreSettings(settings: unknown): Promise<void>;
|
|
27
|
+
invalidateCache(): void;
|
|
28
|
+
close(): Promise<void>;
|
|
29
|
+
}
|
package/dist/bridge.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bridge.js","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type PlanObj } from "./plans.js";
|
|
2
|
+
/** True when another plan already uses `name` (case-insensitive, trimmed). */
|
|
3
|
+
export declare function planNameTaken(plans: unknown[], name: string): boolean;
|
|
4
|
+
/**
|
|
5
|
+
* Build a copy of `source` to append to `plans` as a new plan.
|
|
6
|
+
*
|
|
7
|
+
* Event ids are kept as they are. Event ids are scoped to their plan:
|
|
8
|
+
* ProjectionLab's own duplicated plans share every event id, built-in
|
|
9
|
+
* milestones use fixed ids such as "retirement", and events, Monte Carlo
|
|
10
|
+
* settings, the withdrawal strategy and bond account order all reference
|
|
11
|
+
* milestone and account ids within the same plan. Copying them unchanged keeps
|
|
12
|
+
* those references intact.
|
|
13
|
+
*
|
|
14
|
+
* The plan-level identity is replaced: a new id and name, a fresh lastUpdated,
|
|
15
|
+
* inactive, and simKey / meta.index / meta.runKey one past the highest among
|
|
16
|
+
* `plans` so the copy does not collide with an existing plan.
|
|
17
|
+
*/
|
|
18
|
+
export declare function buildPlanClone(source: PlanObj, plans: unknown[], newName: string, now?: number): PlanObj;
|
|
19
|
+
export declare function verifyPlanClone(before: unknown[], after: unknown[], clone: PlanObj): string[];
|