projectionlab-mcp 1.0.1 → 1.2.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 +2 -2
- package/MANUAL-SETUP.md +64 -0
- package/README.md +34 -271
- package/REFERENCE.md +221 -0
- package/SETUP-FOR-AGENTS.md +37 -0
- package/dist/bootstrap.js +20 -3
- package/dist/bootstrap.js.map +1 -1
- package/dist/bridge.d.ts +2 -0
- package/dist/clonePlan.d.ts +9 -0
- package/dist/clonePlan.js +15 -14
- package/dist/clonePlan.js.map +1 -1
- package/dist/config.d.ts +4 -0
- package/dist/config.js +41 -6
- package/dist/config.js.map +1 -1
- package/dist/currentFinances.d.ts +12 -1
- package/dist/currentFinances.js +60 -0
- package/dist/currentFinances.js.map +1 -1
- package/dist/dataModel.d.ts +58 -0
- package/dist/dataModel.js +1429 -0
- package/dist/dataModel.js.map +1 -0
- package/dist/deletePlan.d.ts +17 -0
- package/dist/deletePlan.js +72 -0
- package/dist/deletePlan.js.map +1 -0
- package/dist/diff.d.ts +288 -0
- package/dist/diff.js +744 -0
- package/dist/diff.js.map +1 -0
- package/dist/errors.d.ts +6 -0
- package/dist/errors.js +14 -1
- package/dist/errors.js.map +1 -1
- package/dist/extensionBridge.d.ts +16 -2
- package/dist/extensionBridge.js +104 -23
- package/dist/extensionBridge.js.map +1 -1
- package/dist/index.js +27 -1
- package/dist/index.js.map +1 -1
- package/dist/instructions.d.ts +1 -1
- package/dist/instructions.js +3 -2
- package/dist/instructions.js.map +1 -1
- package/dist/largeOutput.js +25 -0
- package/dist/largeOutput.js.map +1 -1
- package/dist/logger.js +11 -4
- package/dist/logger.js.map +1 -1
- package/dist/pathPatch.d.ts +5 -2
- package/dist/pathPatch.js +16 -2
- package/dist/pathPatch.js.map +1 -1
- package/dist/planSettings.d.ts +1 -1
- package/dist/planSettings.js +3 -3
- package/dist/planSettings.js.map +1 -1
- package/dist/plans.d.ts +73 -10
- package/dist/plans.js +206 -42
- package/dist/plans.js.map +1 -1
- package/dist/prompts/index.js +7 -4
- package/dist/prompts/index.js.map +1 -1
- package/dist/realEstate.d.ts +22 -0
- package/dist/realEstate.js +118 -0
- package/dist/realEstate.js.map +1 -0
- package/dist/redact.d.ts +26 -0
- package/dist/redact.js +93 -1
- package/dist/redact.js.map +1 -1
- package/dist/resources/dataModel.d.ts +3 -0
- package/dist/resources/dataModel.js +66 -0
- package/dist/resources/dataModel.js.map +1 -0
- package/dist/resources/knowledge.js +189 -47
- package/dist/resources/knowledge.js.map +1 -1
- package/dist/resources/planData.js +3 -1
- package/dist/resources/planData.js.map +1 -1
- package/dist/resources/summary.js +4 -1
- package/dist/resources/summary.js.map +1 -1
- package/dist/snapshot.d.ts +21 -0
- package/dist/snapshot.js +116 -12
- package/dist/snapshot.js.map +1 -1
- package/dist/summary.js +19 -6
- package/dist/summary.js.map +1 -1
- package/dist/tools/clonePlan.js +17 -8
- package/dist/tools/clonePlan.js.map +1 -1
- package/dist/tools/connection.js +29 -4
- package/dist/tools/connection.js.map +1 -1
- package/dist/tools/deletePlan.d.ts +4 -0
- package/dist/tools/deletePlan.js +103 -0
- package/dist/tools/deletePlan.js.map +1 -0
- package/dist/tools/diff.d.ts +4 -0
- package/dist/tools/diff.js +417 -0
- package/dist/tools/diff.js.map +1 -0
- package/dist/tools/getCurrentFinances.js +27 -4
- package/dist/tools/getCurrentFinances.js.map +1 -1
- package/dist/tools/getPlan.js +4 -4
- package/dist/tools/getPlan.js.map +1 -1
- package/dist/tools/getPlanSection.js +7 -6
- package/dist/tools/getPlanSection.js.map +1 -1
- package/dist/tools/getPlanSettings.js +10 -6
- package/dist/tools/getPlanSettings.js.map +1 -1
- package/dist/tools/restore.js +18 -1
- package/dist/tools/restore.js.map +1 -1
- package/dist/tools/schema.js +28 -4
- package/dist/tools/schema.js.map +1 -1
- package/dist/tools/snapshots.d.ts +1 -1
- package/dist/tools/snapshots.js +53 -6
- package/dist/tools/snapshots.js.map +1 -1
- package/dist/tools/summary.js +3 -2
- package/dist/tools/summary.js.map +1 -1
- package/dist/tools/updateAccount.js +9 -7
- package/dist/tools/updateAccount.js.map +1 -1
- package/dist/tools/updatePlanEvent.js +71 -33
- package/dist/tools/updatePlanEvent.js.map +1 -1
- package/dist/tools/updatePlanSection.js +17 -32
- package/dist/tools/updatePlanSection.js.map +1 -1
- package/dist/tools/updatePlanSettings.js +11 -7
- package/dist/tools/updatePlanSettings.js.map +1 -1
- package/dist/types/projectionlab.d.ts +3 -1
- package/dist/types/wire.d.ts +27 -2
- package/dist/types/wire.js +6 -2
- package/dist/types/wire.js.map +1 -1
- package/dist/updateCheck.d.ts +22 -0
- package/dist/updateCheck.js +58 -0
- package/dist/updateCheck.js.map +1 -0
- package/dist/versionCompare.d.ts +5 -0
- package/dist/versionCompare.js +33 -0
- package/dist/versionCompare.js.map +1 -0
- package/dist/versionNotices.d.ts +9 -0
- package/dist/versionNotices.js +42 -0
- package/dist/versionNotices.js.map +1 -0
- package/package.json +6 -3
- package/skills/projectionlab/SKILL.md +329 -29
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Setting up ProjectionLab MCP: instructions for AI assistants
|
|
2
|
+
|
|
3
|
+
This file is for the AI agent doing the install. A person reading it can skip
|
|
4
|
+
it: the [README](https://github.com/Backroads4Me/projectionlab-mcp#readme) has
|
|
5
|
+
the steps for people.
|
|
6
|
+
|
|
7
|
+
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.
|
|
8
|
+
|
|
9
|
+
**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.
|
|
10
|
+
|
|
11
|
+
**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.
|
|
12
|
+
|
|
13
|
+
**3. Register the server with the user's MCP host.** The server runs with `npx`, which needs Node.js 20 or newer:
|
|
14
|
+
```json
|
|
15
|
+
{
|
|
16
|
+
"mcpServers": {
|
|
17
|
+
"projectionlab": {
|
|
18
|
+
"command": "npx",
|
|
19
|
+
"args": ["-y", "projectionlab-mcp"]
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
The config location varies by host; see [Adding the server to your MCP host](https://github.com/Backroads4Me/projectionlab-mcp/blob/main/REFERENCE.md#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`.
|
|
25
|
+
|
|
26
|
+
**4. Connect the browser.** The server reaches ProjectionLab through the user's own browser. Pick the path that fits:
|
|
27
|
+
|
|
28
|
+
- **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):
|
|
29
|
+
- **Chrome / Edge / Brave:** open `chrome://extensions`, turn on "Developer mode" (top-right), click "Load unpacked", and choose the unzipped Chromium folder.
|
|
30
|
+
- **Firefox (140+):** 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.
|
|
31
|
+
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`.
|
|
32
|
+
|
|
33
|
+
- **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.
|
|
34
|
+
|
|
35
|
+
**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.
|
|
36
|
+
|
|
37
|
+
**6. Enabling changes (writes).** Leave everything 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 confirm with `projectionlab_check_connection` that `writesEnabled` is `true`. Every write tool still previews unless called with `confirm: true`: show the user each preview and confirm only the change they approve. Every confirmed change is snapshotted first so it can be rolled back. Tell the user this before the first change.
|
package/dist/bootstrap.js
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import { PROTOCOL_VERSION } from "./types/wire.js";
|
|
2
2
|
import { VERSION } from "./version.js";
|
|
3
|
+
/** The Plugin API calls the snippet will run; the meta-operations
|
|
4
|
+
* `describeApi` and `waitForApi` are handled by the snippet itself. */
|
|
5
|
+
const PLUGIN_METHODS = [
|
|
6
|
+
"exportData",
|
|
7
|
+
"updateAccount",
|
|
8
|
+
"restoreCurrentFinances",
|
|
9
|
+
"restorePlans",
|
|
10
|
+
"restoreProgress",
|
|
11
|
+
"restoreSettings",
|
|
12
|
+
"validateApiKey",
|
|
13
|
+
];
|
|
3
14
|
/**
|
|
4
15
|
* Tier 0 transport: a self-contained snippet the user pastes into the
|
|
5
16
|
* ProjectionLab tab's devtools console. It plays the same executor role as
|
|
@@ -17,9 +28,11 @@ export function buildBootstrapSnippet(port, token) {
|
|
|
17
28
|
});
|
|
18
29
|
return `(() => {
|
|
19
30
|
const cfg = ${cfg};
|
|
31
|
+
const PLUGIN_METHODS = ${JSON.stringify(PLUGIN_METHODS)};
|
|
20
32
|
if (window.__plmcpBootstrap) { console.log("[plmcp] bootstrap already running"); return; }
|
|
21
33
|
window.__plmcpBootstrap = true;
|
|
22
34
|
let stopped = false;
|
|
35
|
+
const instanceId = crypto.randomUUID();
|
|
23
36
|
const log = (m) => console.log("[plmcp]", m);
|
|
24
37
|
function waitForApi(timeoutMs) {
|
|
25
38
|
return new Promise((resolve, reject) => {
|
|
@@ -46,6 +59,9 @@ export function buildBootstrapSnippet(port, token) {
|
|
|
46
59
|
}
|
|
47
60
|
async function handle(frame, ws) {
|
|
48
61
|
try {
|
|
62
|
+
if (frame.method !== "waitForApi" && frame.method !== "describeApi" && !PLUGIN_METHODS.includes(frame.method)) {
|
|
63
|
+
throw new Error("Not a wire protocol method: " + frame.method);
|
|
64
|
+
}
|
|
49
65
|
const requested = frame.method === "waitForApi" ? Number((frame.params || [])[0]) : NaN;
|
|
50
66
|
const api = await waitForApi(Number.isFinite(requested) && requested > 0 ? requested : 15000);
|
|
51
67
|
let result;
|
|
@@ -60,14 +76,14 @@ export function buildBootstrapSnippet(port, token) {
|
|
|
60
76
|
} catch (err) {
|
|
61
77
|
const msg = err && err.message ? err.message : String(err);
|
|
62
78
|
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";
|
|
79
|
+
: /api.?key|invalid key|unauthori[sz]ed|forbidden|\\b401\\b|\\b403\\b/i.test(msg) ? "API_KEY_INVALID" : "UNKNOWN";
|
|
64
80
|
ws.send(JSON.stringify({ type: "response", id: frame.id, ok: false, error: { code, message: msg } }));
|
|
65
81
|
}
|
|
66
82
|
}
|
|
67
83
|
function connect() {
|
|
68
84
|
if (stopped) return;
|
|
69
85
|
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 }));
|
|
86
|
+
ws.onopen = () => ws.send(JSON.stringify({ type: "hello", token: cfg.token, protocolVersion: cfg.protocolVersion, executor: cfg.executor, instanceId }));
|
|
71
87
|
ws.onmessage = (ev) => {
|
|
72
88
|
let frame;
|
|
73
89
|
try { frame = JSON.parse(ev.data); } catch { return; }
|
|
@@ -78,7 +94,8 @@ export function buildBootstrapSnippet(port, token) {
|
|
|
78
94
|
ws.onclose = (ev) => {
|
|
79
95
|
if (stopped) return;
|
|
80
96
|
if (ev.code === 4001 || ev.code === 4002) { stopped = true; window.__plmcpBootstrap = false; log("stopped: " + (ev.reason || ev.code)); return; }
|
|
81
|
-
if (ev.code ===
|
|
97
|
+
if (ev.code === 4004) { log("another executor is connected; this tab will connect if that one goes away (retrying in 30s)"); setTimeout(connect, 30000); return; }
|
|
98
|
+
if (ev.code === 4003) { log("another executor took over the connection (retrying in 30s)"); setTimeout(connect, 30000); return; }
|
|
82
99
|
log("disconnected; retrying in 3s");
|
|
83
100
|
setTimeout(connect, 3000);
|
|
84
101
|
};
|
package/dist/bootstrap.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bootstrap.js","sourceRoot":"","sources":["../src/bootstrap.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,
|
|
1
|
+
{"version":3,"file":"bootstrap.js","sourceRoot":"","sources":["../src/bootstrap.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAmB,MAAM,iBAAiB,CAAC;AACpE,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAEvC;uEACuE;AACvE,MAAM,cAAc,GAAiE;IACnF,YAAY;IACZ,eAAe;IACf,wBAAwB;IACxB,cAAc;IACd,iBAAiB;IACjB,iBAAiB;IACjB,gBAAgB;CACjB,CAAC;AAEF;;;;;;;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;2BACQ,IAAI,CAAC,SAAS,CAAC,cAAc,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;MA0EnD,CAAC;AACP,CAAC"}
|
package/dist/bridge.d.ts
CHANGED
package/dist/clonePlan.d.ts
CHANGED
|
@@ -16,4 +16,13 @@ export declare function planNameTaken(plans: unknown[], name: string): boolean;
|
|
|
16
16
|
* `plans` so the copy does not collide with an existing plan.
|
|
17
17
|
*/
|
|
18
18
|
export declare function buildPlanClone(source: PlanObj, plans: unknown[], newName: string, now?: number): PlanObj;
|
|
19
|
+
/**
|
|
20
|
+
* Check a fresh read after appending `clone` to `before`: every original plan
|
|
21
|
+
* is still present with its input content, its `simKey` did not vanish, its
|
|
22
|
+
* `lastUpdated` stamp never went backwards or disappeared, and the clone
|
|
23
|
+
* exists exactly once under its name with the input content that was sent.
|
|
24
|
+
* Returns the problems found; empty
|
|
25
|
+
* means verified. The Current Finances reads are checked by
|
|
26
|
+
* `currentFinancesProblems`, over the same two exports.
|
|
27
|
+
*/
|
|
19
28
|
export declare function verifyPlanClone(before: unknown[], after: unknown[], clone: PlanObj): string[];
|
package/dist/clonePlan.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { ProjectionLabError } from "./errors.js";
|
|
3
|
-
import {
|
|
3
|
+
import { keptPlanIdentityProblems, samePlanContent, } from "./plans.js";
|
|
4
4
|
function maxNumber(values) {
|
|
5
5
|
const nums = values.filter((v) => typeof v === "number");
|
|
6
6
|
return nums.length === 0 ? undefined : Math.max(...nums);
|
|
@@ -62,18 +62,13 @@ export function buildPlanClone(source, plans, newName, now = Date.now()) {
|
|
|
62
62
|
}
|
|
63
63
|
/**
|
|
64
64
|
* Check a fresh read after appending `clone` to `before`: every original plan
|
|
65
|
-
* is still present
|
|
66
|
-
*
|
|
65
|
+
* is still present with its input content, its `simKey` did not vanish, its
|
|
66
|
+
* `lastUpdated` stamp never went backwards or disappeared, and the clone
|
|
67
|
+
* exists exactly once under its name with the input content that was sent.
|
|
68
|
+
* Returns the problems found; empty
|
|
69
|
+
* means verified. The Current Finances reads are checked by
|
|
70
|
+
* `currentFinancesProblems`, over the same two exports.
|
|
67
71
|
*/
|
|
68
|
-
/**
|
|
69
|
-
* True when `after` still holds every value of `before`. The `lastUpdated`
|
|
70
|
-
* stamp is left out: restorePlans saves every plan, so ProjectionLab may
|
|
71
|
-
* re-stamp originals that were not otherwise changed.
|
|
72
|
-
*/
|
|
73
|
-
function samePlanContent(before, after) {
|
|
74
|
-
const { lastUpdated: _stamp, ...content } = before;
|
|
75
|
-
return containsSubset(content, after);
|
|
76
|
-
}
|
|
77
72
|
export function verifyPlanClone(before, after, clone) {
|
|
78
73
|
const problems = [];
|
|
79
74
|
const afterById = new Map();
|
|
@@ -87,11 +82,14 @@ export function verifyPlanClone(before, after, clone) {
|
|
|
87
82
|
continue;
|
|
88
83
|
const id = String(p.id ?? "");
|
|
89
84
|
const post = afterById.get(id);
|
|
90
|
-
if (!post)
|
|
85
|
+
if (!post) {
|
|
91
86
|
problems.push(`original plan ${id} is missing`);
|
|
92
|
-
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (!samePlanContent(p, post)) {
|
|
93
90
|
problems.push(`original plan ${id} was modified`);
|
|
94
91
|
}
|
|
92
|
+
problems.push(...keptPlanIdentityProblems(`original plan ${id}`, p, post));
|
|
95
93
|
}
|
|
96
94
|
const copies = after.filter((p) => !!p && typeof p === "object" && p.id === clone.id);
|
|
97
95
|
if (copies.length !== 1) {
|
|
@@ -100,6 +98,9 @@ export function verifyPlanClone(before, after, clone) {
|
|
|
100
98
|
else if (copies[0].name !== clone.name) {
|
|
101
99
|
problems.push(`new plan is named "${String(copies[0].name)}", expected "${String(clone.name)}"`);
|
|
102
100
|
}
|
|
101
|
+
else if (!samePlanContent(clone, copies[0])) {
|
|
102
|
+
problems.push(`new plan "${String(clone.name)}" differs from the copy that was sent`);
|
|
103
|
+
}
|
|
103
104
|
if (after.length !== before.length + 1) {
|
|
104
105
|
problems.push(`plan count is ${after.length}, expected ${before.length + 1}`);
|
|
105
106
|
}
|
package/dist/clonePlan.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"clonePlan.js","sourceRoot":"","sources":["../src/clonePlan.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,
|
|
1
|
+
{"version":3,"file":"clonePlan.js","sourceRoot":"","sources":["../src/clonePlan.ts"],"names":[],"mappings":"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"}
|
package/dist/config.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/** Per-user config file shared by every MCP host on the machine. */
|
|
2
2
|
export declare const USER_ENV_FILE: string;
|
|
3
|
+
declare const LOG_LEVELS: readonly ["error", "warn", "info", "debug"];
|
|
4
|
+
export type LogLevel = (typeof LOG_LEVELS)[number];
|
|
3
5
|
export declare const config: {
|
|
4
6
|
readonly apiKey: string | undefined;
|
|
5
7
|
readonly wsPort: number;
|
|
@@ -9,7 +11,9 @@ export declare const config: {
|
|
|
9
11
|
readonly exportCacheMs: number;
|
|
10
12
|
readonly prettyJson: boolean;
|
|
11
13
|
readonly readOnly: boolean;
|
|
14
|
+
readonly updateCheck: boolean;
|
|
12
15
|
readonly snapshotDir: string;
|
|
13
16
|
readonly logLevel: "error" | "warn" | "info" | "debug";
|
|
14
17
|
};
|
|
15
18
|
export type Config = typeof config;
|
|
19
|
+
export {};
|
package/dist/config.js
CHANGED
|
@@ -23,6 +23,36 @@ function optionalInt(name, fallback) {
|
|
|
23
23
|
}
|
|
24
24
|
return value;
|
|
25
25
|
}
|
|
26
|
+
/** A listen port: all digits, 1 to 65535. parseInt alone would accept
|
|
27
|
+
* "8788abc" and bind whatever number it reads out of it. */
|
|
28
|
+
function portFromEnv(name, fallback) {
|
|
29
|
+
const raw = process.env[name];
|
|
30
|
+
if (!raw)
|
|
31
|
+
return fallback;
|
|
32
|
+
const value = /^\d+$/.test(raw.trim()) ? Number(raw.trim()) : NaN;
|
|
33
|
+
if (!Number.isInteger(value) || value < 1 || value > 65535) {
|
|
34
|
+
throw new Error(`Invalid value for ${name}: "${raw}" (expected a port number from 1 to 65535)`);
|
|
35
|
+
}
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
const LOG_LEVELS = ["error", "warn", "info", "debug"];
|
|
39
|
+
/** The configured log level; anything else falls back to info. */
|
|
40
|
+
function logLevel() {
|
|
41
|
+
const raw = process.env.LOG_LEVEL;
|
|
42
|
+
return LOG_LEVELS.includes(raw) ? raw : "info";
|
|
43
|
+
}
|
|
44
|
+
/** The app origin the browser executors come from; it must parse as a URL
|
|
45
|
+
* because the connection handler derives the allowed origin from it. */
|
|
46
|
+
function appUrl() {
|
|
47
|
+
const raw = optional("PL_APP_URL", "https://app.projectionlab.com");
|
|
48
|
+
try {
|
|
49
|
+
new URL(raw);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
throw new Error(`Invalid value for PL_APP_URL: "${raw}" (expected an absolute URL)`);
|
|
53
|
+
}
|
|
54
|
+
return raw;
|
|
55
|
+
}
|
|
26
56
|
function optionalBoolean(name) {
|
|
27
57
|
const value = process.env[name];
|
|
28
58
|
if (value === undefined)
|
|
@@ -41,11 +71,13 @@ export const config = {
|
|
|
41
71
|
// what is missing; operations that need the key reject without it.
|
|
42
72
|
apiKey: process.env.PL_API_KEY || undefined,
|
|
43
73
|
// Localhost WebSocket the browser extension (or bootstrap snippet) connects
|
|
44
|
-
// to. The token is a shared secret the executor must present; if unset
|
|
45
|
-
// ExtensionBridge generates one and persists it
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
74
|
+
// to. The token is a shared secret the executor must present; if unset or
|
|
75
|
+
// blank, ExtensionBridge generates one and persists it next to
|
|
76
|
+
// USER_ENV_FILE. A blank value never counts as a token: it would let any
|
|
77
|
+
// client authenticate with an empty string.
|
|
78
|
+
wsPort: portFromEnv("PL_WS_PORT", 8788),
|
|
79
|
+
wsToken: process.env.PL_WS_TOKEN?.trim() || undefined,
|
|
80
|
+
appUrl: appUrl(),
|
|
49
81
|
apiReadyTimeoutMs: optionalInt("PL_API_READY_TIMEOUT_MS", 30000),
|
|
50
82
|
// How long a full export may be served from cache. Writes always invalidate,
|
|
51
83
|
// so this can be generous for read-heavy analysis sessions.
|
|
@@ -54,7 +86,10 @@ export const config = {
|
|
|
54
86
|
prettyJson: optionalBoolean("PL_PRETTY_JSON") ?? false,
|
|
55
87
|
// Writes stay disabled unless PL_READ_ONLY is exactly "false".
|
|
56
88
|
readOnly: (process.env.PL_READ_ONLY ?? "").trim().toLowerCase() !== "false",
|
|
89
|
+
// On unless PL_UPDATE_CHECK is exactly "false"; the only registry request
|
|
90
|
+
// the server makes is the one this gates.
|
|
91
|
+
updateCheck: (process.env.PL_UPDATE_CHECK ?? "").trim().toLowerCase() !== "false",
|
|
57
92
|
snapshotDir: expandHome(optional("PL_SNAPSHOT_DIR", "~/.projectionlab-mcp/snapshots")),
|
|
58
|
-
logLevel:
|
|
93
|
+
logLevel: logLevel(),
|
|
59
94
|
};
|
|
60
95
|
//# sourceMappingURL=config.js.map
|
package/dist/config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACnD,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,SAAS,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAE1D,oEAAoE;AACpE,MAAM,CAAC,MAAM,aAAa,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,oBAAoB,EAAE,MAAM,CAAC,CAAC;AAE3E,yEAAyE;AACzE,oEAAoE;AACpE,yEAAyE;AACzE,UAAU,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,CAAC,CAAC;AACpC,UAAU,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,SAAS,EAAE,SAAS,CAAC,EAAE,CAAC,CAAC;AAEpD,SAAS,QAAQ,CAAC,IAAY,EAAE,QAAgB;IAC9C,OAAO,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,QAAQ,CAAC;AACvC,CAAC;AAED,SAAS,WAAW,CAAC,IAAY,EAAE,QAAgB;IACjD,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC9B,IAAI,CAAC,GAAG;QAAE,OAAO,QAAQ,CAAC;IAC1B,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;IAChC,IAAI,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;QACrC,MAAM,IAAI,KAAK,CAAC,qBAAqB,IAAI,MAAM,GAAG,qCAAqC,CAAC,CAAC;IAC3F,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,eAAe,CAAC,IAAY;IACnC,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAChC,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAC1C,OAAO,KAAK,CAAC,WAAW,EAAE,KAAK,MAAM,CAAC;AACxC,CAAC;AAED,SAAS,UAAU,CAAC,CAAS;IAC3B,IAAI,CAAC,KAAK,GAAG;QAAE,OAAO,OAAO,EAAE,CAAC;IAChC,IAAI,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAC9D,OAAO,OAAO,CAAC,CAAC,CAAC,CAAC;AACpB,CAAC;AAED,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB,2EAA2E;IAC3E,mEAAmE;IACnE,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,SAAS;IAC3C,4EAA4E;IAC5E,
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ProjectionLabError } from "./errors.js";
|
|
2
|
-
import type
|
|
2
|
+
import { type PlanObj } from "./plans.js";
|
|
3
3
|
export declare const CURRENT_FINANCE_COLLECTIONS: readonly ["investmentAccounts", "savingsAccounts", "debts", "assets"];
|
|
4
4
|
export interface CurrentFinanceEntry {
|
|
5
5
|
collection: (typeof CURRENT_FINANCE_COLLECTIONS)[number];
|
|
@@ -10,3 +10,14 @@ export declare function listCurrentFinanceEntries(today: unknown): CurrentFinanc
|
|
|
10
10
|
export declare function findCurrentFinanceEntry(today: unknown, accountId: string): CurrentFinanceEntry | undefined;
|
|
11
11
|
export declare function requireCurrentFinances(today: unknown): PlanObj;
|
|
12
12
|
export declare function currentFinanceAccountNotFoundError(today: unknown, accountId: string): ProjectionLabError;
|
|
13
|
+
/**
|
|
14
|
+
* Problems for a Current Finances read taken before a write and one taken after
|
|
15
|
+
* it: an id-bearing account the second read dropped or added, and a money field
|
|
16
|
+
* that no longer holds the value the first read saw. Accounts are matched by
|
|
17
|
+
* collection and id, so a reordered list, an account without an id, and a field
|
|
18
|
+
* the first read did not carry never read as a change. A first read that is not
|
|
19
|
+
* an object, or that holds no id-bearing account, has nothing to compare; an
|
|
20
|
+
* id-bearing baseline that the second read does not return as an object is
|
|
21
|
+
* itself the problem.
|
|
22
|
+
*/
|
|
23
|
+
export declare function currentFinancesProblems(before: unknown, after: unknown): string[];
|
package/dist/currentFinances.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ProjectionLabError } from "./errors.js";
|
|
2
|
+
import { containsSubset } from "./plans.js";
|
|
2
3
|
export const CURRENT_FINANCE_COLLECTIONS = [
|
|
3
4
|
"investmentAccounts",
|
|
4
5
|
"savingsAccounts",
|
|
@@ -40,4 +41,63 @@ export function currentFinanceAccountNotFoundError(today, accountId) {
|
|
|
40
41
|
const count = listCurrentFinanceEntries(today).length;
|
|
41
42
|
return new ProjectionLabError("INVALID_INPUT", `No Current Finances account with ID "${accountId}" found`, `Call projectionlab_get_current_finances to inspect the ${count} available account record(s)`);
|
|
42
43
|
}
|
|
44
|
+
/** The money fields of a Current Finances account. */
|
|
45
|
+
const BALANCE_FIELDS = ["balance", "amount", "value", "costBasis"];
|
|
46
|
+
/** The id-bearing accounts of one read, keyed by collection and id. */
|
|
47
|
+
function accountsById(today) {
|
|
48
|
+
const accounts = new Map();
|
|
49
|
+
for (const entry of listCurrentFinanceEntries(today)) {
|
|
50
|
+
const id = String(entry.account.id ?? "");
|
|
51
|
+
if (id)
|
|
52
|
+
accounts.set(`${entry.collection} ${id}`, entry.account);
|
|
53
|
+
}
|
|
54
|
+
return accounts;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Problems for a Current Finances read taken before a write and one taken after
|
|
58
|
+
* it: an id-bearing account the second read dropped or added, and a money field
|
|
59
|
+
* that no longer holds the value the first read saw. Accounts are matched by
|
|
60
|
+
* collection and id, so a reordered list, an account without an id, and a field
|
|
61
|
+
* the first read did not carry never read as a change. A first read that is not
|
|
62
|
+
* an object, or that holds no id-bearing account, has nothing to compare; an
|
|
63
|
+
* id-bearing baseline that the second read does not return as an object is
|
|
64
|
+
* itself the problem.
|
|
65
|
+
*/
|
|
66
|
+
export function currentFinancesProblems(before, after) {
|
|
67
|
+
if (!before || typeof before !== "object" || Array.isArray(before))
|
|
68
|
+
return [];
|
|
69
|
+
const beforeAccounts = accountsById(before);
|
|
70
|
+
if (beforeAccounts.size === 0)
|
|
71
|
+
return [];
|
|
72
|
+
if (!after || typeof after !== "object" || Array.isArray(after)) {
|
|
73
|
+
return ["Current Finances is missing from the re-read"];
|
|
74
|
+
}
|
|
75
|
+
const afterAccounts = accountsById(after);
|
|
76
|
+
const problems = [];
|
|
77
|
+
for (const [label, account] of beforeAccounts) {
|
|
78
|
+
const current = afterAccounts.get(label);
|
|
79
|
+
if (!current) {
|
|
80
|
+
problems.push(`Current Finances ${label} is missing`);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
for (const field of BALANCE_FIELDS) {
|
|
84
|
+
const value = account[field];
|
|
85
|
+
if (value === undefined || value === null)
|
|
86
|
+
continue;
|
|
87
|
+
const now = current[field];
|
|
88
|
+
if (now === undefined || now === null) {
|
|
89
|
+
problems.push(`Current Finances ${label} lost its ${field}`);
|
|
90
|
+
}
|
|
91
|
+
else if (!containsSubset(value, now)) {
|
|
92
|
+
problems.push(`Current Finances ${label} ${field} changed from ${JSON.stringify(value)} to ${JSON.stringify(now)}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
for (const label of afterAccounts.keys()) {
|
|
97
|
+
if (!beforeAccounts.has(label)) {
|
|
98
|
+
problems.push(`Current Finances ${label} was added`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return problems;
|
|
102
|
+
}
|
|
43
103
|
//# sourceMappingURL=currentFinances.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"currentFinances.js","sourceRoot":"","sources":["../src/currentFinances.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"currentFinances.js","sourceRoot":"","sources":["../src/currentFinances.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { ProjectionLabError } from "./errors.js";
|
|
2
|
+
/**
|
|
3
|
+
* Verified field knowledge for the values this server reads and writes.
|
|
4
|
+
* Single source for the `projectionlab://data-model` resource, the
|
|
5
|
+
* `listPaths` annotations, and the enum refusals in the shared write path.
|
|
6
|
+
*
|
|
7
|
+
* Entries carry evidence:
|
|
8
|
+
* - "observed": a value was set through the UI and the stored value was
|
|
9
|
+
* read back (or matched field-for-field against a known plan).
|
|
10
|
+
* - "ui-label": the control was seen in the UI but not confirmed by a
|
|
11
|
+
* stored-value test.
|
|
12
|
+
* Only fields confirmed against ProjectionLab 4.6.0 are listed; fields known
|
|
13
|
+
* from names alone are left out entirely.
|
|
14
|
+
*/
|
|
15
|
+
export declare const DATA_MODEL_PROJECTIONLAB_VERSION = "4.6.0";
|
|
16
|
+
export type DataModelEvidence = "observed" | "ui-label";
|
|
17
|
+
export interface DataModelField {
|
|
18
|
+
description: string;
|
|
19
|
+
/** Scale for numeric fields, e.g. "percentage points" or "decimal fraction". */
|
|
20
|
+
units?: string;
|
|
21
|
+
/** Complete set of values an enum field accepts. */
|
|
22
|
+
allowedValues?: readonly string[];
|
|
23
|
+
/** Which other field must be set for this field to matter. */
|
|
24
|
+
enabledBy?: string;
|
|
25
|
+
note?: string;
|
|
26
|
+
evidence: DataModelEvidence;
|
|
27
|
+
}
|
|
28
|
+
export interface DataModelGroup {
|
|
29
|
+
/** Prefix the paths are listed and written under: a settings section,
|
|
30
|
+
* an event section, or a Current Finances collection. */
|
|
31
|
+
scope: string;
|
|
32
|
+
title: string;
|
|
33
|
+
fields: Record<string, DataModelField>;
|
|
34
|
+
}
|
|
35
|
+
export declare const DATA_MODEL_GROUPS: readonly DataModelGroup[];
|
|
36
|
+
/** Field entry for a scope-prefixed concrete path, or undefined. */
|
|
37
|
+
export declare function matchDataModelField(scope: string, path: string): DataModelField | undefined;
|
|
38
|
+
export interface DataModelAnnotation {
|
|
39
|
+
description?: string;
|
|
40
|
+
units?: string;
|
|
41
|
+
allowedValues?: readonly string[];
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Copy listed values, attaching description, units, and allowedValues for
|
|
45
|
+
* paths with a registry entry. Entries for unknown paths are returned
|
|
46
|
+
* unchanged, so existing listPaths fields never change.
|
|
47
|
+
*/
|
|
48
|
+
export declare function annotateAddressableValues<T extends {
|
|
49
|
+
path: string;
|
|
50
|
+
}>(scope: string | undefined, values: readonly T[]): (T & DataModelAnnotation)[];
|
|
51
|
+
/**
|
|
52
|
+
* Choices to refuse a value over: an observed enum's list when value is a
|
|
53
|
+
* string outside it. undefined when no refusal applies. Numeric and boolean
|
|
54
|
+
* values are never refused; ui-label enums are annotated but not enforced.
|
|
55
|
+
*/
|
|
56
|
+
export declare function refusedEnumChoices(scope: string | undefined, path: string, value: unknown): readonly string[] | undefined;
|
|
57
|
+
/** Refusal for a value a write sets outside an observed enum's choices. */
|
|
58
|
+
export declare function enumRefusalError(scope: string, path: string, value: unknown): ProjectionLabError | undefined;
|