@hasna/todos 0.1.1 → 0.2.0

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 (2) hide show
  1. package/dist/cli/index.js +459 -22
  2. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -2599,6 +2599,10 @@ function getComment(id, db) {
2599
2599
  const d = db || getDatabase();
2600
2600
  return d.query("SELECT * FROM task_comments WHERE id = ?").get(id);
2601
2601
  }
2602
+ function listComments(taskId, db) {
2603
+ const d = db || getDatabase();
2604
+ return d.query("SELECT * FROM task_comments WHERE task_id = ? ORDER BY created_at").all(taskId);
2605
+ }
2602
2606
  var init_comments = __esm(() => {
2603
2607
  init_types();
2604
2608
  init_database();
@@ -6965,6 +6969,389 @@ ${text}` }] };
6965
6969
  });
6966
6970
  });
6967
6971
 
6972
+ // src/server/serve.ts
6973
+ var exports_serve = {};
6974
+ __export(exports_serve, {
6975
+ startServer: () => startServer
6976
+ });
6977
+ import { execSync } from "child_process";
6978
+ import { existsSync as existsSync2, readFileSync } from "fs";
6979
+ import { join as join2, dirname as dirname2, extname } from "path";
6980
+ import { fileURLToPath } from "url";
6981
+ function resolveDashboardDir() {
6982
+ const candidates = [];
6983
+ try {
6984
+ const scriptDir = dirname2(fileURLToPath(import.meta.url));
6985
+ candidates.push(join2(scriptDir, "..", "dashboard", "dist"));
6986
+ candidates.push(join2(scriptDir, "..", "..", "dashboard", "dist"));
6987
+ } catch {}
6988
+ if (process.argv[1]) {
6989
+ const mainDir = dirname2(process.argv[1]);
6990
+ candidates.push(join2(mainDir, "..", "dashboard", "dist"));
6991
+ candidates.push(join2(mainDir, "..", "..", "dashboard", "dist"));
6992
+ }
6993
+ candidates.push(join2(process.cwd(), "dashboard", "dist"));
6994
+ for (const candidate of candidates) {
6995
+ if (existsSync2(candidate))
6996
+ return candidate;
6997
+ }
6998
+ return join2(process.cwd(), "dashboard", "dist");
6999
+ }
7000
+ function json(data, status = 200, port) {
7001
+ return new Response(JSON.stringify(data), {
7002
+ status,
7003
+ headers: {
7004
+ "Content-Type": "application/json",
7005
+ "Access-Control-Allow-Origin": port ? `http://localhost:${port}` : "*",
7006
+ ...SECURITY_HEADERS
7007
+ }
7008
+ });
7009
+ }
7010
+ function getPackageVersion() {
7011
+ try {
7012
+ const pkgPath = join2(dirname2(fileURLToPath(import.meta.url)), "..", "..", "package.json");
7013
+ return JSON.parse(readFileSync(pkgPath, "utf-8")).version || "0.0.0";
7014
+ } catch {
7015
+ return "0.0.0";
7016
+ }
7017
+ }
7018
+ function serveStaticFile(filePath) {
7019
+ if (!existsSync2(filePath))
7020
+ return null;
7021
+ const ext = extname(filePath);
7022
+ const contentType = MIME_TYPES[ext] || "application/octet-stream";
7023
+ return new Response(Bun.file(filePath), {
7024
+ headers: { "Content-Type": contentType }
7025
+ });
7026
+ }
7027
+ async function startServer(port, options) {
7028
+ const shouldOpen = options?.open ?? true;
7029
+ const dashboardDir = resolveDashboardDir();
7030
+ const dashboardExists = existsSync2(dashboardDir);
7031
+ if (!dashboardExists) {
7032
+ console.error(`
7033
+ Dashboard not found at: ${dashboardDir}`);
7034
+ console.error(`Run this to build it:
7035
+ `);
7036
+ console.error(` cd dashboard && bun install && bun run build
7037
+ `);
7038
+ console.error(`Or from the project root:
7039
+ `);
7040
+ console.error(` bun run build:dashboard
7041
+ `);
7042
+ }
7043
+ const server2 = Bun.serve({
7044
+ port,
7045
+ async fetch(req) {
7046
+ const url = new URL(req.url);
7047
+ const path = url.pathname;
7048
+ const method = req.method;
7049
+ if (path === "/api/tasks" && method === "GET") {
7050
+ try {
7051
+ const filter = {};
7052
+ const status = url.searchParams.get("status");
7053
+ const priority = url.searchParams.get("priority");
7054
+ const projectId = url.searchParams.get("project_id");
7055
+ if (status)
7056
+ filter.status = status;
7057
+ if (priority)
7058
+ filter.priority = priority;
7059
+ if (projectId)
7060
+ filter.project_id = projectId;
7061
+ const tasks = listTasks(filter);
7062
+ const projectCache = new Map;
7063
+ const enriched = tasks.map((t) => {
7064
+ let projectName;
7065
+ if (t.project_id) {
7066
+ if (projectCache.has(t.project_id)) {
7067
+ projectName = projectCache.get(t.project_id);
7068
+ } else {
7069
+ const p = getProject(t.project_id);
7070
+ projectName = p?.name;
7071
+ if (projectName)
7072
+ projectCache.set(t.project_id, projectName);
7073
+ }
7074
+ }
7075
+ return { ...t, project_name: projectName };
7076
+ });
7077
+ return json(enriched, 200, port);
7078
+ } catch (e) {
7079
+ return json({ error: e instanceof Error ? e.message : "Failed to list tasks" }, 500, port);
7080
+ }
7081
+ }
7082
+ const taskGetMatch = path.match(/^\/api\/tasks\/([^/]+)$/);
7083
+ if (taskGetMatch && method === "GET") {
7084
+ try {
7085
+ const id = taskGetMatch[1];
7086
+ const task = getTaskWithRelations(id);
7087
+ if (!task)
7088
+ return json({ error: "Task not found" }, 404, port);
7089
+ let projectName;
7090
+ if (task.project_id) {
7091
+ const p = getProject(task.project_id);
7092
+ projectName = p?.name;
7093
+ }
7094
+ return json({ ...task, project_name: projectName }, 200, port);
7095
+ } catch (e) {
7096
+ return json({ error: e instanceof Error ? e.message : "Failed to get task" }, 500, port);
7097
+ }
7098
+ }
7099
+ if (path === "/api/tasks" && method === "POST") {
7100
+ try {
7101
+ const contentLength = parseInt(req.headers.get("content-length") || "0", 10);
7102
+ if (contentLength > MAX_BODY_SIZE)
7103
+ return json({ error: "Request body too large" }, 413, port);
7104
+ const body = await req.json();
7105
+ if (!body.title || typeof body.title !== "string") {
7106
+ return json({ error: "Missing required field: title" }, 400, port);
7107
+ }
7108
+ const task = createTask({
7109
+ title: body.title,
7110
+ description: body.description,
7111
+ priority: body.priority,
7112
+ project_id: body.project_id,
7113
+ parent_id: body.parent_id,
7114
+ tags: body.tags,
7115
+ assigned_to: body.assigned_to,
7116
+ agent_id: body.agent_id,
7117
+ status: body.status
7118
+ });
7119
+ return json(task, 201, port);
7120
+ } catch (e) {
7121
+ return json({ error: e instanceof Error ? e.message : "Failed to create task" }, 500, port);
7122
+ }
7123
+ }
7124
+ const taskPatchMatch = path.match(/^\/api\/tasks\/([^/]+)$/);
7125
+ if (taskPatchMatch && method === "PATCH") {
7126
+ try {
7127
+ const id = taskPatchMatch[1];
7128
+ const contentLength = parseInt(req.headers.get("content-length") || "0", 10);
7129
+ if (contentLength > MAX_BODY_SIZE)
7130
+ return json({ error: "Request body too large" }, 413, port);
7131
+ const body = await req.json();
7132
+ if (typeof body.version !== "number") {
7133
+ return json({ error: "Missing required field: version" }, 400, port);
7134
+ }
7135
+ const task = updateTask(id, {
7136
+ version: body.version,
7137
+ title: body.title,
7138
+ description: body.description,
7139
+ status: body.status,
7140
+ priority: body.priority,
7141
+ assigned_to: body.assigned_to,
7142
+ tags: body.tags,
7143
+ metadata: body.metadata
7144
+ });
7145
+ return json(task, 200, port);
7146
+ } catch (e) {
7147
+ const status = e instanceof Error && e.name === "VersionConflictError" ? 409 : 500;
7148
+ return json({ error: e instanceof Error ? e.message : "Failed to update task" }, status, port);
7149
+ }
7150
+ }
7151
+ const taskDeleteMatch = path.match(/^\/api\/tasks\/([^/]+)$/);
7152
+ if (taskDeleteMatch && method === "DELETE") {
7153
+ try {
7154
+ const id = taskDeleteMatch[1];
7155
+ const deleted = deleteTask(id);
7156
+ if (!deleted)
7157
+ return json({ error: "Task not found" }, 404, port);
7158
+ return json({ deleted: true }, 200, port);
7159
+ } catch (e) {
7160
+ return json({ error: e instanceof Error ? e.message : "Failed to delete task" }, 500, port);
7161
+ }
7162
+ }
7163
+ const taskStartMatch = path.match(/^\/api\/tasks\/([^/]+)\/start$/);
7164
+ if (taskStartMatch && method === "POST") {
7165
+ try {
7166
+ const id = taskStartMatch[1];
7167
+ const body = await req.json();
7168
+ const agentId = body.agent_id || "dashboard";
7169
+ const task = startTask(id, agentId);
7170
+ return json(task, 200, port);
7171
+ } catch (e) {
7172
+ const status = e instanceof Error && e.name === "TaskNotFoundError" ? 404 : e instanceof Error && e.name === "LockError" ? 409 : 500;
7173
+ return json({ error: e instanceof Error ? e.message : "Failed to start task" }, status, port);
7174
+ }
7175
+ }
7176
+ const taskCompleteMatch = path.match(/^\/api\/tasks\/([^/]+)\/complete$/);
7177
+ if (taskCompleteMatch && method === "POST") {
7178
+ try {
7179
+ const id = taskCompleteMatch[1];
7180
+ const body = await req.json();
7181
+ const agentId = body.agent_id;
7182
+ const task = completeTask(id, agentId);
7183
+ return json(task, 200, port);
7184
+ } catch (e) {
7185
+ const status = e instanceof Error && e.name === "TaskNotFoundError" ? 404 : e instanceof Error && e.name === "LockError" ? 409 : 500;
7186
+ return json({ error: e instanceof Error ? e.message : "Failed to complete task" }, status, port);
7187
+ }
7188
+ }
7189
+ const commentsGetMatch = path.match(/^\/api\/tasks\/([^/]+)\/comments$/);
7190
+ if (commentsGetMatch && method === "GET") {
7191
+ try {
7192
+ const taskId = commentsGetMatch[1];
7193
+ const comments = listComments(taskId);
7194
+ return json(comments, 200, port);
7195
+ } catch (e) {
7196
+ return json({ error: e instanceof Error ? e.message : "Failed to list comments" }, 500, port);
7197
+ }
7198
+ }
7199
+ const commentsPostMatch = path.match(/^\/api\/tasks\/([^/]+)\/comments$/);
7200
+ if (commentsPostMatch && method === "POST") {
7201
+ try {
7202
+ const taskId = commentsPostMatch[1];
7203
+ const contentLength = parseInt(req.headers.get("content-length") || "0", 10);
7204
+ if (contentLength > MAX_BODY_SIZE)
7205
+ return json({ error: "Request body too large" }, 413, port);
7206
+ const body = await req.json();
7207
+ if (!body.content || typeof body.content !== "string") {
7208
+ return json({ error: "Missing required field: content" }, 400, port);
7209
+ }
7210
+ const comment = addComment({
7211
+ task_id: taskId,
7212
+ content: body.content,
7213
+ agent_id: body.agent_id,
7214
+ session_id: body.session_id
7215
+ });
7216
+ return json(comment, 201, port);
7217
+ } catch (e) {
7218
+ return json({ error: e instanceof Error ? e.message : "Failed to add comment" }, 500, port);
7219
+ }
7220
+ }
7221
+ if (path === "/api/projects" && method === "GET") {
7222
+ try {
7223
+ const projects = listProjects();
7224
+ return json(projects, 200, port);
7225
+ } catch (e) {
7226
+ return json({ error: e instanceof Error ? e.message : "Failed to list projects" }, 500, port);
7227
+ }
7228
+ }
7229
+ if (path === "/api/projects" && method === "POST") {
7230
+ try {
7231
+ const contentLength = parseInt(req.headers.get("content-length") || "0", 10);
7232
+ if (contentLength > MAX_BODY_SIZE)
7233
+ return json({ error: "Request body too large" }, 413, port);
7234
+ const body = await req.json();
7235
+ if (!body.name || typeof body.name !== "string") {
7236
+ return json({ error: "Missing required field: name" }, 400, port);
7237
+ }
7238
+ const project = createProject({
7239
+ name: body.name,
7240
+ path: body.path || process.cwd(),
7241
+ description: body.description
7242
+ });
7243
+ return json(project, 201, port);
7244
+ } catch (e) {
7245
+ return json({ error: e instanceof Error ? e.message : "Failed to create project" }, 500, port);
7246
+ }
7247
+ }
7248
+ if (path === "/api/search" && method === "GET") {
7249
+ try {
7250
+ const q = url.searchParams.get("q");
7251
+ if (!q)
7252
+ return json({ error: "Missing query parameter: q" }, 400, port);
7253
+ const projectId = url.searchParams.get("project_id") || undefined;
7254
+ const results = searchTasks(q, projectId);
7255
+ return json(results, 200, port);
7256
+ } catch (e) {
7257
+ return json({ error: e instanceof Error ? e.message : "Search failed" }, 500, port);
7258
+ }
7259
+ }
7260
+ if (path === "/api/system/version" && method === "GET") {
7261
+ try {
7262
+ const current = getPackageVersion();
7263
+ const npmRes = await fetch("https://registry.npmjs.org/@hasna/todos/latest");
7264
+ if (!npmRes.ok) {
7265
+ return json({ current, latest: current, updateAvailable: false }, 200, port);
7266
+ }
7267
+ const data = await npmRes.json();
7268
+ const latest = data.version;
7269
+ return json({ current, latest, updateAvailable: current !== latest }, 200, port);
7270
+ } catch {
7271
+ const current = getPackageVersion();
7272
+ return json({ current, latest: current, updateAvailable: false }, 200, port);
7273
+ }
7274
+ }
7275
+ if (path === "/api/system/update" && method === "POST") {
7276
+ try {
7277
+ let useBun = false;
7278
+ try {
7279
+ execSync("which bun", { stdio: "ignore" });
7280
+ useBun = true;
7281
+ } catch {}
7282
+ const cmd = useBun ? "bun add -g @hasna/todos@latest" : "npm install -g @hasna/todos@latest";
7283
+ execSync(cmd, { stdio: "ignore", timeout: 60000 });
7284
+ return json({ success: true, message: "Updated! Restart the server to use the new version." }, 200, port);
7285
+ } catch (e) {
7286
+ return json({ success: false, message: e instanceof Error ? e.message : "Update failed" }, 500, port);
7287
+ }
7288
+ }
7289
+ if (method === "OPTIONS") {
7290
+ return new Response(null, {
7291
+ headers: {
7292
+ "Access-Control-Allow-Origin": `http://localhost:${port}`,
7293
+ "Access-Control-Allow-Methods": "GET, POST, PATCH, DELETE, OPTIONS",
7294
+ "Access-Control-Allow-Headers": "Content-Type"
7295
+ }
7296
+ });
7297
+ }
7298
+ if (dashboardExists && (method === "GET" || method === "HEAD")) {
7299
+ if (path !== "/") {
7300
+ const filePath = join2(dashboardDir, path);
7301
+ const res2 = serveStaticFile(filePath);
7302
+ if (res2)
7303
+ return res2;
7304
+ }
7305
+ const indexPath = join2(dashboardDir, "index.html");
7306
+ const res = serveStaticFile(indexPath);
7307
+ if (res)
7308
+ return res;
7309
+ }
7310
+ return json({ error: "Not found" }, 404, port);
7311
+ }
7312
+ });
7313
+ const shutdown = () => {
7314
+ server2.stop();
7315
+ process.exit(0);
7316
+ };
7317
+ process.on("SIGINT", shutdown);
7318
+ process.on("SIGTERM", shutdown);
7319
+ const serverUrl = `http://localhost:${port}`;
7320
+ console.log(`Todos Dashboard running at ${serverUrl}`);
7321
+ if (shouldOpen) {
7322
+ try {
7323
+ const { exec } = await import("child_process");
7324
+ const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
7325
+ exec(`${openCmd} ${serverUrl}`);
7326
+ } catch {}
7327
+ }
7328
+ return server2;
7329
+ }
7330
+ var MIME_TYPES, SECURITY_HEADERS, MAX_BODY_SIZE;
7331
+ var init_serve = __esm(() => {
7332
+ init_tasks();
7333
+ init_projects();
7334
+ init_comments();
7335
+ init_search();
7336
+ MIME_TYPES = {
7337
+ ".html": "text/html; charset=utf-8",
7338
+ ".js": "application/javascript",
7339
+ ".css": "text/css",
7340
+ ".json": "application/json",
7341
+ ".png": "image/png",
7342
+ ".jpg": "image/jpeg",
7343
+ ".svg": "image/svg+xml",
7344
+ ".ico": "image/x-icon",
7345
+ ".woff": "font/woff",
7346
+ ".woff2": "font/woff2"
7347
+ };
7348
+ SECURITY_HEADERS = {
7349
+ "X-Content-Type-Options": "nosniff",
7350
+ "X-Frame-Options": "DENY"
7351
+ };
7352
+ MAX_BODY_SIZE = 1024 * 1024;
7353
+ });
7354
+
6968
7355
  // src/cli/components/Header.tsx
6969
7356
  import { Box, Text } from "ink";
6970
7357
  import { jsxDEV, Fragment } from "react/jsx-dev-runtime";
@@ -8000,9 +8387,18 @@ init_projects();
8000
8387
  init_comments();
8001
8388
  init_search();
8002
8389
  import chalk from "chalk";
8003
- import { execSync } from "child_process";
8004
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, writeFileSync } from "fs";
8005
- import { basename, dirname as dirname2, join as join2, resolve as resolve2 } from "path";
8390
+ import { execSync as execSync2 } from "child_process";
8391
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
8392
+ import { basename, dirname as dirname3, join as join3, resolve as resolve2 } from "path";
8393
+ import { fileURLToPath as fileURLToPath2 } from "url";
8394
+ function getPackageVersion2() {
8395
+ try {
8396
+ const pkgPath = join3(dirname3(fileURLToPath2(import.meta.url)), "..", "..", "package.json");
8397
+ return JSON.parse(readFileSync2(pkgPath, "utf-8")).version || "0.0.0";
8398
+ } catch {
8399
+ return "0.0.0";
8400
+ }
8401
+ }
8006
8402
  var program2 = new Command;
8007
8403
  function handleError(e) {
8008
8404
  console.error(chalk.red(e instanceof Error ? e.message : String(e)));
@@ -8019,7 +8415,7 @@ function resolveTaskId(partialId) {
8019
8415
  }
8020
8416
  function detectGitRoot() {
8021
8417
  try {
8022
- return execSync("git rev-parse --show-toplevel", { encoding: "utf-8" }).trim();
8418
+ return execSync2("git rev-parse --show-toplevel", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
8023
8419
  } catch {
8024
8420
  return null;
8025
8421
  }
@@ -8064,7 +8460,7 @@ function formatTaskLine(t) {
8064
8460
  const tags = t.tags.length > 0 ? chalk.dim(` [${t.tags.join(",")}]`) : "";
8065
8461
  return `${chalk.dim(t.id.slice(0, 8))} ${statusFn(t.status.padEnd(11))} ${priorityFn(t.priority.padEnd(8))} ${t.title}${assigned}${lock}${tags}`;
8066
8462
  }
8067
- program2.name("todos").description("Universal task management for AI coding agents").version("0.1.0").option("--project <path>", "Project path").option("--json", "Output as JSON").option("--agent <name>", "Agent name").option("--session <id>", "Session ID");
8463
+ program2.name("todos").description("Universal task management for AI coding agents").version(getPackageVersion2()).option("--project <path>", "Project path").option("--json", "Output as JSON").option("--agent <name>", "Agent name").option("--session <id>", "Session ID");
8068
8464
  program2.command("add <title>").description("Create a new task").option("-d, --description <text>", "Task description").option("-p, --priority <level>", "Priority: low, medium, high, critical").option("--parent <id>", "Parent task ID").option("--tags <tags>", "Comma-separated tags").option("--assign <agent>", "Assign to agent").option("--status <status>", "Initial status").action((title, opts) => {
8069
8465
  const globalOpts = program2.opts();
8070
8466
  const projectId = autoProject(globalOpts);
@@ -8478,45 +8874,45 @@ program2.command("mcp").description("Start MCP server (stdio)").option("--regist
8478
8874
  var HOME = process.env["HOME"] || process.env["USERPROFILE"] || "~";
8479
8875
  function getMcpBinaryPath() {
8480
8876
  try {
8481
- const p = execSync("which todos-mcp", { encoding: "utf-8" }).trim();
8877
+ const p = execSync2("which todos-mcp", { encoding: "utf-8" }).trim();
8482
8878
  if (p)
8483
8879
  return p;
8484
8880
  } catch {}
8485
- const bunBin = join2(HOME, ".bun", "bin", "todos-mcp");
8486
- if (existsSync2(bunBin))
8881
+ const bunBin = join3(HOME, ".bun", "bin", "todos-mcp");
8882
+ if (existsSync3(bunBin))
8487
8883
  return bunBin;
8488
8884
  return "todos-mcp";
8489
8885
  }
8490
8886
  function readJsonFile(path) {
8491
- if (!existsSync2(path))
8887
+ if (!existsSync3(path))
8492
8888
  return {};
8493
8889
  try {
8494
- return JSON.parse(readFileSync(path, "utf-8"));
8890
+ return JSON.parse(readFileSync2(path, "utf-8"));
8495
8891
  } catch {
8496
8892
  return {};
8497
8893
  }
8498
8894
  }
8499
8895
  function writeJsonFile(path, data) {
8500
- const dir = dirname2(path);
8501
- if (!existsSync2(dir))
8896
+ const dir = dirname3(path);
8897
+ if (!existsSync3(dir))
8502
8898
  mkdirSync2(dir, { recursive: true });
8503
8899
  writeFileSync(path, JSON.stringify(data, null, 2) + `
8504
8900
  `);
8505
8901
  }
8506
8902
  function readTomlFile(path) {
8507
- if (!existsSync2(path))
8903
+ if (!existsSync3(path))
8508
8904
  return "";
8509
- return readFileSync(path, "utf-8");
8905
+ return readFileSync2(path, "utf-8");
8510
8906
  }
8511
8907
  function writeTomlFile(path, content) {
8512
- const dir = dirname2(path);
8513
- if (!existsSync2(dir))
8908
+ const dir = dirname3(path);
8909
+ if (!existsSync3(dir))
8514
8910
  mkdirSync2(dir, { recursive: true });
8515
8911
  writeFileSync(path, content);
8516
8912
  }
8517
8913
  function registerClaude(binPath) {
8518
8914
  const cwd = process.cwd();
8519
- const configPath = join2(cwd, ".mcp.json");
8915
+ const configPath = join3(cwd, ".mcp.json");
8520
8916
  const config = readJsonFile(configPath);
8521
8917
  config["todos"] = {
8522
8918
  command: binPath,
@@ -8527,7 +8923,7 @@ function registerClaude(binPath) {
8527
8923
  }
8528
8924
  function unregisterClaude() {
8529
8925
  const cwd = process.cwd();
8530
- const configPath = join2(cwd, ".mcp.json");
8926
+ const configPath = join3(cwd, ".mcp.json");
8531
8927
  const config = readJsonFile(configPath);
8532
8928
  if (!("todos" in config)) {
8533
8929
  console.log(chalk.dim(`Claude Code: todos not found in ${configPath}`));
@@ -8538,7 +8934,7 @@ function unregisterClaude() {
8538
8934
  console.log(chalk.green(`Claude Code: unregistered from ${configPath}`));
8539
8935
  }
8540
8936
  function registerCodex(binPath) {
8541
- const configPath = join2(HOME, ".codex", "config.toml");
8937
+ const configPath = join3(HOME, ".codex", "config.toml");
8542
8938
  let content = readTomlFile(configPath);
8543
8939
  content = removeTomlBlock(content, "mcp_servers.todos");
8544
8940
  const block = `
@@ -8552,7 +8948,7 @@ args = []
8552
8948
  console.log(chalk.green(`Codex CLI: registered in ${configPath}`));
8553
8949
  }
8554
8950
  function unregisterCodex() {
8555
- const configPath = join2(HOME, ".codex", "config.toml");
8951
+ const configPath = join3(HOME, ".codex", "config.toml");
8556
8952
  let content = readTomlFile(configPath);
8557
8953
  if (!content.includes("[mcp_servers.todos]")) {
8558
8954
  console.log(chalk.dim(`Codex CLI: todos not found in ${configPath}`));
@@ -8585,7 +8981,7 @@ function removeTomlBlock(content, blockName) {
8585
8981
  `);
8586
8982
  }
8587
8983
  function registerGemini(binPath) {
8588
- const configPath = join2(HOME, ".gemini", "settings.json");
8984
+ const configPath = join3(HOME, ".gemini", "settings.json");
8589
8985
  const config = readJsonFile(configPath);
8590
8986
  if (!config["mcpServers"]) {
8591
8987
  config["mcpServers"] = {};
@@ -8599,7 +8995,7 @@ function registerGemini(binPath) {
8599
8995
  console.log(chalk.green(`Gemini CLI: registered in ${configPath}`));
8600
8996
  }
8601
8997
  function unregisterGemini() {
8602
- const configPath = join2(HOME, ".gemini", "settings.json");
8998
+ const configPath = join3(HOME, ".gemini", "settings.json");
8603
8999
  const config = readJsonFile(configPath);
8604
9000
  const servers = config["mcpServers"];
8605
9001
  if (!servers || !("todos" in servers)) {
@@ -8647,6 +9043,47 @@ function unregisterMcp(agent) {
8647
9043
  }
8648
9044
  }
8649
9045
  }
9046
+ program2.command("serve").aliases(["dashboard", "open"]).description("Start the web dashboard").option("-p, --port <port>", "Port number", "19420").option("--no-open", "Don't auto-open browser").action(async (opts) => {
9047
+ const { startServer: startServer2 } = await Promise.resolve().then(() => (init_serve(), exports_serve));
9048
+ await startServer2(parseInt(opts.port, 10), { open: opts.open });
9049
+ });
9050
+ program2.command("upgrade").alias("self-update").description("Update todos to the latest version").option("--check", "Only check for updates, don't install").action(async (opts) => {
9051
+ try {
9052
+ const currentVersion = getPackageVersion2();
9053
+ const res = await fetch("https://registry.npmjs.org/@hasna/todos/latest");
9054
+ if (!res.ok) {
9055
+ console.error(chalk.red("Failed to check for updates."));
9056
+ process.exit(1);
9057
+ }
9058
+ const data = await res.json();
9059
+ const latestVersion = data.version;
9060
+ console.log(` Current: ${chalk.dim(currentVersion)}`);
9061
+ console.log(` Latest: ${chalk.green(latestVersion)}`);
9062
+ if (currentVersion === latestVersion) {
9063
+ console.log(chalk.green(`
9064
+ Already up to date!`));
9065
+ return;
9066
+ }
9067
+ if (opts.check) {
9068
+ console.log(chalk.yellow(`
9069
+ Update available: ${currentVersion} \u2192 ${latestVersion}`));
9070
+ return;
9071
+ }
9072
+ let useBun = false;
9073
+ try {
9074
+ execSync2("which bun", { stdio: "ignore" });
9075
+ useBun = true;
9076
+ } catch {}
9077
+ const cmd = useBun ? "bun add -g @hasna/todos@latest" : "npm install -g @hasna/todos@latest";
9078
+ console.log(chalk.dim(`
9079
+ Running: ${cmd}`));
9080
+ execSync2(cmd, { stdio: "inherit" });
9081
+ console.log(chalk.green(`
9082
+ Updated to ${latestVersion}!`));
9083
+ } catch (e) {
9084
+ handleError(e);
9085
+ }
9086
+ });
8650
9087
  program2.command("interactive").description("Launch interactive TUI").action(async () => {
8651
9088
  const { renderApp: renderApp2 } = await Promise.resolve().then(() => (init_App(), exports_App));
8652
9089
  const globalOpts = program2.opts();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/todos",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",