@tasksai/install 0.1.15 → 0.1.17

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/README.md CHANGED
@@ -31,9 +31,28 @@ The installer:
31
31
  - prints customer-facing recovery instructions when an AI assistant is blocked
32
32
  from writing local config files
33
33
  - supports Claude Desktop, Cursor, Windsurf, and Codex
34
- - runs a local health check
34
+ - runs local runtime, API, and authenticated license health checks
35
+ - reports only a content-free `doctor_passed` activation event after core checks
36
+ succeed; activation-reporting outages do not fail an otherwise healthy doctor
35
37
  - exposes a local save-document tool for finished skill outputs
36
38
 
39
+ For known production products, wrapper and remote runtime files are accepted
40
+ only from the registered production source. RealtorTasksAI, FarmerTasksAI,
41
+ LawTasksAI, and TeacherTasksAI are pinned to the `main` branch and product path
42
+ in `TasksAI-Official/tasksai-mcp-wrappers`. Local paths, alternate branches,
43
+ unregistered products, and other repositories are rejected by default.
44
+
45
+ Developers who intentionally need one of those sources must opt in explicitly:
46
+
47
+ ```bash
48
+ tasksai-install realtor \
49
+ --source /absolute/path/to/local/realtor-wrapper \
50
+ --allow-untrusted-development-source
51
+ ```
52
+
53
+ The override is development-only and emits a visible warning. It must not be
54
+ used for customer installs.
55
+
37
56
  TasksAI servers handle authentication, credits, catalog/search metadata, and
38
57
  licensed skill delivery. TasksAI does not process user task content.
39
58
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tasksai/install",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
4
4
  "description": "Shared TasksAI MCP installer CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -33,6 +33,6 @@
33
33
  },
34
34
  "scripts": {
35
35
  "check": "node --check src/index.js",
36
- "test": "python3 -m unittest discover -s tests"
36
+ "test": "node --test tests/*.test.mjs && python3 -m unittest discover -s tests"
37
37
  }
38
38
  }
package/runtime/server.py CHANGED
@@ -89,6 +89,27 @@ LICENSE_KEY = os.getenv("TASKSAI_LICENSE_KEY", os.getenv("LAWTASKSAI_LICENSE_KEY
89
89
  PRODUCT_ID = os.getenv("TASKSAI_PRODUCT_ID", "") # set by installer; used to resolve correct vertical
90
90
  INSTALL_ID = os.getenv("TASKSAI_INSTALL_ID", "")
91
91
 
92
+ _VERTICAL_FALLBACKS = {
93
+ "farmer": {
94
+ "product_id": "farmer",
95
+ "product_name": "FarmerTasksAI",
96
+ "display_name": "FarmerTasksAI",
97
+ "tool_prefix": "farmertasksai",
98
+ "occupation": "farmer",
99
+ "support_email": "hello@farmertasksai.com",
100
+ "domain": "farmertasksai.com",
101
+ },
102
+ "realtor": {
103
+ "product_id": "realtor",
104
+ "product_name": "RealtorTasksAI",
105
+ "display_name": "RealtorTasksAI",
106
+ "tool_prefix": "realtortasksai",
107
+ "occupation": "real estate professional",
108
+ "support_email": "hello@realtortasksai.com",
109
+ "domain": "realtortasksai.com",
110
+ },
111
+ }
112
+
92
113
  if not LICENSE_KEY:
93
114
  print("ERROR: License key is required. Set TASKSAI_LICENSE_KEY in your .env file.", file=sys.stderr, flush=True)
94
115
  print("Find your key in your purchase confirmation email.", file=sys.stderr, flush=True)
@@ -521,23 +542,28 @@ async def report_activation_event(event_name, *, skill_id=None, file_format=None
521
542
 
522
543
 
523
544
  async def load_vertical():
524
- """Fetch vertical metadata from /v1/me on startup. Falls back to farmer."""
545
+ """Fetch vertical metadata from /v1/me without crossing product boundaries."""
525
546
  global _vertical
526
547
  try:
527
548
  path = f"/v1/me?product_id={PRODUCT_ID}" if PRODUCT_ID else "/v1/me"
528
549
  _vertical = await api_get(path)
529
550
  except Exception:
530
- # Fallback: derive from license key prefix client-side
531
- prefix = LICENSE_KEY.split("_")[0] + "_" if "_" in LICENSE_KEY else "ft_"
532
- _vertical = {
533
- "product_id": "farmer",
534
- "product_name": "FarmerTasksAI",
535
- "display_name": "FarmerTasksAI",
536
- "tool_prefix": "farmertasksai",
537
- "occupation": "farmer",
538
- "support_email":"support@farmertasksai.com",
539
- "domain": "farmertasksai.com",
540
- }
551
+ product_id = PRODUCT_ID.strip().lower() or "tasksai"
552
+ known = _VERTICAL_FALLBACKS.get(product_id)
553
+ if known:
554
+ _vertical = dict(known)
555
+ else:
556
+ safe_id = re.sub(r"[^a-z0-9]+", "", product_id) or "tasksai"
557
+ display_name = f"{safe_id.title()}TasksAI"
558
+ _vertical = {
559
+ "product_id": product_id,
560
+ "product_name": display_name,
561
+ "display_name": display_name,
562
+ "tool_prefix": f"{safe_id}tasksai",
563
+ "occupation": "professional",
564
+ "support_email": "hello@tasksai.com",
565
+ "domain": "tasksai.com",
566
+ }
541
567
  return _vertical
542
568
 
543
569
 
package/src/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  import fs from "node:fs";
4
4
  import fsp from "node:fs/promises";
5
+ import { randomUUID } from "node:crypto";
5
6
  import https from "node:https";
6
7
  import os from "node:os";
7
8
  import path from "node:path";
@@ -10,9 +11,23 @@ import { spawnSync } from "node:child_process";
10
11
  import { stdin as input, stdout as output } from "node:process";
11
12
  import { fileURLToPath } from "node:url";
12
13
 
13
- const INSTALLER_VERSION = "0.1.15";
14
14
  const INSTALLER_DIR = path.dirname(fileURLToPath(import.meta.url));
15
+ const INSTALLER_PACKAGE = JSON.parse(fs.readFileSync(path.resolve(INSTALLER_DIR, "..", "package.json"), "utf8"));
16
+ const INSTALLER_VERSION = INSTALLER_PACKAGE.version;
15
17
  const BUNDLED_RUNTIME_DIR = path.resolve(INSTALLER_DIR, "..", "runtime");
18
+ const OFFICIAL_WRAPPER_REPO = "https://github.com/TasksAI-Official/tasksai-mcp-wrappers";
19
+ const TRUSTED_PRODUCTION_SOURCES = new Map([
20
+ ["lawtasksai", { repo: OFFICIAL_WRAPPER_REPO, ref: "main", path: "verticals/lawtasksai" }],
21
+ ["farmer", { repo: OFFICIAL_WRAPPER_REPO, ref: "main", path: "verticals/farmer" }],
22
+ ["realtor", { repo: OFFICIAL_WRAPPER_REPO, ref: "main", path: "verticals/realtor" }],
23
+ ["teacher", { repo: OFFICIAL_WRAPPER_REPO, ref: "main", path: "verticals/teacher" }],
24
+ ["priorauthai", {
25
+ repo: "https://github.com/laudoluxDev/priorauthai-mcp",
26
+ ref: "main",
27
+ path: "",
28
+ legacyManifestPathOptional: true
29
+ }]
30
+ ]);
16
31
  const DEFAULT_SOURCES = {
17
32
  lawtasksai: "https://github.com/TasksAI-Official/tasksai-mcp-wrappers/tree/main/verticals/lawtasksai",
18
33
  farmer: "https://github.com/TasksAI-Official/tasksai-mcp-wrappers/tree/main/verticals/farmer",
@@ -64,10 +79,12 @@ const CLIENTS = {
64
79
  }
65
80
  };
66
81
 
67
- main().catch((error) => {
68
- console.error(formatError(error));
69
- process.exit(1);
70
- });
82
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
83
+ main().catch((error) => {
84
+ console.error(formatError(error));
85
+ process.exit(1);
86
+ });
87
+ }
71
88
 
72
89
  async function main() {
73
90
  const options = parseArgs(process.argv.slice(2));
@@ -106,7 +123,8 @@ function parseArgs(argv) {
106
123
  installDir: process.env.TASKSAI_INSTALL_DIR || null,
107
124
  noBrowser: false,
108
125
  yes: false,
109
- skipPythonDeps: false
126
+ skipPythonDeps: false,
127
+ allowUntrustedDevelopmentSource: false
110
128
  };
111
129
 
112
130
  const positionals = [];
@@ -120,6 +138,7 @@ function parseArgs(argv) {
120
138
  else if (arg === "--no-browser") options.noBrowser = true;
121
139
  else if (arg === "--yes" || arg === "-y") options.yes = true;
122
140
  else if (arg === "--skip-python-deps") options.skipPythonDeps = true;
141
+ else if (arg === "--allow-untrusted-development-source") options.allowUntrustedDevelopmentSource = true;
123
142
  else if (arg === "--help" || arg === "-h") {
124
143
  printUsage();
125
144
  process.exit(0);
@@ -161,6 +180,10 @@ Examples:
161
180
 
162
181
  Environment:
163
182
  TASKSAI_INSTALL_DIR may be used instead of --install-dir.
183
+
184
+ Development only:
185
+ --allow-untrusted-development-source permits a local, non-main, or non-official
186
+ source for a known production product. Never use it for a customer install.
164
187
  `);
165
188
  }
166
189
 
@@ -169,6 +192,9 @@ async function install(options, { updateOnly = false } = {}) {
169
192
  const manifest = await loadJson(source, "agent-install.json");
170
193
  const vertical = await loadJson(source, "vertical.json");
171
194
  verifySource({ options, source, manifest, vertical });
195
+ if (options.allowUntrustedDevelopmentSource) {
196
+ console.warn("WARNING: untrusted development source override enabled; this install is not production-trusted.");
197
+ }
172
198
 
173
199
  const installDir = getInstallDir(options.productId, options);
174
200
  const runtimeDir = path.join(installDir, "runtime");
@@ -190,6 +216,11 @@ async function install(options, { updateOnly = false } = {}) {
190
216
  await downloadRuntime(source, runtimeDir, manifest);
191
217
 
192
218
  const licenseKey = await resolveLicenseKey(options, vertical, installDir);
219
+ const existingEnvPath = path.join(installDir, ".env");
220
+ const existingEnv = fs.existsSync(existingEnvPath)
221
+ ? parseEnv(await fsp.readFile(existingEnvPath, "utf8"))
222
+ : {};
223
+ const installId = firstValue(existingEnv.TASKSAI_INSTALL_ID, process.env.TASKSAI_INSTALL_ID) || randomUUID();
193
224
 
194
225
  if (!options.skipPythonDeps) {
195
226
  installPythonDeps(runtimeDir, vendorDir);
@@ -200,7 +231,8 @@ async function install(options, { updateOnly = false } = {}) {
200
231
  LAWTASKSAI_LICENSE_KEY: licenseKey,
201
232
  TASKSAI_PRODUCT_ID: vertical.product_id,
202
233
  TASKSAI_API_BASE: vertical.api_base_url,
203
- LAWTASKSAI_API_BASE: vertical.api_base_url
234
+ LAWTASKSAI_API_BASE: vertical.api_base_url,
235
+ TASKSAI_INSTALL_ID: installId
204
236
  };
205
237
  await writeEnvFile(path.join(installDir, ".env"), envEntries);
206
238
  await writeEnvFile(path.join(runtimeDir, ".env"), envEntries);
@@ -224,18 +256,27 @@ async function install(options, { updateOnly = false } = {}) {
224
256
  console.log(`After restart, ask: "${vertical.first_prompt}"`);
225
257
  }
226
258
 
227
- async function doctor(options, { quietSuccess = false } = {}) {
259
+ async function doctor(options, {
260
+ quietSuccess = false,
261
+ clientsOverride = null,
262
+ requestJsonImpl = requestJson,
263
+ runtimeHealthImpl = verifyRuntimeHealth
264
+ } = {}) {
228
265
  const installDir = getInstallDir(options.productId, options);
229
266
  const verticalPath = path.join(installDir, "vertical.json");
230
267
  const envPath = path.join(installDir, ".env");
231
268
  const serverPath = path.join(installDir, "runtime", "server.py");
232
- const clients = resolveClients(options.client);
269
+ const requirementsPath = path.join(installDir, "runtime", "requirements.txt");
270
+ const vendorDir = path.join(installDir, "python");
271
+ const clients = clientsOverride || resolveClients(options.client);
233
272
 
234
273
  const problems = [];
235
274
  if (!fs.existsSync(verticalPath)) problems.push(`Missing ${verticalPath}`);
236
275
  if (!fs.existsSync(envPath)) problems.push(`Missing ${envPath}`);
237
276
  if (!fs.existsSync(serverPath)) problems.push(`Missing ${serverPath}`);
277
+ if (!fs.existsSync(requirementsPath)) problems.push(`Missing ${requirementsPath}`);
238
278
  const vertical = fs.existsSync(verticalPath) ? await readJson(verticalPath) : { product_id: options.productId };
279
+ const env = fs.existsSync(envPath) ? parseEnv(await fsp.readFile(envPath, "utf8")) : {};
239
280
 
240
281
  for (const client of clients) {
241
282
  const configPath = client.configPath();
@@ -249,15 +290,98 @@ async function doctor(options, { quietSuccess = false } = {}) {
249
290
  }
250
291
  }
251
292
 
293
+ if (fs.existsSync(serverPath) && fs.existsSync(requirementsPath)) {
294
+ try {
295
+ await runtimeHealthImpl({ serverPath, vendorDir });
296
+ } catch (error) {
297
+ problems.push(`Runtime health check failed (${healthErrorSummary(error)})`);
298
+ }
299
+ }
300
+
301
+ const productId = String(vertical.product_id || options.productId || "").trim().toLowerCase();
302
+ if (productId !== String(options.productId || "").trim().toLowerCase()) {
303
+ problems.push(`Installed product ${vertical.product_id} does not match requested product ${options.productId}`);
304
+ }
305
+ const configuredProductId = firstValue(process.env.TASKSAI_PRODUCT_ID, env.TASKSAI_PRODUCT_ID);
306
+ if (configuredProductId && configuredProductId.trim().toLowerCase() !== productId) {
307
+ problems.push(`Runtime product ${configuredProductId} does not match installed product ${productId}`);
308
+ }
309
+
310
+ const apiBase = firstValue(
311
+ process.env.TASKSAI_API_BASE,
312
+ process.env.LAWTASKSAI_API_BASE,
313
+ env.TASKSAI_API_BASE,
314
+ env.LAWTASKSAI_API_BASE,
315
+ vertical.api_base_url
316
+ )?.replace(/\/$/, "");
317
+ const licenseKey = firstValue(
318
+ process.env.TASKSAI_LICENSE_KEY,
319
+ process.env.LAWTASKSAI_LICENSE_KEY,
320
+ env.TASKSAI_LICENSE_KEY,
321
+ env.LAWTASKSAI_LICENSE_KEY
322
+ );
323
+ const installId = firstValue(process.env.TASKSAI_INSTALL_ID, env.TASKSAI_INSTALL_ID);
324
+
325
+ let apiHealthy = false;
326
+ if (!apiBase) {
327
+ problems.push("No TasksAI API base URL is configured");
328
+ } else {
329
+ try {
330
+ const apiHealth = await requestJsonImpl(`${apiBase}/health`, {
331
+ headers: { "User-Agent": `tasksai-install/${INSTALLER_VERSION}` }
332
+ });
333
+ const status = String(apiHealth?.status || "").trim().toLowerCase();
334
+ if (["error", "failed", "unhealthy"].includes(status)) {
335
+ problems.push("API health check failed (unhealthy)");
336
+ } else {
337
+ apiHealthy = true;
338
+ }
339
+ } catch (error) {
340
+ problems.push(`API health check failed (${healthErrorSummary(error)})`);
341
+ }
342
+ }
343
+
344
+ if (!licenseKey) {
345
+ problems.push("No TasksAI license key is configured");
346
+ } else if (apiBase && apiHealthy) {
347
+ try {
348
+ const account = await requestJsonImpl(`${apiBase}/v1/me?product_id=${encodeURIComponent(productId)}`, {
349
+ headers: authenticatedHeaders({ licenseKey, productId })
350
+ });
351
+ const licensedProduct = String(account?.product_id || account?.product?.product_id || "").trim().toLowerCase();
352
+ if (!licensedProduct) {
353
+ problems.push("License health response did not identify a product");
354
+ } else if (licensedProduct !== productId) {
355
+ problems.push(`License is for ${licensedProduct}, not ${productId}`);
356
+ }
357
+ } catch (error) {
358
+ problems.push(`License health check failed (${healthErrorSummary(error)})`);
359
+ }
360
+ }
361
+
252
362
  if (problems.length) {
253
363
  throw new Error(`TasksAI doctor found issues:\n- ${problems.join("\n- ")}`);
254
364
  }
255
365
 
366
+ const activationReported = await safeReportDoctorPassed({
367
+ apiBase,
368
+ licenseKey,
369
+ productId,
370
+ installId,
371
+ client: options.client,
372
+ requestJsonImpl
373
+ });
256
374
  await safeLogEvent(installDir, "doctor_passed", {
257
- productId: options.productId,
258
- clients: clients.map((client) => client.id)
375
+ productId,
376
+ clients: clients.map((client) => client.id),
377
+ activationReported
259
378
  });
260
- if (!quietSuccess) console.log("TasksAI doctor passed.");
379
+ if (!quietSuccess) {
380
+ console.log("TasksAI doctor passed.");
381
+ if (!activationReported) {
382
+ console.log("Activation reporting is temporarily unavailable; core doctor checks still passed.");
383
+ }
384
+ }
261
385
  }
262
386
 
263
387
  async function uninstall(options) {
@@ -729,6 +853,7 @@ function verifySource({ options, source, manifest, vertical }) {
729
853
  if (vertical.product_id !== options.productId) {
730
854
  throw new Error(`Vertical product_id ${vertical.product_id} does not match requested product ${options.productId}.`);
731
855
  }
856
+ verifyProductionSource({ options, source, manifest });
732
857
  if (manifest.official_package !== "@tasksai/install") {
733
858
  throw new Error(`Unexpected installer package: ${manifest.official_package}`);
734
859
  }
@@ -754,6 +879,46 @@ function verifySource({ options, source, manifest, vertical }) {
754
879
  }
755
880
  }
756
881
 
882
+ function verifyProductionSource({ options, source, manifest }) {
883
+ if (options.allowUntrustedDevelopmentSource) return;
884
+ const expected = TRUSTED_PRODUCTION_SOURCES.get(options.productId);
885
+ if (!expected) {
886
+ throw new Error(
887
+ `${options.productId} is not registered as a trusted production source. `
888
+ + "For deliberate local or untrusted development only, add --allow-untrusted-development-source."
889
+ );
890
+ }
891
+
892
+ const expectedManifestPath = joinGitPath(expected.path, "agent-install.json");
893
+ const manifestPathTrusted = expected.legacyManifestPathOptional && !manifest.official_manifest_path
894
+ ? true
895
+ : normalizeGitPath(manifest.official_manifest_path) === expectedManifestPath;
896
+ const trusted = source.kind === "github"
897
+ && sameRepository(source.repoUrl, expected.repo)
898
+ && source.ref === expected.ref
899
+ && normalizeGitPath(source.basePath) === expected.path
900
+ && sameRepository(manifest.official_github_repo, expected.repo)
901
+ && manifestPathTrusted;
902
+
903
+ if (!trusted) {
904
+ throw new Error(
905
+ `${options.productId} production installs must use ${formatTrustedSource(expected)}. `
906
+ + "For deliberate local or untrusted development only, add --allow-untrusted-development-source."
907
+ );
908
+ }
909
+ }
910
+
911
+ function formatTrustedSource(expected) {
912
+ return expected.path
913
+ ? `${expected.repo}/tree/${expected.ref}/${expected.path}`
914
+ : `${expected.repo} at ref ${expected.ref}`;
915
+ }
916
+
917
+ function sameRepository(left, right) {
918
+ return String(left || "").replace(/\.git$/i, "").replace(/\/$/, "").toLowerCase()
919
+ === String(right || "").replace(/\.git$/i, "").replace(/\/$/, "").toLowerCase();
920
+ }
921
+
757
922
  function getInstaller(manifest) {
758
923
  if (manifest.installer?.npm_exec) return manifest.installer.npm_exec;
759
924
  if (manifest.installer?.npx) return manifest.installer.npx;
@@ -862,21 +1027,27 @@ function fetchText(url) {
862
1027
  });
863
1028
  }
864
1029
 
865
- function postJson(url, body) {
866
- const payload = JSON.stringify(body || {});
1030
+ function requestJson(url, { method = "GET", body, headers = {}, timeoutMs = 7000 } = {}) {
1031
+ const payload = body === undefined ? null : JSON.stringify(body);
867
1032
  return new Promise((resolve, reject) => {
868
1033
  const request = https.request(url, {
869
- method: "POST",
1034
+ method,
870
1035
  headers: {
871
- "Content-Type": "application/json",
872
- "Content-Length": Buffer.byteLength(payload),
873
- "User-Agent": `tasksai-install/${INSTALLER_VERSION}`
1036
+ "User-Agent": `tasksai-install/${INSTALLER_VERSION}`,
1037
+ ...(payload === null ? {} : {
1038
+ "Content-Type": "application/json",
1039
+ "Content-Length": Buffer.byteLength(payload)
1040
+ }),
1041
+ ...headers
874
1042
  }
875
1043
  }, (response) => {
876
1044
  let responseBody = "";
877
1045
  response.setEncoding("utf8");
878
1046
  response.on("data", (chunk) => {
879
1047
  responseBody += chunk;
1048
+ if (responseBody.length > 1_000_000) {
1049
+ request.destroy(new Error("TasksAI API response exceeded the safe size limit."));
1050
+ }
880
1051
  });
881
1052
  response.on("end", () => {
882
1053
  let data = {};
@@ -897,12 +1068,120 @@ function postJson(url, body) {
897
1068
  resolve(data);
898
1069
  });
899
1070
  });
1071
+ request.setTimeout(timeoutMs, () => {
1072
+ const error = new Error("TasksAI API request timed out.");
1073
+ error.code = "ETIMEDOUT";
1074
+ request.destroy(error);
1075
+ });
900
1076
  request.on("error", reject);
901
- request.write(payload);
1077
+ if (payload !== null) request.write(payload);
902
1078
  request.end();
903
1079
  });
904
1080
  }
905
1081
 
1082
+ function postJson(url, body) {
1083
+ return requestJson(url, { method: "POST", body });
1084
+ }
1085
+
1086
+ function authenticatedHeaders({ licenseKey, productId }) {
1087
+ return {
1088
+ Authorization: `Bearer ${licenseKey}`,
1089
+ "Content-Type": "application/json",
1090
+ "X-Product-ID": productId,
1091
+ "X-Client-Type": "tasksai-installer",
1092
+ "X-Client-Version": INSTALLER_VERSION
1093
+ };
1094
+ }
1095
+
1096
+ async function safeReportDoctorPassed({
1097
+ apiBase,
1098
+ licenseKey,
1099
+ productId,
1100
+ installId,
1101
+ client,
1102
+ requestJsonImpl = requestJson
1103
+ }) {
1104
+ if (!apiBase || !licenseKey || !productId) return false;
1105
+ const normalizedClient = String(client || "")
1106
+ .trim()
1107
+ .toLowerCase()
1108
+ .replaceAll("-", "_");
1109
+ const reportableClients = new Set([
1110
+ "claude_desktop", "cursor", "windsurf", "codex", "cline", "openclaw"
1111
+ ]);
1112
+ const payload = {
1113
+ event_name: "doctor_passed",
1114
+ metadata: {
1115
+ source: "installer_doctor",
1116
+ tool_version: INSTALLER_VERSION
1117
+ }
1118
+ };
1119
+ if (reportableClients.has(normalizedClient)) {
1120
+ payload.metadata.client = normalizedClient;
1121
+ }
1122
+ if (installId) payload.install_id = installId;
1123
+
1124
+ try {
1125
+ await requestJsonImpl(`${apiBase}/v1/events/activation`, {
1126
+ method: "POST",
1127
+ body: payload,
1128
+ headers: authenticatedHeaders({ licenseKey, productId })
1129
+ });
1130
+ return true;
1131
+ } catch {
1132
+ return false;
1133
+ }
1134
+ }
1135
+
1136
+ async function verifyRuntimeHealth({ serverPath, vendorDir }) {
1137
+ const python = findPython();
1138
+ if (!python) throw new Error("Python 3 is unavailable");
1139
+ const pythonPath = [vendorDir, process.env.PYTHONPATH].filter(Boolean).join(path.delimiter);
1140
+ const check = [
1141
+ "import ast, pathlib, sys",
1142
+ "ast.parse(pathlib.Path(sys.argv[1]).read_text(encoding='utf-8'), filename=sys.argv[1])",
1143
+ "import httpx, dotenv, mcp, docx"
1144
+ ].join("; ");
1145
+ const result = spawnSync(python, ["-c", check, serverPath], {
1146
+ encoding: "utf8",
1147
+ timeout: 10000,
1148
+ env: { ...process.env, PYTHONPATH: pythonPath }
1149
+ });
1150
+ if (result.error) throw result.error;
1151
+ if (result.status !== 0) {
1152
+ throw new Error("runtime syntax or Python dependencies are unavailable");
1153
+ }
1154
+ }
1155
+
1156
+ function parseEnv(text) {
1157
+ const values = {};
1158
+ for (const rawLine of String(text || "").split(/\r?\n/)) {
1159
+ const line = rawLine.trim();
1160
+ if (!line || line.startsWith("#")) continue;
1161
+ const match = line.match(/^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
1162
+ if (!match) continue;
1163
+ let value = match[2].trim();
1164
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
1165
+ value = value.slice(1, -1);
1166
+ }
1167
+ values[match[1]] = value;
1168
+ }
1169
+ return values;
1170
+ }
1171
+
1172
+ function firstValue(...values) {
1173
+ return values.find((value) => typeof value === "string" && value.trim())?.trim() || "";
1174
+ }
1175
+
1176
+ function healthErrorSummary(error) {
1177
+ if (Number.isInteger(error?.statusCode)) return `HTTP ${error.statusCode}`;
1178
+ if (error?.code === "ETIMEDOUT") return "timeout";
1179
+ if (["ECONNREFUSED", "ENOTFOUND", "EAI_AGAIN", "ECONNRESET"].includes(error?.code)) {
1180
+ return "unreachable";
1181
+ }
1182
+ return "unavailable";
1183
+ }
1184
+
906
1185
  function openExternalUrl(url) {
907
1186
  const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
908
1187
  const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
@@ -1034,3 +1313,16 @@ function redact(text) {
1034
1313
  .replace(/Bearer\s+[A-Za-z0-9._-]+/gi, "Bearer [REDACTED]")
1035
1314
  .replace(/gh[opsu]_[A-Za-z0-9_]+/g, "gh_[REDACTED]");
1036
1315
  }
1316
+
1317
+ export {
1318
+ INSTALLER_VERSION,
1319
+ authenticatedHeaders,
1320
+ doctor,
1321
+ parseArgs,
1322
+ parseEnv,
1323
+ parseSource,
1324
+ safeReportDoctorPassed,
1325
+ verifyProductionSource,
1326
+ verifyRuntimeHealth,
1327
+ verifySource
1328
+ };