@hasna/todos 0.1.1 → 0.2.1

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 +508 -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";
@@ -7215,6 +7602,29 @@ function TaskDetail({ task }) {
7215
7602
  }, undefined, false, undefined, this)
7216
7603
  ]
7217
7604
  }, undefined, true, undefined, this),
7605
+ task.agent_id && /* @__PURE__ */ jsxDEV3(Box3, {
7606
+ children: [
7607
+ /* @__PURE__ */ jsxDEV3(Text3, {
7608
+ dimColor: true,
7609
+ children: "Agent: "
7610
+ }, undefined, false, undefined, this),
7611
+ /* @__PURE__ */ jsxDEV3(Text3, {
7612
+ color: "cyan",
7613
+ children: task.agent_id
7614
+ }, undefined, false, undefined, this)
7615
+ ]
7616
+ }, undefined, true, undefined, this),
7617
+ task.session_id && /* @__PURE__ */ jsxDEV3(Box3, {
7618
+ children: [
7619
+ /* @__PURE__ */ jsxDEV3(Text3, {
7620
+ dimColor: true,
7621
+ children: "Session: "
7622
+ }, undefined, false, undefined, this),
7623
+ /* @__PURE__ */ jsxDEV3(Text3, {
7624
+ children: task.session_id
7625
+ }, undefined, false, undefined, this)
7626
+ ]
7627
+ }, undefined, true, undefined, this),
7218
7628
  task.assigned_to && /* @__PURE__ */ jsxDEV3(Box3, {
7219
7629
  children: [
7220
7630
  /* @__PURE__ */ jsxDEV3(Text3, {
@@ -7227,6 +7637,28 @@ function TaskDetail({ task }) {
7227
7637
  }, undefined, false, undefined, this)
7228
7638
  ]
7229
7639
  }, undefined, true, undefined, this),
7640
+ task.project_id && /* @__PURE__ */ jsxDEV3(Box3, {
7641
+ children: [
7642
+ /* @__PURE__ */ jsxDEV3(Text3, {
7643
+ dimColor: true,
7644
+ children: "Project: "
7645
+ }, undefined, false, undefined, this),
7646
+ /* @__PURE__ */ jsxDEV3(Text3, {
7647
+ children: task.project_id
7648
+ }, undefined, false, undefined, this)
7649
+ ]
7650
+ }, undefined, true, undefined, this),
7651
+ task.working_dir && /* @__PURE__ */ jsxDEV3(Box3, {
7652
+ children: [
7653
+ /* @__PURE__ */ jsxDEV3(Text3, {
7654
+ dimColor: true,
7655
+ children: "Work Dir: "
7656
+ }, undefined, false, undefined, this),
7657
+ /* @__PURE__ */ jsxDEV3(Text3, {
7658
+ children: task.working_dir
7659
+ }, undefined, false, undefined, this)
7660
+ ]
7661
+ }, undefined, true, undefined, this),
7230
7662
  task.locked_by && /* @__PURE__ */ jsxDEV3(Box3, {
7231
7663
  children: [
7232
7664
  /* @__PURE__ */ jsxDEV3(Text3, {
@@ -8000,9 +8432,18 @@ init_projects();
8000
8432
  init_comments();
8001
8433
  init_search();
8002
8434
  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";
8435
+ import { execSync as execSync2 } from "child_process";
8436
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
8437
+ import { basename, dirname as dirname3, join as join3, resolve as resolve2 } from "path";
8438
+ import { fileURLToPath as fileURLToPath2 } from "url";
8439
+ function getPackageVersion2() {
8440
+ try {
8441
+ const pkgPath = join3(dirname3(fileURLToPath2(import.meta.url)), "..", "..", "package.json");
8442
+ return JSON.parse(readFileSync2(pkgPath, "utf-8")).version || "0.0.0";
8443
+ } catch {
8444
+ return "0.0.0";
8445
+ }
8446
+ }
8006
8447
  var program2 = new Command;
8007
8448
  function handleError(e) {
8008
8449
  console.error(chalk.red(e instanceof Error ? e.message : String(e)));
@@ -8019,7 +8460,7 @@ function resolveTaskId(partialId) {
8019
8460
  }
8020
8461
  function detectGitRoot() {
8021
8462
  try {
8022
- return execSync("git rev-parse --show-toplevel", { encoding: "utf-8" }).trim();
8463
+ return execSync2("git rev-parse --show-toplevel", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
8023
8464
  } catch {
8024
8465
  return null;
8025
8466
  }
@@ -8064,7 +8505,7 @@ function formatTaskLine(t) {
8064
8505
  const tags = t.tags.length > 0 ? chalk.dim(` [${t.tags.join(",")}]`) : "";
8065
8506
  return `${chalk.dim(t.id.slice(0, 8))} ${statusFn(t.status.padEnd(11))} ${priorityFn(t.priority.padEnd(8))} ${t.title}${assigned}${lock}${tags}`;
8066
8507
  }
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");
8508
+ 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
8509
  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
8510
  const globalOpts = program2.opts();
8070
8511
  const projectId = autoProject(globalOpts);
@@ -8144,10 +8585,14 @@ program2.command("show <id>").description("Show full task details").action((id)
8144
8585
  console.log(` ${chalk.dim("Assigned:")} ${task.assigned_to}`);
8145
8586
  if (task.agent_id)
8146
8587
  console.log(` ${chalk.dim("Agent:")} ${task.agent_id}`);
8588
+ if (task.session_id)
8589
+ console.log(` ${chalk.dim("Session:")} ${task.session_id}`);
8147
8590
  if (task.locked_by)
8148
8591
  console.log(` ${chalk.dim("Locked:")} ${task.locked_by} (at ${task.locked_at})`);
8149
8592
  if (task.project_id)
8150
8593
  console.log(` ${chalk.dim("Project:")} ${task.project_id}`);
8594
+ if (task.working_dir)
8595
+ console.log(` ${chalk.dim("WorkDir:")} ${task.working_dir}`);
8151
8596
  if (task.parent)
8152
8597
  console.log(` ${chalk.dim("Parent:")} ${task.parent.id.slice(0, 8)} | ${task.parent.title}`);
8153
8598
  if (task.tags.length > 0)
@@ -8478,45 +8923,45 @@ program2.command("mcp").description("Start MCP server (stdio)").option("--regist
8478
8923
  var HOME = process.env["HOME"] || process.env["USERPROFILE"] || "~";
8479
8924
  function getMcpBinaryPath() {
8480
8925
  try {
8481
- const p = execSync("which todos-mcp", { encoding: "utf-8" }).trim();
8926
+ const p = execSync2("which todos-mcp", { encoding: "utf-8" }).trim();
8482
8927
  if (p)
8483
8928
  return p;
8484
8929
  } catch {}
8485
- const bunBin = join2(HOME, ".bun", "bin", "todos-mcp");
8486
- if (existsSync2(bunBin))
8930
+ const bunBin = join3(HOME, ".bun", "bin", "todos-mcp");
8931
+ if (existsSync3(bunBin))
8487
8932
  return bunBin;
8488
8933
  return "todos-mcp";
8489
8934
  }
8490
8935
  function readJsonFile(path) {
8491
- if (!existsSync2(path))
8936
+ if (!existsSync3(path))
8492
8937
  return {};
8493
8938
  try {
8494
- return JSON.parse(readFileSync(path, "utf-8"));
8939
+ return JSON.parse(readFileSync2(path, "utf-8"));
8495
8940
  } catch {
8496
8941
  return {};
8497
8942
  }
8498
8943
  }
8499
8944
  function writeJsonFile(path, data) {
8500
- const dir = dirname2(path);
8501
- if (!existsSync2(dir))
8945
+ const dir = dirname3(path);
8946
+ if (!existsSync3(dir))
8502
8947
  mkdirSync2(dir, { recursive: true });
8503
8948
  writeFileSync(path, JSON.stringify(data, null, 2) + `
8504
8949
  `);
8505
8950
  }
8506
8951
  function readTomlFile(path) {
8507
- if (!existsSync2(path))
8952
+ if (!existsSync3(path))
8508
8953
  return "";
8509
- return readFileSync(path, "utf-8");
8954
+ return readFileSync2(path, "utf-8");
8510
8955
  }
8511
8956
  function writeTomlFile(path, content) {
8512
- const dir = dirname2(path);
8513
- if (!existsSync2(dir))
8957
+ const dir = dirname3(path);
8958
+ if (!existsSync3(dir))
8514
8959
  mkdirSync2(dir, { recursive: true });
8515
8960
  writeFileSync(path, content);
8516
8961
  }
8517
8962
  function registerClaude(binPath) {
8518
8963
  const cwd = process.cwd();
8519
- const configPath = join2(cwd, ".mcp.json");
8964
+ const configPath = join3(cwd, ".mcp.json");
8520
8965
  const config = readJsonFile(configPath);
8521
8966
  config["todos"] = {
8522
8967
  command: binPath,
@@ -8527,7 +8972,7 @@ function registerClaude(binPath) {
8527
8972
  }
8528
8973
  function unregisterClaude() {
8529
8974
  const cwd = process.cwd();
8530
- const configPath = join2(cwd, ".mcp.json");
8975
+ const configPath = join3(cwd, ".mcp.json");
8531
8976
  const config = readJsonFile(configPath);
8532
8977
  if (!("todos" in config)) {
8533
8978
  console.log(chalk.dim(`Claude Code: todos not found in ${configPath}`));
@@ -8538,7 +8983,7 @@ function unregisterClaude() {
8538
8983
  console.log(chalk.green(`Claude Code: unregistered from ${configPath}`));
8539
8984
  }
8540
8985
  function registerCodex(binPath) {
8541
- const configPath = join2(HOME, ".codex", "config.toml");
8986
+ const configPath = join3(HOME, ".codex", "config.toml");
8542
8987
  let content = readTomlFile(configPath);
8543
8988
  content = removeTomlBlock(content, "mcp_servers.todos");
8544
8989
  const block = `
@@ -8552,7 +8997,7 @@ args = []
8552
8997
  console.log(chalk.green(`Codex CLI: registered in ${configPath}`));
8553
8998
  }
8554
8999
  function unregisterCodex() {
8555
- const configPath = join2(HOME, ".codex", "config.toml");
9000
+ const configPath = join3(HOME, ".codex", "config.toml");
8556
9001
  let content = readTomlFile(configPath);
8557
9002
  if (!content.includes("[mcp_servers.todos]")) {
8558
9003
  console.log(chalk.dim(`Codex CLI: todos not found in ${configPath}`));
@@ -8585,7 +9030,7 @@ function removeTomlBlock(content, blockName) {
8585
9030
  `);
8586
9031
  }
8587
9032
  function registerGemini(binPath) {
8588
- const configPath = join2(HOME, ".gemini", "settings.json");
9033
+ const configPath = join3(HOME, ".gemini", "settings.json");
8589
9034
  const config = readJsonFile(configPath);
8590
9035
  if (!config["mcpServers"]) {
8591
9036
  config["mcpServers"] = {};
@@ -8599,7 +9044,7 @@ function registerGemini(binPath) {
8599
9044
  console.log(chalk.green(`Gemini CLI: registered in ${configPath}`));
8600
9045
  }
8601
9046
  function unregisterGemini() {
8602
- const configPath = join2(HOME, ".gemini", "settings.json");
9047
+ const configPath = join3(HOME, ".gemini", "settings.json");
8603
9048
  const config = readJsonFile(configPath);
8604
9049
  const servers = config["mcpServers"];
8605
9050
  if (!servers || !("todos" in servers)) {
@@ -8647,6 +9092,47 @@ function unregisterMcp(agent) {
8647
9092
  }
8648
9093
  }
8649
9094
  }
9095
+ 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) => {
9096
+ const { startServer: startServer2 } = await Promise.resolve().then(() => (init_serve(), exports_serve));
9097
+ await startServer2(parseInt(opts.port, 10), { open: opts.open });
9098
+ });
9099
+ 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) => {
9100
+ try {
9101
+ const currentVersion = getPackageVersion2();
9102
+ const res = await fetch("https://registry.npmjs.org/@hasna/todos/latest");
9103
+ if (!res.ok) {
9104
+ console.error(chalk.red("Failed to check for updates."));
9105
+ process.exit(1);
9106
+ }
9107
+ const data = await res.json();
9108
+ const latestVersion = data.version;
9109
+ console.log(` Current: ${chalk.dim(currentVersion)}`);
9110
+ console.log(` Latest: ${chalk.green(latestVersion)}`);
9111
+ if (currentVersion === latestVersion) {
9112
+ console.log(chalk.green(`
9113
+ Already up to date!`));
9114
+ return;
9115
+ }
9116
+ if (opts.check) {
9117
+ console.log(chalk.yellow(`
9118
+ Update available: ${currentVersion} \u2192 ${latestVersion}`));
9119
+ return;
9120
+ }
9121
+ let useBun = false;
9122
+ try {
9123
+ execSync2("which bun", { stdio: "ignore" });
9124
+ useBun = true;
9125
+ } catch {}
9126
+ const cmd = useBun ? "bun add -g @hasna/todos@latest" : "npm install -g @hasna/todos@latest";
9127
+ console.log(chalk.dim(`
9128
+ Running: ${cmd}`));
9129
+ execSync2(cmd, { stdio: "inherit" });
9130
+ console.log(chalk.green(`
9131
+ Updated to ${latestVersion}!`));
9132
+ } catch (e) {
9133
+ handleError(e);
9134
+ }
9135
+ });
8650
9136
  program2.command("interactive").description("Launch interactive TUI").action(async () => {
8651
9137
  const { renderApp: renderApp2 } = await Promise.resolve().then(() => (init_App(), exports_App));
8652
9138
  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.1",
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",