@tasksai/install 0.1.3 → 0.1.4
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 +1 -0
- package/package.json +1 -1
- package/src/index.js +131 -29
package/README.md
CHANGED
|
@@ -28,6 +28,7 @@ The installer:
|
|
|
28
28
|
- stores credentials outside MCP client config
|
|
29
29
|
- configures supported MCP clients
|
|
30
30
|
- checks required write access before changing local files
|
|
31
|
+
- supports Claude Desktop, Cursor, Windsurf, and Codex
|
|
31
32
|
- runs a local health check
|
|
32
33
|
|
|
33
34
|
TasksAI servers handle authentication, credits, catalog/search metadata, and
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -9,7 +9,7 @@ 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.4";
|
|
13
13
|
const DEFAULT_SOURCES = {
|
|
14
14
|
lawtasksai: "https://github.com/laudoluxDev/lawtasksai-mcp",
|
|
15
15
|
farmer: "https://github.com/laudoluxDev/farmertasksai-mcp",
|
|
@@ -43,6 +43,14 @@ const CLIENTS = {
|
|
|
43
43
|
configPath() {
|
|
44
44
|
return path.join(os.homedir(), ".codeium", "windsurf", "mcp_config.json");
|
|
45
45
|
}
|
|
46
|
+
},
|
|
47
|
+
codex: {
|
|
48
|
+
id: "codex",
|
|
49
|
+
displayName: "Codex",
|
|
50
|
+
configFormat: "toml",
|
|
51
|
+
configPath() {
|
|
52
|
+
return path.join(os.homedir(), ".codex", "config.toml");
|
|
53
|
+
}
|
|
46
54
|
}
|
|
47
55
|
};
|
|
48
56
|
|
|
@@ -129,10 +137,10 @@ function parseArgs(argv) {
|
|
|
129
137
|
|
|
130
138
|
function printUsage() {
|
|
131
139
|
console.log(`Usage:
|
|
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>]
|
|
140
|
+
tasksai-install <product-id> [install] [--source <repo-url>] [--ref <branch>] [--client claude-desktop|cursor|windsurf|codex|all] [--auth browser|license-key] [--install-dir <path>]
|
|
141
|
+
tasksai-install <product-id> doctor [--client claude-desktop|cursor|windsurf|codex|all] [--install-dir <path>]
|
|
134
142
|
tasksai-install <product-id> update [--install-dir <path>]
|
|
135
|
-
tasksai-install <product-id> uninstall [--client claude-desktop|cursor|windsurf|all] [--install-dir <path>]
|
|
143
|
+
tasksai-install <product-id> uninstall [--client claude-desktop|cursor|windsurf|codex|all] [--install-dir <path>]
|
|
136
144
|
|
|
137
145
|
Examples:
|
|
138
146
|
tasksai-install lawtasksai --source https://github.com/laudoluxDev/lawtasksai-mcp
|
|
@@ -220,10 +228,10 @@ async function doctor(options, { quietSuccess = false } = {}) {
|
|
|
220
228
|
for (const client of clients) {
|
|
221
229
|
const configPath = client.configPath();
|
|
222
230
|
if (fs.existsSync(configPath)) {
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
}
|
|
231
|
+
const hasServer = client.configFormat === "toml"
|
|
232
|
+
? await codexConfigHasServer(configPath, vertical.product_id)
|
|
233
|
+
: await jsonConfigHasServer(configPath, vertical.product_id);
|
|
234
|
+
if (!hasServer) problems.push(`${client.displayName} config does not contain mcpServers.${vertical.product_id}`);
|
|
227
235
|
} else {
|
|
228
236
|
problems.push(`${client.displayName} config not found at ${configPath}`);
|
|
229
237
|
}
|
|
@@ -253,15 +261,25 @@ async function uninstall(options) {
|
|
|
253
261
|
}
|
|
254
262
|
|
|
255
263
|
await withLock(`${configPath}.lock`, async () => {
|
|
256
|
-
const config = await readJson(configPath);
|
|
257
264
|
const key = options.productId;
|
|
258
|
-
if (
|
|
259
|
-
|
|
260
|
-
|
|
265
|
+
if (client.configFormat === "toml") {
|
|
266
|
+
const text = await fsp.readFile(configPath, "utf8");
|
|
267
|
+
if (!tomlHasMcpServer(text, key)) {
|
|
268
|
+
console.log(`${client.displayName} does not have a ${key} MCP entry.`);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
await backupFile(configPath);
|
|
272
|
+
await atomicWriteText(configPath, removeTomlSections(text, [`mcp_servers.${key}`, `mcp_servers.${key}.env`]));
|
|
273
|
+
} else {
|
|
274
|
+
const config = await readJson(configPath);
|
|
275
|
+
if (!config.mcpServers?.[key]) {
|
|
276
|
+
console.log(`${client.displayName} does not have a ${key} MCP entry.`);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
await backupFile(configPath);
|
|
280
|
+
delete config.mcpServers[key];
|
|
281
|
+
await atomicWriteJson(configPath, config);
|
|
261
282
|
}
|
|
262
|
-
await backupFile(configPath);
|
|
263
|
-
delete config.mcpServers[key];
|
|
264
|
-
await atomicWriteJson(configPath, config);
|
|
265
283
|
});
|
|
266
284
|
|
|
267
285
|
console.log(`${options.productId} was removed from ${client.displayName}.`);
|
|
@@ -278,26 +296,106 @@ async function configureMcpClient({ client, vertical, installDir, runtimeDir, ve
|
|
|
278
296
|
await fsp.mkdir(path.dirname(configPath), { recursive: true });
|
|
279
297
|
|
|
280
298
|
await withLock(`${configPath}.lock`, async () => {
|
|
281
|
-
const
|
|
282
|
-
if (!config.mcpServers || typeof config.mcpServers !== "object") config.mcpServers = {};
|
|
283
|
-
|
|
299
|
+
const serverConfig = buildMcpServerConfig({ client, vertical, installDir, runtimeDir, vendorDir });
|
|
284
300
|
await backupFile(configPath);
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
TASKSAI_API_BASE: vertical.api_base_url,
|
|
291
|
-
TASKSAI_CLIENT: client.id,
|
|
292
|
-
PYTHONPATH: vendorDir,
|
|
293
|
-
DOTENV_PATH: path.join(installDir, ".env")
|
|
294
|
-
}
|
|
295
|
-
};
|
|
301
|
+
if (client.configFormat === "toml") {
|
|
302
|
+
const text = fs.existsSync(configPath) ? await fsp.readFile(configPath, "utf8") : "";
|
|
303
|
+
await atomicWriteText(configPath, upsertCodexMcpServer(text, vertical.product_id, serverConfig));
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
296
306
|
|
|
307
|
+
const config = fs.existsSync(configPath) ? await readJson(configPath) : {};
|
|
308
|
+
if (!config.mcpServers || typeof config.mcpServers !== "object") config.mcpServers = {};
|
|
309
|
+
config.mcpServers[vertical.product_id] = serverConfig;
|
|
297
310
|
await atomicWriteJson(configPath, config);
|
|
298
311
|
});
|
|
299
312
|
}
|
|
300
313
|
|
|
314
|
+
function buildMcpServerConfig({ client, vertical, installDir, runtimeDir, vendorDir }) {
|
|
315
|
+
return {
|
|
316
|
+
command: "python3",
|
|
317
|
+
args: [path.join(runtimeDir, "server.py")],
|
|
318
|
+
env: {
|
|
319
|
+
TASKSAI_PRODUCT_ID: vertical.product_id,
|
|
320
|
+
TASKSAI_API_BASE: vertical.api_base_url,
|
|
321
|
+
TASKSAI_CLIENT: client.id,
|
|
322
|
+
PYTHONPATH: vendorDir,
|
|
323
|
+
DOTENV_PATH: path.join(installDir, ".env")
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async function jsonConfigHasServer(configPath, productId) {
|
|
329
|
+
const config = await readJson(configPath);
|
|
330
|
+
return Boolean(config.mcpServers?.[productId]);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async function codexConfigHasServer(configPath, productId) {
|
|
334
|
+
const text = await fsp.readFile(configPath, "utf8");
|
|
335
|
+
return tomlHasMcpServer(text, productId);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function tomlHasMcpServer(text, productId) {
|
|
339
|
+
return getTomlSectionNames(text).includes(`mcp_servers.${productId}`);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function getTomlSectionNames(text) {
|
|
343
|
+
return text
|
|
344
|
+
.split(/\r?\n/)
|
|
345
|
+
.map((line) => {
|
|
346
|
+
const match = line.match(/^\s*\[([^\]]+)\]\s*(?:#.*)?$/);
|
|
347
|
+
return match?.[1] || null;
|
|
348
|
+
})
|
|
349
|
+
.filter(Boolean);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function upsertCodexMcpServer(text, productId, serverConfig) {
|
|
353
|
+
const withoutServer = removeTomlSections(text, [`mcp_servers.${productId}`, `mcp_servers.${productId}.env`]).trimEnd();
|
|
354
|
+
const block = formatCodexMcpServer(productId, serverConfig);
|
|
355
|
+
return `${withoutServer ? `${withoutServer}\n\n` : ""}${block}\n`;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function removeTomlSections(text, sectionNames) {
|
|
359
|
+
const sections = new Set(sectionNames);
|
|
360
|
+
const lines = text.split(/\r?\n/);
|
|
361
|
+
const kept = [];
|
|
362
|
+
let skipping = false;
|
|
363
|
+
|
|
364
|
+
for (const line of lines) {
|
|
365
|
+
const match = line.match(/^\s*\[([^\]]+)\]\s*(?:#.*)?$/);
|
|
366
|
+
if (match) {
|
|
367
|
+
skipping = sections.has(match[1]);
|
|
368
|
+
}
|
|
369
|
+
if (!skipping) kept.push(line);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return kept.join("\n").replace(/\n{3,}/g, "\n\n");
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function formatCodexMcpServer(productId, serverConfig) {
|
|
376
|
+
const lines = [
|
|
377
|
+
`[mcp_servers.${productId}]`,
|
|
378
|
+
`command = ${tomlString(serverConfig.command)}`,
|
|
379
|
+
`args = ${tomlArray(serverConfig.args)}`,
|
|
380
|
+
"",
|
|
381
|
+
`[mcp_servers.${productId}.env]`
|
|
382
|
+
];
|
|
383
|
+
|
|
384
|
+
for (const [key, value] of Object.entries(serverConfig.env || {})) {
|
|
385
|
+
lines.push(`${key} = ${tomlString(value)}`);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return lines.join("\n");
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function tomlArray(values) {
|
|
392
|
+
return `[${values.map((value) => tomlString(value)).join(", ")}]`;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function tomlString(value) {
|
|
396
|
+
return `"${String(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
397
|
+
}
|
|
398
|
+
|
|
301
399
|
function resolveClients(clientOption) {
|
|
302
400
|
if (clientOption === "all") {
|
|
303
401
|
return Object.values(CLIENTS);
|
|
@@ -708,6 +806,10 @@ async function writeJson(filePath, value) {
|
|
|
708
806
|
async function atomicWriteJson(filePath, value) {
|
|
709
807
|
const text = `${JSON.stringify(value, null, 2)}\n`;
|
|
710
808
|
JSON.parse(text);
|
|
809
|
+
await atomicWriteText(filePath, text);
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
async function atomicWriteText(filePath, text) {
|
|
711
813
|
const tmpPath = `${filePath}.tmp-${process.pid}`;
|
|
712
814
|
await fsp.writeFile(tmpPath, text, { encoding: "utf8", mode: 0o600 });
|
|
713
815
|
await fsp.rename(tmpPath, filePath);
|