@tasksai/install 0.1.16 → 0.1.18
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 +20 -1
- package/package.json +2 -2
- package/src/index.js +318 -20
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
|
|
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.
|
|
3
|
+
"version": "0.1.18",
|
|
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/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,15 +11,35 @@ 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
|
+
["contractor", { repo: "https://github.com/TasksAI-Official/contractortasksai-mcp", ref: "main", path: "", legacyManifestPathOptional: true }],
|
|
25
|
+
["therapist", { repo: "https://github.com/TasksAI-Official/therapisttasksai-mcp", ref: "main", path: "", legacyManifestPathOptional: true }],
|
|
26
|
+
["marketing", { repo: "https://github.com/TasksAI-Official/marketingtasksai-mcp", ref: "main", path: "", legacyManifestPathOptional: true }],
|
|
27
|
+
["priorauthai", {
|
|
28
|
+
repo: "https://github.com/TasksAI-Official/priorauthai-mcp",
|
|
29
|
+
ref: "main",
|
|
30
|
+
path: "",
|
|
31
|
+
legacyManifestPathOptional: true
|
|
32
|
+
}]
|
|
33
|
+
]);
|
|
16
34
|
const DEFAULT_SOURCES = {
|
|
17
35
|
lawtasksai: "https://github.com/TasksAI-Official/tasksai-mcp-wrappers/tree/main/verticals/lawtasksai",
|
|
18
36
|
farmer: "https://github.com/TasksAI-Official/tasksai-mcp-wrappers/tree/main/verticals/farmer",
|
|
19
37
|
realtor: "https://github.com/TasksAI-Official/tasksai-mcp-wrappers/tree/main/verticals/realtor",
|
|
20
38
|
teacher: "https://github.com/TasksAI-Official/tasksai-mcp-wrappers/tree/main/verticals/teacher",
|
|
21
|
-
|
|
39
|
+
contractor: "https://github.com/TasksAI-Official/contractortasksai-mcp",
|
|
40
|
+
therapist: "https://github.com/TasksAI-Official/therapisttasksai-mcp",
|
|
41
|
+
marketing: "https://github.com/TasksAI-Official/marketingtasksai-mcp",
|
|
42
|
+
priorauthai: "https://github.com/TasksAI-Official/priorauthai-mcp"
|
|
22
43
|
};
|
|
23
44
|
const LEGACY_MCP_SERVER_IDS = {
|
|
24
45
|
farmer: ["farmertasksai"],
|
|
@@ -64,10 +85,12 @@ const CLIENTS = {
|
|
|
64
85
|
}
|
|
65
86
|
};
|
|
66
87
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
88
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
89
|
+
main().catch((error) => {
|
|
90
|
+
console.error(formatError(error));
|
|
91
|
+
process.exit(1);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
71
94
|
|
|
72
95
|
async function main() {
|
|
73
96
|
const options = parseArgs(process.argv.slice(2));
|
|
@@ -106,7 +129,8 @@ function parseArgs(argv) {
|
|
|
106
129
|
installDir: process.env.TASKSAI_INSTALL_DIR || null,
|
|
107
130
|
noBrowser: false,
|
|
108
131
|
yes: false,
|
|
109
|
-
skipPythonDeps: false
|
|
132
|
+
skipPythonDeps: false,
|
|
133
|
+
allowUntrustedDevelopmentSource: false
|
|
110
134
|
};
|
|
111
135
|
|
|
112
136
|
const positionals = [];
|
|
@@ -120,6 +144,7 @@ function parseArgs(argv) {
|
|
|
120
144
|
else if (arg === "--no-browser") options.noBrowser = true;
|
|
121
145
|
else if (arg === "--yes" || arg === "-y") options.yes = true;
|
|
122
146
|
else if (arg === "--skip-python-deps") options.skipPythonDeps = true;
|
|
147
|
+
else if (arg === "--allow-untrusted-development-source") options.allowUntrustedDevelopmentSource = true;
|
|
123
148
|
else if (arg === "--help" || arg === "-h") {
|
|
124
149
|
printUsage();
|
|
125
150
|
process.exit(0);
|
|
@@ -161,6 +186,10 @@ Examples:
|
|
|
161
186
|
|
|
162
187
|
Environment:
|
|
163
188
|
TASKSAI_INSTALL_DIR may be used instead of --install-dir.
|
|
189
|
+
|
|
190
|
+
Development only:
|
|
191
|
+
--allow-untrusted-development-source permits a local, non-main, or non-official
|
|
192
|
+
source for a known production product. Never use it for a customer install.
|
|
164
193
|
`);
|
|
165
194
|
}
|
|
166
195
|
|
|
@@ -169,6 +198,9 @@ async function install(options, { updateOnly = false } = {}) {
|
|
|
169
198
|
const manifest = await loadJson(source, "agent-install.json");
|
|
170
199
|
const vertical = await loadJson(source, "vertical.json");
|
|
171
200
|
verifySource({ options, source, manifest, vertical });
|
|
201
|
+
if (options.allowUntrustedDevelopmentSource) {
|
|
202
|
+
console.warn("WARNING: untrusted development source override enabled; this install is not production-trusted.");
|
|
203
|
+
}
|
|
172
204
|
|
|
173
205
|
const installDir = getInstallDir(options.productId, options);
|
|
174
206
|
const runtimeDir = path.join(installDir, "runtime");
|
|
@@ -190,6 +222,11 @@ async function install(options, { updateOnly = false } = {}) {
|
|
|
190
222
|
await downloadRuntime(source, runtimeDir, manifest);
|
|
191
223
|
|
|
192
224
|
const licenseKey = await resolveLicenseKey(options, vertical, installDir);
|
|
225
|
+
const existingEnvPath = path.join(installDir, ".env");
|
|
226
|
+
const existingEnv = fs.existsSync(existingEnvPath)
|
|
227
|
+
? parseEnv(await fsp.readFile(existingEnvPath, "utf8"))
|
|
228
|
+
: {};
|
|
229
|
+
const installId = firstValue(existingEnv.TASKSAI_INSTALL_ID, process.env.TASKSAI_INSTALL_ID) || randomUUID();
|
|
193
230
|
|
|
194
231
|
if (!options.skipPythonDeps) {
|
|
195
232
|
installPythonDeps(runtimeDir, vendorDir);
|
|
@@ -200,7 +237,8 @@ async function install(options, { updateOnly = false } = {}) {
|
|
|
200
237
|
LAWTASKSAI_LICENSE_KEY: licenseKey,
|
|
201
238
|
TASKSAI_PRODUCT_ID: vertical.product_id,
|
|
202
239
|
TASKSAI_API_BASE: vertical.api_base_url,
|
|
203
|
-
LAWTASKSAI_API_BASE: vertical.api_base_url
|
|
240
|
+
LAWTASKSAI_API_BASE: vertical.api_base_url,
|
|
241
|
+
TASKSAI_INSTALL_ID: installId
|
|
204
242
|
};
|
|
205
243
|
await writeEnvFile(path.join(installDir, ".env"), envEntries);
|
|
206
244
|
await writeEnvFile(path.join(runtimeDir, ".env"), envEntries);
|
|
@@ -224,18 +262,27 @@ async function install(options, { updateOnly = false } = {}) {
|
|
|
224
262
|
console.log(`After restart, ask: "${vertical.first_prompt}"`);
|
|
225
263
|
}
|
|
226
264
|
|
|
227
|
-
async function doctor(options, {
|
|
265
|
+
async function doctor(options, {
|
|
266
|
+
quietSuccess = false,
|
|
267
|
+
clientsOverride = null,
|
|
268
|
+
requestJsonImpl = requestJson,
|
|
269
|
+
runtimeHealthImpl = verifyRuntimeHealth
|
|
270
|
+
} = {}) {
|
|
228
271
|
const installDir = getInstallDir(options.productId, options);
|
|
229
272
|
const verticalPath = path.join(installDir, "vertical.json");
|
|
230
273
|
const envPath = path.join(installDir, ".env");
|
|
231
274
|
const serverPath = path.join(installDir, "runtime", "server.py");
|
|
232
|
-
const
|
|
275
|
+
const requirementsPath = path.join(installDir, "runtime", "requirements.txt");
|
|
276
|
+
const vendorDir = path.join(installDir, "python");
|
|
277
|
+
const clients = clientsOverride || resolveClients(options.client);
|
|
233
278
|
|
|
234
279
|
const problems = [];
|
|
235
280
|
if (!fs.existsSync(verticalPath)) problems.push(`Missing ${verticalPath}`);
|
|
236
281
|
if (!fs.existsSync(envPath)) problems.push(`Missing ${envPath}`);
|
|
237
282
|
if (!fs.existsSync(serverPath)) problems.push(`Missing ${serverPath}`);
|
|
283
|
+
if (!fs.existsSync(requirementsPath)) problems.push(`Missing ${requirementsPath}`);
|
|
238
284
|
const vertical = fs.existsSync(verticalPath) ? await readJson(verticalPath) : { product_id: options.productId };
|
|
285
|
+
const env = fs.existsSync(envPath) ? parseEnv(await fsp.readFile(envPath, "utf8")) : {};
|
|
239
286
|
|
|
240
287
|
for (const client of clients) {
|
|
241
288
|
const configPath = client.configPath();
|
|
@@ -249,15 +296,98 @@ async function doctor(options, { quietSuccess = false } = {}) {
|
|
|
249
296
|
}
|
|
250
297
|
}
|
|
251
298
|
|
|
299
|
+
if (fs.existsSync(serverPath) && fs.existsSync(requirementsPath)) {
|
|
300
|
+
try {
|
|
301
|
+
await runtimeHealthImpl({ serverPath, vendorDir });
|
|
302
|
+
} catch (error) {
|
|
303
|
+
problems.push(`Runtime health check failed (${healthErrorSummary(error)})`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const productId = String(vertical.product_id || options.productId || "").trim().toLowerCase();
|
|
308
|
+
if (productId !== String(options.productId || "").trim().toLowerCase()) {
|
|
309
|
+
problems.push(`Installed product ${vertical.product_id} does not match requested product ${options.productId}`);
|
|
310
|
+
}
|
|
311
|
+
const configuredProductId = firstValue(process.env.TASKSAI_PRODUCT_ID, env.TASKSAI_PRODUCT_ID);
|
|
312
|
+
if (configuredProductId && configuredProductId.trim().toLowerCase() !== productId) {
|
|
313
|
+
problems.push(`Runtime product ${configuredProductId} does not match installed product ${productId}`);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const apiBase = firstValue(
|
|
317
|
+
process.env.TASKSAI_API_BASE,
|
|
318
|
+
process.env.LAWTASKSAI_API_BASE,
|
|
319
|
+
env.TASKSAI_API_BASE,
|
|
320
|
+
env.LAWTASKSAI_API_BASE,
|
|
321
|
+
vertical.api_base_url
|
|
322
|
+
)?.replace(/\/$/, "");
|
|
323
|
+
const licenseKey = firstValue(
|
|
324
|
+
process.env.TASKSAI_LICENSE_KEY,
|
|
325
|
+
process.env.LAWTASKSAI_LICENSE_KEY,
|
|
326
|
+
env.TASKSAI_LICENSE_KEY,
|
|
327
|
+
env.LAWTASKSAI_LICENSE_KEY
|
|
328
|
+
);
|
|
329
|
+
const installId = firstValue(process.env.TASKSAI_INSTALL_ID, env.TASKSAI_INSTALL_ID);
|
|
330
|
+
|
|
331
|
+
let apiHealthy = false;
|
|
332
|
+
if (!apiBase) {
|
|
333
|
+
problems.push("No TasksAI API base URL is configured");
|
|
334
|
+
} else {
|
|
335
|
+
try {
|
|
336
|
+
const apiHealth = await requestJsonImpl(`${apiBase}/health`, {
|
|
337
|
+
headers: { "User-Agent": `tasksai-install/${INSTALLER_VERSION}` }
|
|
338
|
+
});
|
|
339
|
+
const status = String(apiHealth?.status || "").trim().toLowerCase();
|
|
340
|
+
if (["error", "failed", "unhealthy"].includes(status)) {
|
|
341
|
+
problems.push("API health check failed (unhealthy)");
|
|
342
|
+
} else {
|
|
343
|
+
apiHealthy = true;
|
|
344
|
+
}
|
|
345
|
+
} catch (error) {
|
|
346
|
+
problems.push(`API health check failed (${healthErrorSummary(error)})`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (!licenseKey) {
|
|
351
|
+
problems.push("No TasksAI license key is configured");
|
|
352
|
+
} else if (apiBase && apiHealthy) {
|
|
353
|
+
try {
|
|
354
|
+
const account = await requestJsonImpl(`${apiBase}/v1/me?product_id=${encodeURIComponent(productId)}`, {
|
|
355
|
+
headers: authenticatedHeaders({ licenseKey, productId })
|
|
356
|
+
});
|
|
357
|
+
const licensedProduct = String(account?.product_id || account?.product?.product_id || "").trim().toLowerCase();
|
|
358
|
+
if (!licensedProduct) {
|
|
359
|
+
problems.push("License health response did not identify a product");
|
|
360
|
+
} else if (licensedProduct !== productId) {
|
|
361
|
+
problems.push(`License is for ${licensedProduct}, not ${productId}`);
|
|
362
|
+
}
|
|
363
|
+
} catch (error) {
|
|
364
|
+
problems.push(`License health check failed (${healthErrorSummary(error)})`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
252
368
|
if (problems.length) {
|
|
253
369
|
throw new Error(`TasksAI doctor found issues:\n- ${problems.join("\n- ")}`);
|
|
254
370
|
}
|
|
255
371
|
|
|
372
|
+
const activationReported = await safeReportDoctorPassed({
|
|
373
|
+
apiBase,
|
|
374
|
+
licenseKey,
|
|
375
|
+
productId,
|
|
376
|
+
installId,
|
|
377
|
+
client: options.client,
|
|
378
|
+
requestJsonImpl
|
|
379
|
+
});
|
|
256
380
|
await safeLogEvent(installDir, "doctor_passed", {
|
|
257
|
-
productId
|
|
258
|
-
clients: clients.map((client) => client.id)
|
|
381
|
+
productId,
|
|
382
|
+
clients: clients.map((client) => client.id),
|
|
383
|
+
activationReported
|
|
259
384
|
});
|
|
260
|
-
if (!quietSuccess)
|
|
385
|
+
if (!quietSuccess) {
|
|
386
|
+
console.log("TasksAI doctor passed.");
|
|
387
|
+
if (!activationReported) {
|
|
388
|
+
console.log("Activation reporting is temporarily unavailable; core doctor checks still passed.");
|
|
389
|
+
}
|
|
390
|
+
}
|
|
261
391
|
}
|
|
262
392
|
|
|
263
393
|
async function uninstall(options) {
|
|
@@ -729,6 +859,7 @@ function verifySource({ options, source, manifest, vertical }) {
|
|
|
729
859
|
if (vertical.product_id !== options.productId) {
|
|
730
860
|
throw new Error(`Vertical product_id ${vertical.product_id} does not match requested product ${options.productId}.`);
|
|
731
861
|
}
|
|
862
|
+
verifyProductionSource({ options, source, manifest });
|
|
732
863
|
if (manifest.official_package !== "@tasksai/install") {
|
|
733
864
|
throw new Error(`Unexpected installer package: ${manifest.official_package}`);
|
|
734
865
|
}
|
|
@@ -754,6 +885,46 @@ function verifySource({ options, source, manifest, vertical }) {
|
|
|
754
885
|
}
|
|
755
886
|
}
|
|
756
887
|
|
|
888
|
+
function verifyProductionSource({ options, source, manifest }) {
|
|
889
|
+
if (options.allowUntrustedDevelopmentSource) return;
|
|
890
|
+
const expected = TRUSTED_PRODUCTION_SOURCES.get(options.productId);
|
|
891
|
+
if (!expected) {
|
|
892
|
+
throw new Error(
|
|
893
|
+
`${options.productId} is not registered as a trusted production source. `
|
|
894
|
+
+ "For deliberate local or untrusted development only, add --allow-untrusted-development-source."
|
|
895
|
+
);
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
const expectedManifestPath = joinGitPath(expected.path, "agent-install.json");
|
|
899
|
+
const manifestPathTrusted = expected.legacyManifestPathOptional && !manifest.official_manifest_path
|
|
900
|
+
? true
|
|
901
|
+
: normalizeGitPath(manifest.official_manifest_path) === expectedManifestPath;
|
|
902
|
+
const trusted = source.kind === "github"
|
|
903
|
+
&& sameRepository(source.repoUrl, expected.repo)
|
|
904
|
+
&& source.ref === expected.ref
|
|
905
|
+
&& normalizeGitPath(source.basePath) === expected.path
|
|
906
|
+
&& sameRepository(manifest.official_github_repo, expected.repo)
|
|
907
|
+
&& manifestPathTrusted;
|
|
908
|
+
|
|
909
|
+
if (!trusted) {
|
|
910
|
+
throw new Error(
|
|
911
|
+
`${options.productId} production installs must use ${formatTrustedSource(expected)}. `
|
|
912
|
+
+ "For deliberate local or untrusted development only, add --allow-untrusted-development-source."
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function formatTrustedSource(expected) {
|
|
918
|
+
return expected.path
|
|
919
|
+
? `${expected.repo}/tree/${expected.ref}/${expected.path}`
|
|
920
|
+
: `${expected.repo} at ref ${expected.ref}`;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
function sameRepository(left, right) {
|
|
924
|
+
return String(left || "").replace(/\.git$/i, "").replace(/\/$/, "").toLowerCase()
|
|
925
|
+
=== String(right || "").replace(/\.git$/i, "").replace(/\/$/, "").toLowerCase();
|
|
926
|
+
}
|
|
927
|
+
|
|
757
928
|
function getInstaller(manifest) {
|
|
758
929
|
if (manifest.installer?.npm_exec) return manifest.installer.npm_exec;
|
|
759
930
|
if (manifest.installer?.npx) return manifest.installer.npx;
|
|
@@ -862,21 +1033,27 @@ function fetchText(url) {
|
|
|
862
1033
|
});
|
|
863
1034
|
}
|
|
864
1035
|
|
|
865
|
-
function
|
|
866
|
-
const payload = JSON.stringify(body
|
|
1036
|
+
function requestJson(url, { method = "GET", body, headers = {}, timeoutMs = 7000 } = {}) {
|
|
1037
|
+
const payload = body === undefined ? null : JSON.stringify(body);
|
|
867
1038
|
return new Promise((resolve, reject) => {
|
|
868
1039
|
const request = https.request(url, {
|
|
869
|
-
method
|
|
1040
|
+
method,
|
|
870
1041
|
headers: {
|
|
871
|
-
"
|
|
872
|
-
|
|
873
|
-
|
|
1042
|
+
"User-Agent": `tasksai-install/${INSTALLER_VERSION}`,
|
|
1043
|
+
...(payload === null ? {} : {
|
|
1044
|
+
"Content-Type": "application/json",
|
|
1045
|
+
"Content-Length": Buffer.byteLength(payload)
|
|
1046
|
+
}),
|
|
1047
|
+
...headers
|
|
874
1048
|
}
|
|
875
1049
|
}, (response) => {
|
|
876
1050
|
let responseBody = "";
|
|
877
1051
|
response.setEncoding("utf8");
|
|
878
1052
|
response.on("data", (chunk) => {
|
|
879
1053
|
responseBody += chunk;
|
|
1054
|
+
if (responseBody.length > 1_000_000) {
|
|
1055
|
+
request.destroy(new Error("TasksAI API response exceeded the safe size limit."));
|
|
1056
|
+
}
|
|
880
1057
|
});
|
|
881
1058
|
response.on("end", () => {
|
|
882
1059
|
let data = {};
|
|
@@ -897,12 +1074,120 @@ function postJson(url, body) {
|
|
|
897
1074
|
resolve(data);
|
|
898
1075
|
});
|
|
899
1076
|
});
|
|
1077
|
+
request.setTimeout(timeoutMs, () => {
|
|
1078
|
+
const error = new Error("TasksAI API request timed out.");
|
|
1079
|
+
error.code = "ETIMEDOUT";
|
|
1080
|
+
request.destroy(error);
|
|
1081
|
+
});
|
|
900
1082
|
request.on("error", reject);
|
|
901
|
-
request.write(payload);
|
|
1083
|
+
if (payload !== null) request.write(payload);
|
|
902
1084
|
request.end();
|
|
903
1085
|
});
|
|
904
1086
|
}
|
|
905
1087
|
|
|
1088
|
+
function postJson(url, body) {
|
|
1089
|
+
return requestJson(url, { method: "POST", body });
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
function authenticatedHeaders({ licenseKey, productId }) {
|
|
1093
|
+
return {
|
|
1094
|
+
Authorization: `Bearer ${licenseKey}`,
|
|
1095
|
+
"Content-Type": "application/json",
|
|
1096
|
+
"X-Product-ID": productId,
|
|
1097
|
+
"X-Client-Type": "tasksai-installer",
|
|
1098
|
+
"X-Client-Version": INSTALLER_VERSION
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
async function safeReportDoctorPassed({
|
|
1103
|
+
apiBase,
|
|
1104
|
+
licenseKey,
|
|
1105
|
+
productId,
|
|
1106
|
+
installId,
|
|
1107
|
+
client,
|
|
1108
|
+
requestJsonImpl = requestJson
|
|
1109
|
+
}) {
|
|
1110
|
+
if (!apiBase || !licenseKey || !productId) return false;
|
|
1111
|
+
const normalizedClient = String(client || "")
|
|
1112
|
+
.trim()
|
|
1113
|
+
.toLowerCase()
|
|
1114
|
+
.replaceAll("-", "_");
|
|
1115
|
+
const reportableClients = new Set([
|
|
1116
|
+
"claude_desktop", "cursor", "windsurf", "codex", "cline", "openclaw"
|
|
1117
|
+
]);
|
|
1118
|
+
const payload = {
|
|
1119
|
+
event_name: "doctor_passed",
|
|
1120
|
+
metadata: {
|
|
1121
|
+
source: "installer_doctor",
|
|
1122
|
+
tool_version: INSTALLER_VERSION
|
|
1123
|
+
}
|
|
1124
|
+
};
|
|
1125
|
+
if (reportableClients.has(normalizedClient)) {
|
|
1126
|
+
payload.metadata.client = normalizedClient;
|
|
1127
|
+
}
|
|
1128
|
+
if (installId) payload.install_id = installId;
|
|
1129
|
+
|
|
1130
|
+
try {
|
|
1131
|
+
await requestJsonImpl(`${apiBase}/v1/events/activation`, {
|
|
1132
|
+
method: "POST",
|
|
1133
|
+
body: payload,
|
|
1134
|
+
headers: authenticatedHeaders({ licenseKey, productId })
|
|
1135
|
+
});
|
|
1136
|
+
return true;
|
|
1137
|
+
} catch {
|
|
1138
|
+
return false;
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
async function verifyRuntimeHealth({ serverPath, vendorDir }) {
|
|
1143
|
+
const python = findPython();
|
|
1144
|
+
if (!python) throw new Error("Python 3 is unavailable");
|
|
1145
|
+
const pythonPath = [vendorDir, process.env.PYTHONPATH].filter(Boolean).join(path.delimiter);
|
|
1146
|
+
const check = [
|
|
1147
|
+
"import ast, pathlib, sys",
|
|
1148
|
+
"ast.parse(pathlib.Path(sys.argv[1]).read_text(encoding='utf-8'), filename=sys.argv[1])",
|
|
1149
|
+
"import httpx, dotenv, mcp, docx"
|
|
1150
|
+
].join("; ");
|
|
1151
|
+
const result = spawnSync(python, ["-c", check, serverPath], {
|
|
1152
|
+
encoding: "utf8",
|
|
1153
|
+
timeout: 10000,
|
|
1154
|
+
env: { ...process.env, PYTHONPATH: pythonPath }
|
|
1155
|
+
});
|
|
1156
|
+
if (result.error) throw result.error;
|
|
1157
|
+
if (result.status !== 0) {
|
|
1158
|
+
throw new Error("runtime syntax or Python dependencies are unavailable");
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
function parseEnv(text) {
|
|
1163
|
+
const values = {};
|
|
1164
|
+
for (const rawLine of String(text || "").split(/\r?\n/)) {
|
|
1165
|
+
const line = rawLine.trim();
|
|
1166
|
+
if (!line || line.startsWith("#")) continue;
|
|
1167
|
+
const match = line.match(/^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
|
|
1168
|
+
if (!match) continue;
|
|
1169
|
+
let value = match[2].trim();
|
|
1170
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
1171
|
+
value = value.slice(1, -1);
|
|
1172
|
+
}
|
|
1173
|
+
values[match[1]] = value;
|
|
1174
|
+
}
|
|
1175
|
+
return values;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
function firstValue(...values) {
|
|
1179
|
+
return values.find((value) => typeof value === "string" && value.trim())?.trim() || "";
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
function healthErrorSummary(error) {
|
|
1183
|
+
if (Number.isInteger(error?.statusCode)) return `HTTP ${error.statusCode}`;
|
|
1184
|
+
if (error?.code === "ETIMEDOUT") return "timeout";
|
|
1185
|
+
if (["ECONNREFUSED", "ENOTFOUND", "EAI_AGAIN", "ECONNRESET"].includes(error?.code)) {
|
|
1186
|
+
return "unreachable";
|
|
1187
|
+
}
|
|
1188
|
+
return "unavailable";
|
|
1189
|
+
}
|
|
1190
|
+
|
|
906
1191
|
function openExternalUrl(url) {
|
|
907
1192
|
const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
|
|
908
1193
|
const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
@@ -1034,3 +1319,16 @@ function redact(text) {
|
|
|
1034
1319
|
.replace(/Bearer\s+[A-Za-z0-9._-]+/gi, "Bearer [REDACTED]")
|
|
1035
1320
|
.replace(/gh[opsu]_[A-Za-z0-9_]+/g, "gh_[REDACTED]");
|
|
1036
1321
|
}
|
|
1322
|
+
|
|
1323
|
+
export {
|
|
1324
|
+
INSTALLER_VERSION,
|
|
1325
|
+
authenticatedHeaders,
|
|
1326
|
+
doctor,
|
|
1327
|
+
parseArgs,
|
|
1328
|
+
parseEnv,
|
|
1329
|
+
parseSource,
|
|
1330
|
+
safeReportDoctorPassed,
|
|
1331
|
+
verifyProductionSource,
|
|
1332
|
+
verifyRuntimeHealth,
|
|
1333
|
+
verifySource
|
|
1334
|
+
};
|