@tasksai/install 0.1.12 → 0.1.14

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.12",
3
+ "version": "0.1.14",
4
4
  "description": "Shared TasksAI MCP installer CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,7 +8,8 @@
8
8
  },
9
9
  "files": [
10
10
  "src/",
11
- "runtime/",
11
+ "runtime/server.py",
12
+ "runtime/requirements.txt",
12
13
  "README.md"
13
14
  ],
14
15
  "keywords": [
@@ -17,12 +18,14 @@
17
18
  "tasksai",
18
19
  "lawtasksai",
19
20
  "farmertasksai",
20
- "farmer"
21
+ "farmer",
22
+ "realtortasksai",
23
+ "realtor"
21
24
  ],
22
25
  "license": "UNLICENSED",
23
26
  "repository": {
24
27
  "type": "git",
25
- "url": "git+https://github.com/laudoluxDev/tasksai-mcp.git",
28
+ "url": "git+https://github.com/TasksAI-Official/tasksai-mcp.git",
26
29
  "directory": "packages/installer"
27
30
  },
28
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,13 +10,14 @@ 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.14";
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
  };
22
23
 
@@ -147,8 +148,9 @@ function printUsage() {
147
148
  tasksai-install <product-id> uninstall [--client claude-desktop|cursor|windsurf|codex|all] [--install-dir <path>]
148
149
 
149
150
  Examples:
150
- tasksai-install lawtasksai --source https://github.com/laudoluxDev/lawtasksai-mcp
151
- tasksai-install farmer --source https://github.com/laudoluxDev/farmertasksai-mcp
151
+ tasksai-install lawtasksai --source https://github.com/TasksAI-Official/tasksai-mcp-wrappers/tree/main/verticals/lawtasksai
152
+ tasksai-install farmer --source https://github.com/TasksAI-Official/tasksai-mcp-wrappers/tree/main/verticals/farmer
153
+ tasksai-install realtor --source https://github.com/TasksAI-Official/tasksai-mcp-wrappers/tree/main/verticals/realtor
152
154
  tasksai-install farmer --install-dir /tmp/tasksai/farmer
153
155
  tasksai-install lawtasksai doctor
154
156
 
@@ -714,6 +716,12 @@ function verifySource({ options, source, manifest, vertical }) {
714
716
  if (source.kind === "github" && manifest.official_github_repo !== source.repoUrl) {
715
717
  throw new Error(`Source repo ${source.repoUrl} does not match manifest official repo ${manifest.official_github_repo}.`);
716
718
  }
719
+ if (source.kind === "github" && manifest.official_manifest_path) {
720
+ const expectedManifestPath = joinGitPath(source.basePath, "agent-install.json");
721
+ if (normalizeGitPath(manifest.official_manifest_path) !== expectedManifestPath) {
722
+ throw new Error(`Source manifest ${expectedManifestPath} does not match manifest official path ${manifest.official_manifest_path}.`);
723
+ }
724
+ }
717
725
  if (!manifest.official_domain || !vertical.website_url?.includes(manifest.official_domain)) {
718
726
  throw new Error("Manifest official domain does not match vertical website URL.");
719
727
  }
@@ -740,6 +748,37 @@ function parseSource(source, ref) {
740
748
  if (source.startsWith("/") || source.startsWith(".")) {
741
749
  return { kind: "file", root: path.resolve(source), ref, repoUrl: source };
742
750
  }
751
+ const rawMatch = source.match(/^https:\/\/raw\.githubusercontent\.com\/([^/]+)\/([^/]+)\/([^/]+)\/(.+)$/);
752
+ if (rawMatch) {
753
+ const [, owner, repo, rawRef, rawPath] = rawMatch;
754
+ const manifestPath = normalizeGitPath(rawPath);
755
+ const basePath = manifestPath.endsWith("/agent-install.json")
756
+ ? manifestPath.slice(0, -"/agent-install.json".length)
757
+ : path.posix.dirname(manifestPath);
758
+ return {
759
+ kind: "github",
760
+ owner,
761
+ repo,
762
+ ref: rawRef,
763
+ basePath: basePath === "." ? "" : basePath,
764
+ repoUrl: `https://github.com/${owner}/${repo}`
765
+ };
766
+ }
767
+ const treeMatch = source.match(/^https:\/\/github\.com\/([^/]+)\/([^/#?]+)\/(?:tree|blob)\/([^/]+)(?:\/([^#?]+))?(?:[?#].*)?$/);
768
+ if (treeMatch) {
769
+ const [, owner, repo, treeRef, treePath = ""] = treeMatch;
770
+ const normalizedPath = normalizeGitPath(treePath);
771
+ return {
772
+ kind: "github",
773
+ owner,
774
+ repo,
775
+ ref: treeRef,
776
+ basePath: normalizedPath.endsWith("/agent-install.json")
777
+ ? normalizedPath.slice(0, -"/agent-install.json".length)
778
+ : normalizedPath,
779
+ repoUrl: `https://github.com/${owner}/${repo}`
780
+ };
781
+ }
743
782
  const match = source.match(/^https:\/\/github\.com\/([^/]+)\/([^/#?]+)(?:[/?#].*)?$/);
744
783
  if (!match) throw new Error(`Unsupported source URL: ${source}`);
745
784
  const [, owner, repo] = match;
@@ -748,6 +787,7 @@ function parseSource(source, ref) {
748
787
  owner,
749
788
  repo,
750
789
  ref,
790
+ basePath: "",
751
791
  repoUrl: `https://github.com/${owner}/${repo}`
752
792
  };
753
793
  }
@@ -760,10 +800,27 @@ async function loadText(source, filePath) {
760
800
  if (source.kind === "file") {
761
801
  return fsp.readFile(path.join(source.root, filePath), "utf8");
762
802
  }
763
- const rawUrl = `https://raw.githubusercontent.com/${source.owner}/${source.repo}/${encodeURIComponent(source.ref)}/${filePath}`;
803
+ const sourcePath = joinGitPath(source.basePath, filePath);
804
+ const rawUrl = `https://raw.githubusercontent.com/${source.owner}/${source.repo}/${encodeURIComponent(source.ref)}/${encodeGitPath(sourcePath)}`;
764
805
  return fetchText(rawUrl);
765
806
  }
766
807
 
808
+ function normalizeGitPath(value = "") {
809
+ return value
810
+ .split("/")
811
+ .filter(Boolean)
812
+ .map((part) => decodeURIComponent(part))
813
+ .join("/");
814
+ }
815
+
816
+ function joinGitPath(...parts) {
817
+ return normalizeGitPath(parts.filter(Boolean).join("/"));
818
+ }
819
+
820
+ function encodeGitPath(value) {
821
+ return normalizeGitPath(value).split("/").map(encodeURIComponent).join("/");
822
+ }
823
+
767
824
  function fetchText(url) {
768
825
  return new Promise((resolve, reject) => {
769
826
  https.get(url, { headers: { "User-Agent": `tasksai-install/${INSTALLER_VERSION}` } }, (response) => {