@nestpilot/mcp-app 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.
Files changed (149) hide show
  1. package/README.md +350 -0
  2. package/dist/cli/doctor.d.ts +1 -0
  3. package/dist/cli/doctor.js +214 -0
  4. package/dist/cli/export-import.d.ts +6 -0
  5. package/dist/cli/export-import.js +132 -0
  6. package/dist/cli/index.d.ts +2 -0
  7. package/dist/cli/index.js +168 -0
  8. package/dist/cli/init.d.ts +1 -0
  9. package/dist/cli/init.js +171 -0
  10. package/dist/host-configs/cowork.json +11 -0
  11. package/dist/host-configs/goose.yaml +22 -0
  12. package/dist/host-configs/openclaw-manifest.json +16 -0
  13. package/dist/main.d.ts +2 -0
  14. package/dist/main.js +128 -0
  15. package/dist/mcp-app.html +155 -0
  16. package/dist/nestpilot-client.d.ts +44 -0
  17. package/dist/nestpilot-client.js +160 -0
  18. package/dist/planner.html +222 -0
  19. package/dist/server.d.ts +19 -0
  20. package/dist/server.js +245 -0
  21. package/dist/skills/SKILL.md +162 -0
  22. package/dist/skills/manifest.json +51 -0
  23. package/dist/skills/tools/activate_plan.md +36 -0
  24. package/dist/skills/tools/coach.md +59 -0
  25. package/dist/skills/tools/comprehensive_plan.md +65 -0
  26. package/dist/skills/tools/create_plan.md +59 -0
  27. package/dist/skills/tools/create_saved_plan.md +49 -0
  28. package/dist/skills/tools/delete_plan.md +42 -0
  29. package/dist/skills/tools/delete_scenario.md +38 -0
  30. package/dist/skills/tools/generate_proposal.md +63 -0
  31. package/dist/skills/tools/generate_retirement_report.md +50 -0
  32. package/dist/skills/tools/get_active_plan.md +44 -0
  33. package/dist/skills/tools/get_baseline_forecast.md +47 -0
  34. package/dist/skills/tools/get_plan.md +44 -0
  35. package/dist/skills/tools/get_plan_components.md +50 -0
  36. package/dist/skills/tools/get_scenario.md +46 -0
  37. package/dist/skills/tools/list_plans.md +44 -0
  38. package/dist/skills/tools/list_scenarios.md +42 -0
  39. package/dist/skills/tools/medicare-guardian.md +59 -0
  40. package/dist/skills/tools/nestpilot_run_plan.md +61 -0
  41. package/dist/skills/tools/optimize_roth_conversion.md +107 -0
  42. package/dist/skills/tools/optimize_ss_claiming.md +30 -0
  43. package/dist/skills/tools/rename_plan.md +34 -0
  44. package/dist/skills/tools/retirement-planner.md +55 -0
  45. package/dist/skills/tools/run_forecast.md +65 -0
  46. package/dist/skills/tools/run_saved_forecast.md +52 -0
  47. package/dist/skills/tools/run_scenario.md +66 -0
  48. package/dist/skills/tools/save_plan.md +48 -0
  49. package/dist/skills/tools/save_scenario.md +50 -0
  50. package/dist/skills/tools/verify_forecast.md +43 -0
  51. package/dist/src/config.d.ts +20 -0
  52. package/dist/src/config.js +44 -0
  53. package/dist/src/contracts/provenance.d.ts +37 -0
  54. package/dist/src/contracts/provenance.js +71 -0
  55. package/dist/src/contracts/tool-contract-registry.d.ts +43 -0
  56. package/dist/src/contracts/tool-contract-registry.js +282 -0
  57. package/dist/src/local/cloud-compute-client.d.ts +55 -0
  58. package/dist/src/local/cloud-compute-client.js +135 -0
  59. package/dist/src/local/encryption.d.ts +24 -0
  60. package/dist/src/local/encryption.js +105 -0
  61. package/dist/src/local/keychain.d.ts +41 -0
  62. package/dist/src/local/keychain.js +236 -0
  63. package/dist/src/local/local-config.d.ts +34 -0
  64. package/dist/src/local/local-config.js +61 -0
  65. package/dist/src/local/local-data-layer.d.ts +20 -0
  66. package/dist/src/local/local-data-layer.js +15 -0
  67. package/dist/src/local/local-plan-store.d.ts +66 -0
  68. package/dist/src/local/local-plan-store.js +195 -0
  69. package/dist/src/local/pii-scrubber.d.ts +26 -0
  70. package/dist/src/local/pii-scrubber.js +219 -0
  71. package/dist/src/policy/policy-engine.d.ts +44 -0
  72. package/dist/src/policy/policy-engine.js +119 -0
  73. package/dist/src/rate-limit.d.ts +17 -0
  74. package/dist/src/rate-limit.js +41 -0
  75. package/dist/src/security.d.ts +19 -0
  76. package/dist/src/security.js +118 -0
  77. package/dist/src/skills/index.d.ts +12 -0
  78. package/dist/src/skills/index.js +16 -0
  79. package/dist/src/skills/retirement-pack-v1.d.ts +28 -0
  80. package/dist/src/skills/retirement-pack-v1.js +295 -0
  81. package/dist/src/skills/skill-executor.d.ts +65 -0
  82. package/dist/src/skills/skill-executor.js +174 -0
  83. package/dist/src/skills/skill-manifest-schema.d.ts +337 -0
  84. package/dist/src/skills/skill-manifest-schema.js +94 -0
  85. package/dist/src/skills/skill-registry.d.ts +71 -0
  86. package/dist/src/skills/skill-registry.js +116 -0
  87. package/dist/src/telemetry.d.ts +12 -0
  88. package/dist/src/telemetry.js +59 -0
  89. package/dist/src/types.d.ts +46 -0
  90. package/dist/src/types.js +4 -0
  91. package/dist/tools/agent-tools.d.ts +12 -0
  92. package/dist/tools/agent-tools.js +141 -0
  93. package/dist/tools/forecast-management-tools.d.ts +9 -0
  94. package/dist/tools/forecast-management-tools.js +133 -0
  95. package/dist/tools/local-plan-tools.d.ts +8 -0
  96. package/dist/tools/local-plan-tools.js +357 -0
  97. package/dist/tools/mcp-helpers.d.ts +52 -0
  98. package/dist/tools/mcp-helpers.js +177 -0
  99. package/dist/tools/medicare-tools.d.ts +3 -0
  100. package/dist/tools/medicare-tools.js +162 -0
  101. package/dist/tools/optimize-roth-tools-test.d.ts +2 -0
  102. package/dist/tools/optimize-roth-tools-test.js +36 -0
  103. package/dist/tools/optimize-roth-tools.d.ts +3 -0
  104. package/dist/tools/optimize-roth-tools.js +818 -0
  105. package/dist/tools/plan-management-tools.d.ts +3 -0
  106. package/dist/tools/plan-management-tools.js +196 -0
  107. package/dist/tools/planning-tools.d.ts +3 -0
  108. package/dist/tools/planning-tools.js +290 -0
  109. package/dist/tools/proposal-tools.d.ts +3 -0
  110. package/dist/tools/proposal-tools.js +428 -0
  111. package/dist/tools/report-tools.d.ts +3 -0
  112. package/dist/tools/report-tools.js +245 -0
  113. package/dist/tools/scenario-management-tools.d.ts +3 -0
  114. package/dist/tools/scenario-management-tools.js +136 -0
  115. package/dist/views/verification-packet.html +211 -0
  116. package/host-configs/cowork.json +11 -0
  117. package/host-configs/goose.yaml +22 -0
  118. package/host-configs/openclaw-manifest.json +16 -0
  119. package/package.json +66 -0
  120. package/skills/SKILL.md +162 -0
  121. package/skills/manifest.json +51 -0
  122. package/skills/tools/activate_plan.md +36 -0
  123. package/skills/tools/coach.md +59 -0
  124. package/skills/tools/comprehensive_plan.md +65 -0
  125. package/skills/tools/create_plan.md +59 -0
  126. package/skills/tools/create_saved_plan.md +49 -0
  127. package/skills/tools/delete_plan.md +42 -0
  128. package/skills/tools/delete_scenario.md +38 -0
  129. package/skills/tools/generate_proposal.md +63 -0
  130. package/skills/tools/generate_retirement_report.md +50 -0
  131. package/skills/tools/get_active_plan.md +44 -0
  132. package/skills/tools/get_baseline_forecast.md +47 -0
  133. package/skills/tools/get_plan.md +44 -0
  134. package/skills/tools/get_plan_components.md +50 -0
  135. package/skills/tools/get_scenario.md +46 -0
  136. package/skills/tools/list_plans.md +44 -0
  137. package/skills/tools/list_scenarios.md +42 -0
  138. package/skills/tools/medicare-guardian.md +59 -0
  139. package/skills/tools/nestpilot_run_plan.md +61 -0
  140. package/skills/tools/optimize_roth_conversion.md +107 -0
  141. package/skills/tools/optimize_ss_claiming.md +30 -0
  142. package/skills/tools/rename_plan.md +34 -0
  143. package/skills/tools/retirement-planner.md +55 -0
  144. package/skills/tools/run_forecast.md +65 -0
  145. package/skills/tools/run_saved_forecast.md +52 -0
  146. package/skills/tools/run_scenario.md +66 -0
  147. package/skills/tools/save_plan.md +48 -0
  148. package/skills/tools/save_scenario.md +50 -0
  149. package/skills/tools/verify_forecast.md +43 -0
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Cloud Compute Client — sends PII-free mathematical payloads to the
3
+ * NestPilot cloud API for compute-intensive operations (forecast, scenario,
4
+ * Roth optimization, coaching).
5
+ *
6
+ * All requests include:
7
+ * - `Authorization: Bearer {apiKey}` for metering
8
+ * - `X-Client-Mode: local` to distinguish local-first traffic
9
+ * - 30 second timeout
10
+ *
11
+ * On network failure, the client returns cached results when available.
12
+ *
13
+ * @feature FEAT-0087
14
+ */
15
+ // ── Configuration ────────────────────────────────────────────────────────
16
+ const DEFAULT_TIMEOUT_MS = 30_000;
17
+ // ── CloudComputeClient ───────────────────────────────────────────────────
18
+ export class CloudComputeClient {
19
+ cloudApiUrl;
20
+ apiKey;
21
+ constructor(cloudApiUrl, apiKey) {
22
+ this.cloudApiUrl = cloudApiUrl;
23
+ this.apiKey = apiKey;
24
+ // Strip trailing slash
25
+ if (this.cloudApiUrl.endsWith("/")) {
26
+ this.cloudApiUrl = this.cloudApiUrl.slice(0, -1);
27
+ }
28
+ }
29
+ /**
30
+ * Run a retirement forecast against the cloud engine.
31
+ */
32
+ async forecast(payload) {
33
+ return this.post("/api/plan/forecast", payload);
34
+ }
35
+ /**
36
+ * Run a what-if scenario comparison against the cloud engine.
37
+ */
38
+ async scenario(payload) {
39
+ return this.post("/api/plan/scenario/run", payload);
40
+ }
41
+ /**
42
+ * Run Roth conversion optimization against the cloud engine.
43
+ */
44
+ async rothOptimize(payload) {
45
+ return this.post("/api/plan/roth/optimize", payload);
46
+ }
47
+ /**
48
+ * Get AI coaching guidance from the cloud engine.
49
+ */
50
+ async coach(payload) {
51
+ return this.post("/api/spring_ai/coach", payload);
52
+ }
53
+ /**
54
+ * Run forecast verification against the cloud engine.
55
+ */
56
+ async verify(payload) {
57
+ return this.post("/api/plan/forecast/verify", payload);
58
+ }
59
+ /**
60
+ * Quick-calc for initial plan creation.
61
+ */
62
+ async quickCalc(payload) {
63
+ return this.post("/api/plan/calc", payload);
64
+ }
65
+ /**
66
+ * Health check — verifies the cloud API is reachable.
67
+ */
68
+ async healthCheck() {
69
+ try {
70
+ const response = await fetch(`${this.cloudApiUrl}/actuator/health`, {
71
+ method: "GET",
72
+ headers: this.buildHeaders(),
73
+ signal: AbortSignal.timeout(5_000),
74
+ });
75
+ return response.ok;
76
+ }
77
+ catch {
78
+ return false;
79
+ }
80
+ }
81
+ // ── Internal ──────────────────────────────────────────────────────────
82
+ async post(endpoint, payload) {
83
+ const url = `${this.cloudApiUrl}${endpoint}`;
84
+ try {
85
+ const response = await fetch(url, {
86
+ method: "POST",
87
+ headers: this.buildHeaders(),
88
+ body: JSON.stringify(payload),
89
+ signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS),
90
+ });
91
+ if (!response.ok) {
92
+ const text = await response.text();
93
+ // Rate limiting
94
+ if (response.status === 429) {
95
+ const retryAfter = response.headers.get("Retry-After");
96
+ return {
97
+ error: true,
98
+ message: `Cloud API rate limit exceeded. ${retryAfter
99
+ ? `Retry after ${retryAfter} seconds.`
100
+ : "Please wait and try again."} You may be on the free tier (10 forecasts/month). Upgrade at https://nestpilot.com/pricing.`,
101
+ };
102
+ }
103
+ return {
104
+ error: true,
105
+ message: `Cloud API error (${response.status}): ${text}`,
106
+ };
107
+ }
108
+ const data = (await response.json());
109
+ return { data };
110
+ }
111
+ catch (e) {
112
+ const errorMessage = e instanceof Error ? e.message : String(e);
113
+ // Distinguish timeout from network errors
114
+ if (errorMessage.includes("abort") || errorMessage.includes("timeout")) {
115
+ return {
116
+ error: true,
117
+ message: "Cloud API request timed out after 30 seconds. The compute engine may be under heavy load. Try again shortly.",
118
+ };
119
+ }
120
+ return {
121
+ error: true,
122
+ message: `Cannot reach NestPilot cloud API at ${this.cloudApiUrl}. Check your internet connection. Error: ${errorMessage}`,
123
+ };
124
+ }
125
+ }
126
+ buildHeaders() {
127
+ return {
128
+ "Content-Type": "application/json",
129
+ Accept: "application/json",
130
+ Authorization: `Bearer ${this.apiKey}`,
131
+ "X-Client-Mode": "local",
132
+ "X-Client-Version": "1.0.0",
133
+ };
134
+ }
135
+ }
@@ -0,0 +1,24 @@
1
+ import type { KeychainProvider } from "./keychain.js";
2
+ export declare class EncryptionService {
3
+ private keyProvider;
4
+ private cachedKey;
5
+ constructor(keyProvider: KeychainProvider);
6
+ /**
7
+ * Encrypts a UTF-8 string into an NPE envelope buffer.
8
+ */
9
+ encrypt(data: string): Promise<Buffer>;
10
+ /**
11
+ * Decrypts an NPE envelope buffer back into a UTF-8 string.
12
+ */
13
+ decrypt(envelope: Buffer): Promise<string>;
14
+ /**
15
+ * Generates a fresh 256-bit encryption key and stores it in the keychain.
16
+ * Returns the hex-encoded key.
17
+ */
18
+ generateAndStoreKey(): Promise<string>;
19
+ /**
20
+ * Checks whether an encryption key exists in the keychain.
21
+ */
22
+ hasKey(): Promise<boolean>;
23
+ private getKey;
24
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * AES-256-GCM encryption for NestPilot data at rest.
3
+ *
4
+ * All plan data stored under ~/.nestpilot/ is encrypted using a key
5
+ * fetched from the OS keychain. The envelope format is:
6
+ *
7
+ * [3-byte magic "NPE"] [1-byte version] [12-byte IV] [ciphertext] [16-byte auth tag]
8
+ *
9
+ * @feature FEAT-0087
10
+ */
11
+ import { createCipheriv, createDecipheriv, randomBytes, } from "node:crypto";
12
+ // ── Constants ────────────────────────────────────────────────────────────
13
+ const ALGORITHM = "aes-256-gcm";
14
+ const IV_LENGTH = 12; // 96 bits — GCM recommended
15
+ const AUTH_TAG_LENGTH = 16;
16
+ const MAGIC = Buffer.from("NPE"); // NestPilot Encrypted
17
+ const ENVELOPE_VERSION = 1;
18
+ const HEADER_LENGTH = MAGIC.length + 1 + IV_LENGTH; // 3 + 1 + 12 = 16
19
+ const KEYCHAIN_SERVICE = "nestpilot";
20
+ const KEYCHAIN_ENCRYPTION_ACCOUNT = "encryption-key";
21
+ // ── EncryptionService ────────────────────────────────────────────────────
22
+ export class EncryptionService {
23
+ keyProvider;
24
+ cachedKey = null;
25
+ constructor(keyProvider) {
26
+ this.keyProvider = keyProvider;
27
+ }
28
+ /**
29
+ * Encrypts a UTF-8 string into an NPE envelope buffer.
30
+ */
31
+ async encrypt(data) {
32
+ const key = await this.getKey();
33
+ const iv = randomBytes(IV_LENGTH);
34
+ const cipher = createCipheriv(ALGORITHM, key, iv, {
35
+ authTagLength: AUTH_TAG_LENGTH,
36
+ });
37
+ const encrypted = Buffer.concat([
38
+ cipher.update(data, "utf-8"),
39
+ cipher.final(),
40
+ ]);
41
+ const authTag = cipher.getAuthTag();
42
+ // Envelope: MAGIC + VERSION + IV + CIPHERTEXT + AUTH_TAG
43
+ return Buffer.concat([
44
+ MAGIC,
45
+ Buffer.from([ENVELOPE_VERSION]),
46
+ iv,
47
+ encrypted,
48
+ authTag,
49
+ ]);
50
+ }
51
+ /**
52
+ * Decrypts an NPE envelope buffer back into a UTF-8 string.
53
+ */
54
+ async decrypt(envelope) {
55
+ // Validate magic
56
+ if (envelope.length < HEADER_LENGTH + AUTH_TAG_LENGTH ||
57
+ !envelope.subarray(0, MAGIC.length).equals(MAGIC)) {
58
+ throw new Error("Invalid encrypted envelope: bad magic header");
59
+ }
60
+ const version = envelope[MAGIC.length];
61
+ if (version !== ENVELOPE_VERSION) {
62
+ throw new Error(`Unsupported encryption version: ${version}`);
63
+ }
64
+ const iv = envelope.subarray(MAGIC.length + 1, HEADER_LENGTH);
65
+ const authTag = envelope.subarray(envelope.length - AUTH_TAG_LENGTH);
66
+ const ciphertext = envelope.subarray(HEADER_LENGTH, envelope.length - AUTH_TAG_LENGTH);
67
+ const key = await this.getKey();
68
+ const decipher = createDecipheriv(ALGORITHM, key, iv, {
69
+ authTagLength: AUTH_TAG_LENGTH,
70
+ });
71
+ decipher.setAuthTag(authTag);
72
+ return Buffer.concat([
73
+ decipher.update(ciphertext),
74
+ decipher.final(),
75
+ ]).toString("utf-8");
76
+ }
77
+ /**
78
+ * Generates a fresh 256-bit encryption key and stores it in the keychain.
79
+ * Returns the hex-encoded key.
80
+ */
81
+ async generateAndStoreKey() {
82
+ const key = randomBytes(32).toString("hex");
83
+ await this.keyProvider.set(KEYCHAIN_SERVICE, KEYCHAIN_ENCRYPTION_ACCOUNT, key);
84
+ this.cachedKey = Buffer.from(key, "hex");
85
+ return key;
86
+ }
87
+ /**
88
+ * Checks whether an encryption key exists in the keychain.
89
+ */
90
+ async hasKey() {
91
+ const raw = await this.keyProvider.get(KEYCHAIN_SERVICE, KEYCHAIN_ENCRYPTION_ACCOUNT);
92
+ return raw !== null;
93
+ }
94
+ // ── Internal ──────────────────────────────────────────────────────────
95
+ async getKey() {
96
+ if (this.cachedKey)
97
+ return this.cachedKey;
98
+ const raw = await this.keyProvider.get(KEYCHAIN_SERVICE, KEYCHAIN_ENCRYPTION_ACCOUNT);
99
+ if (!raw) {
100
+ throw new Error("Encryption key not found in keychain. Run `nestpilot init` to set up encryption.");
101
+ }
102
+ this.cachedKey = Buffer.from(raw, "hex");
103
+ return this.cachedKey;
104
+ }
105
+ }
@@ -0,0 +1,41 @@
1
+ export interface KeychainProvider {
2
+ get(service: string, account: string): Promise<string | null>;
3
+ set(service: string, account: string, value: string): Promise<void>;
4
+ delete(service: string, account: string): Promise<void>;
5
+ }
6
+ declare class MacOSKeychain implements KeychainProvider {
7
+ get(service: string, account: string): Promise<string | null>;
8
+ set(service: string, account: string, value: string): Promise<void>;
9
+ delete(service: string, account: string): Promise<void>;
10
+ }
11
+ declare class WindowsKeychain implements KeychainProvider {
12
+ private target;
13
+ get(service: string, account: string): Promise<string | null>;
14
+ set(service: string, account: string, value: string): Promise<void>;
15
+ delete(service: string, account: string): Promise<void>;
16
+ }
17
+ declare class LinuxKeychain implements KeychainProvider {
18
+ get(service: string, account: string): Promise<string | null>;
19
+ set(service: string, account: string, value: string): Promise<void>;
20
+ delete(service: string, account: string): Promise<void>;
21
+ }
22
+ /**
23
+ * Fallback keychain that stores credentials in a JSON file.
24
+ * Used when no OS keychain is available. The file itself should be
25
+ * in a protected directory with 0600 permissions.
26
+ */
27
+ declare class FileKeychain implements KeychainProvider {
28
+ private filePath;
29
+ constructor(dataDir?: string);
30
+ get(service: string, account: string): Promise<string | null>;
31
+ set(service: string, account: string, value: string): Promise<void>;
32
+ delete(service: string, account: string): Promise<void>;
33
+ private load;
34
+ private save;
35
+ }
36
+ /**
37
+ * Creates the appropriate KeychainProvider for the current platform.
38
+ * Falls back to file-based storage if no OS keychain is detected.
39
+ */
40
+ export declare function createKeychainProvider(dataDir?: string): KeychainProvider;
41
+ export { FileKeychain, LinuxKeychain, MacOSKeychain, WindowsKeychain };
@@ -0,0 +1,236 @@
1
+ /**
2
+ * OS-native keychain integration for NestPilot secrets.
3
+ *
4
+ * Provides a platform-agnostic interface for storing API keys and encryption
5
+ * keys in the operating system's secure credential store.
6
+ *
7
+ * Platform mapping:
8
+ * - macOS : Keychain (via `security` CLI)
9
+ * - Windows: Windows Credential Manager (via `cmdkey` / PowerShell)
10
+ * - Linux : libsecret / Secret Service API (GNOME Keyring / KWallet)
11
+ * - Fallback: Encrypted file at ~/.nestpilot/credentials.enc
12
+ *
13
+ * @feature FEAT-0087
14
+ */
15
+ import { execFile } from "node:child_process";
16
+ import fs from "node:fs/promises";
17
+ import os from "node:os";
18
+ import path from "node:path";
19
+ import { promisify } from "node:util";
20
+ const execFileAsync = promisify(execFile);
21
+ // ── macOS Keychain ───────────────────────────────────────────────────────
22
+ class MacOSKeychain {
23
+ async get(service, account) {
24
+ try {
25
+ const { stdout } = await execFileAsync("security", [
26
+ "find-generic-password",
27
+ "-s",
28
+ service,
29
+ "-a",
30
+ account,
31
+ "-w",
32
+ ]);
33
+ return stdout.trim() || null;
34
+ }
35
+ catch {
36
+ return null;
37
+ }
38
+ }
39
+ async set(service, account, value) {
40
+ // Delete first to allow update
41
+ try {
42
+ await execFileAsync("security", [
43
+ "delete-generic-password",
44
+ "-s",
45
+ service,
46
+ "-a",
47
+ account,
48
+ ]);
49
+ }
50
+ catch {
51
+ // Ignore if not found
52
+ }
53
+ await execFileAsync("security", [
54
+ "add-generic-password",
55
+ "-s",
56
+ service,
57
+ "-a",
58
+ account,
59
+ "-w",
60
+ value,
61
+ ]);
62
+ }
63
+ async delete(service, account) {
64
+ try {
65
+ await execFileAsync("security", [
66
+ "delete-generic-password",
67
+ "-s",
68
+ service,
69
+ "-a",
70
+ account,
71
+ ]);
72
+ }
73
+ catch {
74
+ // Ignore if not found
75
+ }
76
+ }
77
+ }
78
+ // ── Windows Credential Manager ───────────────────────────────────────────
79
+ class WindowsKeychain {
80
+ target(service, account) {
81
+ return `${service}:${account}`;
82
+ }
83
+ async get(service, account) {
84
+ try {
85
+ const target = this.target(service, account);
86
+ const { stdout } = await execFileAsync("powershell.exe", [
87
+ "-NoProfile",
88
+ "-Command",
89
+ `$cred = Get-StoredCredential -Target '${target}' -ErrorAction SilentlyContinue; if ($cred) { $cred.GetNetworkCredential().Password } else { $null }`,
90
+ ]);
91
+ const result = stdout.trim();
92
+ return result && result !== "" ? result : null;
93
+ }
94
+ catch {
95
+ // Fallback: try cmdkey
96
+ try {
97
+ const target = this.target(service, account);
98
+ const { stdout } = await execFileAsync("powershell.exe", [
99
+ "-NoProfile",
100
+ "-Command",
101
+ `[System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR((New-Object System.Management.Automation.PSCredential('_', (cmdkey /generic:${target} /list 2>$null | Out-Null; [System.Security.SecureString]::new()))).Password))`,
102
+ ]);
103
+ return stdout.trim() || null;
104
+ }
105
+ catch {
106
+ return null;
107
+ }
108
+ }
109
+ }
110
+ async set(service, account, value) {
111
+ const target = this.target(service, account);
112
+ await execFileAsync("cmdkey", [
113
+ `/generic:${target}`,
114
+ `/user:${account}`,
115
+ `/pass:${value}`,
116
+ ]);
117
+ }
118
+ async delete(service, account) {
119
+ try {
120
+ const target = this.target(service, account);
121
+ await execFileAsync("cmdkey", [`/delete:${target}`]);
122
+ }
123
+ catch {
124
+ // Ignore if not found
125
+ }
126
+ }
127
+ }
128
+ // ── Linux Secret Service ─────────────────────────────────────────────────
129
+ class LinuxKeychain {
130
+ async get(service, account) {
131
+ try {
132
+ const { stdout } = await execFileAsync("secret-tool", [
133
+ "lookup",
134
+ "service",
135
+ service,
136
+ "account",
137
+ account,
138
+ ]);
139
+ return stdout.trim() || null;
140
+ }
141
+ catch {
142
+ return null;
143
+ }
144
+ }
145
+ async set(service, account, value) {
146
+ const child = execFileAsync("secret-tool", [
147
+ "store",
148
+ "--label",
149
+ `${service}:${account}`,
150
+ "service",
151
+ service,
152
+ "account",
153
+ account,
154
+ ]);
155
+ // secret-tool reads the secret from stdin
156
+ child.child.stdin?.write(value);
157
+ child.child.stdin?.end();
158
+ await child;
159
+ }
160
+ async delete(service, account) {
161
+ try {
162
+ await execFileAsync("secret-tool", [
163
+ "clear",
164
+ "service",
165
+ service,
166
+ "account",
167
+ account,
168
+ ]);
169
+ }
170
+ catch {
171
+ // Ignore if not found
172
+ }
173
+ }
174
+ }
175
+ // ── File-based fallback ──────────────────────────────────────────────────
176
+ /**
177
+ * Fallback keychain that stores credentials in a JSON file.
178
+ * Used when no OS keychain is available. The file itself should be
179
+ * in a protected directory with 0600 permissions.
180
+ */
181
+ class FileKeychain {
182
+ filePath;
183
+ constructor(dataDir) {
184
+ this.filePath = path.join(dataDir ?? path.join(os.homedir(), ".nestpilot"), "credentials.json");
185
+ }
186
+ async get(service, account) {
187
+ const data = await this.load();
188
+ return data[`${service}:${account}`] ?? null;
189
+ }
190
+ async set(service, account, value) {
191
+ const data = await this.load();
192
+ data[`${service}:${account}`] = value;
193
+ await this.save(data);
194
+ }
195
+ async delete(service, account) {
196
+ const data = await this.load();
197
+ delete data[`${service}:${account}`];
198
+ await this.save(data);
199
+ }
200
+ async load() {
201
+ try {
202
+ const raw = await fs.readFile(this.filePath, "utf-8");
203
+ return JSON.parse(raw);
204
+ }
205
+ catch {
206
+ return {};
207
+ }
208
+ }
209
+ async save(data) {
210
+ const dir = path.dirname(this.filePath);
211
+ await fs.mkdir(dir, { recursive: true });
212
+ await fs.writeFile(this.filePath, JSON.stringify(data, null, 2), {
213
+ mode: 0o600, // Owner read/write only
214
+ });
215
+ }
216
+ }
217
+ // ── Factory ──────────────────────────────────────────────────────────────
218
+ /**
219
+ * Creates the appropriate KeychainProvider for the current platform.
220
+ * Falls back to file-based storage if no OS keychain is detected.
221
+ */
222
+ export function createKeychainProvider(dataDir) {
223
+ const platform = os.platform();
224
+ switch (platform) {
225
+ case "darwin":
226
+ return new MacOSKeychain();
227
+ case "win32":
228
+ return new WindowsKeychain();
229
+ case "linux":
230
+ return new LinuxKeychain();
231
+ default:
232
+ console.warn(`[keychain] No native keychain support for ${platform}, using file-based fallback`);
233
+ return new FileKeychain(dataDir);
234
+ }
235
+ }
236
+ export { FileKeychain, LinuxKeychain, MacOSKeychain, WindowsKeychain };
@@ -0,0 +1,34 @@
1
+ export type NestpilotMode = "cloud" | "local";
2
+ export interface LocalConfig {
3
+ /** Runtime mode — "cloud" proxies to Spring Boot, "local" uses local data layer. */
4
+ mode: NestpilotMode;
5
+ /** Root directory for local data (default: ~/.nestpilot). */
6
+ dataDir: string;
7
+ /** Cloud API URL for PII-free compute calls. */
8
+ cloudApiUrl: string;
9
+ /** API key retrieved from keychain (populated lazily). */
10
+ apiKey?: string;
11
+ /** Whether data-at-rest encryption is active (always true in local mode). */
12
+ encryptionEnabled: boolean;
13
+ }
14
+ export declare const DATA_SUBDIRS: readonly ["plans", "forecasts", "scenarios", "logs"];
15
+ export declare function plansDir(dataDir: string): string;
16
+ export declare function forecastsDir(dataDir: string): string;
17
+ export declare function scenariosDir(dataDir: string): string;
18
+ export declare function logsDir(dataDir: string): string;
19
+ export declare function configFilePath(dataDir: string): string;
20
+ /**
21
+ * Loads the local configuration from environment variables.
22
+ *
23
+ * Environment variables:
24
+ * - `NESTPILOT_MODE` — "cloud" (default) or "local"
25
+ * - `NESTPILOT_DATA_DIR` — data root (default: ~/.nestpilot)
26
+ * - `NESTPILOT_CLOUD_API_URL` — cloud compute URL
27
+ * - `NESTPILOT_API_KEY` — API key override (normally loaded from keychain)
28
+ */
29
+ export declare function loadLocalConfig(env?: NodeJS.ProcessEnv): LocalConfig;
30
+ /**
31
+ * Returns `true` when the server is running in local-first mode.
32
+ * Convenience function used by the server bootstrap.
33
+ */
34
+ export declare function isLocalMode(env?: NodeJS.ProcessEnv): boolean;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Local-first mode detection and configuration.
3
+ *
4
+ * Reads NESTPILOT_MODE ("cloud" | "local") from the environment and
5
+ * builds the LocalConfig object that drives all local-first behavior.
6
+ *
7
+ * @feature FEAT-0087
8
+ */
9
+ import os from "node:os";
10
+ import path from "node:path";
11
+ // ── Well-known paths inside dataDir ──────────────────────────────────────
12
+ export const DATA_SUBDIRS = [
13
+ "plans",
14
+ "forecasts",
15
+ "scenarios",
16
+ "logs",
17
+ ];
18
+ export function plansDir(dataDir) {
19
+ return path.join(dataDir, "plans");
20
+ }
21
+ export function forecastsDir(dataDir) {
22
+ return path.join(dataDir, "forecasts");
23
+ }
24
+ export function scenariosDir(dataDir) {
25
+ return path.join(dataDir, "scenarios");
26
+ }
27
+ export function logsDir(dataDir) {
28
+ return path.join(dataDir, "logs");
29
+ }
30
+ export function configFilePath(dataDir) {
31
+ return path.join(dataDir, "config.json");
32
+ }
33
+ // ── Configuration loader ─────────────────────────────────────────────────
34
+ const DEFAULT_CLOUD_API_URL = "https://api.nestpilot.com";
35
+ /**
36
+ * Loads the local configuration from environment variables.
37
+ *
38
+ * Environment variables:
39
+ * - `NESTPILOT_MODE` — "cloud" (default) or "local"
40
+ * - `NESTPILOT_DATA_DIR` — data root (default: ~/.nestpilot)
41
+ * - `NESTPILOT_CLOUD_API_URL` — cloud compute URL
42
+ * - `NESTPILOT_API_KEY` — API key override (normally loaded from keychain)
43
+ */
44
+ export function loadLocalConfig(env = process.env) {
45
+ const mode = (env.NESTPILOT_MODE ?? "cloud");
46
+ const dataDir = env.NESTPILOT_DATA_DIR ?? path.join(os.homedir(), ".nestpilot");
47
+ return {
48
+ mode,
49
+ dataDir,
50
+ cloudApiUrl: env.NESTPILOT_CLOUD_API_URL ?? DEFAULT_CLOUD_API_URL,
51
+ apiKey: env.NESTPILOT_API_KEY,
52
+ encryptionEnabled: mode === "local",
53
+ };
54
+ }
55
+ /**
56
+ * Returns `true` when the server is running in local-first mode.
57
+ * Convenience function used by the server bootstrap.
58
+ */
59
+ export function isLocalMode(env = process.env) {
60
+ return (env.NESTPILOT_MODE ?? "cloud") === "local";
61
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Local Data Layer — barrel export for all local-first modules.
3
+ *
4
+ * Provides a single import point for the local runtime:
5
+ *
6
+ * import { LocalPlanStore, CloudComputeClient, ... } from "./local/local-data-layer.js";
7
+ *
8
+ * @feature FEAT-0087
9
+ */
10
+ export { loadLocalConfig, isLocalMode } from "./local-config.js";
11
+ export type { NestpilotMode, LocalConfig } from "./local-config.js";
12
+ export { EncryptionService } from "./encryption.js";
13
+ export { createKeychainProvider, FileKeychain, } from "./keychain.js";
14
+ export type { KeychainProvider } from "./keychain.js";
15
+ export { LocalPlanStore } from "./local-plan-store.js";
16
+ export type { LocalPlan, PlanSummary } from "./local-plan-store.js";
17
+ export { scrubPII, validateNoPII } from "./pii-scrubber.js";
18
+ export type { ScrubResult } from "./pii-scrubber.js";
19
+ export { CloudComputeClient } from "./cloud-compute-client.js";
20
+ export type { CloudComputeResult } from "./cloud-compute-client.js";
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Local Data Layer — barrel export for all local-first modules.
3
+ *
4
+ * Provides a single import point for the local runtime:
5
+ *
6
+ * import { LocalPlanStore, CloudComputeClient, ... } from "./local/local-data-layer.js";
7
+ *
8
+ * @feature FEAT-0087
9
+ */
10
+ export { loadLocalConfig, isLocalMode } from "./local-config.js";
11
+ export { EncryptionService } from "./encryption.js";
12
+ export { createKeychainProvider, FileKeychain, } from "./keychain.js";
13
+ export { LocalPlanStore } from "./local-plan-store.js";
14
+ export { scrubPII, validateNoPII } from "./pii-scrubber.js";
15
+ export { CloudComputeClient } from "./cloud-compute-client.js";