@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 +20 -1
- package/package.json +2 -2
- package/runtime/server.py +38 -12
- package/src/index.js +311 -19
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.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
|
|
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
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
"
|
|
536
|
-
"
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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, {
|
|
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
|
|
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
|
|
258
|
-
clients: clients.map((client) => client.id)
|
|
375
|
+
productId,
|
|
376
|
+
clients: clients.map((client) => client.id),
|
|
377
|
+
activationReported
|
|
259
378
|
});
|
|
260
|
-
if (!quietSuccess)
|
|
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
|
|
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
|
|
1034
|
+
method,
|
|
870
1035
|
headers: {
|
|
871
|
-
"
|
|
872
|
-
|
|
873
|
-
|
|
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
|
+
};
|