@hasna/todos 0.3.3 → 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 +227 -180
- package/dist/mcp/index.js +204 -0
- package/package.json +1 -1
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");
|
|
@@ -9137,18 +9176,21 @@ hooks.command("install").description("Install Claude Code hooks for auto-sync").
|
|
|
9137
9176
|
const hookScript = `#!/usr/bin/env bash
|
|
9138
9177
|
# Auto-generated by: todos hooks install
|
|
9139
9178
|
# Syncs todos with Claude Code task list on tool use events.
|
|
9140
|
-
#
|
|
9179
|
+
# Reads session_id and tool_name from the hook JSON stdin.
|
|
9180
|
+
|
|
9181
|
+
INPUT=$(cat)
|
|
9141
9182
|
|
|
9142
|
-
|
|
9183
|
+
# Extract session_id from stdin JSON (hooks always receive this)
|
|
9184
|
+
SESSION_ID=$(echo "$INPUT" | grep -o '"session_id":"[^"]*"' | head -1 | cut -d'"' -f4 2>/dev/null || true)
|
|
9143
9185
|
|
|
9144
|
-
#
|
|
9145
|
-
TASK_LIST="\${TODOS_CLAUDE_TASK_LIST:-\${CLAUDE_CODE_TASK_LIST_ID
|
|
9186
|
+
# Task list priority: env override > session ID from hook input
|
|
9187
|
+
TASK_LIST="\${TODOS_CLAUDE_TASK_LIST:-\${CLAUDE_CODE_TASK_LIST_ID:-$SESSION_ID}}"
|
|
9146
9188
|
|
|
9147
9189
|
if [ -z "$TASK_LIST" ]; then
|
|
9148
9190
|
exit 0
|
|
9149
9191
|
fi
|
|
9150
9192
|
|
|
9151
|
-
TOOL_NAME=$(
|
|
9193
|
+
TOOL_NAME=$(echo "$INPUT" | grep -o '"tool_name":"[^"]*"' | head -1 | cut -d'"' -f4 2>/dev/null || true)
|
|
9152
9194
|
|
|
9153
9195
|
case "$TOOL_NAME" in
|
|
9154
9196
|
TaskCreate|TaskUpdate)
|
|
@@ -9175,19 +9217,24 @@ exit 0
|
|
|
9175
9217
|
hooksConfig["PostToolUse"] = [];
|
|
9176
9218
|
}
|
|
9177
9219
|
const postToolUse = hooksConfig["PostToolUse"];
|
|
9178
|
-
const filtered = postToolUse.filter((
|
|
9220
|
+
const filtered = postToolUse.filter((group) => {
|
|
9221
|
+
const groupHooks = group["hooks"];
|
|
9222
|
+
if (!groupHooks)
|
|
9223
|
+
return true;
|
|
9224
|
+
return !groupHooks.some((h) => (h["command"] || "").includes("todos-sync.sh"));
|
|
9225
|
+
});
|
|
9179
9226
|
filtered.push({
|
|
9180
9227
|
matcher: "TaskCreate|TaskUpdate",
|
|
9181
|
-
command: hookPath
|
|
9228
|
+
hooks: [{ type: "command", command: hookPath }]
|
|
9182
9229
|
});
|
|
9183
9230
|
filtered.push({
|
|
9184
9231
|
matcher: "mcp__todos__create_task|mcp__todos__update_task|mcp__todos__complete_task|mcp__todos__start_task",
|
|
9185
|
-
command: hookPath
|
|
9232
|
+
hooks: [{ type: "command", command: hookPath }]
|
|
9186
9233
|
});
|
|
9187
9234
|
hooksConfig["PostToolUse"] = filtered;
|
|
9188
9235
|
writeJsonFile(settingsPath, settings);
|
|
9189
9236
|
console.log(chalk.green(`Claude Code hooks configured in: ${settingsPath}`));
|
|
9190
|
-
console.log(chalk.dim("Task list ID auto-detected from
|
|
9237
|
+
console.log(chalk.dim("Task list ID auto-detected from hook stdin session_id."));
|
|
9191
9238
|
});
|
|
9192
9239
|
program2.command("mcp").description("Start MCP server (stdio)").option("--register <agent>", "Register MCP server with an agent (claude, codex, gemini, all)").option("--unregister <agent>", "Unregister MCP server from an agent (claude, codex, gemini, all)").option("-g, --global", "Register/unregister globally (user-level) instead of project-level").action(async (opts) => {
|
|
9193
9240
|
if (opts.register) {
|
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" }] };
|