@hasna/todos 0.3.4 → 0.3.5

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/dist/cli/index.js CHANGED
@@ -2638,6 +2638,178 @@ var init_search = __esm(() => {
2638
2638
  init_database();
2639
2639
  });
2640
2640
 
2641
+ // src/lib/claude-tasks.ts
2642
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, readdirSync, writeFileSync } from "fs";
2643
+ import { join as join2 } from "path";
2644
+ function getTaskListDir(taskListId) {
2645
+ return join2(HOME, ".claude", "tasks", taskListId);
2646
+ }
2647
+ function readHighWaterMark(dir) {
2648
+ const path = join2(dir, ".highwatermark");
2649
+ if (!existsSync2(path))
2650
+ return 1;
2651
+ const val = parseInt(readFileSync(path, "utf-8").trim(), 10);
2652
+ return isNaN(val) ? 1 : val;
2653
+ }
2654
+ function writeHighWaterMark(dir, value) {
2655
+ writeFileSync(join2(dir, ".highwatermark"), String(value));
2656
+ }
2657
+ function readClaudeTask(dir, filename) {
2658
+ try {
2659
+ const content = readFileSync(join2(dir, filename), "utf-8");
2660
+ return JSON.parse(content);
2661
+ } catch {
2662
+ return null;
2663
+ }
2664
+ }
2665
+ function writeClaudeTask(dir, task) {
2666
+ writeFileSync(join2(dir, `${task.id}.json`), JSON.stringify(task, null, 2) + `
2667
+ `);
2668
+ }
2669
+ function toClaudeStatus(status) {
2670
+ if (status === "pending" || status === "in_progress" || status === "completed") {
2671
+ return status;
2672
+ }
2673
+ return "completed";
2674
+ }
2675
+ function toSqliteStatus(status) {
2676
+ return status;
2677
+ }
2678
+ function taskToClaudeTask(task, claudeTaskId) {
2679
+ return {
2680
+ id: claudeTaskId,
2681
+ subject: task.title,
2682
+ description: task.description || "",
2683
+ activeForm: "",
2684
+ status: toClaudeStatus(task.status),
2685
+ owner: task.assigned_to || task.agent_id || "",
2686
+ blocks: [],
2687
+ blockedBy: [],
2688
+ metadata: {
2689
+ todos_id: task.id,
2690
+ priority: task.priority
2691
+ }
2692
+ };
2693
+ }
2694
+ function pushToClaudeTaskList(taskListId, projectId) {
2695
+ const dir = getTaskListDir(taskListId);
2696
+ if (!existsSync2(dir))
2697
+ mkdirSync2(dir, { recursive: true });
2698
+ const filter = {};
2699
+ if (projectId)
2700
+ filter["project_id"] = projectId;
2701
+ const tasks = listTasks(filter);
2702
+ const existingByTodosId = new Map;
2703
+ const files = readdirSync(dir).filter((f) => f.endsWith(".json"));
2704
+ for (const f of files) {
2705
+ const ct = readClaudeTask(dir, f);
2706
+ if (ct?.metadata?.["todos_id"]) {
2707
+ existingByTodosId.set(ct.metadata["todos_id"], ct);
2708
+ }
2709
+ }
2710
+ let hwm = readHighWaterMark(dir);
2711
+ let pushed = 0;
2712
+ const errors = [];
2713
+ for (const task of tasks) {
2714
+ try {
2715
+ const existing = existingByTodosId.get(task.id);
2716
+ if (existing) {
2717
+ const updated = taskToClaudeTask(task, existing.id);
2718
+ updated.blocks = existing.blocks;
2719
+ updated.blockedBy = existing.blockedBy;
2720
+ updated.activeForm = existing.activeForm;
2721
+ writeClaudeTask(dir, updated);
2722
+ } else {
2723
+ const claudeId = String(hwm);
2724
+ hwm++;
2725
+ const ct = taskToClaudeTask(task, claudeId);
2726
+ writeClaudeTask(dir, ct);
2727
+ const current = getTask(task.id);
2728
+ if (current) {
2729
+ const newMeta = { ...current.metadata, claude_task_id: claudeId };
2730
+ updateTask(task.id, { version: current.version, metadata: newMeta });
2731
+ }
2732
+ }
2733
+ pushed++;
2734
+ } catch (e) {
2735
+ errors.push(`push ${task.id}: ${e instanceof Error ? e.message : String(e)}`);
2736
+ }
2737
+ }
2738
+ writeHighWaterMark(dir, hwm);
2739
+ return { pushed, pulled: 0, errors };
2740
+ }
2741
+ function pullFromClaudeTaskList(taskListId, projectId) {
2742
+ const dir = getTaskListDir(taskListId);
2743
+ if (!existsSync2(dir)) {
2744
+ return { pushed: 0, pulled: 0, errors: [`Task list directory not found: ${dir}`] };
2745
+ }
2746
+ const files = readdirSync(dir).filter((f) => f.endsWith(".json"));
2747
+ let pulled = 0;
2748
+ const errors = [];
2749
+ const allTasks = listTasks({});
2750
+ const byClaudeId = new Map;
2751
+ for (const t of allTasks) {
2752
+ const cid = t.metadata["claude_task_id"];
2753
+ if (cid)
2754
+ byClaudeId.set(String(cid), t);
2755
+ }
2756
+ const byTodosId = new Map;
2757
+ for (const t of allTasks) {
2758
+ byTodosId.set(t.id, t);
2759
+ }
2760
+ for (const f of files) {
2761
+ try {
2762
+ const ct = readClaudeTask(dir, f);
2763
+ if (!ct)
2764
+ continue;
2765
+ if (ct.metadata?.["_internal"])
2766
+ continue;
2767
+ const todosId = ct.metadata?.["todos_id"];
2768
+ const existingByMapping = byClaudeId.get(ct.id);
2769
+ const existingByTodos = todosId ? byTodosId.get(todosId) : undefined;
2770
+ const existing = existingByMapping || existingByTodos;
2771
+ if (existing) {
2772
+ updateTask(existing.id, {
2773
+ version: existing.version,
2774
+ title: ct.subject,
2775
+ description: ct.description || undefined,
2776
+ status: toSqliteStatus(ct.status),
2777
+ assigned_to: ct.owner || undefined,
2778
+ metadata: { ...existing.metadata, claude_task_id: ct.id }
2779
+ });
2780
+ } else {
2781
+ createTask({
2782
+ title: ct.subject,
2783
+ description: ct.description || undefined,
2784
+ status: toSqliteStatus(ct.status),
2785
+ assigned_to: ct.owner || undefined,
2786
+ project_id: projectId,
2787
+ metadata: { claude_task_id: ct.id },
2788
+ priority: ct.metadata?.["priority"] || "medium"
2789
+ });
2790
+ }
2791
+ pulled++;
2792
+ } catch (e) {
2793
+ errors.push(`pull ${f}: ${e instanceof Error ? e.message : String(e)}`);
2794
+ }
2795
+ }
2796
+ return { pushed: 0, pulled, errors };
2797
+ }
2798
+ function syncClaudeTaskList(taskListId, projectId) {
2799
+ const pullResult = pullFromClaudeTaskList(taskListId, projectId);
2800
+ const pushResult = pushToClaudeTaskList(taskListId, projectId);
2801
+ return {
2802
+ pushed: pushResult.pushed,
2803
+ pulled: pullResult.pulled,
2804
+ errors: [...pullResult.errors, ...pushResult.errors]
2805
+ };
2806
+ }
2807
+ var HOME;
2808
+ var init_claude_tasks = __esm(() => {
2809
+ init_tasks();
2810
+ HOME = process.env["HOME"] || process.env["USERPROFILE"] || "~";
2811
+ });
2812
+
2641
2813
  // node_modules/zod/v3/helpers/util.js
2642
2814
  var util, objectUtil, ZodParsedType, getParsedType = (data) => {
2643
2815
  const t = typeof data;
@@ -6667,6 +6839,7 @@ var init_mcp = __esm(() => {
6667
6839
  init_comments();
6668
6840
  init_projects();
6669
6841
  init_search();
6842
+ init_claude_tasks();
6670
6843
  init_database();
6671
6844
  init_types();
6672
6845
  server = new McpServer({
@@ -6964,6 +7137,42 @@ ${text}` }] };
6964
7137
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
6965
7138
  }
6966
7139
  });
7140
+ server.tool("sync", "Sync tasks with a Claude Code task list. Auto-detects task list from session ID if not specified. Use --push to write SQLite tasks to Claude task list, --pull to import, or both for bidirectional sync.", {
7141
+ task_list_id: exports_external.string().optional().describe("Claude Code task list ID (defaults to session ID)"),
7142
+ project_id: exports_external.string().optional().describe("Limit sync to a project"),
7143
+ direction: exports_external.enum(["push", "pull", "both"]).optional().describe("Sync direction: push (SQLite->Claude), pull (Claude->SQLite), or both (default)")
7144
+ }, async ({ task_list_id, project_id, direction }) => {
7145
+ try {
7146
+ const resolvedProjectId = project_id ? resolveId(project_id, "projects") : undefined;
7147
+ const taskListId = task_list_id || process.env["TODOS_CLAUDE_TASK_LIST"] || process.env["CLAUDE_CODE_TASK_LIST_ID"] || process.env["CLAUDE_CODE_SESSION_ID"];
7148
+ if (!taskListId) {
7149
+ return { content: [{ type: "text", text: "Could not detect task list ID. Pass task_list_id or set CLAUDE_CODE_TASK_LIST_ID." }], isError: true };
7150
+ }
7151
+ let result;
7152
+ if (direction === "push") {
7153
+ result = pushToClaudeTaskList(taskListId, resolvedProjectId);
7154
+ } else if (direction === "pull") {
7155
+ result = pullFromClaudeTaskList(taskListId, resolvedProjectId);
7156
+ } else {
7157
+ result = syncClaudeTaskList(taskListId, resolvedProjectId);
7158
+ }
7159
+ const parts = [];
7160
+ if (result.pulled > 0)
7161
+ parts.push(`Pulled ${result.pulled} task(s) from Claude task list.`);
7162
+ if (result.pushed > 0)
7163
+ parts.push(`Pushed ${result.pushed} task(s) to Claude task list.`);
7164
+ if (result.pulled === 0 && result.pushed === 0 && result.errors.length === 0) {
7165
+ parts.push("Nothing to sync.");
7166
+ }
7167
+ for (const err of result.errors) {
7168
+ parts.push(`Error: ${err}`);
7169
+ }
7170
+ return { content: [{ type: "text", text: parts.join(`
7171
+ `) }] };
7172
+ } catch (e) {
7173
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
7174
+ }
7175
+ });
6967
7176
  server.resource("tasks", "todos://tasks", { description: "All active tasks", mimeType: "application/json" }, async () => {
6968
7177
  const tasks = listTasks({ status: ["pending", "in_progress"] });
6969
7178
  return { contents: [{ uri: "todos://tasks", text: JSON.stringify(tasks, null, 2), mimeType: "application/json" }] };
@@ -8440,182 +8649,12 @@ init_tasks();
8440
8649
  init_projects();
8441
8650
  init_comments();
8442
8651
  init_search();
8652
+ init_claude_tasks();
8443
8653
  import chalk from "chalk";
8444
8654
  import { execSync as execSync2 } from "child_process";
8445
8655
  import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
8446
8656
  import { basename, dirname as dirname3, join as join4, resolve as resolve2 } from "path";
8447
8657
  import { fileURLToPath as fileURLToPath2 } from "url";
8448
-
8449
- // src/lib/claude-tasks.ts
8450
- init_tasks();
8451
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, readdirSync, writeFileSync } from "fs";
8452
- import { join as join2 } from "path";
8453
- var HOME = process.env["HOME"] || process.env["USERPROFILE"] || "~";
8454
- function getTaskListDir(taskListId) {
8455
- return join2(HOME, ".claude", "tasks", taskListId);
8456
- }
8457
- function readHighWaterMark(dir) {
8458
- const path = join2(dir, ".highwatermark");
8459
- if (!existsSync2(path))
8460
- return 1;
8461
- const val = parseInt(readFileSync(path, "utf-8").trim(), 10);
8462
- return isNaN(val) ? 1 : val;
8463
- }
8464
- function writeHighWaterMark(dir, value) {
8465
- writeFileSync(join2(dir, ".highwatermark"), String(value));
8466
- }
8467
- function readClaudeTask(dir, filename) {
8468
- try {
8469
- const content = readFileSync(join2(dir, filename), "utf-8");
8470
- return JSON.parse(content);
8471
- } catch {
8472
- return null;
8473
- }
8474
- }
8475
- function writeClaudeTask(dir, task) {
8476
- writeFileSync(join2(dir, `${task.id}.json`), JSON.stringify(task, null, 2) + `
8477
- `);
8478
- }
8479
- function toClaudeStatus(status) {
8480
- if (status === "pending" || status === "in_progress" || status === "completed") {
8481
- return status;
8482
- }
8483
- return "completed";
8484
- }
8485
- function toSqliteStatus(status) {
8486
- return status;
8487
- }
8488
- function taskToClaudeTask(task, claudeTaskId) {
8489
- return {
8490
- id: claudeTaskId,
8491
- subject: task.title,
8492
- description: task.description || "",
8493
- activeForm: "",
8494
- status: toClaudeStatus(task.status),
8495
- owner: task.assigned_to || task.agent_id || "",
8496
- blocks: [],
8497
- blockedBy: [],
8498
- metadata: {
8499
- todos_id: task.id,
8500
- priority: task.priority
8501
- }
8502
- };
8503
- }
8504
- function pushToClaudeTaskList(taskListId, projectId) {
8505
- const dir = getTaskListDir(taskListId);
8506
- if (!existsSync2(dir))
8507
- mkdirSync2(dir, { recursive: true });
8508
- const filter = {};
8509
- if (projectId)
8510
- filter["project_id"] = projectId;
8511
- const tasks = listTasks(filter);
8512
- const existingByTodosId = new Map;
8513
- const files = readdirSync(dir).filter((f) => f.endsWith(".json"));
8514
- for (const f of files) {
8515
- const ct = readClaudeTask(dir, f);
8516
- if (ct?.metadata?.["todos_id"]) {
8517
- existingByTodosId.set(ct.metadata["todos_id"], ct);
8518
- }
8519
- }
8520
- let hwm = readHighWaterMark(dir);
8521
- let pushed = 0;
8522
- const errors = [];
8523
- for (const task of tasks) {
8524
- try {
8525
- const existing = existingByTodosId.get(task.id);
8526
- if (existing) {
8527
- const updated = taskToClaudeTask(task, existing.id);
8528
- updated.blocks = existing.blocks;
8529
- updated.blockedBy = existing.blockedBy;
8530
- updated.activeForm = existing.activeForm;
8531
- writeClaudeTask(dir, updated);
8532
- } else {
8533
- const claudeId = String(hwm);
8534
- hwm++;
8535
- const ct = taskToClaudeTask(task, claudeId);
8536
- writeClaudeTask(dir, ct);
8537
- const current = getTask(task.id);
8538
- if (current) {
8539
- const newMeta = { ...current.metadata, claude_task_id: claudeId };
8540
- updateTask(task.id, { version: current.version, metadata: newMeta });
8541
- }
8542
- }
8543
- pushed++;
8544
- } catch (e) {
8545
- errors.push(`push ${task.id}: ${e instanceof Error ? e.message : String(e)}`);
8546
- }
8547
- }
8548
- writeHighWaterMark(dir, hwm);
8549
- return { pushed, pulled: 0, errors };
8550
- }
8551
- function pullFromClaudeTaskList(taskListId, projectId) {
8552
- const dir = getTaskListDir(taskListId);
8553
- if (!existsSync2(dir)) {
8554
- return { pushed: 0, pulled: 0, errors: [`Task list directory not found: ${dir}`] };
8555
- }
8556
- const files = readdirSync(dir).filter((f) => f.endsWith(".json"));
8557
- let pulled = 0;
8558
- const errors = [];
8559
- const allTasks = listTasks({});
8560
- const byClaudeId = new Map;
8561
- for (const t of allTasks) {
8562
- const cid = t.metadata["claude_task_id"];
8563
- if (cid)
8564
- byClaudeId.set(String(cid), t);
8565
- }
8566
- const byTodosId = new Map;
8567
- for (const t of allTasks) {
8568
- byTodosId.set(t.id, t);
8569
- }
8570
- for (const f of files) {
8571
- try {
8572
- const ct = readClaudeTask(dir, f);
8573
- if (!ct)
8574
- continue;
8575
- if (ct.metadata?.["_internal"])
8576
- continue;
8577
- const todosId = ct.metadata?.["todos_id"];
8578
- const existingByMapping = byClaudeId.get(ct.id);
8579
- const existingByTodos = todosId ? byTodosId.get(todosId) : undefined;
8580
- const existing = existingByMapping || existingByTodos;
8581
- if (existing) {
8582
- updateTask(existing.id, {
8583
- version: existing.version,
8584
- title: ct.subject,
8585
- description: ct.description || undefined,
8586
- status: toSqliteStatus(ct.status),
8587
- assigned_to: ct.owner || undefined,
8588
- metadata: { ...existing.metadata, claude_task_id: ct.id }
8589
- });
8590
- } else {
8591
- createTask({
8592
- title: ct.subject,
8593
- description: ct.description || undefined,
8594
- status: toSqliteStatus(ct.status),
8595
- assigned_to: ct.owner || undefined,
8596
- project_id: projectId,
8597
- metadata: { claude_task_id: ct.id },
8598
- priority: ct.metadata?.["priority"] || "medium"
8599
- });
8600
- }
8601
- pulled++;
8602
- } catch (e) {
8603
- errors.push(`pull ${f}: ${e instanceof Error ? e.message : String(e)}`);
8604
- }
8605
- }
8606
- return { pushed: 0, pulled, errors };
8607
- }
8608
- function syncClaudeTaskList(taskListId, projectId) {
8609
- const pullResult = pullFromClaudeTaskList(taskListId, projectId);
8610
- const pushResult = pushToClaudeTaskList(taskListId, projectId);
8611
- return {
8612
- pushed: pushResult.pushed,
8613
- pulled: pullResult.pulled,
8614
- errors: [...pullResult.errors, ...pushResult.errors]
8615
- };
8616
- }
8617
-
8618
- // src/cli/index.tsx
8619
8658
  function getPackageVersion2() {
8620
8659
  try {
8621
8660
  const pkgPath = join4(dirname3(fileURLToPath2(import.meta.url)), "..", "..", "package.json");
package/dist/mcp/index.js CHANGED
@@ -4529,6 +4529,174 @@ function searchTasks(query, projectId, db) {
4529
4529
  return rows.map(rowToTask2);
4530
4530
  }
4531
4531
 
4532
+ // src/lib/claude-tasks.ts
4533
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, readdirSync, writeFileSync } from "fs";
4534
+ import { join as join2 } from "path";
4535
+ var HOME = process.env["HOME"] || process.env["USERPROFILE"] || "~";
4536
+ function getTaskListDir(taskListId) {
4537
+ return join2(HOME, ".claude", "tasks", taskListId);
4538
+ }
4539
+ function readHighWaterMark(dir) {
4540
+ const path = join2(dir, ".highwatermark");
4541
+ if (!existsSync2(path))
4542
+ return 1;
4543
+ const val = parseInt(readFileSync(path, "utf-8").trim(), 10);
4544
+ return isNaN(val) ? 1 : val;
4545
+ }
4546
+ function writeHighWaterMark(dir, value) {
4547
+ writeFileSync(join2(dir, ".highwatermark"), String(value));
4548
+ }
4549
+ function readClaudeTask(dir, filename) {
4550
+ try {
4551
+ const content = readFileSync(join2(dir, filename), "utf-8");
4552
+ return JSON.parse(content);
4553
+ } catch {
4554
+ return null;
4555
+ }
4556
+ }
4557
+ function writeClaudeTask(dir, task) {
4558
+ writeFileSync(join2(dir, `${task.id}.json`), JSON.stringify(task, null, 2) + `
4559
+ `);
4560
+ }
4561
+ function toClaudeStatus(status) {
4562
+ if (status === "pending" || status === "in_progress" || status === "completed") {
4563
+ return status;
4564
+ }
4565
+ return "completed";
4566
+ }
4567
+ function toSqliteStatus(status) {
4568
+ return status;
4569
+ }
4570
+ function taskToClaudeTask(task, claudeTaskId) {
4571
+ return {
4572
+ id: claudeTaskId,
4573
+ subject: task.title,
4574
+ description: task.description || "",
4575
+ activeForm: "",
4576
+ status: toClaudeStatus(task.status),
4577
+ owner: task.assigned_to || task.agent_id || "",
4578
+ blocks: [],
4579
+ blockedBy: [],
4580
+ metadata: {
4581
+ todos_id: task.id,
4582
+ priority: task.priority
4583
+ }
4584
+ };
4585
+ }
4586
+ function pushToClaudeTaskList(taskListId, projectId) {
4587
+ const dir = getTaskListDir(taskListId);
4588
+ if (!existsSync2(dir))
4589
+ mkdirSync2(dir, { recursive: true });
4590
+ const filter = {};
4591
+ if (projectId)
4592
+ filter["project_id"] = projectId;
4593
+ const tasks = listTasks(filter);
4594
+ const existingByTodosId = new Map;
4595
+ const files = readdirSync(dir).filter((f) => f.endsWith(".json"));
4596
+ for (const f of files) {
4597
+ const ct = readClaudeTask(dir, f);
4598
+ if (ct?.metadata?.["todos_id"]) {
4599
+ existingByTodosId.set(ct.metadata["todos_id"], ct);
4600
+ }
4601
+ }
4602
+ let hwm = readHighWaterMark(dir);
4603
+ let pushed = 0;
4604
+ const errors2 = [];
4605
+ for (const task of tasks) {
4606
+ try {
4607
+ const existing = existingByTodosId.get(task.id);
4608
+ if (existing) {
4609
+ const updated = taskToClaudeTask(task, existing.id);
4610
+ updated.blocks = existing.blocks;
4611
+ updated.blockedBy = existing.blockedBy;
4612
+ updated.activeForm = existing.activeForm;
4613
+ writeClaudeTask(dir, updated);
4614
+ } else {
4615
+ const claudeId = String(hwm);
4616
+ hwm++;
4617
+ const ct = taskToClaudeTask(task, claudeId);
4618
+ writeClaudeTask(dir, ct);
4619
+ const current = getTask(task.id);
4620
+ if (current) {
4621
+ const newMeta = { ...current.metadata, claude_task_id: claudeId };
4622
+ updateTask(task.id, { version: current.version, metadata: newMeta });
4623
+ }
4624
+ }
4625
+ pushed++;
4626
+ } catch (e) {
4627
+ errors2.push(`push ${task.id}: ${e instanceof Error ? e.message : String(e)}`);
4628
+ }
4629
+ }
4630
+ writeHighWaterMark(dir, hwm);
4631
+ return { pushed, pulled: 0, errors: errors2 };
4632
+ }
4633
+ function pullFromClaudeTaskList(taskListId, projectId) {
4634
+ const dir = getTaskListDir(taskListId);
4635
+ if (!existsSync2(dir)) {
4636
+ return { pushed: 0, pulled: 0, errors: [`Task list directory not found: ${dir}`] };
4637
+ }
4638
+ const files = readdirSync(dir).filter((f) => f.endsWith(".json"));
4639
+ let pulled = 0;
4640
+ const errors2 = [];
4641
+ const allTasks = listTasks({});
4642
+ const byClaudeId = new Map;
4643
+ for (const t of allTasks) {
4644
+ const cid = t.metadata["claude_task_id"];
4645
+ if (cid)
4646
+ byClaudeId.set(String(cid), t);
4647
+ }
4648
+ const byTodosId = new Map;
4649
+ for (const t of allTasks) {
4650
+ byTodosId.set(t.id, t);
4651
+ }
4652
+ for (const f of files) {
4653
+ try {
4654
+ const ct = readClaudeTask(dir, f);
4655
+ if (!ct)
4656
+ continue;
4657
+ if (ct.metadata?.["_internal"])
4658
+ continue;
4659
+ const todosId = ct.metadata?.["todos_id"];
4660
+ const existingByMapping = byClaudeId.get(ct.id);
4661
+ const existingByTodos = todosId ? byTodosId.get(todosId) : undefined;
4662
+ const existing = existingByMapping || existingByTodos;
4663
+ if (existing) {
4664
+ updateTask(existing.id, {
4665
+ version: existing.version,
4666
+ title: ct.subject,
4667
+ description: ct.description || undefined,
4668
+ status: toSqliteStatus(ct.status),
4669
+ assigned_to: ct.owner || undefined,
4670
+ metadata: { ...existing.metadata, claude_task_id: ct.id }
4671
+ });
4672
+ } else {
4673
+ createTask({
4674
+ title: ct.subject,
4675
+ description: ct.description || undefined,
4676
+ status: toSqliteStatus(ct.status),
4677
+ assigned_to: ct.owner || undefined,
4678
+ project_id: projectId,
4679
+ metadata: { claude_task_id: ct.id },
4680
+ priority: ct.metadata?.["priority"] || "medium"
4681
+ });
4682
+ }
4683
+ pulled++;
4684
+ } catch (e) {
4685
+ errors2.push(`pull ${f}: ${e instanceof Error ? e.message : String(e)}`);
4686
+ }
4687
+ }
4688
+ return { pushed: 0, pulled, errors: errors2 };
4689
+ }
4690
+ function syncClaudeTaskList(taskListId, projectId) {
4691
+ const pullResult = pullFromClaudeTaskList(taskListId, projectId);
4692
+ const pushResult = pushToClaudeTaskList(taskListId, projectId);
4693
+ return {
4694
+ pushed: pushResult.pushed,
4695
+ pulled: pullResult.pulled,
4696
+ errors: [...pullResult.errors, ...pushResult.errors]
4697
+ };
4698
+ }
4699
+
4532
4700
  // src/mcp/index.ts
4533
4701
  var server = new McpServer({
4534
4702
  name: "todos",
@@ -4873,6 +5041,42 @@ ${text}` }] };
4873
5041
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
4874
5042
  }
4875
5043
  });
5044
+ server.tool("sync", "Sync tasks with a Claude Code task list. Auto-detects task list from session ID if not specified. Use --push to write SQLite tasks to Claude task list, --pull to import, or both for bidirectional sync.", {
5045
+ task_list_id: exports_external.string().optional().describe("Claude Code task list ID (defaults to session ID)"),
5046
+ project_id: exports_external.string().optional().describe("Limit sync to a project"),
5047
+ direction: exports_external.enum(["push", "pull", "both"]).optional().describe("Sync direction: push (SQLite->Claude), pull (Claude->SQLite), or both (default)")
5048
+ }, async ({ task_list_id, project_id, direction }) => {
5049
+ try {
5050
+ const resolvedProjectId = project_id ? resolveId(project_id, "projects") : undefined;
5051
+ const taskListId = task_list_id || process.env["TODOS_CLAUDE_TASK_LIST"] || process.env["CLAUDE_CODE_TASK_LIST_ID"] || process.env["CLAUDE_CODE_SESSION_ID"];
5052
+ if (!taskListId) {
5053
+ return { content: [{ type: "text", text: "Could not detect task list ID. Pass task_list_id or set CLAUDE_CODE_TASK_LIST_ID." }], isError: true };
5054
+ }
5055
+ let result;
5056
+ if (direction === "push") {
5057
+ result = pushToClaudeTaskList(taskListId, resolvedProjectId);
5058
+ } else if (direction === "pull") {
5059
+ result = pullFromClaudeTaskList(taskListId, resolvedProjectId);
5060
+ } else {
5061
+ result = syncClaudeTaskList(taskListId, resolvedProjectId);
5062
+ }
5063
+ const parts = [];
5064
+ if (result.pulled > 0)
5065
+ parts.push(`Pulled ${result.pulled} task(s) from Claude task list.`);
5066
+ if (result.pushed > 0)
5067
+ parts.push(`Pushed ${result.pushed} task(s) to Claude task list.`);
5068
+ if (result.pulled === 0 && result.pushed === 0 && result.errors.length === 0) {
5069
+ parts.push("Nothing to sync.");
5070
+ }
5071
+ for (const err of result.errors) {
5072
+ parts.push(`Error: ${err}`);
5073
+ }
5074
+ return { content: [{ type: "text", text: parts.join(`
5075
+ `) }] };
5076
+ } catch (e) {
5077
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
5078
+ }
5079
+ });
4876
5080
  server.resource("tasks", "todos://tasks", { description: "All active tasks", mimeType: "application/json" }, async () => {
4877
5081
  const tasks = listTasks({ status: ["pending", "in_progress"] });
4878
5082
  return { contents: [{ uri: "todos://tasks", text: JSON.stringify(tasks, null, 2), mimeType: "application/json" }] };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/todos",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
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",