@tasksai/install 0.1.1 → 0.1.3
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 +13 -0
- package/package.json +4 -2
- package/src/index.js +126 -12
package/README.md
CHANGED
|
@@ -8,6 +8,18 @@ Users normally run this through a product-specific GitHub manifest, for example:
|
|
|
8
8
|
npx @tasksai/install lawtasksai --source https://github.com/laudoluxDev/lawtasksai-mcp
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
+
In restricted agent environments, grant write access to the default TasksAI
|
|
12
|
+
application data folder and the selected MCP client config. If the runtime must
|
|
13
|
+
be installed elsewhere, pass an exact product install directory:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npx @tasksai/install farmer \
|
|
17
|
+
--source https://github.com/laudoluxDev/farmertasksai-mcp \
|
|
18
|
+
--install-dir /tmp/tasksai/farmer
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`TASKSAI_INSTALL_DIR=/tmp/tasksai/farmer` is equivalent to `--install-dir`.
|
|
22
|
+
|
|
11
23
|
The installer:
|
|
12
24
|
|
|
13
25
|
- verifies the official vertical manifest
|
|
@@ -15,6 +27,7 @@ The installer:
|
|
|
15
27
|
- connects the user's TasksAI account through browser approval when available
|
|
16
28
|
- stores credentials outside MCP client config
|
|
17
29
|
- configures supported MCP clients
|
|
30
|
+
- checks required write access before changing local files
|
|
18
31
|
- runs a local health check
|
|
19
32
|
|
|
20
33
|
TasksAI servers handle authentication, credits, catalog/search metadata, and
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tasksai/install",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Shared TasksAI MCP installer CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
"mcp",
|
|
15
15
|
"installer",
|
|
16
16
|
"tasksai",
|
|
17
|
-
"lawtasksai"
|
|
17
|
+
"lawtasksai",
|
|
18
|
+
"farmertasksai",
|
|
19
|
+
"farmer"
|
|
18
20
|
],
|
|
19
21
|
"license": "UNLICENSED",
|
|
20
22
|
"repository": {
|
package/src/index.js
CHANGED
|
@@ -9,9 +9,10 @@ import readline from "node:readline/promises";
|
|
|
9
9
|
import { spawnSync } from "node:child_process";
|
|
10
10
|
import { stdin as input, stdout as output } from "node:process";
|
|
11
11
|
|
|
12
|
-
const INSTALLER_VERSION = "0.1.
|
|
12
|
+
const INSTALLER_VERSION = "0.1.3";
|
|
13
13
|
const DEFAULT_SOURCES = {
|
|
14
14
|
lawtasksai: "https://github.com/laudoluxDev/lawtasksai-mcp",
|
|
15
|
+
farmer: "https://github.com/laudoluxDev/farmertasksai-mcp",
|
|
15
16
|
priorauthai: "https://github.com/laudoluxDev/priorauthai-mcp"
|
|
16
17
|
};
|
|
17
18
|
|
|
@@ -84,6 +85,7 @@ function parseArgs(argv) {
|
|
|
84
85
|
ref: "main",
|
|
85
86
|
client: "claude-desktop",
|
|
86
87
|
auth: "browser",
|
|
88
|
+
installDir: process.env.TASKSAI_INSTALL_DIR || null,
|
|
87
89
|
noBrowser: false,
|
|
88
90
|
yes: false,
|
|
89
91
|
skipPythonDeps: false
|
|
@@ -96,6 +98,7 @@ function parseArgs(argv) {
|
|
|
96
98
|
else if (arg === "--ref") options.ref = argv[++i];
|
|
97
99
|
else if (arg === "--client") options.client = argv[++i];
|
|
98
100
|
else if (arg === "--auth") options.auth = argv[++i];
|
|
101
|
+
else if (arg === "--install-dir") options.installDir = argv[++i];
|
|
99
102
|
else if (arg === "--no-browser") options.noBrowser = true;
|
|
100
103
|
else if (arg === "--yes" || arg === "-y") options.yes = true;
|
|
101
104
|
else if (arg === "--skip-python-deps") options.skipPythonDeps = true;
|
|
@@ -118,7 +121,7 @@ function parseArgs(argv) {
|
|
|
118
121
|
throw new Error(`Unsupported auth mode: ${options.auth}. Supported modes: browser, license-key`);
|
|
119
122
|
}
|
|
120
123
|
options.source ||= DEFAULT_SOURCES[options.productId];
|
|
121
|
-
if (!options.source) {
|
|
124
|
+
if (!options.source && ["install", "update"].includes(options.command)) {
|
|
122
125
|
throw new Error(`No default source is known for product: ${options.productId}`);
|
|
123
126
|
}
|
|
124
127
|
return options;
|
|
@@ -126,14 +129,19 @@ function parseArgs(argv) {
|
|
|
126
129
|
|
|
127
130
|
function printUsage() {
|
|
128
131
|
console.log(`Usage:
|
|
129
|
-
tasksai-install <product-id> [install] [--source <repo-url>] [--ref <branch>] [--client claude-desktop|cursor|windsurf|all] [--auth browser|license-key]
|
|
130
|
-
tasksai-install <product-id> doctor [--client claude-desktop|cursor|windsurf|all]
|
|
131
|
-
tasksai-install <product-id> update
|
|
132
|
-
tasksai-install <product-id> uninstall [--client claude-desktop|cursor|windsurf|all]
|
|
132
|
+
tasksai-install <product-id> [install] [--source <repo-url>] [--ref <branch>] [--client claude-desktop|cursor|windsurf|all] [--auth browser|license-key] [--install-dir <path>]
|
|
133
|
+
tasksai-install <product-id> doctor [--client claude-desktop|cursor|windsurf|all] [--install-dir <path>]
|
|
134
|
+
tasksai-install <product-id> update [--install-dir <path>]
|
|
135
|
+
tasksai-install <product-id> uninstall [--client claude-desktop|cursor|windsurf|all] [--install-dir <path>]
|
|
133
136
|
|
|
134
137
|
Examples:
|
|
135
138
|
tasksai-install lawtasksai --source https://github.com/laudoluxDev/lawtasksai-mcp
|
|
139
|
+
tasksai-install farmer --source https://github.com/laudoluxDev/farmertasksai-mcp
|
|
140
|
+
tasksai-install farmer --install-dir /tmp/tasksai/farmer
|
|
136
141
|
tasksai-install lawtasksai doctor
|
|
142
|
+
|
|
143
|
+
Environment:
|
|
144
|
+
TASKSAI_INSTALL_DIR may be used instead of --install-dir.
|
|
137
145
|
`);
|
|
138
146
|
}
|
|
139
147
|
|
|
@@ -143,9 +151,12 @@ async function install(options, { updateOnly = false } = {}) {
|
|
|
143
151
|
const vertical = await loadJson(source, "vertical.json");
|
|
144
152
|
verifySource({ options, source, manifest, vertical });
|
|
145
153
|
|
|
146
|
-
const installDir = getInstallDir(options.productId);
|
|
154
|
+
const installDir = getInstallDir(options.productId, options);
|
|
147
155
|
const runtimeDir = path.join(installDir, "runtime");
|
|
148
156
|
const vendorDir = path.join(installDir, "python");
|
|
157
|
+
const clients = updateOnly ? [] : resolveClients(options.client);
|
|
158
|
+
await preflightWriteAccess({ operation: updateOnly ? "update" : "install", installDir, clients });
|
|
159
|
+
|
|
149
160
|
await fsp.mkdir(runtimeDir, { recursive: true });
|
|
150
161
|
await fsp.mkdir(path.join(installDir, "logs"), { recursive: true });
|
|
151
162
|
await logEvent(installDir, "install_start", {
|
|
@@ -180,7 +191,6 @@ async function install(options, { updateOnly = false } = {}) {
|
|
|
180
191
|
return;
|
|
181
192
|
}
|
|
182
193
|
|
|
183
|
-
const clients = resolveClients(options.client);
|
|
184
194
|
for (const client of clients) {
|
|
185
195
|
await configureMcpClient({ client, vertical, installDir, runtimeDir, vendorDir });
|
|
186
196
|
}
|
|
@@ -195,7 +205,7 @@ async function install(options, { updateOnly = false } = {}) {
|
|
|
195
205
|
}
|
|
196
206
|
|
|
197
207
|
async function doctor(options, { quietSuccess = false } = {}) {
|
|
198
|
-
const installDir = getInstallDir(options.productId);
|
|
208
|
+
const installDir = getInstallDir(options.productId, options);
|
|
199
209
|
const verticalPath = path.join(installDir, "vertical.json");
|
|
200
210
|
const envPath = path.join(installDir, ".env");
|
|
201
211
|
const serverPath = path.join(installDir, "runtime", "server.py");
|
|
@@ -223,7 +233,7 @@ async function doctor(options, { quietSuccess = false } = {}) {
|
|
|
223
233
|
throw new Error(`TasksAI doctor found issues:\n- ${problems.join("\n- ")}`);
|
|
224
234
|
}
|
|
225
235
|
|
|
226
|
-
await
|
|
236
|
+
await safeLogEvent(installDir, "doctor_passed", {
|
|
227
237
|
productId: options.productId,
|
|
228
238
|
clients: clients.map((client) => client.id)
|
|
229
239
|
});
|
|
@@ -231,8 +241,9 @@ async function doctor(options, { quietSuccess = false } = {}) {
|
|
|
231
241
|
}
|
|
232
242
|
|
|
233
243
|
async function uninstall(options) {
|
|
234
|
-
const installDir = getInstallDir(options.productId);
|
|
244
|
+
const installDir = getInstallDir(options.productId, options);
|
|
235
245
|
const clients = resolveClients(options.client);
|
|
246
|
+
await preflightWriteAccess({ operation: "uninstall", installDir, clients });
|
|
236
247
|
|
|
237
248
|
for (const client of clients) {
|
|
238
249
|
const configPath = client.configPath();
|
|
@@ -298,6 +309,79 @@ function resolveClients(clientOption) {
|
|
|
298
309
|
return [client];
|
|
299
310
|
}
|
|
300
311
|
|
|
312
|
+
async function preflightWriteAccess({ operation, installDir, clients }) {
|
|
313
|
+
const checks = [
|
|
314
|
+
{
|
|
315
|
+
label: "TasksAI install directory",
|
|
316
|
+
targetPath: installDir,
|
|
317
|
+
kind: "directory"
|
|
318
|
+
}
|
|
319
|
+
];
|
|
320
|
+
|
|
321
|
+
for (const client of clients) {
|
|
322
|
+
const configPath = client.configPath();
|
|
323
|
+
checks.push({
|
|
324
|
+
label: `${client.displayName} config directory`,
|
|
325
|
+
targetPath: path.dirname(configPath),
|
|
326
|
+
kind: "directory"
|
|
327
|
+
});
|
|
328
|
+
if (fs.existsSync(configPath)) {
|
|
329
|
+
checks.push({
|
|
330
|
+
label: `${client.displayName} config file`,
|
|
331
|
+
targetPath: configPath,
|
|
332
|
+
kind: "file"
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const failures = [];
|
|
338
|
+
for (const check of checks) {
|
|
339
|
+
const problem = await checkWritable(check);
|
|
340
|
+
if (problem) failures.push(problem);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (!failures.length) return;
|
|
344
|
+
|
|
345
|
+
const detail = failures
|
|
346
|
+
.map((failure) => `- ${failure.label}: ${failure.targetPath} (${failure.reason})`)
|
|
347
|
+
.join("\n");
|
|
348
|
+
throw new Error(
|
|
349
|
+
`TasksAI cannot ${operation} until it has write access to the required local paths:\n${detail}\n` +
|
|
350
|
+
"Grant write access to these paths, or use --install-dir /path/to/product-dir for the TasksAI runtime."
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async function checkWritable({ label, targetPath, kind }) {
|
|
355
|
+
const resolvedPath = resolveUserPath(targetPath);
|
|
356
|
+
try {
|
|
357
|
+
if (fs.existsSync(resolvedPath)) {
|
|
358
|
+
const stat = await fsp.stat(resolvedPath);
|
|
359
|
+
if (kind === "directory" && !stat.isDirectory()) {
|
|
360
|
+
return { label, targetPath: resolvedPath, reason: "expected a directory" };
|
|
361
|
+
}
|
|
362
|
+
if (kind === "file" && !stat.isFile()) {
|
|
363
|
+
return { label, targetPath: resolvedPath, reason: "expected a file" };
|
|
364
|
+
}
|
|
365
|
+
await fsp.access(resolvedPath, fs.constants.W_OK);
|
|
366
|
+
if (kind === "file") {
|
|
367
|
+
await fsp.access(path.dirname(resolvedPath), fs.constants.W_OK);
|
|
368
|
+
}
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const basePath = kind === "file" ? path.dirname(resolvedPath) : resolvedPath;
|
|
373
|
+
const ancestor = findExistingAncestor(basePath);
|
|
374
|
+
await fsp.access(ancestor, fs.constants.W_OK);
|
|
375
|
+
return null;
|
|
376
|
+
} catch (error) {
|
|
377
|
+
return {
|
|
378
|
+
label,
|
|
379
|
+
targetPath: resolvedPath,
|
|
380
|
+
reason: error.code === "EACCES" || error.code === "EPERM" ? "permission denied" : error.message
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
301
385
|
async function downloadRuntime(source, runtimeDir) {
|
|
302
386
|
const serverText = await loadText(source, "server.py");
|
|
303
387
|
const requirementsText = await loadText(source, "requirements.txt");
|
|
@@ -551,7 +635,10 @@ function delay(ms) {
|
|
|
551
635
|
});
|
|
552
636
|
}
|
|
553
637
|
|
|
554
|
-
function getInstallDir(productId) {
|
|
638
|
+
function getInstallDir(productId, options = {}) {
|
|
639
|
+
if (options.installDir) {
|
|
640
|
+
return resolveUserPath(options.installDir);
|
|
641
|
+
}
|
|
555
642
|
if (process.platform === "darwin") {
|
|
556
643
|
return path.join(os.homedir(), "Library", "Application Support", "TasksAI", productId);
|
|
557
644
|
}
|
|
@@ -561,6 +648,25 @@ function getInstallDir(productId) {
|
|
|
561
648
|
return path.join(os.homedir(), ".local", "share", "TasksAI", productId);
|
|
562
649
|
}
|
|
563
650
|
|
|
651
|
+
function resolveUserPath(value) {
|
|
652
|
+
if (!value) return value;
|
|
653
|
+
if (value === "~") return os.homedir();
|
|
654
|
+
if (value.startsWith("~/") || value.startsWith("~\\")) {
|
|
655
|
+
return path.join(os.homedir(), value.slice(2));
|
|
656
|
+
}
|
|
657
|
+
return path.resolve(value);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function findExistingAncestor(targetPath) {
|
|
661
|
+
let current = resolveUserPath(targetPath);
|
|
662
|
+
while (!fs.existsSync(current)) {
|
|
663
|
+
const parent = path.dirname(current);
|
|
664
|
+
if (parent === current) return current;
|
|
665
|
+
current = parent;
|
|
666
|
+
}
|
|
667
|
+
return current;
|
|
668
|
+
}
|
|
669
|
+
|
|
564
670
|
async function withLock(lockPath, callback) {
|
|
565
671
|
let handle;
|
|
566
672
|
try {
|
|
@@ -625,6 +731,14 @@ async function logEvent(installDir, event, details = {}) {
|
|
|
625
731
|
await fsp.appendFile(path.join(logDir, "installer.log"), line, "utf8");
|
|
626
732
|
}
|
|
627
733
|
|
|
734
|
+
async function safeLogEvent(installDir, event, details = {}) {
|
|
735
|
+
try {
|
|
736
|
+
await logEvent(installDir, event, details);
|
|
737
|
+
} catch {
|
|
738
|
+
// Health checks should report installation health, not fail on log writes.
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
628
742
|
function formatError(error) {
|
|
629
743
|
const message = error?.message || String(error);
|
|
630
744
|
return `TasksAI installer failed: ${redact(message)}`;
|