@tasksai/install 0.1.13 → 0.1.15

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
@@ -5,7 +5,8 @@ Official installer CLI for TasksAI MCP verticals.
5
5
  Users normally run this through a product-specific GitHub manifest, for example:
6
6
 
7
7
  ```bash
8
- npm exec --package=@tasksai/install --call 'tasksai-install lawtasksai --source https://github.com/laudoluxDev/lawtasksai-mcp'
8
+ npm exec --package=@tasksai/install --call 'tasksai-install lawtasksai --source https://github.com/TasksAI-Official/tasksai-mcp-wrappers/tree/main/verticals/lawtasksai'
9
+ npm exec --package=@tasksai/install --call 'tasksai-install realtor --source https://github.com/TasksAI-Official/tasksai-mcp-wrappers/tree/main/verticals/realtor'
9
10
  ```
10
11
 
11
12
  In restricted agent environments, grant write access to the default TasksAI
@@ -13,7 +14,8 @@ application data folder and the selected MCP client config. If the runtime must
13
14
  be installed elsewhere, pass an exact product install directory:
14
15
 
15
16
  ```bash
16
- npm exec --package=@tasksai/install --call 'tasksai-install farmer --source https://github.com/laudoluxDev/farmertasksai-mcp --install-dir /tmp/tasksai/farmer'
17
+ npm exec --package=@tasksai/install --call 'tasksai-install farmer --source https://github.com/TasksAI-Official/tasksai-mcp-wrappers/tree/main/verticals/farmer --install-dir /tmp/tasksai/farmer'
18
+ npm exec --package=@tasksai/install --call 'tasksai-install realtor --source https://github.com/TasksAI-Official/tasksai-mcp-wrappers/tree/main/verticals/realtor --install-dir /tmp/tasksai/realtor'
17
19
  ```
18
20
 
19
21
  `TASKSAI_INSTALL_DIR=/tmp/tasksai/farmer` is equivalent to `--install-dir`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tasksai/install",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "description": "Shared TasksAI MCP installer CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -18,12 +18,14 @@
18
18
  "tasksai",
19
19
  "lawtasksai",
20
20
  "farmertasksai",
21
- "farmer"
21
+ "farmer",
22
+ "realtortasksai",
23
+ "realtor"
22
24
  ],
23
25
  "license": "UNLICENSED",
24
26
  "repository": {
25
27
  "type": "git",
26
- "url": "git+https://github.com/laudoluxDev/tasksai-mcp.git",
28
+ "url": "git+https://github.com/TasksAI-Official/tasksai-mcp.git",
27
29
  "directory": "packages/installer"
28
30
  },
29
31
  "publishConfig": {
package/runtime/server.py CHANGED
@@ -25,6 +25,7 @@ import time
25
25
  import asyncio
26
26
  import platform
27
27
  import httpx
28
+ import uuid
28
29
  from datetime import datetime
29
30
  from pathlib import Path
30
31
 
@@ -86,6 +87,7 @@ else:
86
87
  API_BASE = os.getenv("TASKSAI_API_BASE", os.getenv("LAWTASKSAI_API_BASE", "https://api.lawtasksai.com"))
87
88
  LICENSE_KEY = os.getenv("TASKSAI_LICENSE_KEY", os.getenv("LAWTASKSAI_LICENSE_KEY", ""))
88
89
  PRODUCT_ID = os.getenv("TASKSAI_PRODUCT_ID", "") # set by installer; used to resolve correct vertical
90
+ INSTALL_ID = os.getenv("TASKSAI_INSTALL_ID", "")
89
91
 
90
92
  if not LICENSE_KEY:
91
93
  print("ERROR: License key is required. Set TASKSAI_LICENSE_KEY in your .env file.", file=sys.stderr, flush=True)
@@ -473,6 +475,51 @@ async def api_get(path):
473
475
  return resp.json()
474
476
 
475
477
 
478
+ async def api_post(path, payload):
479
+ async with httpx.AsyncClient(timeout=10.0) as client:
480
+ resp = await client.post(
481
+ f"{API_BASE}{path}",
482
+ json=payload,
483
+ headers={**AUTH_HEADERS, "X-Product-ID": (_vertical or {}).get("product_id", "law")}
484
+ )
485
+ resp.raise_for_status()
486
+ return resp.json()
487
+
488
+
489
+ def get_install_id() -> str:
490
+ """Stable anonymous install id for attribution; stored locally only."""
491
+ global INSTALL_ID
492
+ if INSTALL_ID:
493
+ return INSTALL_ID
494
+ env_path = Path(_dotenv_path) if _dotenv_path else None
495
+ INSTALL_ID = str(uuid.uuid4())
496
+ if env_path:
497
+ try:
498
+ with env_path.open("a", encoding="utf-8") as f:
499
+ f.write(f"\nTASKSAI_INSTALL_ID={INSTALL_ID}\n")
500
+ except Exception:
501
+ pass
502
+ return INSTALL_ID
503
+
504
+
505
+ async def report_activation_event(event_name, *, skill_id=None, file_format=None):
506
+ """Best-effort activation telemetry. Never sends document content or paths."""
507
+ try:
508
+ await api_post("/v1/events/activation", {
509
+ "event_name": event_name,
510
+ "skill_id": skill_id,
511
+ "file_format": file_format,
512
+ "install_id": get_install_id(),
513
+ "metadata": {
514
+ "source": "mcp_runtime",
515
+ "client": "mcp-server",
516
+ "tool_version": SERVER_VERSION,
517
+ },
518
+ })
519
+ except Exception:
520
+ pass
521
+
522
+
476
523
  async def load_vertical():
477
524
  """Fetch vertical metadata from /v1/me on startup. Falls back to farmer."""
478
525
  global _vertical
@@ -1270,6 +1317,11 @@ async def call_tool(name, arguments):
1270
1317
  # ── Save Document ───────────────────────────────────────────────────
1271
1318
  elif name == f"{prefix}_save_document":
1272
1319
  output_path, fmt = save_document(arguments or {}, product_name)
1320
+ await report_activation_event(
1321
+ "first_file_generated",
1322
+ skill_id=(arguments or {}).get("skill_id"),
1323
+ file_format=fmt,
1324
+ )
1273
1325
  return [TextContent(type="text", text=(
1274
1326
  "**Document Saved**\n\n"
1275
1327
  f"- File: `{output_path}`\n"
package/src/index.js CHANGED
@@ -10,15 +10,21 @@ import { spawnSync } from "node:child_process";
10
10
  import { stdin as input, stdout as output } from "node:process";
11
11
  import { fileURLToPath } from "node:url";
12
12
 
13
- const INSTALLER_VERSION = "0.1.9";
13
+ const INSTALLER_VERSION = "0.1.15";
14
14
  const INSTALLER_DIR = path.dirname(fileURLToPath(import.meta.url));
15
15
  const BUNDLED_RUNTIME_DIR = path.resolve(INSTALLER_DIR, "..", "runtime");
16
16
  const DEFAULT_SOURCES = {
17
- lawtasksai: "https://github.com/laudoluxDev/lawtasksai-mcp",
18
- farmer: "https://github.com/laudoluxDev/farmertasksai-mcp",
19
- teacher: "https://github.com/laudoluxDev/teachertasksai-mcp",
17
+ lawtasksai: "https://github.com/TasksAI-Official/tasksai-mcp-wrappers/tree/main/verticals/lawtasksai",
18
+ farmer: "https://github.com/TasksAI-Official/tasksai-mcp-wrappers/tree/main/verticals/farmer",
19
+ realtor: "https://github.com/TasksAI-Official/tasksai-mcp-wrappers/tree/main/verticals/realtor",
20
+ teacher: "https://github.com/TasksAI-Official/tasksai-mcp-wrappers/tree/main/verticals/teacher",
20
21
  priorauthai: "https://github.com/laudoluxDev/priorauthai-mcp"
21
22
  };
23
+ const LEGACY_MCP_SERVER_IDS = {
24
+ farmer: ["farmertasksai"],
25
+ realtor: ["realtortasksai"],
26
+ teacher: ["teachertasksai"]
27
+ };
22
28
 
23
29
  const CLIENTS = {
24
30
  "claude-desktop": {
@@ -147,8 +153,9 @@ function printUsage() {
147
153
  tasksai-install <product-id> uninstall [--client claude-desktop|cursor|windsurf|codex|all] [--install-dir <path>]
148
154
 
149
155
  Examples:
150
- tasksai-install lawtasksai --source https://github.com/laudoluxDev/lawtasksai-mcp
151
- tasksai-install farmer --source https://github.com/laudoluxDev/farmertasksai-mcp
156
+ tasksai-install lawtasksai --source https://github.com/TasksAI-Official/tasksai-mcp-wrappers/tree/main/verticals/lawtasksai
157
+ tasksai-install farmer --source https://github.com/TasksAI-Official/tasksai-mcp-wrappers/tree/main/verticals/farmer
158
+ tasksai-install realtor --source https://github.com/TasksAI-Official/tasksai-mcp-wrappers/tree/main/verticals/realtor
152
159
  tasksai-install farmer --install-dir /tmp/tasksai/farmer
153
160
  tasksai-install lawtasksai doctor
154
161
 
@@ -266,23 +273,23 @@ async function uninstall(options) {
266
273
  }
267
274
 
268
275
  await withLock(`${configPath}.lock`, async () => {
269
- const key = options.productId;
276
+ const keys = mcpServerKeysForProduct(options.productId);
270
277
  if (client.configFormat === "toml") {
271
278
  const text = await fsp.readFile(configPath, "utf8");
272
- if (!tomlHasMcpServer(text, key)) {
273
- console.log(`${client.displayName} does not have a ${key} MCP entry.`);
279
+ if (!tomlHasAnyMcpServer(text, keys)) {
280
+ console.log(`${client.displayName} does not have a ${options.productId} MCP entry.`);
274
281
  return;
275
282
  }
276
283
  await backupFile(configPath);
277
- await atomicWriteText(configPath, removeTomlSections(text, [`mcp_servers.${key}`, `mcp_servers.${key}.env`]));
284
+ await atomicWriteText(configPath, removeTomlSections(text, tomlSectionNamesForProduct(options.productId)));
278
285
  } else {
279
286
  const config = await readJson(configPath);
280
- if (!config.mcpServers?.[key]) {
281
- console.log(`${client.displayName} does not have a ${key} MCP entry.`);
287
+ if (!keys.some((key) => config.mcpServers?.[key])) {
288
+ console.log(`${client.displayName} does not have a ${options.productId} MCP entry.`);
282
289
  return;
283
290
  }
284
291
  await backupFile(configPath);
285
- delete config.mcpServers[key];
292
+ for (const key of keys) delete config.mcpServers[key];
286
293
  await atomicWriteJson(configPath, config);
287
294
  }
288
295
  });
@@ -311,6 +318,7 @@ async function configureMcpClient({ client, vertical, installDir, runtimeDir, ve
311
318
 
312
319
  const config = fs.existsSync(configPath) ? await readJson(configPath) : {};
313
320
  if (!config.mcpServers || typeof config.mcpServers !== "object") config.mcpServers = {};
321
+ for (const key of mcpServerKeysForProduct(vertical.product_id)) delete config.mcpServers[key];
314
322
  config.mcpServers[vertical.product_id] = serverConfig;
315
323
  await atomicWriteJson(configPath, config);
316
324
  });
@@ -344,6 +352,11 @@ function tomlHasMcpServer(text, productId) {
344
352
  return getTomlSectionNames(text).includes(`mcp_servers.${productId}`);
345
353
  }
346
354
 
355
+ function tomlHasAnyMcpServer(text, productIds) {
356
+ const sections = new Set(getTomlSectionNames(text));
357
+ return productIds.some((productId) => sections.has(`mcp_servers.${productId}`));
358
+ }
359
+
347
360
  function getTomlSectionNames(text) {
348
361
  return text
349
362
  .split(/\r?\n/)
@@ -355,11 +368,19 @@ function getTomlSectionNames(text) {
355
368
  }
356
369
 
357
370
  function upsertCodexMcpServer(text, productId, serverConfig) {
358
- const withoutServer = removeTomlSections(text, [`mcp_servers.${productId}`, `mcp_servers.${productId}.env`]).trimEnd();
371
+ const withoutServer = removeTomlSections(text, tomlSectionNamesForProduct(productId)).trimEnd();
359
372
  const block = formatCodexMcpServer(productId, serverConfig);
360
373
  return `${withoutServer ? `${withoutServer}\n\n` : ""}${block}\n`;
361
374
  }
362
375
 
376
+ function mcpServerKeysForProduct(productId) {
377
+ return [...new Set([productId, ...(LEGACY_MCP_SERVER_IDS[productId] || [])])];
378
+ }
379
+
380
+ function tomlSectionNamesForProduct(productId) {
381
+ return mcpServerKeysForProduct(productId).flatMap((key) => [`mcp_servers.${key}`, `mcp_servers.${key}.env`]);
382
+ }
383
+
363
384
  function removeTomlSections(text, sectionNames) {
364
385
  const sections = new Set(sectionNames);
365
386
  const lines = text.split(/\r?\n/);
@@ -714,6 +735,12 @@ function verifySource({ options, source, manifest, vertical }) {
714
735
  if (source.kind === "github" && manifest.official_github_repo !== source.repoUrl) {
715
736
  throw new Error(`Source repo ${source.repoUrl} does not match manifest official repo ${manifest.official_github_repo}.`);
716
737
  }
738
+ if (source.kind === "github" && manifest.official_manifest_path) {
739
+ const expectedManifestPath = joinGitPath(source.basePath, "agent-install.json");
740
+ if (normalizeGitPath(manifest.official_manifest_path) !== expectedManifestPath) {
741
+ throw new Error(`Source manifest ${expectedManifestPath} does not match manifest official path ${manifest.official_manifest_path}.`);
742
+ }
743
+ }
717
744
  if (!manifest.official_domain || !vertical.website_url?.includes(manifest.official_domain)) {
718
745
  throw new Error("Manifest official domain does not match vertical website URL.");
719
746
  }
@@ -740,6 +767,37 @@ function parseSource(source, ref) {
740
767
  if (source.startsWith("/") || source.startsWith(".")) {
741
768
  return { kind: "file", root: path.resolve(source), ref, repoUrl: source };
742
769
  }
770
+ const rawMatch = source.match(/^https:\/\/raw\.githubusercontent\.com\/([^/]+)\/([^/]+)\/([^/]+)\/(.+)$/);
771
+ if (rawMatch) {
772
+ const [, owner, repo, rawRef, rawPath] = rawMatch;
773
+ const manifestPath = normalizeGitPath(rawPath);
774
+ const basePath = manifestPath.endsWith("/agent-install.json")
775
+ ? manifestPath.slice(0, -"/agent-install.json".length)
776
+ : path.posix.dirname(manifestPath);
777
+ return {
778
+ kind: "github",
779
+ owner,
780
+ repo,
781
+ ref: rawRef,
782
+ basePath: basePath === "." ? "" : basePath,
783
+ repoUrl: `https://github.com/${owner}/${repo}`
784
+ };
785
+ }
786
+ const treeMatch = source.match(/^https:\/\/github\.com\/([^/]+)\/([^/#?]+)\/(?:tree|blob)\/([^/]+)(?:\/([^#?]+))?(?:[?#].*)?$/);
787
+ if (treeMatch) {
788
+ const [, owner, repo, treeRef, treePath = ""] = treeMatch;
789
+ const normalizedPath = normalizeGitPath(treePath);
790
+ return {
791
+ kind: "github",
792
+ owner,
793
+ repo,
794
+ ref: treeRef,
795
+ basePath: normalizedPath.endsWith("/agent-install.json")
796
+ ? normalizedPath.slice(0, -"/agent-install.json".length)
797
+ : normalizedPath,
798
+ repoUrl: `https://github.com/${owner}/${repo}`
799
+ };
800
+ }
743
801
  const match = source.match(/^https:\/\/github\.com\/([^/]+)\/([^/#?]+)(?:[/?#].*)?$/);
744
802
  if (!match) throw new Error(`Unsupported source URL: ${source}`);
745
803
  const [, owner, repo] = match;
@@ -748,6 +806,7 @@ function parseSource(source, ref) {
748
806
  owner,
749
807
  repo,
750
808
  ref,
809
+ basePath: "",
751
810
  repoUrl: `https://github.com/${owner}/${repo}`
752
811
  };
753
812
  }
@@ -760,10 +819,27 @@ async function loadText(source, filePath) {
760
819
  if (source.kind === "file") {
761
820
  return fsp.readFile(path.join(source.root, filePath), "utf8");
762
821
  }
763
- const rawUrl = `https://raw.githubusercontent.com/${source.owner}/${source.repo}/${encodeURIComponent(source.ref)}/${filePath}`;
822
+ const sourcePath = joinGitPath(source.basePath, filePath);
823
+ const rawUrl = `https://raw.githubusercontent.com/${source.owner}/${source.repo}/${encodeURIComponent(source.ref)}/${encodeGitPath(sourcePath)}`;
764
824
  return fetchText(rawUrl);
765
825
  }
766
826
 
827
+ function normalizeGitPath(value = "") {
828
+ return value
829
+ .split("/")
830
+ .filter(Boolean)
831
+ .map((part) => decodeURIComponent(part))
832
+ .join("/");
833
+ }
834
+
835
+ function joinGitPath(...parts) {
836
+ return normalizeGitPath(parts.filter(Boolean).join("/"));
837
+ }
838
+
839
+ function encodeGitPath(value) {
840
+ return normalizeGitPath(value).split("/").map(encodeURIComponent).join("/");
841
+ }
842
+
767
843
  function fetchText(url) {
768
844
  return new Promise((resolve, reject) => {
769
845
  https.get(url, { headers: { "User-Agent": `tasksai-install/${INSTALLER_VERSION}` } }, (response) => {