@aurora-foundation/obsidian-next 0.4.7 → 0.4.9

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 (232) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/LICENSE +628 -190
  3. package/README.md +23 -9
  4. package/dist/auditLog-6WDBDNYL.js +8 -0
  5. package/dist/auditLog-HGPVDSDC.js +8 -0
  6. package/dist/auditLog-TDIKFBM4.js +8 -0
  7. package/dist/auditLog-XC2KY3ZZ.js +8 -0
  8. package/dist/chunk-2I235WNB.js +133 -0
  9. package/dist/chunk-2JWDGXTR.js +42 -0
  10. package/dist/chunk-2NOB6W2B.js +133 -0
  11. package/dist/chunk-3LFKVKKL.js +7199 -0
  12. package/dist/chunk-3U6WHPDX.js +4695 -0
  13. package/dist/chunk-3UCL6RYE.js +7272 -0
  14. package/dist/chunk-4GN2UQLI.js +130 -0
  15. package/dist/chunk-4MW33MZD.js +516 -0
  16. package/dist/chunk-4PUJBUKZ.js +4716 -0
  17. package/dist/chunk-4QHK6H6O.js +130 -0
  18. package/dist/chunk-55CQIHCO.js +133 -0
  19. package/dist/chunk-5LWINFWI.js +676 -0
  20. package/dist/chunk-5OKGLNQW.js +439 -0
  21. package/dist/chunk-5T6ETZEO.js +6183 -0
  22. package/dist/chunk-5WGIFUVL.js +4234 -0
  23. package/dist/chunk-66EW47T3.js +4237 -0
  24. package/dist/chunk-6TXUOTT2.js +581 -0
  25. package/dist/chunk-6YUYSYDA.js +130 -0
  26. package/dist/chunk-74VPNFMX.js +133 -0
  27. package/dist/chunk-77CGJRGV.js +6188 -0
  28. package/dist/chunk-7DS3VT4C.js +7135 -0
  29. package/dist/chunk-7FHX3VBT.js +133 -0
  30. package/dist/chunk-7MHF56YU.js +6178 -0
  31. package/dist/chunk-ABLPMV7G.js +133 -0
  32. package/dist/chunk-B77K6OQZ.js +687 -0
  33. package/dist/chunk-BKOXH66O.js +133 -0
  34. package/dist/chunk-BPP76UN2.js +130 -0
  35. package/dist/chunk-C4D56GRC.js +5936 -0
  36. package/dist/chunk-CCDPY4WE.js +370 -0
  37. package/dist/chunk-CHNVBJN3.js +7272 -0
  38. package/dist/chunk-CKBZI576.js +7229 -0
  39. package/dist/chunk-CW5HBSJ2.js +7198 -0
  40. package/dist/chunk-DGHDJEY7.js +133 -0
  41. package/dist/chunk-DPNIQWKZ.js +439 -0
  42. package/dist/chunk-DU4T3V2T.js +214 -0
  43. package/dist/chunk-DV3WFKNB.js +4679 -0
  44. package/dist/chunk-DZI2OVN2.js +516 -0
  45. package/dist/chunk-E45VILML.js +7198 -0
  46. package/dist/chunk-ECEUUYXC.js +7199 -0
  47. package/dist/chunk-EJRRSHPW.js +685 -0
  48. package/dist/chunk-EMBMLZFE.js +370 -0
  49. package/dist/chunk-EPG5V5OO.js +285 -0
  50. package/dist/chunk-F2R4HXXW.js +130 -0
  51. package/dist/chunk-FK6N66ES.js +581 -0
  52. package/dist/chunk-G3CZKGYA.js +197 -0
  53. package/dist/chunk-GUUPG4A7.js +7111 -0
  54. package/dist/chunk-HBAAUGUN.js +7230 -0
  55. package/dist/chunk-HHFJMK2Q.js +6177 -0
  56. package/dist/chunk-HINRQTCZ.js +196 -0
  57. package/dist/chunk-HRKJ3R2U.js +288 -0
  58. package/dist/chunk-HWVK4CVE.js +439 -0
  59. package/dist/chunk-JEYSADNZ.js +581 -0
  60. package/dist/chunk-JNEIL7UN.js +4252 -0
  61. package/dist/chunk-JTWSK277.js +676 -0
  62. package/dist/chunk-K4CHTTCJ.js +942 -0
  63. package/dist/chunk-K7R5KUDS.js +4695 -0
  64. package/dist/chunk-KNJFOURE.js +7151 -0
  65. package/dist/chunk-KY22FIT3.js +7256 -0
  66. package/dist/chunk-L2OTIJSF.js +4228 -0
  67. package/dist/chunk-LEEBUHP6.js +4655 -0
  68. package/dist/chunk-LK7UP2T7.js +130 -0
  69. package/dist/chunk-LPGNO3PK.js +284 -0
  70. package/dist/chunk-LYQYJMWS.js +133 -0
  71. package/dist/chunk-MBYFJXR3.js +130 -0
  72. package/dist/chunk-N3WX44L3.js +130 -0
  73. package/dist/chunk-N6AQWES3.js +6197 -0
  74. package/dist/chunk-NW4XSTQZ.js +130 -0
  75. package/dist/chunk-NWG2XURH.js +130 -0
  76. package/dist/chunk-O3GF3LJD.js +6142 -0
  77. package/dist/chunk-OHP5LD3Y.js +6188 -0
  78. package/dist/chunk-P5PQSFZT.js +6182 -0
  79. package/dist/chunk-PAADOWNP.js +130 -0
  80. package/dist/chunk-PERGND7L.js +7213 -0
  81. package/dist/chunk-PWA7V4XX.js +179 -0
  82. package/dist/chunk-QGCWEP6L.js +7111 -0
  83. package/dist/chunk-QVT2IHNJ.js +175 -0
  84. package/dist/chunk-QZNGYPMS.js +6161 -0
  85. package/dist/chunk-R6P2E2ZQ.js +207 -0
  86. package/dist/chunk-ROSDMGIL.js +4679 -0
  87. package/dist/chunk-RQZP7IKG.js +196 -0
  88. package/dist/chunk-RUQSPX3U.js +133 -0
  89. package/dist/chunk-S3BYHP5M.js +130 -0
  90. package/dist/chunk-S6GNETVE.js +438 -0
  91. package/dist/chunk-SDT2ZE2R.js +133 -0
  92. package/dist/chunk-SHQBXJFC.js +6166 -0
  93. package/dist/chunk-TJNISYTE.js +42 -0
  94. package/dist/chunk-TJW74HFF.js +130 -0
  95. package/dist/chunk-TPP72DTK.js +7096 -0
  96. package/dist/chunk-UOESII6R.js +42 -0
  97. package/dist/chunk-UWEDGLYJ.js +6142 -0
  98. package/dist/chunk-V5FYNAFX.js +133 -0
  99. package/dist/chunk-VPURF6UT.js +7198 -0
  100. package/dist/chunk-VQH6LWIZ.js +6184 -0
  101. package/dist/chunk-VS22YVX6.js +7111 -0
  102. package/dist/chunk-VSF5KBW7.js +367 -0
  103. package/dist/chunk-VV3JMCKY.js +214 -0
  104. package/dist/chunk-W5L7HOE3.js +133 -0
  105. package/dist/chunk-WFEVQISK.js +676 -0
  106. package/dist/chunk-WJZPSCEP.js +516 -0
  107. package/dist/chunk-WLV4MKEF.js +16 -0
  108. package/dist/chunk-WSEVQFFI.js +5428 -0
  109. package/dist/chunk-X7N2RNR3.js +5428 -0
  110. package/dist/chunk-XKZNMRNO.js +133 -0
  111. package/dist/chunk-Y7BVEC36.js +130 -0
  112. package/dist/chunk-YG7YSNNU.js +4226 -0
  113. package/dist/chunk-YHM62466.js +261 -0
  114. package/dist/chunk-YLTYJLDZ.js +7208 -0
  115. package/dist/chunk-YPMJD4YE.js +56 -0
  116. package/dist/chunk-YTX3FU2A.js +7199 -0
  117. package/dist/chunk-ZEQ3EBBN.js +214 -0
  118. package/dist/chunk-ZIWLQSLK.js +42 -0
  119. package/dist/chunk-ZJELNTEO.js +516 -0
  120. package/dist/chunk-ZOSSVNGK.js +370 -0
  121. package/dist/config-EYK32F2E.js +10 -0
  122. package/dist/config-FJPPPYTY.js +10 -0
  123. package/dist/config-VAHPVILX.js +10 -0
  124. package/dist/context-2YGE4U75.js +10 -0
  125. package/dist/context-5UFVYKES.js +9 -0
  126. package/dist/context-ANZF4J72.js +10 -0
  127. package/dist/context-GLUNCUBQ.js +10 -0
  128. package/dist/context-M5ULPZKQ.js +10 -0
  129. package/dist/context-NYOIRZKV.js +10 -0
  130. package/dist/context-YP2REI6A.js +10 -0
  131. package/dist/database-MP2JBLMF.js +8 -0
  132. package/dist/index.js +6177 -4688
  133. package/dist/keyManager-P2SZONKE.js +8 -0
  134. package/dist/mcp/index.js +127 -47
  135. package/dist/memory-BPGJAL4J.js +13 -0
  136. package/dist/memory-FRQOUI6W.js +13 -0
  137. package/dist/memory-OAMK27IZ.js +13 -0
  138. package/dist/memory-OZ734ALL.js +13 -0
  139. package/dist/memory-PQ2EWRMU.js +13 -0
  140. package/dist/memory-PXL45M6W.js +13 -0
  141. package/dist/memory-QZTBTYPH.js +13 -0
  142. package/dist/migrations-TLJ3WRVW.js +188 -0
  143. package/dist/resume-2NHDK6EI.js +17 -0
  144. package/dist/resume-44L2PDB2.js +17 -0
  145. package/dist/resume-4MXIWUJO.js +7 -0
  146. package/dist/resume-72VJX66I.js +17 -0
  147. package/dist/resume-7ZW4XM3B.js +15 -0
  148. package/dist/resume-AZHYQ657.js +17 -0
  149. package/dist/resume-CNLXSYHV.js +17 -0
  150. package/dist/resume-DPN4Q777.js +16 -0
  151. package/dist/resume-DSFHVNPI.js +15 -0
  152. package/dist/resume-GHLQJJTO.js +17 -0
  153. package/dist/resume-HJ6SBWTF.js +17 -0
  154. package/dist/resume-HRLYHY2L.js +17 -0
  155. package/dist/resume-ISIQFKO6.js +17 -0
  156. package/dist/resume-JQDEA6PS.js +15 -0
  157. package/dist/resume-KP3Y3Y7P.js +17 -0
  158. package/dist/resume-M4KHR5OI.js +17 -0
  159. package/dist/resume-N62OAMBG.js +17 -0
  160. package/dist/resume-NNMA5POT.js +17 -0
  161. package/dist/resume-ODYT3J4H.js +17 -0
  162. package/dist/resume-PCFJXA5O.js +17 -0
  163. package/dist/resume-PLF4XGBD.js +15 -0
  164. package/dist/resume-Q2PFX57V.js +17 -0
  165. package/dist/resume-QB4XI2J5.js +17 -0
  166. package/dist/resume-R5MFTUPF.js +17 -0
  167. package/dist/resume-SVA7223Z.js +17 -0
  168. package/dist/resume-TYKKDJZI.js +17 -0
  169. package/dist/resume-XIS45HKV.js +17 -0
  170. package/dist/resume-YCSEJTU7.js +17 -0
  171. package/dist/resume-YD76GI2J.js +15 -0
  172. package/dist/resume-YDN7EL77.js +17 -0
  173. package/dist/resume-YE7DB4ZA.js +17 -0
  174. package/dist/resume-YKAKOXWV.js +15 -0
  175. package/dist/resume-ZHBCVFDY.js +17 -0
  176. package/dist/scheduler-2CK24A2Q.js +14 -0
  177. package/dist/scheduler-7OAF2XKX.js +14 -0
  178. package/dist/scheduler-AS23AAB5.js +14 -0
  179. package/dist/scheduler-PCOYQJA5.js +14 -0
  180. package/dist/scheduler-V2ECBQPK.js +14 -0
  181. package/dist/scheduler-VEWZ6L7V.js +13 -0
  182. package/dist/scheduler-W37QMGDQ.js +14 -0
  183. package/dist/session-2E2JKPD7.js +15 -0
  184. package/dist/session-2VSF257B.js +14 -0
  185. package/dist/session-4APFTDJU.js +14 -0
  186. package/dist/session-5YS5LNNL.js +16 -0
  187. package/dist/session-6MO5ZPOB.js +16 -0
  188. package/dist/session-6XMGPRTQ.js +14 -0
  189. package/dist/session-7DJR77R7.js +16 -0
  190. package/dist/session-7DQHPWTR.js +14 -0
  191. package/dist/session-ADKIQCR5.js +16 -0
  192. package/dist/session-AOGH2GGI.js +16 -0
  193. package/dist/session-C4W6GDYG.js +16 -0
  194. package/dist/session-DCGNGGMV.js +14 -0
  195. package/dist/session-F5JKZAN2.js +16 -0
  196. package/dist/session-G6F3O2FQ.js +16 -0
  197. package/dist/session-GFBSARRO.js +16 -0
  198. package/dist/session-H5IWAIUI.js +16 -0
  199. package/dist/session-IPFA6AHC.js +14 -0
  200. package/dist/session-IWG2UOAX.js +14 -0
  201. package/dist/session-KJ2K4Y4M.js +14 -0
  202. package/dist/session-KPXFBW6Q.js +14 -0
  203. package/dist/session-KR256UL5.js +16 -0
  204. package/dist/session-M72LJXPR.js +16 -0
  205. package/dist/session-MBK3FODN.js +14 -0
  206. package/dist/session-MOUFAU7G.js +16 -0
  207. package/dist/session-NRC6ZXFQ.js +16 -0
  208. package/dist/session-NRPQMV4K.js +16 -0
  209. package/dist/session-O5IFFJZQ.js +14 -0
  210. package/dist/session-OF5BGKDE.js +16 -0
  211. package/dist/session-OGRZMIM7.js +14 -0
  212. package/dist/session-OJOFAJG3.js +16 -0
  213. package/dist/session-OKU4N3SP.js +16 -0
  214. package/dist/session-P2VAOSFB.js +14 -0
  215. package/dist/session-PKOVZD4M.js +16 -0
  216. package/dist/session-POAIMUVN.js +16 -0
  217. package/dist/session-PSHFONFE.js +16 -0
  218. package/dist/session-QKYVVZFV.js +16 -0
  219. package/dist/session-QPWGBMUS.js +14 -0
  220. package/dist/session-R5UG5PZR.js +14 -0
  221. package/dist/session-RAY6BZRQ.js +16 -0
  222. package/dist/session-S3VATHMU.js +16 -0
  223. package/dist/session-SYTD7RHW.js +14 -0
  224. package/dist/session-UHMMVO4J.js +16 -0
  225. package/dist/session-WEX5K3ZY.js +14 -0
  226. package/dist/session-XFLOXGU3.js +14 -0
  227. package/dist/session-XV2A4HHG.js +14 -0
  228. package/dist/settings-3VPJYD4D.js +8 -0
  229. package/dist/settings-GZTJJTBK.js +8 -0
  230. package/dist/settings-YKJFSKMO.js +8 -0
  231. package/dist/shell-FM34624T.js +8 -0
  232. package/package.json +14 -4
@@ -0,0 +1,4695 @@
1
+ import {
2
+ scheduler
3
+ } from "./chunk-VV3JMCKY.js";
4
+ import {
5
+ auditLog,
6
+ redactor
7
+ } from "./chunk-JTWSK277.js";
8
+ import {
9
+ context
10
+ } from "./chunk-ZOSSVNGK.js";
11
+ import {
12
+ activateApp,
13
+ calculateScaleForAPI,
14
+ clickElementByLabel,
15
+ computer,
16
+ findClickableByLabel,
17
+ getButtons,
18
+ getDisplayDimensions,
19
+ getFocusedApp,
20
+ getToolConfig,
21
+ getUIContext,
22
+ takeScreenshotForAPI
23
+ } from "./chunk-MTJI7ZQR.js";
24
+ import {
25
+ config
26
+ } from "./chunk-7MMY74WO.js";
27
+ import {
28
+ settings
29
+ } from "./chunk-EPG5V5OO.js";
30
+ import {
31
+ detectEnvFile,
32
+ keyManager
33
+ } from "./chunk-57D77KRX.js";
34
+ import {
35
+ bus
36
+ } from "./chunk-WQM6FFSD.js";
37
+ import {
38
+ db
39
+ } from "./chunk-FNLWB54Z.js";
40
+
41
+ // src/core/history.ts
42
+ var HistoryManager = class {
43
+ saveTimer = null;
44
+ constructor() {
45
+ }
46
+ async load() {
47
+ const currentSessionId = context.get().session_id;
48
+ if (!currentSessionId) return [];
49
+ try {
50
+ const rows = db.getDb().prepare(`
51
+ SELECT content FROM events
52
+ WHERE session_id = ?
53
+ ORDER BY timestamp ASC
54
+ `).all(currentSessionId);
55
+ return rows.map((r) => JSON.parse(r.content));
56
+ } catch (e) {
57
+ console.error("Failed to load history:", e);
58
+ return [];
59
+ }
60
+ }
61
+ async save(events) {
62
+ if (this.saveTimer) clearTimeout(this.saveTimer);
63
+ this.saveTimer = setTimeout(async () => {
64
+ const currentSessionId = context.get().session_id;
65
+ if (!currentSessionId) return;
66
+ try {
67
+ const transaction = db.getDb().transaction(() => {
68
+ db.getDb().prepare("DELETE FROM events WHERE session_id = ?").run(currentSessionId);
69
+ const insert = db.getDb().prepare(`
70
+ INSERT INTO events (session_id, type, content, timestamp)
71
+ VALUES (?, ?, ?, ?)
72
+ `);
73
+ for (const event of events) {
74
+ insert.run(
75
+ currentSessionId,
76
+ event.type,
77
+ JSON.stringify(event),
78
+ event.timestamp || Date.now()
79
+ );
80
+ }
81
+ });
82
+ transaction();
83
+ } catch (error) {
84
+ console.error("Failed to save history:", error);
85
+ }
86
+ }, 500);
87
+ }
88
+ async archive(events) {
89
+ return null;
90
+ }
91
+ async clear() {
92
+ if (this.saveTimer) clearTimeout(this.saveTimer);
93
+ const currentSessionId = context.get().session_id;
94
+ if (!currentSessionId) return;
95
+ try {
96
+ db.getDb().prepare("DELETE FROM events WHERE session_id = ?").run(currentSessionId);
97
+ } catch {
98
+ }
99
+ }
100
+ };
101
+ var history = new HistoryManager();
102
+
103
+ // src/core/tasks.ts
104
+ var TaskTracker = class {
105
+ task = null;
106
+ constructor() {
107
+ }
108
+ async init() {
109
+ await this.load();
110
+ }
111
+ async load() {
112
+ try {
113
+ const currentSessionId = context.get().session_id;
114
+ if (!currentSessionId) {
115
+ this.task = null;
116
+ return;
117
+ }
118
+ const row = db.getDb().prepare(`
119
+ SELECT * FROM tasks
120
+ WHERE session_id = ?
121
+ AND status != 'done' -- Only load active task? Or the most recent one?
122
+ ORDER BY created_at DESC
123
+ LIMIT 1
124
+ `).get(currentSessionId);
125
+ if (row) {
126
+ const subtasks = db.getDb().prepare(`
127
+ SELECT text, done FROM subtasks
128
+ WHERE task_id = ?
129
+ ORDER BY position ASC
130
+ `).all(row.id);
131
+ this.task = {
132
+ id: row.id,
133
+ title: row.title,
134
+ status: row.status,
135
+ subtasks: subtasks.map((s) => ({ text: s.text, done: s.done === 1 })),
136
+ context: row.context ? JSON.parse(row.context) : [],
137
+ created_at: new Date(row.created_at).toISOString(),
138
+ updated_at: new Date(row.updated_at).toISOString()
139
+ };
140
+ } else {
141
+ this.task = null;
142
+ }
143
+ } catch (e) {
144
+ bus.emitAgent({ type: "error", message: `[Tasks] Failed to load: ${e instanceof Error ? e.message : String(e)}` });
145
+ this.task = null;
146
+ }
147
+ bus.emitAgent({ type: "task_update", task: this.task });
148
+ }
149
+ async save() {
150
+ if (!this.task) return;
151
+ try {
152
+ const currentSessionId = context.get().session_id;
153
+ const updateSubtasks = db.getDb().transaction(() => {
154
+ db.getDb().prepare(`
155
+ INSERT INTO tasks (id, session_id, title, status, context, created_at, updated_at)
156
+ VALUES (?, ?, ?, ?, ?, ?, ?)
157
+ ON CONFLICT(id) DO UPDATE SET
158
+ status = excluded.status,
159
+ context = excluded.context,
160
+ updated_at = excluded.updated_at
161
+ `).run(
162
+ this.task.id,
163
+ currentSessionId,
164
+ this.task.title,
165
+ this.task.status,
166
+ JSON.stringify(this.task.context),
167
+ new Date(this.task.created_at).getTime(),
168
+ new Date(this.task.updated_at).getTime()
169
+ );
170
+ db.getDb().prepare("DELETE FROM subtasks WHERE task_id = ?").run(this.task.id);
171
+ const stmt = db.getDb().prepare("INSERT INTO subtasks (id, task_id, text, done, position) VALUES (?, ?, ?, ?, ?)");
172
+ this.task.subtasks.forEach((st, idx) => {
173
+ stmt.run(
174
+ this.task.id + "_" + idx,
175
+ // Simple deterministic ID
176
+ this.task.id,
177
+ st.text,
178
+ st.done ? 1 : 0,
179
+ idx
180
+ );
181
+ });
182
+ });
183
+ updateSubtasks();
184
+ bus.emitAgent({ type: "task_update", task: this.task });
185
+ } catch (e) {
186
+ bus.emitAgent({ type: "error", message: `[Tasks] Failed to save: ${e instanceof Error ? e.message : String(e)}` });
187
+ }
188
+ }
189
+ async archive() {
190
+ }
191
+ // Task management
192
+ async create(title) {
193
+ const now = (/* @__PURE__ */ new Date()).toISOString();
194
+ this.task = {
195
+ id: Date.now().toString(36),
196
+ // Use simple ID, or UUID?
197
+ title,
198
+ status: "in_progress",
199
+ subtasks: [],
200
+ context: [],
201
+ created_at: now,
202
+ updated_at: now
203
+ };
204
+ await this.save();
205
+ return this.task;
206
+ }
207
+ async addSubtask(text) {
208
+ if (!this.task) return;
209
+ this.task.subtasks.push({ text, done: false });
210
+ this.task.updated_at = (/* @__PURE__ */ new Date()).toISOString();
211
+ await this.save();
212
+ }
213
+ async completeSubtask(index) {
214
+ if (!this.task || index >= this.task.subtasks.length) return;
215
+ this.task.subtasks[index].done = true;
216
+ this.task.updated_at = (/* @__PURE__ */ new Date()).toISOString();
217
+ await this.save();
218
+ }
219
+ async setStatus(status) {
220
+ if (!this.task) return;
221
+ this.task.status = status;
222
+ this.task.updated_at = (/* @__PURE__ */ new Date()).toISOString();
223
+ await this.save();
224
+ }
225
+ async addContext(ctx) {
226
+ if (!this.task) return;
227
+ if (!this.task.context.includes(ctx)) {
228
+ this.task.context.push(ctx);
229
+ await this.save();
230
+ }
231
+ }
232
+ async complete() {
233
+ if (!this.task) return;
234
+ this.task.status = "done";
235
+ for (const st of this.task.subtasks) {
236
+ st.done = true;
237
+ }
238
+ await this.save();
239
+ }
240
+ async clear() {
241
+ this.task = null;
242
+ bus.emitAgent({ type: "task_update", task: null });
243
+ }
244
+ // Getters
245
+ get() {
246
+ return this.task ? { ...this.task } : null;
247
+ }
248
+ getProgress() {
249
+ if (!this.task) return "No active task";
250
+ const done = this.task.subtasks.filter((s) => s.done).length;
251
+ const total = this.task.subtasks.length;
252
+ return `${this.task.title} [${done}/${total}]`;
253
+ }
254
+ hasActiveTask() {
255
+ return this.task !== null && this.task.status !== "done";
256
+ }
257
+ };
258
+ var tasks = new TaskTracker();
259
+
260
+ // src/core/usage.ts
261
+ import { z } from "zod";
262
+ var UsageSchema = z.object({
263
+ totalSessions: z.number().default(0),
264
+ totalRequests: z.number().default(0),
265
+ totalInputTokens: z.number().default(0),
266
+ totalOutputTokens: z.number().default(0),
267
+ totalCacheReadTokens: z.number().default(0),
268
+ totalCacheCreationTokens: z.number().default(0),
269
+ totalCost: z.number().default(0)
270
+ });
271
+ var MODEL_PRICES = {
272
+ // Claude 4.5 Family (2025-2026)
273
+ "claude-sonnet-4-5-20250929": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
274
+ "claude-haiku-4-5-20251001": { input: 1, output: 5, cacheRead: 0.1, cacheWrite: 1.25 },
275
+ "claude-opus-4-5-20251101": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
276
+ // Claude 3.5 Family
277
+ "claude-3-5-sonnet": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
278
+ "claude-3-5-haiku": { input: 0.25, output: 1.25, cacheRead: 0.03, cacheWrite: 0.3 },
279
+ "claude-3-haiku": { input: 0.25, output: 1.25, cacheRead: 0.03, cacheWrite: 0.3 },
280
+ "claude-3-opus": { input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75 },
281
+ "claude-3-sonnet": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 }
282
+ };
283
+ var CONTEXT_WINDOW_SIZES = {
284
+ "claude-sonnet-4-5-20250929": 2e5,
285
+ "claude-haiku-4-5-20251001": 2e5,
286
+ "claude-opus-4-5-20251101": 2e5,
287
+ "claude-3-5-sonnet": 2e5,
288
+ "claude-3-5-haiku": 2e5
289
+ };
290
+ var UsageTracker = class {
291
+ // In-memory session stats (for quick access/display)
292
+ sessionCost = 0;
293
+ sessionInputTokens = 0;
294
+ sessionOutputTokens = 0;
295
+ sessionCacheReadTokens = 0;
296
+ sessionCacheCreationTokens = 0;
297
+ sessionDuration = 0;
298
+ lastContextSize = 0;
299
+ lastCacheRead = 0;
300
+ lastCacheCreation = 0;
301
+ constructor() {
302
+ }
303
+ async init() {
304
+ }
305
+ async track(model, input, output, cacheRead = 0, cacheCreation = 0, contextSize) {
306
+ let prices = MODEL_PRICES[model];
307
+ if (!prices) {
308
+ const key = Object.keys(MODEL_PRICES).find((k) => model.includes(k));
309
+ prices = key ? MODEL_PRICES[key] : { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
310
+ }
311
+ const costInput = input / 1e6 * prices.input;
312
+ const costOutput = output / 1e6 * prices.output;
313
+ const costCacheRead = cacheRead / 1e6 * prices.cacheRead;
314
+ const costCacheWrite = cacheCreation / 1e6 * prices.cacheWrite;
315
+ const totalReqCost = costInput + costOutput + costCacheRead + costCacheWrite;
316
+ try {
317
+ const currentSessionId = context.get().session_id;
318
+ db.getDb().prepare(`
319
+ INSERT INTO usage_stats (session_id, model, input_tokens, output_tokens, cache_read_tokens, cache_creation_tokens, cost, timestamp)
320
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
321
+ `).run(currentSessionId, model, input, output, cacheRead, cacheCreation, totalReqCost, Date.now());
322
+ } catch (e) {
323
+ console.error("Failed to log usage to DB:", e);
324
+ }
325
+ this.sessionCost += totalReqCost;
326
+ this.sessionInputTokens += input;
327
+ this.sessionOutputTokens += output;
328
+ this.sessionCacheReadTokens += cacheRead;
329
+ this.sessionCacheCreationTokens += cacheCreation;
330
+ this.lastContextSize = contextSize !== void 0 ? contextSize : input + cacheRead;
331
+ this.lastCacheRead = cacheRead;
332
+ this.lastCacheCreation = cacheCreation;
333
+ }
334
+ getSessionCost() {
335
+ return this.sessionCost;
336
+ }
337
+ getSessionTokens() {
338
+ return {
339
+ input: this.sessionInputTokens,
340
+ output: this.sessionOutputTokens,
341
+ cacheRead: this.sessionCacheReadTokens,
342
+ cacheCreation: this.sessionCacheCreationTokens,
343
+ total: this.sessionInputTokens + this.sessionOutputTokens + this.sessionCacheReadTokens + this.sessionCacheCreationTokens
344
+ };
345
+ }
346
+ addSessionDuration(ms) {
347
+ this.sessionDuration += ms;
348
+ }
349
+ getSessionDuration() {
350
+ return this.sessionDuration;
351
+ }
352
+ getContextUsage(model) {
353
+ let limit = 2e5;
354
+ if (CONTEXT_WINDOW_SIZES[model]) {
355
+ limit = CONTEXT_WINDOW_SIZES[model];
356
+ } else {
357
+ const key = Object.keys(CONTEXT_WINDOW_SIZES).find((k) => model.includes(k));
358
+ if (key) limit = CONTEXT_WINDOW_SIZES[key];
359
+ }
360
+ const used = this.lastContextSize;
361
+ const cached = this.lastCacheRead + this.lastCacheCreation;
362
+ const remaining = Math.max(0, limit - used);
363
+ const percentUsed = used / limit * 100;
364
+ return {
365
+ used,
366
+ cached,
367
+ limit,
368
+ remaining,
369
+ percentRemaining: 100 - percentUsed
370
+ };
371
+ }
372
+ async trackSession() {
373
+ }
374
+ getStats() {
375
+ try {
376
+ const result = db.getDb().prepare(`
377
+ SELECT
378
+ SUM(input_tokens) as totalInputTokens,
379
+ SUM(output_tokens) as totalOutputTokens,
380
+ COALESCE(SUM(cache_read_tokens), 0) as totalCacheReadTokens,
381
+ COALESCE(SUM(cache_creation_tokens), 0) as totalCacheCreationTokens,
382
+ SUM(cost) as totalCost,
383
+ COUNT(*) as totalRequests,
384
+ COUNT(DISTINCT session_id) as totalSessions
385
+ FROM usage_stats
386
+ `).get();
387
+ return {
388
+ totalSessions: result.totalSessions || 0,
389
+ totalRequests: result.totalRequests || 0,
390
+ totalInputTokens: result.totalInputTokens || 0,
391
+ totalOutputTokens: result.totalOutputTokens || 0,
392
+ totalCacheReadTokens: result.totalCacheReadTokens || 0,
393
+ totalCacheCreationTokens: result.totalCacheCreationTokens || 0,
394
+ totalCost: result.totalCost || 0
395
+ };
396
+ } catch (e) {
397
+ return UsageSchema.parse({});
398
+ }
399
+ }
400
+ /**
401
+ * Get cache hit rate for current session
402
+ */
403
+ getCacheHitRate() {
404
+ const totalCached = this.sessionCacheReadTokens;
405
+ const totalInput = this.sessionInputTokens + this.sessionCacheReadTokens + this.sessionCacheCreationTokens;
406
+ const hitRate = totalInput > 0 ? totalCached / totalInput * 100 : 0;
407
+ return { hitRate, totalCached, totalInput };
408
+ }
409
+ restoreSessionState(stats) {
410
+ this.sessionCost = stats.cost;
411
+ this.sessionInputTokens = stats.inputTokens;
412
+ this.sessionOutputTokens = stats.outputTokens;
413
+ this.sessionCacheReadTokens = stats.cacheReadTokens;
414
+ this.sessionCacheCreationTokens = stats.cacheCreationTokens;
415
+ this.sessionDuration = stats.duration;
416
+ this.lastContextSize = stats.inputTokens + stats.cacheReadTokens + stats.cacheCreationTokens;
417
+ this.lastCacheRead = stats.cacheReadTokens;
418
+ }
419
+ };
420
+ var usage = new UsageTracker();
421
+
422
+ // src/core/llm.ts
423
+ import Anthropic from "@anthropic-ai/sdk";
424
+
425
+ // src/core/tools.ts
426
+ import { exec as exec2 } from "child_process";
427
+ import { promisify as promisify2 } from "util";
428
+ import fs5 from "fs/promises";
429
+ import * as fsSync from "fs";
430
+ import path5 from "path";
431
+ import os4 from "os";
432
+
433
+ // src/core/auditor.ts
434
+ import path from "path";
435
+ import fs from "fs/promises";
436
+ var BLOCKED_PATTERNS = [
437
+ "rm -rf /",
438
+ "rm -fr /",
439
+ ":(){:|:&};:",
440
+ // Fork bomb
441
+ "> /dev/sda",
442
+ // Disk overwrite
443
+ "mkfs",
444
+ "dd if=",
445
+ "chmod -R 777",
446
+ ":(){ :|:& };:"
447
+ ];
448
+ var BLOCKED_REGEX_PATTERNS = [
449
+ /curl\s+[^\|]+\|\s*(sh|bash)/i,
450
+ // curl URL | sh/bash
451
+ /wget\s+[^\|]+\|\s*(sh|bash)/i,
452
+ // wget URL | sh/bash
453
+ /curl\s*\|\s*(sh|bash)/i,
454
+ // curl | sh/bash (direct)
455
+ /wget\s*\|\s*(sh|bash)/i
456
+ // wget | sh/bash (direct)
457
+ ];
458
+ var APPROVAL_PATTERNS = [
459
+ { pattern: "rm -rf", reason: "Recursive delete operation" },
460
+ { pattern: "rm -r", reason: "Recursive delete operation" },
461
+ { pattern: "git push --force", reason: "Force push to remote" },
462
+ { pattern: "git reset --hard", reason: "Hard reset (loses changes)" },
463
+ { pattern: "npm publish", reason: "Publishing to npm registry" },
464
+ { pattern: "docker rm", reason: "Removing Docker containers" },
465
+ { pattern: "DROP TABLE", reason: "SQL table deletion" },
466
+ { pattern: "DROP DATABASE", reason: "SQL database deletion" },
467
+ { pattern: "truncate", reason: "Truncating data" }
468
+ ];
469
+ var Auditor = class {
470
+ workspaceRoot;
471
+ constructor(root = process.cwd()) {
472
+ this.workspaceRoot = path.resolve(root);
473
+ }
474
+ setWorkspaceRoot(root) {
475
+ this.workspaceRoot = path.resolve(root);
476
+ }
477
+ async checkCommand(command) {
478
+ const s = await settings.load();
479
+ const lowerCommand = command.toLowerCase();
480
+ if (BLOCKED_PATTERNS.some((p) => command.includes(p))) {
481
+ return {
482
+ approved: false,
483
+ reason: "Detected destructive command pattern",
484
+ isCritical: true
485
+ };
486
+ }
487
+ if (BLOCKED_REGEX_PATTERNS.some((p) => p.test(command))) {
488
+ return {
489
+ approved: false,
490
+ reason: "Detected dangerous pipe-to-shell pattern",
491
+ isCritical: true
492
+ };
493
+ }
494
+ if (await settings.isDenied("bash", command)) {
495
+ return {
496
+ approved: false,
497
+ reason: "Command blocked by settings",
498
+ isCritical: false
499
+ };
500
+ }
501
+ if (s.mode === "safe") {
502
+ if (await settings.isSessionAuthorized("bash", command)) {
503
+ return {
504
+ approved: true,
505
+ autoApproved: true
506
+ };
507
+ }
508
+ } else {
509
+ if (await settings.isAllowed("bash", command)) {
510
+ return {
511
+ approved: true,
512
+ autoApproved: true
513
+ };
514
+ }
515
+ }
516
+ for (const { pattern, reason } of APPROVAL_PATTERNS) {
517
+ if (lowerCommand.includes(pattern.toLowerCase())) {
518
+ return {
519
+ approved: false,
520
+ requiresApproval: true,
521
+ reason
522
+ };
523
+ }
524
+ }
525
+ return { approved: true };
526
+ }
527
+ checkPath(filePath) {
528
+ try {
529
+ const resolved = path.resolve(this.workspaceRoot, filePath);
530
+ const relative = path.relative(this.workspaceRoot, resolved);
531
+ if (relative.startsWith("..") || path.isAbsolute(relative)) {
532
+ return { approved: false, reason: `Access denied: Path '${filePath}' is outside the workspace.`, isCritical: true };
533
+ }
534
+ const parts = relative.split(path.sep);
535
+ if (parts.some((p) => p.startsWith(".") && p !== ".obsidian" && p !== ".agent" && p !== ".claude")) {
536
+ }
537
+ return { approved: true };
538
+ } catch (error) {
539
+ return { approved: false, reason: `Invalid path: ${filePath}`, isCritical: false };
540
+ }
541
+ }
542
+ async checkFileEdit(filePath) {
543
+ const pathCheck = this.checkPath(filePath);
544
+ if (!pathCheck.approved) return pathCheck;
545
+ try {
546
+ await fs.access(filePath);
547
+ return { approved: true };
548
+ } catch {
549
+ return { approved: false, reason: `File not found: ${filePath}`, isCritical: false };
550
+ }
551
+ }
552
+ };
553
+ var auditor = new Auditor();
554
+
555
+ // src/core/sandbox.ts
556
+ import { exec } from "child_process";
557
+ import { promisify } from "util";
558
+ import os from "os";
559
+ var execAsync = promisify(exec);
560
+ var DEFAULT_SANDBOX_CONFIG = {
561
+ mode: "local",
562
+ allowedDomains: [
563
+ "*.github.com",
564
+ "*.npmjs.org",
565
+ "*.npmjs.com",
566
+ "api.anthropic.com",
567
+ "registry.npmjs.org"
568
+ ],
569
+ deniedDomains: [],
570
+ denyRead: [
571
+ "~/.ssh",
572
+ "~/.aws",
573
+ "~/.config/gcloud",
574
+ "~/.kube",
575
+ "~/.gnupg"
576
+ ],
577
+ allowWrite: [
578
+ ".",
579
+ // Current workspace
580
+ "/tmp"
581
+ ],
582
+ denyWrite: [
583
+ ".env",
584
+ ".env.*",
585
+ "*.key",
586
+ "*.pem",
587
+ ".git/config"
588
+ ]
589
+ };
590
+ var SandboxExecutor = class {
591
+ initialized = false;
592
+ mode = "local";
593
+ sandboxManager = null;
594
+ config = DEFAULT_SANDBOX_CONFIG;
595
+ /**
596
+ * Initialize sandbox with configuration
597
+ */
598
+ async initialize() {
599
+ try {
600
+ const cfg = await config.load();
601
+ const s = await settings.load();
602
+ if (cfg.executionMode === "sandbox" || s.security.sandbox) {
603
+ this.mode = "sandbox";
604
+ } else {
605
+ this.mode = "local";
606
+ }
607
+ this.config = {
608
+ ...DEFAULT_SANDBOX_CONFIG,
609
+ ...cfg.sandbox || {}
610
+ };
611
+ if (this.mode !== "sandbox") {
612
+ this.initialized = true;
613
+ return true;
614
+ }
615
+ try {
616
+ const { SandboxManager } = await import("@anthropic-ai/sandbox-runtime");
617
+ const runtimeConfig = {
618
+ network: {
619
+ allowedDomains: this.config.allowedDomains,
620
+ deniedDomains: this.config.deniedDomains
621
+ },
622
+ filesystem: {
623
+ denyRead: this.config.denyRead,
624
+ allowWrite: this.config.allowWrite,
625
+ denyWrite: this.config.denyWrite
626
+ }
627
+ };
628
+ await SandboxManager.initialize(runtimeConfig);
629
+ this.sandboxManager = SandboxManager;
630
+ this.initialized = true;
631
+ bus.emitAgent({
632
+ type: "thought",
633
+ content: "[SANDBOX] Initialized with OS-level isolation"
634
+ });
635
+ return true;
636
+ } catch (importError) {
637
+ bus.emitAgent({
638
+ type: "thought",
639
+ content: `[SANDBOX] Runtime library unavailable (Error: ${importError.message}). Falling back to native OS sandbox.`
640
+ });
641
+ this.sandboxManager = null;
642
+ this.initialized = true;
643
+ return true;
644
+ }
645
+ } catch (error) {
646
+ bus.emitAgent({
647
+ type: "error",
648
+ message: `Sandbox initialization failed: ${error.message}`
649
+ });
650
+ return false;
651
+ }
652
+ }
653
+ /**
654
+ * Wrap a command with sandbox protection
655
+ */
656
+ async wrapCommand(command, bypass = false) {
657
+ if (!this.initialized) {
658
+ await this.initialize();
659
+ }
660
+ if (this.mode === "local" || bypass) {
661
+ return command;
662
+ }
663
+ if (this.sandboxManager) {
664
+ try {
665
+ return await this.sandboxManager.wrapWithSandbox(command);
666
+ } catch (error) {
667
+ }
668
+ }
669
+ return this.wrapWithNativeSandbox(command);
670
+ }
671
+ /**
672
+ * Wrap command with native OS sandbox (macOS sandbox-exec or Linux firejail)
673
+ */
674
+ async wrapWithNativeSandbox(command) {
675
+ const platform = os.platform();
676
+ const cfg = await config.load();
677
+ if (platform === "darwin") {
678
+ const profile = `(version 1)
679
+ (deny default)
680
+ (allow process-exec*)
681
+ (allow process-fork)
682
+ (allow signal)
683
+ (allow syscall-unix)
684
+ (allow sysctl-read)
685
+ (deny file-read* (subpath "${os.homedir()}"))
686
+ (allow file-read* (subpath "${cfg.workspaceRoot}"))
687
+ (allow file-read* (subpath "/tmp"))
688
+ (allow file-read* (subpath "/usr"))
689
+ (allow file-read* (subpath "/bin"))
690
+ (allow file-read* (subpath "/sbin"))
691
+ (allow file-read* (subpath "/lib"))
692
+ (allow file-read* (subpath "/private/var/run"))
693
+ (allow file-read* (subpath "/Library/Preferences"))
694
+ (allow file-read* (subpath "/dev"))
695
+ (allow file-write* (subpath "/tmp"))
696
+ (allow file-write* (subpath "${cfg.workspaceRoot}"))
697
+ (deny file-write* (literal "${cfg.workspaceRoot}/.env"))
698
+ (allow network-outbound (remote tcp "*:80" "*:443"))
699
+ (allow network*)
700
+ (allow mach-lookup*)
701
+ (allow iokit*)
702
+ `.trim();
703
+ const profilePath = `/tmp/obsidian-sandbox-${Date.now()}.sb`;
704
+ const fs6 = await import("fs/promises");
705
+ await fs6.writeFile(profilePath, profile);
706
+ const escapedCommand = command.replace(/'/g, "'\\''");
707
+ return `sandbox-exec -f "${profilePath}" bash -c '${escapedCommand}'`;
708
+ }
709
+ if (platform === "linux") {
710
+ try {
711
+ await execAsync("which firejail");
712
+ const escapedCommand = command.replace(/'/g, "'\\''");
713
+ return `firejail --net=none --blacklist=~/.ssh --blacklist=~/.aws --blacklist=~/.gnupg bash -c '${escapedCommand}'`;
714
+ } catch {
715
+ }
716
+ }
717
+ return command;
718
+ }
719
+ /**
720
+ * Get current execution mode
721
+ */
722
+ getMode() {
723
+ return this.mode;
724
+ }
725
+ /**
726
+ * Set execution mode
727
+ */
728
+ async setMode(mode) {
729
+ this.mode = mode;
730
+ if (mode === "sandbox" && !this.sandboxManager) {
731
+ await this.initialize();
732
+ }
733
+ bus.emitAgent({
734
+ type: "thought",
735
+ content: `[sandbox] Execution mode set to: ${mode}`
736
+ });
737
+ }
738
+ /**
739
+ * Get current sandbox configuration
740
+ */
741
+ getConfig() {
742
+ return { ...this.config };
743
+ }
744
+ /**
745
+ * Update sandbox configuration
746
+ */
747
+ updateConfig(updates) {
748
+ this.config = { ...this.config, ...updates };
749
+ }
750
+ /**
751
+ * Reset and cleanup sandbox resources
752
+ */
753
+ async reset() {
754
+ if (this.sandboxManager) {
755
+ try {
756
+ await this.sandboxManager.reset();
757
+ } catch {
758
+ }
759
+ }
760
+ this.initialized = false;
761
+ this.sandboxManager = null;
762
+ }
763
+ /**
764
+ * Check if sandbox mode is available on this system
765
+ */
766
+ async isAvailable() {
767
+ try {
768
+ await import("@anthropic-ai/sandbox-runtime");
769
+ return true;
770
+ } catch {
771
+ return false;
772
+ }
773
+ }
774
+ };
775
+ var sandbox = new SandboxExecutor();
776
+
777
+ // src/core/undo.ts
778
+ import fs2 from "fs/promises";
779
+ import path2 from "path";
780
+ var UndoManager = class {
781
+ sessionId = null;
782
+ changes = [];
783
+ // In-memory stack for fast access
784
+ async init(sessionId) {
785
+ this.sessionId = sessionId;
786
+ this.changes = [];
787
+ }
788
+ async recordChange(filePath, operation, beforeContent, afterContent) {
789
+ const id = `chg_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
790
+ const cfg = await config.load();
791
+ const change = {
792
+ id,
793
+ filePath: path2.relative(cfg.workspaceRoot, path2.resolve(filePath)),
794
+ operation,
795
+ beforeContent,
796
+ afterContent,
797
+ timestamp: /* @__PURE__ */ new Date(),
798
+ undone: false
799
+ };
800
+ this.changes.unshift(change);
801
+ if (this.changes.length > 100) {
802
+ this.changes = this.changes.slice(0, 100);
803
+ }
804
+ return id;
805
+ }
806
+ async undo(count = 1) {
807
+ const toUndo = this.changes.filter((c) => !c.undone).slice(0, count);
808
+ if (toUndo.length === 0) {
809
+ return { success: false, message: "Nothing to undo" };
810
+ }
811
+ const results = [];
812
+ const cfg = await config.load();
813
+ for (const change of toUndo) {
814
+ try {
815
+ const fullPath = path2.resolve(cfg.workspaceRoot, change.filePath);
816
+ switch (change.operation) {
817
+ case "create":
818
+ await fs2.unlink(fullPath);
819
+ results.push(`Deleted: ${change.filePath}`);
820
+ break;
821
+ case "edit":
822
+ if (change.beforeContent !== null) {
823
+ await fs2.writeFile(fullPath, change.beforeContent, "utf-8");
824
+ results.push(`Restored: ${change.filePath}`);
825
+ }
826
+ break;
827
+ case "delete":
828
+ if (change.beforeContent !== null) {
829
+ await fs2.mkdir(path2.dirname(fullPath), { recursive: true });
830
+ await fs2.writeFile(fullPath, change.beforeContent, "utf-8");
831
+ results.push(`Restored: ${change.filePath}`);
832
+ }
833
+ break;
834
+ }
835
+ change.undone = true;
836
+ } catch (error) {
837
+ results.push(`Failed: ${change.filePath} - ${error.message}`);
838
+ }
839
+ }
840
+ return {
841
+ success: true,
842
+ message: results.join("\n")
843
+ };
844
+ }
845
+ getHistory(limit = 10) {
846
+ return this.changes.filter((c) => !c.undone).slice(0, limit);
847
+ }
848
+ async close() {
849
+ }
850
+ };
851
+ var undo = new UndoManager();
852
+
853
+ // src/core/diff.ts
854
+ import fs3 from "fs/promises";
855
+ import path3 from "path";
856
+ import os2 from "os";
857
+ var DIFF_DIR = ".obsidian-next/diffs";
858
+ var MAX_DIFF_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
859
+ var MAX_DIFFS = 100;
860
+ function generateDiff(oldContent, newContent, filePath) {
861
+ const oldLines = oldContent.split("\n");
862
+ const newLines = newContent.split("\n");
863
+ const hunks = computeHunks(oldLines, newLines);
864
+ if (hunks.length === 0) {
865
+ return "";
866
+ }
867
+ const header = [
868
+ `--- a/${filePath}`,
869
+ `+++ b/${filePath}`
870
+ ];
871
+ const diffLines = [...header];
872
+ for (const hunk of hunks) {
873
+ const hunkHeader = `@@ -${hunk.oldStart},${hunk.oldCount} +${hunk.newStart},${hunk.newCount} @@`;
874
+ diffLines.push(hunkHeader);
875
+ diffLines.push(...hunk.lines);
876
+ }
877
+ return diffLines.join("\n");
878
+ }
879
+ function computeHunks(oldLines, newLines) {
880
+ const hunks = [];
881
+ const lcs = computeLCS(oldLines, newLines);
882
+ let oldIdx = 0;
883
+ let newIdx = 0;
884
+ let lcsIdx = 0;
885
+ let currentHunk = null;
886
+ const contextLines = 3;
887
+ while (oldIdx < oldLines.length || newIdx < newLines.length) {
888
+ const lcsLine = lcsIdx < lcs.length ? lcs[lcsIdx] : null;
889
+ const oldMatches = lcsLine !== null && oldIdx < oldLines.length && oldLines[oldIdx] === lcsLine;
890
+ const newMatches = lcsLine !== null && newIdx < newLines.length && newLines[newIdx] === lcsLine;
891
+ if (oldMatches && newMatches) {
892
+ if (currentHunk) {
893
+ currentHunk.lines.push(` ${oldLines[oldIdx]}`);
894
+ currentHunk.oldCount++;
895
+ currentHunk.newCount++;
896
+ const remainingChanges = hasMoreChanges(oldLines, newLines, lcs, oldIdx + 1, newIdx + 1, lcsIdx + 1);
897
+ if (!remainingChanges || currentHunk.lines.length > 50) {
898
+ hunks.push(currentHunk);
899
+ currentHunk = null;
900
+ }
901
+ }
902
+ oldIdx++;
903
+ newIdx++;
904
+ lcsIdx++;
905
+ } else if (!oldMatches && oldIdx < oldLines.length && (lcsLine === null || oldLines[oldIdx] !== lcsLine)) {
906
+ if (!currentHunk) {
907
+ currentHunk = createHunk(oldIdx + 1, newIdx + 1);
908
+ }
909
+ currentHunk.lines.push(`-${oldLines[oldIdx]}`);
910
+ currentHunk.oldCount++;
911
+ oldIdx++;
912
+ } else if (!newMatches && newIdx < newLines.length && (lcsLine === null || newLines[newIdx] !== lcsLine)) {
913
+ if (!currentHunk) {
914
+ currentHunk = createHunk(oldIdx + 1, newIdx + 1);
915
+ }
916
+ currentHunk.lines.push(`+${newLines[newIdx]}`);
917
+ currentHunk.newCount++;
918
+ newIdx++;
919
+ } else {
920
+ if (oldIdx < oldLines.length) oldIdx++;
921
+ if (newIdx < newLines.length) newIdx++;
922
+ }
923
+ }
924
+ if (currentHunk && currentHunk.lines.length > 0) {
925
+ hunks.push(currentHunk);
926
+ }
927
+ return hunks;
928
+ }
929
+ function createHunk(oldStart, newStart) {
930
+ return {
931
+ oldStart,
932
+ oldCount: 0,
933
+ newStart,
934
+ newCount: 0,
935
+ lines: []
936
+ };
937
+ }
938
+ function hasMoreChanges(oldLines, newLines, lcs, oldIdx, newIdx, lcsIdx) {
939
+ const lookAhead = 6;
940
+ for (let i = 0; i < lookAhead; i++) {
941
+ const oi = oldIdx + i;
942
+ const ni = newIdx + i;
943
+ const li = lcsIdx + i;
944
+ if (oi >= oldLines.length && ni >= newLines.length) return false;
945
+ const lcsLine = li < lcs.length ? lcs[li] : null;
946
+ if (lcsLine === null) return oi < oldLines.length || ni < newLines.length;
947
+ if (oi < oldLines.length && oldLines[oi] !== lcsLine) return true;
948
+ if (ni < newLines.length && ni < newLines.length && newLines[ni] !== lcsLine) return true;
949
+ }
950
+ return false;
951
+ }
952
+ function computeLCS(a, b) {
953
+ const m = a.length;
954
+ const n = b.length;
955
+ if (m > 1e3 || n > 1e3) {
956
+ return simpleLCS(a, b);
957
+ }
958
+ const dp = Array(m + 1).fill(null).map(() => Array(n + 1).fill(0));
959
+ for (let i2 = 1; i2 <= m; i2++) {
960
+ for (let j2 = 1; j2 <= n; j2++) {
961
+ if (a[i2 - 1] === b[j2 - 1]) {
962
+ dp[i2][j2] = dp[i2 - 1][j2 - 1] + 1;
963
+ } else {
964
+ dp[i2][j2] = Math.max(dp[i2 - 1][j2], dp[i2][j2 - 1]);
965
+ }
966
+ }
967
+ }
968
+ const lcs = [];
969
+ let i = m, j = n;
970
+ while (i > 0 && j > 0) {
971
+ if (a[i - 1] === b[j - 1]) {
972
+ lcs.unshift(a[i - 1]);
973
+ i--;
974
+ j--;
975
+ } else if (dp[i - 1][j] > dp[i][j - 1]) {
976
+ i--;
977
+ } else {
978
+ j--;
979
+ }
980
+ }
981
+ return lcs;
982
+ }
983
+ function simpleLCS(a, b) {
984
+ const bSet = new Set(b);
985
+ return a.filter((line) => bSet.has(line));
986
+ }
987
+ function countChanges(diff) {
988
+ const lines = diff.split("\n");
989
+ let additions = 0;
990
+ let deletions = 0;
991
+ for (const line of lines) {
992
+ if (line.startsWith("+") && !line.startsWith("+++")) {
993
+ additions++;
994
+ } else if (line.startsWith("-") && !line.startsWith("---")) {
995
+ deletions++;
996
+ }
997
+ }
998
+ return { additions, deletions };
999
+ }
1000
+ var DiffManager = class {
1001
+ diffDir = null;
1002
+ constructor() {
1003
+ }
1004
+ async getDiffDir() {
1005
+ if (this.diffDir) return this.diffDir;
1006
+ this.diffDir = path3.join(os2.homedir(), DIFF_DIR);
1007
+ return this.diffDir;
1008
+ }
1009
+ /**
1010
+ * Save a diff to storage
1011
+ */
1012
+ async saveDiff(filePath, oldContent, newContent) {
1013
+ const diff = generateDiff(oldContent, newContent, filePath);
1014
+ if (!diff) {
1015
+ return null;
1016
+ }
1017
+ const diffDir = await this.getDiffDir();
1018
+ await fs3.mkdir(diffDir, { recursive: true });
1019
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
1020
+ const { additions, deletions } = countChanges(diff);
1021
+ const entry = {
1022
+ timestamp,
1023
+ filePath,
1024
+ beforeLines: oldContent.split("\n").length,
1025
+ afterLines: newContent.split("\n").length,
1026
+ additions,
1027
+ deletions,
1028
+ diff
1029
+ };
1030
+ const sanitizedPath = filePath.replace(/[/\\]/g, "_").replace(/[^a-zA-Z0-9_.-]/g, "");
1031
+ const filename = `${Date.now()}_${sanitizedPath}.diff.json`;
1032
+ await fs3.writeFile(
1033
+ path3.join(diffDir, filename),
1034
+ JSON.stringify(entry, null, 2)
1035
+ );
1036
+ await this.cleanup();
1037
+ return entry;
1038
+ }
1039
+ /**
1040
+ * List recent diffs
1041
+ */
1042
+ async listDiffs(limit = 20) {
1043
+ try {
1044
+ const diffDir = await this.getDiffDir();
1045
+ await fs3.mkdir(diffDir, { recursive: true });
1046
+ const files = await fs3.readdir(diffDir);
1047
+ const diffs = [];
1048
+ for (const file of files.slice(-limit * 2)) {
1049
+ if (!file.endsWith(".diff.json")) continue;
1050
+ try {
1051
+ const content = await fs3.readFile(
1052
+ path3.join(diffDir, file),
1053
+ "utf-8"
1054
+ );
1055
+ diffs.push(JSON.parse(content));
1056
+ } catch {
1057
+ }
1058
+ }
1059
+ return diffs.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()).slice(0, limit);
1060
+ } catch {
1061
+ return [];
1062
+ }
1063
+ }
1064
+ /**
1065
+ * Get diff for a specific file
1066
+ */
1067
+ async getDiffForFile(filePath) {
1068
+ const diffs = await this.listDiffs(100);
1069
+ return diffs.find((d) => d.filePath === filePath) || null;
1070
+ }
1071
+ /**
1072
+ * Cleanup old diffs
1073
+ */
1074
+ async cleanup() {
1075
+ try {
1076
+ const diffDir = await this.getDiffDir();
1077
+ const files = await fs3.readdir(diffDir);
1078
+ const now = Date.now();
1079
+ const validFiles = [];
1080
+ for (const file of files) {
1081
+ if (!file.endsWith(".diff.json")) continue;
1082
+ const match = file.match(/^(\d+)_/);
1083
+ if (match) {
1084
+ const time = parseInt(match[1], 10);
1085
+ if (now - time > MAX_DIFF_AGE_MS) {
1086
+ await fs3.unlink(path3.join(diffDir, file));
1087
+ } else {
1088
+ validFiles.push({ name: file, time });
1089
+ }
1090
+ }
1091
+ }
1092
+ if (validFiles.length > MAX_DIFFS) {
1093
+ validFiles.sort((a, b) => a.time - b.time);
1094
+ const toDelete = validFiles.slice(0, validFiles.length - MAX_DIFFS);
1095
+ for (const { name } of toDelete) {
1096
+ await fs3.unlink(path3.join(diffDir, name));
1097
+ }
1098
+ }
1099
+ } catch {
1100
+ }
1101
+ }
1102
+ /**
1103
+ * Clear all diffs
1104
+ */
1105
+ async clearAll() {
1106
+ try {
1107
+ const diffDir = await this.getDiffDir();
1108
+ const files = await fs3.readdir(diffDir);
1109
+ for (const file of files) {
1110
+ if (file.endsWith(".diff.json")) {
1111
+ await fs3.unlink(path3.join(diffDir, file));
1112
+ }
1113
+ }
1114
+ } catch {
1115
+ }
1116
+ }
1117
+ };
1118
+ var diffManager = new DiffManager();
1119
+
1120
+ // src/core/mcp.ts
1121
+ import path4 from "path";
1122
+ import fs4 from "fs/promises";
1123
+ import os3 from "os";
1124
+ import { z as z2 } from "zod";
1125
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
1126
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
1127
+ import { CallToolResultSchema, ListToolsResultSchema } from "@modelcontextprotocol/sdk/types.js";
1128
+ var MCPServerConfigSchema = z2.object({
1129
+ command: z2.string(),
1130
+ args: z2.array(z2.string()).default([]),
1131
+ env: z2.record(z2.string()).optional(),
1132
+ secureEnv: z2.array(z2.string()).optional(),
1133
+ // List of env vars to load from KeyManager
1134
+ autoConnect: z2.boolean().default(false)
1135
+ });
1136
+ var MCPConfigSchema = z2.object({
1137
+ servers: z2.record(MCPServerConfigSchema).default({})
1138
+ });
1139
+ var MCPManager = class {
1140
+ connections = /* @__PURE__ */ new Map();
1141
+ config = { servers: {} };
1142
+ constructor() {
1143
+ }
1144
+ /**
1145
+ * Load configuration from .obsidian-next/mcp.json
1146
+ */
1147
+ async init() {
1148
+ try {
1149
+ const configPath = path4.join(os3.homedir(), ".obsidian-next", "mcp.json");
1150
+ const exists = await fs4.stat(configPath).then(() => true).catch(() => false);
1151
+ if (exists) {
1152
+ const content = await fs4.readFile(configPath, "utf-8");
1153
+ this.config = MCPConfigSchema.parse(JSON.parse(content));
1154
+ } else {
1155
+ this.config = { servers: {} };
1156
+ }
1157
+ Object.entries(this.config.servers).forEach(([name, serverConfig]) => {
1158
+ if (serverConfig.autoConnect) {
1159
+ this.connect(name, serverConfig).catch((err) => {
1160
+ });
1161
+ }
1162
+ });
1163
+ } catch (error) {
1164
+ console.error("Failed to initialize MCP Manager:", error);
1165
+ }
1166
+ }
1167
+ /**
1168
+ * Connect to an MCP server
1169
+ */
1170
+ async connect(name, serverConfig) {
1171
+ if (this.connections.has(name)) {
1172
+ return;
1173
+ }
1174
+ try {
1175
+ const sanitizedEnv = serverConfig.env ? Object.fromEntries(
1176
+ Object.entries(serverConfig.env).map(([k, v]) => [k, v.trim()])
1177
+ ) : {};
1178
+ if (serverConfig.secureEnv) {
1179
+ for (const envName of serverConfig.secureEnv) {
1180
+ const key = await keyManager.loadKey({ service: "obsidian-mcp", account: envName });
1181
+ if (key) {
1182
+ sanitizedEnv[envName] = key;
1183
+ } else {
1184
+ console.warn(`[MCP] Missing secure key for ${envName} (server: ${name})`);
1185
+ }
1186
+ }
1187
+ }
1188
+ const transport = new StdioClientTransport({
1189
+ command: serverConfig.command,
1190
+ args: serverConfig.args,
1191
+ env: sanitizedEnv
1192
+ });
1193
+ const client = new Client(
1194
+ {
1195
+ name: "obsidian-next-client",
1196
+ version: "0.4.5"
1197
+ },
1198
+ {
1199
+ capabilities: {
1200
+ // Client does not provide tools, it consumes them
1201
+ }
1202
+ }
1203
+ );
1204
+ const connectTimeout = 1e4;
1205
+ await Promise.race([
1206
+ client.connect(transport),
1207
+ new Promise((_, reject) => setTimeout(() => reject(new Error("Connection timeout")), connectTimeout))
1208
+ ]);
1209
+ this.connections.set(name, {
1210
+ client,
1211
+ transport,
1212
+ capabilities: client.getServerCapabilities()
1213
+ });
1214
+ if (this.config.servers[name] && !this.config.servers[name].autoConnect) {
1215
+ this.config.servers[name].autoConnect = true;
1216
+ await this.saveConfig();
1217
+ }
1218
+ } catch (error) {
1219
+ throw new Error(`Failed to connect to ${name}: ${error}`);
1220
+ }
1221
+ }
1222
+ /**
1223
+ * Add and connect to a new server, then persist config
1224
+ * Set skipConnect to true if the server requires manual config (API keys) before it can start
1225
+ */
1226
+ async addServer(name, serverConfig, skipConnect = false) {
1227
+ if (this.config.servers[name]) {
1228
+ throw new Error(`Server '${name}' already exists`);
1229
+ }
1230
+ this.config.servers[name] = serverConfig;
1231
+ try {
1232
+ if (!skipConnect) {
1233
+ await this.connect(name, serverConfig);
1234
+ }
1235
+ await this.saveConfig();
1236
+ } catch (error) {
1237
+ delete this.config.servers[name];
1238
+ throw error;
1239
+ }
1240
+ }
1241
+ /**
1242
+ * Remove a server and persist config
1243
+ */
1244
+ async removeServer(name) {
1245
+ if (!this.config.servers[name]) {
1246
+ throw new Error(`Server '${name}' not found`);
1247
+ }
1248
+ await this.disconnect(name);
1249
+ delete this.config.servers[name];
1250
+ await this.saveConfig();
1251
+ }
1252
+ /**
1253
+ * Update an existing server configuration and persist
1254
+ */
1255
+ async updateServer(name, updates) {
1256
+ const existing = this.config.servers[name];
1257
+ if (!existing) {
1258
+ throw new Error(`Server '${name}' not found`);
1259
+ }
1260
+ this.config.servers[name] = {
1261
+ ...existing,
1262
+ ...updates,
1263
+ env: {
1264
+ ...existing.env || {},
1265
+ ...updates.env || {}
1266
+ }
1267
+ };
1268
+ await this.saveConfig();
1269
+ if (this.connections.has(name)) {
1270
+ await this.disconnect(name);
1271
+ await this.connect(name, this.config.servers[name]);
1272
+ }
1273
+ }
1274
+ /**
1275
+ * Persist current config to disk
1276
+ */
1277
+ async saveConfig() {
1278
+ const configPath = path4.join(os3.homedir(), ".obsidian-next", "mcp.json");
1279
+ await fs4.mkdir(path4.dirname(configPath), { recursive: true });
1280
+ await fs4.writeFile(configPath, JSON.stringify(this.config, null, 2), "utf-8");
1281
+ }
1282
+ /**
1283
+ * Disconnect from a server
1284
+ */
1285
+ async disconnect(name) {
1286
+ const conn = this.connections.get(name);
1287
+ if (conn) {
1288
+ try {
1289
+ const disconnectTimeout = 2e3;
1290
+ await Promise.race([
1291
+ conn.client.close(),
1292
+ new Promise((_, reject) => setTimeout(() => reject(new Error("Disconnect timeout")), disconnectTimeout))
1293
+ ]);
1294
+ } catch (error) {
1295
+ console.warn(`Disconnect from ${name} timed out or failed:`, error);
1296
+ } finally {
1297
+ try {
1298
+ const transport = conn.transport;
1299
+ if (transport._process && typeof transport._process.kill === "function") {
1300
+ transport._process.kill("SIGKILL");
1301
+ }
1302
+ } catch (e) {
1303
+ }
1304
+ this.connections.delete(name);
1305
+ if (this.config.servers[name] && this.config.servers[name].autoConnect) {
1306
+ this.config.servers[name].autoConnect = false;
1307
+ await this.saveConfig();
1308
+ }
1309
+ }
1310
+ }
1311
+ }
1312
+ /**
1313
+ * List all tools from all connected servers
1314
+ */
1315
+ async listTools() {
1316
+ const allTools = [];
1317
+ for (const [serverName, conn] of this.connections) {
1318
+ try {
1319
+ const timeoutMs = 5e3;
1320
+ const timeoutPromise = new Promise(
1321
+ (_, reject) => setTimeout(() => reject(new Error("Timeout listing tools")), timeoutMs)
1322
+ );
1323
+ const result = await Promise.race([
1324
+ conn.client.request(
1325
+ { method: "tools/list" },
1326
+ // Raw request or use helper if available
1327
+ ListToolsResultSchema
1328
+ ),
1329
+ timeoutPromise
1330
+ ]);
1331
+ if (result && result.tools) {
1332
+ const serverTools = result.tools.map((t) => ({
1333
+ ...t,
1334
+ server: serverName
1335
+ // Attach server name for routing
1336
+ // If we wanted to rename: name: `${serverName}__${t.name}`
1337
+ }));
1338
+ allTools.push(...serverTools);
1339
+ }
1340
+ } catch (error) {
1341
+ console.error(`Failed to list tools from ${serverName}:`, error);
1342
+ }
1343
+ }
1344
+ return allTools;
1345
+ }
1346
+ /**
1347
+ * Call a specific tool on a server
1348
+ */
1349
+ async callTool(serverName, toolName, args) {
1350
+ const conn = this.connections.get(serverName);
1351
+ if (!conn) {
1352
+ throw new Error(`Server '${serverName}' not connected.`);
1353
+ }
1354
+ try {
1355
+ const result = await conn.client.request(
1356
+ {
1357
+ method: "tools/call",
1358
+ params: {
1359
+ name: toolName,
1360
+ arguments: args
1361
+ }
1362
+ },
1363
+ CallToolResultSchema
1364
+ );
1365
+ return result;
1366
+ } catch (error) {
1367
+ throw new Error(`Tool execution failed on ${serverName}/${toolName}: ${error}`);
1368
+ }
1369
+ }
1370
+ /**
1371
+ * Get active connections status
1372
+ */
1373
+ getStatus() {
1374
+ return Object.keys(this.config.servers).map((name) => ({
1375
+ name,
1376
+ config: this.config.servers[name],
1377
+ connected: this.connections.has(name),
1378
+ capabilities: this.connections.get(name)?.capabilities
1379
+ }));
1380
+ }
1381
+ };
1382
+ var mcp = new MCPManager();
1383
+
1384
+ // src/core/mcp-registry.ts
1385
+ var MCP_REGISTRY = {
1386
+ "filesystem": {
1387
+ name: "filesystem",
1388
+ description: "Access local files and directories outside the workspace (requires permission)",
1389
+ command: "npx",
1390
+ args: ["-y", "@modelcontextprotocol/server-filesystem", "/"]
1391
+ // Root access, dangerous but powerful
1392
+ },
1393
+ "research": {
1394
+ name: "research",
1395
+ description: "Search the web using Brave Search. Excellent for docs and current info.",
1396
+ command: "npx",
1397
+ args: ["-y", "@modelcontextprotocol/server-brave-search"],
1398
+ requiresApiKey: true,
1399
+ env: {
1400
+ "BRAVE_API_KEY": "Needs Configuration"
1401
+ }
1402
+ },
1403
+ "git": {
1404
+ name: "git",
1405
+ description: "Git repository management operations",
1406
+ command: "npx",
1407
+ args: ["-y", "@modelcontextprotocol/server-git"]
1408
+ },
1409
+ "memory": {
1410
+ name: "memory",
1411
+ description: "Persistent knowledge graph for long-term agent memory",
1412
+ command: "npx",
1413
+ args: ["-y", "@modelcontextprotocol/server-memory"]
1414
+ },
1415
+ "google-search": {
1416
+ name: "google-search",
1417
+ description: "Search the web using official Google Custom Search API",
1418
+ command: "npx",
1419
+ args: ["-y", "@mcp-get/google-search"],
1420
+ requiresApiKey: true,
1421
+ env: {
1422
+ "GOOGLE_API_KEY": "Needs Key",
1423
+ "GOOGLE_CSE_ID": "Needs ID"
1424
+ }
1425
+ },
1426
+ "github": {
1427
+ name: "github",
1428
+ description: "Advanced GitHub repository management (Issues, PRs, etc)",
1429
+ command: "npx",
1430
+ args: ["-y", "@modelcontextprotocol/server-github"],
1431
+ requiresApiKey: true,
1432
+ env: {
1433
+ "GITHUB_PERSONAL_ACCESS_TOKEN": "Needs Token"
1434
+ }
1435
+ },
1436
+ "context7": {
1437
+ name: "context7",
1438
+ description: "Official documentation search and retrieval (Certified Correct Docs)",
1439
+ command: "npx",
1440
+ args: ["-y", "@upstash/context7-mcp"],
1441
+ requiresApiKey: true,
1442
+ env: {
1443
+ "CONTEXT7_API_KEY": "Needs Key"
1444
+ }
1445
+ }
1446
+ };
1447
+ function getRegistryDefinition(name) {
1448
+ return MCP_REGISTRY[name];
1449
+ }
1450
+ function listRegistry() {
1451
+ return Object.values(MCP_REGISTRY);
1452
+ }
1453
+
1454
+ // src/core/lane.ts
1455
+ var Lane = class {
1456
+ queue = [];
1457
+ running = false;
1458
+ /**
1459
+ * Enqueue a task for serial execution.
1460
+ * Returns a promise that resolves with the task's result.
1461
+ */
1462
+ async enqueue(execute) {
1463
+ return new Promise((resolve, reject) => {
1464
+ this.queue.push({ execute, resolve, reject });
1465
+ this.drain();
1466
+ });
1467
+ }
1468
+ async drain() {
1469
+ if (this.running) return;
1470
+ this.running = true;
1471
+ while (this.queue.length > 0) {
1472
+ const task = this.queue.shift();
1473
+ try {
1474
+ const result = await task.execute();
1475
+ task.resolve(result);
1476
+ } catch (error) {
1477
+ task.reject(error);
1478
+ }
1479
+ }
1480
+ this.running = false;
1481
+ }
1482
+ get pending() {
1483
+ return this.queue.length;
1484
+ }
1485
+ get isRunning() {
1486
+ return this.running;
1487
+ }
1488
+ };
1489
+ var toolLane = new Lane();
1490
+
1491
+ // src/core/tools.ts
1492
+ var execAsync2 = promisify2(exec2);
1493
+ var lastScreenshotScale = 1;
1494
+ var MAX_OUTPUT_LENGTH = 1e4;
1495
+ var MAX_FILE_READ_LINES = 500;
1496
+ var IGNORED_DIRS = ["node_modules", ".git", "dist", ".next", "__pycache__", ".cache", "coverage"];
1497
+ var APPROVAL_TIMEOUT = 3e4;
1498
+ function truncateOutput(output, maxLength = MAX_OUTPUT_LENGTH) {
1499
+ if (output.length <= maxLength) return output;
1500
+ const truncated = output.slice(0, maxLength);
1501
+ const remaining = output.length - maxLength;
1502
+ return `${truncated}
1503
+
1504
+ ... [TRUNCATED: ${remaining} more characters]`;
1505
+ }
1506
+ function filterSystemNoise(stderr) {
1507
+ if (!stderr) return stderr;
1508
+ const noisePatterns = [
1509
+ /^aks:aks_get_lock_state:\d+:\d+: aks connection failed\s*/gm,
1510
+ // macOS keychain noise
1511
+ /^objc\[\d+\]: .* may have been in progress in another thread.*$/gm,
1512
+ // Objective-C runtime
1513
+ /^Warning: .* is deprecated.*$/gm,
1514
+ // Deprecation warnings
1515
+ /^\[warn\].*$/gmi,
1516
+ // Generic warn prefixes
1517
+ /^MESA-LOADER:.*$/gm,
1518
+ // Mesa graphics loader
1519
+ /^libEGL warning:.*$/gm,
1520
+ // EGL warnings
1521
+ /^Fontconfig warning:.*$/gm
1522
+ // Font config
1523
+ ];
1524
+ let filtered = stderr;
1525
+ for (const pattern of noisePatterns) {
1526
+ filtered = filtered.replace(pattern, "");
1527
+ }
1528
+ filtered = filtered.replace(/^\s*[\r\n]/gm, "").trim();
1529
+ return filtered;
1530
+ }
1531
+ var pendingApprovals = /* @__PURE__ */ new Map();
1532
+ bus.on("user", (event) => {
1533
+ if (event.type === "approval_response") {
1534
+ const pending = pendingApprovals.get(event.requestId);
1535
+ if (pending) {
1536
+ clearTimeout(pending.timeout);
1537
+ pendingApprovals.delete(event.requestId);
1538
+ pending.resolve({ approved: event.approved, scope: event.scope, bypass: event.bypass });
1539
+ }
1540
+ }
1541
+ });
1542
+ async function requestApproval(command, reason) {
1543
+ const requestId = `approval_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
1544
+ return new Promise((resolve) => {
1545
+ const timeout = setTimeout(() => {
1546
+ pendingApprovals.delete(requestId);
1547
+ bus.emitAgent({
1548
+ type: "error",
1549
+ message: "No response received. Command blocked for safety."
1550
+ });
1551
+ resolve({ approved: false, scope: "session" });
1552
+ }, APPROVAL_TIMEOUT);
1553
+ pendingApprovals.set(requestId, { resolve, timeout });
1554
+ const context2 = [
1555
+ `Command: ${command}`,
1556
+ `Reason: ${reason}`
1557
+ ].join("\n");
1558
+ bus.emitAgent({
1559
+ type: "approval_request",
1560
+ requestId,
1561
+ context: context2
1562
+ });
1563
+ });
1564
+ }
1565
+ var BashTool = {
1566
+ name: "bash",
1567
+ description: "Execute shell commands in the workspace",
1568
+ inputSchema: {
1569
+ command: {
1570
+ type: "string",
1571
+ description: "The shell command to execute"
1572
+ }
1573
+ },
1574
+ requiredParams: ["command"],
1575
+ async execute(args) {
1576
+ const command = args.command;
1577
+ if (!command) {
1578
+ return { success: false, error: "No command provided" };
1579
+ }
1580
+ const audit = await auditor.checkCommand(command);
1581
+ if (!audit.approved && audit.isCritical) {
1582
+ await auditLog.logSecurityViolation(command, audit.reason || "Critical security violation");
1583
+ return {
1584
+ success: false,
1585
+ error: `Security violation: ${audit.reason}`
1586
+ };
1587
+ }
1588
+ if (!audit.approved && audit.requiresApproval) {
1589
+ await auditLog.logApproval("requested", command, audit.reason);
1590
+ const { approved, scope, bypass } = await requestApproval(command, audit.reason || "Potentially dangerous operation");
1591
+ if (approved) {
1592
+ if (scope === "persistent") {
1593
+ if (bypass) {
1594
+ await settings.addUnsandboxedPermission("bash", command);
1595
+ } else {
1596
+ await settings.addAllowedPermission("bash", command);
1597
+ }
1598
+ } else {
1599
+ await settings.addSessionPermission("bash", command, true, bypass);
1600
+ }
1601
+ await auditLog.logApproval("granted", command, bypass ? "Bypass enabled" : void 0);
1602
+ } else {
1603
+ if (scope === "persistent") {
1604
+ await settings.addDeniedPermission("bash", command);
1605
+ } else {
1606
+ await settings.addSessionPermission("bash", command, false);
1607
+ }
1608
+ await auditLog.logApproval("denied", command);
1609
+ return {
1610
+ success: false,
1611
+ error: "Command rejected by user"
1612
+ };
1613
+ }
1614
+ }
1615
+ const bypassSandbox = await settings.isUnsandboxed("bash", command);
1616
+ const cfg = await config.load();
1617
+ try {
1618
+ const execCommand = await sandbox.wrapCommand(command, bypassSandbox);
1619
+ const { stdout, stderr } = await execAsync2(execCommand, {
1620
+ cwd: cfg.workspaceRoot,
1621
+ timeout: 3e4,
1622
+ // 30 second timeout
1623
+ maxBuffer: 1024 * 1024
1624
+ // 1MB buffer (reduced from 10MB)
1625
+ });
1626
+ const filteredStderr = filterSystemNoise(stderr);
1627
+ const output = stdout || filteredStderr || "Command executed successfully";
1628
+ await auditLog.logCommand(command, true);
1629
+ return {
1630
+ success: true,
1631
+ output: truncateOutput(output)
1632
+ };
1633
+ } catch (error) {
1634
+ const msg = error instanceof Error ? error.message : "Command execution failed";
1635
+ await auditLog.logCommand(command, false, msg);
1636
+ return {
1637
+ success: false,
1638
+ error: msg
1639
+ };
1640
+ }
1641
+ }
1642
+ };
1643
+ var ReadTool = {
1644
+ name: "read",
1645
+ description: "Read file contents from the workspace",
1646
+ inputSchema: {
1647
+ path: {
1648
+ type: "string",
1649
+ description: "Path to the file to read (relative to workspace)"
1650
+ }
1651
+ },
1652
+ requiredParams: ["path"],
1653
+ async execute(args) {
1654
+ const filePath = args.path;
1655
+ if (!filePath) {
1656
+ return { success: false, error: "No file path provided" };
1657
+ }
1658
+ const pathCheck = auditor.checkPath(filePath);
1659
+ if (!pathCheck.approved) {
1660
+ return {
1661
+ success: false,
1662
+ error: pathCheck.reason
1663
+ };
1664
+ }
1665
+ if (IGNORED_DIRS.some((dir) => filePath.includes(`/${dir}/`) || filePath.startsWith(`${dir}/`))) {
1666
+ return {
1667
+ success: false,
1668
+ error: `Cannot read from ignored directory. Paths containing ${IGNORED_DIRS.join(", ")} are blocked to prevent context explosion.`
1669
+ };
1670
+ }
1671
+ try {
1672
+ const cfg = await config.load();
1673
+ const fullPath = path5.resolve(cfg.workspaceRoot, filePath);
1674
+ const content = await fs5.readFile(fullPath, "utf-8");
1675
+ const lines = content.split("\n");
1676
+ const limitedLines = lines.slice(0, MAX_FILE_READ_LINES);
1677
+ const numbered = limitedLines.map((line, i) => `${String(i + 1).padStart(4)} | ${line}`).join("\n");
1678
+ const truncationNote = lines.length > MAX_FILE_READ_LINES ? `
1679
+
1680
+ ... [TRUNCATED: ${lines.length - MAX_FILE_READ_LINES} more lines. Use offset parameter to read more.]` : "";
1681
+ await context.trackRead(filePath);
1682
+ await auditLog.logFileOperation("read", filePath, true);
1683
+ return {
1684
+ success: true,
1685
+ output: truncateOutput(`File: ${filePath} (${lines.length} lines)
1686
+ ${"=".repeat(60)}
1687
+ ${numbered}${truncationNote}`)
1688
+ };
1689
+ } catch (error) {
1690
+ const msg = error instanceof Error ? error.message : String(error);
1691
+ await auditLog.logFileOperation("read", filePath, false, msg);
1692
+ return {
1693
+ success: false,
1694
+ error: `Failed to read file: ${msg}`
1695
+ };
1696
+ }
1697
+ }
1698
+ };
1699
+ var WriteTool = {
1700
+ name: "write",
1701
+ description: "Create new files in the workspace",
1702
+ inputSchema: {
1703
+ path: {
1704
+ type: "string",
1705
+ description: "Path where to create the new file"
1706
+ },
1707
+ content: {
1708
+ type: "string",
1709
+ description: "Content to write to the file"
1710
+ }
1711
+ },
1712
+ requiredParams: ["path", "content"],
1713
+ async execute(args) {
1714
+ const filePath = args.path;
1715
+ const content = args.content;
1716
+ if (!filePath) {
1717
+ return { success: false, error: "No file path provided" };
1718
+ }
1719
+ if (content === void 0) {
1720
+ return { success: false, error: "No content provided" };
1721
+ }
1722
+ const pathCheck = auditor.checkPath(filePath);
1723
+ if (!pathCheck.approved) {
1724
+ return {
1725
+ success: false,
1726
+ error: pathCheck.reason
1727
+ };
1728
+ }
1729
+ try {
1730
+ const cfg = await config.load();
1731
+ const fullPath = path5.resolve(cfg.workspaceRoot, filePath);
1732
+ try {
1733
+ await fs5.access(fullPath);
1734
+ return {
1735
+ success: false,
1736
+ error: `File already exists: ${filePath}. Use 'edit' tool to modify.`
1737
+ };
1738
+ } catch {
1739
+ }
1740
+ await fs5.mkdir(path5.dirname(fullPath), { recursive: true });
1741
+ await fs5.writeFile(fullPath, content, "utf-8");
1742
+ await context.trackModified(filePath);
1743
+ await undo.recordChange(filePath, "create", null, content);
1744
+ await auditLog.logFileOperation("write", filePath, true);
1745
+ await diffManager.saveDiff(filePath, "", content);
1746
+ return {
1747
+ success: true,
1748
+ output: `Created file: ${filePath} (${content.length} bytes)`
1749
+ };
1750
+ } catch (error) {
1751
+ const msg = error instanceof Error ? error.message : String(error);
1752
+ await auditLog.logFileOperation("write", filePath, false, msg);
1753
+ return {
1754
+ success: false,
1755
+ error: `Failed to write file: ${msg}`
1756
+ };
1757
+ }
1758
+ }
1759
+ };
1760
+ var EditTool = {
1761
+ name: "edit",
1762
+ description: "Edit existing files using search and replace",
1763
+ inputSchema: {
1764
+ path: {
1765
+ type: "string",
1766
+ description: "Path to the file to edit"
1767
+ },
1768
+ search: {
1769
+ type: "string",
1770
+ description: "Text to search for (must match exactly)"
1771
+ },
1772
+ replace: {
1773
+ type: "string",
1774
+ description: "Text to replace with"
1775
+ }
1776
+ },
1777
+ requiredParams: ["path", "search", "replace"],
1778
+ async execute(args) {
1779
+ const filePath = args.path;
1780
+ const search = args.search;
1781
+ const replace = args.replace;
1782
+ if (!filePath) {
1783
+ return { success: false, error: "No file path provided" };
1784
+ }
1785
+ if (!search) {
1786
+ return { success: false, error: "No search string provided" };
1787
+ }
1788
+ if (replace === void 0) {
1789
+ return { success: false, error: "No replacement string provided" };
1790
+ }
1791
+ const fileCheck = await auditor.checkFileEdit(filePath);
1792
+ if (!fileCheck.approved) {
1793
+ return {
1794
+ success: false,
1795
+ error: fileCheck.reason
1796
+ };
1797
+ }
1798
+ try {
1799
+ const cfg = await config.load();
1800
+ const fullPath = path5.resolve(cfg.workspaceRoot, filePath);
1801
+ const original = await fs5.readFile(fullPath, "utf-8");
1802
+ if (!original.includes(search)) {
1803
+ return {
1804
+ success: false,
1805
+ error: `Search string not found in ${filePath}`
1806
+ };
1807
+ }
1808
+ const occurrences = original.split(search).length - 1;
1809
+ const modified = original.replaceAll(search, replace);
1810
+ const diffPreview = generateDiffPreview(search, replace);
1811
+ await fs5.writeFile(fullPath, modified, "utf-8");
1812
+ const originalLines = original.split("\n").length;
1813
+ const modifiedLines = modified.split("\n").length;
1814
+ const delta = modifiedLines - originalLines;
1815
+ await context.trackModified(filePath);
1816
+ await undo.recordChange(filePath, "edit", original, modified);
1817
+ await auditLog.logFileOperation("edit", filePath, true);
1818
+ await diffManager.saveDiff(filePath, original, modified);
1819
+ const occurrenceText = occurrences > 1 ? ` (${occurrences} occurrences)` : "";
1820
+ return {
1821
+ success: true,
1822
+ output: `Edited ${filePath}${occurrenceText}:
1823
+ ${diffPreview}
1824
+ Lines: ${originalLines} -> ${modifiedLines} (${delta >= 0 ? "+" : ""}${delta})`
1825
+ };
1826
+ } catch (error) {
1827
+ const msg = error instanceof Error ? error.message : String(error);
1828
+ await auditLog.logFileOperation("edit", filePath, false, msg);
1829
+ return {
1830
+ success: false,
1831
+ error: `Failed to edit file: ${msg}`
1832
+ };
1833
+ }
1834
+ }
1835
+ };
1836
+ function generateDiffPreview(search, replace) {
1837
+ const searchLines = search.split("\n").slice(0, 5);
1838
+ const replaceLines = replace.split("\n").slice(0, 5);
1839
+ let preview = "";
1840
+ for (const line of searchLines) {
1841
+ preview += `- ${line}
1842
+ `;
1843
+ }
1844
+ if (search.split("\n").length > 5) {
1845
+ preview += `- ... (${search.split("\n").length - 5} more lines)
1846
+ `;
1847
+ }
1848
+ for (const line of replaceLines) {
1849
+ preview += `+ ${line}
1850
+ `;
1851
+ }
1852
+ if (replace.split("\n").length > 5) {
1853
+ preview += `+ ... (${replace.split("\n").length - 5} more lines)
1854
+ `;
1855
+ }
1856
+ return preview.trim();
1857
+ }
1858
+ var ListTool = {
1859
+ name: "list",
1860
+ description: "List files and directories in the workspace",
1861
+ inputSchema: {
1862
+ path: {
1863
+ type: "string",
1864
+ description: "Directory path to list (defaults to current directory)"
1865
+ }
1866
+ },
1867
+ requiredParams: [],
1868
+ async execute(args) {
1869
+ const dirPath = args.path || ".";
1870
+ const pathCheck = auditor.checkPath(dirPath);
1871
+ if (!pathCheck.approved) {
1872
+ return {
1873
+ success: false,
1874
+ error: pathCheck.reason
1875
+ };
1876
+ }
1877
+ try {
1878
+ const cfg = await config.load();
1879
+ const fullPath = path5.resolve(cfg.workspaceRoot, dirPath);
1880
+ const entries = await fs5.readdir(fullPath, { withFileTypes: true });
1881
+ const filtered = entries.filter(
1882
+ (entry) => !IGNORED_DIRS.includes(entry.name) && !entry.name.startsWith(".")
1883
+ );
1884
+ const formatted = filtered.map((entry) => {
1885
+ const prefix = entry.isDirectory() ? "[DIR]" : "[FILE]";
1886
+ return `${prefix} ${entry.name}`;
1887
+ }).join("\n");
1888
+ const hiddenCount = entries.length - filtered.length;
1889
+ const hiddenNote = hiddenCount > 0 ? `
1890
+
1891
+ (${hiddenCount} hidden: node_modules, .git, etc.)` : "";
1892
+ return {
1893
+ success: true,
1894
+ output: `Directory: ${dirPath}
1895
+ ${"=".repeat(60)}
1896
+ ${formatted}${hiddenNote}`
1897
+ };
1898
+ } catch (error) {
1899
+ return {
1900
+ success: false,
1901
+ error: `Failed to list directory: ${error instanceof Error ? error.message : String(error)}`
1902
+ };
1903
+ }
1904
+ }
1905
+ };
1906
+ var GrepTool = {
1907
+ name: "grep",
1908
+ description: "Search for patterns in files using regex",
1909
+ inputSchema: {
1910
+ pattern: {
1911
+ type: "string",
1912
+ description: "Regex pattern to search for"
1913
+ },
1914
+ path: {
1915
+ type: "string",
1916
+ description: "Directory to search in (defaults to current directory)"
1917
+ },
1918
+ limit: {
1919
+ type: "number",
1920
+ description: "Maximum number of results (default: 50)"
1921
+ }
1922
+ },
1923
+ requiredParams: ["pattern"],
1924
+ async execute(args) {
1925
+ const pattern = args.pattern;
1926
+ const searchPath = args.path || ".";
1927
+ const maxResults = args.limit || 50;
1928
+ if (!pattern) {
1929
+ return { success: false, error: "No search pattern provided" };
1930
+ }
1931
+ const pathCheck = auditor.checkPath(searchPath);
1932
+ if (!pathCheck.approved) {
1933
+ return {
1934
+ success: false,
1935
+ error: pathCheck.reason
1936
+ };
1937
+ }
1938
+ try {
1939
+ const cfg = await config.load();
1940
+ const fullPath = path5.resolve(cfg.workspaceRoot, searchPath);
1941
+ const results = [];
1942
+ await searchDirectory(fullPath, pattern, results, maxResults, 0, cfg.workspaceRoot);
1943
+ if (results.length === 0) {
1944
+ return {
1945
+ success: true,
1946
+ output: `No matches found for: ${pattern}`
1947
+ };
1948
+ }
1949
+ return {
1950
+ success: true,
1951
+ output: truncateOutput(`Found ${results.length} matches for "${pattern}":
1952
+ ${"=".repeat(60)}
1953
+ ${results.join("\n")}`)
1954
+ };
1955
+ } catch (error) {
1956
+ return {
1957
+ success: false,
1958
+ error: `Search failed: ${error instanceof Error ? error.message : String(error)}`
1959
+ };
1960
+ }
1961
+ }
1962
+ };
1963
+ async function searchDirectory(dir, pattern, results, maxResults, depth = 0, workspaceRoot = process.cwd()) {
1964
+ if (results.length >= maxResults || depth > 10) return;
1965
+ try {
1966
+ const entries = await fs5.readdir(dir, { withFileTypes: true });
1967
+ const regex = new RegExp(pattern, "gi");
1968
+ for (const entry of entries) {
1969
+ if (results.length >= maxResults) break;
1970
+ const fullPath = path5.join(dir, entry.name);
1971
+ const relativePath = path5.relative(workspaceRoot, fullPath);
1972
+ if (entry.name.startsWith(".") || IGNORED_DIRS.includes(entry.name)) {
1973
+ continue;
1974
+ }
1975
+ if (entry.isDirectory()) {
1976
+ await searchDirectory(fullPath, pattern, results, maxResults, depth + 1, workspaceRoot);
1977
+ } else if (entry.isFile()) {
1978
+ const ext = path5.extname(entry.name).toLowerCase();
1979
+ const textExtensions = [".ts", ".tsx", ".js", ".jsx", ".json", ".md", ".txt", ".yaml", ".yml", ".css", ".html", ".sh"];
1980
+ if (textExtensions.includes(ext) || ext === "") {
1981
+ try {
1982
+ const content = await fs5.readFile(fullPath, "utf-8");
1983
+ const lines = content.split("\n");
1984
+ for (let i = 0; i < lines.length && results.length < maxResults; i++) {
1985
+ if (regex.test(lines[i])) {
1986
+ results.push(`${relativePath}:${i + 1}: ${lines[i].trim().slice(0, 100)}`);
1987
+ }
1988
+ regex.lastIndex = 0;
1989
+ }
1990
+ } catch {
1991
+ }
1992
+ }
1993
+ }
1994
+ }
1995
+ } catch {
1996
+ }
1997
+ }
1998
+ var GlobTool = {
1999
+ name: "glob",
2000
+ description: "Find files matching a glob pattern (e.g., **/*.ts, src/**/*.tsx)",
2001
+ inputSchema: {
2002
+ pattern: {
2003
+ type: "string",
2004
+ description: "Glob pattern like **/*.ts or src/**/*.tsx"
2005
+ },
2006
+ path: {
2007
+ type: "string",
2008
+ description: "Base directory (defaults to current directory)"
2009
+ }
2010
+ },
2011
+ requiredParams: ["pattern"],
2012
+ async execute(args) {
2013
+ const pattern = args.pattern;
2014
+ const basePath = args.path || ".";
2015
+ if (!pattern) {
2016
+ return { success: false, error: "No pattern provided" };
2017
+ }
2018
+ try {
2019
+ const cfg = await config.load();
2020
+ const results = [];
2021
+ const fullBase = path5.resolve(cfg.workspaceRoot, basePath);
2022
+ await globSearch(fullBase, pattern, results, 100, 0, cfg.workspaceRoot);
2023
+ if (results.length === 0) {
2024
+ return {
2025
+ success: true,
2026
+ output: `No files matching: ${pattern}`
2027
+ };
2028
+ }
2029
+ return {
2030
+ success: true,
2031
+ output: truncateOutput(`Found ${results.length} files:
2032
+ ${results.join("\n")}`)
2033
+ };
2034
+ } catch (error) {
2035
+ return {
2036
+ success: false,
2037
+ error: `Glob failed: ${error instanceof Error ? error.message : String(error)}`
2038
+ };
2039
+ }
2040
+ }
2041
+ };
2042
+ async function globSearch(dir, pattern, results, maxResults, depth = 0, workspaceRoot = process.cwd()) {
2043
+ if (results.length >= maxResults || depth > 15) return;
2044
+ try {
2045
+ const entries = await fs5.readdir(dir, { withFileTypes: true });
2046
+ const regexPattern = pattern.replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/\?/g, ".").replace(/{{GLOBSTAR}}/g, ".*");
2047
+ const regex = new RegExp(`^${regexPattern}$`);
2048
+ for (const entry of entries) {
2049
+ if (results.length >= maxResults) break;
2050
+ if (entry.name.startsWith(".") || IGNORED_DIRS.includes(entry.name)) {
2051
+ continue;
2052
+ }
2053
+ const fullPath = path5.join(dir, entry.name);
2054
+ const relativePath = path5.relative(workspaceRoot, fullPath);
2055
+ if (entry.isDirectory()) {
2056
+ if (pattern.includes("**") || pattern.includes("/")) {
2057
+ await globSearch(fullPath, pattern, results, maxResults, depth + 1, workspaceRoot);
2058
+ }
2059
+ } else if (entry.isFile()) {
2060
+ if (regex.test(relativePath) || regex.test(entry.name)) {
2061
+ results.push(relativePath);
2062
+ }
2063
+ }
2064
+ }
2065
+ } catch {
2066
+ }
2067
+ }
2068
+ var TaskTool = {
2069
+ name: "task",
2070
+ description: "Manage the project plan and todo list. Track in-session work items, subtasks, and progress. NOT for scheduled/recurring cron jobs (use schedule_task for those). Actions: create, list, add_step, complete_step, fail_step, complete_task.",
2071
+ inputSchema: {
2072
+ action: {
2073
+ type: "string",
2074
+ description: "Action to perform: create, list, add_step, complete_step, fail_step, complete_task"
2075
+ },
2076
+ title: {
2077
+ type: "string",
2078
+ description: "Title for new task (required for create)"
2079
+ },
2080
+ step: {
2081
+ type: "string",
2082
+ description: "Step description (required for add_step)"
2083
+ },
2084
+ step_index: {
2085
+ type: "number",
2086
+ description: "Index of step to complete/fail (required for step actions)"
2087
+ }
2088
+ },
2089
+ requiredParams: ["action"],
2090
+ async execute(args) {
2091
+ const action = args.action;
2092
+ const title = args.title;
2093
+ const step = args.step;
2094
+ const stepIndex = args.step_index;
2095
+ if (!action) return { success: false, error: "No action provided" };
2096
+ try {
2097
+ switch (action) {
2098
+ case "list": {
2099
+ const progress = tasks.getProgress();
2100
+ return { success: true, output: progress };
2101
+ }
2102
+ case "create":
2103
+ if (!title) return { success: false, error: "Title required for create" };
2104
+ await tasks.create(title);
2105
+ return { success: true, output: `Created task: ${title}` };
2106
+ case "add_step":
2107
+ if (!step) return { success: false, error: "Step text required" };
2108
+ await tasks.addSubtask(step);
2109
+ return { success: true, output: `Added step: ${step}` };
2110
+ case "complete_step":
2111
+ if (stepIndex === void 0) return { success: false, error: "Step index required" };
2112
+ await tasks.completeSubtask(stepIndex);
2113
+ return { success: true, output: `Completed step ${stepIndex}` };
2114
+ case "fail_step":
2115
+ if (stepIndex === void 0) return { success: false, error: "Step index required" };
2116
+ const currentTask = tasks.get();
2117
+ if (!currentTask || stepIndex >= currentTask.subtasks.length) {
2118
+ return { success: false, error: `Invalid step index: ${stepIndex}` };
2119
+ }
2120
+ const originalText = currentTask.subtasks[stepIndex].text;
2121
+ if (!originalText.startsWith("[FAILED]")) {
2122
+ currentTask.subtasks[stepIndex].text = `[FAILED] ${originalText}`;
2123
+ currentTask.subtasks[stepIndex].done = true;
2124
+ }
2125
+ await tasks.completeSubtask(stepIndex);
2126
+ return { success: true, output: `Marked step ${stepIndex} as failed: ${originalText}` };
2127
+ case "complete_task":
2128
+ await tasks.complete();
2129
+ return { success: true, output: "Marked task as complete" };
2130
+ default:
2131
+ return { success: false, error: `Unknown action: ${action}` };
2132
+ }
2133
+ } catch (error) {
2134
+ return { success: false, error: `Task action failed: ${error instanceof Error ? error.message : String(error)}` };
2135
+ }
2136
+ }
2137
+ };
2138
+ var WebFetchTool = {
2139
+ name: "web_fetch",
2140
+ description: "Fetch content from a URL via GET request (for documentation, web pages, etc.). GET-only -- use http_request for POST/PUT/PATCH/DELETE.",
2141
+ inputSchema: {
2142
+ url: {
2143
+ type: "string",
2144
+ description: "URL to fetch content from"
2145
+ }
2146
+ },
2147
+ requiredParams: ["url"],
2148
+ async execute(args) {
2149
+ const url = args.url;
2150
+ if (!url) {
2151
+ return { success: false, error: "No URL provided" };
2152
+ }
2153
+ try {
2154
+ new URL(url);
2155
+ } catch {
2156
+ return { success: false, error: "Invalid URL format" };
2157
+ }
2158
+ const blockedDomains = ["localhost", "127.0.0.1", "0.0.0.0", "169.254"];
2159
+ const urlObj = new URL(url);
2160
+ if (blockedDomains.some((d) => urlObj.hostname.includes(d))) {
2161
+ return {
2162
+ success: false,
2163
+ error: "Cannot fetch from local/private addresses"
2164
+ };
2165
+ }
2166
+ try {
2167
+ const controller = new AbortController();
2168
+ const timeoutId = setTimeout(() => controller.abort(), 1e4);
2169
+ const response = await fetch(url, {
2170
+ signal: controller.signal,
2171
+ headers: {
2172
+ "User-Agent": "Obsidian-Next/1.0 (AI Agent CLI)",
2173
+ "Accept": "text/html,application/json,text/plain,*/*"
2174
+ }
2175
+ });
2176
+ clearTimeout(timeoutId);
2177
+ if (!response.ok) {
2178
+ return {
2179
+ success: false,
2180
+ error: `HTTP ${response.status}: ${response.statusText}`
2181
+ };
2182
+ }
2183
+ const contentType = response.headers.get("content-type") || "";
2184
+ let content = await response.text();
2185
+ if (content.length > MAX_OUTPUT_LENGTH) {
2186
+ content = content.slice(0, MAX_OUTPUT_LENGTH) + `
2187
+
2188
+ ... [TRUNCATED: ${content.length - MAX_OUTPUT_LENGTH} more characters]`;
2189
+ }
2190
+ if (contentType.includes("text/html")) {
2191
+ content = content.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "").replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
2192
+ }
2193
+ return {
2194
+ success: true,
2195
+ output: truncateOutput(`URL: ${url}
2196
+ Content-Type: ${contentType}
2197
+ ${"=".repeat(60)}
2198
+ ${content}`)
2199
+ };
2200
+ } catch (error) {
2201
+ if (error instanceof Error && error.name === "AbortError") {
2202
+ return { success: false, error: "Request timed out after 10 seconds" };
2203
+ }
2204
+ return {
2205
+ success: false,
2206
+ error: `Fetch failed: ${error instanceof Error ? error.message : String(error)}`
2207
+ };
2208
+ }
2209
+ }
2210
+ };
2211
+ var MCPManagementTool = {
2212
+ name: "mcp_manage",
2213
+ description: 'Manage MCP servers. actions: add, remove, install. Use "install" to easily add certified tools like "filesystem", "git", "research", or "context7".',
2214
+ inputSchema: {
2215
+ action: {
2216
+ type: "string",
2217
+ description: "Action to perform: add, remove, install"
2218
+ },
2219
+ name: {
2220
+ type: "string",
2221
+ description: 'Name of the server (e.g. "filesystem", "research", "context7")'
2222
+ },
2223
+ command: {
2224
+ type: "string",
2225
+ description: "Command to execute (add only)"
2226
+ },
2227
+ args: {
2228
+ type: "string",
2229
+ description: "Args for command (add only)"
2230
+ }
2231
+ },
2232
+ requiredParams: ["action", "name"],
2233
+ async execute(args) {
2234
+ const action = args.action;
2235
+ const name = args.name;
2236
+ if (action === "install") {
2237
+ const def = getRegistryDefinition(name);
2238
+ if (!def) {
2239
+ const available = listRegistry().map((r) => r.name).join(", ");
2240
+ return { success: false, error: `Unknown registry item '${name}'. Available: ${available}` };
2241
+ }
2242
+ const existing = mcp.getStatus().find((s) => s.name === name);
2243
+ if (existing && existing.connected) {
2244
+ return { success: true, output: `MCP server '${name}' is already installed and connected.` };
2245
+ }
2246
+ try {
2247
+ await mcp.addServer(name, {
2248
+ command: def.command,
2249
+ args: def.args,
2250
+ autoConnect: false,
2251
+ env: def.env
2252
+ });
2253
+ return { success: true, output: `Successfully installed and connected to certified MCP server '${name}' (${def.description})` };
2254
+ } catch (e) {
2255
+ return { success: false, error: `Failed to install server: ${e instanceof Error ? e.message : String(e)}` };
2256
+ }
2257
+ }
2258
+ if (action === "add") {
2259
+ if (!args.command) return { success: false, error: 'Command is required for "add" action' };
2260
+ const command = args.command;
2261
+ const commandArgs = (args.args || "").split(" ").filter((a) => a.length > 0);
2262
+ try {
2263
+ await mcp.addServer(name, {
2264
+ command,
2265
+ args: commandArgs,
2266
+ autoConnect: false
2267
+ });
2268
+ return { success: true, output: `Successfully added and connected to MCP server '${name}'` };
2269
+ } catch (e) {
2270
+ return { success: false, error: `Failed to add server: ${e instanceof Error ? e.message : String(e)}` };
2271
+ }
2272
+ }
2273
+ if (action === "remove") {
2274
+ try {
2275
+ await mcp.removeServer(name);
2276
+ return { success: true, output: `Successfully removed MCP server '${name}'` };
2277
+ } catch (e) {
2278
+ return { success: false, error: `Failed to remove server: ${e instanceof Error ? e.message : String(e)}` };
2279
+ }
2280
+ }
2281
+ if (action === "connect") {
2282
+ try {
2283
+ const status = mcp.getStatus().find((s) => s.name === name);
2284
+ if (!status) return { success: false, error: `Server '${name}' not found in config` };
2285
+ if (status.connected) return { success: true, output: `Server '${name}' is already connected` };
2286
+ await mcp.connect(name, status.config);
2287
+ return { success: true, output: `Successfully connected to MCP server '${name}'` };
2288
+ } catch (e) {
2289
+ return { success: false, error: `Failed to connect: ${e instanceof Error ? e.message : String(e)}` };
2290
+ }
2291
+ }
2292
+ if (action === "disconnect") {
2293
+ try {
2294
+ await mcp.disconnect(name);
2295
+ return { success: true, output: `Successfully disconnected MCP server '${name}'` };
2296
+ } catch (e) {
2297
+ return { success: false, error: `Failed to disconnect: ${e instanceof Error ? e.message : String(e)}` };
2298
+ }
2299
+ }
2300
+ if (action === "status") {
2301
+ const status = mcp.getStatus().find((s) => s.name === name);
2302
+ if (!status) return { success: false, error: `Server '${name}' not found` };
2303
+ return {
2304
+ success: true,
2305
+ output: `Server: ${name}
2306
+ Status: ${status.connected ? "Connected" : "Disconnected"}
2307
+ Tools: ${status.capabilities ? "Available" : "N/A"}`
2308
+ };
2309
+ }
2310
+ return { success: false, error: `Unknown action: ${action}` };
2311
+ }
2312
+ };
2313
+ var MemoryTool = {
2314
+ name: "memory",
2315
+ description: "Store and recall long-term memories: user preferences, project facts, and learned patterns. Use this to remember user information across sessions.",
2316
+ inputSchema: {
2317
+ action: { type: "string", description: "Action: store, recall, search, list, forget" },
2318
+ type: { type: "string", description: "Memory type: user_preference, project_fact, decision_log, learned_pattern (for store)" },
2319
+ key: { type: "string", description: "Unique key for the memory (for store, recall, forget)" },
2320
+ content: { type: "string", description: "Content to store (for store action)" },
2321
+ query: { type: "string", description: "Search query (for search action)" }
2322
+ },
2323
+ requiredParams: ["action"],
2324
+ async execute(args) {
2325
+ const { memory } = await import("./memory-QZTBTYPH.js");
2326
+ const action = args.action;
2327
+ const type = args.type;
2328
+ const key = args.key;
2329
+ const content = args.content;
2330
+ const query = args.query;
2331
+ await memory.init();
2332
+ if (action === "store") {
2333
+ if (!key || !content) {
2334
+ return { success: false, error: "store requires key and content" };
2335
+ }
2336
+ const memoType = type || "user_preference";
2337
+ const success = await memory.store(memoType, key, content);
2338
+ if (success) {
2339
+ return { success: true, output: `Stored memory: ${key}` };
2340
+ }
2341
+ return { success: false, error: "Failed to store memory" };
2342
+ }
2343
+ if (action === "recall") {
2344
+ if (!key) {
2345
+ return { success: false, error: "recall requires key" };
2346
+ }
2347
+ const memo = await memory.recall(key);
2348
+ if (memo) {
2349
+ return { success: true, output: `[${memo.type}] ${memo.key}: ${memo.content}` };
2350
+ }
2351
+ return { success: true, output: `No memory found for key: ${key}` };
2352
+ }
2353
+ if (action === "search") {
2354
+ if (!query) {
2355
+ return { success: false, error: "search requires query" };
2356
+ }
2357
+ const memos = await memory.search(query, type);
2358
+ if (memos.length === 0) {
2359
+ return { success: true, output: "No memories found matching query" };
2360
+ }
2361
+ const lines = memos.map((m) => `- [${m.type}] ${m.key}: ${m.content}`);
2362
+ return { success: true, output: `Found ${memos.length} memories:
2363
+ ${lines.join("\n")}` };
2364
+ }
2365
+ if (action === "list") {
2366
+ const memoType = type;
2367
+ let memos;
2368
+ if (memoType) {
2369
+ memos = await memory.getByType(memoType);
2370
+ if (memos.length === 0) {
2371
+ return { success: true, output: `No ${memoType} memories found` };
2372
+ }
2373
+ const lines = memos.map((m) => `- ${m.key}: ${m.content}`);
2374
+ return { success: true, output: `${memoType} memories:
2375
+ ${lines.join("\n")}` };
2376
+ } else {
2377
+ const stats = await memory.getStats();
2378
+ const allMemos = [];
2379
+ for (const t of Object.keys(stats.byType)) {
2380
+ const typeMemos = await memory.getByType(t);
2381
+ if (typeMemos.length > 0) {
2382
+ allMemos.push(`--- ${t} ---`);
2383
+ allMemos.push(...typeMemos.map((m) => `- ${m.key}: ${m.content}`));
2384
+ }
2385
+ }
2386
+ if (allMemos.length === 0) return { success: true, output: "No memories found in any category." };
2387
+ return { success: true, output: `Current Memory Bank:
2388
+ ${allMemos.join("\n")}` };
2389
+ }
2390
+ }
2391
+ if (action === "forget") {
2392
+ if (!key) {
2393
+ return { success: false, error: "forget requires key" };
2394
+ }
2395
+ const success = await memory.forget(key);
2396
+ if (success) {
2397
+ return { success: true, output: `Forgot memory: ${key}` };
2398
+ }
2399
+ return { success: false, error: "Failed to forget memory" };
2400
+ }
2401
+ return { success: false, error: `Unknown action: ${action}. Valid: store, recall, search, list, forget` };
2402
+ }
2403
+ };
2404
+ var UnscheduleTool = {
2405
+ name: "unschedule_task",
2406
+ description: "Unschedule a previously scheduled background cron job. Requires the task ID.",
2407
+ inputSchema: {
2408
+ taskId: {
2409
+ type: "string",
2410
+ description: "The ID of the task to unschedule (obtained from list_scheduled_tasks)."
2411
+ }
2412
+ },
2413
+ requiredParams: ["taskId"],
2414
+ async execute(args) {
2415
+ const taskId = args.taskId;
2416
+ if (!taskId) {
2417
+ return { success: false, error: "Task ID is required to unschedule a task." };
2418
+ }
2419
+ try {
2420
+ const success = await scheduler.removeTask(taskId);
2421
+ if (success) {
2422
+ return { success: true, output: `Successfully unscheduled task: ${taskId}` };
2423
+ } else {
2424
+ return { success: false, output: `Failed to unschedule task: ${taskId}. Task not found or already inactive.` };
2425
+ }
2426
+ } catch (error) {
2427
+ return { success: false, error: `Failed to unschedule task: ${error instanceof Error ? error.message : String(error)}` };
2428
+ }
2429
+ }
2430
+ };
2431
+ var ComputerUseTool = {
2432
+ name: "computer",
2433
+ description: `Desktop interaction. PREFER BASH for URLs/apps (e.g., bash 'open "https://youtube.com"').
2434
+
2435
+ SMART ACTIONS (use first):
2436
+ - find_and_click: Click by label ("Submit", "Play") - no coordinates needed
2437
+ - get_ui_context: List buttons/fields in current window
2438
+ - get_buttons: List all button labels
2439
+ - activate_app: Bring app to front
2440
+
2441
+ COORDINATE ACTIONS (use when smart actions fail):
2442
+ - screenshot: Capture screen
2443
+ - left_click: Click at [x,y]
2444
+ - type: Type text
2445
+ - key: Press key (cmd+l, Return, etc.)
2446
+ - scroll: Scroll at position`,
2447
+ inputSchema: {
2448
+ action: {
2449
+ type: "string",
2450
+ description: "SMART (prefer): find_and_click, get_ui_context, get_buttons, activate_app, get_focused_app. COORDINATE (fallback): screenshot, left_click, type, key, scroll, mouse_move, double_click, right_click, left_click_drag, wait, zoom."
2451
+ },
2452
+ coordinate: {
2453
+ type: "array",
2454
+ items: { type: "number" },
2455
+ description: "[x, y] pixel coordinates for coordinate-based actions."
2456
+ },
2457
+ text: {
2458
+ type: "string",
2459
+ description: "Text to type, or modifier key for clicks (shift, control, alt, command)."
2460
+ },
2461
+ key: {
2462
+ type: "string",
2463
+ description: "Key to press (e.g., enter, escape, ctrl+c)."
2464
+ },
2465
+ label: {
2466
+ type: "string",
2467
+ description: 'UI element label for find_and_click (e.g., "Submit", "Cancel", "OK").'
2468
+ },
2469
+ app_name: {
2470
+ type: "string",
2471
+ description: "Application name for activate_app or to scope element search."
2472
+ },
2473
+ scroll_direction: {
2474
+ type: "string",
2475
+ description: "up, down, left, or right"
2476
+ },
2477
+ scroll_amount: {
2478
+ type: "number",
2479
+ description: "Amount to scroll (default: 3)"
2480
+ },
2481
+ start_coordinate: {
2482
+ type: "array",
2483
+ items: { type: "number" },
2484
+ description: "[x, y] start coordinates for drag"
2485
+ },
2486
+ end_coordinate: {
2487
+ type: "array",
2488
+ items: { type: "number" },
2489
+ description: "[x, y] end coordinates for drag"
2490
+ },
2491
+ duration: {
2492
+ type: "number",
2493
+ description: "Duration in milliseconds (for wait, hold_key)"
2494
+ },
2495
+ region: {
2496
+ type: "array",
2497
+ items: { type: "number" },
2498
+ description: "[x1, y1, x2, y2] region for zoom"
2499
+ }
2500
+ },
2501
+ requiredParams: ["action"],
2502
+ async execute(args) {
2503
+ const actionType = args.action;
2504
+ if (!actionType) {
2505
+ return { success: false, error: "No computer action provided. Use: screenshot, left_click, type, key, mouse_move, scroll, etc." };
2506
+ }
2507
+ const validateCoord = (coord, name = "coordinate") => {
2508
+ if (!coord || !Array.isArray(coord) || coord.length < 2) {
2509
+ return null;
2510
+ }
2511
+ const x = Number(coord[0]);
2512
+ const y = Number(coord[1]);
2513
+ if (isNaN(x) || isNaN(y)) return null;
2514
+ return [x, y];
2515
+ };
2516
+ const scaleToNative = (coord) => {
2517
+ if (lastScreenshotScale >= 1) return coord;
2518
+ return [
2519
+ Math.round(coord[0] / lastScreenshotScale),
2520
+ Math.round(coord[1] / lastScreenshotScale)
2521
+ ];
2522
+ };
2523
+ try {
2524
+ let output;
2525
+ switch (actionType) {
2526
+ case "screenshot":
2527
+ const screenshotResult = await takeScreenshotForAPI(false);
2528
+ lastScreenshotScale = screenshotResult.scale;
2529
+ bus.emitAgent({
2530
+ type: "computer_scale_update",
2531
+ scale: screenshotResult.scale,
2532
+ scaledWidth: screenshotResult.width,
2533
+ scaledHeight: screenshotResult.height,
2534
+ nativeWidth: Math.round(screenshotResult.width / screenshotResult.scale),
2535
+ nativeHeight: Math.round(screenshotResult.height / screenshotResult.scale)
2536
+ });
2537
+ return {
2538
+ success: true,
2539
+ output: `Screenshot captured (${screenshotResult.width}x${screenshotResult.height}, scale: ${screenshotResult.scale.toFixed(2)}). Native: ${Math.round(screenshotResult.width / screenshotResult.scale)}x${Math.round(screenshotResult.height / screenshotResult.scale)}`,
2540
+ content: [{ type: "image", data: screenshotResult.base64, mimeType: "image/png" }]
2541
+ };
2542
+ case "left_click": {
2543
+ const coord = validateCoord(args.coordinate);
2544
+ if (!coord) return { success: false, error: "left_click requires coordinate: [x, y] as numbers" };
2545
+ const [nativeX, nativeY] = scaleToNative(coord);
2546
+ await computer.leftClick(nativeX, nativeY, args.text);
2547
+ output = `Clicked at screenshot (${coord[0]}, ${coord[1]}) -> native (${nativeX}, ${nativeY}). If this missed the target, re-examine the screenshot and identify the EXACT center of the element you want to click.`;
2548
+ break;
2549
+ }
2550
+ case "type": {
2551
+ const text = args.text;
2552
+ if (!text && text !== "") return { success: false, error: "type requires text parameter" };
2553
+ await computer.typeText(text);
2554
+ output = `Typed: "${text.substring(0, 30)}${text.length > 30 ? "..." : ""}"`;
2555
+ break;
2556
+ }
2557
+ case "key": {
2558
+ const keyToPress = args.key || args.text;
2559
+ if (!keyToPress) return { success: false, error: 'key requires key parameter (e.g., "enter", "escape", "ctrl+c")' };
2560
+ await computer.pressKey(keyToPress);
2561
+ output = `Pressed key: ${keyToPress}`;
2562
+ break;
2563
+ }
2564
+ case "mouse_move": {
2565
+ const coord = validateCoord(args.coordinate);
2566
+ if (!coord) return { success: false, error: "mouse_move requires coordinate: [x, y]" };
2567
+ const [nativeX, nativeY] = scaleToNative(coord);
2568
+ await computer.mouseMove(nativeX, nativeY);
2569
+ output = `Mouse moved to screenshot (${coord[0]}, ${coord[1]}) -> native (${nativeX}, ${nativeY}).`;
2570
+ break;
2571
+ }
2572
+ case "scroll": {
2573
+ const coord = validateCoord(args.coordinate);
2574
+ if (!coord) return { success: false, error: "scroll requires coordinate: [x, y]" };
2575
+ const scrollDir = args.scroll_direction;
2576
+ if (!scrollDir) return { success: false, error: "scroll requires scroll_direction: up|down|left|right" };
2577
+ const amount = args.scroll_amount || 3;
2578
+ const [nativeX, nativeY] = scaleToNative(coord);
2579
+ await computer.scroll(nativeX, nativeY, scrollDir, amount, args.text);
2580
+ output = `Scrolled ${scrollDir} by ${amount} at screenshot (${coord[0]}, ${coord[1]}) -> native (${nativeX}, ${nativeY}).`;
2581
+ break;
2582
+ }
2583
+ case "left_click_drag": {
2584
+ const startCoord = validateCoord(args.start_coordinate, "start_coordinate");
2585
+ const endCoord = validateCoord(args.end_coordinate, "end_coordinate");
2586
+ if (!startCoord) return { success: false, error: "left_click_drag requires start_coordinate: [x, y]" };
2587
+ if (!endCoord) return { success: false, error: "left_click_drag requires end_coordinate: [x, y]" };
2588
+ const [nativeStartX, nativeStartY] = scaleToNative(startCoord);
2589
+ const [nativeEndX, nativeEndY] = scaleToNative(endCoord);
2590
+ await computer.leftClickDrag(nativeStartX, nativeStartY, nativeEndX, nativeEndY);
2591
+ output = `Dragged from screenshot (${startCoord[0]}, ${startCoord[1]}) -> native (${nativeStartX}, ${nativeStartY}) to screenshot (${endCoord[0]}, ${endCoord[1]}) -> native (${nativeEndX}, ${nativeEndY}).`;
2592
+ break;
2593
+ }
2594
+ case "right_click": {
2595
+ const coord = validateCoord(args.coordinate);
2596
+ if (!coord) return { success: false, error: "right_click requires coordinate: [x, y]" };
2597
+ const [nativeX, nativeY] = scaleToNative(coord);
2598
+ await computer.rightClick(nativeX, nativeY, args.text);
2599
+ output = `Right click at screenshot (${coord[0]}, ${coord[1]}) -> native (${nativeX}, ${nativeY}).`;
2600
+ break;
2601
+ }
2602
+ case "middle_click": {
2603
+ const coord = validateCoord(args.coordinate);
2604
+ if (!coord) return { success: false, error: "middle_click requires coordinate: [x, y]" };
2605
+ const [nativeX, nativeY] = scaleToNative(coord);
2606
+ await computer.middleClick(nativeX, nativeY, args.text);
2607
+ output = `Middle click at screenshot (${coord[0]}, ${coord[1]}) -> native (${nativeX}, ${nativeY}).`;
2608
+ break;
2609
+ }
2610
+ case "double_click": {
2611
+ const coord = validateCoord(args.coordinate);
2612
+ if (!coord) return { success: false, error: "double_click requires coordinate: [x, y]" };
2613
+ const [nativeX, nativeY] = scaleToNative(coord);
2614
+ await computer.doubleClick(nativeX, nativeY, args.text);
2615
+ output = `Double click at screenshot (${coord[0]}, ${coord[1]}) -> native (${nativeX}, ${nativeY}).`;
2616
+ break;
2617
+ }
2618
+ case "triple_click": {
2619
+ const coord = validateCoord(args.coordinate);
2620
+ if (!coord) return { success: false, error: "triple_click requires coordinate: [x, y]" };
2621
+ const [nativeX, nativeY] = scaleToNative(coord);
2622
+ await computer.tripleClick(nativeX, nativeY, args.text);
2623
+ output = `Triple click at screenshot (${coord[0]}, ${coord[1]}) -> native (${nativeX}, ${nativeY}).`;
2624
+ break;
2625
+ }
2626
+ case "left_mouse_down": {
2627
+ const coord = validateCoord(args.coordinate);
2628
+ if (!coord) return { success: false, error: "left_mouse_down requires coordinate: [x, y]" };
2629
+ const [nativeX, nativeY] = scaleToNative(coord);
2630
+ await computer.leftMouseDown(nativeX, nativeY);
2631
+ output = `Mouse down at screenshot (${coord[0]}, ${coord[1]}) -> native (${nativeX}, ${nativeY}).`;
2632
+ break;
2633
+ }
2634
+ case "left_mouse_up": {
2635
+ const coord = validateCoord(args.coordinate);
2636
+ if (!coord) return { success: false, error: "left_mouse_up requires coordinate: [x, y]" };
2637
+ const [nativeX, nativeY] = scaleToNative(coord);
2638
+ await computer.leftMouseUp(nativeX, nativeY);
2639
+ output = `Mouse up at screenshot (${coord[0]}, ${coord[1]}) -> native (${nativeX}, ${nativeY}).`;
2640
+ break;
2641
+ }
2642
+ case "hold_key": {
2643
+ const keyToHold = args.key || args.text;
2644
+ if (!keyToHold) return { success: false, error: "hold_key requires key parameter" };
2645
+ const duration = args.duration || 1;
2646
+ await computer.holdKey(keyToHold, duration);
2647
+ output = `Held ${keyToHold} for ${duration}s.`;
2648
+ break;
2649
+ }
2650
+ case "wait": {
2651
+ const duration = args.duration || 1e3;
2652
+ await computer.wait(duration);
2653
+ output = `Waited ${duration}ms.`;
2654
+ break;
2655
+ }
2656
+ case "zoom": {
2657
+ const regionVal = args.region;
2658
+ if (!regionVal || !Array.isArray(regionVal) || regionVal.length < 4) {
2659
+ return { success: false, error: "zoom requires region: [x1, y1, x2, y2]" };
2660
+ }
2661
+ const [x1, y1, x2, y2] = regionVal;
2662
+ const [nativeX1, nativeY1] = scaleToNative([x1, y1]);
2663
+ const [nativeX2, nativeY2] = scaleToNative([x2, y2]);
2664
+ const zoomResult = await computer.zoom(nativeX1, nativeY1, nativeX2, nativeY2);
2665
+ return {
2666
+ success: true,
2667
+ output: `Zoomed region screenshot (${x1}, ${y1}) to (${x2}, ${y2}) -> native (${nativeX1}, ${nativeY1}) to (${nativeX2}, ${nativeY2}).`,
2668
+ content: [{ type: "image", data: zoomResult, mimeType: "image/png" }]
2669
+ };
2670
+ }
2671
+ case "get_dimensions":
2672
+ const dims = await computer.getDisplayDimensions();
2673
+ return { success: true, output: `Display: ${dims.width}x${dims.height}. Use these dimensions for coordinate calculations.` };
2674
+ case "batch": {
2675
+ const cmds = args.commands;
2676
+ if (!cmds || !Array.isArray(cmds)) {
2677
+ return { success: false, error: "batch requires commands: string[]" };
2678
+ }
2679
+ await computer.executeBatch(cmds);
2680
+ output = `Batch executed ${cmds.length} commands.`;
2681
+ break;
2682
+ }
2683
+ // ==================== SMART ACCESSIBILITY-BASED ACTIONS ====================
2684
+ case "find_and_click": {
2685
+ if (!args.label) return { success: false, error: 'find_and_click requires label parameter (e.g., "Submit", "OK")' };
2686
+ const label = args.label;
2687
+ const clicked = await clickElementByLabel(label, args.app_name);
2688
+ if (clicked) {
2689
+ output = `Clicked element labeled "${label}" via accessibility API.`;
2690
+ break;
2691
+ }
2692
+ const coords = await findClickableByLabel(label);
2693
+ if (coords) {
2694
+ await computer.leftClick(coords[0], coords[1]);
2695
+ output = `Clicked "${label}" at (${coords[0]}, ${coords[1]}).`;
2696
+ break;
2697
+ }
2698
+ return { success: false, error: `Could not find element labeled "${label}". Try using screenshot + coordinate-based click.` };
2699
+ }
2700
+ case "get_ui_context": {
2701
+ const uiContext = await getUIContext();
2702
+ return {
2703
+ success: true,
2704
+ output: `Current UI State:
2705
+ ${uiContext}
2706
+
2707
+ Use find_and_click with a button label, or screenshot + coordinates for unlisted elements.`
2708
+ };
2709
+ }
2710
+ case "get_buttons": {
2711
+ const buttons = await getButtons(args.app_name);
2712
+ if (buttons.length === 0) {
2713
+ return { success: true, output: "No buttons found in current window. Try screenshot to see the UI." };
2714
+ }
2715
+ return {
2716
+ success: true,
2717
+ output: `Available buttons (${buttons.length}):
2718
+ ${buttons.map((b) => ` - "${b}"`).join("\n")}
2719
+
2720
+ Use find_and_click with any of these labels.`
2721
+ };
2722
+ }
2723
+ case "activate_app": {
2724
+ if (!args.app_name) return { success: false, error: "activate_app requires app_name parameter" };
2725
+ const appName = args.app_name;
2726
+ const activated = await activateApp(appName);
2727
+ if (activated) {
2728
+ output = `Activated "${appName}".`;
2729
+ await new Promise((r) => setTimeout(r, 500));
2730
+ const verifyScreenshot = await takeScreenshotForAPI(false);
2731
+ return {
2732
+ success: true,
2733
+ output: `${output} Window now in focus.`,
2734
+ content: [{ type: "image", data: verifyScreenshot.base64, mimeType: "image/png" }]
2735
+ };
2736
+ }
2737
+ return { success: false, error: `Could not activate "${appName}". Check if the app is running.` };
2738
+ }
2739
+ case "get_focused_app": {
2740
+ const app = await getFocusedApp();
2741
+ return { success: true, output: `Currently focused: ${app}` };
2742
+ }
2743
+ default:
2744
+ return { success: false, error: `Unknown action: ${actionType}. SMART: find_and_click, get_ui_context, get_buttons, activate_app. COORDINATE: screenshot, left_click, type, key, scroll, etc.` };
2745
+ }
2746
+ const interactionActions = ["left_click", "right_click", "double_click", "triple_click", "type", "key", "left_click_drag", "batch", "find_and_click"];
2747
+ if (interactionActions.includes(actionType)) {
2748
+ await new Promise((resolve) => setTimeout(resolve, 500));
2749
+ const verifyScreenshot = await takeScreenshotForAPI(true);
2750
+ return {
2751
+ success: true,
2752
+ output: `${output || "Action executed."} Verification captured (${verifyScreenshot.width}x${verifyScreenshot.height}).`,
2753
+ content: [{ type: "image", data: verifyScreenshot.base64, mimeType: "image/png" }]
2754
+ };
2755
+ }
2756
+ return { success: true, output: output || "Computer action executed successfully." };
2757
+ } catch (error) {
2758
+ return { success: false, error: `Computer action failed: ${error instanceof Error ? error.message : String(error)}` };
2759
+ }
2760
+ }
2761
+ };
2762
+ var HttpRequestTool = {
2763
+ name: "http_request",
2764
+ description: "Make HTTP requests with any method (GET, POST, PUT, PATCH, DELETE). Use this for API calls, webhooks, and HTTP interactions beyond simple page fetching.",
2765
+ inputSchema: {
2766
+ method: {
2767
+ type: "string",
2768
+ description: "HTTP method: GET, POST, PUT, PATCH, DELETE"
2769
+ },
2770
+ url: {
2771
+ type: "string",
2772
+ description: "Full URL to request"
2773
+ },
2774
+ headers: {
2775
+ type: "string",
2776
+ description: 'JSON string of headers (e.g., {"Authorization": "Bearer ...","Content-Type": "application/json"})'
2777
+ },
2778
+ body: {
2779
+ type: "string",
2780
+ description: "Request body (string or JSON string). Sent as-is for POST/PUT/PATCH."
2781
+ }
2782
+ },
2783
+ requiredParams: ["method", "url"],
2784
+ async execute(args) {
2785
+ const method = (args.method || "GET").toUpperCase();
2786
+ const url = args.url;
2787
+ const headersStr = args.headers;
2788
+ const body = args.body;
2789
+ if (!url) {
2790
+ return { success: false, error: "URL is required" };
2791
+ }
2792
+ const validMethods = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
2793
+ if (!validMethods.includes(method)) {
2794
+ return { success: false, error: `Invalid method: ${method}. Use: ${validMethods.join(", ")}` };
2795
+ }
2796
+ let urlObj;
2797
+ try {
2798
+ urlObj = new URL(url);
2799
+ } catch {
2800
+ return { success: false, error: "Invalid URL format" };
2801
+ }
2802
+ const blockedDomains = ["localhost", "127.0.0.1", "0.0.0.0", "169.254", "10.", "192.168.", "172.16."];
2803
+ if (blockedDomains.some((d) => urlObj.hostname.includes(d) || urlObj.hostname.startsWith(d))) {
2804
+ return { success: false, error: "Cannot make requests to local/private addresses" };
2805
+ }
2806
+ let headers = {
2807
+ "User-Agent": "Obsidian-Next/1.0 (AI Agent CLI)"
2808
+ };
2809
+ if (headersStr) {
2810
+ try {
2811
+ const parsed = JSON.parse(headersStr);
2812
+ headers = { ...headers, ...parsed };
2813
+ } catch {
2814
+ return { success: false, error: "Invalid headers JSON" };
2815
+ }
2816
+ }
2817
+ try {
2818
+ const controller = new AbortController();
2819
+ const timeoutId = setTimeout(() => controller.abort(), 15e3);
2820
+ const fetchOpts = {
2821
+ method,
2822
+ headers,
2823
+ signal: controller.signal
2824
+ };
2825
+ if (body && ["POST", "PUT", "PATCH"].includes(method)) {
2826
+ fetchOpts.body = body;
2827
+ if (!headers["Content-Type"] && !headers["content-type"]) {
2828
+ try {
2829
+ JSON.parse(body);
2830
+ fetchOpts.headers["Content-Type"] = "application/json";
2831
+ } catch {
2832
+ }
2833
+ }
2834
+ }
2835
+ const response = await fetch(url, fetchOpts);
2836
+ clearTimeout(timeoutId);
2837
+ const contentType = response.headers.get("content-type") || "";
2838
+ let content = await response.text();
2839
+ const statusLine = `${response.status} ${response.statusText}`;
2840
+ const respHeaders = {};
2841
+ response.headers.forEach((value, key) => {
2842
+ respHeaders[key] = value;
2843
+ });
2844
+ const headerSummary = Object.entries(respHeaders).slice(0, 10).map(([k, v]) => ` ${k}: ${v}`).join("\n");
2845
+ const output = [
2846
+ `${method} ${url} -> ${statusLine}`,
2847
+ `Response Headers:
2848
+ ${headerSummary}`,
2849
+ `${"=".repeat(60)}`,
2850
+ content
2851
+ ].join("\n");
2852
+ return {
2853
+ success: response.ok,
2854
+ output: truncateOutput(output),
2855
+ error: response.ok ? void 0 : `HTTP ${statusLine}`
2856
+ };
2857
+ } catch (error) {
2858
+ if (error instanceof Error && error.name === "AbortError") {
2859
+ return { success: false, error: "Request timed out after 15 seconds" };
2860
+ }
2861
+ return { success: false, error: `Request failed: ${error instanceof Error ? error.message : String(error)}` };
2862
+ }
2863
+ }
2864
+ };
2865
+ var DeleteTool = {
2866
+ name: "delete",
2867
+ description: "Delete a file or directory from the workspace. Requires approval in safe mode. Cannot delete .git, node_modules, or files outside workspace.",
2868
+ inputSchema: {
2869
+ path: {
2870
+ type: "string",
2871
+ description: "Path to the file or directory to delete (relative to workspace)"
2872
+ }
2873
+ },
2874
+ requiredParams: ["path"],
2875
+ async execute(args) {
2876
+ const filePath = args.path;
2877
+ if (!filePath) {
2878
+ return { success: false, error: "No path provided" };
2879
+ }
2880
+ const pathCheck = auditor.checkPath(filePath);
2881
+ if (!pathCheck.approved) {
2882
+ return { success: false, error: pathCheck.reason };
2883
+ }
2884
+ const blocked = [".git", "node_modules", ".env", "package-lock.json", "yarn.lock"];
2885
+ const basename = path5.basename(filePath);
2886
+ const normalized = filePath.replace(/\\/g, "/");
2887
+ if (blocked.includes(basename) || blocked.some((b) => normalized.startsWith(b + "/") || normalized === b)) {
2888
+ return { success: false, error: `Cannot delete protected path: ${filePath}` };
2889
+ }
2890
+ const cfg = await config.load();
2891
+ const fullPath = path5.resolve(cfg.workspaceRoot, filePath);
2892
+ const relative = path5.relative(cfg.workspaceRoot, fullPath);
2893
+ if (relative.startsWith("..")) {
2894
+ return { success: false, error: "Cannot delete files outside workspace" };
2895
+ }
2896
+ try {
2897
+ const stat = await fs5.stat(fullPath);
2898
+ const isDir = stat.isDirectory();
2899
+ if (isDir) {
2900
+ await fs5.rm(fullPath, { recursive: true });
2901
+ } else {
2902
+ const content = await fs5.readFile(fullPath, "utf-8").catch(() => null);
2903
+ await fs5.unlink(fullPath);
2904
+ if (content !== null) {
2905
+ await undo.recordChange(filePath, "delete", content, null);
2906
+ }
2907
+ }
2908
+ await context.trackModified(filePath);
2909
+ await auditLog.logFileOperation("delete", filePath, true);
2910
+ return {
2911
+ success: true,
2912
+ output: `Deleted ${isDir ? "directory" : "file"}: ${filePath}`
2913
+ };
2914
+ } catch (error) {
2915
+ await auditLog.logFileOperation("delete", filePath, false, error instanceof Error ? error.message : String(error));
2916
+ return { success: false, error: `Failed to delete: ${error instanceof Error ? error.message : String(error)}` };
2917
+ }
2918
+ }
2919
+ };
2920
+ var NotifyTool = {
2921
+ name: "notify",
2922
+ description: "Send an immediate notification (sound alert, native OS notification, optional voice). Use this for direct alerts without scheduling.",
2923
+ inputSchema: {
2924
+ title: {
2925
+ type: "string",
2926
+ description: 'Notification title (default: "Obsidian")'
2927
+ },
2928
+ message: {
2929
+ type: "string",
2930
+ description: "Notification message text"
2931
+ },
2932
+ sound: {
2933
+ type: "string",
2934
+ description: 'macOS sound name (default: "Glass"). Options: Glass, Basso, Blow, Bottle, Frog, Funk, Hero, Morse, Ping, Pop, Purr, Sosumi, Submarine, Tink'
2935
+ },
2936
+ speak: {
2937
+ type: "boolean",
2938
+ description: "Whether to speak the message aloud via TTS (default: true on macOS)"
2939
+ }
2940
+ },
2941
+ requiredParams: ["message"],
2942
+ async execute(args) {
2943
+ const title = args.title || "Obsidian";
2944
+ const message = args.message;
2945
+ const sound = args.sound || "Glass";
2946
+ const speak = args.speak !== false;
2947
+ if (!message) {
2948
+ return { success: false, error: "Message is required" };
2949
+ }
2950
+ try {
2951
+ if (process.platform === "darwin") {
2952
+ process.stdout.write("\x07");
2953
+ await execAsync2(`afplay /System/Library/Sounds/${sound}.aiff`).catch(() => {
2954
+ });
2955
+ const escapedMessage = message.replace(/"/g, '\\"');
2956
+ const escapedTitle = title.replace(/"/g, '\\"');
2957
+ await execAsync2(`osascript -e 'display notification "${escapedMessage}" with title "${escapedTitle}" sound name "${sound}"'`).catch(() => {
2958
+ });
2959
+ if (speak) {
2960
+ const safeMsg = message.replace(/["'$`\\]/g, "");
2961
+ await execAsync2(`say -v Daniel "${safeMsg}"`).catch(() => {
2962
+ });
2963
+ }
2964
+ } else if (process.platform === "linux") {
2965
+ await execAsync2(`notify-send "${title}" "${message}"`).catch(() => {
2966
+ });
2967
+ }
2968
+ return {
2969
+ success: true,
2970
+ output: `Notification sent: "${title}: ${message}" (sound: ${sound}, voice: ${speak ? "on" : "off"})`
2971
+ };
2972
+ } catch (error) {
2973
+ return { success: false, error: `Notification failed: ${error instanceof Error ? error.message : String(error)}` };
2974
+ }
2975
+ }
2976
+ };
2977
+ var CreateSkillTool = {
2978
+ name: "create_skill",
2979
+ description: "Create a new autonomous skill (tool) for the agent. This tool writes the implementation, runs tests, and registers it dynamically. The code MUST be a valid Node.js module that exports default a Tool object.",
2980
+ inputSchema: {
2981
+ name: {
2982
+ type: "string",
2983
+ description: 'Name of the tool (e.g., "jira_issue_create")'
2984
+ },
2985
+ description: {
2986
+ type: "string",
2987
+ description: "What the tool does"
2988
+ },
2989
+ code: {
2990
+ type: "string",
2991
+ description: "Node.js code for the tool. Must export default a Tool object."
2992
+ }
2993
+ },
2994
+ requiredParams: ["name", "description", "code"],
2995
+ async execute(args) {
2996
+ const name = args.name;
2997
+ const code = args.code;
2998
+ const skillsDir = path5.join(os4.homedir(), ".obsidian-next", "skills");
2999
+ const skillPath = path5.join(skillsDir, `${name}.js`);
3000
+ try {
3001
+ if (!fsSync.existsSync(skillsDir)) {
3002
+ fsSync.mkdirSync(skillsDir, { recursive: true });
3003
+ }
3004
+ await fs5.writeFile(skillPath, code, "utf-8");
3005
+ const module = await import(`file://${skillPath}?t=${Date.now()}`);
3006
+ if (module.default && module.default.name) {
3007
+ tools.register(module.default);
3008
+ return {
3009
+ success: true,
3010
+ output: `Skill '${name}' created and registered successfully. It is now available for use.`
3011
+ };
3012
+ }
3013
+ return { success: false, error: "Skill code must export default a Tool object." };
3014
+ } catch (error) {
3015
+ return {
3016
+ success: false,
3017
+ error: `Failed to create skill: ${error instanceof Error ? error.message : String(error)}`
3018
+ };
3019
+ }
3020
+ }
3021
+ };
3022
+ var ToolRegistry = class {
3023
+ tools = /* @__PURE__ */ new Map();
3024
+ skillsDir = path5.join(os4.homedir(), ".obsidian-next", "skills");
3025
+ constructor() {
3026
+ this.register(BashTool);
3027
+ this.register(ReadTool);
3028
+ this.register(WriteTool);
3029
+ this.register(EditTool);
3030
+ this.register(ListTool);
3031
+ this.register(GrepTool);
3032
+ this.register(GlobTool);
3033
+ this.register(TaskTool);
3034
+ this.register(WebFetchTool);
3035
+ this.register(MCPManagementTool);
3036
+ this.register(ScheduleTool);
3037
+ this.register(ListScheduledTasksTool);
3038
+ this.register(UnscheduleTool);
3039
+ this.register(MemoryTool);
3040
+ this.register(ComputerUseTool);
3041
+ this.register(CreateSkillTool);
3042
+ this.register(HttpRequestTool);
3043
+ this.register(DeleteTool);
3044
+ this.register(NotifyTool);
3045
+ }
3046
+ async init() {
3047
+ await this.loadSkills();
3048
+ }
3049
+ async loadSkills() {
3050
+ if (!fsSync.existsSync(this.skillsDir)) {
3051
+ fsSync.mkdirSync(this.skillsDir, { recursive: true });
3052
+ }
3053
+ try {
3054
+ const files = await fs5.readdir(this.skillsDir);
3055
+ for (const file of files) {
3056
+ if (file.endsWith(".js")) {
3057
+ try {
3058
+ const skillPath = path5.join(this.skillsDir, file);
3059
+ const module = await import(`file://${skillPath}`);
3060
+ if (module.default && module.default.name) {
3061
+ this.register(module.default);
3062
+ }
3063
+ } catch (e) {
3064
+ console.error(`Failed to load skill ${file}:`, e);
3065
+ }
3066
+ }
3067
+ }
3068
+ } catch (e) {
3069
+ console.error("Failed to read skills directory:", e);
3070
+ }
3071
+ }
3072
+ register(tool) {
3073
+ this.tools.set(tool.name, tool);
3074
+ }
3075
+ has(name) {
3076
+ return this.tools.has(name);
3077
+ }
3078
+ get(name) {
3079
+ return this.tools.get(name);
3080
+ }
3081
+ async list() {
3082
+ const staticTools = Array.from(this.tools.values());
3083
+ try {
3084
+ const dynamicTools = await mcp.listTools();
3085
+ const mcpAdapters = dynamicTools.map((dt) => {
3086
+ const mcpTool = dt;
3087
+ return {
3088
+ name: `${mcpTool.server}_${mcpTool.name}`,
3089
+ description: `[MCP: ${mcpTool.server}] ${mcpTool.description || ""}`,
3090
+ inputSchema: mcpTool.inputSchema?.properties || {},
3091
+ requiredParams: mcpTool.inputSchema?.required || [],
3092
+ execute: async (args) => {
3093
+ const result = await mcp.callTool(mcpTool.server, mcpTool.name, args);
3094
+ if (result.isError) {
3095
+ return { success: false, error: "MCP Tool Error" };
3096
+ }
3097
+ const output = result.content.filter((c) => "text" in c).map((c) => c.text).join("\n");
3098
+ return { success: !result.isError, output, content: result.content };
3099
+ }
3100
+ };
3101
+ });
3102
+ return [...staticTools, ...mcpAdapters];
3103
+ } catch (error) {
3104
+ bus.emitAgent({ type: "thought", content: `[MCP] Failed to list tools: ${error instanceof Error ? error.message : String(error)}`, hidden: true });
3105
+ return staticTools;
3106
+ }
3107
+ }
3108
+ async execute(name, args) {
3109
+ let tool = this.tools.get(name);
3110
+ if (!tool) {
3111
+ const mcpTools = await this.list();
3112
+ tool = mcpTools.find((t) => t.name === name);
3113
+ }
3114
+ if (!tool) {
3115
+ return {
3116
+ success: false,
3117
+ error: `Unknown tool: ${name}. Available: ${(await this.list()).map((t) => t.name).join(", ")}`
3118
+ };
3119
+ }
3120
+ bus.emitAgent({
3121
+ type: "tool_start",
3122
+ tool: name,
3123
+ args: JSON.stringify(args, null, 2)
3124
+ });
3125
+ const result = await toolLane.enqueue(() => tool.execute(args));
3126
+ const rawOutput = result.success ? result.output || "Success" : result.error || "Failed";
3127
+ const redacted = redactor.redactToolOutput(name, rawOutput);
3128
+ bus.emitAgent({
3129
+ type: "tool_result",
3130
+ tool: name,
3131
+ output: redacted.text,
3132
+ isError: !result.success
3133
+ });
3134
+ return result;
3135
+ }
3136
+ };
3137
+ function parseHumanInterval(input) {
3138
+ const s = input.trim().toLowerCase();
3139
+ const dayMap = {
3140
+ sunday: "0",
3141
+ sun: "0",
3142
+ monday: "1",
3143
+ mon: "1",
3144
+ tuesday: "2",
3145
+ tue: "2",
3146
+ wednesday: "3",
3147
+ wed: "3",
3148
+ thursday: "4",
3149
+ thu: "4",
3150
+ friday: "5",
3151
+ fri: "5",
3152
+ saturday: "6",
3153
+ sat: "6"
3154
+ };
3155
+ function parseTime(str) {
3156
+ const atMatch = str.match(/at\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)?/i);
3157
+ if (atMatch) {
3158
+ let hour = parseInt(atMatch[1], 10);
3159
+ const minute = atMatch[2] ? parseInt(atMatch[2], 10) : 0;
3160
+ const ampm = atMatch[3]?.toLowerCase();
3161
+ if (ampm === "pm" && hour < 12) hour += 12;
3162
+ if (ampm === "am" && hour === 12) hour = 0;
3163
+ return { hour, minute };
3164
+ }
3165
+ return { hour: 9, minute: 0 };
3166
+ }
3167
+ const minMatch = s.match(/^every\s+(\d+)\s*(?:m|min|mins|minutes?)$/);
3168
+ if (minMatch) {
3169
+ const n = parseInt(minMatch[1], 10);
3170
+ if (n < 1 || n > 59) return null;
3171
+ return `*/${n} * * * *`;
3172
+ }
3173
+ const hourMatch = s.match(/^every\s+(\d+)\s*(?:h|hrs?|hours?)$/);
3174
+ if (hourMatch) {
3175
+ const n = parseInt(hourMatch[1], 10);
3176
+ if (n < 1 || n > 23) return null;
3177
+ return `0 */${n} * * *`;
3178
+ }
3179
+ if (s === "hourly") return "0 * * * *";
3180
+ if (s.startsWith("daily")) {
3181
+ const { hour, minute } = parseTime(s);
3182
+ return `${minute} ${hour} * * *`;
3183
+ }
3184
+ if (s.startsWith("weekly")) {
3185
+ const { hour, minute } = parseTime(s);
3186
+ return `${minute} ${hour} * * 1`;
3187
+ }
3188
+ if (s.includes("weekday")) {
3189
+ const { hour, minute } = parseTime(s);
3190
+ return `${minute} ${hour} * * 1-5`;
3191
+ }
3192
+ if (s.includes("weekend")) {
3193
+ const { hour, minute } = parseTime(s);
3194
+ return `${minute} ${hour} * * 0,6`;
3195
+ }
3196
+ for (const [dayName, dayNum] of Object.entries(dayMap)) {
3197
+ if (s.includes(dayName)) {
3198
+ const { hour, minute } = parseTime(s);
3199
+ return `${minute} ${hour} * * ${dayNum}`;
3200
+ }
3201
+ }
3202
+ return null;
3203
+ }
3204
+ function parseOneTimeSchedule(input) {
3205
+ const s = input.trim().toLowerCase();
3206
+ const dayMap = {
3207
+ sunday: 0,
3208
+ sun: 0,
3209
+ monday: 1,
3210
+ mon: 1,
3211
+ tuesday: 2,
3212
+ tue: 2,
3213
+ wednesday: 3,
3214
+ wed: 3,
3215
+ thursday: 4,
3216
+ thu: 4,
3217
+ friday: 5,
3218
+ fri: 5,
3219
+ saturday: 6,
3220
+ sat: 6
3221
+ };
3222
+ function parseTime(str) {
3223
+ const atMatch = str.match(/at\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)?/i);
3224
+ if (atMatch) {
3225
+ let hour = parseInt(atMatch[1], 10);
3226
+ const minute = atMatch[2] ? parseInt(atMatch[2], 10) : 0;
3227
+ const ampm = atMatch[3]?.toLowerCase();
3228
+ if (ampm === "pm" && hour < 12) hour += 12;
3229
+ if (ampm === "am" && hour === 12) hour = 0;
3230
+ return { hour, minute };
3231
+ }
3232
+ return { hour: 9, minute: 0 };
3233
+ }
3234
+ const inMinMatch = s.match(/^in\s+(\d+)\s*(?:m|min|mins|minutes?)$/);
3235
+ if (inMinMatch) {
3236
+ const d = /* @__PURE__ */ new Date();
3237
+ d.setMinutes(d.getMinutes() + parseInt(inMinMatch[1], 10));
3238
+ return d;
3239
+ }
3240
+ const inHourMatch = s.match(/^in\s+(\d+)\s*(?:h|hrs?|hours?)$/);
3241
+ if (inHourMatch) {
3242
+ const d = /* @__PURE__ */ new Date();
3243
+ d.setHours(d.getHours() + parseInt(inHourMatch[1], 10));
3244
+ return d;
3245
+ }
3246
+ if (s.startsWith("tomorrow")) {
3247
+ const { hour, minute } = parseTime(s);
3248
+ const d = /* @__PURE__ */ new Date();
3249
+ d.setDate(d.getDate() + 1);
3250
+ d.setHours(hour, minute, 0, 0);
3251
+ return d;
3252
+ }
3253
+ for (const [dayName, dayNum] of Object.entries(dayMap)) {
3254
+ if (s.includes(dayName)) {
3255
+ const { hour, minute } = parseTime(s);
3256
+ const now = /* @__PURE__ */ new Date();
3257
+ const currentDay = now.getDay();
3258
+ let daysUntil = dayNum - currentDay;
3259
+ if (daysUntil <= 0) daysUntil += 7;
3260
+ const d = /* @__PURE__ */ new Date();
3261
+ d.setDate(d.getDate() + daysUntil);
3262
+ d.setHours(hour, minute, 0, 0);
3263
+ return d;
3264
+ }
3265
+ }
3266
+ return null;
3267
+ }
3268
+ var ScheduleTool = {
3269
+ name: "schedule_task",
3270
+ description: `Schedule a background task. Supports recurring cron jobs and one-time scheduled events.
3271
+
3272
+ RECURRING - use "interval" (human-readable) or "cron" (raw expression):
3273
+ interval: "every 5m", "every 2h", "hourly", "daily at 3pm", "weekly", "every monday at 9am", "every weekday at 9am"
3274
+ cron: "*/5 * * * *", "0 9 * * 1-5" (standard 5-field cron)
3275
+
3276
+ ONE-TIME - use "at" for a single future execution:
3277
+ at: "tomorrow at 3pm", "tuesday at 10am", "in 30 minutes", "in 2 hours"
3278
+
3279
+ ABILITIES (what to execute):
3280
+ system:notify - Sound alert + native notification + voice (params: title, message, sound, speak)
3281
+ system:bash - Run a shell command (params: command)
3282
+ system:echo - Log a message (params: message)
3283
+ system:summary - Show usage/cost stats
3284
+ system:heartbeat - Silent liveness check
3285
+ system:audit - Proactive security scan of workspace
3286
+ system:index - Regenerate codebase MAP.md
3287
+
3288
+ Provide EITHER cron/interval (recurring) OR at (one-time), plus ability and optional params.`,
3289
+ inputSchema: {
3290
+ cron: {
3291
+ type: "string",
3292
+ description: 'Raw cron expression (e.g., "*/5 * * * *", "0 9 * * 1-5"). Use this OR interval, not both.'
3293
+ },
3294
+ interval: {
3295
+ type: "string",
3296
+ description: 'Human-readable interval (e.g., "every 5m", "daily at 3pm", "every monday at 9am"). Converted to cron internally.'
3297
+ },
3298
+ at: {
3299
+ type: "string",
3300
+ description: 'One-time schedule (e.g., "tomorrow at 3pm", "tuesday at 10am", "in 30 minutes"). Task runs once then deactivates.'
3301
+ },
3302
+ ability: {
3303
+ type: "string",
3304
+ description: "Ability to execute: system:notify, system:bash, system:echo, system:summary, system:heartbeat, system:audit, system:index"
3305
+ },
3306
+ params: {
3307
+ type: "string",
3308
+ description: 'JSON string of parameters. For system:notify: {"title":"...","message":"...","sound":"Glass","speak":true}. For system:bash: {"command":"..."}. For system:echo: {"message":"..."}'
3309
+ }
3310
+ },
3311
+ requiredParams: ["ability"],
3312
+ async execute(args) {
3313
+ const cronRaw = args.cron;
3314
+ const interval = args.interval;
3315
+ const at = args.at;
3316
+ const ability = args.ability;
3317
+ const paramsStr = args.params || "{}";
3318
+ if (!ability) {
3319
+ return { success: false, error: "Ability name is required" };
3320
+ }
3321
+ let params = {};
3322
+ try {
3323
+ params = JSON.parse(paramsStr);
3324
+ } catch {
3325
+ return { success: false, error: "Invalid JSON parameters" };
3326
+ }
3327
+ if (at) {
3328
+ const targetDate = parseOneTimeSchedule(at);
3329
+ if (!targetDate) {
3330
+ return { success: false, error: `Could not parse one-time schedule: "${at}". Try "tomorrow at 3pm", "tuesday at 10am", or "in 30 minutes".` };
3331
+ }
3332
+ if (targetDate.getTime() <= Date.now()) {
3333
+ return { success: false, error: `Scheduled time is in the past: ${targetDate.toLocaleString()}` };
3334
+ }
3335
+ const cronExpr2 = `${targetDate.getMinutes()} ${targetDate.getHours()} ${targetDate.getDate()} ${targetDate.getMonth() + 1} *`;
3336
+ try {
3337
+ const task = await scheduler.scheduleTask(cronExpr2, ability, { ...params, __once: true, __target: targetDate.getTime() });
3338
+ return {
3339
+ success: true,
3340
+ output: `Scheduled one-time task ${task.id}: ${ability} at ${targetDate.toLocaleString()}`
3341
+ };
3342
+ } catch (error) {
3343
+ return { success: false, error: `Failed to schedule: ${error instanceof Error ? error.message : String(error)}` };
3344
+ }
3345
+ }
3346
+ if (!cronRaw && !interval) {
3347
+ return { success: false, error: 'Provide cron, interval, or at. Examples: cron="*/5 * * * *", interval="every 5m", at="tomorrow at 3pm"' };
3348
+ }
3349
+ if (cronRaw && interval) {
3350
+ return { success: false, error: "Provide either cron or interval, not both." };
3351
+ }
3352
+ let cronExpr;
3353
+ if (interval) {
3354
+ const parsed = parseHumanInterval(interval);
3355
+ if (!parsed) {
3356
+ return { success: false, error: `Could not parse interval: "${interval}". Try "every 5m", "hourly", "daily at 3pm", "every monday at 9am".` };
3357
+ }
3358
+ cronExpr = parsed;
3359
+ } else {
3360
+ cronExpr = cronRaw;
3361
+ }
3362
+ try {
3363
+ const task = await scheduler.scheduleTask(cronExpr, ability, params);
3364
+ const source = interval ? `interval="${interval}" -> cron="${cronExpr}"` : `cron="${cronExpr}"`;
3365
+ return {
3366
+ success: true,
3367
+ output: `Scheduled recurring task ${task.id}: ${ability} (${source})`
3368
+ };
3369
+ } catch (error) {
3370
+ return {
3371
+ success: false,
3372
+ error: `Failed to schedule task: ${error instanceof Error ? error.message : String(error)}`
3373
+ };
3374
+ }
3375
+ }
3376
+ };
3377
+ var ListScheduledTasksTool = {
3378
+ name: "list_scheduled_tasks",
3379
+ description: 'List all scheduled/recurring background cron jobs. Use this when the user asks "what tasks are scheduled", "show scheduled tasks", "check scheduled jobs", "list cron jobs", or any variation asking about background recurring tasks.',
3380
+ inputSchema: {},
3381
+ requiredParams: [],
3382
+ async execute(args) {
3383
+ try {
3384
+ const tasks2 = scheduler.listTasks();
3385
+ if (tasks2.length === 0) {
3386
+ return { success: true, output: "No active scheduled tasks." };
3387
+ }
3388
+ const header = `ID | CRON | COMMAND | LAST RUN | NEXT RUN
3389
+ ${"-".repeat(80)}`;
3390
+ const rows = tasks2.map((t) => {
3391
+ const last = t.last_run_at ? new Date(t.last_run_at).toLocaleString() : "Never";
3392
+ const next = t.next_run_at ? new Date(t.next_run_at).toLocaleString() : "Unknown";
3393
+ return `${t.id} | ${t.cron_expression} | ${t.command} | ${last} | ${next}`;
3394
+ }).join("\n");
3395
+ return {
3396
+ success: true,
3397
+ output: `${header}
3398
+ ${rows}`
3399
+ };
3400
+ } catch (error) {
3401
+ return {
3402
+ success: false,
3403
+ error: `Failed to list tasks: ${error instanceof Error ? error.message : String(error)}`
3404
+ };
3405
+ }
3406
+ }
3407
+ };
3408
+ var tools = new ToolRegistry();
3409
+
3410
+ // src/core/llm.ts
3411
+ var MAX_TOOL_ITERATIONS = 67;
3412
+ var CONTEXT = {
3413
+ MAX_MESSAGES: 40,
3414
+ KEEP_FIRST: 2,
3415
+ KEEP_LAST: 15,
3416
+ BUFFER: 5,
3417
+ TOKEN_LIMIT_WARN: 0.8,
3418
+ TOKEN_LIMIT_PRUNE: 0.9,
3419
+ TOKEN_LIMIT_STOP: 0.98,
3420
+ MAX_TOKENS_TOTAL: 2e5
3421
+ };
3422
+ var LLMClient = class {
3423
+ client = null;
3424
+ lastConfig = null;
3425
+ conversationHistory = [];
3426
+ toolIterations = 0;
3427
+ accumulatedInputTokens = 0;
3428
+ accumulatedOutputTokens = 0;
3429
+ accumulatedCacheReadTokens = 0;
3430
+ accumulatedCacheCreationTokens = 0;
3431
+ abortController = null;
3432
+ currentInterruptHandler = null;
3433
+ lastToolOutputs = [];
3434
+ constructor() {
3435
+ bus.on("agent", (event) => {
3436
+ if (event.type === "computer_scale_update" && this.computerUseState.enabled) {
3437
+ this.computerUseState.scale = event.scale ?? this.computerUseState.scale;
3438
+ this.computerUseState.scaledWidth = event.scaledWidth ?? this.computerUseState.scaledWidth;
3439
+ this.computerUseState.scaledHeight = event.scaledHeight ?? this.computerUseState.scaledHeight;
3440
+ this.computerUseState.displayWidth = event.nativeWidth ?? this.computerUseState.displayWidth;
3441
+ this.computerUseState.displayHeight = event.nativeHeight ?? this.computerUseState.displayHeight;
3442
+ }
3443
+ });
3444
+ }
3445
+ // Computer Use State - proper Anthropic API integration
3446
+ computerUseState = {
3447
+ enabled: false,
3448
+ displayWidth: 1920,
3449
+ displayHeight: 1080,
3450
+ scale: 1,
3451
+ scaledWidth: 1920,
3452
+ scaledHeight: 1080
3453
+ };
3454
+ async initialize() {
3455
+ const cfg = await config.load();
3456
+ await usage.init();
3457
+ const envFileCheck = await detectEnvFile(cfg.workspaceRoot);
3458
+ if (envFileCheck.found) {
3459
+ bus.emitAgent({
3460
+ type: "thought",
3461
+ content: `[WARN] Found .env file with API key at ${envFileCheck.path}. For security, run /init to migrate to secure storage.`,
3462
+ hidden: false
3463
+ });
3464
+ }
3465
+ if (config.hasDeprecatedKey()) {
3466
+ const deprecatedKey = config.getDeprecatedApiKey();
3467
+ if (deprecatedKey) {
3468
+ bus.emitAgent({
3469
+ type: "thought",
3470
+ content: "[WARN] Found API key in config file. Migrating to secure storage...",
3471
+ hidden: false
3472
+ });
3473
+ const result = await keyManager.storeKey(deprecatedKey);
3474
+ if (result.success) {
3475
+ await config.removeApiKeyFromConfig();
3476
+ bus.emitAgent({
3477
+ type: "thought",
3478
+ content: `[INFO] API key migrated to ${result.backend}. Config file cleaned.`,
3479
+ hidden: false
3480
+ });
3481
+ }
3482
+ }
3483
+ }
3484
+ const apiKey = await keyManager.loadKey();
3485
+ if (!apiKey) {
3486
+ bus.emitAgent({
3487
+ type: "error",
3488
+ message: "Missing API key. Run /init to set up secure key storage, or set ANTHROPIC_API_KEY environment variable."
3489
+ });
3490
+ return false;
3491
+ }
3492
+ this.client = new Anthropic({
3493
+ apiKey
3494
+ });
3495
+ this.lastConfig = cfg;
3496
+ const backend = keyManager.getBackend();
3497
+ if (backend && backend !== "env") {
3498
+ bus.emitAgent({
3499
+ type: "thought",
3500
+ content: `[INFO] API key loaded from: ${backend}`,
3501
+ hidden: true
3502
+ });
3503
+ }
3504
+ return true;
3505
+ }
3506
+ /**
3507
+ * Refresh the API client if key has rotated
3508
+ */
3509
+ async refreshIfNeeded() {
3510
+ if (keyManager.shouldRotate()) {
3511
+ const newKey = await keyManager.refreshKey();
3512
+ if (newKey) {
3513
+ this.client = new Anthropic({ apiKey: newKey });
3514
+ return true;
3515
+ }
3516
+ }
3517
+ return false;
3518
+ }
3519
+ /**
3520
+ * Count tokens for a potential message before sending
3521
+ * Uses Anthropic's countTokens API for accurate estimation
3522
+ * Returns null on failure (caller should fall back to heuristic)
3523
+ */
3524
+ async countTokens(systemPrompt, messages, tools2) {
3525
+ if (!this.client) return null;
3526
+ try {
3527
+ const modelMap = {
3528
+ "claude-opus-4-6": "claude-opus-4-6-20260207",
3529
+ "claude-sonnet-4-5": "claude-sonnet-4-5-20250929",
3530
+ "claude-haiku-4-5": "claude-haiku-4-5-20251001"
3531
+ };
3532
+ const requestedModel = this.lastConfig?.model || "claude-opus-4-6-20260207";
3533
+ const model = modelMap[requestedModel] || requestedModel;
3534
+ const response = await this.client.messages.countTokens({
3535
+ model,
3536
+ system: systemPrompt,
3537
+ messages,
3538
+ tools: tools2
3539
+ });
3540
+ return response.input_tokens;
3541
+ } catch (error) {
3542
+ bus.emitAgent({
3543
+ type: "thought",
3544
+ content: "[Context] Token count API unavailable, using heuristic.",
3545
+ hidden: true
3546
+ });
3547
+ return null;
3548
+ }
3549
+ }
3550
+ /**
3551
+ * Enable Computer Use mode with proper Anthropic beta API
3552
+ * This activates:
3553
+ * - Beta header (computer-use-2025-01-24 or computer-use-2025-11-24)
3554
+ * - Anthropic-defined schema-less tools
3555
+ * - Coordinate scaling for high-res displays
3556
+ */
3557
+ async enableComputerUse() {
3558
+ const dims = await getDisplayDimensions();
3559
+ const scale = calculateScaleForAPI(dims.width, dims.height);
3560
+ this.computerUseState = {
3561
+ enabled: true,
3562
+ displayWidth: dims.width,
3563
+ displayHeight: dims.height,
3564
+ scale,
3565
+ scaledWidth: Math.floor(dims.width * scale),
3566
+ scaledHeight: Math.floor(dims.height * scale)
3567
+ };
3568
+ bus.emitAgent({
3569
+ type: "thought",
3570
+ content: `[Computer Use] Enabled. Display: ${dims.width}x${dims.height}, API Scale: ${scale.toFixed(3)} -> ${this.computerUseState.scaledWidth}x${this.computerUseState.scaledHeight}`,
3571
+ hidden: false
3572
+ });
3573
+ }
3574
+ /**
3575
+ * Disable Computer Use mode
3576
+ */
3577
+ disableComputerUse() {
3578
+ this.computerUseState.enabled = false;
3579
+ bus.emitAgent({
3580
+ type: "thought",
3581
+ content: "[Computer Use] Disabled.",
3582
+ hidden: false
3583
+ });
3584
+ }
3585
+ /**
3586
+ * Check if Computer Use mode is enabled
3587
+ */
3588
+ isComputerUseEnabled() {
3589
+ return this.computerUseState.enabled;
3590
+ }
3591
+ /**
3592
+ * Update the scale factor (called after screenshot to ensure accuracy)
3593
+ */
3594
+ updateComputerScale(actualScale, scaledWidth, scaledHeight) {
3595
+ if (this.computerUseState.enabled && actualScale > 0) {
3596
+ this.computerUseState.scale = actualScale;
3597
+ this.computerUseState.scaledWidth = scaledWidth;
3598
+ this.computerUseState.scaledHeight = scaledHeight;
3599
+ this.computerUseState.displayWidth = Math.round(scaledWidth / actualScale);
3600
+ this.computerUseState.displayHeight = Math.round(scaledHeight / actualScale);
3601
+ }
3602
+ }
3603
+ /**
3604
+ * Prune old images from conversation history to prevent context explosion
3605
+ * Keeps only the most recent N images, replacing older ones with text placeholders
3606
+ */
3607
+ pruneImagesFromHistory(keepCount) {
3608
+ const imageLocations = [];
3609
+ for (let i = this.conversationHistory.length - 1; i >= 0; i--) {
3610
+ const msg = this.conversationHistory[i];
3611
+ if (Array.isArray(msg.content)) {
3612
+ for (let j = msg.content.length - 1; j >= 0; j--) {
3613
+ const block = msg.content[j];
3614
+ if (block.type === "image" || block.type === "tool_result" && Array.isArray(block.content)) {
3615
+ if (block.type === "tool_result" && Array.isArray(block.content)) {
3616
+ for (let k = block.content.length - 1; k >= 0; k--) {
3617
+ if (block.content[k].type === "image") {
3618
+ imageLocations.push({ msgIdx: i, blockIdx: j });
3619
+ break;
3620
+ }
3621
+ }
3622
+ } else if (block.type === "image") {
3623
+ imageLocations.push({ msgIdx: i, blockIdx: j });
3624
+ }
3625
+ }
3626
+ }
3627
+ }
3628
+ }
3629
+ const toRemove = imageLocations.slice(keepCount);
3630
+ for (const loc of toRemove) {
3631
+ const msg = this.conversationHistory[loc.msgIdx];
3632
+ if (Array.isArray(msg.content)) {
3633
+ const block = msg.content[loc.blockIdx];
3634
+ if (block.type === "tool_result" && Array.isArray(block.content)) {
3635
+ block.content = block.content.map(
3636
+ (c) => c.type === "image" ? { type: "text", text: "[Previous screenshot removed to save context]" } : c
3637
+ );
3638
+ } else if (block.type === "image") {
3639
+ msg.content[loc.blockIdx] = {
3640
+ type: "text",
3641
+ text: "[Previous screenshot removed to save context]"
3642
+ };
3643
+ }
3644
+ }
3645
+ }
3646
+ if (toRemove.length > 0) {
3647
+ bus.emitAgent({
3648
+ type: "thought",
3649
+ content: `[Context] Pruned ${toRemove.length} old screenshot(s) to save context.`,
3650
+ hidden: true
3651
+ });
3652
+ }
3653
+ }
3654
+ async streamChat(userMessage, options) {
3655
+ if (!this.client) {
3656
+ const initialized = await this.initialize();
3657
+ if (!initialized || !this.client) return null;
3658
+ }
3659
+ try {
3660
+ const modelMap = {
3661
+ // New 4.6 / 4.5 Aliases
3662
+ "claude-opus-4-6": "claude-opus-4-6-20260207",
3663
+ "claude-sonnet-4-5": "claude-sonnet-4-5-20250929",
3664
+ "claude-haiku-4-5": "claude-haiku-4-5-20251001",
3665
+ "claude-opus-4-5": "claude-opus-4-5-20251101",
3666
+ "ollama": "llama3"
3667
+ };
3668
+ const requestedModel = this.lastConfig?.model || "claude-opus-4-6-20260207";
3669
+ let apiModel = modelMap[requestedModel] || requestedModel;
3670
+ const currentUsage = Math.max(this.accumulatedInputTokens, usage.getContextUsage(apiModel).used);
3671
+ if (currentUsage > CONTEXT.MAX_TOKENS_TOTAL * CONTEXT.TOKEN_LIMIT_STOP) {
3672
+ bus.emitAgent({
3673
+ type: "error",
3674
+ message: `[SAFETY] Context limit reached (${(currentUsage / 1e3).toFixed(1)}k). Please run /clear to reset.`
3675
+ });
3676
+ return null;
3677
+ }
3678
+ if (currentUsage > CONTEXT.MAX_TOKENS_TOTAL * CONTEXT.TOKEN_LIMIT_PRUNE) {
3679
+ bus.emitAgent({
3680
+ type: "thought",
3681
+ content: `[SAFETY] CRITICAL CONTEXT LEVEL (${(currentUsage / 1e3).toFixed(1)}k). Aggressive pruning engaged.`
3682
+ });
3683
+ await this.compressHistory();
3684
+ } else if (currentUsage > CONTEXT.MAX_TOKENS_TOTAL * CONTEXT.TOKEN_LIMIT_WARN) {
3685
+ bus.emitAgent({
3686
+ type: "thought",
3687
+ content: `[WARN] High context usage (${(currentUsage / 1e3).toFixed(1)}k). Consider resetting soon.`
3688
+ });
3689
+ }
3690
+ if (userMessage.trim()) {
3691
+ this.toolIterations = 0;
3692
+ this.lastToolOutputs = [];
3693
+ this.accumulatedInputTokens = 0;
3694
+ this.accumulatedOutputTokens = 0;
3695
+ this.accumulatedCacheReadTokens = 0;
3696
+ this.accumulatedCacheCreationTokens = 0;
3697
+ this.conversationHistory.push({
3698
+ role: "user",
3699
+ content: userMessage
3700
+ });
3701
+ } else {
3702
+ this.toolIterations++;
3703
+ if (this.toolIterations > MAX_TOOL_ITERATIONS) {
3704
+ bus.emitAgent({
3705
+ type: "error",
3706
+ message: `Tool iteration limit (${MAX_TOOL_ITERATIONS}) exceeded. Stopping to prevent infinite loop.`
3707
+ });
3708
+ return null;
3709
+ }
3710
+ }
3711
+ const availableTools = await tools.list();
3712
+ let toolDefinitionsForApi = [];
3713
+ let computerUseToolsForApi = [];
3714
+ let usesBetaApi = false;
3715
+ if (this.computerUseState.enabled) {
3716
+ const { toolVersion, betaFlag } = getToolConfig(apiModel);
3717
+ usesBetaApi = true;
3718
+ computerUseToolsForApi.push({
3719
+ type: toolVersion,
3720
+ name: "computer",
3721
+ display_width_px: this.computerUseState.scaledWidth,
3722
+ display_height_px: this.computerUseState.scaledHeight,
3723
+ display_number: 1,
3724
+ // Enable zoom for Opus 4.5
3725
+ ...toolVersion === "computer_20251124" ? { enable_zoom: true } : {}
3726
+ });
3727
+ computerUseToolsForApi.push({
3728
+ type: "text_editor_20250728",
3729
+ name: "str_replace_based_edit_tool"
3730
+ });
3731
+ computerUseToolsForApi.push({
3732
+ type: "bash_20250124",
3733
+ name: "bash"
3734
+ });
3735
+ const conflictingTools = ["computer", "bash"];
3736
+ const filteredTools = availableTools.filter((t) => !conflictingTools.includes(t.name));
3737
+ toolDefinitionsForApi = filteredTools.map((tool) => ({
3738
+ name: tool.name,
3739
+ description: tool.description,
3740
+ input_schema: {
3741
+ type: "object",
3742
+ properties: tool.inputSchema,
3743
+ required: tool.requiredParams
3744
+ }
3745
+ }));
3746
+ } else {
3747
+ toolDefinitionsForApi = availableTools.map((tool) => ({
3748
+ name: tool.name,
3749
+ description: tool.description,
3750
+ input_schema: {
3751
+ type: "object",
3752
+ properties: tool.inputSchema,
3753
+ required: tool.requiredParams
3754
+ }
3755
+ }));
3756
+ }
3757
+ const allToolsForApi = [...computerUseToolsForApi, ...toolDefinitionsForApi];
3758
+ if (options?.allowedTools) {
3759
+ const filtered = allToolsForApi.filter(
3760
+ (t) => t.type || options.allowedTools.includes(t.name)
3761
+ );
3762
+ toolDefinitionsForApi = filtered;
3763
+ } else {
3764
+ toolDefinitionsForApi = allToolsForApi;
3765
+ }
3766
+ const mcpStatus = mcp.getStatus();
3767
+ const activeServers = mcpStatus.filter((s) => s.connected).map((s) => s.name);
3768
+ const offlineServers = mcpStatus.filter((s) => !s.connected).map((s) => s.name);
3769
+ const registry = listRegistry();
3770
+ const installableServers = registry.filter((r) => !mcpStatus.find((s) => s.name === r.name));
3771
+ const activeList = availableTools.map((t) => `- ${t.name}: ${t.description}`).join("\n");
3772
+ const offlineList = offlineServers.map((n) => {
3773
+ const def = registry.find((r) => r.name === n);
3774
+ return `- ${n}: ${def?.description || "Configured server"} (run 'mcp_manage connect ${n}' to use)`;
3775
+ }).join("\n");
3776
+ const registryList = installableServers.map((r) => `- ${r.name}: ${r.description} (run 'mcp_manage install ${r.name}')`).join("\n");
3777
+ await this.compressHistory();
3778
+ let userContext = "";
3779
+ let memoryAvailable = true;
3780
+ try {
3781
+ const { memory } = await import("./memory-QZTBTYPH.js");
3782
+ userContext = await memory.getUserContext();
3783
+ } catch (e) {
3784
+ memoryAvailable = false;
3785
+ bus.emitAgent({
3786
+ type: "thought",
3787
+ content: "[WARN] Memory system unavailable. Personalization disabled.",
3788
+ hidden: true
3789
+ });
3790
+ }
3791
+ const currentMode = (await import("./context-NYOIRZKV.js")).context.getMode();
3792
+ const ctxUsage = usage.getContextUsage(apiModel);
3793
+ const tokenBudget = CONTEXT.MAX_TOKENS_TOTAL;
3794
+ const tokensUsed = ctxUsage.used;
3795
+ const tokensRemaining = tokenBudget - tokensUsed;
3796
+ const cfg = await config.load();
3797
+ const systemPromptBlock = {
3798
+ type: "text",
3799
+ text: `You are Obsidian, a CLI engineering agent. You run inside a terminal.
3800
+
3801
+ OUTPUT RULES:
3802
+ - This is a CLI. Your output renders in a monospace terminal, NOT a browser.
3803
+ - NEVER use Markdown formatting: no **, no ##, no *, no >, no \`backticks\`, no [links](). The terminal does not render any of it and it looks broken.
3804
+ - Write plain text only. Use CAPS, dashes, or indentation for structure.
3805
+ - Keep responses short and direct. One to three sentences for simple answers. Longer only when the task demands it.
3806
+ - When listing items, use plain dashes or numbers. No bullets, no bold, no headers.
3807
+
3808
+ TONE:
3809
+ - Direct, blunt, concise. You are a peer, not an assistant.
3810
+ - Never sycophantic. No "Great question!", no "I'd be happy to help!", no "Absolutely!".
3811
+ - If the user is wrong, say so plainly.
3812
+
3813
+ MODE: ${currentMode.toUpperCase()}
3814
+ ${currentMode === "auto" ? "Full autonomy. Execute without confirmation." : ""}${currentMode === "plan" ? "READ-ONLY. Only use read/list/grep/glob. No writes. Output a plan for approval." : ""}${currentMode === "safe" ? "Reads auto-approved. Writes and commands require user confirmation." : ""}
3815
+
3816
+ DIRECTIVES:
3817
+ 1. ACT FIRST - Call tools in the same turn you mention them. Never end a turn with "let me check" and no tool call.
3818
+ 2. EXPLORE BEFORE EDITING - Read files before modifying. Use list/grep to understand structure.
3819
+ 3. ALWAYS RESPOND - After tool calls, summarize results in plain text. The user cannot see raw tool output.
3820
+ 4. MEMORY - When the user shares preferences or facts, store them with the memory tool. Check memory before saying "I don't know".
3821
+ 5. SECURITY - Never output secrets. Stay within workspaceRoot.
3822
+
3823
+ CWD: ${cfg.workspaceRoot}
3824
+ ${userContext ? `
3825
+ ${userContext}
3826
+ ` : ""}
3827
+ TOOLS:
3828
+ ${activeList}
3829
+ ${offlineList ? `
3830
+ Offline:
3831
+ ${offlineList}
3832
+ ` : ""}${registryList ? `
3833
+ Installable:
3834
+ ${registryList}
3835
+ ` : ""}
3836
+ CONTEXT: ${tokensUsed}/${tokenBudget} tokens (${(tokensRemaining / tokenBudget * 100).toFixed(0)}% free)${tokensUsed > tokenBudget * 0.7 ? " -- CONTEXT HIGH, be concise, suggest /clear if done" : ""}`,
3837
+ cache_control: { type: "ephemeral" }
3838
+ };
3839
+ if (this.lastConfig?.preCountTokens !== false) {
3840
+ const preRequestTokens = await this.countTokens(
3841
+ [systemPromptBlock],
3842
+ this.conversationHistory,
3843
+ toolDefinitionsForApi
3844
+ );
3845
+ if (preRequestTokens !== null) {
3846
+ const accurateUsage = preRequestTokens;
3847
+ if (accurateUsage > CONTEXT.MAX_TOKENS_TOTAL * CONTEXT.TOKEN_LIMIT_STOP) {
3848
+ bus.emitAgent({
3849
+ type: "error",
3850
+ message: `[Context] Message would use ${(accurateUsage / 1e3).toFixed(1)}k tokens (limit: ${(CONTEXT.MAX_TOKENS_TOTAL * CONTEXT.TOKEN_LIMIT_STOP / 1e3).toFixed(0)}k). Run /clear or auto-pruning.`
3851
+ });
3852
+ await this.compressHistory();
3853
+ return await this.streamChat(userMessage, options);
3854
+ }
3855
+ if (accurateUsage > CONTEXT.MAX_TOKENS_TOTAL * CONTEXT.TOKEN_LIMIT_PRUNE) {
3856
+ bus.emitAgent({
3857
+ type: "thought",
3858
+ content: `[Context] Pre-check: ${(accurateUsage / 1e3).toFixed(1)}k tokens (${(accurateUsage / CONTEXT.MAX_TOKENS_TOTAL * 100).toFixed(0)}%). Pruning before send.`,
3859
+ hidden: false
3860
+ });
3861
+ await this.compressHistory();
3862
+ } else if (accurateUsage > CONTEXT.MAX_TOKENS_TOTAL * CONTEXT.TOKEN_LIMIT_WARN) {
3863
+ bus.emitAgent({
3864
+ type: "thought",
3865
+ content: `[Context] Pre-check: ${(accurateUsage / 1e3).toFixed(1)}k tokens (${(accurateUsage / CONTEXT.MAX_TOKENS_TOTAL * 100).toFixed(0)}%). Consider /clear soon.`,
3866
+ hidden: true
3867
+ });
3868
+ }
3869
+ }
3870
+ }
3871
+ this.abortController = new AbortController();
3872
+ const signal = this.abortController.signal;
3873
+ if (this.currentInterruptHandler) {
3874
+ bus.off("user", this.currentInterruptHandler);
3875
+ }
3876
+ this.currentInterruptHandler = (e) => {
3877
+ if (e.type === "user_interrupt" && this.abortController) {
3878
+ this.abortController.abort();
3879
+ bus.emitAgent({ type: "thought", content: "[Stop] Interrupted by user." });
3880
+ }
3881
+ };
3882
+ bus.on("user", this.currentInterruptHandler);
3883
+ if (this.conversationHistory.length > 0 && this.conversationHistory.length % 5 === 0) {
3884
+ const lastMsg = this.conversationHistory[this.conversationHistory.length - 1];
3885
+ if (lastMsg.content) {
3886
+ if (lastMsg.role === "user" && typeof lastMsg.content === "string") {
3887
+ lastMsg.content = [
3888
+ { type: "text", text: lastMsg.content, cache_control: { type: "ephemeral" } }
3889
+ ];
3890
+ }
3891
+ }
3892
+ }
3893
+ const createMessage = async (model) => {
3894
+ const isOpus46 = model.startsWith("claude-opus-4-6");
3895
+ const baseParams = {
3896
+ model,
3897
+ max_tokens: this.lastConfig?.maxTokens || 8192,
3898
+ system: [systemPromptBlock],
3899
+ messages: [...this.conversationHistory],
3900
+ tools: toolDefinitionsForApi,
3901
+ stream: true
3902
+ };
3903
+ const isOpus = model.includes("opus");
3904
+ if (isOpus) {
3905
+ baseParams.thinking = {
3906
+ type: "enabled",
3907
+ budget_tokens: Math.floor((this.lastConfig?.maxTokens || 8192) / 2)
3908
+ };
3909
+ }
3910
+ if (usesBetaApi && this.computerUseState.enabled) {
3911
+ const { betaFlag } = getToolConfig(model);
3912
+ const computerUseSystemAddition = `
3913
+
3914
+ COMPUTER USE ACTIVE:
3915
+ - Screenshot: ~1429px wide (scaled from native ${this.computerUseState.displayWidth}x${this.computerUseState.displayHeight})
3916
+ - Coordinates auto-scaled to native
3917
+
3918
+ YOUTUBE SPECIFIC - USE KEYBOARD:
3919
+ After opening YouTube search results:
3920
+ 1. Press Tab 3-4 times to focus first video
3921
+ 2. Press Return to play
3922
+ This is MORE RELIABLE than clicking thumbnails.
3923
+
3924
+ IF YOU MUST CLICK:
3925
+ - State exact reasoning: "The video thumbnail is at approximately X% from left = [x], Y% from top = [y]"
3926
+ - YouTube sidebar is x:0-170, videos start at x:200+
3927
+ - First video thumbnail typically: x\u2248350, y\u2248350
3928
+
3929
+ EVALUATION: After each action, state "I see [what changed]. [Success/Retry]"`;
3930
+ const enhancedSystemBlock = {
3931
+ ...systemPromptBlock,
3932
+ text: systemPromptBlock.text + computerUseSystemAddition
3933
+ };
3934
+ return await this.client.beta.messages.create({
3935
+ ...baseParams,
3936
+ system: [enhancedSystemBlock],
3937
+ betas: [betaFlag],
3938
+ stream: true
3939
+ }, { signal });
3940
+ }
3941
+ return await this.client.messages.create(baseParams, { signal });
3942
+ };
3943
+ let stream;
3944
+ let currentModel = apiModel;
3945
+ let inputTokens = 0;
3946
+ let outputTokens = 0;
3947
+ let cacheReadTokens = 0;
3948
+ let cacheCreationTokens = 0;
3949
+ try {
3950
+ stream = await createMessage(apiModel);
3951
+ } catch (error) {
3952
+ const apiErr = error;
3953
+ const isNotFound = apiErr.status === 404 || apiErr.message && apiErr.message.includes("not_found_error") || apiErr.error && apiErr.error.type === "not_found_error";
3954
+ const isHistoryCorruption = apiErr.status === 400 && (apiErr.message?.includes("tool_use_id") || apiErr.message?.includes("tool_result") || apiErr.error?.message?.includes("tool_use_id") || apiErr.error?.message?.includes("tool_result"));
3955
+ if (isHistoryCorruption) {
3956
+ bus.emitAgent({
3957
+ type: "thought",
3958
+ content: "[Context] Detected corrupted history - clearing and retrying...",
3959
+ hidden: false
3960
+ });
3961
+ this.conversationHistory = [{
3962
+ role: "user",
3963
+ content: userMessage || "Continue from where we left off."
3964
+ }];
3965
+ stream = await createMessage(apiModel);
3966
+ } else if (isNotFound) {
3967
+ bus.emitAgent({
3968
+ type: "error",
3969
+ message: `Model ${apiModel} not available. Falling back to claude-haiku-4-5.`
3970
+ });
3971
+ currentModel = "claude-haiku-4-5-20251001";
3972
+ stream = await createMessage(currentModel);
3973
+ } else {
3974
+ throw error;
3975
+ }
3976
+ }
3977
+ let fullResponse = "";
3978
+ let fullThinking = "";
3979
+ let buffer = "";
3980
+ let thinkingBuffer = "";
3981
+ let toolUses = [];
3982
+ let currentToolUse = null;
3983
+ let currentThinking = null;
3984
+ for await (const chunk of stream) {
3985
+ if (chunk.type === "message_start" && chunk.message && chunk.message.usage) {
3986
+ inputTokens += chunk.message.usage.input_tokens || 0;
3987
+ outputTokens += chunk.message.usage.output_tokens || 0;
3988
+ if (chunk.message.usage.cache_read_input_tokens) {
3989
+ cacheReadTokens += chunk.message.usage.cache_read_input_tokens;
3990
+ this.accumulatedCacheReadTokens += chunk.message.usage.cache_read_input_tokens;
3991
+ }
3992
+ if (chunk.message.usage.cache_creation_input_tokens) {
3993
+ cacheCreationTokens += chunk.message.usage.cache_creation_input_tokens;
3994
+ this.accumulatedCacheCreationTokens += chunk.message.usage.cache_creation_input_tokens;
3995
+ }
3996
+ this.accumulatedInputTokens += inputTokens;
3997
+ this.accumulatedOutputTokens += outputTokens;
3998
+ }
3999
+ if (chunk.type === "message_delta" && chunk.usage) {
4000
+ outputTokens += chunk.usage.output_tokens || 0;
4001
+ this.accumulatedOutputTokens += chunk.usage.output_tokens || 0;
4002
+ }
4003
+ if (chunk.type === "content_block_start" && chunk.content_block.type === "thinking") {
4004
+ currentThinking = { type: "thinking" };
4005
+ }
4006
+ if (chunk.type === "content_block_delta" && chunk.delta.type === "thinking_delta") {
4007
+ const text = chunk.delta.thinking;
4008
+ fullThinking += text;
4009
+ thinkingBuffer += text;
4010
+ if (thinkingBuffer.length >= 100 || text.includes("\n")) {
4011
+ bus.emitAgent({
4012
+ type: "thought",
4013
+ content: `[Thinking] ${thinkingBuffer.trim()}`,
4014
+ hidden: false
4015
+ });
4016
+ thinkingBuffer = "";
4017
+ }
4018
+ }
4019
+ if (chunk.type === "content_block_stop" && currentThinking) {
4020
+ currentThinking = null;
4021
+ if (thinkingBuffer) {
4022
+ bus.emitAgent({
4023
+ type: "thought",
4024
+ content: `[Thinking] ${thinkingBuffer.trim()}`,
4025
+ hidden: false
4026
+ });
4027
+ thinkingBuffer = "";
4028
+ }
4029
+ }
4030
+ if (chunk.type === "content_block_start" && chunk.content_block.type === "tool_use") {
4031
+ currentToolUse = {
4032
+ id: chunk.content_block.id,
4033
+ name: chunk.content_block.name,
4034
+ input: ""
4035
+ };
4036
+ }
4037
+ if (chunk.type === "content_block_delta" && chunk.delta.type === "input_json_delta") {
4038
+ if (currentToolUse) {
4039
+ currentToolUse.input += chunk.delta.partial_json;
4040
+ }
4041
+ }
4042
+ if (chunk.type === "content_block_stop" && currentToolUse) {
4043
+ try {
4044
+ const rawInput = (typeof currentToolUse.input === "string" ? currentToolUse.input : "{}") || "{}";
4045
+ currentToolUse.input = JSON.parse(rawInput);
4046
+ toolUses.push(currentToolUse);
4047
+ } catch (e) {
4048
+ currentToolUse.input = {};
4049
+ toolUses.push(currentToolUse);
4050
+ }
4051
+ }
4052
+ if (chunk.type === "content_block_delta" && chunk.delta.type === "text_delta") {
4053
+ const text = chunk.delta.text;
4054
+ fullResponse += text;
4055
+ buffer += text;
4056
+ const shouldEmit = buffer.length >= 50 || buffer.match(/[.!?]\s*$/) || buffer.match(/\n/);
4057
+ if (shouldEmit) {
4058
+ bus.emitAgent({
4059
+ type: "thought",
4060
+ content: fullResponse
4061
+ });
4062
+ buffer = "";
4063
+ }
4064
+ }
4065
+ }
4066
+ if (buffer.length > 0) {
4067
+ bus.emitAgent({
4068
+ type: "thought",
4069
+ content: fullResponse
4070
+ });
4071
+ }
4072
+ if (toolUses.length > 0) {
4073
+ const toolResults = [];
4074
+ for (const toolUse of toolUses) {
4075
+ if (signal.aborted) break;
4076
+ let result;
4077
+ try {
4078
+ const args = typeof toolUse.input === "string" ? {} : toolUse.input;
4079
+ if (this.computerUseState.enabled && toolUse.name === "computer") {
4080
+ result = await tools.execute("computer", args);
4081
+ } else if (this.computerUseState.enabled && toolUse.name === "str_replace_based_edit_tool") {
4082
+ const cmd = args.command;
4083
+ if (cmd === "view") {
4084
+ result = await tools.execute("read", { path: args.path });
4085
+ } else if (cmd === "create") {
4086
+ result = await tools.execute("write", {
4087
+ path: args.path,
4088
+ content: args.file_text || ""
4089
+ });
4090
+ } else if (cmd === "str_replace") {
4091
+ result = await tools.execute("edit", {
4092
+ path: args.path,
4093
+ search: args.old_str,
4094
+ replace: args.new_str
4095
+ });
4096
+ } else {
4097
+ result = { success: false, error: `Unknown editor command: ${cmd}` };
4098
+ }
4099
+ } else if (this.computerUseState.enabled && toolUse.name === "bash") {
4100
+ result = await tools.execute("bash", { command: args.command });
4101
+ } else {
4102
+ result = await tools.execute(toolUse.name, args);
4103
+ }
4104
+ } catch (toolError) {
4105
+ bus.emitAgent({ type: "error", message: `[Tool] ${toolUse.name} execution failed: ${toolError instanceof Error ? toolError.message : String(toolError)}` });
4106
+ result = { success: false, error: `Tool execution failed: ${toolError instanceof Error ? toolError.message : String(toolError)}` };
4107
+ }
4108
+ let outputContent = result.success ? result.output || "Success" : result.error || "Failed";
4109
+ const redactionResult = redactor.redactToolOutput(toolUse.name, outputContent);
4110
+ if (redactionResult.redactionCount > 0) {
4111
+ outputContent = redactionResult.text;
4112
+ bus.emitAgent({
4113
+ type: "thought",
4114
+ content: `[Security] Redacted ${redactionResult.redactionCount} sensitive item(s)`,
4115
+ hidden: true
4116
+ });
4117
+ }
4118
+ if (outputContent.length > 2e4) {
4119
+ outputContent = outputContent.slice(0, 5e3) + `
4120
+ ... [${outputContent.length - 1e4} chars truncated] ...
4121
+ ` + outputContent.slice(-5e3);
4122
+ }
4123
+ let toolResultContent = outputContent;
4124
+ if (result.content && Array.isArray(result.content)) {
4125
+ toolResultContent = result.content.map((block) => {
4126
+ if (block.type === "image") {
4127
+ return {
4128
+ type: "image",
4129
+ source: {
4130
+ type: "base64",
4131
+ media_type: block.mimeType || "image/png",
4132
+ data: block.data
4133
+ }
4134
+ };
4135
+ }
4136
+ if (block.type === "text") {
4137
+ return {
4138
+ type: "text",
4139
+ text: block.text
4140
+ };
4141
+ }
4142
+ return block;
4143
+ });
4144
+ }
4145
+ toolResults.push({
4146
+ type: "tool_result",
4147
+ tool_use_id: toolUse.id,
4148
+ content: toolResultContent,
4149
+ is_error: !result.success
4150
+ });
4151
+ if (result.success && outputContent && !result.content) {
4152
+ bus.emitAgent({
4153
+ type: "thought",
4154
+ content: outputContent
4155
+ });
4156
+ this.lastToolOutputs.push(outputContent);
4157
+ }
4158
+ }
4159
+ this.conversationHistory.push({
4160
+ role: "assistant",
4161
+ content: [
4162
+ ...fullThinking ? [{ type: "thinking", thinking: fullThinking }] : [],
4163
+ ...fullResponse ? [{ type: "text", text: fullResponse }] : [],
4164
+ ...toolUses.map((tu) => ({
4165
+ type: "tool_use",
4166
+ id: tu.id,
4167
+ name: tu.name,
4168
+ input: tu.input
4169
+ }))
4170
+ ]
4171
+ });
4172
+ const postToolUsage = usage.getContextUsage(currentModel);
4173
+ const postToolPercent = postToolUsage.used / CONTEXT.MAX_TOKENS_TOTAL * 100;
4174
+ let systemWarning = null;
4175
+ if (postToolPercent > 70) {
4176
+ systemWarning = {
4177
+ type: "text",
4178
+ text: `<system_warning>Token usage: ${postToolUsage.used}/${CONTEXT.MAX_TOKENS_TOTAL}; ${postToolUsage.remaining} remaining</system_warning>`
4179
+ };
4180
+ }
4181
+ if (this.computerUseState.enabled) {
4182
+ this.pruneImagesFromHistory(1);
4183
+ }
4184
+ this.conversationHistory.push({
4185
+ role: "user",
4186
+ content: systemWarning ? [...toolResults, systemWarning] : toolResults
4187
+ });
4188
+ const recursiveResponse = await this.streamChat("");
4189
+ if (recursiveResponse && recursiveResponse.trim()) {
4190
+ return recursiveResponse;
4191
+ }
4192
+ if (toolResults.length > 0) {
4193
+ const lastToolResult = toolResults[toolResults.length - 1];
4194
+ let content = lastToolResult.content;
4195
+ if (Array.isArray(content)) {
4196
+ content = content.filter((b) => b.type === "text").map((b) => b.text).join("\n");
4197
+ }
4198
+ if (typeof content === "string" && content.trim()) {
4199
+ bus.emitAgent({
4200
+ type: "thought",
4201
+ content
4202
+ });
4203
+ return content;
4204
+ }
4205
+ }
4206
+ return recursiveResponse || "";
4207
+ }
4208
+ if (fullResponse || fullThinking) {
4209
+ this.conversationHistory.push({
4210
+ role: "assistant",
4211
+ content: [
4212
+ ...fullThinking ? [{ type: "thinking", thinking: fullThinking }] : [],
4213
+ ...fullResponse ? [{ type: "text", text: fullResponse }] : []
4214
+ ]
4215
+ });
4216
+ }
4217
+ const currentContextSize = inputTokens + cacheReadTokens;
4218
+ await usage.track(
4219
+ currentModel,
4220
+ this.accumulatedInputTokens,
4221
+ this.accumulatedOutputTokens,
4222
+ this.accumulatedCacheReadTokens,
4223
+ this.accumulatedCacheCreationTokens,
4224
+ currentContextSize
4225
+ );
4226
+ await this.persistHistory();
4227
+ return fullResponse;
4228
+ } catch (error) {
4229
+ const apiErr = error;
4230
+ if (apiErr.name === "AbortError" || apiErr.type === "aborted") {
4231
+ return null;
4232
+ }
4233
+ const errorDetails = apiErr.status ? `[${apiErr.status}] ${apiErr.message}` : apiErr.message || String(error);
4234
+ bus.emitAgent({
4235
+ type: "error",
4236
+ message: `LLM Error: ${errorDetails}`
4237
+ });
4238
+ if (process.env.DEBUG) console.error("[LLM] API Error:", error);
4239
+ return null;
4240
+ } finally {
4241
+ this.abortController = null;
4242
+ if (this.currentInterruptHandler) {
4243
+ bus.off("user", this.currentInterruptHandler);
4244
+ this.currentInterruptHandler = null;
4245
+ }
4246
+ }
4247
+ }
4248
+ /**
4249
+ * History Pruning (Context Editing)
4250
+ * Keep recent 30 messages + System Prompt (handled separate)
4251
+ * Limit history to ~150k tokens (heuristic)
4252
+ */
4253
+ /**
4254
+ * Smart Context Management
4255
+ * Uses summarization to compress older history instead of deleting it.
4256
+ */
4257
+ async compressHistory() {
4258
+ if (this.conversationHistory.length > CONTEXT.MAX_MESSAGES) {
4259
+ if (this.conversationHistory.length < CONTEXT.MAX_MESSAGES + CONTEXT.BUFFER) return;
4260
+ const summarizeStart = CONTEXT.KEEP_FIRST;
4261
+ const summarizeEnd = this.conversationHistory.length - CONTEXT.KEEP_LAST;
4262
+ const messagesToSummarize = this.conversationHistory.slice(summarizeStart, summarizeEnd);
4263
+ if (messagesToSummarize.length < 5) return;
4264
+ bus.emitAgent({
4265
+ type: "thought",
4266
+ content: `[Context] Compressing ${messagesToSummarize.length} messages using ${this.lastConfig?.summarizerModel || "Haiku"}...`,
4267
+ hidden: true
4268
+ });
4269
+ try {
4270
+ const summary = await this.summarizeBlock(messagesToSummarize);
4271
+ try {
4272
+ const { memory } = await import("./memory-QZTBTYPH.js");
4273
+ await memory.store("daily_summary", `context_compression_${Date.now()}`, summary);
4274
+ } catch (memError) {
4275
+ }
4276
+ const keptStart = this.conversationHistory.slice(0, CONTEXT.KEEP_FIRST);
4277
+ const keptEnd = this.conversationHistory.slice(summarizeEnd);
4278
+ this.conversationHistory = [
4279
+ ...keptStart,
4280
+ {
4281
+ role: "user",
4282
+ content: `[System: Context compressed. Previous conversation summary below.]
4283
+ <conversation_summary>
4284
+ ${summary}
4285
+ </conversation_summary>`
4286
+ },
4287
+ ...keptEnd
4288
+ ];
4289
+ bus.emitAgent({
4290
+ type: "thought",
4291
+ content: `[Context] Successfully compressed history.`,
4292
+ hidden: true
4293
+ });
4294
+ } catch (error) {
4295
+ bus.emitAgent({
4296
+ type: "error",
4297
+ message: `[Context] Summarization failed: ${error}. Falling back to standard pruning.`
4298
+ });
4299
+ this.pruneHistoryFallback();
4300
+ }
4301
+ }
4302
+ }
4303
+ async summarizeBlock(messages) {
4304
+ if (!this.client) return "Summary unavailable.";
4305
+ const summarizerModel = this.lastConfig?.summarizerModel || "claude-haiku-4-5-20251001";
4306
+ const simplifiedMessages = messages.map((m) => {
4307
+ if (Array.isArray(m.content)) {
4308
+ const textContent = m.content.map((b) => {
4309
+ if (b.type === "text") return b.text;
4310
+ if (b.type === "tool_use") return `[Tool Use: ${b.name}]`;
4311
+ if (b.type === "tool_result") return `[Tool Result: ${typeof b.content === "string" ? b.content.slice(0, 500) + "..." : "Data"}]`;
4312
+ return "";
4313
+ }).join("\n");
4314
+ return { role: m.role, content: textContent };
4315
+ }
4316
+ return m;
4317
+ });
4318
+ const prompt = `Please summarize the following conversation segment. Focus on:
4319
+ 1. Key user requests and intents.
4320
+ 2. Important actions taken by the agent (tools used).
4321
+ 3. Key occurrences of errors or successes.
4322
+ 4. Any critical data/context that might be needed later.
4323
+ Be concise but comprehensive.
4324
+
4325
+ CONVERSATION SEGMENT:
4326
+ ${JSON.stringify(simplifiedMessages, null, 2)}
4327
+ `;
4328
+ const response = await this.client.messages.create({
4329
+ model: summarizerModel,
4330
+ max_tokens: 1024,
4331
+ messages: [{ role: "user", content: prompt }]
4332
+ });
4333
+ if (response.content[0].type === "text") {
4334
+ return response.content[0].text;
4335
+ }
4336
+ return "Summary generation returned non-text content.";
4337
+ }
4338
+ pruneHistoryFallback() {
4339
+ if (this.conversationHistory.length > CONTEXT.MAX_MESSAGES) {
4340
+ const keepFirst = CONTEXT.KEEP_FIRST;
4341
+ const keepLast = 20;
4342
+ const removalCount = this.conversationHistory.length - (keepFirst + keepLast);
4343
+ if (removalCount > 0) {
4344
+ const keptStart = this.conversationHistory.slice(0, keepFirst);
4345
+ const keptEnd = this.conversationHistory.slice(-keepLast);
4346
+ this.conversationHistory = [
4347
+ ...keptStart,
4348
+ { role: "user", content: `[... History Pruned: ${removalCount} intermediate messages were removed to save context ...]` },
4349
+ ...keptEnd
4350
+ ];
4351
+ }
4352
+ }
4353
+ }
4354
+ clearHistory() {
4355
+ this.conversationHistory = [];
4356
+ }
4357
+ /**
4358
+ * Get all tool outputs from the current turn (for fallback when LLM generates no text).
4359
+ * Returns concatenated output or null if no tools were used.
4360
+ */
4361
+ getLastToolOutput() {
4362
+ if (this.lastToolOutputs.length === 0) return null;
4363
+ const output = this.lastToolOutputs.join("\n\n");
4364
+ return output;
4365
+ }
4366
+ /**
4367
+ * Get a snapshot of the current conversation history for persistence
4368
+ */
4369
+ getHistorySnapshot() {
4370
+ return [...this.conversationHistory];
4371
+ }
4372
+ /**
4373
+ * Persist current history to DB
4374
+ */
4375
+ async persistHistory() {
4376
+ const { context: context2 } = await import("./context-NYOIRZKV.js");
4377
+ const { db: db2 } = await import("./database-M457QD3O.js");
4378
+ const sessionId = context2.get().session_id;
4379
+ if (!sessionId) return;
4380
+ try {
4381
+ db2.getDb().prepare("UPDATE sessions SET llm_history = ? WHERE id = ?").run(JSON.stringify(this.conversationHistory), sessionId);
4382
+ } catch (e) {
4383
+ bus.emitAgent({ type: "thought", content: `[Context] Failed to persist LLM history: ${e instanceof Error ? e.message : String(e)}`, hidden: true });
4384
+ }
4385
+ }
4386
+ /**
4387
+ * Restore conversation history from a saved session
4388
+ * Validates history to prevent API errors from orphaned tool_use blocks
4389
+ */
4390
+ restoreHistory(history2) {
4391
+ if (!Array.isArray(history2) || history2.length === 0) {
4392
+ return;
4393
+ }
4394
+ const validatedHistory = this.validateAndFixHistory(history2);
4395
+ if (!this.verifyHistoryIntegrity(validatedHistory)) {
4396
+ bus.emitAgent({
4397
+ type: "thought",
4398
+ content: `[Context] History validation failed - starting fresh to avoid API errors.`,
4399
+ hidden: false
4400
+ });
4401
+ this.conversationHistory = [];
4402
+ return;
4403
+ }
4404
+ this.conversationHistory = validatedHistory;
4405
+ bus.emitAgent({
4406
+ type: "thought",
4407
+ content: `[Context] Restored ${validatedHistory.length} messages from saved session.`,
4408
+ hidden: true
4409
+ });
4410
+ }
4411
+ /**
4412
+ * Verify history integrity - check that all tool_results have matching tool_uses
4413
+ */
4414
+ verifyHistoryIntegrity(history2) {
4415
+ for (let i = 0; i < history2.length; i++) {
4416
+ const msg = history2[i];
4417
+ const prevMsg = history2[i - 1];
4418
+ if (msg.role === "user" && Array.isArray(msg.content)) {
4419
+ const toolResults = msg.content.filter((b) => b.type === "tool_result");
4420
+ if (toolResults.length > 0) {
4421
+ if (!prevMsg || prevMsg.role !== "assistant" || !Array.isArray(prevMsg.content)) {
4422
+ return false;
4423
+ }
4424
+ const toolUseIds = new Set(
4425
+ prevMsg.content.filter((b) => b.type === "tool_use").map((b) => b.id)
4426
+ );
4427
+ for (const tr of toolResults) {
4428
+ if (!toolUseIds.has(tr.tool_use_id)) {
4429
+ return false;
4430
+ }
4431
+ }
4432
+ }
4433
+ }
4434
+ }
4435
+ return true;
4436
+ }
4437
+ /**
4438
+ * Validate conversation history and remove orphaned tool blocks
4439
+ * - Each tool_use must have a corresponding tool_result in the next message
4440
+ * - Each tool_result must have a corresponding tool_use in the previous message
4441
+ */
4442
+ validateAndFixHistory(history2) {
4443
+ const validated = [];
4444
+ for (let i = 0; i < history2.length; i++) {
4445
+ const msg = history2[i];
4446
+ const nextMsg = history2[i + 1];
4447
+ const prevMsg = validated[validated.length - 1];
4448
+ if (msg.role === "user" && Array.isArray(msg.content)) {
4449
+ const toolResultBlocks = msg.content.filter((b) => b.type === "tool_result");
4450
+ if (toolResultBlocks.length > 0) {
4451
+ if (!prevMsg || prevMsg.role !== "assistant" || !Array.isArray(prevMsg.content)) {
4452
+ const nonToolBlocks2 = msg.content.filter((b) => b.type !== "tool_result");
4453
+ if (nonToolBlocks2.length > 0) {
4454
+ validated.push({
4455
+ role: "user",
4456
+ content: nonToolBlocks2
4457
+ });
4458
+ }
4459
+ continue;
4460
+ }
4461
+ const toolUseIds = new Set(
4462
+ prevMsg.content.filter((b) => b.type === "tool_use").map((b) => b.id)
4463
+ );
4464
+ const validToolResults = toolResultBlocks.filter((tr) => toolUseIds.has(tr.tool_use_id));
4465
+ const nonToolBlocks = msg.content.filter((b) => b.type !== "tool_result");
4466
+ if (validToolResults.length !== toolResultBlocks.length) {
4467
+ if (validToolResults.length === 0 && nonToolBlocks.length > 0) {
4468
+ validated.push({
4469
+ role: "user",
4470
+ content: nonToolBlocks
4471
+ });
4472
+ continue;
4473
+ } else if (validToolResults.length > 0) {
4474
+ validated.push({
4475
+ role: "user",
4476
+ content: [...nonToolBlocks, ...validToolResults]
4477
+ });
4478
+ continue;
4479
+ }
4480
+ continue;
4481
+ }
4482
+ }
4483
+ }
4484
+ if (msg.role === "assistant" && Array.isArray(msg.content)) {
4485
+ const toolUseBlocks = msg.content.filter((b) => b.type === "tool_use");
4486
+ if (toolUseBlocks.length > 0) {
4487
+ if (!nextMsg || nextMsg.role !== "user" || !Array.isArray(nextMsg.content)) {
4488
+ const textBlocks = msg.content.filter((b) => b.type === "text");
4489
+ if (textBlocks.length > 0) {
4490
+ validated.push({
4491
+ role: "assistant",
4492
+ content: textBlocks.map((b) => b.text).join("\n")
4493
+ });
4494
+ }
4495
+ continue;
4496
+ }
4497
+ const toolResultIds = new Set(
4498
+ nextMsg.content.filter((b) => b.type === "tool_result").map((b) => b.tool_use_id)
4499
+ );
4500
+ const validToolUses = toolUseBlocks.filter((tu) => toolResultIds.has(tu.id));
4501
+ if (validToolUses.length !== toolUseBlocks.length) {
4502
+ const textBlocks = msg.content.filter((b) => b.type === "text");
4503
+ if (validToolUses.length === 0 && textBlocks.length > 0) {
4504
+ validated.push({
4505
+ role: "assistant",
4506
+ content: textBlocks.map((b) => b.text).join("\n")
4507
+ });
4508
+ continue;
4509
+ } else if (validToolUses.length > 0) {
4510
+ validated.push({
4511
+ role: "assistant",
4512
+ content: [...textBlocks, ...validToolUses]
4513
+ });
4514
+ continue;
4515
+ }
4516
+ continue;
4517
+ }
4518
+ }
4519
+ }
4520
+ validated.push(msg);
4521
+ }
4522
+ return validated;
4523
+ }
4524
+ };
4525
+ var llm = new LLMClient();
4526
+
4527
+ // src/core/session.ts
4528
+ var SessionManager = class {
4529
+ startTime = Date.now();
4530
+ /**
4531
+ * Save current session state
4532
+ * (V13: Mostly a no-op as state is continuously saved to SQLite)
4533
+ */
4534
+ async save() {
4535
+ const ctx = context.get();
4536
+ await context.save();
4537
+ return { sessionId: ctx.session_id, path: "sqlite" };
4538
+ }
4539
+ /**
4540
+ * List all saved sessions
4541
+ */
4542
+ async list() {
4543
+ try {
4544
+ const cfg = await config.load();
4545
+ const rows = db.getDb().prepare(`
4546
+ SELECT id, created_at, workspace
4547
+ FROM sessions
4548
+ ORDER BY created_at DESC
4549
+ `).all();
4550
+ const sessions = [];
4551
+ for (const row of rows) {
4552
+ const taskRow = db.getDb().prepare(`
4553
+ SELECT title FROM tasks
4554
+ WHERE session_id = ?
4555
+ ORDER BY created_at DESC LIMIT 1
4556
+ `).get(row.id);
4557
+ const modCount = db.getDb().prepare(`
4558
+ SELECT COUNT(*) as count FROM working_set WHERE session_id = ?
4559
+ `).get(row.id);
4560
+ sessions.push({
4561
+ id: row.id,
4562
+ savedAt: new Date(row.created_at).toISOString(),
4563
+ task: taskRow ? taskRow.title : null,
4564
+ filesModified: modCount ? modCount.count : 0,
4565
+ workspace: row.workspace || cfg.workspaceRoot
4566
+ });
4567
+ }
4568
+ return sessions;
4569
+ } catch {
4570
+ return [];
4571
+ }
4572
+ }
4573
+ /**
4574
+ * Delete a saved session and all related data
4575
+ */
4576
+ async delete(sessionId) {
4577
+ try {
4578
+ const transaction = db.getDb().transaction(() => {
4579
+ db.getDb().prepare("DELETE FROM subtasks WHERE task_id IN (SELECT id FROM tasks WHERE session_id = ?)").run(sessionId);
4580
+ db.getDb().prepare("DELETE FROM tasks WHERE session_id = ?").run(sessionId);
4581
+ db.getDb().prepare("DELETE FROM working_set WHERE session_id = ?").run(sessionId);
4582
+ db.getDb().prepare("DELETE FROM usage_stats WHERE session_id = ?").run(sessionId);
4583
+ db.getDb().prepare("DELETE FROM events WHERE session_id = ?").run(sessionId);
4584
+ db.getDb().prepare("DELETE FROM sessions WHERE id = ?").run(sessionId);
4585
+ });
4586
+ transaction();
4587
+ return true;
4588
+ } catch (e) {
4589
+ console.error("Failed to delete session:", e);
4590
+ return false;
4591
+ }
4592
+ }
4593
+ /**
4594
+ * Restore a saved session
4595
+ */
4596
+ async restore(sessionId) {
4597
+ const sessionRow = db.getDb().prepare("SELECT * FROM sessions WHERE id = ?").get(sessionId);
4598
+ if (!sessionRow) {
4599
+ return { success: false, error: `Session ${sessionId} not found` };
4600
+ }
4601
+ await context.load(sessionId);
4602
+ const { bus: bus2 } = await import("./bus-N5GVOUS7.js");
4603
+ const events = await history.load();
4604
+ const filteredEvents = events.filter(
4605
+ (e) => e.type !== "approval_request" && e.type !== "choice_request"
4606
+ );
4607
+ bus2.emitAgent({ type: "restore_history", events: filteredEvents });
4608
+ await tasks.init();
4609
+ const stats = db.getDb().prepare(`
4610
+ SELECT
4611
+ SUM(cost) as sessionCost,
4612
+ SUM(input_tokens) as sessionInputTokens,
4613
+ SUM(output_tokens) as sessionOutputTokens,
4614
+ MIN(timestamp) as firstLog,
4615
+ MAX(timestamp) as lastLog
4616
+ FROM usage_stats
4617
+ WHERE session_id = ?
4618
+ `).get(sessionId);
4619
+ const duration = stats?.lastLog && stats?.firstLog ? stats.lastLog - stats.firstLog : 0;
4620
+ usage.restoreSessionState({
4621
+ cost: stats?.sessionCost ?? 0,
4622
+ inputTokens: stats?.sessionInputTokens ?? 0,
4623
+ outputTokens: stats?.sessionOutputTokens ?? 0,
4624
+ cacheReadTokens: 0,
4625
+ cacheCreationTokens: 0,
4626
+ duration
4627
+ });
4628
+ this.resetStartTime();
4629
+ this.startTime = Date.now() - duration;
4630
+ const row = sessionRow;
4631
+ if (row.llm_history) {
4632
+ try {
4633
+ const restoredHistory = JSON.parse(row.llm_history);
4634
+ llm.restoreHistory(restoredHistory);
4635
+ } catch (e) {
4636
+ console.error("Failed to parse LLM history:", e);
4637
+ }
4638
+ }
4639
+ return { success: true };
4640
+ }
4641
+ /**
4642
+ * Generate session summary for shutdown
4643
+ */
4644
+ async getSummary() {
4645
+ const ctx = context.get();
4646
+ const task = tasks.get();
4647
+ let tasksCompleted = 0;
4648
+ let tasksPending = 0;
4649
+ if (task) {
4650
+ for (const subtask of task.subtasks) {
4651
+ if (subtask.done) tasksCompleted++;
4652
+ else tasksPending++;
4653
+ }
4654
+ }
4655
+ return {
4656
+ sessionId: ctx.session_id,
4657
+ duration: Date.now() - this.startTime,
4658
+ filesRead: ctx.files_read.length,
4659
+ filesModified: ctx.files_modified.length,
4660
+ tasksCompleted,
4661
+ tasksPending,
4662
+ totalCost: usage.getSessionCost()
4663
+ };
4664
+ }
4665
+ resetStartTime() {
4666
+ this.startTime = Date.now();
4667
+ }
4668
+ getDuration() {
4669
+ return Date.now() - this.startTime;
4670
+ }
4671
+ formatDuration(ms) {
4672
+ const seconds = Math.floor(ms / 1e3);
4673
+ const minutes = Math.floor(seconds / 60);
4674
+ const hours = Math.floor(minutes / 60);
4675
+ if (hours > 0) return `${hours}h ${minutes % 60}m`;
4676
+ else if (minutes > 0) return `${minutes}m ${seconds % 60}s`;
4677
+ else return `${seconds}s`;
4678
+ }
4679
+ };
4680
+ var session = new SessionManager();
4681
+
4682
+ export {
4683
+ usage,
4684
+ auditor,
4685
+ sandbox,
4686
+ tasks,
4687
+ undo,
4688
+ diffManager,
4689
+ mcp,
4690
+ listRegistry,
4691
+ tools,
4692
+ history,
4693
+ llm,
4694
+ session
4695
+ };