@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.
Files changed (3) hide show
  1. package/README.md +13 -0
  2. package/package.json +4 -2
  3. 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.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.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 logEvent(installDir, "doctor_passed", {
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)}`;