@fukuchang/taskchute-mcp 0.3.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);
@@ -218,14 +213,14 @@ async function deriveKEK(password, salt, params = DEFAULT_ARGON2ID_PARAMS) {
218
213
  }
219
214
 
220
215
  // ../crypto/src/wrap.ts
221
- async function unwrapMasterKey(wrapped, iv, kek) {
216
+ async function unwrapMasterKey(wrapped, iv, kek, options = {}) {
222
217
  return crypto.subtle.unwrapKey(
223
218
  "raw",
224
219
  wrapped,
225
220
  kek,
226
221
  { name: "AES-GCM", iv },
227
222
  { name: "AES-GCM", length: 256 },
228
- false,
223
+ options.extractable ?? false,
229
224
  ["encrypt", "decrypt"]
230
225
  );
231
226
  }
@@ -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,
@@ -511,6 +495,7 @@ async function rowToContext(row, cryptoSession) {
511
495
  id: row.id,
512
496
  name,
513
497
  status: row.status,
498
+ notifyAtScheduledStart: row.notify_at_scheduled_start,
514
499
  createdAt: new Date(row.created_at),
515
500
  updatedAt: new Date(row.updated_at)
516
501
  };
@@ -524,6 +509,9 @@ function nonEncryptedFieldsToRow(input) {
524
509
  if ("icon" in input) row.icon = input.icon ?? null;
525
510
  if ("color" in input) row.color = input.color ?? null;
526
511
  if ("status" in input && input.status !== void 0) row.status = input.status;
512
+ if ("notifyAtScheduledStart" in input && input.notifyAtScheduledStart !== void 0) {
513
+ row.notify_at_scheduled_start = input.notifyAtScheduledStart;
514
+ }
527
515
  return row;
528
516
  }
529
517
  async function encryptContextFieldsInto(row, entityId, patch, cryptoSession) {
@@ -611,13 +599,10 @@ var SupabaseContextRepository = class {
611
599
  // ../infra-supabase/src/supabase-project-repository.ts
612
600
  async function rowToProject(row, cryptoSession) {
613
601
  const [name, description] = await Promise.all([
614
- decryptRequiredTextFromWire(
615
- cryptoSession,
616
- row.name_ciphertext,
617
- row.name_iv,
618
- row.key_id,
619
- { entityId: row.id, field: "name" }
620
- ),
602
+ decryptRequiredTextFromWire(cryptoSession, row.name_ciphertext, row.name_iv, row.key_id, {
603
+ entityId: row.id,
604
+ field: "name"
605
+ }),
621
606
  decryptOptionalTextFromWire(
622
607
  cryptoSession,
623
608
  row.description_ciphertext,
@@ -771,13 +756,10 @@ var SupabaseRoutineExpansionRepository = class {
771
756
  // ../infra-supabase/src/supabase-routine-repository.ts
772
757
  async function rowToRoutine(row, cryptoSession) {
773
758
  const [title, description] = await Promise.all([
774
- decryptRequiredTextFromWire(
775
- cryptoSession,
776
- row.title_ciphertext,
777
- row.title_iv,
778
- row.key_id,
779
- { entityId: row.id, field: "title" }
780
- ),
759
+ decryptRequiredTextFromWire(cryptoSession, row.title_ciphertext, row.title_iv, row.key_id, {
760
+ entityId: row.id,
761
+ field: "title"
762
+ }),
781
763
  decryptOptionalTextFromWire(
782
764
  cryptoSession,
783
765
  row.description_ciphertext,
@@ -918,13 +900,10 @@ var SupabaseRoutineRepository = class {
918
900
  // ../infra-supabase/src/supabase-tag-repository.ts
919
901
  async function rowToTag(row, cryptoSession) {
920
902
  const [name, description] = await Promise.all([
921
- decryptRequiredTextFromWire(
922
- cryptoSession,
923
- row.name_ciphertext,
924
- row.name_iv,
925
- row.key_id,
926
- { entityId: row.id, field: "name" }
927
- ),
903
+ decryptRequiredTextFromWire(cryptoSession, row.name_ciphertext, row.name_iv, row.key_id, {
904
+ entityId: row.id,
905
+ field: "name"
906
+ }),
928
907
  decryptOptionalTextFromWire(
929
908
  cryptoSession,
930
909
  row.description_ciphertext,
@@ -1035,13 +1014,10 @@ var SupabaseTagRepository = class {
1035
1014
  // ../infra-supabase/src/supabase-task-note-repository.ts
1036
1015
  async function rowToTaskNote(row, cryptoSession) {
1037
1016
  const [body, metaJson, historyJson] = await Promise.all([
1038
- decryptRequiredTextFromWire(
1039
- cryptoSession,
1040
- row.body_ciphertext,
1041
- row.body_iv,
1042
- row.key_id,
1043
- { entityId: row.id, field: "body" }
1044
- ),
1017
+ decryptRequiredTextFromWire(cryptoSession, row.body_ciphertext, row.body_iv, row.key_id, {
1018
+ entityId: row.id,
1019
+ field: "body"
1020
+ }),
1045
1021
  decryptOptionalTextFromWire(
1046
1022
  cryptoSession,
1047
1023
  row.metadata_ciphertext,
@@ -1132,9 +1108,7 @@ async function encryptTaskNoteFieldsInto(row, entityId, patch, cryptoSession) {
1132
1108
  if (touched) {
1133
1109
  const keyId = cryptoSession.getCurrentKeyId();
1134
1110
  if (keyId === null) {
1135
- throw new Error(
1136
- "SupabaseTaskNoteRepository: session became locked mid-write"
1137
- );
1111
+ throw new Error("SupabaseTaskNoteRepository: session became locked mid-write");
1138
1112
  }
1139
1113
  row.key_id = keyId;
1140
1114
  }
@@ -1161,9 +1135,7 @@ var SupabaseTaskNoteRepository = class {
1161
1135
  if (taskIds.length === 0) return result;
1162
1136
  const { data, error } = await this.supabase.from("task_notes").select("*").in("task_id", taskIds).order("created_at", { ascending: true });
1163
1137
  if (error) throw error;
1164
- const decoded = await Promise.all(
1165
- data.map((row) => rowToTaskNote(row, this.cryptoSession))
1166
- );
1138
+ const decoded = await Promise.all(data.map((row) => rowToTaskNote(row, this.cryptoSession)));
1167
1139
  for (const note of decoded) {
1168
1140
  const bucket = result.get(note.taskId);
1169
1141
  if (bucket) bucket.push(note);
@@ -1212,13 +1184,10 @@ var SupabaseTaskNoteRepository = class {
1212
1184
  // ../infra-supabase/src/supabase-task-repository.ts
1213
1185
  async function rowToTask(row, cryptoSession) {
1214
1186
  const [title, description] = await Promise.all([
1215
- decryptRequiredTextFromWire(
1216
- cryptoSession,
1217
- row.title_ciphertext,
1218
- row.title_iv,
1219
- row.key_id,
1220
- { entityId: row.id, field: "title" }
1221
- ),
1187
+ decryptRequiredTextFromWire(cryptoSession, row.title_ciphertext, row.title_iv, row.key_id, {
1188
+ entityId: row.id,
1189
+ field: "title"
1190
+ }),
1222
1191
  decryptOptionalTextFromWire(
1223
1192
  cryptoSession,
1224
1193
  row.description_ciphertext,
@@ -1662,20 +1631,41 @@ async function startTask(repo, taskId) {
1662
1631
  return repo.update(taskId, patch);
1663
1632
  }
1664
1633
  async function pauseTask(repo, taskId) {
1665
- const task = await repo.findById(taskId);
1666
- if (!task) throw new Error(`Task not found: ${taskId}`);
1667
- if (!task.currentSession) {
1634
+ const target = await repo.findById(taskId);
1635
+ if (!target) throw new Error(`Task not found: ${taskId}`);
1636
+ if (!target.currentSession) {
1668
1637
  throw new Error(`Task is not currently running: ${taskId}`);
1669
1638
  }
1670
- const newSession = {
1671
- startedAt: task.currentSession.startedAt,
1672
- finishedAt: /* @__PURE__ */ new Date()
1639
+ const now = /* @__PURE__ */ new Date();
1640
+ const closedSession = {
1641
+ startedAt: target.currentSession.startedAt,
1642
+ finishedAt: now
1673
1643
  };
1674
- return repo.update(taskId, {
1675
- status: "paused",
1676
- sessions: [...task.sessions, newSession],
1644
+ const finishedOriginal = await repo.update(target.id, {
1645
+ status: "completed",
1646
+ sessions: [...target.sessions, closedSession],
1677
1647
  currentSession: void 0
1678
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 };
1679
1669
  }
1680
1670
  async function skipTask(repo, taskId) {
1681
1671
  const task = await repo.findById(taskId);
@@ -1830,19 +1820,13 @@ function buildSetActualTimesPatch(task, input) {
1830
1820
  // ../domain/src/task-reorder.ts
1831
1821
  function computeNextOrderIndex(existingTasks) {
1832
1822
  if (existingTasks.length === 0) return 10;
1833
- const max = existingTasks.reduce(
1834
- (acc, t) => t.orderIndex > acc ? t.orderIndex : acc,
1835
- 0
1836
- );
1823
+ const max = existingTasks.reduce((acc, t) => t.orderIndex > acc ? t.orderIndex : acc, 0);
1837
1824
  return max + 10;
1838
1825
  }
1839
1826
 
1840
1827
  // ../domain/src/task-suggest.ts
1841
1828
  function normalizeForSearch(str) {
1842
- return str.trim().toLowerCase().replace(
1843
- /[ぁ-ゖ]/g,
1844
- (ch) => String.fromCharCode(ch.charCodeAt(0) + 96)
1845
- );
1829
+ return str.trim().toLowerCase().replace(/[ぁ-ゖ]/g, (ch) => String.fromCharCode(ch.charCodeAt(0) + 96));
1846
1830
  }
1847
1831
  function fuzzyScore(query, target) {
1848
1832
  const q = query;
@@ -1905,7 +1889,6 @@ function findSimilarTasks(query, tasks, limit = 5) {
1905
1889
  function describeStatusTransition(from, to) {
1906
1890
  if (from === "planned" && to === "running") return "\u958B\u59CB";
1907
1891
  if (from === "paused" && to === "running") return "\u518D\u958B";
1908
- if (from === "running" && to === "paused") return "\u4E00\u6642\u505C\u6B62";
1909
1892
  if (to === "completed") return "\u5B8C\u4E86";
1910
1893
  if (to === "skipped") return "\u30B9\u30AD\u30C3\u30D7";
1911
1894
  if (to === "planned") return "\u672A\u7740\u624B\u306B\u623B\u3059";
@@ -2155,6 +2138,42 @@ async function interruptTaskWithAudit(taskRepo, taskNoteRepo, params) {
2155
2138
  }
2156
2139
  return result;
2157
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
+ }
2158
2177
  function buildSplitNoteInputs(source, interruption, continuation) {
2159
2178
  const sourceTitle = titleOrPlaceholder(source);
2160
2179
  const interruptionTitle = titleOrPlaceholder(interruption);
@@ -2191,8 +2210,6 @@ function buildSplitNoteInputs(source, interruption, continuation) {
2191
2210
  }
2192
2211
  ];
2193
2212
  }
2194
-
2195
- // ../domain/src/routine-expansion.ts
2196
2213
  function buildRoutineTaskPatch(routine) {
2197
2214
  return {
2198
2215
  title: routine.title,
@@ -2210,6 +2227,14 @@ function pickPropagationTargets(routine, tasks, today) {
2210
2227
  );
2211
2228
  }
2212
2229
 
2230
+ // ../domain/src/context-task-inheritance.ts
2231
+ function applyContextNotificationInheritance(input, context) {
2232
+ if (context?.notifyAtScheduledStart !== true) return input;
2233
+ const current = input.notifyAtScheduledStart;
2234
+ if (current !== void 0) return input;
2235
+ return { ...input, notifyAtScheduledStart: true };
2236
+ }
2237
+
2213
2238
  // ../infra-supabase/src/audit-task-repository.ts
2214
2239
  var AuditingTaskRepository = class {
2215
2240
  // `erasableSyntaxOnly` 配下では parameter property が使えないので、明示的に
@@ -2499,13 +2524,17 @@ async function addTaskHandler(ctx, input) {
2499
2524
  const project = await ctx.repos.project.findById(input.project_id);
2500
2525
  if (!project) throw new McpToolError(`Project not found: ${input.project_id}`);
2501
2526
  }
2527
+ let context = null;
2502
2528
  if (input.context_id) {
2503
- const context = await ctx.repos.context.findById(input.context_id);
2529
+ context = await ctx.repos.context.findById(input.context_id);
2504
2530
  if (!context) throw new McpToolError(`Context not found: ${input.context_id}`);
2505
2531
  }
2506
2532
  if (input.tag_ids && input.tag_ids.length > 0) {
2507
2533
  const checked = await Promise.all(
2508
- 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
+ }))
2509
2538
  );
2510
2539
  const missing = checked.filter((c) => !c.exists).map((c) => c.id);
2511
2540
  if (missing.length > 0) {
@@ -2516,7 +2545,7 @@ async function addTaskHandler(ctx, input) {
2516
2545
  const estimatedSeconds = input.estimated_seconds ?? DEFAULT_ESTIMATED_SECONDS;
2517
2546
  const existing = await ctx.repos.task.findByDate(date);
2518
2547
  const orderIndex = computeNextOrderIndex(existing);
2519
- const task = await ctx.repos.task.create({
2548
+ const createInput = {
2520
2549
  date,
2521
2550
  title: input.title,
2522
2551
  description: input.description,
@@ -2530,7 +2559,10 @@ async function addTaskHandler(ctx, input) {
2530
2559
  scheduledStartTime: input.scheduled_start_time,
2531
2560
  notifyAtScheduledStart: input.notify_at_scheduled_start,
2532
2561
  isBackground: input.is_background
2533
- });
2562
+ };
2563
+ const task = await ctx.repos.task.create(
2564
+ applyContextNotificationInheritance(createInput, context)
2565
+ );
2534
2566
  return { task: taskSummaryFrom(task) };
2535
2567
  }
2536
2568
 
@@ -2540,6 +2572,7 @@ function contextSummaryFrom(context) {
2540
2572
  id: context.id,
2541
2573
  name: context.name,
2542
2574
  status: context.status,
2575
+ notify_at_scheduled_start: context.notifyAtScheduledStart ?? false,
2543
2576
  created_at: context.createdAt.toISOString(),
2544
2577
  updated_at: context.updatedAt.toISOString()
2545
2578
  };
@@ -2701,7 +2734,8 @@ var createContextInputSchema = z.object({
2701
2734
  name: z.string().min(1, "name must not be empty"),
2702
2735
  description: z.string().optional(),
2703
2736
  icon: z.string().optional().describe("\u7D75\u6587\u5B57\u307E\u305F\u306F\u30A2\u30A4\u30B3\u30F3\u540D (\u4F8B: \u{1F3E0} / mdi-home)"),
2704
- color: z.string().optional().describe("UI \u8272 (\u4F8B: #ff6b6b)")
2737
+ color: z.string().optional().describe("UI \u8272 (\u4F8B: #ff6b6b)"),
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")
2705
2739
  });
2706
2740
  async function createContextHandler(ctx, input) {
2707
2741
  const createInput = {
@@ -2711,6 +2745,9 @@ async function createContextHandler(ctx, input) {
2711
2745
  if (input.description !== void 0) createInput.description = input.description;
2712
2746
  if (input.icon !== void 0) createInput.icon = input.icon;
2713
2747
  if (input.color !== void 0) createInput.color = input.color;
2748
+ if (input.notify_at_scheduled_start !== void 0) {
2749
+ createInput.notifyAtScheduledStart = input.notify_at_scheduled_start;
2750
+ }
2714
2751
  const context = await ctx.repos.context.create(createInput);
2715
2752
  return { context: contextSummaryFrom(context) };
2716
2753
  }
@@ -2749,12 +2786,23 @@ var frequencySchema = z.discriminatedUnion("type", [
2749
2786
  z.object({ type: z.literal("daily") }),
2750
2787
  z.object({
2751
2788
  type: z.literal("weekly"),
2752
- 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")
2753
2792
  }),
2754
2793
  z.object({
2755
2794
  type: z.literal("monthly"),
2756
2795
  days: z.array(z.number().int().min(1).max(31)).min(1, "at least 1 day")
2757
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
+ }),
2758
2806
  z.object({ type: z.literal("custom"), cron: z.string().min(1) })
2759
2807
  ]);
2760
2808
  var createRoutineInputSchema = z.object({
@@ -3105,12 +3153,13 @@ var pauseTaskInputSchema = z.object({
3105
3153
  id: z.string().uuid({ message: "id must be a UUID" })
3106
3154
  });
3107
3155
  async function pauseTaskHandler(ctx, input) {
3108
- const existing = await ctx.repos.task.findById(input.id);
3109
- if (!existing) {
3110
- throw new McpToolError(`Task not found: ${input.id}`);
3111
- }
3112
- const updated = await pauseTask(ctx.repos.task, input.id);
3113
- 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
+ };
3114
3163
  }
3115
3164
  var reopenTaskInputSchema = z.object({
3116
3165
  id: z.string().uuid({ message: "id must be a UUID" })
@@ -3192,7 +3241,8 @@ var updateContextInputSchema = z.object({
3192
3241
  description: z.union([z.string(), z.null()]).optional(),
3193
3242
  icon: z.union([z.string(), z.null()]).optional(),
3194
3243
  color: z.union([z.string(), z.null()]).optional(),
3195
- status: z.enum(["active", "archived"]).optional().describe("archived \u5316\u306F archive_context tool \u63A8\u5968")
3244
+ status: z.enum(["active", "archived"]).optional().describe("archived \u5316\u306F archive_context tool \u63A8\u5968"),
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")
3196
3246
  });
3197
3247
  async function updateContextHandler(ctx, input) {
3198
3248
  const existing = await ctx.repos.context.findById(input.id);
@@ -3211,6 +3261,9 @@ async function updateContextHandler(ctx, input) {
3211
3261
  patch.color = input.color === null ? void 0 : input.color;
3212
3262
  }
3213
3263
  if (input.status !== void 0) patch.status = input.status;
3264
+ if (input.notify_at_scheduled_start !== void 0) {
3265
+ patch.notifyAtScheduledStart = input.notify_at_scheduled_start;
3266
+ }
3214
3267
  const updated = await ctx.repos.context.update(input.id, patch);
3215
3268
  return { context: contextSummaryFrom(updated) };
3216
3269
  }
@@ -3267,12 +3320,23 @@ var frequencySchema2 = z.discriminatedUnion("type", [
3267
3320
  z.object({ type: z.literal("daily") }),
3268
3321
  z.object({
3269
3322
  type: z.literal("weekly"),
3270
- 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()
3271
3326
  }),
3272
3327
  z.object({
3273
3328
  type: z.literal("monthly"),
3274
3329
  days: z.array(z.number().int().min(1).max(31)).min(1)
3275
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
+ }),
3276
3340
  z.object({ type: z.literal("custom"), cron: z.string().min(1) })
3277
3341
  ]);
3278
3342
  var updateRoutineInputSchema = z.object({
@@ -3350,9 +3414,11 @@ async function updateRoutineHandler(ctx, input) {
3350
3414
  );
3351
3415
  }
3352
3416
  const taskPatch = buildRoutineTaskPatch(updated);
3417
+ const routineContext = updated.contextId ? await ctx.repos.context.findById(updated.contextId) : null;
3418
+ const propagatedPatch = applyContextNotificationInheritance(taskPatch, routineContext);
3353
3419
  await Promise.all(
3354
3420
  targets.map(
3355
- (task) => auditingTask.updateWithCause(task.id, taskPatch, "routine-propagation")
3421
+ (task) => auditingTask.updateWithCause(task.id, propagatedPatch, "routine-propagation")
3356
3422
  )
3357
3423
  );
3358
3424
  }
@@ -3446,6 +3512,11 @@ async function updateTaskHandler(ctx, input) {
3446
3512
  patch.notifyAtScheduledStart = input.notify_at_scheduled_start;
3447
3513
  }
3448
3514
  if (input.is_background !== void 0) patch.isBackground = input.is_background;
3515
+ const newContextId = patch.contextId;
3516
+ if (newContextId) {
3517
+ const newContext = await ctx.repos.context.findById(newContextId);
3518
+ Object.assign(patch, applyContextNotificationInheritance(patch, newContext));
3519
+ }
3449
3520
  const updated = await ctx.repos.task.update(input.id, patch);
3450
3521
  return { task: taskSummaryFrom(updated) };
3451
3522
  }
@@ -3900,7 +3971,7 @@ function createMcpServer(opts) {
3900
3971
 
3901
3972
  // src/index.ts
3902
3973
  async function main() {
3903
- const version = "0.3.0" ;
3974
+ const version = "0.6.0" ;
3904
3975
  const server = createMcpServer({ version });
3905
3976
  const transport = new StdioServerTransport();
3906
3977
  await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fukuchang/taskchute-mcp",
3
- "version": "0.3.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",