@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.
Files changed (3) hide show
  1. package/README.md +1 -0
  2. package/package.json +1 -1
  3. 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tasksai/install",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Shared TasksAI MCP installer CLI",
5
5
  "type": "module",
6
6
  "bin": {
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.3";
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 config = await readJson(configPath);
224
- if (!config.mcpServers?.[vertical.product_id]) {
225
- problems.push(`${client.displayName} config does not contain mcpServers.${vertical.product_id}`);
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 (!config.mcpServers?.[key]) {
259
- console.log(`${client.displayName} does not have a ${key} MCP entry.`);
260
- return;
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 config = fs.existsSync(configPath) ? await readJson(configPath) : {};
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
- config.mcpServers[vertical.product_id] = {
286
- command: "python3",
287
- args: [path.join(runtimeDir, "server.py")],
288
- env: {
289
- TASKSAI_PRODUCT_ID: vertical.product_id,
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);