@fukuchang/taskchute-mcp 0.4.0 → 0.6.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.0 (2026-10-22) — Node.js セキュリティ修正版を要求
4
+
5
+ ### Changed
6
+
7
+ - 対応 Node.js を 22.23.4 以降の22系、24.22.1 以降の24系、26.12.1 以降の26系に変更。
8
+ - Node.js 20 のサポートを終了し、ビルド対象を Node.js 22 に変更。
9
+ - MCP パッケージの更新とは別に、起動に使う Node.js 本体を更新し、MCP サーバーを再起動する必要があります。
10
+
3
11
  ## 0.3.0 (2026-05-28) — 実績時刻の秒精度サポート
4
12
 
5
13
  ### Added
@@ -95,7 +103,7 @@ PR2 で露出した `repos.auditingTask` 経由で `routine-propagation` cause
95
103
  (`daily` / `weekly` + weekdays / `monthly` + days / `custom` + cron)
96
104
  - update_routine は `pickPropagationTargets` で未来 planned task を絞り込み、
97
105
  `buildRoutineTaskPatch` で patch を構築、`auditingTask.updateWithCause(id,
98
- patch, 'routine-propagation')` で伝播。`propagated_task_count` を返す
106
+ patch, 'routine-propagation')` で伝播。`propagated_task_count` を返す
99
107
  - 過去日 / running / completed / skipped / 別 routine_id は保護される
100
108
  - `auditingTask` が undefined の時 (テスト等) は通常 update に fallback
101
109
  - `create_time_block` / `update_time_block` / `archive_time_block` /
@@ -182,9 +190,9 @@ update_task のフル機能が初めて実用化される。
182
190
  Web 側 `useAuditingTaskRepository` は既存の `instanceof` 判定で動き続ける
183
191
  (互換)
184
192
  - `test-helpers.ts` に `InMemoryRoutineRepository` / `InMemoryTimeBlockRepository`
185
- + `makeRoutine` / `makeTimeBlock` を追加。`makeMockAppContext` に
186
- `routineRepo` / `timeBlockRepo` opts と `refreshLookups: () => Promise.resolve()`
187
- 既定実装を追加
193
+ - `makeRoutine` / `makeTimeBlock` を追加。`makeMockAppContext` に
194
+ `routineRepo` / `timeBlockRepo` opts と `refreshLookups: () => Promise.resolve()`
195
+ 既定実装を追加
188
196
 
189
197
  ### PR1: Task CRUD パリティ
190
198
 
package/README.md CHANGED
@@ -10,6 +10,15 @@ E2EE 暗号化された TaskChute タスクを Claude Desktop / Claude Code 等
10
10
  > も有効。初回バージョン (0.1.2) は CLI と歩調を揃えるためのもので、以降は
11
11
  > 独立 SemVer。
12
12
 
13
+ ## 必要な Node.js
14
+
15
+ Node.js 22.23.4 以降の22系 / 24.22.1 以降の24系 / 26.12.1 以降の26系に対応します。
16
+ 24系を推奨し、各系統の最新パッチ版を使用してください。
17
+ [2026年10月のセキュリティ修正](https://nodejs.org/en/blog/vulnerability/october-2026-security-releases)を含むバージョンが必要です。
18
+
19
+ MCP パッケージの更新だけでは Node.js 本体は更新されません。
20
+ MCP クライアントが起動に使う Node.js を更新し、MCP サーバーを再起動してください。
21
+
13
22
  ## インストール
14
23
 
15
24
  ```bash
@@ -21,32 +30,32 @@ npm i -g @fukuchang/taskchute-mcp
21
30
 
22
31
  ## 提供 tool
23
32
 
24
- | tool | 説明 |
25
- | --- | --- |
26
- | `list_tasks` | 指定日 (省略時は今日) のタスク一覧を返す |
27
- | `add_task` | タスクを追加する (title, date?, estimated_seconds?, description?, project_id?, context_id?, tag_ids?, scheduled_start_time?, notify_at_scheduled_start?, is_background?) |
28
- | `update_task` | タスクのフィールドを部分更新する (タイトル / 詳細 / 見積もり / 予定時刻 / project / context / tags / 通知 / バックグラウンドフラグ)。`null` で明示クリア、`tag_ids` は全置換 |
29
- | `delete_task` | タスクを削除する (Routine 由来タスクを今日以降に消すと `routine_expansions` に skip 記録、`hard_delete: true` で物理削除のみ) |
30
- | `start_task` | タスクの計測を開始する (status → running) |
31
- | `pause_task` | タスクを一時停止する (status → paused) |
32
- | `done_task` | タスクを完了する (status → completed) |
33
- | `skip_task` | タスクをスキップする (status → skipped)。reopen で planned に戻せる |
34
- | `reopen_task` | completed / skipped を planned に戻す |
35
- | `list_projects` | プロジェクト一覧 (status filter / limit 既定 100)。add/update_task の `project_id` を引くために使う |
36
- | `list_tags` | タグ一覧。`tag_ids` を引くために使う |
37
- | `list_contexts` | コンテキスト一覧。`context_id` を引くために使う |
38
- | `list_routines` | ルーチン (繰り返しタスクテンプレート) 一覧。`frequency` は discriminated union (`{type: 'daily'}` / `{type: 'weekly', weekdays: [...]}` 等) のまま |
39
- | `list_time_blocks` | タイムブロック (時間帯テンプレート) 一覧 |
40
- | `list_recent_tasks` | 過去 N 日 (既定 90) のタスク。`query` で fuzzy match (ひらがな↔カタカナ正規化込み) 検索可能 |
41
- | `create_project` / `update_project` / `archive_project` / `delete_project` | プロジェクト CRUD。`update` は partial / null クリア。`archive` はタスクの project_id を維持、`delete` は NULL 化 (ON DELETE SET NULL) |
42
- | `create_tag` / `update_tag` / `archive_tag` / `delete_tag` | タグ CRUD。`delete` は tag_ids 配列に孤児 id を残すので注意 (UI 表示時に「不明なタグ」扱い) |
43
- | `create_context` / `update_context` / `archive_context` / `delete_context` | コンテキスト CRUD。`delete` はタスクの context_id を NULL 化 |
44
- | `create_routine` / `update_routine` / `archive_routine` / `delete_routine` | ルーチン CRUD。`update_routine` は **未来 planned task に自動伝播** (title / estimated / project / context / tags / scheduled_start_time)。手動編集も上書きされる点に注意。`propagated_task_count` を返す |
45
- | `create_time_block` / `update_time_block` / `archive_time_block` / `delete_time_block` | タイムブロック CRUD。`weekdays` は 0=日曜~6=土曜の配列 (空不可) |
46
- | `set_actual_times` | 実績開始/終了時刻を直接指定 (Play 押し忘れリカバリ用)。start+end → completed、start のみ → running、両方 null → planned リセット、end < start → 日跨ぎセッション |
47
- | `interrupt_task` | 走行中タスクを「割り込み」で分割。1 呼び出しで 3 タスクを生成 (元タスク完了 / 割り込み running / 残作業 planned) + split note 3 件自動記録 |
48
- | `add_parallel_task` | 進行中タスクと並行する新規タスクを追加。既定で running 状態 + 即計測開始 |
49
- | `reorder_task` | 並び順 (order_index) と所属タイムブロックを直接指定 (慣習 10 単位) |
33
+ | tool | 説明 |
34
+ | -------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
35
+ | `list_tasks` | 指定日 (省略時は今日) のタスク一覧を返す |
36
+ | `add_task` | タスクを追加する (title, date?, estimated_seconds?, description?, project_id?, context_id?, tag_ids?, scheduled_start_time?, notify_at_scheduled_start?, is_background?) |
37
+ | `update_task` | タスクのフィールドを部分更新する (タイトル / 詳細 / 見積もり / 予定時刻 / project / context / tags / 通知 / バックグラウンドフラグ)。`null` で明示クリア、`tag_ids` は全置換 |
38
+ | `delete_task` | タスクを削除する (Routine 由来タスクを今日以降に消すと `routine_expansions` に skip 記録、`hard_delete: true` で物理削除のみ) |
39
+ | `start_task` | タスクの計測を開始する (status → running) |
40
+ | `pause_task` | タスクを一時停止する (status → paused) |
41
+ | `done_task` | タスクを完了する (status → completed) |
42
+ | `skip_task` | タスクをスキップする (status → skipped)。reopen で planned に戻せる |
43
+ | `reopen_task` | completed / skipped を planned に戻す |
44
+ | `list_projects` | プロジェクト一覧 (status filter / limit 既定 100)。add/update_task の `project_id` を引くために使う |
45
+ | `list_tags` | タグ一覧。`tag_ids` を引くために使う |
46
+ | `list_contexts` | コンテキスト一覧。`context_id` を引くために使う |
47
+ | `list_routines` | ルーチン (繰り返しタスクテンプレート) 一覧。`frequency` は discriminated union (`{type: 'daily'}` / `{type: 'weekly', weekdays: [...]}` 等) のまま |
48
+ | `list_time_blocks` | タイムブロック (時間帯テンプレート) 一覧 |
49
+ | `list_recent_tasks` | 過去 N 日 (既定 90) のタスク。`query` で fuzzy match (ひらがな↔カタカナ正規化込み) 検索可能 |
50
+ | `create_project` / `update_project` / `archive_project` / `delete_project` | プロジェクト CRUD。`update` は partial / null クリア。`archive` はタスクの project_id を維持、`delete` は NULL 化 (ON DELETE SET NULL) |
51
+ | `create_tag` / `update_tag` / `archive_tag` / `delete_tag` | タグ CRUD。`delete` は tag_ids 配列に孤児 id を残すので注意 (UI 表示時に「不明なタグ」扱い) |
52
+ | `create_context` / `update_context` / `archive_context` / `delete_context` | コンテキスト CRUD。`delete` はタスクの context_id を NULL 化 |
53
+ | `create_routine` / `update_routine` / `archive_routine` / `delete_routine` | ルーチン CRUD。`update_routine` は **未来 planned task に自動伝播** (title / estimated / project / context / tags / scheduled_start_time)。手動編集も上書きされる点に注意。`propagated_task_count` を返す |
54
+ | `create_time_block` / `update_time_block` / `archive_time_block` / `delete_time_block` | タイムブロック CRUD。`weekdays` は 0=日曜~6=土曜の配列 (空不可) |
55
+ | `set_actual_times` | 実績開始/終了時刻を直接指定 (Play 押し忘れリカバリ用)。start+end → completed、start のみ → running、両方 null → planned リセット、end < start → 日跨ぎセッション |
56
+ | `interrupt_task` | 走行中タスクを「割り込み」で分割。1 呼び出しで 3 タスクを生成 (元タスク完了 / 割り込み running / 残作業 planned) + split note 3 件自動記録 |
57
+ | `add_parallel_task` | 進行中タスクと並行する新規タスクを追加。既定で running 状態 + 即計測開始 |
58
+ | `reorder_task` | 並び順 (order_index) と所属タイムブロックを直接指定 (慣習 10 単位) |
50
59
 
51
60
  入力 `id` は full UUID。`list_tasks` の結果から取って各 tool に渡す想定。
52
61
  `estimated_seconds` は秒数の整数 (CLI と違って文字列パースはしない)。
@@ -98,9 +107,7 @@ cp .mcp.json.example .mcp.json # repo root で
98
107
  "mcpServers": {
99
108
  "taskchute": {
100
109
  "command": "node",
101
- "args": [
102
- "/path/to/TaskChute/packages/mcp/dist/index.js"
103
- ],
110
+ "args": ["/path/to/TaskChute/packages/mcp/dist/index.js"],
104
111
  "env": {
105
112
  "TASKCHUTE_SUPABASE_URL": "https://YOUR_PROJECT.supabase.co",
106
113
  "TASKCHUTE_SUPABASE_PUBLISHABLE_KEY": "sb_publishable_..."
package/dist/index.js CHANGED
@@ -23,9 +23,7 @@ function readSupabaseConfigFromEnv() {
23
23
  const url = process.env.TASKCHUTE_SUPABASE_URL;
24
24
  const publishableKey = process.env.TASKCHUTE_SUPABASE_PUBLISHABLE_KEY;
25
25
  if (!url || !publishableKey) {
26
- throw new Error(
27
- `Supabase \u306E\u74B0\u5883\u5909\u6570\u304C\u898B\u3064\u304B\u3089\u306A\u3044\u3002${ENV_URL} \u3068 ${ENV_KEY} \u3092\u8A2D\u5B9A\u3057\u3066\u306D\u301C`
28
- );
26
+ throw new Error(`Supabase \u306E\u74B0\u5883\u5909\u6570\u304C\u898B\u3064\u304B\u3089\u306A\u3044\u3002${ENV_URL} \u3068 ${ENV_KEY} \u3092\u8A2D\u5B9A\u3057\u3066\u306D\u301C`);
29
27
  }
30
28
  return { url, publishableKey };
31
29
  }
@@ -204,13 +202,10 @@ async function deriveKeyMaterial(password, salt, params = DEFAULT_ARGON2ID_PARAM
204
202
  return copyToArrayBuffer(hash);
205
203
  }
206
204
  async function importKEK(material) {
207
- return crypto.subtle.importKey(
208
- "raw",
209
- material,
210
- { name: "AES-GCM" },
211
- false,
212
- ["wrapKey", "unwrapKey"]
213
- );
205
+ return crypto.subtle.importKey("raw", material, { name: "AES-GCM" }, false, [
206
+ "wrapKey",
207
+ "unwrapKey"
208
+ ]);
214
209
  }
215
210
  async function deriveKEK(password, salt, params = DEFAULT_ARGON2ID_PARAMS) {
216
211
  const material = await deriveKeyMaterial(password, salt, params);
@@ -414,11 +409,7 @@ async function unlockMasterKey(snapshot, method) {
414
409
  snapshot.passphrase.kdfParams
415
410
  );
416
411
  try {
417
- return await unwrapMasterKey(
418
- snapshot.passphrase.wrapped,
419
- snapshot.passphrase.iv,
420
- kek
421
- );
412
+ return await unwrapMasterKey(snapshot.passphrase.wrapped, snapshot.passphrase.iv, kek);
422
413
  } catch {
423
414
  throw new UnlockError("Wrong passphrase", "wrong-key");
424
415
  }
@@ -434,11 +425,7 @@ async function unlockMasterKey(snapshot, method) {
434
425
  );
435
426
  const kek = await importKEK(material);
436
427
  try {
437
- return await unwrapMasterKey(
438
- snapshot.recovery.wrapped,
439
- snapshot.recovery.iv,
440
- kek
441
- );
428
+ return await unwrapMasterKey(snapshot.recovery.wrapped, snapshot.recovery.iv, kek);
442
429
  } catch {
443
430
  throw new UnlockError("Wrong recovery key", "wrong-key");
444
431
  }
@@ -492,13 +479,10 @@ function dateToIsoOrNull(date) {
492
479
  // ../infra-supabase/src/supabase-context-repository.ts
493
480
  async function rowToContext(row, cryptoSession) {
494
481
  const [name, description] = await Promise.all([
495
- decryptRequiredTextFromWire(
496
- cryptoSession,
497
- row.name_ciphertext,
498
- row.name_iv,
499
- row.key_id,
500
- { entityId: row.id, field: "name" }
501
- ),
482
+ decryptRequiredTextFromWire(cryptoSession, row.name_ciphertext, row.name_iv, row.key_id, {
483
+ entityId: row.id,
484
+ field: "name"
485
+ }),
502
486
  decryptOptionalTextFromWire(
503
487
  cryptoSession,
504
488
  row.description_ciphertext,
@@ -615,13 +599,10 @@ var SupabaseContextRepository = class {
615
599
  // ../infra-supabase/src/supabase-project-repository.ts
616
600
  async function rowToProject(row, cryptoSession) {
617
601
  const [name, description] = await Promise.all([
618
- decryptRequiredTextFromWire(
619
- cryptoSession,
620
- row.name_ciphertext,
621
- row.name_iv,
622
- row.key_id,
623
- { entityId: row.id, field: "name" }
624
- ),
602
+ decryptRequiredTextFromWire(cryptoSession, row.name_ciphertext, row.name_iv, row.key_id, {
603
+ entityId: row.id,
604
+ field: "name"
605
+ }),
625
606
  decryptOptionalTextFromWire(
626
607
  cryptoSession,
627
608
  row.description_ciphertext,
@@ -775,13 +756,10 @@ var SupabaseRoutineExpansionRepository = class {
775
756
  // ../infra-supabase/src/supabase-routine-repository.ts
776
757
  async function rowToRoutine(row, cryptoSession) {
777
758
  const [title, description] = await Promise.all([
778
- decryptRequiredTextFromWire(
779
- cryptoSession,
780
- row.title_ciphertext,
781
- row.title_iv,
782
- row.key_id,
783
- { entityId: row.id, field: "title" }
784
- ),
759
+ decryptRequiredTextFromWire(cryptoSession, row.title_ciphertext, row.title_iv, row.key_id, {
760
+ entityId: row.id,
761
+ field: "title"
762
+ }),
785
763
  decryptOptionalTextFromWire(
786
764
  cryptoSession,
787
765
  row.description_ciphertext,
@@ -922,13 +900,10 @@ var SupabaseRoutineRepository = class {
922
900
  // ../infra-supabase/src/supabase-tag-repository.ts
923
901
  async function rowToTag(row, cryptoSession) {
924
902
  const [name, description] = await Promise.all([
925
- decryptRequiredTextFromWire(
926
- cryptoSession,
927
- row.name_ciphertext,
928
- row.name_iv,
929
- row.key_id,
930
- { entityId: row.id, field: "name" }
931
- ),
903
+ decryptRequiredTextFromWire(cryptoSession, row.name_ciphertext, row.name_iv, row.key_id, {
904
+ entityId: row.id,
905
+ field: "name"
906
+ }),
932
907
  decryptOptionalTextFromWire(
933
908
  cryptoSession,
934
909
  row.description_ciphertext,
@@ -1039,13 +1014,10 @@ var SupabaseTagRepository = class {
1039
1014
  // ../infra-supabase/src/supabase-task-note-repository.ts
1040
1015
  async function rowToTaskNote(row, cryptoSession) {
1041
1016
  const [body, metaJson, historyJson] = await Promise.all([
1042
- decryptRequiredTextFromWire(
1043
- cryptoSession,
1044
- row.body_ciphertext,
1045
- row.body_iv,
1046
- row.key_id,
1047
- { entityId: row.id, field: "body" }
1048
- ),
1017
+ decryptRequiredTextFromWire(cryptoSession, row.body_ciphertext, row.body_iv, row.key_id, {
1018
+ entityId: row.id,
1019
+ field: "body"
1020
+ }),
1049
1021
  decryptOptionalTextFromWire(
1050
1022
  cryptoSession,
1051
1023
  row.metadata_ciphertext,
@@ -1136,9 +1108,7 @@ async function encryptTaskNoteFieldsInto(row, entityId, patch, cryptoSession) {
1136
1108
  if (touched) {
1137
1109
  const keyId = cryptoSession.getCurrentKeyId();
1138
1110
  if (keyId === null) {
1139
- throw new Error(
1140
- "SupabaseTaskNoteRepository: session became locked mid-write"
1141
- );
1111
+ throw new Error("SupabaseTaskNoteRepository: session became locked mid-write");
1142
1112
  }
1143
1113
  row.key_id = keyId;
1144
1114
  }
@@ -1165,9 +1135,7 @@ var SupabaseTaskNoteRepository = class {
1165
1135
  if (taskIds.length === 0) return result;
1166
1136
  const { data, error } = await this.supabase.from("task_notes").select("*").in("task_id", taskIds).order("created_at", { ascending: true });
1167
1137
  if (error) throw error;
1168
- const decoded = await Promise.all(
1169
- data.map((row) => rowToTaskNote(row, this.cryptoSession))
1170
- );
1138
+ const decoded = await Promise.all(data.map((row) => rowToTaskNote(row, this.cryptoSession)));
1171
1139
  for (const note of decoded) {
1172
1140
  const bucket = result.get(note.taskId);
1173
1141
  if (bucket) bucket.push(note);
@@ -1216,13 +1184,10 @@ var SupabaseTaskNoteRepository = class {
1216
1184
  // ../infra-supabase/src/supabase-task-repository.ts
1217
1185
  async function rowToTask(row, cryptoSession) {
1218
1186
  const [title, description] = await Promise.all([
1219
- decryptRequiredTextFromWire(
1220
- cryptoSession,
1221
- row.title_ciphertext,
1222
- row.title_iv,
1223
- row.key_id,
1224
- { entityId: row.id, field: "title" }
1225
- ),
1187
+ decryptRequiredTextFromWire(cryptoSession, row.title_ciphertext, row.title_iv, row.key_id, {
1188
+ entityId: row.id,
1189
+ field: "title"
1190
+ }),
1226
1191
  decryptOptionalTextFromWire(
1227
1192
  cryptoSession,
1228
1193
  row.description_ciphertext,
@@ -1666,20 +1631,41 @@ async function startTask(repo, taskId) {
1666
1631
  return repo.update(taskId, patch);
1667
1632
  }
1668
1633
  async function pauseTask(repo, taskId) {
1669
- const task = await repo.findById(taskId);
1670
- if (!task) throw new Error(`Task not found: ${taskId}`);
1671
- if (!task.currentSession) {
1634
+ const target = await repo.findById(taskId);
1635
+ if (!target) throw new Error(`Task not found: ${taskId}`);
1636
+ if (!target.currentSession) {
1672
1637
  throw new Error(`Task is not currently running: ${taskId}`);
1673
1638
  }
1674
- const newSession = {
1675
- startedAt: task.currentSession.startedAt,
1676
- finishedAt: /* @__PURE__ */ new Date()
1639
+ const now = /* @__PURE__ */ new Date();
1640
+ const closedSession = {
1641
+ startedAt: target.currentSession.startedAt,
1642
+ finishedAt: now
1677
1643
  };
1678
- return repo.update(taskId, {
1679
- status: "paused",
1680
- sessions: [...task.sessions, newSession],
1644
+ const finishedOriginal = await repo.update(target.id, {
1645
+ status: "completed",
1646
+ sessions: [...target.sessions, closedSession],
1681
1647
  currentSession: void 0
1682
1648
  });
1649
+ const elapsed = calculateActualSeconds(finishedOriginal);
1650
+ const continuation = await repo.create({
1651
+ date: target.date,
1652
+ title: target.title,
1653
+ description: target.description,
1654
+ estimatedSeconds: Math.max(0, Math.floor(target.estimatedSeconds - elapsed)),
1655
+ sessions: [],
1656
+ status: "planned",
1657
+ orderIndex: target.orderIndex + 1,
1658
+ splitFromTaskId: target.id,
1659
+ projectId: target.projectId,
1660
+ contextId: target.contextId,
1661
+ // continuation は「中断したタスクの残り」。中断時刻を予定時刻にすることで
1662
+ // ソート上は元タスクの直後 (effectiveTime 同値 + orderIndex 順) に並び、
1663
+ // TimeBlock 境界をまたいだ場合も自然に新しいブロックへ追随する。
1664
+ scheduledStartTime: toTimeString(now),
1665
+ tagIds: target.tagIds,
1666
+ isBackground: target.isBackground
1667
+ });
1668
+ return { finishedOriginal, continuation };
1683
1669
  }
1684
1670
  async function skipTask(repo, taskId) {
1685
1671
  const task = await repo.findById(taskId);
@@ -1834,19 +1820,13 @@ function buildSetActualTimesPatch(task, input) {
1834
1820
  // ../domain/src/task-reorder.ts
1835
1821
  function computeNextOrderIndex(existingTasks) {
1836
1822
  if (existingTasks.length === 0) return 10;
1837
- const max = existingTasks.reduce(
1838
- (acc, t) => t.orderIndex > acc ? t.orderIndex : acc,
1839
- 0
1840
- );
1823
+ const max = existingTasks.reduce((acc, t) => t.orderIndex > acc ? t.orderIndex : acc, 0);
1841
1824
  return max + 10;
1842
1825
  }
1843
1826
 
1844
1827
  // ../domain/src/task-suggest.ts
1845
1828
  function normalizeForSearch(str) {
1846
- return str.trim().toLowerCase().replace(
1847
- /[ぁ-ゖ]/g,
1848
- (ch) => String.fromCharCode(ch.charCodeAt(0) + 96)
1849
- );
1829
+ return str.trim().toLowerCase().replace(/[ぁ-ゖ]/g, (ch) => String.fromCharCode(ch.charCodeAt(0) + 96));
1850
1830
  }
1851
1831
  function fuzzyScore(query, target) {
1852
1832
  const q = query;
@@ -1909,7 +1889,6 @@ function findSimilarTasks(query, tasks, limit = 5) {
1909
1889
  function describeStatusTransition(from, to) {
1910
1890
  if (from === "planned" && to === "running") return "\u958B\u59CB";
1911
1891
  if (from === "paused" && to === "running") return "\u518D\u958B";
1912
- if (from === "running" && to === "paused") return "\u4E00\u6642\u505C\u6B62";
1913
1892
  if (to === "completed") return "\u5B8C\u4E86";
1914
1893
  if (to === "skipped") return "\u30B9\u30AD\u30C3\u30D7";
1915
1894
  if (to === "planned") return "\u672A\u7740\u624B\u306B\u623B\u3059";
@@ -2159,6 +2138,42 @@ async function interruptTaskWithAudit(taskRepo, taskNoteRepo, params) {
2159
2138
  }
2160
2139
  return result;
2161
2140
  }
2141
+ async function pauseTaskWithAudit(taskRepo, taskNoteRepo, taskId) {
2142
+ const result = await pauseTask(taskRepo, taskId);
2143
+ const notes = buildPauseSplitNoteInputs(result.finishedOriginal, result.continuation);
2144
+ try {
2145
+ await Promise.all(notes.map((input) => taskNoteRepo.create(input)));
2146
+ } catch (err) {
2147
+ console.error("[audit] failed to record pause split notes", {
2148
+ sourceTaskId: result.finishedOriginal.id,
2149
+ err
2150
+ });
2151
+ }
2152
+ return result;
2153
+ }
2154
+ function buildPauseSplitNoteInputs(source, continuation) {
2155
+ const sourceTitle = titleOrPlaceholder(source);
2156
+ return [
2157
+ {
2158
+ taskId: source.id,
2159
+ type: "split",
2160
+ body: `\u4E2D\u65AD\u3057\u3066\u6B8B\u4F5C\u696D\u3092\u5206\u5272\u3002\u7D9A\u304D\u306F\u5225\u30BF\u30B9\u30AF\u3067\u7D99\u7D9A\u3002`,
2161
+ metadata: {
2162
+ event: "pause-source",
2163
+ continuationTaskId: continuation.id
2164
+ }
2165
+ },
2166
+ {
2167
+ taskId: continuation.id,
2168
+ type: "split",
2169
+ body: `\u300C${sourceTitle}\u300D\u306E\u6B8B\u4F5C\u696D\u3068\u3057\u3066\u7D99\u7D9A\u3002`,
2170
+ metadata: {
2171
+ event: "pause-continuation",
2172
+ sourceTaskId: source.id
2173
+ }
2174
+ }
2175
+ ];
2176
+ }
2162
2177
  function buildSplitNoteInputs(source, interruption, continuation) {
2163
2178
  const sourceTitle = titleOrPlaceholder(source);
2164
2179
  const interruptionTitle = titleOrPlaceholder(interruption);
@@ -2195,8 +2210,6 @@ function buildSplitNoteInputs(source, interruption, continuation) {
2195
2210
  }
2196
2211
  ];
2197
2212
  }
2198
-
2199
- // ../domain/src/routine-expansion.ts
2200
2213
  function buildRoutineTaskPatch(routine) {
2201
2214
  return {
2202
2215
  title: routine.title,
@@ -2518,7 +2531,10 @@ async function addTaskHandler(ctx, input) {
2518
2531
  }
2519
2532
  if (input.tag_ids && input.tag_ids.length > 0) {
2520
2533
  const checked = await Promise.all(
2521
- input.tag_ids.map(async (id) => ({ id, exists: await ctx.repos.tag.findById(id) !== null }))
2534
+ input.tag_ids.map(async (id) => ({
2535
+ id,
2536
+ exists: await ctx.repos.tag.findById(id) !== null
2537
+ }))
2522
2538
  );
2523
2539
  const missing = checked.filter((c) => !c.exists).map((c) => c.id);
2524
2540
  if (missing.length > 0) {
@@ -2719,9 +2735,7 @@ var createContextInputSchema = z.object({
2719
2735
  description: z.string().optional(),
2720
2736
  icon: z.string().optional().describe("\u7D75\u6587\u5B57\u307E\u305F\u306F\u30A2\u30A4\u30B3\u30F3\u540D (\u4F8B: \u{1F3E0} / mdi-home)"),
2721
2737
  color: z.string().optional().describe("UI \u8272 (\u4F8B: #ff6b6b)"),
2722
- notify_at_scheduled_start: z.boolean().optional().describe(
2723
- "true \u306A\u3089\u3001\u3053\u306E\u30B3\u30F3\u30C6\u30AD\u30B9\u30C8\u3092\u4ED8\u3051\u305F\u65B0\u898F/\u66F4\u65B0\u30BF\u30B9\u30AF\u306E\u901A\u77E5\u3092 ON \u306B\u81EA\u52D5\u7D99\u627F\u3059\u308B"
2724
- )
2738
+ notify_at_scheduled_start: z.boolean().optional().describe("true \u306A\u3089\u3001\u3053\u306E\u30B3\u30F3\u30C6\u30AD\u30B9\u30C8\u3092\u4ED8\u3051\u305F\u65B0\u898F/\u66F4\u65B0\u30BF\u30B9\u30AF\u306E\u901A\u77E5\u3092 ON \u306B\u81EA\u52D5\u7D99\u627F\u3059\u308B")
2725
2739
  });
2726
2740
  async function createContextHandler(ctx, input) {
2727
2741
  const createInput = {
@@ -2772,12 +2786,23 @@ var frequencySchema = z.discriminatedUnion("type", [
2772
2786
  z.object({ type: z.literal("daily") }),
2773
2787
  z.object({
2774
2788
  type: z.literal("weekly"),
2775
- weekdays: z.array(z.number().int().min(0).max(6)).min(1, "at least 1 weekday")
2789
+ weekdays: z.array(z.number().int().min(0).max(6)).min(1, "at least 1 weekday"),
2790
+ intervalWeeks: z.number().int().min(1).max(4).optional().describe("1=\u6BCE\u9031 (default) / 2=\u9694\u9031 / 3+ \u3082\u540C\u69D8\u3002anchorDate \u3068\u7D44\u3067\u4F7F\u3046"),
2791
+ anchorDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("intervalWeeks>=2 \u306E\u57FA\u6E96\u9031\u3092\u542B\u3080\u65E5\u4ED8\u3002\u4F8B: 2026-06-22")
2776
2792
  }),
2777
2793
  z.object({
2778
2794
  type: z.literal("monthly"),
2779
2795
  days: z.array(z.number().int().min(1).max(31)).min(1, "at least 1 day")
2780
2796
  }),
2797
+ z.object({
2798
+ type: z.literal("monthly-nth-weekday"),
2799
+ occurrences: z.array(
2800
+ z.object({
2801
+ nth: z.number().int().refine((n) => n >= 1 && n <= 5 || n === -1, "nth must be 1-5 or -1 (last)"),
2802
+ weekday: z.number().int().min(0).max(6)
2803
+ })
2804
+ ).min(1, "at least 1 occurrence").describe("\u4F8B: [{nth:2,weekday:5},{nth:4,weekday:5}] \u3067\u7B2C2\u30FB\u7B2C4\u91D1\u66DC\u3002nth=-1 \u306F\u6700\u7D42\u9031")
2805
+ }),
2781
2806
  z.object({ type: z.literal("custom"), cron: z.string().min(1) })
2782
2807
  ]);
2783
2808
  var createRoutineInputSchema = z.object({
@@ -3128,12 +3153,13 @@ var pauseTaskInputSchema = z.object({
3128
3153
  id: z.string().uuid({ message: "id must be a UUID" })
3129
3154
  });
3130
3155
  async function pauseTaskHandler(ctx, input) {
3131
- const existing = await ctx.repos.task.findById(input.id);
3132
- if (!existing) {
3133
- throw new McpToolError(`Task not found: ${input.id}`);
3134
- }
3135
- const updated = await pauseTask(ctx.repos.task, input.id);
3136
- return { task: taskSummaryFrom(updated) };
3156
+ const result = await wrapDomainErrors(
3157
+ () => pauseTaskWithAudit(ctx.repos.task, ctx.repos.taskNote, input.id)
3158
+ );
3159
+ return {
3160
+ finished_original: taskSummaryFrom(result.finishedOriginal),
3161
+ continuation: taskSummaryFrom(result.continuation)
3162
+ };
3137
3163
  }
3138
3164
  var reopenTaskInputSchema = z.object({
3139
3165
  id: z.string().uuid({ message: "id must be a UUID" })
@@ -3216,9 +3242,7 @@ var updateContextInputSchema = z.object({
3216
3242
  icon: z.union([z.string(), z.null()]).optional(),
3217
3243
  color: z.union([z.string(), z.null()]).optional(),
3218
3244
  status: z.enum(["active", "archived"]).optional().describe("archived \u5316\u306F archive_context tool \u63A8\u5968"),
3219
- notify_at_scheduled_start: z.boolean().optional().describe(
3220
- "true \u306A\u3089\u3001\u3053\u306E\u30B3\u30F3\u30C6\u30AD\u30B9\u30C8\u3092\u4ED8\u3051\u305F\u65B0\u898F/\u66F4\u65B0\u30BF\u30B9\u30AF\u306E\u901A\u77E5\u3092 ON \u306B\u81EA\u52D5\u7D99\u627F\u3059\u308B"
3221
- )
3245
+ notify_at_scheduled_start: z.boolean().optional().describe("true \u306A\u3089\u3001\u3053\u306E\u30B3\u30F3\u30C6\u30AD\u30B9\u30C8\u3092\u4ED8\u3051\u305F\u65B0\u898F/\u66F4\u65B0\u30BF\u30B9\u30AF\u306E\u901A\u77E5\u3092 ON \u306B\u81EA\u52D5\u7D99\u627F\u3059\u308B")
3222
3246
  });
3223
3247
  async function updateContextHandler(ctx, input) {
3224
3248
  const existing = await ctx.repos.context.findById(input.id);
@@ -3296,12 +3320,23 @@ var frequencySchema2 = z.discriminatedUnion("type", [
3296
3320
  z.object({ type: z.literal("daily") }),
3297
3321
  z.object({
3298
3322
  type: z.literal("weekly"),
3299
- weekdays: z.array(z.number().int().min(0).max(6)).min(1)
3323
+ weekdays: z.array(z.number().int().min(0).max(6)).min(1),
3324
+ intervalWeeks: z.number().int().min(1).max(4).optional(),
3325
+ anchorDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional()
3300
3326
  }),
3301
3327
  z.object({
3302
3328
  type: z.literal("monthly"),
3303
3329
  days: z.array(z.number().int().min(1).max(31)).min(1)
3304
3330
  }),
3331
+ z.object({
3332
+ type: z.literal("monthly-nth-weekday"),
3333
+ occurrences: z.array(
3334
+ z.object({
3335
+ nth: z.number().int().refine((n) => n >= 1 && n <= 5 || n === -1, "nth must be 1-5 or -1 (last)"),
3336
+ weekday: z.number().int().min(0).max(6)
3337
+ })
3338
+ ).min(1)
3339
+ }),
3305
3340
  z.object({ type: z.literal("custom"), cron: z.string().min(1) })
3306
3341
  ]);
3307
3342
  var updateRoutineInputSchema = z.object({
@@ -3936,7 +3971,7 @@ function createMcpServer(opts) {
3936
3971
 
3937
3972
  // src/index.ts
3938
3973
  async function main() {
3939
- const version = "0.4.0" ;
3974
+ const version = "0.6.0" ;
3940
3975
  const server = createMcpServer({ version });
3941
3976
  const transport = new StdioServerTransport();
3942
3977
  await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fukuchang/taskchute-mcp",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "TaskChute MCP server — Model Context Protocol 経由で E2EE 暗号化された TaskChute タスクを Claude / 各種 MCP クライアントから操作する",
5
5
  "license": "MIT",
6
6
  "author": "fukuchang",
@@ -23,7 +23,7 @@
23
23
  "e2ee"
24
24
  ],
25
25
  "engines": {
26
- "node": ">=20"
26
+ "node": "^22.23.4 || ^24.22.1 || ^26.12.1"
27
27
  },
28
28
  "type": "module",
29
29
  "main": "./dist/index.js",
@@ -48,14 +48,14 @@
48
48
  "zod": "^4.0.0"
49
49
  },
50
50
  "devDependencies": {
51
- "@types/node": "^24.12.3",
52
- "tsup": "^8.5.1",
53
- "tsx": "^4.20.0",
54
- "typescript": "~6.0.2",
55
51
  "@taskchute/crypto": "0.0.0",
56
52
  "@taskchute/domain": "0.0.0",
57
53
  "@taskchute/infra-supabase": "0.0.0",
58
- "@taskchute/node-shared": "0.0.0"
54
+ "@taskchute/node-shared": "0.0.0",
55
+ "@types/node": "^24.12.3",
56
+ "tsup": "^8.5.1",
57
+ "tsx": "^4.20.0",
58
+ "typescript": "~6.0.2"
59
59
  },
60
60
  "scripts": {
61
61
  "start": "tsx src/index.ts",