@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 +4 -2
- package/package.json +5 -3
- package/runtime/server.py +52 -0
- package/src/index.js +91 -15
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/
|
|
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/
|
|
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.
|
|
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/
|
|
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.
|
|
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/
|
|
18
|
-
farmer: "https://github.com/
|
|
19
|
-
|
|
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/
|
|
151
|
-
tasksai-install farmer --source https://github.com/
|
|
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
|
|
276
|
+
const keys = mcpServerKeysForProduct(options.productId);
|
|
270
277
|
if (client.configFormat === "toml") {
|
|
271
278
|
const text = await fsp.readFile(configPath, "utf8");
|
|
272
|
-
if (!
|
|
273
|
-
console.log(`${client.displayName} does not have a ${
|
|
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,
|
|
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 ${
|
|
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,
|
|
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
|
|
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) => {
|