open-walnut 0.3.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.
Files changed (155) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +339 -0
  3. package/bin/open-walnut.js +2 -0
  4. package/bin/walnut.js +2 -0
  5. package/dist/cli.js +137849 -0
  6. package/dist/daemon-binaries/acp-worker.js +44 -0
  7. package/dist/daemon-binaries/acp-worker.js.version +1 -0
  8. package/dist/data/skills/daily-standup/SKILL.md +16 -0
  9. package/dist/data/skills/install-plugin/SKILL.md +118 -0
  10. package/dist/data/skills/learn/SKILL.md +46 -0
  11. package/dist/data/skills/register-repo/SKILL.md +20 -0
  12. package/dist/data/skills/setup-cloud-companion/SKILL.md +326 -0
  13. package/dist/data/skills/triage/SKILL.md +23 -0
  14. package/dist/data/skills/walnut-calendar/SKILL.md +92 -0
  15. package/dist/data/skills/walnut-manage-repos/SKILL.md +107 -0
  16. package/dist/data/skills/walnut-peer-sessions/SKILL.md +61 -0
  17. package/dist/data/skills/walnut-register-repo/SKILL.md +72 -0
  18. package/dist/data/skills/walnut-register-repo/references/yaml-format.md +111 -0
  19. package/dist/data/skills/walnut-register-repo/scripts/validate-repo.py +177 -0
  20. package/dist/data/skills/walnut-routines/SKILL.md +65 -0
  21. package/dist/data/skills/weekly-review/SKILL.md +22 -0
  22. package/dist/data/walnut-calendar.swift +213 -0
  23. package/dist/data/walnut-extract.swift +80 -0
  24. package/dist/hooks/on-compact.js +976 -0
  25. package/dist/hooks/on-stop.js +1329 -0
  26. package/dist/integrations/jira/index.js +4200 -0
  27. package/dist/integrations/jira/manifest.json +26 -0
  28. package/dist/integrations/local/index.js +51 -0
  29. package/dist/integrations/local/manifest.json +10 -0
  30. package/dist/integrations/ms-todo/index.js +4672 -0
  31. package/dist/integrations/ms-todo/manifest.json +17 -0
  32. package/dist/session-server/index.js +1382 -0
  33. package/dist/web/server.js +135275 -0
  34. package/dist/web/static/assets/apl-B4CMkyY2.js +1 -0
  35. package/dist/web/static/assets/asciiarmor-Df11BRmG.js +1 -0
  36. package/dist/web/static/assets/asn1-EdZsLKOL.js +1 -0
  37. package/dist/web/static/assets/asterisk-B-8jnY81.js +1 -0
  38. package/dist/web/static/assets/brainfuck-C4LP7Hcl.js +1 -0
  39. package/dist/web/static/assets/clike-B9uivgTg.js +1 -0
  40. package/dist/web/static/assets/clojure-BMjYHr_A.js +1 -0
  41. package/dist/web/static/assets/cmake-BQqOBYOt.js +1 -0
  42. package/dist/web/static/assets/cobol-CWcv1MsR.js +1 -0
  43. package/dist/web/static/assets/coffeescript-S37ZYGWr.js +1 -0
  44. package/dist/web/static/assets/commonlisp-DBKNyK5s.js +1 -0
  45. package/dist/web/static/assets/crystal-SjHAIU92.js +1 -0
  46. package/dist/web/static/assets/css-BnMrqG3P.js +1 -0
  47. package/dist/web/static/assets/cypher-C_CwsFkJ.js +1 -0
  48. package/dist/web/static/assets/d-pRatUO7H.js +1 -0
  49. package/dist/web/static/assets/diff-DbItnlRl.js +1 -0
  50. package/dist/web/static/assets/dockerfile-BKs6k2Af.js +1 -0
  51. package/dist/web/static/assets/dtd-DF_7sFjM.js +1 -0
  52. package/dist/web/static/assets/dylan-DwRh75JA.js +1 -0
  53. package/dist/web/static/assets/ebnf-CDyGwa7X.js +1 -0
  54. package/dist/web/static/assets/ecl-Cabwm37j.js +1 -0
  55. package/dist/web/static/assets/eiffel-CnydiIhH.js +1 -0
  56. package/dist/web/static/assets/elm-vLlmbW-K.js +1 -0
  57. package/dist/web/static/assets/erlang-BNw1qcRV.js +1 -0
  58. package/dist/web/static/assets/factor-kuTfRLto.js +1 -0
  59. package/dist/web/static/assets/fcl-Kvtd6kyn.js +1 -0
  60. package/dist/web/static/assets/forth-Ffai-XNe.js +1 -0
  61. package/dist/web/static/assets/fortran-DYz_wnZ1.js +1 -0
  62. package/dist/web/static/assets/gas-Bneqetm1.js +1 -0
  63. package/dist/web/static/assets/gherkin-heZmZLOM.js +1 -0
  64. package/dist/web/static/assets/groovy-D9Dt4D0W.js +1 -0
  65. package/dist/web/static/assets/haskell-Cw1EW3IL.js +1 -0
  66. package/dist/web/static/assets/haxe-H-WmDvRZ.js +1 -0
  67. package/dist/web/static/assets/http-DBlCnlav.js +1 -0
  68. package/dist/web/static/assets/idl-BEugSyMb.js +1 -0
  69. package/dist/web/static/assets/index-B3K5yAdA.js +1 -0
  70. package/dist/web/static/assets/index-B7OvpXOs.js +632 -0
  71. package/dist/web/static/assets/index-C07UNJIJ.js +1 -0
  72. package/dist/web/static/assets/index-C8k-3wfu.js +2 -0
  73. package/dist/web/static/assets/index-CBXGsT3t.js +1 -0
  74. package/dist/web/static/assets/index-CUrO3qpX.js +1 -0
  75. package/dist/web/static/assets/index-CpTvBBAa.js +1 -0
  76. package/dist/web/static/assets/index-D4J6slTf.js +1 -0
  77. package/dist/web/static/assets/index-DUgC5K_q.js +3 -0
  78. package/dist/web/static/assets/index-DhqXE1wa.js +1 -0
  79. package/dist/web/static/assets/index-DkGqHTCk.js +1 -0
  80. package/dist/web/static/assets/index-DsxFiBic.js +1 -0
  81. package/dist/web/static/assets/index-Dz3umkfm.js +1 -0
  82. package/dist/web/static/assets/index-G2v0oaqp.js +1 -0
  83. package/dist/web/static/assets/index-GNDKJKwy.js +1 -0
  84. package/dist/web/static/assets/index-U1CVtNUH.css +32 -0
  85. package/dist/web/static/assets/index-ft4kpQXq.js +1 -0
  86. package/dist/web/static/assets/index-ls7h0u-p.js +1 -0
  87. package/dist/web/static/assets/index-nL5l5WPW.js +7 -0
  88. package/dist/web/static/assets/javascript-iXu5QeM3.js +1 -0
  89. package/dist/web/static/assets/julia-DuME0IfC.js +1 -0
  90. package/dist/web/static/assets/livescript-BwQOo05w.js +1 -0
  91. package/dist/web/static/assets/lua-VAEuO923.js +1 -0
  92. package/dist/web/static/assets/mathematica-DTrFuWx2.js +1 -0
  93. package/dist/web/static/assets/mbox-CNhZ1qSd.js +1 -0
  94. package/dist/web/static/assets/mirc-CjQqDB4T.js +1 -0
  95. package/dist/web/static/assets/mllike-CXdrOF99.js +1 -0
  96. package/dist/web/static/assets/modelica-Dc1JOy9r.js +1 -0
  97. package/dist/web/static/assets/mscgen-BA5vi2Kp.js +1 -0
  98. package/dist/web/static/assets/mumps-BT43cFF4.js +1 -0
  99. package/dist/web/static/assets/nginx-DdIZxoE0.js +1 -0
  100. package/dist/web/static/assets/nsis-LdVXkNf5.js +1 -0
  101. package/dist/web/static/assets/ntriples-BfvgReVJ.js +1 -0
  102. package/dist/web/static/assets/octave-Ck1zUtKM.js +1 -0
  103. package/dist/web/static/assets/oz-BzwKVEFT.js +1 -0
  104. package/dist/web/static/assets/pascal--L3eBynH.js +1 -0
  105. package/dist/web/static/assets/perl-CdXCOZ3F.js +1 -0
  106. package/dist/web/static/assets/pig-CevX1Tat.js +1 -0
  107. package/dist/web/static/assets/powershell-CFHJl5sT.js +1 -0
  108. package/dist/web/static/assets/properties-C78fOPTZ.js +1 -0
  109. package/dist/web/static/assets/protobuf-ChK-085T.js +1 -0
  110. package/dist/web/static/assets/pug-DeIclll2.js +1 -0
  111. package/dist/web/static/assets/puppet-DMA9R1ak.js +1 -0
  112. package/dist/web/static/assets/python-BuPzkPfP.js +1 -0
  113. package/dist/web/static/assets/q-pXgVlZs6.js +1 -0
  114. package/dist/web/static/assets/r-B6wPVr8A.js +1 -0
  115. package/dist/web/static/assets/rpm-CTu-6PCP.js +1 -0
  116. package/dist/web/static/assets/ruby-B2Rjki9n.js +1 -0
  117. package/dist/web/static/assets/sas-B4kiWyti.js +1 -0
  118. package/dist/web/static/assets/scheme-C41bIUwD.js +1 -0
  119. package/dist/web/static/assets/shell-CjFT_Tl9.js +1 -0
  120. package/dist/web/static/assets/sieve-C3Gn_uJK.js +1 -0
  121. package/dist/web/static/assets/simple-mode-GW_nhZxv.js +1 -0
  122. package/dist/web/static/assets/smalltalk-CnHTOXQT.js +1 -0
  123. package/dist/web/static/assets/solr-DehyRSwq.js +1 -0
  124. package/dist/web/static/assets/sparql-DkYu6x3z.js +1 -0
  125. package/dist/web/static/assets/spreadsheet-BCZA_wO0.js +1 -0
  126. package/dist/web/static/assets/sql-D0XecflT.js +1 -0
  127. package/dist/web/static/assets/stex-C3f8Ysf7.js +1 -0
  128. package/dist/web/static/assets/stylus-B533Al4x.js +1 -0
  129. package/dist/web/static/assets/swift-BzpIVaGY.js +1 -0
  130. package/dist/web/static/assets/tcl-DVfN8rqt.js +1 -0
  131. package/dist/web/static/assets/textile-CnDTJFAw.js +1 -0
  132. package/dist/web/static/assets/tiddlywiki-DO-Gjzrf.js +1 -0
  133. package/dist/web/static/assets/tiki-DGYXhP31.js +1 -0
  134. package/dist/web/static/assets/toml-Bm5Em-hy.js +1 -0
  135. package/dist/web/static/assets/troff-wAsdV37c.js +1 -0
  136. package/dist/web/static/assets/ttcn-CfJYG6tj.js +1 -0
  137. package/dist/web/static/assets/ttcn-cfg-B9xdYoR4.js +1 -0
  138. package/dist/web/static/assets/turtle-B1tBg_DP.js +1 -0
  139. package/dist/web/static/assets/vb-CmGdzxic.js +1 -0
  140. package/dist/web/static/assets/vbscript-BuJXcnF6.js +1 -0
  141. package/dist/web/static/assets/velocity-D8B20fx6.js +1 -0
  142. package/dist/web/static/assets/verilog-C6RDOZhf.js +1 -0
  143. package/dist/web/static/assets/vhdl-lSbBsy5d.js +1 -0
  144. package/dist/web/static/assets/webidl-ZXfAyPTL.js +1 -0
  145. package/dist/web/static/assets/xquery-DzFWVndE.js +1 -0
  146. package/dist/web/static/assets/yacas-BJ4BC0dw.js +1 -0
  147. package/dist/web/static/assets/z80-Hz9HOZM7.js +1 -0
  148. package/dist/web/static/index.html +22 -0
  149. package/dist/web/static/walnut-icon.png +0 -0
  150. package/dist/workers/git-compaction-worker.js +898 -0
  151. package/dist/workers/qmd-index-worker.js +97162 -0
  152. package/package.json +143 -0
  153. package/patches/@anthropic-ai+claude-agent-sdk+0.2.50.patch +13 -0
  154. package/scripts/patch-qmd.mjs +171 -0
  155. package/scripts/postinstall.mjs +120 -0
@@ -0,0 +1,4672 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res) => function __init() {
4
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
+ };
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+
11
+ // src/constants.ts
12
+ import path from "path";
13
+ import fs from "fs";
14
+ import os from "os";
15
+ import { fileURLToPath } from "url";
16
+ function resolveOpenWalnutHome() {
17
+ const envHome = process.env.OPEN_WALNUT_HOME;
18
+ const productionHome = path.join(os.homedir(), ".open-walnut");
19
+ const isTestEnv = !!(process.env.VITEST || process.env.VITEST_WORKER_ID || process.env.NODE_ENV === "test");
20
+ if (isTestEnv) {
21
+ if (envHome && envHome !== productionHome && !envHome.startsWith(productionHome + path.sep)) {
22
+ assertNotProductionPath(envHome);
23
+ return envHome;
24
+ }
25
+ const testHome = path.join(os.tmpdir(), `open-walnut-test-${process.pid}`);
26
+ process.env.OPEN_WALNUT_HOME = testHome;
27
+ return testHome;
28
+ }
29
+ if (!envHome) return productionHome;
30
+ if (process.argv.includes("--_ephemeral-child")) return envHome;
31
+ if (isEphemeralTmpDir(envHome)) {
32
+ process.stderr.write(
33
+ `WARNING: OPEN_WALNUT_HOME=${envHome} looks like a leaked ephemeral temp dir.
34
+ Overriding to ${productionHome}. (Only --_ephemeral-child processes may use one.)
35
+ `
36
+ );
37
+ process.env.OPEN_WALNUT_HOME = productionHome;
38
+ return productionHome;
39
+ }
40
+ return envHome;
41
+ }
42
+ function assertNotProductionPath(inputPath) {
43
+ const isTestEnv = !!(process.env.VITEST || process.env.VITEST_WORKER_ID || process.env.NODE_ENV === "test");
44
+ if (!isTestEnv) return;
45
+ const resolved = path.resolve(inputPath);
46
+ const prodHome = path.join(os.homedir(), ".open-walnut");
47
+ if (resolved === prodHome || resolved.startsWith(prodHome + path.sep)) {
48
+ throw new Error(
49
+ `SAFETY: Test process attempted to use production path: ${resolved}
50
+ This would destroy real user data in ~/.open-walnut/.
51
+ Set OPEN_WALNUT_HOME to a temp directory or let constants.ts auto-assign one.`
52
+ );
53
+ }
54
+ }
55
+ function isEphemeralTmpDir(inputPath) {
56
+ if (!/[\\/]open-walnut-[^\\/]+$/.test(inputPath)) return false;
57
+ const tmpDirs = /* @__PURE__ */ new Set();
58
+ tmpDirs.add(os.tmpdir());
59
+ try {
60
+ tmpDirs.add(fs.realpathSync(os.tmpdir()));
61
+ } catch {
62
+ }
63
+ try {
64
+ tmpDirs.add(fs.realpathSync("/tmp"));
65
+ } catch {
66
+ }
67
+ const parent = path.dirname(path.resolve(inputPath));
68
+ if (tmpDirs.has(parent)) return true;
69
+ try {
70
+ return tmpDirs.has(fs.realpathSync(parent));
71
+ } catch {
72
+ return false;
73
+ }
74
+ }
75
+ function agentMemoryDir(agentId) {
76
+ if (!agentId || agentId === "general") return WALNUT_HOME;
77
+ return path.join(MEMORY_DIR, "agents", agentId);
78
+ }
79
+ var WALNUT_HOME, IS_EPHEMERAL, CLOUD_MODE, TASKS_DIR, TASKS_FILE, ARCHIVE_DIR, MEMORY_DIR, SESSIONS_DIR, PROJECTS_DIR, CONFIG_FILE, SYNC_DIR, PLUGIN_STORES_DIR, SESSIONS_FILE, CLAUDE_HOME, HISTORY_CACHE_DIR, PROJECTION_CACHE_DIR, TRANSCRIPT_CACHE_DIR, TASK_QUEUE_DIR, HOST_MODEL_CATALOG_FILE, HOOK_LOG_FILE, DAILY_DIR, MEMORY_FILE, USER_FILE, LEGACY_MEMORY_FILE, PROJECTS_MEMORY_DIR, CHAT_HISTORY_FILE, CONVERSATIONS_DIR, GLOBAL_SKILLS_DIR, SKILL_SETTINGS_FILE, CLAUDE_SKILLS_DIR, CLAUDE_SETTINGS_FILE, CLAUDE_PLUGINS_DIR, CLAUDE_CREDENTIALS_FILE, CRON_FILE, USAGE_DB_FILE, LOG_DIR, SESSION_STREAMS_DIR, DTACH_SOCKET_DIR, SESSION_QUEUE_FILE, IMAGES_DIR, REMOTE_IMAGES_DIR, PASTES_DIR, HEARTBEAT_FILE, COMMANDS_DIR, BUILTIN_COMMANDS_DIR, BUILTIN_SKILLS_DIR, FREQUENT_DIRS_FILE, MENTION_DIRS_FILE, NOTES_DIR, GLOBAL_NOTES_FILE, NOTES_AGENTS_FILE, NOTES_CLAUDE_FILE, REPOSITORIES_DIR, REPOS_MEMORY_DIR, TOPICS_DIR, COMPACTION_DIR, MEMORY_INDEX_FILE, WORKING_MEMORY_FILE, TIMELINE_DIR, CONFIG_SHARE_DIR, UI_PREFS_FILE, STT_VOCAB_FILE, TMP_DIR, RECORDINGS_DIR, LOG_PREFIX, DAEMON_BINARIES_DIR, WALNUT_INSTALL_DIR;
80
+ var init_constants = __esm({
81
+ "src/constants.ts"() {
82
+ "use strict";
83
+ WALNUT_HOME = resolveOpenWalnutHome();
84
+ IS_EPHEMERAL = process.argv.includes("--_ephemeral-child");
85
+ CLOUD_MODE = process.env.WALNUT_CLOUD_MODE === "1";
86
+ TASKS_DIR = path.join(WALNUT_HOME, "tasks");
87
+ TASKS_FILE = path.join(TASKS_DIR, "tasks.json");
88
+ ARCHIVE_DIR = path.join(TASKS_DIR, "archive");
89
+ MEMORY_DIR = path.join(WALNUT_HOME, "memory");
90
+ SESSIONS_DIR = path.join(MEMORY_DIR, "sessions");
91
+ PROJECTS_DIR = path.join(MEMORY_DIR, "projects");
92
+ CONFIG_FILE = path.join(WALNUT_HOME, "config.yaml");
93
+ SYNC_DIR = path.join(WALNUT_HOME, "sync");
94
+ PLUGIN_STORES_DIR = path.join(WALNUT_HOME, "plugin-stores");
95
+ SESSIONS_FILE = path.join(WALNUT_HOME, "sessions.json");
96
+ CLAUDE_HOME = path.join(os.homedir(), ".claude");
97
+ HISTORY_CACHE_DIR = path.join(WALNUT_HOME, "cache", "history");
98
+ PROJECTION_CACHE_DIR = path.join(WALNUT_HOME, "cache", "projections");
99
+ TRANSCRIPT_CACHE_DIR = path.join(WALNUT_HOME, "cache", "transcripts");
100
+ TASK_QUEUE_DIR = path.join(WALNUT_HOME, "cache", "task-queue");
101
+ HOST_MODEL_CATALOG_FILE = path.join(WALNUT_HOME, "cache", "host-model-catalogs.json");
102
+ HOOK_LOG_FILE = path.join(WALNUT_HOME, "hook-errors.log");
103
+ DAILY_DIR = path.join(MEMORY_DIR, "daily");
104
+ MEMORY_FILE = path.join(MEMORY_DIR, "MEMORY.md");
105
+ USER_FILE = path.join(MEMORY_DIR, "USER.md");
106
+ LEGACY_MEMORY_FILE = path.join(WALNUT_HOME, "MEMORY.md");
107
+ PROJECTS_MEMORY_DIR = path.join(MEMORY_DIR, "projects");
108
+ CHAT_HISTORY_FILE = path.join(WALNUT_HOME, "chat-history.json");
109
+ CONVERSATIONS_DIR = path.join(WALNUT_HOME, "conversations");
110
+ GLOBAL_SKILLS_DIR = path.join(WALNUT_HOME, "skills");
111
+ SKILL_SETTINGS_FILE = path.join(WALNUT_HOME, "skill-settings.json");
112
+ CLAUDE_SKILLS_DIR = path.join(CLAUDE_HOME, "skills");
113
+ CLAUDE_SETTINGS_FILE = path.join(CLAUDE_HOME, "settings.json");
114
+ CLAUDE_PLUGINS_DIR = path.join(CLAUDE_HOME, "plugins");
115
+ CLAUDE_CREDENTIALS_FILE = path.join(CLAUDE_HOME, ".credentials.json");
116
+ CRON_FILE = path.join(WALNUT_HOME, "cron-jobs.json");
117
+ USAGE_DB_FILE = path.join(WALNUT_HOME, "usage.sqlite");
118
+ LOG_DIR = process.env.WALNUT_DAEMON_DIR || "/tmp/open-walnut";
119
+ SESSION_STREAMS_DIR = path.join(LOG_DIR, "streams");
120
+ DTACH_SOCKET_DIR = path.join(LOG_DIR, "term");
121
+ SESSION_QUEUE_FILE = path.join(WALNUT_HOME, "session-message-queue.json");
122
+ IMAGES_DIR = path.join(LOG_DIR, "images");
123
+ REMOTE_IMAGES_DIR = path.join(IMAGES_DIR, "remote");
124
+ PASTES_DIR = path.join(LOG_DIR, "pastes");
125
+ HEARTBEAT_FILE = path.join(WALNUT_HOME, "HEARTBEAT.md");
126
+ COMMANDS_DIR = path.join(WALNUT_HOME, "commands");
127
+ BUILTIN_COMMANDS_DIR = (() => {
128
+ let dir = path.dirname(fileURLToPath(import.meta.url));
129
+ for (let i = 0; i < 5; i++) {
130
+ const candidate = path.join(dir, "data", "slash-commands");
131
+ try {
132
+ if (fs.statSync(candidate).isDirectory()) return candidate;
133
+ } catch {
134
+ }
135
+ dir = path.dirname(dir);
136
+ }
137
+ return path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "data", "slash-commands");
138
+ })();
139
+ BUILTIN_SKILLS_DIR = (() => {
140
+ let dir = path.dirname(fileURLToPath(import.meta.url));
141
+ for (let i = 0; i < 5; i++) {
142
+ const candidate = path.join(dir, "data", "skills");
143
+ try {
144
+ if (fs.statSync(candidate).isDirectory()) return candidate;
145
+ } catch {
146
+ }
147
+ dir = path.dirname(dir);
148
+ }
149
+ return path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "data", "skills");
150
+ })();
151
+ FREQUENT_DIRS_FILE = path.join(WALNUT_HOME, "frequent-directories.json");
152
+ MENTION_DIRS_FILE = path.join(WALNUT_HOME, "mention-directories.json");
153
+ NOTES_DIR = path.join(WALNUT_HOME, "notes");
154
+ GLOBAL_NOTES_FILE = path.join(NOTES_DIR, "global-notes.md");
155
+ NOTES_AGENTS_FILE = path.join(NOTES_DIR, "AGENTS.md");
156
+ NOTES_CLAUDE_FILE = path.join(NOTES_DIR, "CLAUDE.md");
157
+ REPOSITORIES_DIR = path.join(WALNUT_HOME, "repositories");
158
+ REPOS_MEMORY_DIR = path.join(GLOBAL_SKILLS_DIR, "repos");
159
+ TOPICS_DIR = path.join(MEMORY_DIR, "topics");
160
+ COMPACTION_DIR = path.join(MEMORY_DIR, "compaction");
161
+ MEMORY_INDEX_FILE = path.join(MEMORY_DIR, "index.md");
162
+ WORKING_MEMORY_FILE = path.join(MEMORY_DIR, "working-memory.md");
163
+ TIMELINE_DIR = path.join(WALNUT_HOME, "timeline");
164
+ CONFIG_SHARE_DIR = path.join(WALNUT_HOME, "config", "share");
165
+ UI_PREFS_FILE = path.join(CONFIG_SHARE_DIR, "ui-prefs.json");
166
+ STT_VOCAB_FILE = path.join(CONFIG_SHARE_DIR, "stt-vocab.txt");
167
+ TMP_DIR = path.join(WALNUT_HOME, "tmp");
168
+ RECORDINGS_DIR = path.join(TMP_DIR, "recordings");
169
+ LOG_PREFIX = "open-walnut-";
170
+ DAEMON_BINARIES_DIR = (() => {
171
+ let dir = path.dirname(fileURLToPath(import.meta.url));
172
+ for (let i = 0; i < 5; i++) {
173
+ const candidate = path.join(dir, "dist", "daemon-binaries");
174
+ try {
175
+ if (fs.statSync(candidate).isDirectory()) return candidate;
176
+ } catch {
177
+ }
178
+ dir = path.dirname(dir);
179
+ }
180
+ return path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "dist", "daemon-binaries");
181
+ })();
182
+ WALNUT_INSTALL_DIR = (() => {
183
+ let dir = path.dirname(fileURLToPath(import.meta.url));
184
+ for (let i = 0; i < 6; i++) {
185
+ try {
186
+ const pkg = JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf8"));
187
+ if (pkg.name === "open-walnut" && fs.statSync(path.join(dir, ".git")).isDirectory()) return dir;
188
+ } catch {
189
+ }
190
+ const parent = path.dirname(dir);
191
+ if (parent === dir) break;
192
+ dir = parent;
193
+ }
194
+ return null;
195
+ })();
196
+ }
197
+ });
198
+
199
+ // src/logging/redact.ts
200
+ function redactSensitiveText(text) {
201
+ let result = text;
202
+ for (const { re, replacement } of patterns) {
203
+ result = result.replace(re, replacement);
204
+ }
205
+ return result;
206
+ }
207
+ var REDACTED, patterns;
208
+ var init_redact = __esm({
209
+ "src/logging/redact.ts"() {
210
+ "use strict";
211
+ REDACTED = "[REDACTED]";
212
+ patterns = [
213
+ // PEM private key blocks (multiline)
214
+ {
215
+ re: /-----BEGIN\s[\w\s]*PRIVATE KEY-----[\s\S]*?-----END\s[\w\s]*PRIVATE KEY-----/g,
216
+ replacement: REDACTED
217
+ },
218
+ // Bearer tokens: Authorization: Bearer <token>
219
+ {
220
+ re: /(Bearer\s+)\S+/gi,
221
+ replacement: `$1${REDACTED}`
222
+ },
223
+ // OpenAI / Anthropic style keys: sk-... (at least 20 chars after prefix)
224
+ {
225
+ re: /\bsk-[A-Za-z0-9_-]{20,}\b/g,
226
+ replacement: REDACTED
227
+ },
228
+ // AWS access key IDs: AKIA...
229
+ {
230
+ re: /\bAKIA[A-Z0-9]{12,}\b/g,
231
+ replacement: REDACTED
232
+ },
233
+ // AWS secret access key in key=value form
234
+ {
235
+ re: /(aws_secret_access_key\s*[=:]\s*)\S+/gi,
236
+ replacement: `$1${REDACTED}`
237
+ },
238
+ // AWS session token in key=value form
239
+ {
240
+ re: /(aws_session_token\s*[=:]\s*)\S+/gi,
241
+ replacement: `$1${REDACTED}`
242
+ },
243
+ // Credentials in URL userinfo: https://user:secret@host/...
244
+ // Motivating shape: execSync embeds the whole command in error.message, so a
245
+ // failed `git remote add origin https://walnut:<device-token>@host/git/data`
246
+ // otherwise logs the cloud companion's long-lived credential verbatim.
247
+ {
248
+ re: /(https?:\/\/[^/\s:@]+:)[^@/\s]+@/gi,
249
+ replacement: `$1${REDACTED}@`
250
+ },
251
+ // Generic secrets: password=, secret=, token=, api_key=, apikey=
252
+ {
253
+ re: /((?:password|secret|token|api_key|apikey)\s*[=:]\s*)\S+/gi,
254
+ replacement: `$1${REDACTED}`
255
+ }
256
+ ];
257
+ }
258
+ });
259
+
260
+ // src/logging/logger.ts
261
+ import fs2 from "fs";
262
+ import fsp from "fs/promises";
263
+ import path2 from "path";
264
+ function ensureLogDir() {
265
+ if (dirEnsured) return;
266
+ try {
267
+ fs2.mkdirSync(LOG_DIR, { recursive: true });
268
+ dirEnsured = true;
269
+ } catch {
270
+ }
271
+ }
272
+ function todayFileName() {
273
+ const d = /* @__PURE__ */ new Date();
274
+ const yyyy = d.getFullYear();
275
+ const mm = String(d.getMonth() + 1).padStart(2, "0");
276
+ const dd = String(d.getDate()).padStart(2, "0");
277
+ return `${LOG_PREFIX}${yyyy}-${mm}-${dd}.log`;
278
+ }
279
+ function logFilePath() {
280
+ return path2.join(LOG_DIR, todayFileName());
281
+ }
282
+ function startFlushTimer() {
283
+ if (flushTimer) return;
284
+ flushTimer = setInterval(() => {
285
+ void flushLogBuffer();
286
+ }, FLUSH_INTERVAL_MS);
287
+ if (flushTimer && typeof flushTimer === "object" && "unref" in flushTimer) {
288
+ flushTimer.unref();
289
+ }
290
+ }
291
+ async function flushLogBuffer() {
292
+ if (flushing || buffer.length === 0) return;
293
+ flushing = true;
294
+ const batch = buffer;
295
+ buffer = [];
296
+ try {
297
+ ensureLogDir();
298
+ await fsp.appendFile(logFilePath(), batch.join(""), "utf-8");
299
+ } catch {
300
+ } finally {
301
+ flushing = false;
302
+ }
303
+ }
304
+ function flushLogBufferSync() {
305
+ if (buffer.length === 0) return;
306
+ const batch = buffer;
307
+ buffer = [];
308
+ try {
309
+ ensureLogDir();
310
+ fs2.appendFileSync(logFilePath(), batch.join(""), "utf-8");
311
+ } catch {
312
+ }
313
+ }
314
+ function writeLogEntry(entry) {
315
+ const line = redactSensitiveText(JSON.stringify(entry)) + "\n";
316
+ buffer.push(line);
317
+ startFlushTimer();
318
+ }
319
+ var dirEnsured, FLUSH_INTERVAL_MS, buffer, flushTimer, flushing;
320
+ var init_logger = __esm({
321
+ "src/logging/logger.ts"() {
322
+ "use strict";
323
+ init_constants();
324
+ init_redact();
325
+ dirEnsured = false;
326
+ FLUSH_INTERVAL_MS = 2e3;
327
+ buffer = [];
328
+ flushTimer = null;
329
+ flushing = false;
330
+ process.on("beforeExit", flushLogBufferSync);
331
+ process.on("exit", flushLogBufferSync);
332
+ }
333
+ });
334
+
335
+ // src/logging/levels.ts
336
+ function resolveConfiguredLevel() {
337
+ const raw = process.env.WALNUT_LOG_LEVEL?.toLowerCase().trim();
338
+ if (raw && raw in LOG_LEVEL_ORDER) return raw;
339
+ return DEFAULT_LEVEL;
340
+ }
341
+ function shouldLog(messageLevel, configuredLevel = CONFIGURED_LOG_LEVEL) {
342
+ return LOG_LEVEL_ORDER[messageLevel] >= LOG_LEVEL_ORDER[configuredLevel];
343
+ }
344
+ var LOG_LEVEL_ORDER, DEFAULT_LEVEL, CONFIGURED_LOG_LEVEL;
345
+ var init_levels = __esm({
346
+ "src/logging/levels.ts"() {
347
+ "use strict";
348
+ LOG_LEVEL_ORDER = {
349
+ trace: 0,
350
+ debug: 1,
351
+ info: 2,
352
+ warn: 3,
353
+ error: 4,
354
+ fatal: 5
355
+ };
356
+ DEFAULT_LEVEL = "info";
357
+ CONFIGURED_LOG_LEVEL = resolveConfiguredLevel();
358
+ }
359
+ });
360
+
361
+ // src/logging/subsystem.ts
362
+ import chalk from "chalk";
363
+ function timeStamp(d) {
364
+ const hh = String(d.getHours()).padStart(2, "0");
365
+ const mm = String(d.getMinutes()).padStart(2, "0");
366
+ const ss = String(d.getSeconds()).padStart(2, "0");
367
+ return `${hh}:${mm}:${ss}`;
368
+ }
369
+ function formatMeta(meta) {
370
+ if (!meta || Object.keys(meta).length === 0) return "";
371
+ return " " + chalk.dim(JSON.stringify(meta));
372
+ }
373
+ function forwardToErrorSink(subsystem, message, meta) {
374
+ const sink = globalThis[ERROR_SINK_KEY];
375
+ if (!sink || inErrorSink) return;
376
+ if (subsystem.startsWith("notif")) return;
377
+ inErrorSink = true;
378
+ try {
379
+ sink({ subsystem, message, meta });
380
+ } catch {
381
+ } finally {
382
+ inErrorSink = false;
383
+ }
384
+ }
385
+ function emit(level, subsystem, message, meta) {
386
+ if (!shouldLog(level)) return;
387
+ const now = /* @__PURE__ */ new Date();
388
+ writeLogEntry({
389
+ time: now.toISOString(),
390
+ level,
391
+ subsystem,
392
+ message,
393
+ ...meta && Object.keys(meta).length > 0 ? meta : {}
394
+ });
395
+ if (level === "error" || level === "fatal") {
396
+ forwardToErrorSink(subsystem, message, meta);
397
+ }
398
+ try {
399
+ const colorFn = levelColor[level];
400
+ const tag = chalk.bold(`[${subsystem}]`);
401
+ const lbl = colorFn(levelLabel[level]);
402
+ const ts = chalk.dim(timeStamp(now));
403
+ const metaStr = formatMeta(meta);
404
+ const line = redactSensitiveText(`${ts} ${lbl} ${tag} ${message}${metaStr}
405
+ `);
406
+ process.stderr.write(line);
407
+ } catch {
408
+ }
409
+ }
410
+ function createSubsystemLogger(subsystem) {
411
+ const logger = {
412
+ trace: (msg, meta) => emit("trace", subsystem, msg, meta),
413
+ debug: (msg, meta) => emit("debug", subsystem, msg, meta),
414
+ info: (msg, meta) => emit("info", subsystem, msg, meta),
415
+ warn: (msg, meta) => emit("warn", subsystem, msg, meta),
416
+ error: (msg, meta) => emit("error", subsystem, msg, meta),
417
+ fatal: (msg, meta) => emit("fatal", subsystem, msg, meta),
418
+ child(subName) {
419
+ return createSubsystemLogger(`${subsystem}/${subName}`);
420
+ }
421
+ };
422
+ return logger;
423
+ }
424
+ var levelColor, levelLabel, ERROR_SINK_KEY, inErrorSink;
425
+ var init_subsystem = __esm({
426
+ "src/logging/subsystem.ts"() {
427
+ "use strict";
428
+ init_levels();
429
+ init_logger();
430
+ init_redact();
431
+ levelColor = {
432
+ trace: chalk.gray,
433
+ debug: chalk.cyan,
434
+ info: chalk.green,
435
+ warn: chalk.yellow,
436
+ error: chalk.red,
437
+ fatal: chalk.bgRed.white
438
+ };
439
+ levelLabel = {
440
+ trace: "TRC",
441
+ debug: "DBG",
442
+ info: "INF",
443
+ warn: "WRN",
444
+ error: "ERR",
445
+ fatal: "FTL"
446
+ };
447
+ ERROR_SINK_KEY = /* @__PURE__ */ Symbol.for("open-walnut.errorNotificationSink");
448
+ inErrorSink = false;
449
+ }
450
+ });
451
+
452
+ // src/logging/index.ts
453
+ var log;
454
+ var init_logging = __esm({
455
+ "src/logging/index.ts"() {
456
+ "use strict";
457
+ init_logger();
458
+ init_subsystem();
459
+ init_subsystem();
460
+ init_levels();
461
+ init_redact();
462
+ log = {
463
+ bus: createSubsystemLogger("bus"),
464
+ agent: createSubsystemLogger("agent"),
465
+ session: createSubsystemLogger("session"),
466
+ subagent: createSubsystemLogger("subagent"),
467
+ web: createSubsystemLogger("web"),
468
+ ws: createSubsystemLogger("ws"),
469
+ hook: createSubsystemLogger("hook"),
470
+ task: createSubsystemLogger("task"),
471
+ memory: createSubsystemLogger("memory"),
472
+ cron: createSubsystemLogger("cron"),
473
+ notif: createSubsystemLogger("notif"),
474
+ usage: createSubsystemLogger("usage"),
475
+ heartbeat: createSubsystemLogger("heartbeat"),
476
+ git: createSubsystemLogger("git"),
477
+ browser: createSubsystemLogger("browser"),
478
+ audio: createSubsystemLogger("audio"),
479
+ stt: createSubsystemLogger("stt"),
480
+ obs: createSubsystemLogger("obs"),
481
+ calendar: createSubsystemLogger("calendar")
482
+ };
483
+ }
484
+ });
485
+
486
+ // src/utils/file-lock.ts
487
+ import fs3 from "fs";
488
+ import fsp2 from "fs/promises";
489
+ import path3 from "path";
490
+ function lockDir(filePath) {
491
+ return filePath + ".lock";
492
+ }
493
+ function pidFile(lock) {
494
+ return path3.join(lock, "pid");
495
+ }
496
+ function isPidAlive(pid) {
497
+ try {
498
+ process.kill(pid, 0);
499
+ return true;
500
+ } catch {
501
+ return false;
502
+ }
503
+ }
504
+ async function withFileLock(filePath, fn) {
505
+ const lock = lockDir(filePath);
506
+ const deadline = Date.now() + TIMEOUT_MS;
507
+ await fsp2.mkdir(path3.dirname(lock), { recursive: true });
508
+ while (true) {
509
+ try {
510
+ await fsp2.mkdir(lock);
511
+ try {
512
+ await fsp2.writeFile(pidFile(lock), String(process.pid));
513
+ } catch {
514
+ }
515
+ break;
516
+ } catch (err) {
517
+ if (err.code !== "EEXIST") throw err;
518
+ if (await isLockStaleAsync(lock)) {
519
+ try {
520
+ await fsp2.rm(lock, { recursive: true, force: true });
521
+ } catch {
522
+ }
523
+ continue;
524
+ }
525
+ if (Date.now() > deadline) {
526
+ throw new Error(`File lock timeout after ${TIMEOUT_MS}ms: ${lock}`);
527
+ }
528
+ await new Promise((resolve) => setTimeout(resolve, POLL_MS));
529
+ }
530
+ }
531
+ try {
532
+ return await fn();
533
+ } finally {
534
+ try {
535
+ await fsp2.rm(lock, { recursive: true, force: true });
536
+ } catch {
537
+ }
538
+ }
539
+ }
540
+ async function isLockStaleAsync(lock) {
541
+ try {
542
+ const raw = await fsp2.readFile(pidFile(lock), "utf-8");
543
+ const pid = parseInt(raw.trim(), 10);
544
+ if (!isNaN(pid) && pid > 0) {
545
+ return !isPidAlive(pid);
546
+ }
547
+ } catch {
548
+ }
549
+ try {
550
+ const stat = await fsp2.stat(lock);
551
+ return Date.now() - stat.mtimeMs > STALE_AGE_MS;
552
+ } catch {
553
+ return true;
554
+ }
555
+ }
556
+ var STALE_AGE_MS, TIMEOUT_MS, POLL_MS;
557
+ var init_file_lock = __esm({
558
+ "src/utils/file-lock.ts"() {
559
+ "use strict";
560
+ STALE_AGE_MS = 3e4;
561
+ TIMEOUT_MS = 1e4;
562
+ POLL_MS = 15;
563
+ }
564
+ });
565
+
566
+ // src/utils/fs.ts
567
+ import fs4 from "fs/promises";
568
+ import path4 from "path";
569
+ import crypto from "crypto";
570
+ async function writeJsonFile(filePath, data) {
571
+ const dir = path4.dirname(filePath);
572
+ await fs4.mkdir(dir, { recursive: true });
573
+ const tmpFile = path4.join(
574
+ dir,
575
+ `.open-walnut-${crypto.randomBytes(8).toString("hex")}.tmp`
576
+ );
577
+ try {
578
+ await fs4.writeFile(tmpFile, JSON.stringify(data, null, 2) + "\n", "utf-8");
579
+ await fs4.rename(tmpFile, filePath);
580
+ } catch (err) {
581
+ await fs4.rm(tmpFile, { force: true }).catch(() => {
582
+ });
583
+ throw err;
584
+ }
585
+ }
586
+ async function readJsonFile(filePath, fallback) {
587
+ let content;
588
+ try {
589
+ content = await fs4.readFile(filePath, "utf-8");
590
+ } catch (err) {
591
+ if (err && typeof err === "object" && "code" in err && err.code === "ENOENT") {
592
+ return fallback;
593
+ }
594
+ log.web.warn("non-ENOENT error reading JSON file", { filePath, error: err instanceof Error ? err.message : String(err) });
595
+ return fallback;
596
+ }
597
+ try {
598
+ return JSON.parse(content);
599
+ } catch (parseErr) {
600
+ if (content.trim().length === 0) {
601
+ return fallback;
602
+ }
603
+ throw new Error(
604
+ `Failed to parse ${filePath}: ${parseErr instanceof Error ? parseErr.message : String(parseErr)}`
605
+ );
606
+ }
607
+ }
608
+ async function ensureDir(dirPath) {
609
+ await fs4.mkdir(dirPath, { recursive: true });
610
+ }
611
+ var init_fs = __esm({
612
+ "src/utils/fs.ts"() {
613
+ "use strict";
614
+ init_logging();
615
+ init_file_lock();
616
+ }
617
+ });
618
+
619
+ // src/core/types.ts
620
+ var VALID_PRIORITIES, SESSION_MODELS, SESSION_MODEL_CLI_MAP, VALID_SESSION_MODEL_IDS, SESSION_EFFORT_IDS, VALID_SESSION_EFFORT_IDS, SESSION_MODES, SESSION_MODE_IDS, VALID_SESSION_MODE_IDS, SESSION_MODE_CLI_MAP, SESSION_MODE_LABELS;
621
+ var init_types = __esm({
622
+ "src/core/types.ts"() {
623
+ "use strict";
624
+ VALID_PRIORITIES = ["immediate", "important", "backlog", "none"];
625
+ SESSION_MODELS = [
626
+ { id: "opus", label: "Opus", description: "Most capable", cliModel: "opus", family: "opus", is1m: false },
627
+ { id: "opus-1m", label: "Opus 1M", description: "1M context window", cliModel: "opus[1m]", family: "opus", is1m: true },
628
+ { id: "sonnet", label: "Sonnet", description: "Balanced", cliModel: "sonnet", family: "sonnet", is1m: false },
629
+ { id: "sonnet-1m", label: "Sonnet 1M", description: "1M context window", cliModel: "sonnet[1m]", family: "sonnet", is1m: true },
630
+ { id: "haiku", label: "Haiku", description: "Fastest", cliModel: "haiku", family: "haiku", is1m: false },
631
+ { id: "fable", label: "Fable", description: "Fast & capable", cliModel: "fable", family: "fable", is1m: false },
632
+ { id: "fable-1m", label: "Fable 1M", description: "1M context window", cliModel: "fable[1m]", family: "fable", is1m: true }
633
+ ];
634
+ SESSION_MODEL_CLI_MAP = Object.fromEntries(SESSION_MODELS.map((m) => [m.id, m.cliModel]));
635
+ VALID_SESSION_MODEL_IDS = new Set(SESSION_MODELS.map((m) => m.id));
636
+ SESSION_EFFORT_IDS = ["low", "medium", "high", "xhigh", "max"];
637
+ VALID_SESSION_EFFORT_IDS = new Set(SESSION_EFFORT_IDS);
638
+ SESSION_MODES = [
639
+ { id: "plan", cli: "plan", label: "Plan", description: "Explore and plan only \u2014 no edits or commands" },
640
+ { id: "default", cli: "default", label: "Default", description: "Ask before anything sensitive" },
641
+ { id: "dontAsk", cli: "dontAsk", label: "Don't Ask", description: "Never prompt \u2014 deny whatever is not pre-approved" },
642
+ { id: "accept", cli: "acceptEdits", label: "Accept", description: "Auto-accept file edits, still ask for the rest" },
643
+ { id: "auto", cli: "auto", label: "Auto", description: "Classify each call and auto-allow the safe ones" },
644
+ { id: "bypass", cli: "bypassPermissions", label: "Bypass", description: "Full trust \u2014 never ask, allow everything" }
645
+ ];
646
+ SESSION_MODE_IDS = SESSION_MODES.map((m) => m.id);
647
+ VALID_SESSION_MODE_IDS = new Set(SESSION_MODE_IDS);
648
+ SESSION_MODE_CLI_MAP = Object.fromEntries(SESSION_MODES.map((m) => [m.id, m.cli]));
649
+ SESSION_MODE_LABELS = Object.fromEntries(SESSION_MODES.map((m) => [m.id, m.label]));
650
+ }
651
+ });
652
+
653
+ // src/agent/providers/model-catalog.ts
654
+ var MODEL_CATALOG;
655
+ var init_model_catalog = __esm({
656
+ "src/agent/providers/model-catalog.ts"() {
657
+ "use strict";
658
+ MODEL_CATALOG = {
659
+ // ── AWS Bedrock (Claude via cross-region inference) ──
660
+ // Models with 1M variants use model_id to map back to the same API model ID.
661
+ // The API has no -1m model; 1M capability is activated via the context-1m beta header.
662
+ // model_id therefore redirects the -1m catalog entry to the real API endpoint.
663
+ bedrock: [
664
+ // Opus 5: 1M context is native and the only size — no -1m variant, no beta header.
665
+ {
666
+ id: "global.anthropic.claude-opus-5",
667
+ provider: "bedrock",
668
+ label: "Opus 5",
669
+ max_tokens: 128e3,
670
+ context_window: 1e6,
671
+ compat: { thinking_format: "anthropic", supports_adaptive: true, native_1m: true }
672
+ },
673
+ {
674
+ id: "global.anthropic.claude-opus-4-8",
675
+ provider: "bedrock",
676
+ label: "Opus 4.8",
677
+ max_tokens: 128e3,
678
+ context_window: 2e5,
679
+ compat: { thinking_format: "anthropic", supports_adaptive: true }
680
+ },
681
+ {
682
+ id: "global.anthropic.claude-opus-4-8-1m",
683
+ provider: "bedrock",
684
+ model_id: "global.anthropic.claude-opus-4-8",
685
+ label: "Opus 4.8 (1M)",
686
+ max_tokens: 128e3,
687
+ context_window: 1e6,
688
+ compat: { thinking_format: "anthropic", supports_adaptive: true }
689
+ },
690
+ {
691
+ id: "global.anthropic.claude-opus-4-7",
692
+ provider: "bedrock",
693
+ label: "Opus 4.7",
694
+ max_tokens: 128e3,
695
+ context_window: 2e5,
696
+ compat: { thinking_format: "anthropic", supports_adaptive: true }
697
+ },
698
+ {
699
+ id: "global.anthropic.claude-opus-4-7-1m",
700
+ provider: "bedrock",
701
+ model_id: "global.anthropic.claude-opus-4-7",
702
+ label: "Opus 4.7 (1M)",
703
+ max_tokens: 128e3,
704
+ context_window: 1e6,
705
+ compat: { thinking_format: "anthropic", supports_adaptive: true }
706
+ },
707
+ {
708
+ id: "global.anthropic.claude-opus-4-6-v1",
709
+ provider: "bedrock",
710
+ label: "Opus 4.6",
711
+ max_tokens: 128e3,
712
+ context_window: 2e5,
713
+ compat: { thinking_format: "anthropic", supports_adaptive: true }
714
+ },
715
+ {
716
+ id: "global.anthropic.claude-opus-4-6-v1-1m",
717
+ provider: "bedrock",
718
+ model_id: "global.anthropic.claude-opus-4-6-v1",
719
+ label: "Opus 4.6 (1M)",
720
+ max_tokens: 128e3,
721
+ context_window: 1e6,
722
+ compat: { thinking_format: "anthropic", supports_adaptive: true }
723
+ },
724
+ {
725
+ id: "global.anthropic.claude-sonnet-4-6",
726
+ provider: "bedrock",
727
+ label: "Sonnet 4.6",
728
+ max_tokens: 64e3,
729
+ context_window: 2e5,
730
+ compat: { thinking_format: "anthropic", supports_adaptive: true }
731
+ },
732
+ {
733
+ id: "global.anthropic.claude-sonnet-4-6-1m",
734
+ provider: "bedrock",
735
+ model_id: "global.anthropic.claude-sonnet-4-6",
736
+ label: "Sonnet 4.6 (1M)",
737
+ max_tokens: 64e3,
738
+ context_window: 1e6,
739
+ compat: { thinking_format: "anthropic", supports_adaptive: true }
740
+ },
741
+ {
742
+ id: "us.anthropic.claude-haiku-4-5-20251001-v1:0",
743
+ provider: "bedrock",
744
+ label: "Haiku 4.5",
745
+ max_tokens: 64e3,
746
+ context_window: 2e5
747
+ }
748
+ // No extended thinking — Haiku 4.5 doesn't support it
749
+ ],
750
+ // ── Anthropic Direct API ──
751
+ anthropic: [
752
+ // Opus 5: 1M context is native and the only size — no -1m variant, no beta header.
753
+ {
754
+ id: "claude-opus-5",
755
+ provider: "anthropic",
756
+ label: "Opus 5",
757
+ max_tokens: 128e3,
758
+ context_window: 1e6,
759
+ compat: { thinking_format: "anthropic", supports_adaptive: true, native_1m: true }
760
+ },
761
+ {
762
+ id: "claude-opus-4-8",
763
+ provider: "anthropic",
764
+ label: "Opus 4.8",
765
+ max_tokens: 128e3,
766
+ context_window: 2e5,
767
+ compat: { thinking_format: "anthropic", supports_adaptive: true }
768
+ },
769
+ {
770
+ id: "claude-opus-4-8-1m",
771
+ provider: "anthropic",
772
+ model_id: "claude-opus-4-8",
773
+ label: "Opus 4.8 (1M)",
774
+ max_tokens: 128e3,
775
+ context_window: 1e6,
776
+ compat: { thinking_format: "anthropic", supports_adaptive: true }
777
+ },
778
+ {
779
+ id: "claude-opus-4-7",
780
+ provider: "anthropic",
781
+ label: "Opus 4.7",
782
+ max_tokens: 128e3,
783
+ context_window: 2e5,
784
+ compat: { thinking_format: "anthropic", supports_adaptive: true }
785
+ },
786
+ {
787
+ id: "claude-opus-4-7-1m",
788
+ provider: "anthropic",
789
+ model_id: "claude-opus-4-7",
790
+ label: "Opus 4.7 (1M)",
791
+ max_tokens: 128e3,
792
+ context_window: 1e6,
793
+ compat: { thinking_format: "anthropic", supports_adaptive: true }
794
+ },
795
+ {
796
+ id: "claude-opus-4-6",
797
+ provider: "anthropic",
798
+ label: "Opus 4.6",
799
+ max_tokens: 128e3,
800
+ context_window: 2e5,
801
+ compat: { thinking_format: "anthropic", supports_adaptive: true }
802
+ },
803
+ {
804
+ id: "claude-opus-4-6-1m",
805
+ provider: "anthropic",
806
+ model_id: "claude-opus-4-6",
807
+ label: "Opus 4.6 (1M)",
808
+ max_tokens: 128e3,
809
+ context_window: 1e6,
810
+ compat: { thinking_format: "anthropic", supports_adaptive: true }
811
+ },
812
+ {
813
+ id: "claude-sonnet-4-6",
814
+ provider: "anthropic",
815
+ label: "Sonnet 4.6",
816
+ max_tokens: 64e3,
817
+ context_window: 2e5,
818
+ compat: { thinking_format: "anthropic", supports_adaptive: true }
819
+ },
820
+ {
821
+ id: "claude-sonnet-4-6-1m",
822
+ provider: "anthropic",
823
+ model_id: "claude-sonnet-4-6",
824
+ label: "Sonnet 4.6 (1M)",
825
+ max_tokens: 64e3,
826
+ context_window: 1e6,
827
+ compat: { thinking_format: "anthropic", supports_adaptive: true }
828
+ },
829
+ {
830
+ id: "claude-haiku-4-5",
831
+ provider: "anthropic",
832
+ label: "Haiku 4.5",
833
+ max_tokens: 64e3,
834
+ context_window: 2e5
835
+ }
836
+ // No extended thinking — Haiku 4.5 doesn't support it
837
+ ],
838
+ // ── OpenAI ──
839
+ openai: [
840
+ {
841
+ id: "gpt-5.4",
842
+ provider: "openai",
843
+ label: "GPT-5.4",
844
+ max_tokens: 128e3,
845
+ context_window: 105e4
846
+ },
847
+ {
848
+ id: "gpt-5-mini-2025-08-07",
849
+ provider: "openai",
850
+ label: "GPT-5 Mini",
851
+ max_tokens: 128e3,
852
+ context_window: 4e5
853
+ },
854
+ {
855
+ id: "gpt-5-nano-2025-08-07",
856
+ provider: "openai",
857
+ label: "GPT-5 Nano",
858
+ max_tokens: 128e3,
859
+ context_window: 4e5
860
+ }
861
+ ],
862
+ // ── Google Gemini ──
863
+ gemini: [
864
+ {
865
+ id: "gemini-3.1-pro-preview",
866
+ provider: "gemini",
867
+ label: "Gemini 3.1 Pro",
868
+ max_tokens: 65536,
869
+ context_window: 1e6
870
+ },
871
+ {
872
+ id: "gemini-3-flash-preview",
873
+ provider: "gemini",
874
+ label: "Gemini 3 Flash",
875
+ max_tokens: 65536,
876
+ context_window: 1e6
877
+ },
878
+ {
879
+ id: "gemini-3.1-flash-lite-preview",
880
+ provider: "gemini",
881
+ label: "Gemini 3.1 Flash Lite",
882
+ max_tokens: 65536,
883
+ context_window: 1e6
884
+ }
885
+ ],
886
+ // ── OpenRouter (aggregator — sorted by release date, newest first) ──
887
+ openrouter: [
888
+ // -- Anthropic --
889
+ {
890
+ id: "anthropic/claude-opus-5",
891
+ provider: "openrouter",
892
+ label: "Claude Opus 5",
893
+ max_tokens: 128e3,
894
+ context_window: 1e6
895
+ },
896
+ {
897
+ id: "anthropic/claude-opus-5-fast",
898
+ provider: "openrouter",
899
+ label: "Claude Opus 5 (Fast)",
900
+ max_tokens: 128e3,
901
+ context_window: 1e6
902
+ },
903
+ {
904
+ id: "anthropic/claude-opus-4.6",
905
+ provider: "openrouter",
906
+ label: "Claude Opus 4.6",
907
+ max_tokens: 128e3,
908
+ context_window: 1e6
909
+ },
910
+ {
911
+ id: "anthropic/claude-sonnet-4.6",
912
+ provider: "openrouter",
913
+ label: "Claude Sonnet 4.6",
914
+ max_tokens: 64e3,
915
+ context_window: 1e6
916
+ },
917
+ // -- OpenAI --
918
+ {
919
+ id: "openai/gpt-5.2",
920
+ provider: "openrouter",
921
+ label: "GPT-5.2",
922
+ max_tokens: 128e3,
923
+ context_window: 4e5
924
+ },
925
+ {
926
+ id: "openai/gpt-5.2-pro",
927
+ provider: "openrouter",
928
+ label: "GPT-5.2 Pro",
929
+ max_tokens: 128e3,
930
+ context_window: 4e5
931
+ },
932
+ {
933
+ id: "openai/gpt-5.2-chat",
934
+ provider: "openrouter",
935
+ label: "GPT-5.2 Chat",
936
+ max_tokens: 128e3,
937
+ context_window: 128e3
938
+ },
939
+ // -- Google --
940
+ {
941
+ id: "google/gemini-3.1-pro-preview",
942
+ provider: "openrouter",
943
+ label: "Gemini 3.1 Pro",
944
+ max_tokens: 65536,
945
+ context_window: 1048576
946
+ },
947
+ {
948
+ id: "google/gemini-3-flash-preview",
949
+ provider: "openrouter",
950
+ label: "Gemini 3 Flash",
951
+ max_tokens: 65536,
952
+ context_window: 1048576
953
+ },
954
+ // -- Qwen --
955
+ {
956
+ id: "qwen/qwen3.5-plus-02-15",
957
+ provider: "openrouter",
958
+ label: "Qwen 3.5 Plus",
959
+ max_tokens: 128e3,
960
+ context_window: 1e6
961
+ },
962
+ {
963
+ id: "qwen/qwen3.5-397b-a17b",
964
+ provider: "openrouter",
965
+ label: "Qwen 3.5 397B",
966
+ max_tokens: 128e3,
967
+ context_window: 262144
968
+ },
969
+ {
970
+ id: "qwen/qwen3-max-thinking",
971
+ provider: "openrouter",
972
+ label: "Qwen3 Max Thinking",
973
+ max_tokens: 128e3,
974
+ context_window: 262144
975
+ },
976
+ {
977
+ id: "qwen/qwen3-coder-next",
978
+ provider: "openrouter",
979
+ label: "Qwen3 Coder Next",
980
+ max_tokens: 128e3,
981
+ context_window: 262144
982
+ },
983
+ // -- MiniMax --
984
+ {
985
+ id: "minimax/minimax-m2.5",
986
+ provider: "openrouter",
987
+ label: "MiniMax M2.5",
988
+ max_tokens: 128e3,
989
+ context_window: 196608
990
+ },
991
+ {
992
+ id: "minimax/minimax-m2.1",
993
+ provider: "openrouter",
994
+ label: "MiniMax M2.1",
995
+ max_tokens: 128e3,
996
+ context_window: 196608
997
+ },
998
+ {
999
+ id: "minimax/minimax-m2-her",
1000
+ provider: "openrouter",
1001
+ label: "MiniMax M2 Her",
1002
+ max_tokens: 128e3,
1003
+ context_window: 65536
1004
+ },
1005
+ // -- Z.ai (GLM) --
1006
+ {
1007
+ id: "z-ai/glm-5",
1008
+ provider: "openrouter",
1009
+ label: "GLM 5",
1010
+ max_tokens: 128e3,
1011
+ context_window: 202752
1012
+ },
1013
+ {
1014
+ id: "z-ai/glm-4.7",
1015
+ provider: "openrouter",
1016
+ label: "GLM 4.7",
1017
+ max_tokens: 128e3,
1018
+ context_window: 202752
1019
+ },
1020
+ {
1021
+ id: "z-ai/glm-4.7-flash",
1022
+ provider: "openrouter",
1023
+ label: "GLM 4.7 Flash",
1024
+ max_tokens: 128e3,
1025
+ context_window: 202752
1026
+ },
1027
+ // -- Moonshot / Kimi --
1028
+ {
1029
+ id: "moonshotai/kimi-k2.5",
1030
+ provider: "openrouter",
1031
+ label: "Kimi K2.5",
1032
+ max_tokens: 128e3,
1033
+ context_window: 262144
1034
+ },
1035
+ // -- AionLabs --
1036
+ {
1037
+ id: "aion-labs/aion-2.0",
1038
+ provider: "openrouter",
1039
+ label: "Aion 2.0",
1040
+ max_tokens: 128e3,
1041
+ context_window: 131072
1042
+ },
1043
+ // -- StepFun --
1044
+ {
1045
+ id: "stepfun/step-3.5-flash",
1046
+ provider: "openrouter",
1047
+ label: "Step 3.5 Flash",
1048
+ max_tokens: 128e3,
1049
+ context_window: 256e3
1050
+ },
1051
+ {
1052
+ id: "stepfun/step-3.5-flash:free",
1053
+ provider: "openrouter",
1054
+ label: "Step 3.5 Flash (free)",
1055
+ max_tokens: 128e3,
1056
+ context_window: 256e3
1057
+ },
1058
+ // -- ByteDance Seed --
1059
+ {
1060
+ id: "bytedance-seed/seed-1.6",
1061
+ provider: "openrouter",
1062
+ label: "Seed 1.6",
1063
+ max_tokens: 128e3,
1064
+ context_window: 262144
1065
+ },
1066
+ {
1067
+ id: "bytedance-seed/seed-1.6-flash",
1068
+ provider: "openrouter",
1069
+ label: "Seed 1.6 Flash",
1070
+ max_tokens: 128e3,
1071
+ context_window: 262144
1072
+ },
1073
+ // -- Xiaomi --
1074
+ {
1075
+ id: "xiaomi/mimo-v2-flash",
1076
+ provider: "openrouter",
1077
+ label: "MiMo V2 Flash",
1078
+ max_tokens: 128e3,
1079
+ context_window: 262144
1080
+ },
1081
+ // -- NVIDIA --
1082
+ {
1083
+ id: "nvidia/nemotron-3-nano-30b-a3b",
1084
+ provider: "openrouter",
1085
+ label: "Nemotron 3 Nano 30B",
1086
+ max_tokens: 128e3,
1087
+ context_window: 262144
1088
+ },
1089
+ {
1090
+ id: "nvidia/nemotron-3-nano-30b-a3b:free",
1091
+ provider: "openrouter",
1092
+ label: "Nemotron 3 Nano 30B (free)",
1093
+ max_tokens: 128e3,
1094
+ context_window: 256e3
1095
+ },
1096
+ // -- Writer --
1097
+ {
1098
+ id: "writer/palmyra-x5",
1099
+ provider: "openrouter",
1100
+ label: "Palmyra X5",
1101
+ max_tokens: 128e3,
1102
+ context_window: 104e4
1103
+ },
1104
+ // -- Upstage --
1105
+ {
1106
+ id: "upstage/solar-pro-3",
1107
+ provider: "openrouter",
1108
+ label: "Solar Pro 3",
1109
+ max_tokens: 128e3,
1110
+ context_window: 128e3
1111
+ },
1112
+ // -- Mistral --
1113
+ {
1114
+ id: "mistralai/mistral-small-creative",
1115
+ provider: "openrouter",
1116
+ label: "Mistral Small Creative",
1117
+ max_tokens: 128e3,
1118
+ context_window: 32768
1119
+ },
1120
+ // -- AllenAI --
1121
+ {
1122
+ id: "allenai/olmo-3.1-32b-instruct",
1123
+ provider: "openrouter",
1124
+ label: "OLMo 3.1 32B Instruct",
1125
+ max_tokens: 128e3,
1126
+ context_window: 65536
1127
+ },
1128
+ {
1129
+ id: "allenai/olmo-3.1-32b-think",
1130
+ provider: "openrouter",
1131
+ label: "OLMo 3.1 32B Think",
1132
+ max_tokens: 128e3,
1133
+ context_window: 65536
1134
+ },
1135
+ {
1136
+ id: "allenai/molmo-2-8b",
1137
+ provider: "openrouter",
1138
+ label: "Molmo2 8B",
1139
+ max_tokens: 128e3,
1140
+ context_window: 36864
1141
+ },
1142
+ // -- Arcee --
1143
+ {
1144
+ id: "arcee-ai/trinity-large-preview:free",
1145
+ provider: "openrouter",
1146
+ label: "Trinity Large Preview (free)",
1147
+ max_tokens: 128e3,
1148
+ context_window: 131e3
1149
+ }
1150
+ ],
1151
+ // ── Ollama (local) — empty baseline, user adds their installed models ──
1152
+ ollama: [],
1153
+ // ── Claude Code subscription (local `claude -p`, text-only) ──
1154
+ // The CLI picks the actual model from the subscription; these are short
1155
+ // aliases the adapter forwards as --model (a bare alias resolves server-side).
1156
+ // 'default' means "let the subscription decide" (adapter sends no --model).
1157
+ claude_cli: [
1158
+ { id: "default", provider: "claude_cli", label: "Subscription default", context_window: 2e5 },
1159
+ { id: "opus", provider: "claude_cli", label: "Opus (subscription)", context_window: 2e5 },
1160
+ { id: "sonnet", provider: "claude_cli", label: "Sonnet (subscription)", context_window: 2e5 },
1161
+ { id: "haiku", provider: "claude_cli", label: "Haiku (subscription)", context_window: 2e5 }
1162
+ ]
1163
+ };
1164
+ }
1165
+ });
1166
+
1167
+ // src/core/ssh-config-scanner.ts
1168
+ import fs5 from "fs/promises";
1169
+ import path5 from "path";
1170
+ import os2 from "os";
1171
+ function isGitServerHost(h) {
1172
+ if (h.user === "git") return true;
1173
+ const gitPrefix = /^(git|github|gitlab|bitbucket|codecommit|ssh\.github)\./i;
1174
+ return gitPrefix.test(h.alias) || gitPrefix.test(h.hostname);
1175
+ }
1176
+ function shortLabelForFqdn(hostname) {
1177
+ const first = hostname.split(".")[0];
1178
+ if (first.length <= 24) return first;
1179
+ const parts = first.split("-");
1180
+ if (parts.length >= 3) {
1181
+ return `${parts[0]}-${parts[1]}\u2026${parts[parts.length - 1]}`;
1182
+ }
1183
+ return `${first.slice(0, 12)}\u2026${first.slice(-8)}`;
1184
+ }
1185
+ async function scanSshConfig(configPath) {
1186
+ const sshConfigPath = configPath ?? path5.join(os2.homedir(), ".ssh", "config");
1187
+ let mtimeMs2;
1188
+ try {
1189
+ mtimeMs2 = (await fs5.stat(sshConfigPath)).mtimeMs;
1190
+ } catch {
1191
+ return /* @__PURE__ */ new Map();
1192
+ }
1193
+ const cached = scanCache.get(sshConfigPath);
1194
+ if (cached && cached.mtimeMs === mtimeMs2) return cached.result;
1195
+ const result = await parseSshConfigFile(sshConfigPath);
1196
+ scanCache.set(sshConfigPath, { mtimeMs: mtimeMs2, result });
1197
+ return result;
1198
+ }
1199
+ async function parseSshConfigFile(sshConfigPath) {
1200
+ const discovered = /* @__PURE__ */ new Map();
1201
+ try {
1202
+ const content = await fs5.readFile(sshConfigPath, "utf-8");
1203
+ const lines = content.split("\n");
1204
+ let currentHost = null;
1205
+ for (const rawLine of lines) {
1206
+ const line = rawLine.trim();
1207
+ if (!line || line.startsWith("#")) continue;
1208
+ if (line.match(/^Host\s+/i)) {
1209
+ if (currentHost && currentHost.hostname) {
1210
+ discovered.set(currentHost.alias, currentHost);
1211
+ }
1212
+ const hostMatch = line.match(/^Host\s+(\S+)/i);
1213
+ if (hostMatch) {
1214
+ const alias = hostMatch[1];
1215
+ if (alias.includes("*") || alias.includes("?")) {
1216
+ currentHost = null;
1217
+ continue;
1218
+ }
1219
+ currentHost = { alias, hostname: alias };
1220
+ }
1221
+ continue;
1222
+ }
1223
+ if (!currentHost) continue;
1224
+ const hostnameMatch = line.match(/^HostName\s+(\S+)/i);
1225
+ if (hostnameMatch) {
1226
+ currentHost.hostname = hostnameMatch[1];
1227
+ continue;
1228
+ }
1229
+ const userMatch = line.match(/^User\s+(\S+)/i);
1230
+ if (userMatch) {
1231
+ currentHost.user = userMatch[1];
1232
+ continue;
1233
+ }
1234
+ const portMatch = line.match(/^Port\s+(\d+)/i);
1235
+ if (portMatch) {
1236
+ currentHost.port = Number.parseInt(portMatch[1], 10);
1237
+ continue;
1238
+ }
1239
+ }
1240
+ if (currentHost && currentHost.hostname) {
1241
+ discovered.set(currentHost.alias, currentHost);
1242
+ }
1243
+ for (const [alias, h] of discovered) {
1244
+ if (isGitServerHost(h)) discovered.delete(alias);
1245
+ }
1246
+ const byHostname = /* @__PURE__ */ new Map();
1247
+ for (const h of discovered.values()) {
1248
+ const key = h.hostname.toLowerCase();
1249
+ const existing = byHostname.get(key);
1250
+ if (!existing) {
1251
+ byHostname.set(key, h);
1252
+ continue;
1253
+ }
1254
+ const existingIsShortAlias = existing.alias !== existing.hostname;
1255
+ const candidateIsShortAlias = h.alias !== h.hostname;
1256
+ if (candidateIsShortAlias && !existingIsShortAlias) byHostname.set(key, h);
1257
+ }
1258
+ const result = /* @__PURE__ */ new Map();
1259
+ for (const h of byHostname.values()) {
1260
+ if (h.alias === h.hostname && !h.label) h.label = shortLabelForFqdn(h.hostname);
1261
+ result.set(h.alias, h);
1262
+ }
1263
+ log.session.info("ssh-config-scanner: discovered hosts", {
1264
+ count: result.size,
1265
+ dropped: discovered.size - result.size
1266
+ });
1267
+ return result;
1268
+ } catch (err) {
1269
+ if (err.code === "ENOENT") {
1270
+ log.session.debug("ssh-config-scanner: no ~/.ssh/config found");
1271
+ } else {
1272
+ log.session.warn("ssh-config-scanner: failed to parse SSH config", {
1273
+ error: err instanceof Error ? err.message : String(err)
1274
+ });
1275
+ }
1276
+ return discovered;
1277
+ }
1278
+ }
1279
+ var scanCache;
1280
+ var init_ssh_config_scanner = __esm({
1281
+ "src/core/ssh-config-scanner.ts"() {
1282
+ "use strict";
1283
+ init_logging();
1284
+ scanCache = /* @__PURE__ */ new Map();
1285
+ }
1286
+ });
1287
+
1288
+ // src/core/config-manager.ts
1289
+ import fs6 from "fs/promises";
1290
+ import yaml from "js-yaml";
1291
+ function migrateConfigToProjectOnly(config) {
1292
+ let changed = false;
1293
+ const defaults = config.defaults;
1294
+ if (defaults && "category" in defaults) {
1295
+ delete defaults.category;
1296
+ changed = true;
1297
+ }
1298
+ if ("local" in config) {
1299
+ delete config.local;
1300
+ changed = true;
1301
+ }
1302
+ const favorites = config.favorites;
1303
+ if (favorites && "categories" in favorites) {
1304
+ delete favorites.categories;
1305
+ changed = true;
1306
+ }
1307
+ const ordering = config.ordering;
1308
+ if (ordering && ("categories" in ordering || !Array.isArray(ordering.projects))) {
1309
+ const catOrder = Array.isArray(ordering.categories) ? ordering.categories : [];
1310
+ const nested = ordering.projects;
1311
+ const flat = [];
1312
+ const seen = /* @__PURE__ */ new Set();
1313
+ const push = (raw) => {
1314
+ const name = typeof raw === "string" ? raw.trim() : "";
1315
+ if (!name) return;
1316
+ const key = name.toLowerCase();
1317
+ if (seen.has(key)) return;
1318
+ seen.add(key);
1319
+ flat.push(name);
1320
+ };
1321
+ if (nested && typeof nested === "object" && !Array.isArray(nested)) {
1322
+ const byCategory = nested;
1323
+ const ordered = [
1324
+ ...catOrder.map((c) => typeof c === "string" ? c : ""),
1325
+ ...Object.keys(byCategory).filter(
1326
+ (k) => !catOrder.some((c) => typeof c === "string" && c.toLowerCase() === k.toLowerCase())
1327
+ )
1328
+ ];
1329
+ for (const cat of ordered) {
1330
+ const key = Object.keys(byCategory).find((k) => k.toLowerCase() === cat.toLowerCase());
1331
+ if (!key) continue;
1332
+ const list = byCategory[key];
1333
+ if (Array.isArray(list)) for (const p of list) push(p);
1334
+ }
1335
+ } else if (Array.isArray(nested)) {
1336
+ for (const p of nested) push(p);
1337
+ }
1338
+ delete ordering.categories;
1339
+ ordering.projects = flat;
1340
+ changed = true;
1341
+ }
1342
+ const plugins = config.plugins;
1343
+ for (const entry of Object.values(plugins ?? {})) {
1344
+ if (!entry || typeof entry !== "object") continue;
1345
+ const plugin = entry;
1346
+ if ("category" in plugin) {
1347
+ if (plugin.project === void 0 && typeof plugin.category === "string") {
1348
+ plugin.project = plugin.category;
1349
+ }
1350
+ delete plugin.category;
1351
+ changed = true;
1352
+ }
1353
+ if ("categories" in plugin) {
1354
+ delete plugin.categories;
1355
+ changed = true;
1356
+ }
1357
+ }
1358
+ return changed;
1359
+ }
1360
+ async function readRawConfigContent() {
1361
+ try {
1362
+ return await fs6.readFile(CONFIG_FILE, "utf-8");
1363
+ } catch (primaryErr) {
1364
+ let backup;
1365
+ try {
1366
+ backup = await fs6.readFile(CONFIG_BACKUP_FILE, "utf-8");
1367
+ } catch {
1368
+ return null;
1369
+ }
1370
+ if (!backup.trim()) return null;
1371
+ log.session.error("config-manager: config.yaml missing/unreadable \u2014 recovering from config.yaml.bak", {
1372
+ error: primaryErr instanceof Error ? primaryErr.message : String(primaryErr),
1373
+ backup: CONFIG_BACKUP_FILE
1374
+ });
1375
+ try {
1376
+ await fs6.writeFile(CONFIG_FILE, backup, "utf-8");
1377
+ } catch {
1378
+ }
1379
+ return backup;
1380
+ }
1381
+ }
1382
+ async function writeConfigWithBackup(content) {
1383
+ await fs6.writeFile(CONFIG_FILE, content, "utf-8");
1384
+ try {
1385
+ await fs6.writeFile(CONFIG_BACKUP_FILE, content, "utf-8");
1386
+ } catch (err) {
1387
+ log.session.warn("config-manager: failed to update config backup", {
1388
+ error: err instanceof Error ? err.message : String(err)
1389
+ });
1390
+ }
1391
+ }
1392
+ async function getConfig() {
1393
+ try {
1394
+ const content = await readRawConfigContent();
1395
+ if (content === null) throw new Error("no config file and no backup");
1396
+ const parsed = yaml.load(content);
1397
+ const config = { ...DEFAULT_CONFIG, ...parsed };
1398
+ migrateConfigToProjectOnly(config);
1399
+ if (config.defaults?.priority && !VALID_PRIORITIES.includes(config.defaults.priority)) {
1400
+ const p = config.defaults.priority;
1401
+ if (p === "high") config.defaults.priority = "immediate";
1402
+ else if (p === "medium" || p === "low") config.defaults.priority = "backlog";
1403
+ else config.defaults.priority = "none";
1404
+ }
1405
+ if (!config.agent?.available_models) {
1406
+ config.agent = { ...config.agent, available_models: (MODEL_CATALOG.bedrock ?? []).map((m) => m.id) };
1407
+ }
1408
+ if (!config.agent?.main_model) {
1409
+ config.agent = { ...config.agent, main_model: (MODEL_CATALOG.bedrock ?? [])[0]?.id };
1410
+ }
1411
+ await mergeSshDiscoveredHosts(config);
1412
+ return config;
1413
+ } catch (err) {
1414
+ const firstRun = err instanceof Error && err.message === "no config file and no backup";
1415
+ const detail = { error: err instanceof Error ? err.message : String(err), configFile: CONFIG_FILE };
1416
+ if (firstRun) log.session.info("config-manager: no config.yaml and no backup \u2014 first run, using defaults", detail);
1417
+ else log.session.error("config-manager: config.yaml UNREADABLE and backup did not cover it \u2014 falling back to DEFAULTS. Machine-local settings (hosts/plugins/stt/provider) are missing until this is fixed.", detail);
1418
+ const defaultModels = (MODEL_CATALOG.bedrock ?? []).map((m) => m.id);
1419
+ return { ...DEFAULT_CONFIG, agent: { available_models: defaultModels, main_model: defaultModels[0] } };
1420
+ }
1421
+ }
1422
+ async function mergeSshDiscoveredHosts(config) {
1423
+ const discovered = await scanSshConfig();
1424
+ if (discovered.size === 0) return;
1425
+ if (!config.hosts) {
1426
+ config.hosts = {};
1427
+ }
1428
+ const knownHostnames = new Set(
1429
+ Object.values(config.hosts).map((h) => h.hostname.toLowerCase())
1430
+ );
1431
+ for (const [alias, host] of discovered) {
1432
+ if (config.hosts[alias] || knownHostnames.has(host.hostname.toLowerCase())) {
1433
+ continue;
1434
+ }
1435
+ config.hosts[alias] = {
1436
+ hostname: host.hostname,
1437
+ user: host.user,
1438
+ port: host.port,
1439
+ label: host.label,
1440
+ enabled: true,
1441
+ discovered: true
1442
+ };
1443
+ knownHostnames.add(host.hostname.toLowerCase());
1444
+ }
1445
+ }
1446
+ function withWriteLock(fn) {
1447
+ const prev = writeLock;
1448
+ let resolve;
1449
+ writeLock = new Promise((r) => {
1450
+ resolve = r;
1451
+ });
1452
+ return prev.then(fn).finally(() => resolve());
1453
+ }
1454
+ async function updateConfig(partial) {
1455
+ return withWriteLock(async () => {
1456
+ let existing = {};
1457
+ try {
1458
+ const content2 = await readRawConfigContent();
1459
+ existing = content2 === null ? {} : yaml.load(content2) ?? {};
1460
+ } catch (err) {
1461
+ log.session.warn("config-manager: existing config unreadable, starting from empty", {
1462
+ error: err instanceof Error ? err.message : String(err)
1463
+ });
1464
+ }
1465
+ const merged = { ...existing };
1466
+ for (const [key, value] of Object.entries(partial)) {
1467
+ if (value !== void 0) {
1468
+ merged[key] = value;
1469
+ }
1470
+ }
1471
+ let content = yaml.dump(merged, { indent: 2, lineWidth: 120 });
1472
+ content = content.replace(
1473
+ /^(\s+)available_models:/m,
1474
+ "$1# Available models for the agent form dropdown.\n$1# Edit this list to add or remove models.\n$1available_models:"
1475
+ );
1476
+ await writeConfigWithBackup(content);
1477
+ });
1478
+ }
1479
+ async function migrateConfigFileToProjectOnly() {
1480
+ return withWriteLock(async () => {
1481
+ let parsed;
1482
+ try {
1483
+ const content2 = await readRawConfigContent();
1484
+ if (content2 === null) return false;
1485
+ parsed = yaml.load(content2) ?? {};
1486
+ } catch (err) {
1487
+ log.session.warn("config-manager: skipping project-only migration, config unreadable", {
1488
+ error: err instanceof Error ? err.message : String(err)
1489
+ });
1490
+ return false;
1491
+ }
1492
+ if (!migrateConfigToProjectOnly(parsed)) return false;
1493
+ let content = yaml.dump(parsed, { indent: 2, lineWidth: 120 });
1494
+ content = content.replace(
1495
+ /^(\s+)available_models:/m,
1496
+ "$1# Available models for the agent form dropdown.\n$1# Edit this list to add or remove models.\n$1available_models:"
1497
+ );
1498
+ await writeConfigWithBackup(content);
1499
+ log.session.info("config-manager: migrated config.yaml to the project-only model");
1500
+ return true;
1501
+ });
1502
+ }
1503
+ async function seedConfigDefaults() {
1504
+ await migrateConfigFileToProjectOnly();
1505
+ const config = await getConfig();
1506
+ let needsWrite = false;
1507
+ try {
1508
+ const raw = await fs6.readFile(CONFIG_FILE, "utf-8");
1509
+ if (!raw.includes("available_models")) {
1510
+ needsWrite = true;
1511
+ }
1512
+ } catch (err) {
1513
+ log.session.debug("config-manager: no config file found, will write defaults", {
1514
+ error: err instanceof Error ? err.message : String(err)
1515
+ });
1516
+ needsWrite = true;
1517
+ }
1518
+ if (needsWrite) {
1519
+ await updateConfig({
1520
+ agent: config.agent,
1521
+ version: config.version,
1522
+ user: config.user,
1523
+ defaults: config.defaults,
1524
+ provider: config.provider
1525
+ });
1526
+ }
1527
+ }
1528
+ var DEFAULT_CONFIG, CONFIG_BACKUP_FILE, writeLock;
1529
+ var init_config_manager = __esm({
1530
+ "src/core/config-manager.ts"() {
1531
+ "use strict";
1532
+ init_logging();
1533
+ init_constants();
1534
+ init_types();
1535
+ init_model_catalog();
1536
+ init_ssh_config_scanner();
1537
+ DEFAULT_CONFIG = {
1538
+ version: 1,
1539
+ user: {},
1540
+ // New tasks default to Inbox (no project) and stay local, so quick-add is
1541
+ // instant and never silently routed to an external sync target.
1542
+ // `platform: 'local'` makes the source explicit. Existing users keep whatever
1543
+ // `defaults` is already on disk (getConfig spreads parsed over DEFAULT_CONFIG),
1544
+ // so this never re-routes an established setup.
1545
+ defaults: { priority: "none", platform: "local" },
1546
+ provider: { type: "claude-code" }
1547
+ };
1548
+ CONFIG_BACKUP_FILE = `${CONFIG_FILE}.bak`;
1549
+ writeLock = Promise.resolve();
1550
+ }
1551
+ });
1552
+
1553
+ // src/core/phase.ts
1554
+ function deriveStatusFromPhase(phase) {
1555
+ return PHASE_TO_STATUS[phase] ?? "todo";
1556
+ }
1557
+ var PHASE_TO_STATUS, PHASE_ORDER, VALID_PHASES;
1558
+ var init_phase = __esm({
1559
+ "src/core/phase.ts"() {
1560
+ "use strict";
1561
+ init_logging();
1562
+ PHASE_TO_STATUS = {
1563
+ TODO: "todo",
1564
+ IN_PROGRESS: "in_progress",
1565
+ AGENT_COMPLETE: "in_progress",
1566
+ AWAIT_HUMAN_ACTION: "in_progress",
1567
+ HUMAN_VERIFIED: "in_progress",
1568
+ POST_WORK_COMPLETED: "in_progress",
1569
+ COMPLETE: "done"
1570
+ };
1571
+ PHASE_ORDER = [
1572
+ "TODO",
1573
+ "IN_PROGRESS",
1574
+ "AGENT_COMPLETE",
1575
+ "AWAIT_HUMAN_ACTION",
1576
+ "HUMAN_VERIFIED",
1577
+ "POST_WORK_COMPLETED",
1578
+ "COMPLETE"
1579
+ ];
1580
+ VALID_PHASES = new Set(PHASE_ORDER);
1581
+ }
1582
+ });
1583
+
1584
+ // src/integrations/ms-todo/phase.ts
1585
+ function phaseToMsStatus(phase) {
1586
+ return PHASE_TO_MS_STATUS[phase] ?? "notStarted";
1587
+ }
1588
+ function phaseFromMsStatus(msStatus) {
1589
+ return MS_STATUS_TO_DEFAULT_PHASE[msStatus] ?? "TODO";
1590
+ }
1591
+ var PHASE_TO_MS_STATUS, MS_STATUS_TO_DEFAULT_PHASE;
1592
+ var init_phase2 = __esm({
1593
+ "src/integrations/ms-todo/phase.ts"() {
1594
+ "use strict";
1595
+ PHASE_TO_MS_STATUS = {
1596
+ TODO: "notStarted",
1597
+ IN_PROGRESS: "inProgress",
1598
+ AGENT_COMPLETE: "inProgress",
1599
+ AWAIT_HUMAN_ACTION: "inProgress",
1600
+ HUMAN_VERIFIED: "inProgress",
1601
+ POST_WORK_COMPLETED: "inProgress",
1602
+ COMPLETE: "completed"
1603
+ };
1604
+ MS_STATUS_TO_DEFAULT_PHASE = {
1605
+ notStarted: "TODO",
1606
+ inProgress: "IN_PROGRESS",
1607
+ completed: "COMPLETE"
1608
+ };
1609
+ }
1610
+ });
1611
+
1612
+ // src/utils/format.ts
1613
+ import crypto2 from "crypto";
1614
+ function parseProjectFromListName(listDisplayName) {
1615
+ const sep = " / ";
1616
+ const raw = (listDisplayName ?? "").trim();
1617
+ const idx = raw.indexOf(sep);
1618
+ if (idx === -1) return titleCase(raw);
1619
+ return titleCase(raw.slice(idx + sep.length).trim());
1620
+ }
1621
+ function isRetiredQuickStartGroup(name) {
1622
+ return (name ?? "").trim().toLowerCase() === "quick start";
1623
+ }
1624
+ function isLegacyInboxGroup(name) {
1625
+ return (name ?? "").trim().toLowerCase() === "inbox";
1626
+ }
1627
+ function routePulledListToProject(listDisplayName) {
1628
+ const raw = (listDisplayName ?? "").trim();
1629
+ if (!raw) return "";
1630
+ if (isLegacyInboxGroup(raw)) return "";
1631
+ const project = parseProjectFromListName(raw);
1632
+ if (isRetiredQuickStartGroup(project)) return "";
1633
+ return project;
1634
+ }
1635
+ function titleCase(s) {
1636
+ if (!s) return s;
1637
+ return s.charAt(0).toUpperCase() + s.slice(1);
1638
+ }
1639
+ var init_format = __esm({
1640
+ "src/utils/format.ts"() {
1641
+ "use strict";
1642
+ }
1643
+ });
1644
+
1645
+ // src/utils/file-ops.ts
1646
+ import crypto3 from "crypto";
1647
+ import fsp3 from "fs/promises";
1648
+ import path6 from "path";
1649
+ var MAX_READ_SIZE;
1650
+ var init_file_ops = __esm({
1651
+ "src/utils/file-ops.ts"() {
1652
+ "use strict";
1653
+ init_file_lock();
1654
+ MAX_READ_SIZE = 10 * 1024 * 1024;
1655
+ }
1656
+ });
1657
+
1658
+ // src/core/memory-file.ts
1659
+ import fs7 from "fs";
1660
+ import path7 from "path";
1661
+ function resolveMemoryPath(agentId) {
1662
+ if (!agentId || agentId === "general") return MEMORY_FILE;
1663
+ return path7.join(agentMemoryDir(agentId), "MEMORY.md");
1664
+ }
1665
+ function ensureMemoryFile(agentId) {
1666
+ const filePath = resolveMemoryPath(agentId);
1667
+ if (fs7.existsSync(filePath)) return;
1668
+ fs7.mkdirSync(path7.dirname(filePath), { recursive: true });
1669
+ fs7.writeFileSync(filePath, DEFAULT_TEMPLATE, "utf-8");
1670
+ }
1671
+ var DEFAULT_TEMPLATE;
1672
+ var init_memory_file = __esm({
1673
+ "src/core/memory-file.ts"() {
1674
+ "use strict";
1675
+ init_logging();
1676
+ init_constants();
1677
+ init_file_ops();
1678
+ DEFAULT_TEMPLATE = `---
1679
+ name: Global Memory
1680
+ description: >
1681
+ Curated knowledge and preferences. Updated by the agent as it learns.
1682
+ ---
1683
+ `;
1684
+ }
1685
+ });
1686
+
1687
+ // src/core/tmp-sweep.ts
1688
+ import fsp4 from "fs/promises";
1689
+ import path8 from "path";
1690
+ function sweepDirs() {
1691
+ return [
1692
+ WALNUT_HOME,
1693
+ SYNC_DIR,
1694
+ path8.join(WALNUT_HOME, "sessions"),
1695
+ path8.join(WALNUT_HOME, "sessions", "transcripts"),
1696
+ TASKS_DIR,
1697
+ path8.join(TASKS_DIR, "outbox"),
1698
+ CONVERSATIONS_DIR,
1699
+ MEMORY_DIR,
1700
+ DAILY_DIR,
1701
+ // Projection cache (Phase 3) — gitignored, but a crashed rename still
1702
+ // leaves an orphan the read path never cleans up.
1703
+ PROJECTION_CACHE_DIR,
1704
+ TRANSCRIPT_CACHE_DIR,
1705
+ // Offline task-op queue (Phase 4) — same story: gitignored, but a torn
1706
+ // write leaves a .tmp the flush loop skips forever.
1707
+ TASK_QUEUE_DIR
1708
+ ];
1709
+ }
1710
+ async function sweepOrphanAtomicTmpFiles() {
1711
+ const removed = [];
1712
+ const cutoff = Date.now() - MIN_AGE_MS;
1713
+ for (const dir of sweepDirs()) {
1714
+ try {
1715
+ const entries = await fsp4.readdir(dir, { withFileTypes: true });
1716
+ for (const entry of entries) {
1717
+ if (!entry.isFile() || !ORPHAN_RE.test(entry.name)) continue;
1718
+ const full = path8.join(dir, entry.name);
1719
+ try {
1720
+ const st = await fsp4.stat(full);
1721
+ if (st.mtimeMs > cutoff) continue;
1722
+ await fsp4.unlink(full);
1723
+ removed.push(full);
1724
+ } catch {
1725
+ }
1726
+ }
1727
+ } catch {
1728
+ }
1729
+ }
1730
+ if (removed.length > 0) {
1731
+ log.web.info("swept orphaned atomic-write temp files", {
1732
+ count: removed.length,
1733
+ files: removed.slice(0, 10)
1734
+ });
1735
+ }
1736
+ return removed;
1737
+ }
1738
+ var ORPHAN_RE, MIN_AGE_MS;
1739
+ var init_tmp_sweep = __esm({
1740
+ "src/core/tmp-sweep.ts"() {
1741
+ "use strict";
1742
+ init_constants();
1743
+ init_logging();
1744
+ ORPHAN_RE = /^\.open-walnut-[0-9a-f]+\.tmp$/;
1745
+ MIN_AGE_MS = 60 * 60 * 1e3;
1746
+ }
1747
+ });
1748
+
1749
+ // src/core/init.ts
1750
+ import fsp5 from "fs/promises";
1751
+ import fs8 from "fs";
1752
+ async function migrateLegacyMemoryFile() {
1753
+ try {
1754
+ if (fs8.existsSync(LEGACY_MEMORY_FILE) && !fs8.existsSync(MEMORY_FILE)) {
1755
+ await fsp5.rename(LEGACY_MEMORY_FILE, MEMORY_FILE);
1756
+ log.memory.info("Migrated MEMORY.md into memory/ folder", { from: LEGACY_MEMORY_FILE, to: MEMORY_FILE });
1757
+ }
1758
+ } catch (err) {
1759
+ log.memory.warn("MEMORY.md migration failed (will retry next start)", {
1760
+ error: err instanceof Error ? err.message : String(err)
1761
+ });
1762
+ }
1763
+ }
1764
+ async function initDirectories() {
1765
+ await ensureDir(WALNUT_HOME);
1766
+ await ensureDir(TASKS_DIR);
1767
+ await ensureDir(ARCHIVE_DIR);
1768
+ await ensureDir(MEMORY_DIR);
1769
+ await ensureDir(SESSIONS_DIR);
1770
+ await ensureDir(SYNC_DIR);
1771
+ await ensureDir(DAILY_DIR);
1772
+ await ensureDir(REPOS_MEMORY_DIR);
1773
+ await ensureDir(COMPACTION_DIR);
1774
+ await ensureDir(NOTES_DIR);
1775
+ await ensureDir(REPOSITORIES_DIR);
1776
+ await ensureDir(TIMELINE_DIR);
1777
+ await ensureDir(TMP_DIR);
1778
+ await ensureDir(RECORDINGS_DIR);
1779
+ await ensureDir(CONFIG_SHARE_DIR);
1780
+ await ensureDir(COMMANDS_DIR);
1781
+ await ensureDir(GLOBAL_SKILLS_DIR);
1782
+ await ensureDir(SESSION_STREAMS_DIR);
1783
+ await ensureDir(IMAGES_DIR);
1784
+ await ensureDir(REMOTE_IMAGES_DIR);
1785
+ await migrateLegacyMemoryFile();
1786
+ ensureMemoryFile();
1787
+ await seedConfigDefaults();
1788
+ void sweepOrphanAtomicTmpFiles();
1789
+ }
1790
+ var init_init = __esm({
1791
+ "src/core/init.ts"() {
1792
+ "use strict";
1793
+ init_fs();
1794
+ init_constants();
1795
+ init_memory_file();
1796
+ init_config_manager();
1797
+ init_tmp_sweep();
1798
+ init_logging();
1799
+ }
1800
+ });
1801
+
1802
+ // src/core/event-types.ts
1803
+ var init_event_types = __esm({
1804
+ "src/core/event-types.ts"() {
1805
+ "use strict";
1806
+ }
1807
+ });
1808
+
1809
+ // src/core/event-bus.ts
1810
+ import { randomBytes } from "crypto";
1811
+ var EventNames, EVENT_HISTORY_SIZE, eventHistory, KEY_BUS_EVENTS, EventBus, bus;
1812
+ var init_event_bus = __esm({
1813
+ "src/core/event-bus.ts"() {
1814
+ "use strict";
1815
+ init_logging();
1816
+ init_event_types();
1817
+ EventNames = {
1818
+ // Task events
1819
+ TASK_CREATED: "task:created",
1820
+ TASK_UPDATED: "task:updated",
1821
+ TASK_COMPLETED: "task:completed",
1822
+ TASK_STARRED: "task:starred",
1823
+ TASK_DELETED: "task:deleted",
1824
+ TASK_REORDERED: "task:reordered",
1825
+ TASK_UNBLOCKED: "task:unblocked",
1826
+ TASK_GROUPS_CHANGED: "task:groups-changed",
1827
+ // Calendar (external calendar events cache refreshed / event written)
1828
+ CALENDAR_UPDATED: "calendar:updated",
1829
+ // Inline subagent streaming
1830
+ AGENT_SUBAGENT_STREAM: "agent:subagent-stream",
1831
+ // Agent events (chat streaming)
1832
+ AGENT_TEXT_DELTA: "agent:text-delta",
1833
+ AGENT_TOOL_ACTIVITY: "agent:tool-activity",
1834
+ AGENT_TOOL_CALL: "agent:tool-call",
1835
+ AGENT_TOOL_RESULT: "agent:tool-result",
1836
+ AGENT_THINKING: "agent:thinking",
1837
+ AGENT_RESPONSE: "agent:response",
1838
+ AGENT_ERROR: "agent:error",
1839
+ // Session events
1840
+ SESSION_START: "session:start",
1841
+ SESSION_SEND: "session:send",
1842
+ SESSION_INTERRUPT: "session:interrupt",
1843
+ SESSION_STARTED: "session:started",
1844
+ SESSION_ENDED: "session:ended",
1845
+ SESSION_DELETED: "session:deleted",
1846
+ SESSION_CONTENT_UPDATED: "session:content-updated",
1847
+ SESSION_RESULT: "session:result",
1848
+ SESSION_ERROR: "session:error",
1849
+ // Session streaming events (from --output-format stream-json)
1850
+ SESSION_TEXT_DELTA: "session:text-delta",
1851
+ SESSION_THINKING_DELTA: "session:thinking-delta",
1852
+ SESSION_TOOL_USE: "session:tool-use",
1853
+ SESSION_TOOL_RESULT: "session:tool-result",
1854
+ SESSION_UNKNOWN_EVENT: "session:unknown-event",
1855
+ SESSION_STATUS_CHANGED: "session:status-changed",
1856
+ SESSION_MESSAGES_DELIVERED: "session:messages-delivered",
1857
+ SESSION_BATCH_COMPLETED: "session:batch-completed",
1858
+ SESSION_BATCH_FAILED: "session:batch-failed",
1859
+ SESSION_MESSAGE_QUEUED: "session:message-queued",
1860
+ SESSION_SYSTEM_EVENT: "session:system-event",
1861
+ SESSION_BACKGROUND_TASKS: "session:background-tasks",
1862
+ SESSION_USAGE_UPDATE: "session:usage-update",
1863
+ SESSION_SETTINGS_APPLIED: "session:settings-applied",
1864
+ SESSION_MODEL_CATALOG: "session:model-catalog",
1865
+ SESSION_PERMISSION_REQUEST: "session:permission-request",
1866
+ SESSION_PERMISSION_RESOLVED: "session:permission-resolved",
1867
+ // Side question ("/btw") — native Claude Code side_question round-trip results.
1868
+ // The drawer subscribes to these to update without polling.
1869
+ SESSION_SIDE_QUESTION_DONE: "session:side-question-done",
1870
+ SESSION_SIDE_QUESTION_ERROR: "session:side-question-error",
1871
+ // Team events (Claude Code Teams — parallel agents)
1872
+ SESSION_TEAM_INFO: "session:team-info",
1873
+ SESSION_TEAM_AGENT_DELTA: "session:team-agent-delta",
1874
+ // Chat history events
1875
+ CHAT_HISTORY_UPDATED: "chat:history-updated",
1876
+ CHAT_COMPACTING: "chat:compacting",
1877
+ CHAT_COMPACTED: "chat:compacted",
1878
+ // Conversation events (multi-conversation per agent)
1879
+ CONVERSATION_CREATED: "conversation:created",
1880
+ CONVERSATION_DELETED: "conversation:deleted",
1881
+ CONVERSATION_UPDATED: "conversation:updated",
1882
+ // rename / pin / active-change
1883
+ // Cron events
1884
+ CRON_JOB_ADDED: "cron:job-added",
1885
+ CRON_JOB_UPDATED: "cron:job-updated",
1886
+ CRON_JOB_REMOVED: "cron:job-removed",
1887
+ CRON_JOB_STARTED: "cron:job-started",
1888
+ CRON_JOB_FINISHED: "cron:job-finished",
1889
+ CRON_NOTIFICATION: "cron:notification",
1890
+ // Subagent events
1891
+ SUBAGENT_START: "subagent:start",
1892
+ SUBAGENT_SEND: "subagent:send",
1893
+ SUBAGENT_STARTED: "subagent:started",
1894
+ SUBAGENT_RESULT: "subagent:result",
1895
+ SUBAGENT_ERROR: "subagent:error",
1896
+ // Sync events
1897
+ SYNC_PULLED: "sync:pulled",
1898
+ SYNC_CONFLICT_RESOLVED: "sync:conflict-resolved",
1899
+ // Project registry events
1900
+ PROJECT_CREATED: "project:created",
1901
+ // Notes events
1902
+ NOTES_UPDATED: "notes:updated",
1903
+ NOTES_TREE_CHANGED: "notes:tree-changed",
1904
+ // Config events
1905
+ CONFIG_CHANGED: "config:changed",
1906
+ // Audio capture events
1907
+ AUDIO_STARTED: "audio:started",
1908
+ AUDIO_STOPPED: "audio:stopped",
1909
+ AUDIO_CHUNK_SAVED: "audio:chunk-saved",
1910
+ AUDIO_ERROR: "audio:error",
1911
+ AUDIO_TRANSCRIPTION_COMPLETE: "audio:transcription-complete",
1912
+ // System health events
1913
+ SYSTEM_HEALTH: "system:health",
1914
+ // Mobile client incidents (a freeze/crash line arrived in an uploaded iOS log)
1915
+ CLIENT_INCIDENT: "client:incident",
1916
+ // Cloud-companion setup job progress (never carries the pairing code)
1917
+ CLOUD_SETUP_UPDATE: "cloud-setup:update"
1918
+ };
1919
+ EVENT_HISTORY_SIZE = 200;
1920
+ eventHistory = [];
1921
+ KEY_BUS_EVENTS = /* @__PURE__ */ new Set([
1922
+ "session:start",
1923
+ "session:send",
1924
+ "session:started",
1925
+ "session:ended",
1926
+ "session:deleted",
1927
+ "session:content-updated",
1928
+ "session:result",
1929
+ "session:error",
1930
+ "session:status-changed",
1931
+ "subagent:start",
1932
+ "subagent:result",
1933
+ "subagent:error",
1934
+ "task:created",
1935
+ "task:updated",
1936
+ "task:completed",
1937
+ "task:deleted",
1938
+ "task:unblocked"
1939
+ ]);
1940
+ EventBus = class {
1941
+ subscribers = /* @__PURE__ */ new Map();
1942
+ /**
1943
+ * Register a named subscriber.
1944
+ * Events are delivered only when the subscriber's name is in the event's destinations
1945
+ * (or destinations includes "*"), unless `global: true` is set — then the subscriber
1946
+ * receives ALL events regardless of destinations.
1947
+ */
1948
+ subscribe(name, handler, filter) {
1949
+ if (filter && typeof filter === "object" && ("global" in filter || "interest" in filter || "filter" in filter)) {
1950
+ this.subscribers.set(name, { name, handler, filter: filter.filter, global: filter.global, interest: filter.interest });
1951
+ } else {
1952
+ this.subscribers.set(name, { name, handler, filter });
1953
+ }
1954
+ }
1955
+ /**
1956
+ * Remove a subscriber by name.
1957
+ */
1958
+ unsubscribe(name) {
1959
+ this.subscribers.delete(name);
1960
+ }
1961
+ emit(name, data, destinations, options) {
1962
+ const event = {
1963
+ name,
1964
+ data,
1965
+ destinations,
1966
+ urgency: options?.urgency ?? "normal",
1967
+ timestamp: Date.now(),
1968
+ source: options?.source ?? "unknown",
1969
+ traceId: randomBytes(4).toString("hex"),
1970
+ ...options?.reemit ? { reemit: true } : {}
1971
+ };
1972
+ eventHistory.push(event);
1973
+ if (eventHistory.length > EVENT_HISTORY_SIZE) {
1974
+ eventHistory.shift();
1975
+ }
1976
+ log.bus.debug(`emit ${name}`, { traceId: event.traceId, destinations, source: options?.source });
1977
+ if (KEY_BUS_EVENTS.has(name)) {
1978
+ log.bus.info(`emit ${name}`, { traceId: event.traceId, destinations, source: options?.source });
1979
+ }
1980
+ for (const [, subscriber] of this.subscribers) {
1981
+ if (subscriber.global && event.reemit) continue;
1982
+ if (!subscriber.global && !destinations.includes("*") && !destinations.includes(subscriber.name)) {
1983
+ continue;
1984
+ }
1985
+ if (subscriber.global && subscriber.interest && !subscriber.interest.some((p) => name.startsWith(p))) {
1986
+ continue;
1987
+ }
1988
+ if (subscriber.filter && !subscriber.filter(event)) {
1989
+ continue;
1990
+ }
1991
+ try {
1992
+ log.bus.debug("event delivered", { name, subscriber: subscriber.name, traceId: event.traceId });
1993
+ const result = subscriber.handler(event);
1994
+ if (result && typeof result.then === "function") {
1995
+ result.catch((err) => {
1996
+ log.bus.error(`subscriber "${subscriber.name}" threw on event "${name}" (async)`, {
1997
+ eventName: name,
1998
+ traceId: event.traceId,
1999
+ error: err instanceof Error ? err.message : String(err),
2000
+ stack: err instanceof Error ? err.stack : void 0
2001
+ });
2002
+ });
2003
+ }
2004
+ } catch (err) {
2005
+ log.bus.error(`subscriber "${subscriber.name}" threw on event "${name}"`, {
2006
+ eventName: name,
2007
+ traceId: event.traceId,
2008
+ error: err instanceof Error ? err.message : String(err),
2009
+ stack: err instanceof Error ? err.stack : void 0
2010
+ });
2011
+ }
2012
+ }
2013
+ }
2014
+ /**
2015
+ * Check if a subscriber is registered.
2016
+ */
2017
+ has(name) {
2018
+ return this.subscribers.has(name);
2019
+ }
2020
+ /**
2021
+ * Remove all subscribers.
2022
+ */
2023
+ clear() {
2024
+ this.subscribers.clear();
2025
+ }
2026
+ };
2027
+ bus = new EventBus();
2028
+ }
2029
+ });
2030
+
2031
+ // src/core/integration-registry.ts
2032
+ var noopLocalSync, IntegrationRegistry, registry;
2033
+ var init_integration_registry = __esm({
2034
+ "src/core/integration-registry.ts"() {
2035
+ "use strict";
2036
+ noopLocalSync = {
2037
+ createTask: async () => null,
2038
+ deleteTask: async () => {
2039
+ },
2040
+ updateTitle: async () => {
2041
+ },
2042
+ updateDescription: async () => {
2043
+ },
2044
+ updateSummary: async () => {
2045
+ },
2046
+ updateNote: async () => {
2047
+ },
2048
+ updateConversationLog: async () => {
2049
+ },
2050
+ updatePriority: async () => {
2051
+ },
2052
+ updatePhase: async () => {
2053
+ },
2054
+ updateDueDate: async () => {
2055
+ },
2056
+ updateStar: async () => {
2057
+ },
2058
+ updateProject: async () => {
2059
+ },
2060
+ updateDependencies: async () => {
2061
+ },
2062
+ associateSubtask: async () => {
2063
+ },
2064
+ disassociateSubtask: async () => {
2065
+ },
2066
+ pushTask: async () => ({ serverTimestamp: (/* @__PURE__ */ new Date()).toISOString() }),
2067
+ syncPoll: async () => {
2068
+ }
2069
+ };
2070
+ IntegrationRegistry = class {
2071
+ plugins = /* @__PURE__ */ new Map();
2072
+ /** Register a plugin. Throws if duplicate ID. */
2073
+ register(id, plugin) {
2074
+ if (this.plugins.has(id)) {
2075
+ throw new Error(`Plugin "${id}" is already registered.`);
2076
+ }
2077
+ this.plugins.set(id, plugin);
2078
+ }
2079
+ /** Get a plugin by ID. Returns undefined if not found. */
2080
+ get(id) {
2081
+ return this.plugins.get(id);
2082
+ }
2083
+ /** Get all registered plugins. */
2084
+ getAll() {
2085
+ return [...this.plugins.values()];
2086
+ }
2087
+ /** Check if a plugin is registered. */
2088
+ has(id) {
2089
+ return this.plugins.has(id);
2090
+ }
2091
+ /**
2092
+ * Find the plugin that claims ownership of a project.
2093
+ * Iterates claims by priority (highest first). First match wins.
2094
+ * Always returns something — 'local' plugin is the universal fallback (priority -1).
2095
+ *
2096
+ * Inbox (the empty project) is structurally unclaimable, so callers never ask
2097
+ * about it — it is always 'local'.
2098
+ */
2099
+ async getForProject(project) {
2100
+ const claimable = [...this.plugins.values()].filter((p) => p.claim).sort((a, b) => b.claim.priority - a.claim.priority);
2101
+ for (const plugin of claimable) {
2102
+ const claimed = await plugin.claim.fn(project);
2103
+ if (claimed) return plugin;
2104
+ }
2105
+ const local = this.plugins.get("local");
2106
+ if (local) return local;
2107
+ throw new Error("No plugin registered for project and no local fallback found.");
2108
+ }
2109
+ /** Remove all plugins (useful for testing). Re-registers the local fallback. */
2110
+ clear() {
2111
+ this.plugins.clear();
2112
+ this.ensureLocalFallback();
2113
+ }
2114
+ /** Ensure the local fallback plugin is registered. Called at init and after clear(). */
2115
+ ensureLocalFallback() {
2116
+ if (this.plugins.has("local")) return;
2117
+ this.plugins.set("local", {
2118
+ id: "local",
2119
+ name: "Local (fallback)",
2120
+ config: {},
2121
+ sync: noopLocalSync,
2122
+ claim: { fn: (() => true), priority: -1 },
2123
+ migrations: [],
2124
+ httpRoutes: []
2125
+ });
2126
+ }
2127
+ };
2128
+ registry = new IntegrationRegistry();
2129
+ registry.ensureLocalFallback();
2130
+ }
2131
+ });
2132
+
2133
+ // src/core/task-db.ts
2134
+ import Database from "better-sqlite3";
2135
+ import fs9 from "fs";
2136
+ import path9 from "path";
2137
+ import yaml2 from "js-yaml";
2138
+ function getDb() {
2139
+ if (db) return db;
2140
+ if (initAttempted) throw initError;
2141
+ initAttempted = true;
2142
+ try {
2143
+ fs9.mkdirSync(path9.dirname(TASK_DB_PATH), { recursive: true });
2144
+ const handle = new Database(TASK_DB_PATH);
2145
+ handle.pragma("journal_mode = WAL");
2146
+ handle.pragma("busy_timeout = 5000");
2147
+ handle.pragma("wal_autocheckpoint = 1000");
2148
+ handle.pragma("synchronous = NORMAL");
2149
+ handle.pragma("foreign_keys = ON");
2150
+ handle.exec(SCHEMA_SQL);
2151
+ runOneTimeMigrations(handle);
2152
+ let checkpoint = null;
2153
+ try {
2154
+ checkpoint = handle.pragma("wal_checkpoint(TRUNCATE)");
2155
+ } catch (err) {
2156
+ log.task.warn("task-db: WAL checkpoint on open failed", { err: String(err) });
2157
+ }
2158
+ db = handle;
2159
+ log.task.info("task-db opened", { path: TASK_DB_PATH });
2160
+ log.task.info("task-db: WAL checkpoint on open", { result: checkpoint });
2161
+ return db;
2162
+ } catch (err) {
2163
+ initError = err;
2164
+ log.task.error("task-db open failed", { path: TASK_DB_PATH, err: String(err) });
2165
+ throw err;
2166
+ }
2167
+ }
2168
+ function transaction(fn) {
2169
+ const handle = getDb();
2170
+ if (!handle) {
2171
+ throw new Error("task-db: transaction() called before database was successfully opened");
2172
+ }
2173
+ const tx = handle.transaction(fn);
2174
+ return tx(handle);
2175
+ }
2176
+ function rowToTask(row) {
2177
+ let task = {};
2178
+ if (row.payload != null && row.payload !== "") {
2179
+ try {
2180
+ const parsed = JSON.parse(row.payload);
2181
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
2182
+ task = { ...parsed };
2183
+ }
2184
+ } catch (err) {
2185
+ log.task.warn("rowToTask: payload JSON parse failed", { id: row.id, err: String(err) });
2186
+ }
2187
+ }
2188
+ for (const col of EXPLICIT_TASK_COLUMNS) {
2189
+ const val = row[col];
2190
+ if (val === void 0 || val === null) continue;
2191
+ if (JSON_COLUMNS.has(col)) {
2192
+ if (val === "") continue;
2193
+ try {
2194
+ task[col] = JSON.parse(val);
2195
+ } catch (err) {
2196
+ log.task.warn("rowToTask: JSON column parse failed", { id: row.id, col, err: String(err) });
2197
+ }
2198
+ } else if (col === "pinned") {
2199
+ task.pinned = val === 1 || val === true;
2200
+ } else {
2201
+ task[col] = val;
2202
+ }
2203
+ }
2204
+ if (row._synced_at != null && row._synced_at !== "") {
2205
+ task._syncedAt = row._synced_at;
2206
+ }
2207
+ if (typeof task.project !== "string") task.project = "";
2208
+ if (typeof task.session_ids === "undefined") task.session_ids = [];
2209
+ if (typeof task.description === "undefined") task.description = "";
2210
+ if (typeof task.summary === "undefined") task.summary = "";
2211
+ if (typeof task.note === "undefined") task.note = "";
2212
+ return task;
2213
+ }
2214
+ function taskToRow(task) {
2215
+ const row = {};
2216
+ for (const col of EXPLICIT_TASK_COLUMNS) {
2217
+ if (col === "_synced_at") continue;
2218
+ if (!(col in task)) continue;
2219
+ const val = task[col];
2220
+ if (val === void 0) continue;
2221
+ if (JSON_COLUMNS.has(col)) {
2222
+ row[col] = val === null ? null : JSON.stringify(val);
2223
+ } else if (col === "pinned") {
2224
+ row.pinned = val ? 1 : 0;
2225
+ } else {
2226
+ row[col] = val;
2227
+ }
2228
+ }
2229
+ if ("_syncedAt" in task) {
2230
+ row._synced_at = task._syncedAt ?? null;
2231
+ }
2232
+ const payload = {};
2233
+ let hasPayload = false;
2234
+ for (const key of Object.keys(task)) {
2235
+ if (EXPLICIT_TASK_KEYS.has(key)) continue;
2236
+ if (RETIRED_TASK_KEYS.has(key)) continue;
2237
+ const val = task[key];
2238
+ if (val === void 0) continue;
2239
+ payload[key] = val;
2240
+ hasPayload = true;
2241
+ }
2242
+ if (hasPayload) {
2243
+ row.payload = JSON.stringify(payload);
2244
+ } else if ("payload" in task) {
2245
+ row.payload = null;
2246
+ }
2247
+ return row;
2248
+ }
2249
+ function runOneTimeMigrations(handle) {
2250
+ const current = handle.pragma("user_version", { simple: true });
2251
+ if (current >= SCHEMA_VERSION) return;
2252
+ if (current < 2) {
2253
+ const staleIndexes = handle.prepare(
2254
+ "SELECT name FROM sqlite_master WHERE type = 'index' AND name LIKE 'idx_tasks_ext_%'"
2255
+ ).all();
2256
+ for (const { name } of staleIndexes) {
2257
+ handle.exec(`DROP INDEX IF EXISTS "${name}";`);
2258
+ }
2259
+ handle.exec(`DROP TABLE IF EXISTS task_projects;`);
2260
+ }
2261
+ if (current < 3) {
2262
+ const cols = handle.prepare(`PRAGMA table_info(task_groups)`).all();
2263
+ if (!cols.some((c) => c.name === "hidden")) {
2264
+ handle.exec(`ALTER TABLE task_groups ADD COLUMN hidden INTEGER NOT NULL DEFAULT 0;`);
2265
+ }
2266
+ }
2267
+ if (current < 4) {
2268
+ const cols = handle.prepare(`PRAGMA table_info(tasks)`).all();
2269
+ if (!cols.some((c) => c.name === "start_date")) {
2270
+ handle.exec(`ALTER TABLE tasks ADD COLUMN start_date TEXT;`);
2271
+ }
2272
+ }
2273
+ if (current < 5) {
2274
+ migrateToProjectOnly(handle);
2275
+ }
2276
+ if (current < 6) {
2277
+ migrateReadMarkerToUnread(handle);
2278
+ }
2279
+ handle.pragma("user_version = " + SCHEMA_VERSION);
2280
+ }
2281
+ function legacyListName(category, project) {
2282
+ if (!category || !project || category.toLowerCase() === project.toLowerCase()) {
2283
+ return category || project;
2284
+ }
2285
+ return `${category} / ${project}`;
2286
+ }
2287
+ function pickMajoritySource(weights, projectName) {
2288
+ const ranked = [...weights.entries()].filter(([src, n]) => src && src !== "local" && n > 0).sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
2289
+ if (ranked.length === 0) return "local";
2290
+ if (ranked.length === 1) return ranked[0][0];
2291
+ if (ranked[0][1] > ranked[1][1]) {
2292
+ log.task.warn("task-db: project merged categories from several providers, majority wins", {
2293
+ project: projectName,
2294
+ source: ranked[0][0],
2295
+ weights: Object.fromEntries(ranked)
2296
+ });
2297
+ return ranked[0][0];
2298
+ }
2299
+ log.task.warn("task-db: project merged categories with tied sources, using local", {
2300
+ project: projectName,
2301
+ weights: Object.fromEntries(ranked)
2302
+ });
2303
+ return "local";
2304
+ }
2305
+ function parseYamlObject(text) {
2306
+ if (!text || !text.trim()) return null;
2307
+ try {
2308
+ const parsed = yaml2.load(text);
2309
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
2310
+ return parsed;
2311
+ }
2312
+ } catch (err) {
2313
+ log.task.warn("task-db v5: metadata YAML parse failed", { err: String(err) });
2314
+ }
2315
+ return null;
2316
+ }
2317
+ function mergeFirstWins(target, src, context) {
2318
+ for (const [key, value] of Object.entries(src)) {
2319
+ if (key in target) {
2320
+ if (JSON.stringify(target[key]) !== JSON.stringify(value)) {
2321
+ log.task.warn("task-db v5: metadata conflict, keeping first value", { context, key });
2322
+ }
2323
+ continue;
2324
+ }
2325
+ target[key] = value;
2326
+ }
2327
+ }
2328
+ function promoteLegacyGroup(category, project) {
2329
+ const cat = (category ?? "").trim();
2330
+ const proj = (project ?? "").trim();
2331
+ if (proj.toLowerCase() === "quick start") return "";
2332
+ if (!proj || proj.toLowerCase() === cat.toLowerCase()) {
2333
+ if (!cat || cat.toLowerCase() === "inbox") return "";
2334
+ return cat;
2335
+ }
2336
+ return proj;
2337
+ }
2338
+ function migrateToProjectOnly(handle) {
2339
+ handle.exec(TASK_PROJECTS_DDL);
2340
+ const cols = handle.prepare(`PRAGMA table_info(tasks)`).all();
2341
+ if (!cols.some((c) => c.name === "category")) {
2342
+ return;
2343
+ }
2344
+ try {
2345
+ handle.pragma("wal_checkpoint(TRUNCATE)");
2346
+ if (!fs9.existsSync(TASK_DB_PRE_V5_BACKUP_PATH)) {
2347
+ fs9.copyFileSync(TASK_DB_PATH, TASK_DB_PRE_V5_BACKUP_PATH);
2348
+ log.task.info("task-db v5: wrote pre-migration backup", { path: TASK_DB_PRE_V5_BACKUP_PATH });
2349
+ }
2350
+ } catch (err) {
2351
+ log.task.warn("task-db v5: backup failed (continuing)", {
2352
+ path: TASK_DB_PRE_V5_BACKUP_PATH,
2353
+ err: String(err)
2354
+ });
2355
+ }
2356
+ const summary = handle.transaction(() => {
2357
+ const sentinels = handle.prepare(
2358
+ `SELECT category, project, title, description FROM tasks
2359
+ WHERE title IN ('.metadata_project', '.metadata_category')`
2360
+ ).all();
2361
+ const catMeta = /* @__PURE__ */ new Map();
2362
+ const projMeta = /* @__PURE__ */ new Map();
2363
+ const sentinelPairs = [];
2364
+ for (const row of sentinels) {
2365
+ const cat = (row.category ?? "").trim().toLowerCase();
2366
+ const settings = parseYamlObject(row.description);
2367
+ if (row.title === ".metadata_category") {
2368
+ if (!settings) continue;
2369
+ const bucket2 = catMeta.get(cat) ?? {};
2370
+ mergeFirstWins(bucket2, settings, `category:${cat}`);
2371
+ catMeta.set(cat, bucket2);
2372
+ continue;
2373
+ }
2374
+ sentinelPairs.push({ category: row.category, project: row.project });
2375
+ if (!settings) continue;
2376
+ const key = `${cat}\0${(row.project ?? "").trim().toLowerCase()}`;
2377
+ const bucket = projMeta.get(key) ?? {};
2378
+ mergeFirstWins(bucket, settings, `project:${key}`);
2379
+ projMeta.set(key, bucket);
2380
+ }
2381
+ handle.prepare(`DELETE FROM tasks WHERE title IN ('.metadata_project', '.metadata_category')`).run();
2382
+ const groups = handle.prepare(`SELECT category, project, COUNT(*) AS n FROM tasks GROUP BY category, project`).all().map((r) => ({
2383
+ category: r.category,
2384
+ project: r.project,
2385
+ taskCount: r.n,
2386
+ target: promoteLegacyGroup(r.category ?? "", r.project ?? ""),
2387
+ final: ""
2388
+ }));
2389
+ for (const pair of sentinelPairs) {
2390
+ const exists = groups.some((g) => g.category === pair.category && g.project === pair.project);
2391
+ if (exists) continue;
2392
+ groups.push({
2393
+ category: pair.category,
2394
+ project: pair.project,
2395
+ taskCount: 0,
2396
+ target: promoteLegacyGroup(pair.category ?? "", pair.project ?? ""),
2397
+ final: ""
2398
+ });
2399
+ }
2400
+ const spellings = /* @__PURE__ */ new Map();
2401
+ for (const g of groups) {
2402
+ if (!g.target) continue;
2403
+ const lower = g.target.toLowerCase();
2404
+ const bySpelling = spellings.get(lower) ?? /* @__PURE__ */ new Map();
2405
+ bySpelling.set(g.target, (bySpelling.get(g.target) ?? 0) + g.taskCount);
2406
+ spellings.set(lower, bySpelling);
2407
+ }
2408
+ const canonical = /* @__PURE__ */ new Map();
2409
+ for (const [lower, bySpelling] of spellings) {
2410
+ const best = [...bySpelling.entries()].sort(
2411
+ (a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0)
2412
+ )[0][0];
2413
+ canonical.set(lower, best);
2414
+ }
2415
+ for (const g of groups) {
2416
+ g.final = g.target ? canonical.get(g.target.toLowerCase()) ?? g.target : "";
2417
+ }
2418
+ const updateGroup = handle.prepare(
2419
+ `UPDATE tasks SET project = @project WHERE category IS @category AND project IS @old`
2420
+ );
2421
+ for (const g of groups) {
2422
+ if ((g.project ?? "") === g.final) continue;
2423
+ updateGroup.run({ project: g.final, category: g.category, old: g.project });
2424
+ }
2425
+ let legacyCatSources = /* @__PURE__ */ new Map();
2426
+ let catOrder = [];
2427
+ try {
2428
+ const rows = handle.prepare(
2429
+ `SELECT name, source FROM task_categories
2430
+ ORDER BY (order_index IS NULL), order_index, name`
2431
+ ).all();
2432
+ catOrder = rows.map((r) => r.name);
2433
+ legacyCatSources = new Map(rows.map((r) => [r.name.trim().toLowerCase(), r.source || "local"]));
2434
+ } catch {
2435
+ }
2436
+ const contributors = /* @__PURE__ */ new Map();
2437
+ for (const g of groups) {
2438
+ if (!g.final) continue;
2439
+ const key = g.final.toLowerCase();
2440
+ const list = contributors.get(key) ?? [];
2441
+ list.push(g);
2442
+ contributors.set(key, list);
2443
+ }
2444
+ const orderedKeys = [];
2445
+ const seen = /* @__PURE__ */ new Set();
2446
+ const catsInOrder = [
2447
+ ...catOrder,
2448
+ ...[...new Set(groups.map((g) => (g.category ?? "").trim()))].filter((c) => !catOrder.some((k) => k.trim().toLowerCase() === c.toLowerCase())).sort()
2449
+ ];
2450
+ for (const cat of catsInOrder) {
2451
+ const names = [
2452
+ ...new Set(
2453
+ groups.filter((g) => g.final && (g.category ?? "").trim().toLowerCase() === cat.trim().toLowerCase()).map((g) => g.final)
2454
+ )
2455
+ ].sort();
2456
+ for (const name of names) {
2457
+ const key = name.toLowerCase();
2458
+ if (seen.has(key)) continue;
2459
+ seen.add(key);
2460
+ orderedKeys.push(key);
2461
+ }
2462
+ }
2463
+ for (const key of [...contributors.keys()].sort()) {
2464
+ if (!seen.has(key)) {
2465
+ seen.add(key);
2466
+ orderedKeys.push(key);
2467
+ }
2468
+ }
2469
+ const insertProject = handle.prepare(
2470
+ `INSERT INTO task_projects (name, source, order_index, metadata)
2471
+ VALUES (@name, @source, @order_index, @metadata)
2472
+ ON CONFLICT(name) DO NOTHING`
2473
+ );
2474
+ const selectMinority = handle.prepare(
2475
+ `SELECT id, source FROM tasks WHERE project = @name COLLATE NOCASE AND source IS NOT @source`
2476
+ );
2477
+ const normalizeMinority = handle.prepare(
2478
+ `UPDATE tasks SET source = @source, ext = NULL, sync_error = NULL, updated_at = @now,
2479
+ payload = CASE WHEN payload IS NOT NULL AND json_valid(payload)
2480
+ THEN json_remove(payload, '$.external_url') ELSE payload END
2481
+ WHERE id = @id`
2482
+ );
2483
+ const nowIso = (/* @__PURE__ */ new Date()).toISOString();
2484
+ let normalizedMinorityTotal = 0;
2485
+ const perProject = [];
2486
+ orderedKeys.forEach((key, index) => {
2487
+ const groupsForProject = contributors.get(key);
2488
+ if (!groupsForProject || groupsForProject.length === 0) return;
2489
+ const name = groupsForProject[0].final;
2490
+ const taskCount = groupsForProject.reduce((sum, g) => sum + g.taskCount, 0);
2491
+ const providerWeights = /* @__PURE__ */ new Map();
2492
+ for (const g of groupsForProject) {
2493
+ const src = legacyCatSources.get((g.category ?? "").trim().toLowerCase()) ?? "local";
2494
+ if (!src || src === "local") continue;
2495
+ providerWeights.set(src, (providerWeights.get(src) ?? 0) + g.taskCount);
2496
+ }
2497
+ const source = pickMajoritySource(providerWeights, name);
2498
+ const base = {};
2499
+ const overlay = {};
2500
+ for (const g of groupsForProject) {
2501
+ const cat = (g.category ?? "").trim().toLowerCase();
2502
+ const fromCat = catMeta.get(cat);
2503
+ if (fromCat) mergeFirstWins(base, fromCat, `${name}<-category:${cat}`);
2504
+ const fromProj = projMeta.get(`${cat}\0${(g.project ?? "").trim().toLowerCase()}`);
2505
+ if (fromProj) mergeFirstWins(overlay, fromProj, `${name}<-project:${cat}/${g.project ?? ""}`);
2506
+ }
2507
+ const metadata = { ...base, ...overlay };
2508
+ const legacyCats = [
2509
+ ...new Set(groupsForProject.map((g) => (g.category ?? "").trim()).filter(Boolean))
2510
+ ].sort();
2511
+ if (legacyCats.length === 1) metadata.legacy_category = legacyCats[0];
2512
+ else if (legacyCats.length > 1) metadata.legacy_category = legacyCats;
2513
+ if (source === "ms-todo") {
2514
+ const owned = groupsForProject.filter((g) => (legacyCatSources.get((g.category ?? "").trim().toLowerCase()) ?? "local") === source).sort((a, b) => b.taskCount - a.taskCount);
2515
+ const pick = owned[0];
2516
+ if (pick) {
2517
+ const oldList = legacyListName((pick.category ?? "").trim(), (pick.project ?? "").trim());
2518
+ if (oldList && oldList !== name) metadata.remote_list = oldList;
2519
+ if (owned.length > 1) {
2520
+ log.task.warn("task-db v5: ambiguous remote list for merged project", {
2521
+ project: name,
2522
+ picked: oldList,
2523
+ candidates: owned.length
2524
+ });
2525
+ }
2526
+ }
2527
+ }
2528
+ insertProject.run({
2529
+ name,
2530
+ source,
2531
+ order_index: index,
2532
+ metadata: Object.keys(metadata).length > 0 ? JSON.stringify(metadata) : null
2533
+ });
2534
+ const minority = selectMinority.all({ name, source });
2535
+ const minorityBySource = {};
2536
+ for (const row of minority) {
2537
+ normalizeMinority.run({ id: row.id, source, now: nowIso });
2538
+ const from = row.source ?? "local";
2539
+ minorityBySource[from] = (minorityBySource[from] ?? 0) + 1;
2540
+ }
2541
+ if (minority.length > 0) {
2542
+ log.task.warn("task-db v5: normalized minority-source tasks onto the project claim", {
2543
+ project: name,
2544
+ source,
2545
+ tasks: minority.length,
2546
+ from: minorityBySource
2547
+ });
2548
+ normalizedMinorityTotal += minority.length;
2549
+ }
2550
+ perProject.push({
2551
+ project: name,
2552
+ source,
2553
+ tasks: taskCount,
2554
+ from: groupsForProject.map((g) => `${g.category ?? ""}/${g.project ?? ""}`),
2555
+ remote_list: metadata.remote_list,
2556
+ ...minority.length > 0 ? { normalized_minority: minority.length, normalized_from: minorityBySource } : {}
2557
+ });
2558
+ });
2559
+ const inboxCount = groups.filter((g) => !g.final).reduce((sum, g) => sum + g.taskCount, 0);
2560
+ const inboxProviderRows = handle.prepare(
2561
+ `SELECT id, source FROM tasks
2562
+ WHERE (project IS NULL OR project = '') AND source IS NOT NULL AND source != 'local'`
2563
+ ).all();
2564
+ const inboxProviderBySource = {};
2565
+ if (inboxProviderRows.length > 0) {
2566
+ const toLocal = handle.prepare(
2567
+ `UPDATE tasks SET source = 'local', ext = NULL, sync_error = NULL, updated_at = @now,
2568
+ payload = CASE WHEN payload IS NOT NULL AND json_valid(payload)
2569
+ THEN json_remove(payload, '$.external_url') ELSE payload END
2570
+ WHERE id = @id`
2571
+ );
2572
+ for (const row of inboxProviderRows) {
2573
+ toLocal.run({ id: row.id, now: nowIso });
2574
+ inboxProviderBySource[row.source] = (inboxProviderBySource[row.source] ?? 0) + 1;
2575
+ }
2576
+ log.task.warn("task-db v5: reset provider-sourced Inbox tasks to local", {
2577
+ tasks: inboxProviderRows.length,
2578
+ from: inboxProviderBySource
2579
+ });
2580
+ }
2581
+ handle.exec(`DROP INDEX IF EXISTS tasks_category_project;`);
2582
+ handle.exec(`ALTER TABLE tasks DROP COLUMN category;`);
2583
+ handle.exec(`DROP TABLE IF EXISTS task_categories;`);
2584
+ return {
2585
+ perProject,
2586
+ inboxCount,
2587
+ sentinels: sentinels.length,
2588
+ normalizedMinority: normalizedMinorityTotal,
2589
+ inboxProviderReset: inboxProviderRows.length,
2590
+ inboxProviderResetFrom: inboxProviderBySource
2591
+ };
2592
+ })();
2593
+ log.task.info("task-db v5: category removed \u2014 project is now the only grouping layer", {
2594
+ projects: summary.perProject.length,
2595
+ inboxTasks: summary.inboxCount,
2596
+ sentinelsAbsorbed: summary.sentinels,
2597
+ normalizedMinoritySourceTasks: summary.normalizedMinority,
2598
+ inboxProviderTasksResetToLocal: summary.inboxProviderReset,
2599
+ inboxProviderResetFrom: summary.inboxProviderResetFrom,
2600
+ perProject: summary.perProject
2601
+ });
2602
+ }
2603
+ function migrateReadMarkerToUnread(handle) {
2604
+ const summary = handle.transaction(() => {
2605
+ const carried = handle.prepare(
2606
+ `UPDATE tasks SET payload = json_set(payload, '$.unread', json('true'))
2607
+ WHERE payload IS NOT NULL AND json_valid(payload)
2608
+ AND json_extract(payload, '$.needs_attention') IN (1, 'true')
2609
+ AND json_extract(payload, '$.unread') IS NULL`
2610
+ ).run().changes;
2611
+ const cleared = handle.prepare(
2612
+ `UPDATE tasks SET payload = json_remove(payload, '$.needs_attention')
2613
+ WHERE payload IS NOT NULL AND json_valid(payload)
2614
+ AND json_extract(payload, '$.needs_attention') IS NOT NULL`
2615
+ ).run().changes;
2616
+ return { carried, cleared };
2617
+ })();
2618
+ if (summary.cleared > 0) {
2619
+ log.task.info("task-db v6: read marker renamed needs_attention \u2192 unread", summary);
2620
+ }
2621
+ }
2622
+ var TASK_DB_PATH, EXPLICIT_TASK_COLUMNS, JSON_COLUMNS, EXPLICIT_TASK_KEYS, RETIRED_TASK_KEYS, db, initAttempted, initError, TASK_COLUMNS, TASK_PROJECTS_DDL, SCHEMA_SQL, SCHEMA_VERSION, TASK_DB_PRE_V5_BACKUP_PATH;
2623
+ var init_task_db = __esm({
2624
+ "src/core/task-db.ts"() {
2625
+ "use strict";
2626
+ init_constants();
2627
+ init_logging();
2628
+ TASK_DB_PATH = path9.join(TASKS_DIR, "tasks.sqlite");
2629
+ EXPLICIT_TASK_COLUMNS = [
2630
+ "id",
2631
+ "title",
2632
+ "project",
2633
+ "status",
2634
+ "phase",
2635
+ "priority",
2636
+ "source",
2637
+ "parent_task_id",
2638
+ "due_date",
2639
+ "start_date",
2640
+ "created_at",
2641
+ "updated_at",
2642
+ "completed_at",
2643
+ "sprint",
2644
+ "focus_tier",
2645
+ "pinned",
2646
+ "ext",
2647
+ "tags",
2648
+ "depends_on",
2649
+ "session_ids",
2650
+ "note",
2651
+ "summary",
2652
+ "description",
2653
+ "conversation_log",
2654
+ "sync_error",
2655
+ "_synced_at"
2656
+ ];
2657
+ JSON_COLUMNS = /* @__PURE__ */ new Set([
2658
+ "ext",
2659
+ "tags",
2660
+ "depends_on",
2661
+ "session_ids",
2662
+ "conversation_log"
2663
+ ]);
2664
+ EXPLICIT_TASK_KEYS = /* @__PURE__ */ new Set([
2665
+ ...EXPLICIT_TASK_COLUMNS,
2666
+ // Task type uses camelCase `_syncedAt`; column is snake `_synced_at`.
2667
+ "_syncedAt"
2668
+ ]);
2669
+ RETIRED_TASK_KEYS = /* @__PURE__ */ new Set(["category", "needs_attention"]);
2670
+ db = null;
2671
+ initAttempted = false;
2672
+ initError = null;
2673
+ TASK_COLUMNS = EXPLICIT_TASK_COLUMNS;
2674
+ TASK_PROJECTS_DDL = `
2675
+ CREATE TABLE IF NOT EXISTS task_projects (
2676
+ name TEXT PRIMARY KEY COLLATE NOCASE,
2677
+ source TEXT NOT NULL DEFAULT 'local',
2678
+ order_index INTEGER,
2679
+ metadata TEXT
2680
+ );
2681
+ `;
2682
+ SCHEMA_SQL = `
2683
+ CREATE TABLE IF NOT EXISTS tasks (
2684
+ id TEXT PRIMARY KEY,
2685
+ title TEXT NOT NULL,
2686
+ project TEXT,
2687
+ status TEXT,
2688
+ phase TEXT,
2689
+ priority TEXT,
2690
+ source TEXT,
2691
+ parent_task_id TEXT,
2692
+ due_date TEXT,
2693
+ start_date TEXT,
2694
+ created_at TEXT,
2695
+ updated_at TEXT,
2696
+ completed_at TEXT,
2697
+ sprint TEXT,
2698
+ focus_tier TEXT,
2699
+ pinned INTEGER DEFAULT 0,
2700
+ ext TEXT,
2701
+ tags TEXT,
2702
+ depends_on TEXT,
2703
+ session_ids TEXT,
2704
+ note TEXT,
2705
+ summary TEXT,
2706
+ description TEXT,
2707
+ conversation_log TEXT,
2708
+ sync_error TEXT,
2709
+ _synced_at TEXT,
2710
+ payload TEXT
2711
+ );
2712
+ CREATE INDEX IF NOT EXISTS tasks_project ON tasks(project);
2713
+ CREATE INDEX IF NOT EXISTS tasks_status ON tasks(status);
2714
+ CREATE INDEX IF NOT EXISTS tasks_source ON tasks(source);
2715
+ CREATE INDEX IF NOT EXISTS tasks_updated_at ON tasks(updated_at);
2716
+ CREATE INDEX IF NOT EXISTS tasks_parent ON tasks(parent_task_id);
2717
+
2718
+ -- Plugin ext-id indexes are no longer baked into SCHEMA_SQL \u2014 each plugin
2719
+ -- declares its own ext-index spec via PluginApi.registerExtIndex, and the
2720
+ -- loader calls ensureExtIndexes() after plugins finish loading. This keeps
2721
+ -- core agnostic to which plugins exist and lets external plugins (loaded
2722
+ -- from ~/.open-walnut/plugins/) bring their own indexes without touching
2723
+ -- core code.
2724
+
2725
+ ${TASK_PROJECTS_DDL}
2726
+
2727
+ -- Virtual task-group name registry (local-only). Maps Task.group_id (stored in
2728
+ -- the tasks.payload blob) to a human-readable group label. Membership itself
2729
+ -- lives on the tasks (tasks.group_id); this table only holds the names.
2730
+ CREATE TABLE IF NOT EXISTS task_groups (
2731
+ id TEXT PRIMARY KEY,
2732
+ label TEXT NOT NULL,
2733
+ hidden INTEGER NOT NULL DEFAULT 0
2734
+ );
2735
+
2736
+ -- User-defined focus tiers (local-only). Ids are ct_* strings referenced by
2737
+ -- tasks.focus_tier; order_index preserves the user's Settings ordering.
2738
+ CREATE TABLE IF NOT EXISTS custom_tiers (
2739
+ id TEXT PRIMARY KEY,
2740
+ label TEXT NOT NULL,
2741
+ order_index INTEGER
2742
+ );
2743
+ `;
2744
+ SCHEMA_VERSION = 6;
2745
+ TASK_DB_PRE_V5_BACKUP_PATH = `${TASK_DB_PATH}.pre-v5.backup`;
2746
+ }
2747
+ });
2748
+
2749
+ // src/core/task-db-migration.ts
2750
+ import fs10 from "fs";
2751
+ import path10 from "path";
2752
+ import yaml3 from "js-yaml";
2753
+ async function runMigrationIfNeeded() {
2754
+ const db2 = getDb();
2755
+ if (!db2) {
2756
+ return { migrated: false, count: 0 };
2757
+ }
2758
+ const existing = db2.prepare("SELECT COUNT(*) AS n FROM tasks").get();
2759
+ if (existing.n > 0) {
2760
+ return { migrated: false, count: existing.n };
2761
+ }
2762
+ if (!fs10.existsSync(TASKS_FILE)) {
2763
+ return { migrated: false, count: 0 };
2764
+ }
2765
+ const store = await readJsonFile(TASKS_FILE, { version: 1, tasks: [] });
2766
+ const rawTasks = Array.isArray(store.tasks) ? store.tasks : [];
2767
+ const legacyCategories = store.categories ?? {};
2768
+ const tasks = rawTasks.filter(
2769
+ (t) => t?.title !== ".metadata_project" && t?.title !== ".metadata_category"
2770
+ );
2771
+ const sentinels = rawTasks.filter(
2772
+ (t) => t?.title === ".metadata_project" || t?.title === ".metadata_category"
2773
+ );
2774
+ const targetOf = (t) => promoteLegacyGroup(t.category ?? "", t.project ?? "");
2775
+ const spellingCounts = /* @__PURE__ */ new Map();
2776
+ for (const t of tasks) {
2777
+ const target = targetOf(t);
2778
+ if (!target) continue;
2779
+ const lower = target.toLowerCase();
2780
+ const bucket = spellingCounts.get(lower) ?? /* @__PURE__ */ new Map();
2781
+ bucket.set(target, (bucket.get(target) ?? 0) + 1);
2782
+ spellingCounts.set(lower, bucket);
2783
+ }
2784
+ const canonical = /* @__PURE__ */ new Map();
2785
+ for (const [lower, bucket] of spellingCounts) {
2786
+ canonical.set(
2787
+ lower,
2788
+ [...bucket.entries()].sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0))[0][0]
2789
+ );
2790
+ }
2791
+ const finalProjectOf = (t) => {
2792
+ const target = targetOf(t);
2793
+ return target ? canonical.get(target.toLowerCase()) ?? target : "";
2794
+ };
2795
+ const projectRows = /* @__PURE__ */ new Map();
2796
+ const categorySource = (category) => {
2797
+ const key = Object.keys(legacyCategories).find((k) => k.toLowerCase() === category.trim().toLowerCase());
2798
+ return key ? String(legacyCategories[key]?.source ?? "local") : "local";
2799
+ };
2800
+ for (const t of tasks) {
2801
+ const name = finalProjectOf(t);
2802
+ if (!name) continue;
2803
+ const row = projectRows.get(name.toLowerCase()) ?? { name, source: "local", metadata: {}, weights: /* @__PURE__ */ new Map(), ownedGroups: /* @__PURE__ */ new Map() };
2804
+ const legacyCat = (t.category ?? "").trim();
2805
+ const src = categorySource(legacyCat);
2806
+ if (src !== "local") {
2807
+ row.weights.set(src, (row.weights.get(src) ?? 0) + 1);
2808
+ const legacyProj = (t.project ?? "").trim();
2809
+ const groupKey = `${legacyCat.toLowerCase()}/${legacyProj.toLowerCase()}`;
2810
+ const group = row.ownedGroups.get(groupKey) ?? { category: legacyCat, project: legacyProj, count: 0 };
2811
+ group.count += 1;
2812
+ row.ownedGroups.set(groupKey, group);
2813
+ }
2814
+ if (legacyCat) {
2815
+ const existing2 = row.metadata.legacy_category;
2816
+ if (existing2 === void 0) row.metadata.legacy_category = legacyCat;
2817
+ else if (Array.isArray(existing2)) {
2818
+ if (!existing2.includes(legacyCat)) existing2.push(legacyCat);
2819
+ } else if (existing2 !== legacyCat) {
2820
+ row.metadata.legacy_category = [existing2, legacyCat].sort();
2821
+ }
2822
+ }
2823
+ projectRows.set(name.toLowerCase(), row);
2824
+ }
2825
+ for (const row of projectRows.values()) {
2826
+ row.source = pickMajoritySource(row.weights, row.name);
2827
+ if (row.source !== "ms-todo") continue;
2828
+ const owned = [...row.ownedGroups.values()].filter((g) => categorySource(g.category) === "ms-todo").sort((a, b) => b.count - a.count || (a.project < b.project ? -1 : 1));
2829
+ const pick = owned[0];
2830
+ if (!pick) continue;
2831
+ const oldList = legacyListName(pick.category, pick.project);
2832
+ if (oldList && oldList.toLowerCase() !== row.name.toLowerCase()) {
2833
+ row.metadata.remote_list = oldList;
2834
+ }
2835
+ if (owned.length > 1) {
2836
+ log.task.warn("task-db migration: ambiguous remote list for merged project", {
2837
+ project: row.name,
2838
+ picked: oldList,
2839
+ candidates: owned.length
2840
+ });
2841
+ }
2842
+ }
2843
+ for (const sentinel of sentinels) {
2844
+ const name = finalProjectOf(sentinel);
2845
+ if (!name) continue;
2846
+ const row = projectRows.get(name.toLowerCase());
2847
+ if (!row) continue;
2848
+ let settings;
2849
+ try {
2850
+ settings = yaml3.load(sentinel.description ?? "");
2851
+ } catch {
2852
+ settings = null;
2853
+ }
2854
+ if (!settings || typeof settings !== "object" || Array.isArray(settings)) continue;
2855
+ const overwrite = sentinel.title === ".metadata_project";
2856
+ for (const [key, value] of Object.entries(settings)) {
2857
+ if (overwrite || !(key in row.metadata)) row.metadata[key] = value;
2858
+ }
2859
+ }
2860
+ const taskInsertCols = [...TASK_COLUMNS, "payload"];
2861
+ const taskInsertSql = "INSERT INTO tasks (" + taskInsertCols.join(", ") + ") VALUES (" + taskInsertCols.map((c) => "@" + c).join(", ") + ")";
2862
+ const projectInsertSql = "INSERT INTO task_projects (name, source, order_index, metadata) VALUES (@name, @source, @order_index, @metadata) ON CONFLICT(name) DO NOTHING";
2863
+ const sourceNormalized = [];
2864
+ transaction((h) => {
2865
+ const insertTask = h.prepare(taskInsertSql);
2866
+ const insertProject = h.prepare(projectInsertSql);
2867
+ [...projectRows.values()].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0).forEach((row, index) => {
2868
+ insertProject.run({
2869
+ name: row.name,
2870
+ source: row.source,
2871
+ order_index: index,
2872
+ metadata: Object.keys(row.metadata).length > 0 ? JSON.stringify(row.metadata) : null
2873
+ });
2874
+ });
2875
+ for (const task of tasks) {
2876
+ if (!task || typeof task !== "object" || typeof task.id !== "string") {
2877
+ log.task.warn("task-db migration: skipping malformed task", {
2878
+ sample: String(task?.id ?? "<no id>")
2879
+ });
2880
+ continue;
2881
+ }
2882
+ if (task.title?.trim().startsWith(".metadata")) continue;
2883
+ const { category: _legacyCategory, ...rest } = task;
2884
+ const project = finalProjectOf(task);
2885
+ const registryRow = project ? projectRows.get(project.toLowerCase()) : void 0;
2886
+ const authoritativeSource = project ? registryRow?.source ?? task.source : "local";
2887
+ const normalized = {};
2888
+ if (task.source !== authoritativeSource) {
2889
+ normalized.source = authoritativeSource;
2890
+ normalized.ext = void 0;
2891
+ normalized.external_url = void 0;
2892
+ normalized.sync_error = void 0;
2893
+ sourceNormalized.push({ id: task.id, from: task.source, to: authoritativeSource, project });
2894
+ }
2895
+ const partial = taskToRow({ ...rest, project, ...normalized });
2896
+ if (normalized.source !== void 0) partial.ext = null;
2897
+ const bound = {};
2898
+ for (const col of taskInsertCols) {
2899
+ bound[col] = partial[col] === void 0 ? null : partial[col];
2900
+ }
2901
+ try {
2902
+ insertTask.run(bound);
2903
+ } catch (err) {
2904
+ log.task.warn("task-db migration: failed to insert task, skipping", {
2905
+ id: task.id,
2906
+ err: String(err)
2907
+ });
2908
+ }
2909
+ }
2910
+ });
2911
+ if (sourceNormalized.length > 0) {
2912
+ log.task.warn("task-db migration: normalized task sources onto the project claim", {
2913
+ tasks: sourceNormalized.length,
2914
+ sample: sourceNormalized.slice(0, 20)
2915
+ });
2916
+ }
2917
+ const after = db2.prepare("SELECT COUNT(*) AS n FROM tasks").get();
2918
+ const count = after.n;
2919
+ const backupPath = path10.join(path10.dirname(TASKS_FILE), "tasks.json.migrated-from-json.backup");
2920
+ try {
2921
+ fs10.copyFileSync(TASKS_FILE, backupPath);
2922
+ } catch (err) {
2923
+ log.task.warn("task-db migration: backup copy failed (non-fatal)", {
2924
+ path: backupPath,
2925
+ err: String(err)
2926
+ });
2927
+ }
2928
+ log.task.info("task-db: migrated tasks from tasks.json", {
2929
+ count,
2930
+ projectCount: projectRows.size
2931
+ });
2932
+ return { migrated: true, count };
2933
+ }
2934
+ var init_task_db_migration = __esm({
2935
+ "src/core/task-db-migration.ts"() {
2936
+ "use strict";
2937
+ init_constants();
2938
+ init_logging();
2939
+ init_fs();
2940
+ init_task_db();
2941
+ }
2942
+ });
2943
+
2944
+ // src/core/memory-dir-migration.ts
2945
+ import fs11 from "fs";
2946
+ import fsp6 from "fs/promises";
2947
+ import path11 from "path";
2948
+ function bodyOf(content) {
2949
+ const fm = content.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/);
2950
+ return fm ? content.slice(fm[0].length) : content;
2951
+ }
2952
+ function mergeMemoryContents(base, legacy, legacyLabel) {
2953
+ const legacyBody = bodyOf(legacy).trim();
2954
+ if (!legacyBody) return base;
2955
+ const head = base.endsWith("\n") ? base : `${base}
2956
+ `;
2957
+ return `${head}
2958
+ ## Merged from ${legacyLabel} (legacy)
2959
+
2960
+ ${legacyBody}
2961
+ `;
2962
+ }
2963
+ function mtimeMs(file) {
2964
+ try {
2965
+ return fs11.statSync(file).mtimeMs;
2966
+ } catch {
2967
+ return 0;
2968
+ }
2969
+ }
2970
+ function findCandidates(root) {
2971
+ const out = [];
2972
+ let level1;
2973
+ try {
2974
+ level1 = fs11.readdirSync(root, { withFileTypes: true });
2975
+ } catch {
2976
+ return out;
2977
+ }
2978
+ for (const cat of level1) {
2979
+ if (!cat.isDirectory()) continue;
2980
+ const catDir = path11.join(root, cat.name);
2981
+ if (fs11.existsSync(path11.join(catDir, MEMORY_FILE_NAME))) continue;
2982
+ let level2;
2983
+ try {
2984
+ level2 = fs11.readdirSync(catDir, { withFileTypes: true });
2985
+ } catch {
2986
+ continue;
2987
+ }
2988
+ for (const proj of level2) {
2989
+ if (!proj.isDirectory()) continue;
2990
+ const sourceDir = path11.join(catDir, proj.name);
2991
+ if (!fs11.existsSync(path11.join(sourceDir, MEMORY_FILE_NAME))) continue;
2992
+ out.push({ category: cat.name, project: proj.name, sourceDir });
2993
+ }
2994
+ }
2995
+ return out;
2996
+ }
2997
+ async function drainDir(from, to) {
2998
+ let entries;
2999
+ try {
3000
+ entries = await fsp6.readdir(from);
3001
+ } catch {
3002
+ return;
3003
+ }
3004
+ for (const name of entries) {
3005
+ const target = path11.join(to, name);
3006
+ if (fs11.existsSync(target)) continue;
3007
+ await fsp6.rename(path11.join(from, name), target).catch((err) => {
3008
+ log.memory.warn("memory-dir migration: entry move failed", {
3009
+ from: path11.join(from, name),
3010
+ to: target,
3011
+ error: String(err)
3012
+ });
3013
+ });
3014
+ }
3015
+ await fsp6.rmdir(from).catch(() => {
3016
+ });
3017
+ }
3018
+ async function migrateProjectMemoryDirs() {
3019
+ const result = { ran: false, moved: 0, merged: 0 };
3020
+ try {
3021
+ if (!fs11.existsSync(PROJECTS_MEMORY_DIR)) return result;
3022
+ if (fs11.existsSync(MEMORY_DIR_MIGRATION_MARKER)) return result;
3023
+ result.ran = true;
3024
+ const candidates = findCandidates(PROJECTS_MEMORY_DIR);
3025
+ const touchedCategories = new Set(candidates.map((c) => c.category));
3026
+ for (const c of candidates) {
3027
+ const targetDir = path11.join(PROJECTS_MEMORY_DIR, c.project);
3028
+ const label = `${c.category}/${c.project}`;
3029
+ try {
3030
+ if (!fs11.existsSync(targetDir)) {
3031
+ await fsp6.rename(c.sourceDir, targetDir);
3032
+ result.moved += 1;
3033
+ continue;
3034
+ }
3035
+ const sourceMem = path11.join(c.sourceDir, MEMORY_FILE_NAME);
3036
+ const targetMem = path11.join(targetDir, MEMORY_FILE_NAME);
3037
+ if (!fs11.existsSync(targetMem)) {
3038
+ await fsp6.rename(sourceMem, targetMem);
3039
+ await drainDir(c.sourceDir, targetDir);
3040
+ result.moved += 1;
3041
+ continue;
3042
+ }
3043
+ const sourceContent = await fsp6.readFile(sourceMem, "utf-8");
3044
+ const targetContent = await fsp6.readFile(targetMem, "utf-8");
3045
+ const sourceNewer = mtimeMs(sourceMem) > mtimeMs(targetMem);
3046
+ const mergedContent = sourceNewer ? mergeMemoryContents(sourceContent, targetContent, c.project) : mergeMemoryContents(targetContent, sourceContent, label);
3047
+ await fsp6.writeFile(targetMem, mergedContent, "utf-8");
3048
+ await fsp6.rm(sourceMem, { force: true });
3049
+ await drainDir(c.sourceDir, targetDir);
3050
+ result.merged += 1;
3051
+ } catch (err) {
3052
+ log.memory.warn("memory-dir migration: candidate failed (skipped)", {
3053
+ candidate: label,
3054
+ error: err instanceof Error ? err.message : String(err)
3055
+ });
3056
+ }
3057
+ }
3058
+ for (const cat of touchedCategories) {
3059
+ await fsp6.rmdir(path11.join(PROJECTS_MEMORY_DIR, cat)).catch(() => {
3060
+ });
3061
+ }
3062
+ await fsp6.writeFile(
3063
+ MEMORY_DIR_MIGRATION_MARKER,
3064
+ `migrated ${(/* @__PURE__ */ new Date()).toISOString()}
3065
+ `,
3066
+ "utf-8"
3067
+ );
3068
+ if (result.moved > 0 || result.merged > 0) {
3069
+ log.memory.info("memory-dir migration: flattened project memory to one level", {
3070
+ moved: result.moved,
3071
+ merged: result.merged
3072
+ });
3073
+ }
3074
+ } catch (err) {
3075
+ log.memory.warn("memory-dir migration failed (will retry next start)", {
3076
+ error: err instanceof Error ? err.message : String(err)
3077
+ });
3078
+ }
3079
+ return result;
3080
+ }
3081
+ var MEMORY_DIR_MIGRATION_MARKER, MEMORY_FILE_NAME;
3082
+ var init_memory_dir_migration = __esm({
3083
+ "src/core/memory-dir-migration.ts"() {
3084
+ "use strict";
3085
+ init_constants();
3086
+ init_logging();
3087
+ MEMORY_DIR_MIGRATION_MARKER = path11.join(PROJECTS_MEMORY_DIR, ".migrated-project-only");
3088
+ MEMORY_FILE_NAME = "MEMORY.md";
3089
+ }
3090
+ });
3091
+
3092
+ // src/core/ext-index-registry.ts
3093
+ function getExtIndexSpec(source) {
3094
+ return specs.get(source);
3095
+ }
3096
+ var specs;
3097
+ var init_ext_index_registry = __esm({
3098
+ "src/core/ext-index-registry.ts"() {
3099
+ "use strict";
3100
+ specs = /* @__PURE__ */ new Map();
3101
+ }
3102
+ });
3103
+
3104
+ // src/core/task-manager.ts
3105
+ import fsSync from "fs";
3106
+ function invalidateTaskStoreCache() {
3107
+ taskStoreCache = null;
3108
+ taskStoreCacheDataVersion = null;
3109
+ }
3110
+ function readDataVersion(handle) {
3111
+ try {
3112
+ return handle.pragma("data_version", { simple: true });
3113
+ } catch {
3114
+ return null;
3115
+ }
3116
+ }
3117
+ function withWriteLock2(fn) {
3118
+ const prev = writeLock2;
3119
+ let resolve;
3120
+ writeLock2 = new Promise((r) => {
3121
+ resolve = r;
3122
+ });
3123
+ return prev.then(() => withFileLock(TASKS_FILE, async () => {
3124
+ writeLockDepth += 1;
3125
+ try {
3126
+ return await fn();
3127
+ } finally {
3128
+ writeLockDepth -= 1;
3129
+ }
3130
+ })).finally(() => {
3131
+ invalidateTaskStoreCache();
3132
+ resolve();
3133
+ });
3134
+ }
3135
+ function withWriteLockIfFree(fn) {
3136
+ return writeLockDepth > 0 ? fn() : withWriteLock2(fn);
3137
+ }
3138
+ async function ensureInit() {
3139
+ if (!initialized) {
3140
+ await initDirectories();
3141
+ getDb();
3142
+ await runMigrationIfNeeded();
3143
+ await migrateProjectMemoryDirs();
3144
+ await seedProjectsFromConfig();
3145
+ initialized = true;
3146
+ }
3147
+ }
3148
+ async function seedProjectsFromConfig() {
3149
+ const config = await getConfig();
3150
+ return withWriteLockIfFree(async () => {
3151
+ const db2 = getDb();
3152
+ const desired = [];
3153
+ let added = 0;
3154
+ transaction((handle) => {
3155
+ const existingRows = handle.prepare("SELECT name FROM task_projects").all();
3156
+ const existing = new Set(existingRows.map((r) => r.name.trim().toLowerCase()));
3157
+ const seen = /* @__PURE__ */ new Set();
3158
+ const addIfNew = (rawName, source) => {
3159
+ const name = (rawName ?? "").trim();
3160
+ if (!name) return;
3161
+ try {
3162
+ assertValidProjectName(name);
3163
+ } catch (err) {
3164
+ log.task.warn("seedProjectsFromConfig: skipping invalid project name", {
3165
+ name,
3166
+ source,
3167
+ err: String(err)
3168
+ });
3169
+ return;
3170
+ }
3171
+ const key = name.toLowerCase();
3172
+ if (seen.has(key) || existing.has(key)) return;
3173
+ seen.add(key);
3174
+ desired.push({ name, source });
3175
+ };
3176
+ const plugins = config.plugins ?? {};
3177
+ for (const [pluginId, cfg] of Object.entries(plugins)) {
3178
+ const reserved = cfg.project;
3179
+ if (typeof reserved === "string" && reserved) addIfNew(reserved, pluginId);
3180
+ }
3181
+ const taskRows = handle.prepare("SELECT project, source, COUNT(*) AS n FROM tasks WHERE project IS NOT NULL AND project != '' GROUP BY project, source ORDER BY n DESC").all();
3182
+ for (const row of taskRows) {
3183
+ addIfNew(row.project, row.source ?? "local");
3184
+ }
3185
+ if (desired.length === 0) return;
3186
+ const nextOrder = handle.prepare("SELECT COALESCE(MAX(order_index), -1) + 1 AS next FROM task_projects").get().next;
3187
+ const stmt = handle.prepare(
3188
+ "INSERT INTO task_projects (name, source, order_index) VALUES (?, ?, ?) ON CONFLICT(name) DO NOTHING"
3189
+ );
3190
+ let idx = nextOrder;
3191
+ for (const { name, source } of desired) {
3192
+ stmt.run(name, source, idx);
3193
+ idx += 1;
3194
+ added += 1;
3195
+ }
3196
+ });
3197
+ void db2;
3198
+ if (added === 0) return;
3199
+ invalidateTaskStoreCache();
3200
+ log.task.info("seeded task_projects", { added });
3201
+ });
3202
+ }
3203
+ function parseProjectMetadataJson(raw, project) {
3204
+ if (!raw) return void 0;
3205
+ try {
3206
+ const parsed = JSON.parse(raw);
3207
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
3208
+ return parsed;
3209
+ }
3210
+ } catch (err) {
3211
+ log.task.warn("task_projects: metadata JSON parse failed", { project, err: String(err) });
3212
+ }
3213
+ return void 0;
3214
+ }
3215
+ async function readStore() {
3216
+ await ensureInit();
3217
+ if (STORE_CACHE_ENABLED && taskStoreCache !== null) {
3218
+ const current = readDataVersion(getDb());
3219
+ if (current !== null && current === taskStoreCacheDataVersion) {
3220
+ return cloneTaskStore(taskStoreCache);
3221
+ }
3222
+ log.task.warn("task store cache dropped: external process wrote the DB", {
3223
+ cachedDataVersion: taskStoreCacheDataVersion,
3224
+ currentDataVersion: current
3225
+ });
3226
+ invalidateTaskStoreCache();
3227
+ }
3228
+ const db2 = getDb();
3229
+ const taskRows = db2.prepare("SELECT * FROM tasks").all();
3230
+ const tasks = taskRows.map(rowToTask);
3231
+ const projectRows = db2.prepare("SELECT name, source, order_index, metadata FROM task_projects ORDER BY (order_index IS NULL), order_index ASC, name ASC").all();
3232
+ const projects = {};
3233
+ for (const row of projectRows) {
3234
+ const metadata = parseProjectMetadataJson(row.metadata, row.name);
3235
+ projects[row.name] = {
3236
+ source: row.source,
3237
+ ...row.order_index != null ? { order_index: row.order_index } : {},
3238
+ ...metadata ? { metadata } : {}
3239
+ };
3240
+ }
3241
+ const groupRows = db2.prepare("SELECT id, label, hidden FROM task_groups").all();
3242
+ const taskGroups = {};
3243
+ for (const row of groupRows) {
3244
+ taskGroups[row.id] = { label: row.label, ...row.hidden ? { hidden: true } : {} };
3245
+ }
3246
+ const tierRows = db2.prepare("SELECT id, label FROM custom_tiers ORDER BY order_index ASC").all();
3247
+ const customTiers = tierRows.map((row) => ({ id: row.id, label: row.label }));
3248
+ const store = {
3249
+ tasks,
3250
+ ...Object.keys(projects).length > 0 ? { projects } : {},
3251
+ ...Object.keys(taskGroups).length > 0 ? { task_groups: taskGroups } : {},
3252
+ ...customTiers.length > 0 ? { custom_tiers: customTiers } : {}
3253
+ };
3254
+ if (STORE_CACHE_ENABLED) {
3255
+ taskStoreCache = store;
3256
+ taskStoreCacheDataVersion = readDataVersion(db2);
3257
+ return cloneTaskStore(store);
3258
+ }
3259
+ return store;
3260
+ }
3261
+ function cloneTaskStore(store) {
3262
+ return {
3263
+ ...store,
3264
+ tasks: store.tasks.map((t) => ({ ...t })),
3265
+ // Registry rows carry a nested metadata object; clone it too so a caller
3266
+ // mutating settings can't reach into the cached snapshot.
3267
+ ...store.projects ? {
3268
+ projects: Object.fromEntries(
3269
+ Object.entries(store.projects).map(([name, rec]) => [
3270
+ name,
3271
+ { ...rec, ...rec.metadata ? { metadata: { ...rec.metadata } } : {} }
3272
+ ])
3273
+ )
3274
+ } : {},
3275
+ ...store.task_groups ? { task_groups: { ...store.task_groups } } : {},
3276
+ ...store.custom_tiers ? { custom_tiers: store.custom_tiers.map((t) => ({ ...t })) } : {}
3277
+ };
3278
+ }
3279
+ async function remoteListNameFor(project) {
3280
+ const name = (project ?? "").trim();
3281
+ if (!name) return "";
3282
+ const metadata = await getProjectMetadata(name);
3283
+ const alias = metadata?.remote_list;
3284
+ return typeof alias === "string" && alias.trim() ? alias : name;
3285
+ }
3286
+ async function getStoreProjects() {
3287
+ const store = await readStore();
3288
+ return store.projects ?? {};
3289
+ }
3290
+ async function getProjectRecord(project) {
3291
+ const name = (project ?? "").trim();
3292
+ if (!name) return null;
3293
+ const projects = await getStoreProjects();
3294
+ const key = Object.keys(projects).find((k) => k.toLowerCase() === name.toLowerCase());
3295
+ return key ? { name: key, ...projects[key] } : null;
3296
+ }
3297
+ function assertValidProjectName(name) {
3298
+ const trimmed = (name ?? "").trim();
3299
+ if (!trimmed) {
3300
+ throw new InvalidProjectNameError(
3301
+ "Project name must be a non-empty string. To clear a project, move its tasks to Inbox.",
3302
+ trimmed
3303
+ );
3304
+ }
3305
+ if (trimmed.includes("/") || trimmed.includes("\\")) {
3306
+ throw new InvalidProjectNameError(
3307
+ `Invalid project name "${name}": path separators ('/', '\\') are not allowed \u2014 a project name becomes a directory segment.`,
3308
+ trimmed
3309
+ );
3310
+ }
3311
+ if (trimmed.includes("..")) {
3312
+ throw new InvalidProjectNameError(
3313
+ `Invalid project name "${name}": '..' is not allowed \u2014 a project name becomes a directory segment.`,
3314
+ trimmed
3315
+ );
3316
+ }
3317
+ if (trimmed.includes("\0")) {
3318
+ throw new InvalidProjectNameError(
3319
+ `Invalid project name "${name}": it contains a NUL character.`,
3320
+ trimmed
3321
+ );
3322
+ }
3323
+ if (trimmed.startsWith(".")) {
3324
+ throw new InvalidProjectNameError(
3325
+ `Invalid project name "${name}": it cannot start with '.' (hidden directory / retired .metadata namespace).`,
3326
+ trimmed
3327
+ );
3328
+ }
3329
+ return trimmed;
3330
+ }
3331
+ function ensureProjectRowLocked(name, source) {
3332
+ const trimmed = (name ?? "").trim();
3333
+ if (!trimmed) return { name: "", source: "local", created: false };
3334
+ const db2 = getDb();
3335
+ const rows = db2.prepare("SELECT name, source FROM task_projects").all();
3336
+ const lower = trimmed.toLowerCase();
3337
+ const existing = rows.find((r) => r.name.trim().toLowerCase() === lower);
3338
+ if (existing) {
3339
+ return { name: existing.name, source: existing.source, created: false };
3340
+ }
3341
+ const nextOrder = db2.prepare("SELECT COALESCE(MAX(order_index), -1) + 1 AS next FROM task_projects").get().next;
3342
+ db2.prepare(
3343
+ "INSERT INTO task_projects (name, source, order_index) VALUES (?, ?, ?) ON CONFLICT(name) DO NOTHING"
3344
+ ).run(trimmed, source, nextOrder);
3345
+ log.task.info("project created", { project: trimmed, source });
3346
+ return { name: trimmed, source, created: true };
3347
+ }
3348
+ function emitProjectCreated(name, source) {
3349
+ bus.emit(
3350
+ EventNames.PROJECT_CREATED,
3351
+ { name, source },
3352
+ ["web-ui", "main-agent"],
3353
+ { source: "task-manager" }
3354
+ );
3355
+ }
3356
+ async function ensureProject(name, source = "local") {
3357
+ if (!(name ?? "").trim()) return { name: "", source: "local", created: false };
3358
+ const trimmed = assertValidProjectName(name);
3359
+ await ensureInit();
3360
+ const result = await withWriteLock2(async () => ensureProjectRowLocked(trimmed, source));
3361
+ if (result.created) emitProjectCreated(result.name, result.source);
3362
+ return result;
3363
+ }
3364
+ async function getProjectMetadata(project) {
3365
+ const record = await getProjectRecord(project);
3366
+ if (!record?.metadata || Object.keys(record.metadata).length === 0) return null;
3367
+ return record.metadata;
3368
+ }
3369
+ async function setProjectMetadata(project, settings) {
3370
+ const raw = (project ?? "").trim();
3371
+ if (!raw) throw new Error("Inbox has no project settings \u2014 pass a project name.");
3372
+ const { name } = await ensureProject(raw);
3373
+ return withWriteLock2(async () => {
3374
+ const db2 = getDb();
3375
+ const row = db2.prepare("SELECT name, metadata FROM task_projects WHERE name = ?").get(name);
3376
+ if (!row) throw new Error(`Project "${name}" not found`);
3377
+ const existing = parseProjectMetadataJson(row.metadata, row.name) ?? {};
3378
+ const merged = { ...existing, ...settings };
3379
+ db2.prepare("UPDATE task_projects SET metadata = ? WHERE name = ?").run(
3380
+ Object.keys(merged).length > 0 ? JSON.stringify(merged) : null,
3381
+ row.name
3382
+ );
3383
+ return merged;
3384
+ });
3385
+ }
3386
+ function getFindByExtIdStmt(source, jsonPath) {
3387
+ const cacheKey = `${source}|${jsonPath}`;
3388
+ const cached = findByExtIdStmts.get(cacheKey);
3389
+ if (cached) return cached;
3390
+ const db2 = getDb();
3391
+ const sourceLiteral = source.replace(/'/g, "''");
3392
+ const pathLiteral = jsonPath.replace(/'/g, "''");
3393
+ const sql = `SELECT * FROM tasks WHERE source = '${sourceLiteral}' AND json_extract(ext, '${pathLiteral}') = ? LIMIT 1`;
3394
+ const stmt = db2.prepare(sql);
3395
+ findByExtIdStmts.set(cacheKey, stmt);
3396
+ return stmt;
3397
+ }
3398
+ async function findTaskByExtId(source, extId) {
3399
+ if (!extId) return void 0;
3400
+ await ensureInit();
3401
+ const spec = getExtIndexSpec(source);
3402
+ if (!spec) return void 0;
3403
+ for (const p of spec.paths) {
3404
+ const row = getFindByExtIdStmt(source, p.json).get(extId);
3405
+ if (row) return rowToTask(row);
3406
+ }
3407
+ return void 0;
3408
+ }
3409
+ var initialized, STORE_CACHE_ENABLED, taskStoreCache, taskStoreCacheDataVersion, writeLock2, writeLockDepth, VALID_PRIORITIES_SET, InvalidProjectNameError, findByExtIdStmts;
3410
+ var init_task_manager = __esm({
3411
+ "src/core/task-manager.ts"() {
3412
+ "use strict";
3413
+ init_constants();
3414
+ init_file_lock();
3415
+ init_logging();
3416
+ init_format();
3417
+ init_init();
3418
+ init_config_manager();
3419
+ init_event_bus();
3420
+ init_types();
3421
+ init_phase();
3422
+ init_integration_registry();
3423
+ init_task_db();
3424
+ init_task_db_migration();
3425
+ init_memory_dir_migration();
3426
+ init_ext_index_registry();
3427
+ initialized = false;
3428
+ STORE_CACHE_ENABLED = process.env.WALNUT_STORE_CACHE !== "0";
3429
+ taskStoreCache = null;
3430
+ taskStoreCacheDataVersion = null;
3431
+ writeLock2 = Promise.resolve();
3432
+ writeLockDepth = 0;
3433
+ VALID_PRIORITIES_SET = new Set(VALID_PRIORITIES);
3434
+ InvalidProjectNameError = class extends Error {
3435
+ project;
3436
+ constructor(message, project) {
3437
+ super(message);
3438
+ this.name = "InvalidProjectNameError";
3439
+ this.project = project;
3440
+ }
3441
+ };
3442
+ findByExtIdStmts = /* @__PURE__ */ new Map();
3443
+ }
3444
+ });
3445
+
3446
+ // src/integrations/microsoft-todo.ts
3447
+ var microsoft_todo_exports = {};
3448
+ __export(microsoft_todo_exports, {
3449
+ authenticate: () => authenticate,
3450
+ autoPushTask: () => autoPushTask,
3451
+ clearListIdCache: () => clearListIdCache,
3452
+ createList: () => createList,
3453
+ deleteChecklistItem: () => deleteChecklistItem,
3454
+ deleteList: () => deleteList,
3455
+ deleteListForProject: () => deleteListForProject,
3456
+ deleteMsTodoTask: () => deleteMsTodoTask,
3457
+ deltaPull: () => deltaPull,
3458
+ fetchChecklistItems: () => fetchChecklistItems,
3459
+ fullPullAllTasks: () => fullPullAllTasks,
3460
+ getAccessToken: () => getAccessToken,
3461
+ getMsTodoSyncStatus: () => getMsTodoSyncStatus,
3462
+ getTaskLists: () => getTaskLists,
3463
+ graphRequest: () => graphRequest,
3464
+ mapToLocal: () => mapToLocal,
3465
+ mapToRemote: () => mapToRemote,
3466
+ parseMsTodoBody: () => parseMsTodoBody,
3467
+ pullTasks: () => pullTasks,
3468
+ pushChecklistItem: () => pushChecklistItem,
3469
+ pushTask: () => pushTask,
3470
+ reconcilePulledTasks: () => reconcilePulledTasks,
3471
+ registerDeletedMsIds: () => registerDeletedMsIds,
3472
+ renameList: () => renameList,
3473
+ renameListByName: () => renameListByName,
3474
+ resolveListIdForTask: () => resolveListIdForTask,
3475
+ syncTasks: () => syncTasks
3476
+ });
3477
+ import * as msal from "@azure/msal-node";
3478
+ import https from "https";
3479
+ import path12 from "path";
3480
+ function getMsTodoConfig(config) {
3481
+ return config.plugins?.["ms-todo"];
3482
+ }
3483
+ function msExt(task) {
3484
+ return task.ext?.["ms-todo"];
3485
+ }
3486
+ function getMsTodoId(task) {
3487
+ return msExt(task)?.id;
3488
+ }
3489
+ function getMsTodoList(task) {
3490
+ return msExt(task)?.list_id;
3491
+ }
3492
+ async function createMsalClient() {
3493
+ const config = await getConfig();
3494
+ const clientId = getMsTodoConfig(config)?.client_id;
3495
+ if (!clientId) {
3496
+ throw new Error(
3497
+ "Microsoft To-Do client_id not configured. Add ms_todo.client_id to ~/.open-walnut/config.yaml"
3498
+ );
3499
+ }
3500
+ const msalConfig = {
3501
+ auth: {
3502
+ clientId,
3503
+ authority: "https://login.microsoftonline.com/consumers"
3504
+ }
3505
+ };
3506
+ const app = new msal.PublicClientApplication(msalConfig);
3507
+ const cached = await readJsonFile(TOKENS_FILE, null);
3508
+ if (cached?.msalCache) {
3509
+ app.getTokenCache().deserialize(cached.msalCache);
3510
+ }
3511
+ return app;
3512
+ }
3513
+ async function saveTokenCache(app, accessToken, expiresOn) {
3514
+ const cache = {
3515
+ accessToken,
3516
+ expiresAt: (expiresOn ?? new Date(Date.now() + 3600 * 1e3)).toISOString(),
3517
+ msalCache: app.getTokenCache().serialize()
3518
+ };
3519
+ await writeJsonFile(TOKENS_FILE, cache);
3520
+ }
3521
+ async function authenticate(onDeviceCode) {
3522
+ const app = await createMsalClient();
3523
+ const result = await app.acquireTokenByDeviceCode({
3524
+ scopes: SCOPES,
3525
+ deviceCodeCallback: (response) => {
3526
+ onDeviceCode({
3527
+ userCode: response.userCode,
3528
+ verificationUri: response.verificationUri,
3529
+ message: response.message
3530
+ });
3531
+ }
3532
+ });
3533
+ if (!result) {
3534
+ throw new Error("Authentication failed: no token received");
3535
+ }
3536
+ await saveTokenCache(app, result.accessToken, result.expiresOn);
3537
+ const lists = await fetchTaskLists(result.accessToken);
3538
+ const account = result.account?.username ?? "unknown";
3539
+ return { account, lists };
3540
+ }
3541
+ async function getAccessToken() {
3542
+ const app = await createMsalClient();
3543
+ const accounts = await app.getTokenCache().getAllAccounts();
3544
+ if (accounts.length > 0) {
3545
+ try {
3546
+ const result = await app.acquireTokenSilent({
3547
+ scopes: SCOPES,
3548
+ account: accounts[0]
3549
+ });
3550
+ if (result) {
3551
+ await saveTokenCache(app, result.accessToken, result.expiresOn);
3552
+ return result.accessToken;
3553
+ }
3554
+ } catch {
3555
+ }
3556
+ }
3557
+ const cached = await readJsonFile(TOKENS_FILE, null);
3558
+ if (cached && new Date(cached.expiresAt) > /* @__PURE__ */ new Date()) {
3559
+ return cached.accessToken;
3560
+ }
3561
+ throw new Error(
3562
+ 'Not authenticated with Microsoft To-Do. Run "open-walnut auth" to sign in.'
3563
+ );
3564
+ }
3565
+ function graphRequest(token, method, urlPath, body) {
3566
+ return new Promise((resolve, reject) => {
3567
+ const url = new URL(urlPath.startsWith("http") ? urlPath : `${GRAPH_BASE}${urlPath}`);
3568
+ const postData = body ? JSON.stringify(body) : void 0;
3569
+ const options = {
3570
+ hostname: url.hostname,
3571
+ path: url.pathname + url.search,
3572
+ method,
3573
+ headers: {
3574
+ Authorization: `Bearer ${token}`,
3575
+ "Content-Type": "application/json",
3576
+ ...postData ? { "Content-Length": Buffer.byteLength(postData) } : {}
3577
+ }
3578
+ };
3579
+ const req = https.request(options, (res) => {
3580
+ let data = "";
3581
+ res.on("data", (chunk) => {
3582
+ data += chunk.toString();
3583
+ });
3584
+ res.on("end", () => {
3585
+ if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
3586
+ if (!data) {
3587
+ resolve({});
3588
+ return;
3589
+ }
3590
+ try {
3591
+ resolve(JSON.parse(data));
3592
+ } catch (parseErr) {
3593
+ reject(new Error(`Graph API ${method} ${urlPath}: invalid JSON response`));
3594
+ }
3595
+ } else {
3596
+ reject(new Error(`Graph API ${method} ${urlPath} returned ${res.statusCode}: ${data}`));
3597
+ }
3598
+ });
3599
+ });
3600
+ req.setTimeout(3e4, () => {
3601
+ req.destroy(new Error(`Graph API ${method} ${urlPath} timed out after 30s`));
3602
+ });
3603
+ req.on("error", reject);
3604
+ if (postData) req.write(postData);
3605
+ req.end();
3606
+ });
3607
+ }
3608
+ async function fetchTaskLists(token) {
3609
+ const response = await graphRequest(
3610
+ token,
3611
+ "GET",
3612
+ "/me/todo/lists"
3613
+ );
3614
+ return response.value;
3615
+ }
3616
+ async function fetchChecklistItems(token, listId, taskId) {
3617
+ const response = await graphRequest(
3618
+ token,
3619
+ "GET",
3620
+ `/me/todo/lists/${listId}/tasks/${taskId}/checklistItems`
3621
+ );
3622
+ return response.value;
3623
+ }
3624
+ async function pushChecklistItem(token, listId, taskId, item) {
3625
+ if (item.id) {
3626
+ await graphRequest(
3627
+ token,
3628
+ "PATCH",
3629
+ `/me/todo/lists/${listId}/tasks/${taskId}/checklistItems/${item.id}`,
3630
+ { displayName: item.displayName, isChecked: item.isChecked }
3631
+ );
3632
+ return item.id;
3633
+ } else {
3634
+ const created = await graphRequest(
3635
+ token,
3636
+ "POST",
3637
+ `/me/todo/lists/${listId}/tasks/${taskId}/checklistItems`,
3638
+ { displayName: item.displayName, isChecked: item.isChecked }
3639
+ );
3640
+ return created.id;
3641
+ }
3642
+ }
3643
+ async function deleteChecklistItem(token, listId, taskId, itemId) {
3644
+ await graphRequest(
3645
+ token,
3646
+ "DELETE",
3647
+ `/me/todo/lists/${listId}/tasks/${taskId}/checklistItems/${itemId}`
3648
+ );
3649
+ }
3650
+ async function getTaskLists() {
3651
+ const token = await getAccessToken();
3652
+ return fetchTaskLists(token);
3653
+ }
3654
+ async function createList(displayName) {
3655
+ const token = await getAccessToken();
3656
+ return graphRequest(token, "POST", "/me/todo/lists", { displayName });
3657
+ }
3658
+ async function renameList(listId, newName) {
3659
+ const token = await getAccessToken();
3660
+ const result = await graphRequest(token, "PATCH", `/me/todo/lists/${listId}`, { displayName: newName });
3661
+ clearListIdCache();
3662
+ return result;
3663
+ }
3664
+ async function deleteList(listId) {
3665
+ const token = await getAccessToken();
3666
+ await graphRequest(token, "DELETE", `/me/todo/lists/${listId}`);
3667
+ clearListIdCache();
3668
+ }
3669
+ async function deleteMsTodoTask(listId, taskId) {
3670
+ const token = await getAccessToken();
3671
+ await graphRequest(token, "DELETE", `/me/todo/lists/${listId}/tasks/${taskId}`);
3672
+ }
3673
+ async function renameListByName(oldName, newName) {
3674
+ const token = await getAccessToken();
3675
+ const lists = await fetchTaskLists(token);
3676
+ const match = lists.find(
3677
+ (l) => l.displayName.toLowerCase() === oldName.toLowerCase()
3678
+ );
3679
+ if (!match) {
3680
+ throw new Error(`No remote list found with name "${oldName}"`);
3681
+ }
3682
+ return renameList(match.id, newName);
3683
+ }
3684
+ async function deleteListForProject(args) {
3685
+ const listName = (args.remoteList ?? args.project).trim();
3686
+ for (const task of args.tasks) {
3687
+ await registerDeletedMsIds(task);
3688
+ }
3689
+ const token = await getAccessToken();
3690
+ const lists = await fetchTaskLists(token);
3691
+ const match = lists.find((l) => l.displayName.toLowerCase() === listName.toLowerCase());
3692
+ if (match) {
3693
+ await deleteList(match.id);
3694
+ const deltaState = await readJsonFile(DELTA_FILE, {
3695
+ deltaLinks: {},
3696
+ listNames: {},
3697
+ lastSync: ""
3698
+ });
3699
+ if (deltaState.deltaLinks[match.id] !== void 0 || deltaState.listNames[match.id] !== void 0) {
3700
+ delete deltaState.deltaLinks[match.id];
3701
+ delete deltaState.listNames[match.id];
3702
+ await writeJsonFile(DELTA_FILE, deltaState);
3703
+ }
3704
+ log.web.info("ms-todo: deleted remote list for project", {
3705
+ project: args.project,
3706
+ list: listName,
3707
+ listId: match.id,
3708
+ tombstoned: args.tasks.length
3709
+ });
3710
+ } else {
3711
+ log.web.info("ms-todo: remote list already absent on cascade delete", {
3712
+ project: args.project,
3713
+ list: listName
3714
+ });
3715
+ }
3716
+ }
3717
+ function clearListIdCache() {
3718
+ listIdCache.clear();
3719
+ }
3720
+ async function resolveListIdForTask(task) {
3721
+ const listName = await remoteListNameFor(task.project || "");
3722
+ return resolveListId(listName);
3723
+ }
3724
+ async function resolveListId(listName) {
3725
+ const config = await getConfig();
3726
+ const mapping = getMsTodoConfig(config)?.list_mapping ?? {};
3727
+ if (mapping[listName]) {
3728
+ return mapping[listName];
3729
+ }
3730
+ const cacheKey = listName.toLowerCase();
3731
+ const cached = listIdCache.get(cacheKey);
3732
+ if (cached) return cached;
3733
+ const inflight = listIdInflight.get(cacheKey);
3734
+ if (inflight) return inflight;
3735
+ const promise = resolveListIdImpl(listName, cacheKey);
3736
+ listIdInflight.set(cacheKey, promise);
3737
+ try {
3738
+ return await promise;
3739
+ } finally {
3740
+ listIdInflight.delete(cacheKey);
3741
+ }
3742
+ }
3743
+ async function resolveListIdImpl(listName, cacheKey) {
3744
+ const token = await getAccessToken();
3745
+ const lists = await fetchTaskLists(token);
3746
+ const match = lists.find(
3747
+ (l) => l.displayName.toLowerCase() === cacheKey
3748
+ );
3749
+ if (match) {
3750
+ listIdCache.set(cacheKey, match.id);
3751
+ return match.id;
3752
+ }
3753
+ if (listName) {
3754
+ const created = await createList(listName);
3755
+ listIdCache.set(cacheKey, created.id);
3756
+ return created.id;
3757
+ }
3758
+ const defaultList = lists.find(
3759
+ (l) => l.displayName === "Tasks" || l.displayName === "Aufgaben"
3760
+ );
3761
+ if (defaultList) {
3762
+ listIdCache.set(cacheKey, defaultList.id);
3763
+ return defaultList.id;
3764
+ }
3765
+ if (lists.length > 0) {
3766
+ listIdCache.set(cacheKey, lists[0].id);
3767
+ return lists[0].id;
3768
+ }
3769
+ throw new Error("No task lists found in Microsoft To-Do");
3770
+ }
3771
+ function composeMsTodoBody(description, summary, note, phase, parentTaskId, conversationLog, unread, dependsOn) {
3772
+ const headers = [];
3773
+ if (phase) headers.push(`Phase: ${phase}`);
3774
+ if (parentTaskId) headers.push(`Parent: ${parentTaskId.slice(0, 8)}`);
3775
+ if (unread !== void 0) headers.push(`Attention: ${unread}`);
3776
+ if (dependsOn?.length) headers.push(`DependsOn: ${dependsOn.map((id) => id.slice(0, 8)).join(",")}`);
3777
+ const prefix = headers.length > 0 ? headers.join("\n") + "\n\n" : "";
3778
+ const sections = [];
3779
+ if (description) sections.push(description);
3780
+ const sub = [];
3781
+ if (summary) sub.push(`## Summary
3782
+ ${summary}`);
3783
+ if (note) sub.push(`## Notes
3784
+ ${note}`);
3785
+ if (conversationLog) sub.push(`## Conversation Log
3786
+ ${conversationLog}`);
3787
+ if (sub.length > 0) {
3788
+ sections.push(sub.join("\n\n"));
3789
+ }
3790
+ if (sections.length === 0) return prefix.trimEnd();
3791
+ return prefix + sections.join("\n\n---\n\n");
3792
+ }
3793
+ function parseMsTodoBody(body) {
3794
+ if (!body || !body.trim()) return { description: "", summary: "", note: "" };
3795
+ let phase;
3796
+ let parentTaskId;
3797
+ let unread;
3798
+ let dependsOn;
3799
+ let bodyToParse = body;
3800
+ const headerPattern = /^(Phase|Parent|Attention|DependsOn):\s*(\S+)\s*(?:\n|$)/;
3801
+ let match;
3802
+ while (match = bodyToParse.match(headerPattern)) {
3803
+ const [fullMatch, key, value] = match;
3804
+ if (key === "Phase" && VALID_PHASES.has(value)) {
3805
+ phase = value;
3806
+ } else if (key === "Parent") {
3807
+ parentTaskId = value;
3808
+ } else if (key === "Attention") {
3809
+ unread = value === "true";
3810
+ } else if (key === "DependsOn") {
3811
+ dependsOn = value.split(",").filter(Boolean);
3812
+ }
3813
+ bodyToParse = bodyToParse.slice(fullMatch.length);
3814
+ }
3815
+ bodyToParse = bodyToParse.replace(/^\n/, "");
3816
+ const hrParts = bodyToParse.split(/\n\n---\n\n/);
3817
+ if (hrParts.length >= 2) {
3818
+ const description = hrParts[0].trim();
3819
+ const rest = hrParts.slice(1).join("\n\n---\n\n");
3820
+ const { summary, note, conversation_log } = parseSections(rest);
3821
+ return { description, summary, note, conversation_log, phase, parent_task_id: parentTaskId, ...unread !== void 0 ? { unread } : {}, ...dependsOn ? { depends_on: dependsOn } : {} };
3822
+ }
3823
+ const hasSections = /^## (Summary|Notes|Conversation Log)\b/m.test(bodyToParse);
3824
+ if (hasSections) {
3825
+ const { summary, note, conversation_log } = parseSections(bodyToParse);
3826
+ return { description: "", summary, note, conversation_log, phase, parent_task_id: parentTaskId, ...unread !== void 0 ? { unread } : {}, ...dependsOn ? { depends_on: dependsOn } : {} };
3827
+ }
3828
+ return { description: "", summary: "", note: bodyToParse.trim(), phase, parent_task_id: parentTaskId, ...unread !== void 0 ? { unread } : {}, ...dependsOn ? { depends_on: dependsOn } : {} };
3829
+ }
3830
+ function parseSections(text) {
3831
+ let summary = "";
3832
+ let note = "";
3833
+ let conversationLog = "";
3834
+ const summaryMatch = text.match(/## Summary\n([\s\S]*?)(?=\n## (?:Notes|Conversation Log)\b|$)/);
3835
+ if (summaryMatch) summary = summaryMatch[1].trim();
3836
+ const noteMatch = text.match(/## Notes\n([\s\S]*?)(?=\n## Conversation Log\b|$)/);
3837
+ if (noteMatch) note = noteMatch[1].trim();
3838
+ const logMatch = text.match(/## Conversation Log\n([\s\S]*?)$/);
3839
+ if (logMatch) conversationLog = logMatch[1].trim();
3840
+ if (!summary && !note && !conversationLog && text.trim()) {
3841
+ note = text.trim();
3842
+ }
3843
+ return { summary, note, ...conversationLog ? { conversation_log: conversationLog } : {} };
3844
+ }
3845
+ function mapToRemote(task) {
3846
+ const msTask = {
3847
+ title: task.title,
3848
+ status: phaseToMsStatus(task.phase) ?? "notStarted",
3849
+ importance: PRIORITY_TO_IMPORTANCE[task.priority] ?? "normal"
3850
+ };
3851
+ const bodyContent = composeMsTodoBody(task.description, task.summary, task.note, task.phase, task.parent_task_id, task.conversation_log, task.unread, task.depends_on);
3852
+ if (bodyContent) {
3853
+ msTask.body = {
3854
+ content: bodyContent,
3855
+ contentType: "text"
3856
+ };
3857
+ }
3858
+ if (task.due_date) {
3859
+ const datePart = task.due_date.split("T")[0];
3860
+ msTask.dueDateTime = {
3861
+ dateTime: datePart + "T00:00:00.0000000",
3862
+ timeZone: "UTC"
3863
+ };
3864
+ }
3865
+ if (task.start_date) {
3866
+ const datePart = task.start_date.split("T")[0];
3867
+ msTask.startDateTime = {
3868
+ dateTime: datePart + "T00:00:00.0000000",
3869
+ timeZone: "UTC"
3870
+ };
3871
+ }
3872
+ if (task.phase === "COMPLETE" && task.completed_at) {
3873
+ msTask.completedDateTime = {
3874
+ dateTime: task.completed_at,
3875
+ timeZone: "UTC"
3876
+ };
3877
+ }
3878
+ return msTask;
3879
+ }
3880
+ function mapToLocal(msTask, listDisplayName) {
3881
+ const project = routePulledListToProject(listDisplayName);
3882
+ const parsed = msTask.body?.content ? parseMsTodoBody(msTask.body.content) : void 0;
3883
+ let phase;
3884
+ if (msTask.status === "completed") {
3885
+ phase = "COMPLETE";
3886
+ } else if (msTask.status === "notStarted" && parsed?.phase && parsed.phase !== "TODO") {
3887
+ phase = "TODO";
3888
+ } else {
3889
+ phase = parsed?.phase ?? phaseFromMsStatus(msTask.status);
3890
+ }
3891
+ const local = {
3892
+ title: msTask.title,
3893
+ status: deriveStatusFromPhase(phase),
3894
+ phase,
3895
+ priority: IMPORTANCE_TO_PRIORITY[msTask.importance] ?? "none",
3896
+ ext: { "ms-todo": { id: msTask.id } },
3897
+ project
3898
+ };
3899
+ if (parsed) {
3900
+ local.description = parsed.description;
3901
+ local.summary = parsed.summary;
3902
+ local.note = parsed.note;
3903
+ if (parsed.conversation_log) local.conversation_log = parsed.conversation_log;
3904
+ if (parsed.parent_task_id) local.parent_task_id = parsed.parent_task_id;
3905
+ if (parsed.unread !== void 0) local.unread = parsed.unread;
3906
+ if (parsed.depends_on) local.depends_on = parsed.depends_on;
3907
+ }
3908
+ if (msTask.dueDateTime?.dateTime) {
3909
+ local.due_date = msTask.dueDateTime.dateTime.split("T")[0];
3910
+ }
3911
+ if (msTask.startDateTime?.dateTime) {
3912
+ local.start_date = msTask.startDateTime.dateTime.split("T")[0];
3913
+ }
3914
+ if (msTask.completedDateTime?.dateTime) {
3915
+ local.completed_at = new Date(msTask.completedDateTime.dateTime).toISOString();
3916
+ }
3917
+ return local;
3918
+ }
3919
+ function isStoreUnavailableError(err) {
3920
+ const msg = err instanceof Error ? err.message : String(err);
3921
+ return /task-db is not open/i.test(msg) || /before database was successfully opened/i.test(msg) || /bindings file/i.test(msg) || // better-sqlite3 native module missing
3922
+ /SQLITE_CANTOPEN/i.test(msg) || /reading 'prepare'/.test(msg);
3923
+ }
3924
+ async function ensureProjectForList(listDisplayName) {
3925
+ const parsed = routePulledListToProject(listDisplayName);
3926
+ if (!parsed) {
3927
+ log.web.debug("ms-todo: list name maps to Inbox / no project, skipping", { list: listDisplayName });
3928
+ return null;
3929
+ }
3930
+ try {
3931
+ const { name, source } = await ensureProject(parsed, "ms-todo");
3932
+ if (source !== "ms-todo") {
3933
+ log.web.debug("ms-todo: project claimed by another source, skipping list", {
3934
+ project: name,
3935
+ source,
3936
+ list: listDisplayName
3937
+ });
3938
+ return null;
3939
+ }
3940
+ if (listDisplayName.toLowerCase() === name.toLowerCase()) return name;
3941
+ const existing = (await getProjectRecord(name))?.metadata?.remote_list;
3942
+ if (typeof existing === "string" && existing.trim()) {
3943
+ if (existing !== listDisplayName) {
3944
+ log.web.debug("ms-todo: project already aliased to another remote list", {
3945
+ project: name,
3946
+ alias: existing,
3947
+ list: listDisplayName
3948
+ });
3949
+ }
3950
+ return name;
3951
+ }
3952
+ await setProjectMetadata(name, { remote_list: listDisplayName });
3953
+ return name;
3954
+ } catch (err) {
3955
+ if (isStoreUnavailableError(err)) {
3956
+ log.web.debug("ms-todo: project registry update skipped (no task store)", {
3957
+ list: listDisplayName,
3958
+ error: err instanceof Error ? err.message : String(err)
3959
+ });
3960
+ return parsed;
3961
+ }
3962
+ log.web.error("ms-todo: project registry update failed, skipping list", {
3963
+ list: listDisplayName,
3964
+ error: err instanceof Error ? err.message : String(err)
3965
+ });
3966
+ return null;
3967
+ }
3968
+ }
3969
+ async function syncProjectAliasAfterRename(listDisplayName) {
3970
+ const parsed = routePulledListToProject(listDisplayName);
3971
+ if (!parsed) return null;
3972
+ try {
3973
+ const { name, source } = await ensureProject(parsed, "ms-todo");
3974
+ if (source !== "ms-todo") return null;
3975
+ const current = (await getProjectRecord(name))?.metadata?.remote_list;
3976
+ if (typeof current !== "string" || current.toLowerCase() !== listDisplayName.toLowerCase()) {
3977
+ await setProjectMetadata(name, { remote_list: listDisplayName });
3978
+ }
3979
+ return name;
3980
+ } catch (err) {
3981
+ if (isStoreUnavailableError(err)) {
3982
+ log.web.debug("ms-todo: project alias update skipped (no task store)", {
3983
+ list: listDisplayName,
3984
+ error: err instanceof Error ? err.message : String(err)
3985
+ });
3986
+ return parsed;
3987
+ }
3988
+ log.web.error("ms-todo: project alias update failed, skipping list", {
3989
+ list: listDisplayName,
3990
+ error: err instanceof Error ? err.message : String(err)
3991
+ });
3992
+ return null;
3993
+ }
3994
+ }
3995
+ async function pushTask(task) {
3996
+ const taskProject = task.project || "";
3997
+ if (!taskProject) {
3998
+ throw new Error(
3999
+ `pushTask: refusing to push task "${task.title}" to MS Todo \u2014 it has no project (Inbox tasks are local-only)`
4000
+ );
4001
+ }
4002
+ try {
4003
+ const record = await getProjectRecord(taskProject);
4004
+ if (record && record.source !== "ms-todo") {
4005
+ throw new Error(
4006
+ `pushTask: refusing to push task "${task.title}" to MS Todo \u2014 project "${taskProject}" is registered as ${record.source}`
4007
+ );
4008
+ }
4009
+ } catch (err) {
4010
+ if (err instanceof Error && err.message.startsWith("pushTask: refusing")) throw err;
4011
+ if (!isStoreUnavailableError(err)) {
4012
+ const message = err instanceof Error ? err.message : String(err);
4013
+ log.web.error("ms-todo: project registry read failed, refusing push", {
4014
+ taskId: task.id,
4015
+ project: taskProject,
4016
+ error: message
4017
+ });
4018
+ throw new Error(
4019
+ `pushTask: refusing to push task "${task.title}" to MS Todo \u2014 project registry unreadable (${message})`
4020
+ );
4021
+ }
4022
+ }
4023
+ const token = await getAccessToken();
4024
+ const listId = await resolveListIdForTask(task);
4025
+ const msBody = mapToRemote(task);
4026
+ let msTaskId;
4027
+ let serverTimestamp = (/* @__PURE__ */ new Date()).toISOString();
4028
+ let actualListId = listId;
4029
+ const existingMsTodoId = getMsTodoId(task);
4030
+ const existingMsTodoList = getMsTodoList(task);
4031
+ if (existingMsTodoId) {
4032
+ const oldListId = existingMsTodoList;
4033
+ if (oldListId && oldListId !== listId) {
4034
+ try {
4035
+ await graphRequest(
4036
+ token,
4037
+ "DELETE",
4038
+ `/me/todo/lists/${oldListId}/tasks/${existingMsTodoId}`
4039
+ );
4040
+ } catch (err) {
4041
+ log.web.warn("MS To-Do: failed to delete task from old list during migration", {
4042
+ taskId: task.id,
4043
+ oldListId,
4044
+ msTaskId: existingMsTodoId,
4045
+ error: err instanceof Error ? err.message : String(err)
4046
+ });
4047
+ }
4048
+ log.web.info("ms-todo POST create", {
4049
+ taskId: task.id,
4050
+ title: task.title,
4051
+ project: task.project,
4052
+ oldListId,
4053
+ newListId: listId,
4054
+ oldMsTodoId: existingMsTodoId,
4055
+ reason: "list_migration"
4056
+ });
4057
+ const created = await graphRequest(
4058
+ token,
4059
+ "POST",
4060
+ `/me/todo/lists/${listId}/tasks`,
4061
+ msBody
4062
+ );
4063
+ msTaskId = created.id;
4064
+ serverTimestamp = created.lastModifiedDateTime;
4065
+ log.web.info("ms-todo ext.id assigned", {
4066
+ taskId: task.id,
4067
+ oldId: existingMsTodoId,
4068
+ newId: msTaskId,
4069
+ reason: "list_migration"
4070
+ });
4071
+ const prevIds = (msExt(task)?.previous_ids ?? []).slice();
4072
+ if (!prevIds.includes(existingMsTodoId)) prevIds.push(existingMsTodoId);
4073
+ if (prevIds.length > 10) prevIds.splice(0, prevIds.length - 10);
4074
+ if (!task.ext) task.ext = {};
4075
+ if (!task.ext["ms-todo"]) task.ext["ms-todo"] = {};
4076
+ task.ext["ms-todo"].previous_ids = prevIds;
4077
+ } else {
4078
+ try {
4079
+ const patchBody = { ...msBody };
4080
+ if (!task.due_date) patchBody.dueDateTime = null;
4081
+ if (!task.start_date) patchBody.startDateTime = null;
4082
+ const patched = await graphRequest(
4083
+ token,
4084
+ "PATCH",
4085
+ `/me/todo/lists/${listId}/tasks/${existingMsTodoId}`,
4086
+ patchBody
4087
+ );
4088
+ msTaskId = existingMsTodoId;
4089
+ serverTimestamp = patched.lastModifiedDateTime;
4090
+ } catch (err) {
4091
+ const errMsg = err instanceof Error ? err.message : String(err);
4092
+ const httpStatus = err?.status;
4093
+ log.web.warn("ms-todo PATCH failed (will retry next tick with same ext.id)", {
4094
+ taskId: task.id,
4095
+ existingMsTodoId,
4096
+ listId,
4097
+ httpStatus,
4098
+ error: errMsg
4099
+ });
4100
+ throw err;
4101
+ }
4102
+ }
4103
+ } else {
4104
+ log.web.info("ms-todo POST create", {
4105
+ taskId: task.id,
4106
+ title: task.title,
4107
+ project: task.project,
4108
+ listId,
4109
+ reason: "first_create"
4110
+ });
4111
+ const created = await graphRequest(
4112
+ token,
4113
+ "POST",
4114
+ `/me/todo/lists/${listId}/tasks`,
4115
+ msBody
4116
+ );
4117
+ msTaskId = created.id;
4118
+ serverTimestamp = created.lastModifiedDateTime;
4119
+ log.web.info("ms-todo ext.id assigned", {
4120
+ taskId: task.id,
4121
+ newId: msTaskId,
4122
+ reason: "first_create"
4123
+ });
4124
+ }
4125
+ if (!task.ext) task.ext = {};
4126
+ if (!task.ext["ms-todo"]) task.ext["ms-todo"] = {};
4127
+ task.ext["ms-todo"].id = msTaskId;
4128
+ task.ext["ms-todo"].list_id = actualListId;
4129
+ return { msTaskId, serverTimestamp };
4130
+ }
4131
+ async function pullTasks(listId) {
4132
+ const token = await getAccessToken();
4133
+ const deltaState = await readJsonFile(DELTA_FILE, {
4134
+ deltaLinks: {},
4135
+ listNames: {},
4136
+ lastSync: ""
4137
+ });
4138
+ const existingDelta = deltaState.deltaLinks[listId];
4139
+ let allTasks = [];
4140
+ let nextLink;
4141
+ let deltaLink;
4142
+ const initialUrl = existingDelta ?? `/me/todo/lists/${listId}/tasks/delta`;
4143
+ let response;
4144
+ try {
4145
+ response = await graphRequest(
4146
+ token,
4147
+ "GET",
4148
+ initialUrl
4149
+ );
4150
+ } catch (err) {
4151
+ if (existingDelta && err instanceof Error && err.message.includes(" 410")) {
4152
+ log.web.info("MS To-Do delta link expired, performing full resync", { listId });
4153
+ delete deltaState.deltaLinks[listId];
4154
+ await writeJsonFile(DELTA_FILE, deltaState);
4155
+ response = await graphRequest(
4156
+ token,
4157
+ "GET",
4158
+ `/me/todo/lists/${listId}/tasks/delta`
4159
+ );
4160
+ } else {
4161
+ throw err;
4162
+ }
4163
+ }
4164
+ allTasks.push(...response.value);
4165
+ nextLink = response["@odata.nextLink"];
4166
+ deltaLink = response["@odata.deltaLink"];
4167
+ while (nextLink) {
4168
+ response = await graphRequest(
4169
+ token,
4170
+ "GET",
4171
+ nextLink
4172
+ );
4173
+ allTasks.push(...response.value);
4174
+ nextLink = response["@odata.nextLink"];
4175
+ deltaLink = response["@odata.deltaLink"];
4176
+ }
4177
+ if (deltaLink) {
4178
+ deltaState.deltaLinks[listId] = deltaLink;
4179
+ deltaState.lastSync = (/* @__PURE__ */ new Date()).toISOString();
4180
+ await writeJsonFile(DELTA_FILE, deltaState);
4181
+ }
4182
+ return { tasks: allTasks, deltaLink };
4183
+ }
4184
+ async function fullPullAllTasks() {
4185
+ const token = await getAccessToken();
4186
+ const lists = await fetchTaskLists(token);
4187
+ const result = [];
4188
+ const deltaState = await readJsonFile(DELTA_FILE, {
4189
+ deltaLinks: {},
4190
+ listNames: {},
4191
+ lastSync: ""
4192
+ });
4193
+ const deletedMsIds = new Set(deltaState.deletedMsIds ?? []);
4194
+ for (const list of lists) {
4195
+ let allTasks = [];
4196
+ let response = await graphRequest(
4197
+ token,
4198
+ "GET",
4199
+ `/me/todo/lists/${list.id}/tasks`
4200
+ );
4201
+ allTasks.push(...response.value);
4202
+ let nextLink = response["@odata.nextLink"];
4203
+ while (nextLink) {
4204
+ response = await graphRequest(token, "GET", nextLink);
4205
+ allTasks.push(...response.value);
4206
+ nextLink = response["@odata.nextLink"];
4207
+ }
4208
+ if (allTasks.length === 0) continue;
4209
+ const listProject = await ensureProjectForList(list.displayName);
4210
+ if (listProject === null) continue;
4211
+ for (const msTask of allTasks) {
4212
+ if (deletedMsIds.has(msTask.id)) continue;
4213
+ const fields = mapToLocal(msTask, list.displayName);
4214
+ fields.project = listProject;
4215
+ if (fields.ext?.["ms-todo"]) {
4216
+ fields.ext["ms-todo"].list_id = list.id;
4217
+ }
4218
+ result.push({
4219
+ remoteId: msTask.id,
4220
+ title: msTask.title,
4221
+ remoteUpdatedAt: msTask.lastModifiedDateTime,
4222
+ fields
4223
+ });
4224
+ }
4225
+ }
4226
+ return result;
4227
+ }
4228
+ async function getMsTodoSyncStatus() {
4229
+ const config = await getConfig();
4230
+ const configured = !!getMsTodoConfig(config)?.client_id;
4231
+ if (!configured) {
4232
+ return { configured: false, authenticated: false, lastSync: null, deltaLinksCount: 0 };
4233
+ }
4234
+ let authenticated = false;
4235
+ try {
4236
+ await getAccessToken();
4237
+ authenticated = true;
4238
+ } catch {
4239
+ }
4240
+ const deltaState = await readJsonFile(DELTA_FILE, {
4241
+ deltaLinks: {},
4242
+ listNames: {},
4243
+ lastSync: ""
4244
+ });
4245
+ return {
4246
+ configured,
4247
+ authenticated,
4248
+ lastSync: deltaState.lastSync || null,
4249
+ deltaLinksCount: Object.keys(deltaState.deltaLinks).length
4250
+ };
4251
+ }
4252
+ async function registerDeletedMsIds(task) {
4253
+ const msId = getMsTodoId(task);
4254
+ const prev = msExt(task)?.previous_ids;
4255
+ const idsToRegister = [msId, ...prev ?? []].filter(Boolean);
4256
+ if (idsToRegister.length === 0) return;
4257
+ const deltaState = await readJsonFile(DELTA_FILE, {
4258
+ deltaLinks: {},
4259
+ listNames: {},
4260
+ lastSync: ""
4261
+ });
4262
+ const existing = new Set(deltaState.deletedMsIds ?? []);
4263
+ for (const id of idsToRegister) existing.add(id);
4264
+ const arr = [...existing];
4265
+ deltaState.deletedMsIds = arr.length > 500 ? arr.slice(arr.length - 500) : arr;
4266
+ await writeJsonFile(DELTA_FILE, deltaState);
4267
+ }
4268
+ async function autoPushTask(task) {
4269
+ const key = task.id;
4270
+ const existing = msPushInflight.get(key);
4271
+ if (existing) return existing;
4272
+ const promise = pushTask(task).catch(() => null).finally(() => msPushInflight.delete(key));
4273
+ msPushInflight.set(key, promise);
4274
+ return promise;
4275
+ }
4276
+ async function reconcilePulledTasks(msTasks, list, updateLocalTask, addLocalTask, token, deletedMsIds, previousIdsMap) {
4277
+ let count = 0;
4278
+ if (msTasks.length === 0) return 0;
4279
+ const listProject = await ensureProjectForList(list.displayName);
4280
+ if (listProject === null) {
4281
+ log.web.debug("reconcilePulledTasks: skipped list (no importable project)", {
4282
+ listName: list.displayName,
4283
+ tasks: msTasks.length
4284
+ });
4285
+ return 0;
4286
+ }
4287
+ for (const msTask of msTasks) {
4288
+ if (!msTask.title || msTask.title.trim() === "") continue;
4289
+ if (deletedMsIds?.has(msTask.id)) continue;
4290
+ const existing = await findTaskByExtId("ms-todo", msTask.id) ?? previousIdsMap?.get(msTask.id);
4291
+ if (existing) {
4292
+ const remoteUpdated = new Date(msTask.lastModifiedDateTime).getTime();
4293
+ const syncedAt = existing._syncedAt ? new Date(existing._syncedAt).getTime() : 0;
4294
+ if (remoteUpdated > syncedAt) {
4295
+ const updates = mapToLocal(msTask, list.displayName);
4296
+ updates.project = listProject;
4297
+ await updateLocalTask(existing.id, updates);
4298
+ count++;
4299
+ }
4300
+ } else {
4301
+ const partial = mapToLocal(msTask, list.displayName);
4302
+ try {
4303
+ await addLocalTask({
4304
+ title: partial.title ?? msTask.title,
4305
+ status: partial.status ?? "todo",
4306
+ phase: partial.phase ?? "TODO",
4307
+ priority: partial.priority ?? "none",
4308
+ project: listProject,
4309
+ source: "ms-todo",
4310
+ session_ids: [],
4311
+ ext: { "ms-todo": { id: msTask.id, list_id: list.id } },
4312
+ created_at: msTask.createdDateTime,
4313
+ updated_at: msTask.lastModifiedDateTime,
4314
+ due_date: partial.due_date,
4315
+ start_date: partial.start_date,
4316
+ ...partial.parent_task_id ? { parent_task_id: partial.parent_task_id } : {},
4317
+ description: partial.description ?? "",
4318
+ summary: partial.summary ?? "",
4319
+ note: partial.note ?? "",
4320
+ ...partial.conversation_log ? { conversation_log: partial.conversation_log } : {}
4321
+ });
4322
+ count++;
4323
+ } catch (err) {
4324
+ log.web.debug("reconcilePulledTasks: skipped creating task", {
4325
+ title: msTask.title,
4326
+ listName: list.displayName,
4327
+ error: err instanceof Error ? err.message : String(err)
4328
+ });
4329
+ }
4330
+ }
4331
+ }
4332
+ return count;
4333
+ }
4334
+ function buildMsTodoPreviousIdsMap(localTasks) {
4335
+ const map = /* @__PURE__ */ new Map();
4336
+ for (const t of localTasks) {
4337
+ if (t.source !== "ms-todo") continue;
4338
+ const prevIds = msExt(t)?.previous_ids;
4339
+ if (!prevIds || prevIds.length === 0) continue;
4340
+ for (const oldId of prevIds) {
4341
+ if (!map.has(oldId)) map.set(oldId, t);
4342
+ }
4343
+ }
4344
+ return map;
4345
+ }
4346
+ async function deltaPull(localTasks, updateLocalTask, addLocalTask) {
4347
+ const token = await getAccessToken();
4348
+ const lists = await fetchTaskLists(token);
4349
+ const msLocalTasks = localTasks.filter((t) => t.source === "ms-todo");
4350
+ const previousIdsMap = buildMsTodoPreviousIdsMap(msLocalTasks);
4351
+ let hasChanges = false;
4352
+ const deltaState = await readJsonFile(DELTA_FILE, {
4353
+ deltaLinks: {},
4354
+ listNames: {},
4355
+ lastSync: ""
4356
+ });
4357
+ const storedNames = deltaState.listNames ?? {};
4358
+ const projectByListId = /* @__PURE__ */ new Map();
4359
+ for (const list of lists) {
4360
+ const oldName = storedNames[list.id];
4361
+ if (oldName && oldName !== list.displayName) {
4362
+ const project = await syncProjectAliasAfterRename(list.displayName);
4363
+ projectByListId.set(list.id, project);
4364
+ if (project === null) continue;
4365
+ for (const task of msLocalTasks) {
4366
+ if (getMsTodoList(task) === list.id && (task.project || "").toLowerCase() !== project.toLowerCase()) {
4367
+ await updateLocalTask(task.id, { project });
4368
+ hasChanges = true;
4369
+ }
4370
+ }
4371
+ }
4372
+ }
4373
+ const newListNames = {};
4374
+ for (const list of lists) {
4375
+ newListNames[list.id] = list.displayName;
4376
+ }
4377
+ deltaState.listNames = newListNames;
4378
+ await writeJsonFile(DELTA_FILE, deltaState);
4379
+ const listNameById = new Map(lists.map((l) => [l.id, l.displayName]));
4380
+ for (const task of msLocalTasks) {
4381
+ const taskListId = getMsTodoList(task);
4382
+ if (!taskListId) continue;
4383
+ const currentListName = listNameById.get(taskListId);
4384
+ if (!currentListName) {
4385
+ if (task.phase !== "COMPLETE") {
4386
+ await updateLocalTask(task.id, { phase: "COMPLETE" });
4387
+ hasChanges = true;
4388
+ }
4389
+ continue;
4390
+ }
4391
+ let project = projectByListId.get(taskListId);
4392
+ if (project === void 0) {
4393
+ project = await ensureProjectForList(currentListName);
4394
+ projectByListId.set(taskListId, project);
4395
+ }
4396
+ if (project === null) continue;
4397
+ if ((task.project || "").toLowerCase() !== project.toLowerCase()) {
4398
+ await updateLocalTask(task.id, { project });
4399
+ hasChanges = true;
4400
+ }
4401
+ }
4402
+ const deletedMsIds = new Set(deltaState.deletedMsIds ?? []);
4403
+ for (const list of lists) {
4404
+ const { tasks: msTasks } = await pullTasks(list.id);
4405
+ if (msTasks.length === 0) continue;
4406
+ const count = await reconcilePulledTasks(msTasks, list, updateLocalTask, addLocalTask, token, deletedMsIds, previousIdsMap);
4407
+ if (count > 0) hasChanges = true;
4408
+ }
4409
+ return hasChanges;
4410
+ }
4411
+ async function syncTasks(localTasks, updateLocalTask, addLocalTask) {
4412
+ const result = { pushed: 0, pulled: 0, errors: [] };
4413
+ const token = await getAccessToken();
4414
+ const lists = await fetchTaskLists(token);
4415
+ const msLocalTasks = localTasks.filter((t) => t.source === "ms-todo");
4416
+ const previousIdsMap = buildMsTodoPreviousIdsMap(msLocalTasks);
4417
+ const listByName = /* @__PURE__ */ new Map();
4418
+ for (const list of lists) {
4419
+ listByName.set(list.displayName.toLowerCase(), list.id);
4420
+ }
4421
+ const defaultListId = lists[0]?.id;
4422
+ for (const task of msLocalTasks) {
4423
+ if (!getMsTodoId(task)) {
4424
+ try {
4425
+ const { msTaskId } = await pushTask(task);
4426
+ const taskListName = await remoteListNameFor(task.project || "");
4427
+ const listId = listByName.get(taskListName.toLowerCase()) ?? defaultListId;
4428
+ await updateLocalTask(task.id, {
4429
+ ext: { "ms-todo": { id: msTaskId, list_id: listId } }
4430
+ });
4431
+ result.pushed++;
4432
+ } catch (err) {
4433
+ result.errors.push(`Push failed for "${task.title}": ${err}`);
4434
+ }
4435
+ }
4436
+ }
4437
+ const deltaState = await readJsonFile(DELTA_FILE, {
4438
+ deltaLinks: {},
4439
+ listNames: {},
4440
+ lastSync: ""
4441
+ });
4442
+ const deletedMsIds = new Set(deltaState.deletedMsIds ?? []);
4443
+ for (const list of lists) {
4444
+ try {
4445
+ const { tasks: msTasks } = await pullTasks(list.id);
4446
+ const count = await reconcilePulledTasks(msTasks, list, updateLocalTask, addLocalTask, token, deletedMsIds, previousIdsMap);
4447
+ result.pulled += count;
4448
+ } catch (err) {
4449
+ result.errors.push(`Pull failed for list "${list.displayName}": ${err}`);
4450
+ }
4451
+ }
4452
+ return result;
4453
+ }
4454
+ var TOKENS_FILE, DELTA_FILE, GRAPH_BASE, SCOPES, PRIORITY_TO_IMPORTANCE, IMPORTANCE_TO_PRIORITY, listIdCache, listIdInflight, msPushInflight;
4455
+ var init_microsoft_todo = __esm({
4456
+ "src/integrations/microsoft-todo.ts"() {
4457
+ "use strict";
4458
+ init_constants();
4459
+ init_fs();
4460
+ init_config_manager();
4461
+ init_logging();
4462
+ init_phase();
4463
+ init_phase2();
4464
+ init_format();
4465
+ init_task_manager();
4466
+ TOKENS_FILE = path12.join(SYNC_DIR, "ms-todo-tokens.json");
4467
+ DELTA_FILE = path12.join(SYNC_DIR, "ms-todo-delta.json");
4468
+ GRAPH_BASE = "https://graph.microsoft.com/v1.0";
4469
+ SCOPES = ["Tasks.ReadWrite"];
4470
+ PRIORITY_TO_IMPORTANCE = {
4471
+ immediate: "high",
4472
+ important: "normal",
4473
+ backlog: "low",
4474
+ none: "normal"
4475
+ };
4476
+ IMPORTANCE_TO_PRIORITY = {
4477
+ high: "immediate",
4478
+ low: "backlog",
4479
+ normal: "none"
4480
+ };
4481
+ listIdCache = /* @__PURE__ */ new Map();
4482
+ listIdInflight = /* @__PURE__ */ new Map();
4483
+ msPushInflight = /* @__PURE__ */ new Map();
4484
+ }
4485
+ });
4486
+
4487
+ // src/integrations/ms-todo/index.ts
4488
+ function register(api) {
4489
+ const sync = {
4490
+ async createTask(task) {
4491
+ const { autoPushTask: autoPushTask2 } = await Promise.resolve().then(() => (init_microsoft_todo(), microsoft_todo_exports));
4492
+ const result = await autoPushTask2(task);
4493
+ if (result) {
4494
+ return { "ms-todo": { id: result.msTaskId, list_id: task.ext?.["ms-todo"]?.list_id } };
4495
+ }
4496
+ return null;
4497
+ },
4498
+ async deleteTask(task) {
4499
+ const { deleteMsTodoTask: deleteMsTodoTask2, registerDeletedMsIds: registerDeletedMsIds2 } = await Promise.resolve().then(() => (init_microsoft_todo(), microsoft_todo_exports));
4500
+ await registerDeletedMsIds2(task);
4501
+ const msId = task.ext?.["ms-todo"]?.id;
4502
+ const listId = task.ext?.["ms-todo"]?.list_id;
4503
+ if (!msId || !listId) return;
4504
+ try {
4505
+ await deleteMsTodoTask2(listId, msId);
4506
+ } catch {
4507
+ }
4508
+ },
4509
+ async updateTitle(task) {
4510
+ const { autoPushTask: autoPushTask2 } = await Promise.resolve().then(() => (init_microsoft_todo(), microsoft_todo_exports));
4511
+ await autoPushTask2(task);
4512
+ },
4513
+ async updateDescription(task) {
4514
+ const { autoPushTask: autoPushTask2 } = await Promise.resolve().then(() => (init_microsoft_todo(), microsoft_todo_exports));
4515
+ await autoPushTask2(task);
4516
+ },
4517
+ async updateSummary(task) {
4518
+ const { autoPushTask: autoPushTask2 } = await Promise.resolve().then(() => (init_microsoft_todo(), microsoft_todo_exports));
4519
+ await autoPushTask2(task);
4520
+ },
4521
+ async updateNote(task) {
4522
+ const { autoPushTask: autoPushTask2 } = await Promise.resolve().then(() => (init_microsoft_todo(), microsoft_todo_exports));
4523
+ await autoPushTask2(task);
4524
+ },
4525
+ async updateConversationLog(task) {
4526
+ const { autoPushTask: autoPushTask2 } = await Promise.resolve().then(() => (init_microsoft_todo(), microsoft_todo_exports));
4527
+ await autoPushTask2(task);
4528
+ },
4529
+ async updatePriority(task) {
4530
+ const { autoPushTask: autoPushTask2 } = await Promise.resolve().then(() => (init_microsoft_todo(), microsoft_todo_exports));
4531
+ await autoPushTask2(task);
4532
+ },
4533
+ async updatePhase(task) {
4534
+ const { autoPushTask: autoPushTask2 } = await Promise.resolve().then(() => (init_microsoft_todo(), microsoft_todo_exports));
4535
+ await autoPushTask2(task);
4536
+ },
4537
+ async updateDueDate(task) {
4538
+ const { autoPushTask: autoPushTask2 } = await Promise.resolve().then(() => (init_microsoft_todo(), microsoft_todo_exports));
4539
+ await autoPushTask2(task);
4540
+ },
4541
+ async updateStar(task) {
4542
+ const { autoPushTask: autoPushTask2 } = await Promise.resolve().then(() => (init_microsoft_todo(), microsoft_todo_exports));
4543
+ await autoPushTask2(task);
4544
+ },
4545
+ /**
4546
+ * A single task moved between projects → re-push it, which resolves the target
4547
+ * list (creating one if the project has no remote list yet) and migrates the
4548
+ * remote twin into it.
4549
+ *
4550
+ * NOTE: this is NOT the project-rename path. Renaming a project renames the
4551
+ * remote LIST once (core's renameProject → renameListByName, keyed on the
4552
+ * `remote_list` alias); calling this hook per task instead would resolve the
4553
+ * new name, find no list, and CREATE one — forking the user's list in two.
4554
+ * It stays the fallback for when that container rename fails, and the surviving
4555
+ * alias makes that fallback land in the existing list rather than a new one.
4556
+ */
4557
+ async updateProject(task) {
4558
+ const { autoPushTask: autoPushTask2 } = await Promise.resolve().then(() => (init_microsoft_todo(), microsoft_todo_exports));
4559
+ await autoPushTask2(task);
4560
+ },
4561
+ async updateDependencies(task) {
4562
+ const { autoPushTask: autoPushTask2 } = await Promise.resolve().then(() => (init_microsoft_todo(), microsoft_todo_exports));
4563
+ await autoPushTask2(task);
4564
+ },
4565
+ async pushTask(task) {
4566
+ const { pushTask: msPushTask } = await Promise.resolve().then(() => (init_microsoft_todo(), microsoft_todo_exports));
4567
+ const result = await msPushTask(task);
4568
+ return {
4569
+ serverTimestamp: result.serverTimestamp,
4570
+ ext: { "ms-todo": { id: result.msTaskId, list_id: task.ext?.["ms-todo"]?.list_id } }
4571
+ };
4572
+ },
4573
+ async associateSubtask(_parent, _child) {
4574
+ },
4575
+ async disassociateSubtask(_parent, _child) {
4576
+ },
4577
+ async syncPoll(ctx) {
4578
+ const { deltaPull: deltaPull2 } = await Promise.resolve().then(() => (init_microsoft_todo(), microsoft_todo_exports));
4579
+ const tasks = ctx.getTasks();
4580
+ await deltaPull2(
4581
+ tasks,
4582
+ async (id, updates) => {
4583
+ await ctx.updateTask(id, updates);
4584
+ },
4585
+ async (taskData) => {
4586
+ const t = await ctx.addTask(taskData);
4587
+ return t;
4588
+ }
4589
+ );
4590
+ },
4591
+ async renameProjectRemote({ oldRemoteName, newName }) {
4592
+ const { renameListByName: renameListByName2 } = await Promise.resolve().then(() => (init_microsoft_todo(), microsoft_todo_exports));
4593
+ await renameListByName2(oldRemoteName, newName);
4594
+ },
4595
+ async deleteProjectRemote(args) {
4596
+ const { deleteListForProject: deleteListForProject2 } = await Promise.resolve().then(() => (init_microsoft_todo(), microsoft_todo_exports));
4597
+ await deleteListForProject2(args);
4598
+ return { outcome: "container-deleted" };
4599
+ },
4600
+ async fullPull() {
4601
+ const { fullPullAllTasks: fullPullAllTasks2 } = await Promise.resolve().then(() => (init_microsoft_todo(), microsoft_todo_exports));
4602
+ return fullPullAllTasks2();
4603
+ },
4604
+ extractRemoteId(task) {
4605
+ return task.ext?.["ms-todo"]?.id;
4606
+ }
4607
+ };
4608
+ api.registerSync(sync);
4609
+ api.registerExtIndex({
4610
+ source: "ms-todo",
4611
+ paths: [{ key: "id", json: '$."ms-todo".id' }]
4612
+ });
4613
+ api.registerSourceClaim(() => {
4614
+ return true;
4615
+ }, { priority: 0 });
4616
+ api.registerDisplay({
4617
+ badge: "M",
4618
+ badgeColor: "#0078D4",
4619
+ externalLinkLabel: "Microsoft To-Do",
4620
+ getExternalUrl: () => "https://to-do.microsoft.com",
4621
+ isSynced: (task) => !!task.ext?.["ms-todo"]?.id,
4622
+ syncTooltip: (task) => task.sync_error ? `Sync error: ${task.sync_error}` : "Synced to Microsoft To-Do"
4623
+ });
4624
+ api.registerAgentContext(
4625
+ 'Tasks with source "ms-todo" sync bidirectionally with Microsoft To-Do. Phase maps to 3 MS To-Do statuses: notStarted/inProgress/completed.'
4626
+ );
4627
+ api.registerMigration((tasks) => {
4628
+ for (const task of tasks) {
4629
+ const raw = task;
4630
+ if (raw.ms_todo_id && !task.ext?.["ms-todo"]) {
4631
+ if (!task.ext) task.ext = {};
4632
+ task.ext["ms-todo"] = {
4633
+ id: raw.ms_todo_id,
4634
+ list_id: raw.ms_todo_list
4635
+ };
4636
+ delete raw.ms_todo_id;
4637
+ delete raw.ms_todo_list;
4638
+ } else if (raw.ms_todo_id) {
4639
+ const existingId = task.ext?.["ms-todo"]?.id;
4640
+ if (existingId && existingId !== raw.ms_todo_id) {
4641
+ api.logger.warn("migration conflict: ms_todo_id differs from ext id", {
4642
+ taskId: task.id,
4643
+ legacyId: raw.ms_todo_id,
4644
+ extId: existingId
4645
+ });
4646
+ }
4647
+ delete raw.ms_todo_id;
4648
+ delete raw.ms_todo_list;
4649
+ }
4650
+ const msExt2 = task.ext?.["ms-todo"];
4651
+ if (msExt2 && typeof msExt2 === "object" && msExt2["ms-todo"] && typeof msExt2["ms-todo"] === "object") {
4652
+ const inner = msExt2["ms-todo"];
4653
+ task.ext["ms-todo"] = {
4654
+ id: inner.id ?? msExt2.id,
4655
+ list_id: inner.list_id ?? inner.list ?? msExt2.list_id ?? msExt2.list
4656
+ };
4657
+ }
4658
+ const msExtNorm = task.ext?.["ms-todo"];
4659
+ if (msExtNorm && msExtNorm.list) {
4660
+ if (!msExtNorm.list_id) {
4661
+ msExtNorm.list_id = msExtNorm.list;
4662
+ }
4663
+ delete msExtNorm.list;
4664
+ }
4665
+ }
4666
+ return tasks;
4667
+ });
4668
+ }
4669
+ export {
4670
+ register as default
4671
+ };
4672
+ //# sourceMappingURL=index.js.map